From 36ec66d997e91e3afcbd4a3c7b999370b24254a0 Mon Sep 17 00:00:00 2001 From: Todd Gamblin Date: Sun, 13 Oct 2019 21:05:35 -0700 Subject: concretizer: use clingo json output instead of text Clingo actually has an option to output JSON -- use that instead of parsing the raw otuput ourselves. This also allows us to pick the best answer -- modify the parser to *only* construct a spec for that one rather than building all of them like we did before. --- lib/spack/spack/cmd/solve.py | 31 +++++++---- lib/spack/spack/solver/asp.py | 123 ++++++++++++++++++++++-------------------- 2 files changed, 85 insertions(+), 69 deletions(-) (limited to 'lib') diff --git a/lib/spack/spack/cmd/solve.py b/lib/spack/spack/cmd/solve.py index 7fea75233d..9b10a27cc7 100644 --- a/lib/spack/spack/cmd/solve.py +++ b/lib/spack/spack/cmd/solve.py @@ -22,7 +22,7 @@ section = 'developer' level = 'long' #: output options -dump_options = ('asp', 'warnings', 'output', 'solutions') +dump_options = ('asp', 'output', 'solutions') def setup_parser(subparser): @@ -87,6 +87,8 @@ def solve(parser, args): # dump generated ASP program result = asp.solve(specs, dump=dump, models=models) + if dump == ['asp']: + return # die if no solution was found # TODO: we need to be able to provide better error messages than this @@ -95,13 +97,20 @@ def solve(parser, args): # dump the solutions as concretized specs if 'solutions' in dump: - for i, answer in enumerate(result.answers): - tty.msg("Answer %d" % (i + 1)) - for spec in specs: - answer_spec = answer[spec.name] - if args.yaml: - out = answer_spec.to_yaml() - else: - out = answer_spec.tree( - color=sys.stdout.isatty(), **kwargs) - sys.stdout.write(out) + best = min(result.answers) + assert best[1] == result.answers[-1][1] + + opt, i, answer = best + tty.msg( + "%d Answers. Best optimization %s:" % (i + 1, opt) + ) + + # iterate over roots from command line + for spec in specs: + answer_spec = answer[spec.name] + if args.yaml: + out = answer_spec.to_yaml() + else: + out = answer_spec.tree( + color=sys.stdout.isatty(), **kwargs) + sys.stdout.write(out) diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py index f23fe351cd..4033362e74 100644 --- a/lib/spack/spack/solver/asp.py +++ b/lib/spack/spack/solver/asp.py @@ -6,6 +6,7 @@ from __future__ import print_function import collections +import json import pkgutil import re import sys @@ -503,64 +504,60 @@ class ResultParser(object): self._specs[pkg]._add_dependency( self._specs[dep], ('link', 'run')) - def parse(self, stream, result): - for line in stream: - match = re.match(r'SATISFIABLE', line) - if match: - result.satisfiable = True + def call_actions_for_functions(self, function_strings): + function_re = re.compile(r'(\w+)\(([^)]*)\)') + + # parse functions out of ASP output + functions = [] + for string in function_strings: + m = function_re.match(string) + name, arg_string = m.groups() + args = re.split(r'\s*,\s*', arg_string) + args = [s.strip('"') if s.startswith('"') else int(s) + for s in args] + functions.append((name, args)) + + # Functions don't seem to be in particular order in output. + # Sort them here so that nodes are first, and so created + # before directives that need them (depends_on(), etc.) + functions.sort(key=lambda f: f[0] != "node") + + self._specs = {} + for name, args in functions: + action = getattr(self, name, None) + if not action: + print("%s(%s)" % (name, ", ".join(str(a) for a in args))) continue + assert action and callable(action) + action(*args) - match = re.match(r'OPTIMUM FOUND', line) - if match: - result.satisfiable = True - continue - - match = re.match(r'UNSATISFIABLE', line) - if match: - result.satisfiable = False - continue - - match = re.match(r'Answer: (\d+)', line) - if not match: - continue - - answer_number = int(match.group(1)) - assert answer_number == len(result.answers) + 1 - - answer = next(stream) - tty.debug("Answer: %d" % answer_number, answer) - - # parse functions out of ASP output - functions = [] - for m in re.finditer(r'(\w+)\(([^)]*)\)', answer): - name, arg_string = m.groups() - args = re.split(r'\s*,\s*', arg_string) - args = [s.strip('"') if s.startswith('"') else int(s) - for s in args] - functions.append((name, args)) + def parse_json(self, data, result): + if data["Result"] == "UNSATISFIABLE": + result.satisfiable = False + return - # Functions don't seem to be in particular order in output. - # Sort them here so that nodes are first, and so created - # before directives that need them (depends_on(), etc.) - functions.sort(key=lambda f: f[0] != "node") + result.satisfiable = True + if data["Result"] == "OPTIMUM FOUND": + result.optimal = True - self._specs = {} - for name, args in functions: - action = getattr(self, name, None) - if not action: - print("%s(%s)" % (name, ", ".join(str(a) for a in args))) - continue - assert action and callable(action) - action(*args) + nmodels = data["Models"]["Number"] + best_model_number = nmodels - 1 + best_model = data["Call"][0]["Witnesses"][best_model_number] + opt = list(best_model["Costs"]) - result.answers.append(self._specs) + functions = best_model["Value"] + self.call_actions_for_functions(functions) + result.answers.append((opt, best_model_number, self._specs)) class Result(object): def __init__(self, asp): self.asp = asp self.satisfiable = None + self.optimal = None self.warnings = None + + # specs ordered by optimization level self.answers = [] @@ -625,12 +622,18 @@ def solve(specs, dump=None, models=1): if 'asp' in dump: if sys.stdout.isatty(): tty.msg('ASP program:') - colorize(result.asp) + + if dump == ['asp']: + print(result.asp) + return + else: + colorize(result.asp) with tempfile.TemporaryFile("w+") as output: with tempfile.TemporaryFile() as warnings: clingo( - '--models=%d' % models, + '--models=0', + '--outf=2', input=program, output=output, error=warnings, @@ -640,25 +643,29 @@ def solve(specs, dump=None, models=1): result.warnings = warnings.read().decode("utf-8") # dump any warnings generated by the solver - if 'warnings' in dump: - if result.warnings: - if sys.stdout.isatty(): - tty.msg('Clingo gave the following warnings:') - colorize(result.warnings) - else: - if sys.stdout.isatty(): - tty.msg('No warnings.') + if result.warnings: + if sys.stdout.isatty(): + tty.msg('Clingo gave the following warnings:') + colorize(result.warnings) + else: + if sys.stdout.isatty(): + tty.msg('No warnings.') output.seek(0) result.output = output.read() + data = json.loads(result.output) # dump the raw output of the solver if 'output' in dump: if sys.stdout.isatty(): tty.msg('Clingo output:') - colorize(result.output) + + print(result.output) + + if 'solutions' not in dump: + return output.seek(0) - parser.parse(output, result) + parser.parse_json(data, result) return result -- cgit v1.2.3-60-g2f50