LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
llzk-smt-check.cpp
Go to the documentation of this file.
1//===-- llzk-smt-check.cpp - SMT-LIB staged checker ------------*- C++ -*-===//
2//
3// Part of the LLZK Project, under the Apache License v2.0.
4// See LICENSE.txt for license information.
5// Copyright 2026 Project LLZK
6// SPDX-License-Identifier: Apache-2.0
7//
8//===----------------------------------------------------------------------===//
9
10#include "tools/config.h"
11
12#include <llvm/ADT/ArrayRef.h>
13#include <llvm/ADT/SmallString.h>
14#include <llvm/ADT/SmallVector.h>
15#include <llvm/ADT/StringRef.h>
16#include <llvm/Support/CommandLine.h>
17#include <llvm/Support/ErrorOr.h>
18#include <llvm/Support/FileSystem.h>
19#include <llvm/Support/MemoryBuffer.h>
20#include <llvm/Support/Path.h>
21#include <llvm/Support/PrettyStackTrace.h>
22#include <llvm/Support/Program.h>
23#include <llvm/Support/Signals.h>
24#include <llvm/Support/raw_ostream.h>
25
26#include <array>
27#include <cstdlib>
28#include <optional>
29#include <string>
30#include <system_error>
31#include <utility>
32#include <vector>
33
34using namespace llvm;
35
36namespace {
37
38enum class SatResult { Sat, Unsat, Unknown };
39
40struct StageExpectation {
41 std::string rootName;
42 std::string stageName;
43 SatResult expected;
44 bool hasExpected = false;
45};
46
47struct ScriptMetadata {
48 SmallVector<StageExpectation> stages;
49 size_t checkSatCount = 0;
50};
51
52struct SolverInvocationResult {
53 int exitCode = 0;
54 bool executionFailed = false;
55 std::string errorMessage;
56 std::string stdoutText;
57 std::string stderrText;
58};
59
60struct TempFileCleanup {
61 SmallVector<SmallString<128>> paths;
62
63 ~TempFileCleanup() {
64 for (const SmallString<128> &path : paths) {
65 (void)sys::fs::remove(path);
66 }
67 }
68};
69
70static cl::opt<std::string> InputFilename(cl::Positional, cl::Required);
71static cl::opt<std::string>
72 SolverBinary("solver-binary", cl::desc("SMT solver executable"), cl::init("z3"));
73static cl::opt<bool> Quiet("quiet", cl::desc("Suppress per-stage summaries"));
74static cl::opt<bool>
75 DumpRawOutput("dump-raw-output", cl::desc("Print raw solver stdout after the stage summaries"));
76
77StringRef stringify(SatResult result) {
78 switch (result) {
79 case SatResult::Sat:
80 return "sat";
81 case SatResult::Unsat:
82 return "unsat";
83 case SatResult::Unknown:
84 return "unknown";
85 }
86 llvm_unreachable("unknown sat result");
87}
88
89std::optional<SatResult> parseSatResult(StringRef text) {
90 if (text == "sat") {
91 return SatResult::Sat;
92 }
93 if (text == "unsat") {
94 return SatResult::Unsat;
95 }
96 if (text == "unknown") {
97 return SatResult::Unknown;
98 }
99 return std::nullopt;
100}
101
102Expected<std::string> readInput(StringRef inputFilename) {
103 ErrorOr<std::unique_ptr<MemoryBuffer>> buffer = MemoryBuffer::getFileOrSTDIN(inputFilename);
104 if (!buffer) {
105 return createStringError(buffer.getError(), "failed to read input '%s'", inputFilename.data());
106 }
107 return std::string(buffer.get()->getBuffer());
108}
109
110Expected<ScriptMetadata> scanScript(StringRef script) {
111 ScriptMetadata metadata;
112 std::optional<std::string> currentRoot;
113 std::optional<std::string> pendingInfoStage;
114 std::optional<SatResult> pendingInfoExpected;
115
116 auto parseQuotedString = [&](StringRef value, StringRef fullLine) -> Expected<std::string> {
117 StringRef trimmed = value.trim();
118 if (trimmed.size() < 2 || !trimmed.starts_with("\"") || !trimmed.ends_with("\"")) {
119 return createStringError(
120 inconvertibleErrorCode(), "invalid set-info annotation: '%s'", fullLine.str().c_str()
121 );
122 }
123 return trimmed.drop_front().drop_back().str();
124 };
125
126 SmallVector<StringRef> lines;
127 script.split(lines, '\n');
128 for (StringRef line : lines) {
129 StringRef trimmed = line.ltrim();
130 if (trimmed.starts_with("(set-info")) {
131 StringRef body = trimmed.drop_front(StringRef("(set-info").size()).trim();
132 if (!body.ends_with(")")) {
133 return createStringError(
134 inconvertibleErrorCode(), "invalid set-info annotation: '%s'", trimmed.str().c_str()
135 );
136 }
137 body = body.drop_back().trim();
138 size_t splitPos = body.find_first_of(" \t");
139 if (splitPos == StringRef::npos) {
140 return createStringError(
141 inconvertibleErrorCode(), "invalid set-info annotation: '%s'", trimmed.str().c_str()
142 );
143 }
144 StringRef key = body.take_front(splitPos).trim();
145 StringRef value = body.drop_front(splitPos).trim();
146 if (key == ":status") {
147 pendingInfoExpected = parseSatResult(value);
148 if (!pendingInfoExpected) {
149 return createStringError(
150 inconvertibleErrorCode(), "invalid set-info annotation: '%s'", trimmed.str().c_str()
151 );
152 }
153 } else if (key == ":llzk-stage") {
154 auto parsed = parseQuotedString(value, trimmed);
155 if (!parsed) {
156 return parsed.takeError();
157 }
158 pendingInfoStage = std::move(*parsed);
159 } else if (key == ":llzk-root") {
160 auto parsed = parseQuotedString(value, trimmed);
161 if (!parsed) {
162 return parsed.takeError();
163 }
164 currentRoot = std::move(*parsed);
165 }
166 continue;
167 }
168 if (trimmed == "(check-sat)") {
169 std::optional<std::string> stageName = pendingInfoStage;
170 std::optional<SatResult> expected = pendingInfoExpected;
171
172 ++metadata.checkSatCount;
173
174 if (stageName && !expected) {
175 return createStringError(
176 inconvertibleErrorCode(), "missing expected result before check-sat for stage '%s'",
177 stageName->c_str()
178 );
179 }
180
181 if (stageName || expected) {
182 metadata.stages.push_back(
183 StageExpectation {
184 currentRoot.value_or(""), stageName.value_or(""),
185 expected.value_or(SatResult::Unknown), expected.has_value()
186 }
187 );
188 }
189
190 pendingInfoStage.reset();
191 pendingInfoExpected.reset();
192 continue;
193 }
194 }
195
196 if (!metadata.stages.empty() && metadata.stages.size() != metadata.checkSatCount) {
197 return createStringError(
198 inconvertibleErrorCode(), "check metadata count (%zu) does not match check-sat count (%zu)",
199 metadata.stages.size(), metadata.checkSatCount
200 );
201 }
202
203 return metadata;
204}
205
206std::string stripMetadataForSolver(StringRef script) {
207 std::string sanitized;
208 raw_string_ostream os(sanitized);
209
210 SmallVector<StringRef> lines;
211 script.split(lines, '\n');
212 for (StringRef line : lines) {
213 StringRef trimmed = line.ltrim();
214 bool shouldStrip = false;
215 if (trimmed.starts_with("(set-info")) {
216 StringRef body = trimmed.drop_front(StringRef("(set-info").size()).trim();
217 if (body.ends_with(")")) {
218 body = body.drop_back().trim();
219 size_t splitPos = body.find_first_of(" \t");
220 if (splitPos != StringRef::npos) {
221 StringRef key = body.take_front(splitPos).trim();
222 shouldStrip = key == ":status" || key == ":llzk-stage" || key == ":llzk-root";
223 }
224 }
225 }
226
227 if (!shouldStrip) {
228 os << line << '\n';
229 }
230 }
231
232 return sanitized;
233}
234
235std::string formatStageLabel(const StageExpectation &stage, size_t index) {
236 if (!stage.stageName.empty() && !stage.rootName.empty()) {
237 return (Twine(stage.rootName) + "/" + stage.stageName).str();
238 }
239 if (!stage.stageName.empty()) {
240 return stage.stageName;
241 }
242 return ("check[" + std::to_string(index) + "]");
243}
244
245Expected<std::string> readWholeFile(StringRef path) {
246 ErrorOr<std::unique_ptr<MemoryBuffer>> buffer = MemoryBuffer::getFile(path);
247 if (!buffer) {
248 return createStringError(buffer.getError(), "failed to read '%s'", path.data());
249 }
250 return std::string(buffer.get()->getBuffer());
251}
252
253Expected<std::string> resolveSolverPath(StringRef solverBinary) {
254 if (sys::path::has_parent_path(solverBinary)) {
255 return solverBinary.str();
256 }
257 ErrorOr<std::string> found = sys::findProgramByName(solverBinary);
258 if (!found) {
259 return createStringError(
260 found.getError(), "failed to find solver binary '%s'", solverBinary.data()
261 );
262 }
263 return *found;
264}
265
266Expected<SmallString<128>> createTempFile(StringRef prefix, StringRef suffix, StringRef contents) {
267 SmallString<128> path;
268 int fd = -1;
269 std::error_code ec = sys::fs::createTemporaryFile(prefix, suffix, fd, path);
270 if (ec) {
271 return createStringError(ec, "failed to create temporary file");
272 }
273
274 raw_fd_ostream os(fd, true);
275 if (!contents.empty()) {
276 os << contents;
277 }
278 os.flush();
279 if (os.has_error()) {
280 return createStringError(inconvertibleErrorCode(), "failed to write temporary file");
281 }
282
283 return path;
284}
285
286Expected<SolverInvocationResult> runSolver(StringRef solverPath, StringRef script) {
287 SolverInvocationResult result;
288 TempFileCleanup cleanup;
289
290 auto stdoutFile = createTempFile("llzk-smt-check-stdout", "txt", "");
291 if (!stdoutFile) {
292 return stdoutFile.takeError();
293 }
294 auto stderrFile = createTempFile("llzk-smt-check-stderr", "txt", "");
295 if (!stderrFile) {
296 return stderrFile.takeError();
297 }
298 cleanup.paths.push_back(*stdoutFile);
299 cleanup.paths.push_back(*stderrFile);
300
301 std::array<std::optional<StringRef>, 3> redirects = {
302 std::nullopt, StringRef(stdoutFile->data(), stdoutFile->size()),
303 StringRef(stderrFile->data(), stderrFile->size())
304 };
305
306 SmallVector<StringRef> args;
307 args.push_back(solverPath);
308 auto tempInput = createTempFile("llzk-smt-check-input", "smt2", script);
309 if (!tempInput) {
310 return tempInput.takeError();
311 }
312 cleanup.paths.push_back(*tempInput);
313 args.push_back("-smt2");
314 args.push_back(StringRef(tempInput->data(), tempInput->size()));
315
316 std::string errorMessage;
317 bool executionFailed = false;
318 result.exitCode = sys::ExecuteAndWait(
319 solverPath, args, std::nullopt, redirects, 0, 0, &errorMessage, &executionFailed
320 );
321 result.executionFailed = executionFailed;
322 result.errorMessage = std::move(errorMessage);
323
324 auto stdoutText = readWholeFile(*stdoutFile);
325 if (!stdoutText) {
326 return stdoutText.takeError();
327 }
328 result.stdoutText = std::move(*stdoutText);
329
330 auto stderrText = readWholeFile(*stderrFile);
331 if (!stderrText) {
332 return stderrText.takeError();
333 }
334 result.stderrText = std::move(*stderrText);
335
336 return result;
337}
338
339Expected<std::pair<SmallVector<SatResult>, SmallVector<std::string>>>
340parseSolverStdout(StringRef text) {
341 SmallVector<SatResult> results;
342 SmallVector<std::string> extraLines;
343 SmallVector<StringRef> lines;
344 text.split(lines, '\n');
345 for (StringRef line : lines) {
346 StringRef trimmed = line.trim();
347 if (trimmed.empty()) {
348 continue;
349 }
350 if (std::optional<SatResult> result = parseSatResult(trimmed)) {
351 results.push_back(*result);
352 continue;
353 }
354
355 std::string lowered = trimmed.lower();
356 if (StringRef(lowered).starts_with("z3 ")) {
357 continue;
358 }
359 extraLines.push_back(trimmed.str());
360 }
361 return std::make_pair(std::move(results), std::move(extraLines));
362}
363
364void printSolverFailure(const SolverInvocationResult &invocation) {
365 if (!invocation.errorMessage.empty()) {
366 errs() << "llzk-smt-check: " << invocation.errorMessage << '\n';
367 }
368 if (!invocation.stderrText.empty()) {
369 errs() << invocation.stderrText;
370 if (!invocation.stderrText.ends_with('\n')) {
371 errs() << '\n';
372 }
373 }
374}
375
376} // namespace
377
378int main(int argc, char **argv) {
379 sys::PrintStackTraceOnErrorSignal(StringRef());
380 setBugReportMsg(
381 "PLEASE submit a bug report to " BUG_REPORT_URL
382 " and include the crash backtrace, relevant SMT-LIB inputs, and associated run script(s).\n"
383 );
384
385 cl::ParseCommandLineOptions(
386 argc, argv,
387 "llzk-smt-check: run an SMT solver on staged SMT-LIB and validate per-stage results.\n"
388 );
389
390 auto input = readInput(InputFilename);
391 if (!input) {
392 errs() << toString(input.takeError()) << '\n';
393 return EXIT_FAILURE;
394 }
395
396 auto metadata = scanScript(*input);
397 if (!metadata) {
398 errs() << "llzk-smt-check: " << toString(metadata.takeError()) << '\n';
399 return EXIT_FAILURE;
400 }
401
402 auto solverPath = resolveSolverPath(SolverBinary);
403 if (!solverPath) {
404 errs() << "llzk-smt-check: " << toString(solverPath.takeError()) << '\n';
405 return EXIT_FAILURE;
406 }
407
408 std::string solverScript = stripMetadataForSolver(*input);
409 auto invocation = runSolver(*solverPath, solverScript);
410 if (!invocation) {
411 errs() << "llzk-smt-check: " << toString(invocation.takeError()) << '\n';
412 return EXIT_FAILURE;
413 }
414 if (invocation->executionFailed || invocation->exitCode != 0) {
415 errs() << "llzk-smt-check: solver exited with code " << invocation->exitCode << '\n';
416 printSolverFailure(*invocation);
417 return EXIT_FAILURE;
418 }
419
420 auto parsedStdout = parseSolverStdout(invocation->stdoutText);
421 if (!parsedStdout) {
422 errs() << "llzk-smt-check: " << toString(parsedStdout.takeError()) << '\n';
423 return EXIT_FAILURE;
424 }
425
426 SmallVector<SatResult> solverResults = std::move(parsedStdout->first);
427 SmallVector<std::string> extraLines = std::move(parsedStdout->second);
428 if (!DumpRawOutput && !extraLines.empty()) {
429 errs() << "llzk-smt-check: unexpected solver stdout:\n";
430 for (const std::string &line : extraLines) {
431 errs() << line << '\n';
432 }
433 return EXIT_FAILURE;
434 }
435 if (solverResults.size() != metadata->checkSatCount) {
436 errs() << "llzk-smt-check: solver returned " << solverResults.size() << " result(s) for "
437 << metadata->checkSatCount << " check-sat command(s)\n";
438 return EXIT_FAILURE;
439 }
440
441 SmallVector<std::string> mismatches;
442 SmallVector<std::string> summaries;
443 for (size_t i = 0; i < solverResults.size(); ++i) {
444 std::string label;
445 if (metadata->stages.empty() || metadata->stages[i].stageName.empty()) {
446 label = "check[" + std::to_string(i) + "]";
447 } else {
448 label = formatStageLabel(metadata->stages[i], i);
449 }
450 if (!Quiet) {
451 std::string summary = (Twine(label) + ": " + stringify(solverResults[i])).str();
452 if (!metadata->stages.empty() && metadata->stages[i].hasExpected) {
453 summary =
454 (Twine(summary) + " (expected " + stringify(metadata->stages[i].expected) + ")").str();
455 }
456 summaries.push_back(std::move(summary));
457 }
458 if (!metadata->stages.empty() && metadata->stages[i].hasExpected &&
459 solverResults[i] != metadata->stages[i].expected) {
460 mismatches.push_back((Twine(label) + ": got " + stringify(solverResults[i]) + ", expected " +
461 stringify(metadata->stages[i].expected))
462 .str());
463 }
464 }
465
466 if (DumpRawOutput && !invocation->stdoutText.empty()) {
467 outs() << "--- raw solver stdout ---\n" << invocation->stdoutText;
468 if (!invocation->stdoutText.ends_with('\n')) {
469 outs() << '\n';
470 }
471 }
472
473 if (!mismatches.empty()) {
474 for (const std::string &summary : summaries) {
475 errs() << summary << '\n';
476 }
477 errs() << "llzk-smt-check: stage result mismatch:\n";
478 for (const std::string &mismatch : mismatches) {
479 errs() << mismatch << '\n';
480 }
481 return EXIT_FAILURE;
482 }
483
484 for (const std::string &summary : summaries) {
485 outs() << summary << '\n';
486 }
487
488 return EXIT_SUCCESS;
489}
#define BUG_REPORT_URL
Definition config.h:15
int main(int argc, char **argv)