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