LLZK 2.1.1
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
JSON.cpp
Go to the documentation of this file.
1//===-- JSON.cpp - llzk-witgen JSON conversion helpers ----------*- 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 "JSON.h"
11
12#include "Errors.h"
13#include "WitgenUtils.h"
14#include "WitnessSelection.h"
15
18#include "llzk/Util/Compare.h"
19
20#include <mlir/IR/Operation.h>
21
22#include <llvm/ADT/SmallString.h>
23#include <llvm/ADT/TypeSwitch.h>
24#include <llvm/Support/FormatVariadic.h>
25#include <llvm/Support/MathExtras.h>
26#include <llvm/Support/raw_ostream.h>
27
28using namespace mlir;
29
30namespace llzk::witgen {
31namespace {
32
33static std::string renderJSON(const llvm::json::Value &value) {
34 return llvm::formatv("{0:2}", value).str();
35}
36
37static llvm::StringRef jsonKind(const llvm::json::Value &value) {
38 if (value.getAsNull()) {
39 return "null";
40 }
41 if (value.getAsBoolean().has_value()) {
42 return "bool";
43 }
44 if (value.getAsNumber().has_value()) {
45 return "number";
46 }
47 if (value.getAsString()) {
48 return "string";
49 }
50 if (value.getAsArray()) {
51 return "array";
52 }
53 if (value.getAsObject()) {
54 return "object";
55 }
56 return "unknown";
57}
58
59static std::string appendObjectPath(llvm::StringRef path, llvm::StringRef key) {
60 llvm::SmallString<64> out(path);
61 out += '.';
62 out += key;
63 return std::string(out);
64}
65
66static std::string appendIndexPath(llvm::StringRef path, size_t index) {
67 llvm::SmallString<64> out(path);
68 llvm::raw_svector_ostream os(out);
69 os << '[' << index << ']';
70 return std::string(out);
71}
72
73static void pushMismatch(
74 llvm::SmallVectorImpl<JSONMismatch> &out, llvm::StringRef path, const llvm::Twine &message
75) {
76 out.push_back(JSONMismatch {path.str(), message.str()});
77}
78
79static void diffObjects(
80 const llvm::json::Object &expected, const llvm::json::Object &actual,
81 llvm::SmallVectorImpl<JSONMismatch> &out, llvm::StringRef path
82) {
83 for (const auto &kv : expected) {
84 if (const llvm::json::Value *actualValue = actual.get(kv.first)) {
85 diffJSON(kv.second, *actualValue, out, appendObjectPath(path, kv.first));
86 continue;
87 }
88 pushMismatch(out, appendObjectPath(path, kv.first), "missing key");
89 }
90 for (const auto &kv : actual) {
91 if (!expected.get(kv.first)) {
92 pushMismatch(out, appendObjectPath(path, kv.first), "unexpected key");
93 }
94 }
95}
96
97static void diffArrays(
98 const llvm::json::Array &expected, const llvm::json::Array &actual,
99 llvm::SmallVectorImpl<JSONMismatch> &out, llvm::StringRef path
100) {
101 if (expected.size() != actual.size()) {
102 pushMismatch(
103 out, path,
104 llvm::Twine("array length mismatch: expected ") + llvm::Twine(expected.size()) + ", got " +
105 llvm::Twine(actual.size())
106 );
107 }
108 size_t shared = std::min(expected.size(), actual.size());
109 for (size_t i = 0; i < shared; ++i) {
110 diffJSON(expected[i], actual[i], out, appendIndexPath(path, i));
111 }
112}
113
114} // namespace
115
117static llvm::Expected<int64_t> jsonToInt(const llvm::json::Value *json) {
118 if (std::optional<int64_t> integer = json->getAsInteger()) {
119 return *integer;
120 }
121 if (std::optional<llvm::StringRef> str = json->getAsString()) {
122 int64_t value = 0;
123 if (!str->getAsInteger(10, value)) {
124 return value;
125 }
126 }
127 return makeError("expected integer-compatible JSON value");
128}
129
131static llvm::Expected<llvm::DynamicAPInt>
132jsonToFelt(const llvm::json::Value *json, const Field &field) {
133 if (std::optional<llvm::StringRef> str = json->getAsString()) {
134 return field.reduce(toDynamicAPInt(*str));
135 }
136 if (std::optional<int64_t> integer = json->getAsInteger()) {
137 return field.reduce(*integer);
138 }
139 return makeError("expected felt value as JSON integer or decimal string");
140}
141
143static llvm::Expected<WitnessVal> parseJSONArray(
144 const llvm::json::Value *json, array::ArrayType type, const Field &field, Operation *origin,
145 size_t dimIndex = 0
146) {
147 const auto *jsonArray = json->getAsArray();
148 if (!jsonArray) {
149 return makeError("expected JSON array");
150 }
151
152 llvm::ArrayRef<int64_t> shape = type.getShape();
153 if (dimIndex >= shape.size()) {
154 return makeError("invalid array shape");
155 }
156 auto expectedSize = checkedShapeDimToSize(shape[dimIndex], "JSON array input");
157 if (!expectedSize) {
158 return expectedSize.takeError();
159 }
160 if (jsonArray->size() != *expectedSize) {
161 return makeError("JSON array length does not match LLZK array dimension");
162 }
163
164 auto arrayValue = std::make_shared<ArrayValue>();
165 arrayValue->type = type;
166 if (dimIndex == shape.size() - 1) {
167 arrayValue->elements.reserve(jsonArray->size());
168 for (const llvm::json::Value &elem : *jsonArray) {
169 auto parsed = parseJSONValue(&elem, type.getElementType(), field, origin);
170 if (!parsed) {
171 return parsed.takeError();
172 }
173 arrayValue->elements.push_back(*parsed);
174 }
175 return arrayValue;
176 }
177
178 arrayValue->elements.reserve(jsonArray->size());
179 for (const llvm::json::Value &elem : *jsonArray) {
180 auto parsed = parseJSONArray(&elem, type, field, origin, dimIndex + 1);
181 if (!parsed) {
182 return parsed.takeError();
183 }
184 auto subArray = asArray(*parsed);
185 if (!subArray) {
186 return subArray.takeError();
187 }
188 for (const WitnessVal &subElem : (*subArray)->elements) {
189 arrayValue->elements.push_back(subElem);
190 }
191 }
192 return arrayValue;
193}
194
196static llvm::Expected<llvm::json::Value> feltToJSON(const llvm::DynamicAPInt &value) {
197 std::string rendered;
198 llvm::raw_string_ostream os(rendered);
199 os << value;
200 return llvm::json::Value(os.str());
201}
202
204static llvm::Expected<llvm::json::Value> serializeJSONArray(
205 const ArrayValueRef &arrayValue, array::ArrayType type, SymbolTableCollection &tables,
206 Operation *origin, SerializationMode mode, size_t dimIndex = 0, size_t flatOffset = 0
207) {
208 llvm::json::Array jsonArray;
209 llvm::ArrayRef<int64_t> shape = type.getShape();
210 auto dimSize = checkedShapeDimToSize(shape[dimIndex], "JSON array output");
211 if (!dimSize) {
212 return dimSize.takeError();
213 }
214 if (dimIndex == shape.size() - 1) {
215 for (size_t i = 0; i < *dimSize; ++i) {
216 bool overflow = false;
217 size_t elementOffset = llvm::SaturatingAdd(flatOffset, i, &overflow);
218 if (overflow) {
219 return makeError("JSON array output flat index would overflow size_t");
220 }
221 auto elem = serializeJSONValue(
222 arrayValue->elements[elementOffset], type.getElementType(), tables, origin, mode
223 );
224 if (!elem) {
225 return elem.takeError();
226 }
227 jsonArray.push_back(*elem);
228 }
229 return llvm::json::Value(std::move(jsonArray));
230 }
231
232 size_t subArraySize = 1;
233 for (size_t i = dimIndex + 1; i < shape.size(); ++i) {
234 auto nextDimSize = checkedShapeDimToSize(shape[i], "JSON array output");
235 if (!nextDimSize) {
236 return nextDimSize.takeError();
237 }
238 bool overflow = false;
239 subArraySize = llvm::SaturatingMultiply(subArraySize, *nextDimSize, &overflow);
240 if (overflow) {
241 return makeError("JSON array output sub-array size would overflow size_t");
242 }
243 }
244
245 for (size_t i = 0; i < *dimSize; ++i) {
246 bool overflow = false;
247 size_t nextOffset = llvm::SaturatingMultiplyAdd(i, subArraySize, flatOffset, &overflow);
248 if (overflow) {
249 return makeError("JSON array output flat offset would overflow size_t");
250 }
251 auto subArray =
252 serializeJSONArray(arrayValue, type, tables, origin, mode, dimIndex + 1, nextOffset);
253 if (!subArray) {
254 return subArray.takeError();
255 }
256 jsonArray.push_back(*subArray);
257 }
258 return llvm::json::Value(std::move(jsonArray));
259}
260
262llvm::Expected<WitnessVal>
263parseJSONValue(const llvm::json::Value *json, Type type, const Field &field, Operation *origin) {
264 return llvm::TypeSwitch<Type, llvm::Expected<WitnessVal>>(type)
265 .Case([&](felt::FeltType) -> llvm::Expected<WitnessVal> { return jsonToFelt(json, field); })
266 .Case([&](array::ArrayType arrayType) -> llvm::Expected<WitnessVal> {
267 return parseJSONArray(json, arrayType, field, origin);
268 })
269 .Case([&](pod::PodType) -> llvm::Expected<WitnessVal> {
270 return makeError("pod JSON inputs are not supported in llzk-witgen v1");
271 })
272 .Case([&](component::StructType) -> llvm::Expected<WitnessVal> {
273 return makeError("struct JSON inputs are not supported in llzk-witgen v1");
274 })
275 .Case([&](IndexType) -> llvm::Expected<WitnessVal> {
276 auto integer = jsonToInt(json);
277 if (!integer) {
278 return integer.takeError();
279 }
280 return *integer;
281 })
282 .Case([&](IntegerType intType) -> llvm::Expected<WitnessVal> {
283 if (intType.getWidth() == 1) {
284 if (std::optional<bool> boolValue = json->getAsBoolean()) {
285 return *boolValue;
286 }
287 auto integer = jsonToInt(json);
288 if (!integer) {
289 return integer.takeError();
290 }
291 return *integer != 0;
292 }
293 return makeError("only i1 integer JSON inputs are supported");
294 }).Default([&](Type) -> llvm::Expected<WitnessVal> {
295 return makeError("unsupported input type in llzk-witgen");
296 });
297}
298
300llvm::Expected<llvm::json::Value> serializeJSONValue(
301 const WitnessVal &value, Type type, SymbolTableCollection &tables, Operation *origin,
303) {
304 return llvm::TypeSwitch<Type, llvm::Expected<llvm::json::Value>>(type)
305 .Case([&](felt::FeltType) -> llvm::Expected<llvm::json::Value> {
306 auto feltValue = asFelt(value);
307 if (!feltValue) {
308 return feltValue.takeError();
309 }
310 return feltToJSON(*feltValue);
311 })
312 .Case([&](array::ArrayType arrayType) -> llvm::Expected<llvm::json::Value> {
313 auto arrayValue = asArray(value);
314 if (!arrayValue) {
315 return arrayValue.takeError();
316 }
317 return serializeJSONArray(*arrayValue, arrayType, tables, origin, mode);
318 })
319 .Case([&](pod::PodType podType) -> llvm::Expected<llvm::json::Value> {
320 auto podValue = asPod(value);
321 if (!podValue) {
322 return podValue.takeError();
323 }
324 llvm::json::Object result;
325 for (pod::RecordAttr record : podType.getRecords()) {
326 auto it = (*podValue)->records.find(record.getName().getValue());
327 if (it == (*podValue)->records.end()) {
328 return makeError("missing POD record during JSON serialization");
329 }
330 auto serialized = serializeJSONValue(it->second, record.getType(), tables, origin, mode);
331 if (!serialized) {
332 return serialized.takeError();
333 }
334 result[record.getName().getValue()] = *serialized;
335 }
336 return llvm::json::Value(std::move(result));
337 })
338 .Case([&](component::StructType structType) -> llvm::Expected<llvm::json::Value> {
339 auto structValue = asStruct(value);
340 if (!structValue) {
341 return structValue.takeError();
342 }
343 auto defLookup = structType.getDefinition(tables, origin);
344 if (failed(defLookup)) {
345 return makeError("could not resolve struct type during JSON serialization");
346 }
347 llvm::json::Object result;
348 for (component::MemberDefOp member : defLookup->get().getMemberDefs()) {
349 auto it = (*structValue)->members.find(member.getSymName());
350 if (it == (*structValue)->members.end()) {
351 return makeError("missing struct member during JSON serialization");
352 }
353
355 if (!member.hasPublicAttr()) {
356 continue;
357 }
358 } else {
359 if (!memberIsSignal(defLookup->get(), member) &&
360 !isa<component::StructType>(member.getType())) {
361 continue;
362 }
363 }
364
365 auto serialized = serializeJSONValue(it->second, member.getType(), tables, origin, mode);
366 if (!serialized) {
367 return serialized.takeError();
368 }
369 if (mode == SerializationMode::AllSignals && isa<component::StructType>(member.getType())) {
370 auto *object = serialized->getAsObject();
371 if (!object || object->empty()) {
372 continue;
373 }
374 }
375 result[member.getSymName()] = *serialized;
376 }
377 return llvm::json::Value(std::move(result));
378 })
379 .Case([&](IndexType) -> llvm::Expected<llvm::json::Value> {
380 auto indexValue = asIndex(value);
381 if (!indexValue) {
382 return indexValue.takeError();
383 }
384 return llvm::json::Value(*indexValue);
385 })
386 .Case([&](IntegerType intType) -> llvm::Expected<llvm::json::Value> {
387 if (intType.getWidth() != 1) {
388 return makeError("only i1 integer JSON serialization is supported");
389 }
390 auto boolValue = asBool(value);
391 if (!boolValue) {
392 return boolValue.takeError();
393 }
394 return llvm::json::Value(*boolValue);
395 }).Default([&](Type) -> llvm::Expected<llvm::json::Value> {
396 return makeError("unsupported output type in llzk-witgen");
397 });
398}
399
401llvm::Expected<llvm::json::Object> buildInputsJSONObject(
402 ArrayRef<InputBinding> bindings, ArrayRef<WitnessVal> values, SymbolTableCollection &tables,
403 Operation *origin
404) {
405 if (bindings.size() != values.size()) {
406 return makeError("input binding count mismatch during witness JSON assembly");
407 }
408
409 llvm::json::Object result;
410 for (auto [binding, value] : llvm::zip(bindings, values)) {
411 auto serialized =
412 serializeJSONValue(value, binding.type, tables, origin, SerializationMode::AllSignals);
413 if (!serialized) {
414 return serialized.takeError();
415 }
416 result[binding.name] = *serialized;
417 }
418 return result;
419}
420
422llvm::Expected<WitnessVal> extractValueAtPath(
423 const WitnessVal &root, Type rootType, ArrayRef<std::string> path,
424 SymbolTableCollection &tables, Operation *origin
425) {
426 if (path.empty()) {
427 return root;
428 }
429
430 if (auto structType = dyn_cast<component::StructType>(rootType)) {
431 auto structValue = asStruct(root);
432 if (!structValue) {
433 return structValue.takeError();
434 }
435 auto defLookup = structType.getDefinition(tables, origin);
436 if (failed(defLookup)) {
437 return makeError("could not resolve struct type while extracting witness value");
438 }
439 for (component::MemberDefOp member : defLookup->get().getMemberDefs()) {
440 if (member.getSymName() != path.front()) {
441 continue;
442 }
443 auto it = (*structValue)->members.find(member.getSymName());
444 if (it == (*structValue)->members.end()) {
445 return makeError("missing struct member while extracting witness value");
446 }
447 return extractValueAtPath(it->second, member.getType(), path.drop_front(), tables, origin);
448 }
449 return makeError("unknown struct member while extracting witness value");
450 }
451
452 if (auto podType = dyn_cast<pod::PodType>(rootType)) {
453 auto podValue = asPod(root);
454 if (!podValue) {
455 return podValue.takeError();
456 }
457 for (pod::RecordAttr record : podType.getRecords()) {
458 if (record.getName().getValue() != path.front()) {
459 continue;
460 }
461 auto it = (*podValue)->records.find(record.getName().getValue());
462 if (it == (*podValue)->records.end()) {
463 return makeError("missing POD record while extracting witness value");
464 }
465 return extractValueAtPath(it->second, record.getType(), path.drop_front(), tables, origin);
466 }
467 return makeError("unknown POD record while extracting witness value");
468 }
469
470 return makeError("extra witness path components for non-aggregate value");
471}
472
474 const llvm::json::Value &expected, const llvm::json::Value &actual,
475 llvm::SmallVectorImpl<JSONMismatch> &out, llvm::StringRef path
476) {
477 if (expected.kind() != actual.kind()) {
478 pushMismatch(
479 out, path,
480 llvm::Twine("type mismatch: expected ") + jsonKind(expected) + ", got " + jsonKind(actual)
481 );
482 return;
483 }
484
485 if (const auto *expectedObject = expected.getAsObject()) {
486 diffObjects(*expectedObject, *actual.getAsObject(), out, path);
487 return;
488 }
489 if (const auto *expectedArray = expected.getAsArray()) {
490 diffArrays(*expectedArray, *actual.getAsArray(), out, path);
491 return;
492 }
493 if (expected == actual) {
494 return;
495 }
496
497 pushMismatch(
498 out, path,
499 llvm::Twine("value mismatch: expected ") + renderJSON(expected) + ", got " +
500 renderJSON(actual)
501 );
502}
503
504void printJSONMismatches(llvm::raw_ostream &os, llvm::ArrayRef<JSONMismatch> mismatches) {
505 for (const JSONMismatch &mismatch : mismatches) {
506 os << mismatch.path << ": " << mismatch.message << '\n';
507 }
508}
509
510} // namespace llzk::witgen
Information about the prime finite field used for the interval analysis.
Definition Field.h:36
::mlir::FailureOr< SymbolLookupResult< StructDefOp > > getDefinition(::mlir::SymbolTableCollection &symbolTable, ::mlir::Operation *op, bool reportMissing=true) const
Gets the struct op that defines this struct.
Definition Types.cpp:26
::llvm::ArrayRef<::llzk::pod::RecordAttr > getRecords() const
llvm::Expected< llvm::json::Object > buildInputsJSONObject(ArrayRef< InputBinding > bindings, ArrayRef< WitnessVal > values, SymbolTableCollection &tables, Operation *origin)
Serialize named input values into a JSON object.
Definition JSON.cpp:401
llvm::Expected< PodValueRef > asPod(const WitnessVal &value)
Require a POD value from the runtime variant.
llvm::Expected< llvm::json::Value > serializeJSONValue(const WitnessVal &value, Type type, SymbolTableCollection &tables, Operation *origin, SerializationMode mode)
Serialize a supported LLZK runtime value into JSON.
Definition JSON.cpp:300
llvm::Expected< bool > asBool(const WitnessVal &value)
Require a boolean value from the runtime variant.
llvm::Expected< WitnessVal > extractValueAtPath(const WitnessVal &root, Type rootType, ArrayRef< std::string > path, SymbolTableCollection &tables, Operation *origin)
Extract one nested runtime leaf by path.
Definition JSON.cpp:422
void diffJSON(const llvm::json::Value &expected, const llvm::json::Value &actual, llvm::SmallVectorImpl< JSONMismatch > &out, llvm::StringRef path)
Compare two JSON values structurally and append any mismatches to out.
Definition JSON.cpp:473
SerializationMode
Select how struct values are filtered during JSON serialization.
Definition JSON.h:21
std::shared_ptr< ArrayValue > ArrayValueRef
Shared runtime storage for LLZK array values.
Definition ValueModel.h:42
llvm::Expected< int64_t > asIndex(const WitnessVal &value)
Require an index value from the runtime variant.
bool memberIsSignal(component::StructDefOp owner, component::MemberDefOp member)
Return true iff the member is considered a witness signal.
llvm::Expected< size_t > checkedShapeDimToSize(int64_t dim, llvm::StringRef context)
Convert one static dimension to size_t, rejecting dynamic or invalid sizes.
llvm::Expected< WitnessVal > parseJSONValue(const llvm::json::Value *json, Type type, const Field &field, Operation *origin)
Parse a supported LLZK input type from JSON.
Definition JSON.cpp:263
void printJSONMismatches(llvm::raw_ostream &os, llvm::ArrayRef< JSONMismatch > mismatches)
Render one human-readable mismatch report.
Definition JSON.cpp:504
std::variant< std::monostate, bool, int64_t, llvm::DynamicAPInt, ArrayValueRef, PodValueRef, StructValueRef > WitnessVal
Runtime value representation used by the tool-local interpreter.
Definition ValueModel.h:51
llvm::Expected< llvm::DynamicAPInt > asFelt(const WitnessVal &value)
Require a felt value from the runtime variant.
llvm::Error makeError(const llvm::Twine &msg)
Build a string-backed error for user-facing witgen failures.
Definition Errors.h:18
llvm::Expected< StructValueRef > asStruct(const WitnessVal &value)
Require a struct value from the runtime variant.
llvm::Expected< ArrayValueRef > asArray(const WitnessVal &value)
Require an array value from the runtime variant.
DynamicAPInt toDynamicAPInt(StringRef str)
One structured JSON mismatch between expected and actual witgen output.
Definition JSON.h:27