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