LLZK 3.0.0
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 auto value = parseDynamicAPInt(*str);
135 if (!value) {
136 return value.takeError();
137 }
138 return field.reduce(*value);
139 }
140 if (std::optional<int64_t> integer = json->getAsInteger()) {
141 return field.reduce(*integer);
142 }
143 return makeError("expected felt value as JSON integer or decimal string");
144}
145
147static llvm::Expected<WitnessVal> parseJSONArray(
148 const llvm::json::Value *json, array::ArrayType type, const Field &field, Operation *origin,
149 size_t dimIndex = 0
150) {
151 const auto *jsonArray = json->getAsArray();
152 if (!jsonArray) {
153 return makeError("expected JSON array");
154 }
155
156 llvm::ArrayRef<int64_t> shape = type.getShape();
157 if (dimIndex >= shape.size()) {
158 return makeError("invalid array shape");
159 }
160 auto expectedSize = checkedShapeDimToSize(shape[dimIndex], "JSON array input");
161 if (!expectedSize) {
162 return expectedSize.takeError();
163 }
164 if (jsonArray->size() != *expectedSize) {
165 return makeError("JSON array length does not match LLZK array dimension");
166 }
167
168 auto arrayValue = std::make_shared<ArrayValue>();
169 arrayValue->type = type;
170 if (dimIndex == shape.size() - 1) {
171 arrayValue->elements.reserve(jsonArray->size());
172 for (const llvm::json::Value &elem : *jsonArray) {
173 auto parsed = parseJSONValue(&elem, type.getElementType(), field, origin);
174 if (!parsed) {
175 return parsed.takeError();
176 }
177 arrayValue->elements.push_back(*parsed);
178 }
179 return arrayValue;
180 }
181
182 arrayValue->elements.reserve(jsonArray->size());
183 for (const llvm::json::Value &elem : *jsonArray) {
184 auto parsed = parseJSONArray(&elem, type, field, origin, dimIndex + 1);
185 if (!parsed) {
186 return parsed.takeError();
187 }
188 auto subArray = asArray(*parsed);
189 if (!subArray) {
190 return subArray.takeError();
191 }
192 for (const WitnessVal &subElem : (*subArray)->elements) {
193 arrayValue->elements.push_back(subElem);
194 }
195 }
196 return arrayValue;
197}
198
199llvm::json::Value serializeFeltToJSON(const llvm::DynamicAPInt &value, const Field &field) {
200 std::string rendered;
201 llvm::raw_string_ostream(rendered) << field.reduce(value);
202 return llvm::json::Value(rendered);
203}
204
206static llvm::Expected<llvm::json::Value> serializeJSONArray(
207 const ArrayValueRef &arrayValue, array::ArrayType type, SymbolTableCollection &tables,
208 Operation *origin, SerializationMode mode, size_t dimIndex = 0, size_t flatOffset = 0
209) {
210 llvm::json::Array jsonArray;
211 llvm::ArrayRef<int64_t> shape = type.getShape();
212 auto dimSize = checkedShapeDimToSize(shape[dimIndex], "JSON array output");
213 if (!dimSize) {
214 return dimSize.takeError();
215 }
216 if (dimIndex == shape.size() - 1) {
217 for (size_t i = 0; i < *dimSize; ++i) {
218 bool overflow = false;
219 size_t elementOffset = llvm::SaturatingAdd(flatOffset, i, &overflow);
220 if (overflow) {
221 return makeError("JSON array output flat index would overflow size_t");
222 }
223 auto elem = serializeJSONValue(
224 arrayValue->elements[elementOffset], type.getElementType(), tables, origin, mode
225 );
226 if (!elem) {
227 return elem.takeError();
228 }
229 jsonArray.push_back(*elem);
230 }
231 return llvm::json::Value(std::move(jsonArray));
232 }
233
234 size_t subArraySize = 1;
235 for (size_t i = dimIndex + 1; i < shape.size(); ++i) {
236 auto nextDimSize = checkedShapeDimToSize(shape[i], "JSON array output");
237 if (!nextDimSize) {
238 return nextDimSize.takeError();
239 }
240 bool overflow = false;
241 subArraySize = llvm::SaturatingMultiply(subArraySize, *nextDimSize, &overflow);
242 if (overflow) {
243 return makeError("JSON array output sub-array size would overflow size_t");
244 }
245 }
246
247 for (size_t i = 0; i < *dimSize; ++i) {
248 bool overflow = false;
249 size_t nextOffset = llvm::SaturatingMultiplyAdd(i, subArraySize, flatOffset, &overflow);
250 if (overflow) {
251 return makeError("JSON array output flat offset would overflow size_t");
252 }
253 auto subArray =
254 serializeJSONArray(arrayValue, type, tables, origin, mode, dimIndex + 1, nextOffset);
255 if (!subArray) {
256 return subArray.takeError();
257 }
258 jsonArray.push_back(*subArray);
259 }
260 return llvm::json::Value(std::move(jsonArray));
261}
262
264llvm::Expected<WitnessVal>
265parseJSONValue(const llvm::json::Value *json, Type type, const Field &field, Operation *origin) {
266 return llvm::TypeSwitch<Type, llvm::Expected<WitnessVal>>(type)
267 .Case([&](felt::FeltType) -> llvm::Expected<WitnessVal> { return jsonToFelt(json, field); })
268 .Case([&](array::ArrayType arrayType) -> llvm::Expected<WitnessVal> {
269 return parseJSONArray(json, arrayType, field, origin);
270 })
271 .Case([&](pod::PodType) -> llvm::Expected<WitnessVal> {
272 return makeError("pod JSON inputs are not supported in llzk-witgen v1");
273 })
274 .Case([&](component::StructType) -> llvm::Expected<WitnessVal> {
275 return makeError("struct JSON inputs are not supported in llzk-witgen v1");
276 })
277 .Case([&](IndexType) -> llvm::Expected<WitnessVal> {
278 auto integer = jsonToInt(json);
279 if (!integer) {
280 return integer.takeError();
281 }
282 return *integer;
283 })
284 .Case([&](IntegerType intType) -> llvm::Expected<WitnessVal> {
285 if (intType.getWidth() == 1) {
286 if (std::optional<bool> boolValue = json->getAsBoolean()) {
287 return *boolValue;
288 }
289 auto integer = jsonToInt(json);
290 if (!integer) {
291 return integer.takeError();
292 }
293 return *integer != 0;
294 }
295 return makeError("only i1 integer JSON inputs are supported");
296 }).Default([&](Type) -> llvm::Expected<WitnessVal> {
297 return makeError("unsupported input type in llzk-witgen");
298 });
299}
300
302llvm::Expected<llvm::json::Value> serializeJSONValue(
303 const WitnessVal &value, Type type, SymbolTableCollection &tables, Operation *origin,
305) {
306 return llvm::TypeSwitch<Type, llvm::Expected<llvm::json::Value>>(type)
307 .Case([&](felt::FeltType feltType) -> llvm::Expected<llvm::json::Value> {
308 auto feltValue = asFelt(value);
309 if (!feltValue) {
310 return feltValue.takeError();
311 }
312 if (!feltType.hasField()) {
313 return makeError("felt JSON output requires a declared field");
314 }
315 return serializeFeltToJSON(*feltValue, feltType.getField());
316 })
317 .Case([&](array::ArrayType arrayType) -> llvm::Expected<llvm::json::Value> {
318 auto arrayValue = asArray(value);
319 if (!arrayValue) {
320 return arrayValue.takeError();
321 }
322 return serializeJSONArray(*arrayValue, arrayType, tables, origin, mode);
323 })
324 .Case([&](pod::PodType podType) -> llvm::Expected<llvm::json::Value> {
325 auto podValue = asPod(value);
326 if (!podValue) {
327 return podValue.takeError();
328 }
329 llvm::json::Object result;
330 for (pod::RecordAttr record : podType.getRecords()) {
331 auto it = (*podValue)->records.find(record.getName().getValue());
332 if (it == (*podValue)->records.end()) {
333 return makeError("missing POD record during JSON serialization");
334 }
335 auto serialized = serializeJSONValue(it->second, record.getType(), tables, origin, mode);
336 if (!serialized) {
337 return serialized.takeError();
338 }
339 result[record.getName().getValue()] = *serialized;
340 }
341 return llvm::json::Value(std::move(result));
342 })
343 .Case([&](component::StructType structType) -> llvm::Expected<llvm::json::Value> {
344 auto structValue = asStruct(value);
345 if (!structValue) {
346 return structValue.takeError();
347 }
348 auto defLookup = structType.getDefinition(tables, origin);
349 if (failed(defLookup)) {
350 return makeError("could not resolve struct type during JSON serialization");
351 }
352 llvm::json::Object result;
353 for (component::MemberDefOp member : defLookup->get().getMemberDefs()) {
354 auto it = (*structValue)->members.find(member.getSymName());
355 if (it == (*structValue)->members.end()) {
356 return makeError("missing struct member during JSON serialization");
357 }
358
360 if (!member.hasPublicAttr()) {
361 continue;
362 }
363 } else {
364 if (!memberIsSignal(defLookup->get(), member) &&
365 !isa<component::StructType>(member.getType())) {
366 continue;
367 }
368 }
369
370 auto serialized = serializeJSONValue(it->second, member.getType(), tables, origin, mode);
371 if (!serialized) {
372 return serialized.takeError();
373 }
374 if (mode == SerializationMode::AllSignals && isa<component::StructType>(member.getType())) {
375 auto *object = serialized->getAsObject();
376 if (!object || object->empty()) {
377 continue;
378 }
379 }
380 result[member.getSymName()] = *serialized;
381 }
382 return llvm::json::Value(std::move(result));
383 })
384 .Case([&](IndexType) -> llvm::Expected<llvm::json::Value> {
385 auto indexValue = asIndex(value);
386 if (!indexValue) {
387 return indexValue.takeError();
388 }
389 return llvm::json::Value(*indexValue);
390 })
391 .Case([&](IntegerType intType) -> llvm::Expected<llvm::json::Value> {
392 if (intType.getWidth() != 1) {
393 return makeError("only i1 integer JSON serialization is supported");
394 }
395 auto boolValue = asBool(value);
396 if (!boolValue) {
397 return boolValue.takeError();
398 }
399 return llvm::json::Value(*boolValue);
400 }).Default([&](Type) -> llvm::Expected<llvm::json::Value> {
401 return makeError("unsupported output type in llzk-witgen");
402 });
403}
404
406llvm::Expected<llvm::json::Object> buildInputsJSONObject(
407 ArrayRef<InputBinding> bindings, ArrayRef<WitnessVal> values, SymbolTableCollection &tables,
408 Operation *origin
409) {
410 if (bindings.size() != values.size()) {
411 return makeError("input binding count mismatch during witness JSON assembly");
412 }
413
414 llvm::json::Object result;
415 for (auto [binding, value] : llvm::zip(bindings, values)) {
416 auto serialized =
417 serializeJSONValue(value, binding.type, tables, origin, SerializationMode::AllSignals);
418 if (!serialized) {
419 return serialized.takeError();
420 }
421 result[binding.name] = *serialized;
422 }
423 return result;
424}
425
427llvm::Expected<WitnessVal> extractValueAtPath(
428 const WitnessVal &root, Type rootType, ArrayRef<std::string> path,
429 SymbolTableCollection &tables, Operation *origin
430) {
431 if (path.empty()) {
432 return root;
433 }
434
435 if (auto structType = dyn_cast<component::StructType>(rootType)) {
436 auto structValue = asStruct(root);
437 if (!structValue) {
438 return structValue.takeError();
439 }
440 auto defLookup = structType.getDefinition(tables, origin);
441 if (failed(defLookup)) {
442 return makeError("could not resolve struct type while extracting witness value");
443 }
444 for (component::MemberDefOp member : defLookup->get().getMemberDefs()) {
445 if (member.getSymName() != path.front()) {
446 continue;
447 }
448 auto it = (*structValue)->members.find(member.getSymName());
449 if (it == (*structValue)->members.end()) {
450 return makeError("missing struct member while extracting witness value");
451 }
452 return extractValueAtPath(it->second, member.getType(), path.drop_front(), tables, origin);
453 }
454 return makeError("unknown struct member while extracting witness value");
455 }
456
457 if (auto podType = dyn_cast<pod::PodType>(rootType)) {
458 auto podValue = asPod(root);
459 if (!podValue) {
460 return podValue.takeError();
461 }
462 for (pod::RecordAttr record : podType.getRecords()) {
463 if (record.getName().getValue() != path.front()) {
464 continue;
465 }
466 auto it = (*podValue)->records.find(record.getName().getValue());
467 if (it == (*podValue)->records.end()) {
468 return makeError("missing POD record while extracting witness value");
469 }
470 return extractValueAtPath(it->second, record.getType(), path.drop_front(), tables, origin);
471 }
472 return makeError("unknown POD record while extracting witness value");
473 }
474
475 return makeError("extra witness path components for non-aggregate value");
476}
477
479 const llvm::json::Value &expected, const llvm::json::Value &actual,
480 llvm::SmallVectorImpl<JSONMismatch> &out, llvm::StringRef path
481) {
482 if (expected.kind() != actual.kind()) {
483 pushMismatch(
484 out, path,
485 llvm::Twine("type mismatch: expected ") + jsonKind(expected) + ", got " + jsonKind(actual)
486 );
487 return;
488 }
489
490 if (const auto *expectedObject = expected.getAsObject()) {
491 diffObjects(*expectedObject, *actual.getAsObject(), out, path);
492 return;
493 }
494 if (const auto *expectedArray = expected.getAsArray()) {
495 diffArrays(*expectedArray, *actual.getAsArray(), out, path);
496 return;
497 }
498 if (expected == actual) {
499 return;
500 }
501
502 pushMismatch(
503 out, path,
504 llvm::Twine("value mismatch: expected ") + renderJSON(expected) + ", got " +
505 renderJSON(actual)
506 );
507}
508
509void printJSONMismatches(llvm::raw_ostream &os, llvm::ArrayRef<JSONMismatch> mismatches) {
510 for (const JSONMismatch &mismatch : mismatches) {
511 os << mismatch.path << ": " << mismatch.message << '\n';
512 }
513}
514
515} // namespace llzk::witgen
Information about the prime finite field used for the interval analysis.
Definition Field.h:36
llvm::DynamicAPInt reduce(const llvm::DynamicAPInt &i) const
Returns the canonical representative of i mod p in [0, p).
::llvm::ArrayRef< int64_t > getShape() const
::mlir::Type getElementType() const
::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
const ::llzk::Field & getField() const
Definition Dialect.cpp:255
bool hasField() const
Definition Types.h.inc:50
::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:406
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< 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:302
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:427
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:478
llvm::json::Value serializeFeltToJSON(const llvm::DynamicAPInt &value, const Field &field)
Render a runtime field element as its canonical decimal string without modifying the raw runtime valu...
Definition JSON.cpp:199
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:265
void printJSONMismatches(llvm::raw_ostream &os, llvm::ArrayRef< JSONMismatch > mismatches)
Render one human-readable mismatch report.
Definition JSON.cpp:509
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.
Expected< DynamicAPInt > parseDynamicAPInt(StringRef str)
One structured JSON mismatch between expected and actual witgen output.
Definition JSON.h:27