LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
Interpreter.cpp
Go to the documentation of this file.
1//===-- Interpreter.cpp - llzk-witgen compute interpreter -------*- 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 "Interpreter.h"
11
12#include "Errors.h"
13#include "WitgenUtils.h"
14
23#include "llzk/Util/Compare.h"
26
27#include <mlir/Dialect/Arith/IR/Arith.h>
28#include <mlir/Dialect/SCF/IR/SCF.h>
29#include <mlir/Dialect/Utils/IndexingUtils.h>
30#include <mlir/IR/Operation.h>
31
32#include <llvm/ADT/STLExtras.h>
33#include <llvm/ADT/SmallVector.h>
34#include <llvm/Support/MathExtras.h>
35
36#include <limits>
37#include <random>
38
39using namespace mlir;
40
41namespace llzk::witgen {
42
43namespace {
44
46static bool usesUnsignedCmp(scf::ForOp forOp) {
47 if (auto boolAttr = forOp->getAttrOfType<BoolAttr>("unsignedCmp")) {
48 return boolAttr.getValue();
49 }
50 return forOp->hasAttr("unsignedCmp");
51}
52
54struct BlockResult {
55 bool terminated = false;
56 llvm::SmallVector<WitnessVal> values;
57};
58
60llvm::Expected<size_t> checkedLinearize(
61 llvm::ArrayRef<int64_t> shape, llvm::ArrayRef<int64_t> indices, llvm::StringRef context
62) {
63 if (shape.size() != indices.size()) {
64 return makeError("wrong number of array indices");
65 }
66 for (auto [idx, dim] : llvm::zip_equal(indices, shape)) {
67 if (idx < 0 || dim < 0 || idx >= dim) {
68 return makeError(context);
69 }
70 }
71 auto strides = mlir::computeStrides(shape);
72 return checkedCast<size_t>(mlir::linearize(indices, strides));
73}
74
75} // namespace
76
79 ModuleOp module, SymbolTableCollection &symbolTables, const Field &moduleField,
80 UninitializedBehavior behavior, std::mt19937_64 r
81)
82 : moduleOp(module), tables(symbolTables), field(moduleField), uninitializedBehavior(behavior),
83 rng(r) {}
84
85namespace {
86
88class InvocationInterpreter {
89public:
91 InvocationInterpreter(
92 ModuleOp module, SymbolTableCollection &symbolTables, const Field &moduleField,
93 UninitializedBehavior behavior, std::mt19937_64 &r
94 )
95 : moduleOp(module), tables(symbolTables), field(moduleField), uninitializedBehavior(behavior),
96 rng(r) {}
97
99 llvm::Expected<llvm::SmallVector<WitnessVal>>
100 run(function::FuncDefOp funcOp, ArrayRef<WitnessVal> args) {
101 if (funcOp.isExternal()) {
102 return makeError("extern functions are not supported in llzk-witgen");
103 }
104 if (!funcOp.getBody().hasOneBlock()) {
105 return makeError("multi-block functions are not supported in llzk-witgen");
106 }
107 if (funcOp.getNumArguments() != args.size()) {
108 return makeError("wrong number of arguments passed to function");
109 }
110
111 llvm::DenseMap<mlir::Value, WitnessVal> scope;
112 Block &entry = funcOp.getBody().front();
113 for (auto [arg, value] : llvm::zip(entry.getArguments(), args)) {
114 scope[arg] = value;
115 }
116
117 auto result = runBlock(entry, scope);
118 if (!result) {
119 return result.takeError();
120 }
121 return result->values;
122 }
123
124private:
125 ModuleOp moduleOp;
126 SymbolTableCollection &tables;
127 const Field &field;
128 UninitializedBehavior uninitializedBehavior;
129 std::mt19937_64 &rng;
130
132 llvm::Expected<BlockResult>
133 runBlock(Block &block, llvm::DenseMap<mlir::Value, WitnessVal> &scope) {
134 for (Operation &op : block) {
135 auto handled = runOperation(op, scope);
136 if (!handled) {
137 return handled.takeError();
138 }
139 if (handled->terminated) {
140 return *handled;
141 }
142 }
143 return BlockResult {};
144 }
145
147 llvm::Expected<BlockResult> runRegion(
148 Region &region, ArrayRef<WitnessVal> args, llvm::DenseMap<mlir::Value, WitnessVal> scope
149 ) {
150 if (!region.hasOneBlock()) {
151 return makeError("multi-block regions are not supported in llzk-witgen");
152 }
153 Block &block = region.front();
154 if (block.getNumArguments() != args.size()) {
155 return makeError("region argument count mismatch");
156 }
157 for (auto [arg, value] : llvm::zip(block.getArguments(), args)) {
158 scope[arg] = value;
159 }
160 return runBlock(block, scope);
161 }
162
164 llvm::Expected<WitnessVal>
165 lookup(mlir::Value value, llvm::DenseMap<mlir::Value, WitnessVal> &scope) {
166 auto it = scope.find(value);
167 if (it == scope.end()) {
168 return makeError("failed to find SSA value during interpretation");
169 }
170 return it->second;
171 }
172
174 llvm::Expected<llvm::SmallVector<WitnessVal>>
175 collectOperands(OperandRange operands, llvm::DenseMap<mlir::Value, WitnessVal> &scope) {
176 llvm::SmallVector<WitnessVal> values;
177 values.reserve(operands.size());
178 for (mlir::Value operand : operands) {
179 auto value = lookup(operand, scope);
180 if (!value) {
181 return value.takeError();
182 }
183 values.push_back(*value);
184 }
185 return values;
186 }
187
189 llvm::Expected<BlockResult>
190 runOperation(Operation &op, llvm::DenseMap<mlir::Value, WitnessVal> &scope) {
191 if (auto returnOp = dyn_cast<function::ReturnOp>(op)) {
192 auto values = collectOperands(returnOp.getOperands(), scope);
193 if (!values) {
194 return values.takeError();
195 }
196 return BlockResult {true, std::move(*values)};
197 }
198 if (auto yieldOp = dyn_cast<scf::YieldOp>(op)) {
199 auto values = collectOperands(yieldOp.getOperands(), scope);
200 if (!values) {
201 return values.takeError();
202 }
203 return BlockResult {true, std::move(*values)};
204 }
205 if (auto conditionOp = dyn_cast<scf::ConditionOp>(op)) {
206 auto values = collectOperands(conditionOp.getOperands(), scope);
207 if (!values) {
208 return values.takeError();
209 }
210 return BlockResult {true, std::move(*values)};
211 }
212
213 auto bind = [&](ArrayRef<WitnessVal> results) -> llvm::Expected<BlockResult> {
214 if (results.size() != op.getNumResults()) {
215 return makeError("internal result count mismatch");
216 }
217 for (auto [result, value] : llvm::zip(op.getResults(), results)) {
218 scope[result] = value;
219 }
220 return BlockResult {};
221 };
222
223 if (auto constantOp = dyn_cast<arith::ConstantOp>(op)) {
224 Attribute valueAttr = constantOp.getValue();
225 if (auto integerAttr = dyn_cast<IntegerAttr>(valueAttr)) {
226 if (integerAttr.getType().isInteger(1)) {
227 return bind({WitnessVal(integerAttr.getValue().getBoolValue())});
228 }
229 return bind({WitnessVal(integerAttr.getValue().getSExtValue())});
230 }
231 return makeError("unsupported arith.constant value");
232 }
233
234 if (auto nondetOp = dyn_cast<llzk::NonDetOp>(op)) {
235 auto value = defaultValue(
236 nondetOp.getType(), tables, nondetOp.getOperation(), field, uninitializedBehavior, &rng
237 );
238 if (!value) {
239 return value.takeError();
240 }
241 return bind({*value});
242 }
243
244 if (auto assertOp = dyn_cast<boolean::AssertOp>(op)) {
245 auto condition = lookup(assertOp.getCondition(), scope);
246 if (!condition) {
247 return condition.takeError();
248 }
249 auto boolValue = asBool(*condition);
250 if (!boolValue) {
251 return boolValue.takeError();
252 }
253 if (!*boolValue) {
254 std::string msg = "bool.assert failed";
255 if (auto attr = assertOp.getMsg()) {
256 msg = attr->str();
257 }
258 return makeError(msg);
259 }
260 return BlockResult {};
261 }
262
263 if (auto andOp = dyn_cast<boolean::AndBoolOp>(op)) {
264 auto lhsValue = lookup(andOp.getLhs(), scope);
265 auto rhsValue = lookup(andOp.getRhs(), scope);
266 if (!lhsValue) {
267 return lhsValue.takeError();
268 }
269 if (!rhsValue) {
270 return rhsValue.takeError();
271 }
272 auto lhs = asBool(*lhsValue);
273 if (!lhs) {
274 return lhs.takeError();
275 }
276 auto rhs = asBool(*rhsValue);
277 if (!rhs) {
278 return rhs.takeError();
279 }
280 return bind({WitnessVal(*lhs && *rhs)});
281 }
282 if (auto orOp = dyn_cast<boolean::OrBoolOp>(op)) {
283 auto lhsValue = lookup(orOp.getLhs(), scope);
284 auto rhsValue = lookup(orOp.getRhs(), scope);
285 if (!lhsValue) {
286 return lhsValue.takeError();
287 }
288 if (!rhsValue) {
289 return rhsValue.takeError();
290 }
291 auto lhs = asBool(*lhsValue);
292 if (!lhs) {
293 return lhs.takeError();
294 }
295 auto rhs = asBool(*rhsValue);
296 if (!rhs) {
297 return rhs.takeError();
298 }
299 return bind({WitnessVal(*lhs || *rhs)});
300 }
301 if (auto xorOp = dyn_cast<boolean::XorBoolOp>(op)) {
302 auto lhsValue = lookup(xorOp.getLhs(), scope);
303 auto rhsValue = lookup(xorOp.getRhs(), scope);
304 if (!lhsValue) {
305 return lhsValue.takeError();
306 }
307 if (!rhsValue) {
308 return rhsValue.takeError();
309 }
310 auto lhs = asBool(*lhsValue);
311 if (!lhs) {
312 return lhs.takeError();
313 }
314 auto rhs = asBool(*rhsValue);
315 if (!rhs) {
316 return rhs.takeError();
317 }
318 return bind({WitnessVal(*lhs != *rhs)});
319 }
320 if (auto notOp = dyn_cast<boolean::NotBoolOp>(op)) {
321 auto operand = lookup(notOp.getOperand(), scope);
322 if (!operand) {
323 return operand.takeError();
324 }
325 auto boolValue = asBool(*operand);
326 if (!boolValue) {
327 return boolValue.takeError();
328 }
329 return bind({WitnessVal(!*boolValue)});
330 }
331 if (auto cmpOp = dyn_cast<boolean::CmpOp>(op)) {
332 auto lhs = lookup(cmpOp.getLhs(), scope);
333 auto rhs = lookup(cmpOp.getRhs(), scope);
334 if (!lhs) {
335 return lhs.takeError();
336 }
337 if (!rhs) {
338 return rhs.takeError();
339 }
340 auto lhsValue = asFelt(*lhs);
341 if (!lhsValue) {
342 return lhsValue.takeError();
343 }
344 auto rhsValue = asFelt(*rhs);
345 if (!rhsValue) {
346 return rhsValue.takeError();
347 }
348 bool result = false;
349 switch (cmpOp.getPredicate()) {
351 result = *lhsValue == *rhsValue;
352 break;
354 result = *lhsValue != *rhsValue;
355 break;
357 result = *lhsValue < *rhsValue;
358 break;
360 result = *lhsValue <= *rhsValue;
361 break;
363 result = *lhsValue > *rhsValue;
364 break;
366 result = *lhsValue >= *rhsValue;
367 break;
368 }
369 return bind({WitnessVal(result)});
370 }
371
372 if (auto feltConst = dyn_cast<felt::FeltConstantOp>(op)) {
373 auto value = feltConst.getValue().getReducedValue();
374 if (!value) {
375 return makeError("felt constant requires a specified field for witness generation");
376 }
377 return bind({WitnessVal(*value)});
378 }
379
380 auto handleBinaryFelt = [&](auto feltOp, auto fn) -> llvm::Expected<BlockResult> {
381 auto lhsValue = lookup(feltOp.getLhs(), scope);
382 auto rhsValue = lookup(feltOp.getRhs(), scope);
383 if (!lhsValue) {
384 return lhsValue.takeError();
385 }
386 if (!rhsValue) {
387 return rhsValue.takeError();
388 }
389 auto lhs = asFelt(*lhsValue);
390 if (!lhs) {
391 return lhs.takeError();
392 }
393 auto rhs = asFelt(*rhsValue);
394 if (!rhs) {
395 return rhs.takeError();
396 }
397 return bind({WitnessVal(field.reduce(fn(*lhs, *rhs)))});
398 };
399
400 if (auto addOp = dyn_cast<felt::AddFeltOp>(op)) {
401 return handleBinaryFelt(addOp, [](const auto &lhs, const auto &rhs) { return lhs + rhs; });
402 }
403 if (auto powOp = dyn_cast<felt::PowFeltOp>(op)) {
404 return handleBinaryFelt(powOp, [&](const auto &lhs, const auto &rhs) {
405 return modExp(lhs, rhs, field.prime());
406 });
407 }
408 if (auto andOp = dyn_cast<felt::AndFeltOp>(op)) {
409 return handleBinaryFelt(andOp, [](const auto &lhs, const auto &rhs) { return lhs & rhs; });
410 }
411 if (auto orOp = dyn_cast<felt::OrFeltOp>(op)) {
412 return handleBinaryFelt(orOp, [](const auto &lhs, const auto &rhs) { return lhs | rhs; });
413 }
414 if (auto xorOp = dyn_cast<felt::XorFeltOp>(op)) {
415 return handleBinaryFelt(xorOp, [](const auto &lhs, const auto &rhs) { return lhs ^ rhs; });
416 }
417 if (auto subOp = dyn_cast<felt::SubFeltOp>(op)) {
418 return handleBinaryFelt(subOp, [](const auto &lhs, const auto &rhs) { return lhs - rhs; });
419 }
420 if (auto mulOp = dyn_cast<felt::MulFeltOp>(op)) {
421 return handleBinaryFelt(mulOp, [](const auto &lhs, const auto &rhs) { return lhs * rhs; });
422 }
423 if (auto divOp = dyn_cast<felt::DivFeltOp>(op)) {
424 return handleBinaryFelt(divOp, [&](const auto &lhs, const auto &rhs) {
425 return lhs * field.inv(rhs);
426 });
427 }
428 if (auto uintDivOp = dyn_cast<felt::UnsignedIntDivFeltOp>(op)) {
429 auto lhsValue = lookup(uintDivOp.getLhs(), scope);
430 auto rhsValue = lookup(uintDivOp.getRhs(), scope);
431 if (!lhsValue) {
432 return lhsValue.takeError();
433 }
434 if (!rhsValue) {
435 return rhsValue.takeError();
436 }
437 auto lhs = asFelt(*lhsValue);
438 if (!lhs) {
439 return lhs.takeError();
440 }
441 auto rhs = asFelt(*rhsValue);
442 if (!rhs) {
443 return rhs.takeError();
444 }
445 if (*rhs == 0) {
446 return makeError("felt.uintdiv divisor must be non-zero");
447 }
448 return bind({WitnessVal(*lhs / *rhs)});
449 }
450 if (auto sintDivOp = dyn_cast<felt::SignedIntDivFeltOp>(op)) {
451 auto lhsValue = lookup(sintDivOp.getLhs(), scope);
452 auto rhsValue = lookup(sintDivOp.getRhs(), scope);
453 if (!lhsValue) {
454 return lhsValue.takeError();
455 }
456 if (!rhsValue) {
457 return rhsValue.takeError();
458 }
459 auto lhs = asFelt(*lhsValue);
460 if (!lhs) {
461 return lhs.takeError();
462 }
463 auto rhs = asFelt(*rhsValue);
464 if (!rhs) {
465 return rhs.takeError();
466 }
467 if (*rhs == 0) {
468 return makeError("felt.sintdiv divisor must be non-zero");
469 }
470 return bind({WitnessVal(field.reduce(field.toSigned(*lhs) / field.toSigned(*rhs)))});
471 }
472 if (auto umodOp = dyn_cast<felt::UnsignedModFeltOp>(op)) {
473 auto lhsValue = lookup(umodOp.getLhs(), scope);
474 auto rhsValue = lookup(umodOp.getRhs(), scope);
475 if (!lhsValue) {
476 return lhsValue.takeError();
477 }
478 if (!rhsValue) {
479 return rhsValue.takeError();
480 }
481 auto lhs = asFelt(*lhsValue);
482 if (!lhs) {
483 return lhs.takeError();
484 }
485 auto rhs = asFelt(*rhsValue);
486 if (!rhs) {
487 return rhs.takeError();
488 }
489 if (*rhs == 0) {
490 return makeError("felt.umod divisor must be non-zero");
491 }
492 return bind({WitnessVal(*lhs % *rhs)});
493 }
494 if (auto smodOp = dyn_cast<felt::SignedModFeltOp>(op)) {
495 auto lhsValue = lookup(smodOp.getLhs(), scope);
496 auto rhsValue = lookup(smodOp.getRhs(), scope);
497 if (!lhsValue) {
498 return lhsValue.takeError();
499 }
500 if (!rhsValue) {
501 return rhsValue.takeError();
502 }
503 auto lhs = asFelt(*lhsValue);
504 if (!lhs) {
505 return lhs.takeError();
506 }
507 auto rhs = asFelt(*rhsValue);
508 if (!rhs) {
509 return rhs.takeError();
510 }
511 if (*rhs == 0) {
512 return makeError("felt.smod divisor must be non-zero");
513 }
514 return bind({WitnessVal(field.reduce(field.toSigned(*lhs) % field.toSigned(*rhs)))});
515 }
516 if (auto shrOp = dyn_cast<felt::ShrFeltOp>(op)) {
517 auto lhsValue = lookup(shrOp.getLhs(), scope);
518 auto rhsValue = lookup(shrOp.getRhs(), scope);
519 if (!lhsValue) {
520 return lhsValue.takeError();
521 }
522 if (!rhsValue) {
523 return rhsValue.takeError();
524 }
525 auto lhs = asFelt(*lhsValue);
526 if (!lhs) {
527 return lhs.takeError();
528 }
529 auto rhs = asFelt(*rhsValue);
530 if (!rhs) {
531 return rhs.takeError();
532 }
533 llvm::DynamicAPInt result(0);
534 if (*rhs < field.bitWidth()) {
535 result = *lhs >> *rhs;
536 }
537 return bind({WitnessVal(result)});
538 }
539 if (auto shlOp = dyn_cast<felt::ShlFeltOp>(op)) {
540 auto lhsValue = lookup(shlOp.getLhs(), scope);
541 auto rhsValue = lookup(shlOp.getRhs(), scope);
542 if (!lhsValue) {
543 return lhsValue.takeError();
544 }
545 if (!rhsValue) {
546 return rhsValue.takeError();
547 }
548 auto lhs = asFelt(*lhsValue);
549 if (!lhs) {
550 return lhs.takeError();
551 }
552 auto rhs = asFelt(*rhsValue);
553 if (!rhs) {
554 return rhs.takeError();
555 }
556 llvm::DynamicAPInt two(2);
557 // It's more efficient to use modExp than native shift left, as for large
558 // exponents, << could allocate large temporaries, whereas modExp will be bounded
559 // by the field prime.
560 return bind({WitnessVal(field.reduce(*lhs * modExp(two, *rhs, field.prime())))});
561 }
562 if (auto negOp = dyn_cast<felt::NegFeltOp>(op)) {
563 auto operand = lookup(negOp.getOperand(), scope);
564 if (!operand) {
565 return operand.takeError();
566 }
567 auto feltValue = asFelt(*operand);
568 if (!feltValue) {
569 return feltValue.takeError();
570 }
571 return bind({WitnessVal(field.reduce(-*feltValue))});
572 }
573 if (auto invOp = dyn_cast<felt::InvFeltOp>(op)) {
574 auto operand = lookup(invOp.getOperand(), scope);
575 if (!operand) {
576 return operand.takeError();
577 }
578 auto feltValue = asFelt(*operand);
579 if (!feltValue) {
580 return feltValue.takeError();
581 }
582 return bind({WitnessVal(field.inv(*feltValue))});
583 }
584 if (auto notOp = dyn_cast<felt::NotFeltOp>(op)) {
585 auto operand = lookup(notOp.getOperand(), scope);
586 if (!operand) {
587 return operand.takeError();
588 }
589 auto feltValue = asFelt(*operand);
590 if (!feltValue) {
591 return feltValue.takeError();
592 }
593 llvm::DynamicAPInt maxMask = (llvm::DynamicAPInt(1) << field.bitWidth()) - 1;
594 return bind({WitnessVal(field.reduce(maxMask ^ *feltValue))});
595 }
596 if (auto intToFeltOp = dyn_cast<cast::IntToFeltOp>(op)) {
597 const Field &resultField = intToFeltOp.getType().getField();
598 auto operand = lookup(intToFeltOp.getValue(), scope);
599 if (!operand) {
600 return operand.takeError();
601 }
602 if (std::holds_alternative<bool>(*operand)) {
603 auto result = cast::applyIntToFeltOverflow(
604 llvm::DynamicAPInt(std::get<bool>(*operand) ? 1 : 0), resultField,
605 intToFeltOp.getOverflow()
606 );
607 if (!result) {
608 return makeError("integer does not fit in felt");
609 }
610 return bind({WitnessVal(*result)});
611 }
612 auto integer = asIndex(*operand);
613 if (!integer) {
614 return integer.takeError();
615 }
616 auto result = cast::applyIntToFeltOverflow(
617 llvm::DynamicAPInt(*integer), resultField, intToFeltOp.getOverflow()
618 );
619 if (!result) {
620 return makeError("integer does not fit in felt");
621 }
622 return bind({WitnessVal(*result)});
623 }
624 if (auto feltToIndexOp = dyn_cast<cast::FeltToIndexOp>(op)) {
625 auto operand = lookup(feltToIndexOp.getValue(), scope);
626 if (!operand) {
627 return operand.takeError();
628 }
629 auto feltValue = asFelt(*operand);
630 if (!feltValue) {
631 return feltValue.takeError();
632 }
633 auto result = cast::applyFeltToIndexOverflow(
634 *feltValue, IndexType::kInternalStorageBitWidth, feltToIndexOp.getOverflow()
635 );
636 if (!result) {
637 return makeError("felt value does not fit in index");
638 }
639 return bind({WitnessVal(static_cast<int64_t>(toAPSInt(*result).getZExtValue()))});
640 }
641
642 if (auto structNewOp = dyn_cast<component::CreateStructOp>(op)) {
643 auto value = defaultValue(
644 structNewOp.getType(), tables, structNewOp.getOperation(), field, uninitializedBehavior,
645 &rng
646 );
647 if (!value) {
648 return value.takeError();
649 }
650 return bind({*value});
651 }
652 if (auto readMemberOp = dyn_cast<component::MemberReadOp>(op)) {
653 auto componentValue = lookup(readMemberOp.getComponent(), scope);
654 if (!componentValue) {
655 return componentValue.takeError();
656 }
657 auto structValue = asStruct(*componentValue);
658 if (!structValue) {
659 return structValue.takeError();
660 }
661 auto it = (*structValue)->members.find(readMemberOp.getMemberName());
662 if (it == (*structValue)->members.end()) {
663 return makeError("missing struct member");
664 }
665 return bind({it->second});
666 }
667 if (auto writeMemberOp = dyn_cast<component::MemberWriteOp>(op)) {
668 auto componentValue = lookup(writeMemberOp.getComponent(), scope);
669 auto memberValue = lookup(writeMemberOp.getVal(), scope);
670 if (!componentValue) {
671 return componentValue.takeError();
672 }
673 if (!memberValue) {
674 return memberValue.takeError();
675 }
676 auto structValue = asStruct(*componentValue);
677 if (!structValue) {
678 return structValue.takeError();
679 }
680 (*structValue)->members[writeMemberOp.getMemberName()] = *memberValue;
681 return BlockResult {};
682 }
683
684 if (auto newPodOp = dyn_cast<pod::NewPodOp>(op)) {
685 auto podValue = defaultValue(
686 newPodOp.getType(), tables, newPodOp.getOperation(), field, uninitializedBehavior, &rng
687 );
688 if (!podValue) {
689 return podValue.takeError();
690 }
691 auto podRef = asPod(*podValue);
692 if (!podRef) {
693 return podRef.takeError();
694 }
695 auto initValues = newPodOp.getInitializedRecordValues();
696 for (pod::RecordValue init : initValues) {
697 auto value = lookup(init.value, scope);
698 if (!value) {
699 return value.takeError();
700 }
701 (*podRef)->records[init.name] = *value;
702 }
703 return bind({*podRef});
704 }
705 if (auto readPodOp = dyn_cast<pod::ReadPodOp>(op)) {
706 auto podValue = lookup(readPodOp.getPodRef(), scope);
707 if (!podValue) {
708 return podValue.takeError();
709 }
710 auto podRef = asPod(*podValue);
711 if (!podRef) {
712 return podRef.takeError();
713 }
714 auto it = (*podRef)->records.find(readPodOp.getRecordName());
715 if (it == (*podRef)->records.end()) {
716 return makeError("missing pod record");
717 }
718 return bind({it->second});
719 }
720 if (auto writePodOp = dyn_cast<pod::WritePodOp>(op)) {
721 auto podValue = lookup(writePodOp.getPodRef(), scope);
722 auto recordValue = lookup(writePodOp.getValue(), scope);
723 if (!podValue) {
724 return podValue.takeError();
725 }
726 if (!recordValue) {
727 return recordValue.takeError();
728 }
729 auto podRef = asPod(*podValue);
730 if (!podRef) {
731 return podRef.takeError();
732 }
733 (*podRef)->records[writePodOp.getRecordName()] = *recordValue;
734 return BlockResult {};
735 }
736
737 if (auto arrayNewOp = dyn_cast<array::CreateArrayOp>(op)) {
738 auto arrayValue = std::make_shared<ArrayValue>();
739 arrayValue->type = arrayNewOp.getType();
740 if (arrayNewOp.getElements().empty()) {
741 auto elementCount =
742 getStaticShapeElementCount(arrayValue->type.getShape(), "array.create default value");
743 if (!elementCount) {
744 return elementCount.takeError();
745 }
746 arrayValue->elements.reserve(*elementCount);
747 for (size_t i = 0; i < *elementCount; ++i) {
748 auto elem = defaultValue(
749 arrayValue->type.getElementType(), tables, arrayNewOp.getOperation(), field,
750 uninitializedBehavior, &rng
751 );
752 if (!elem) {
753 return elem.takeError();
754 }
755 arrayValue->elements.push_back(*elem);
756 }
757 } else {
758 auto values = collectOperands(arrayNewOp.getElements(), scope);
759 if (!values) {
760 return values.takeError();
761 }
762 arrayValue->elements.assign(values->begin(), values->end());
763 }
764 return bind({arrayValue});
765 }
766 if (auto readArrayOp = dyn_cast<array::ReadArrayOp>(op)) {
767 auto arrayValue = lookup(readArrayOp.getArrRef(), scope);
768 if (!arrayValue) {
769 return arrayValue.takeError();
770 }
771 auto arrayRef = asArray(*arrayValue);
772 if (!arrayRef) {
773 return arrayRef.takeError();
774 }
775 llvm::SmallVector<int64_t> indices;
776 for (mlir::Value indexVal : readArrayOp.getIndices()) {
777 auto value = lookup(indexVal, scope);
778 if (!value) {
779 return value.takeError();
780 }
781 auto index = asIndex(*value);
782 if (!index) {
783 return index.takeError();
784 }
785 indices.push_back(*index);
786 }
787 auto offset =
788 checkedLinearize((*arrayRef)->type.getShape(), indices, "array index out of bounds");
789 if (!offset) {
790 return offset.takeError();
791 }
792 return bind({(*arrayRef)->elements[*offset]});
793 }
794 if (auto writeArrayOp = dyn_cast<array::WriteArrayOp>(op)) {
795 auto arrayValue = lookup(writeArrayOp.getArrRef(), scope);
796 auto rvalue = lookup(writeArrayOp.getRvalue(), scope);
797 if (!arrayValue) {
798 return arrayValue.takeError();
799 }
800 if (!rvalue) {
801 return rvalue.takeError();
802 }
803 auto arrayRef = asArray(*arrayValue);
804 if (!arrayRef) {
805 return arrayRef.takeError();
806 }
807 llvm::SmallVector<int64_t> indices;
808 for (mlir::Value indexVal : writeArrayOp.getIndices()) {
809 auto value = lookup(indexVal, scope);
810 if (!value) {
811 return value.takeError();
812 }
813 auto index = asIndex(*value);
814 if (!index) {
815 return index.takeError();
816 }
817 indices.push_back(*index);
818 }
819 auto offset =
820 checkedLinearize((*arrayRef)->type.getShape(), indices, "array index out of bounds");
821 if (!offset) {
822 return offset.takeError();
823 }
824 (*arrayRef)->elements[*offset] = *rvalue;
825 return BlockResult {};
826 }
827 if (auto extractArrayOp = dyn_cast<array::ExtractArrayOp>(op)) {
828 auto arrayValue = lookup(extractArrayOp.getArrRef(), scope);
829 if (!arrayValue) {
830 return arrayValue.takeError();
831 }
832 auto arrayRef = asArray(*arrayValue);
833 if (!arrayRef) {
834 return arrayRef.takeError();
835 }
836 llvm::SmallVector<int64_t> indices;
837 for (mlir::Value indexVal : extractArrayOp.getIndices()) {
838 auto value = lookup(indexVal, scope);
839 if (!value) {
840 return value.takeError();
841 }
842 auto index = asIndex(*value);
843 if (!index) {
844 return index.takeError();
845 }
846 indices.push_back(*index);
847 }
848 llvm::ArrayRef<int64_t> shape = (*arrayRef)->type.getShape();
849 if (indices.size() >= shape.size()) {
850 return makeError("array.extract indices exceed array rank");
851 }
852 auto subArraySize =
853 getStaticShapeElementCount(shape.drop_front(indices.size()), "array.extract shape");
854 if (!subArraySize) {
855 return subArraySize.takeError();
856 }
857 auto prefixOffset =
858 checkedLinearize(shape.take_front(indices.size()), indices, "array index out of bounds");
859 if (!prefixOffset) {
860 return prefixOffset.takeError();
861 }
862 bool baseOverflow = false;
863 size_t base = llvm::SaturatingMultiply(*prefixOffset, *subArraySize, &baseOverflow);
864 if (baseOverflow) {
865 return makeError("array.extract element offset would overflow size_t");
866 }
867 auto subArray = std::make_shared<ArrayValue>();
868 subArray->type = extractArrayOp.getType();
869 subArray->elements.reserve(*subArraySize);
870 for (size_t i = 0; i < *subArraySize; ++i) {
871 bool overflow = false;
872 size_t elementOffset = llvm::SaturatingAdd(base, i, &overflow);
873 if (overflow) {
874 return makeError("array.extract element offset would overflow size_t");
875 }
876 subArray->elements.push_back((*arrayRef)->elements[elementOffset]);
877 }
878 return bind({subArray});
879 }
880 if (auto insertArrayOp = dyn_cast<array::InsertArrayOp>(op)) {
881 auto arrayValue = lookup(insertArrayOp.getArrRef(), scope);
882 auto subArrayValue = lookup(insertArrayOp.getRvalue(), scope);
883 if (!arrayValue) {
884 return arrayValue.takeError();
885 }
886 if (!subArrayValue) {
887 return subArrayValue.takeError();
888 }
889 auto arrayRef = asArray(*arrayValue);
890 auto subArrayRef = asArray(*subArrayValue);
891 if (!arrayRef) {
892 return arrayRef.takeError();
893 }
894 if (!subArrayRef) {
895 return subArrayRef.takeError();
896 }
897 llvm::SmallVector<int64_t> indices;
898 for (mlir::Value indexVal : insertArrayOp.getIndices()) {
899 auto value = lookup(indexVal, scope);
900 if (!value) {
901 return value.takeError();
902 }
903 auto index = asIndex(*value);
904 if (!index) {
905 return index.takeError();
906 }
907 indices.push_back(*index);
908 }
909 llvm::ArrayRef<int64_t> shape = (*arrayRef)->type.getShape();
910 size_t subArraySize = (*subArrayRef)->elements.size();
911 auto prefixOffset =
912 checkedLinearize(shape.take_front(indices.size()), indices, "array index out of bounds");
913 if (!prefixOffset) {
914 return prefixOffset.takeError();
915 }
916 bool baseOverflow = false;
917 size_t base = llvm::SaturatingMultiply(*prefixOffset, subArraySize, &baseOverflow);
918 if (baseOverflow) {
919 return makeError("array.insert element offset would overflow size_t");
920 }
921 for (size_t i = 0; i < subArraySize; ++i) {
922 bool overflow = false;
923 size_t elementOffset = llvm::SaturatingAdd(base, i, &overflow);
924 if (overflow) {
925 return makeError("array.insert element offset would overflow size_t");
926 }
927 (*arrayRef)->elements[elementOffset] = (*subArrayRef)->elements[i];
928 }
929 return BlockResult {};
930 }
931 if (auto arrayLenOp = dyn_cast<array::ArrayLengthOp>(op)) {
932 auto dimValue = lookup(arrayLenOp.getDim(), scope);
933 if (!dimValue) {
934 return dimValue.takeError();
935 }
936 auto dim = asIndex(*dimValue);
937 if (!dim) {
938 return dim.takeError();
939 }
940 llvm::ArrayRef<int64_t> shape = arrayLenOp.getArrRefType().getShape();
941 auto dimIndex = checkedShapeDimToSize(*dim, "array.len dimension");
942 if (!dimIndex) {
943 return dimIndex.takeError();
944 }
945 if (*dimIndex >= shape.size()) {
946 return makeError("array.len dimension out of bounds");
947 }
948 return bind({WitnessVal(shape[*dimIndex])});
949 }
950
951 if (auto callOp = dyn_cast<function::CallOp>(op)) {
952 if (callOp.getTemplateParams() || !callOp.getMapOperands().empty()) {
953 return makeError("templated or affine-instantiated calls are not supported in llzk-witgen");
954 }
955 auto callee = lookupTopLevelSymbol<function::FuncDefOp>(tables, callOp.getCalleeAttr(), &op);
956 if (failed(callee)) {
957 return makeError("could not resolve called function");
958 }
959 auto args = collectOperands(callOp.getArgOperands(), scope);
960 if (!args) {
961 return args.takeError();
962 }
963 auto results = run(callee->get(), *args);
964 if (!results) {
965 return results.takeError();
966 }
967 return bind(*results);
968 }
969
970 auto handleBinaryIndex = [&](auto arithOp, auto fn) -> llvm::Expected<BlockResult> {
971 auto lhs = lookup(arithOp.getLhs(), scope);
972 auto rhs = lookup(arithOp.getRhs(), scope);
973 if (!lhs) {
974 return lhs.takeError();
975 }
976 if (!rhs) {
977 return rhs.takeError();
978 }
979 auto lhsValue = asIndex(*lhs);
980 if (!lhsValue) {
981 return lhsValue.takeError();
982 }
983 auto rhsValue = asIndex(*rhs);
984 if (!rhsValue) {
985 return rhsValue.takeError();
986 }
987 return bind({WitnessVal(fn(*lhsValue, *rhsValue))});
988 };
989
990 if (auto addIOp = dyn_cast<arith::AddIOp>(op)) {
991 return handleBinaryIndex(addIOp, [](int64_t lhs, int64_t rhs) { return lhs + rhs; });
992 }
993 if (auto subIOp = dyn_cast<arith::SubIOp>(op)) {
994 return handleBinaryIndex(subIOp, [](int64_t lhs, int64_t rhs) { return lhs - rhs; });
995 }
996 if (auto mulIOp = dyn_cast<arith::MulIOp>(op)) {
997 return handleBinaryIndex(mulIOp, [](int64_t lhs, int64_t rhs) { return lhs * rhs; });
998 }
999 if (auto divUIOp = dyn_cast<arith::DivUIOp>(op)) {
1000 return handleBinaryIndex(divUIOp, [](int64_t lhs, int64_t rhs) {
1001 // Unsigned division directly interprets int64 as unsigned value.
1002 auto divRes = static_cast<uint64_t>(lhs) / static_cast<uint64_t>(rhs);
1003 return static_cast<int64_t>(divRes);
1004 });
1005 }
1006 if (auto cmpIOp = dyn_cast<arith::CmpIOp>(op)) {
1007 return handleBinaryIndex(cmpIOp, [&cmpIOp](int64_t lhs, int64_t rhs) -> bool {
1008 switch (cmpIOp.getPredicate()) {
1009 case arith::CmpIPredicate::eq:
1010 return lhs == rhs;
1011 case arith::CmpIPredicate::ne:
1012 return lhs != rhs;
1013 case arith::CmpIPredicate::slt:
1014 return lhs < rhs;
1015 case arith::CmpIPredicate::sle:
1016 return lhs <= rhs;
1017 case arith::CmpIPredicate::sgt:
1018 return lhs > rhs;
1019 case arith::CmpIPredicate::sge:
1020 return lhs >= rhs;
1021 // Unsigned comparisons directly interprets int64 as unsigned value.
1022 case arith::CmpIPredicate::ult:
1023 return static_cast<uint64_t>(lhs) < static_cast<uint64_t>(rhs);
1024 case arith::CmpIPredicate::ule:
1025 return static_cast<uint64_t>(lhs) <= static_cast<uint64_t>(rhs);
1026 case arith::CmpIPredicate::ugt:
1027 return static_cast<uint64_t>(lhs) > static_cast<uint64_t>(rhs);
1028 case arith::CmpIPredicate::uge:
1029 return static_cast<uint64_t>(lhs) >= static_cast<uint64_t>(rhs);
1030 }
1031 llvm_unreachable("unknown comparison predicate");
1032 });
1033 }
1034
1035 if (auto selectOp = dyn_cast<arith::SelectOp>(op)) {
1036 auto cond = lookup(selectOp.getCondition(), scope);
1037 auto trueValue = lookup(selectOp.getTrueValue(), scope);
1038 auto falseValue = lookup(selectOp.getFalseValue(), scope);
1039 if (!cond) {
1040 return cond.takeError();
1041 }
1042 if (!trueValue) {
1043 return trueValue.takeError();
1044 }
1045 if (!falseValue) {
1046 return falseValue.takeError();
1047 }
1048 auto condition = asBool(*cond);
1049 if (!condition) {
1050 return condition.takeError();
1051 }
1052 return bind({*condition ? *trueValue : *falseValue});
1053 }
1054
1055 if (auto ifOp = dyn_cast<scf::IfOp>(op)) {
1056 auto cond = lookup(ifOp.getCondition(), scope);
1057 if (!cond) {
1058 return cond.takeError();
1059 }
1060 auto condition = asBool(*cond);
1061 if (!condition) {
1062 return condition.takeError();
1063 }
1064 if (!*condition && ifOp.getNumResults() == 0 && ifOp.getElseRegion().empty()) {
1065 return bind({});
1066 }
1067 Region &region = *condition ? ifOp.getThenRegion() : ifOp.getElseRegion();
1068 auto result = runRegion(region, {}, scope);
1069 if (!result) {
1070 return result.takeError();
1071 }
1072 return bind(result->values);
1073 }
1074
1075 if (auto forOp = dyn_cast<scf::ForOp>(op)) {
1076 auto lowerBoundValue = lookup(forOp.getLowerBound(), scope);
1077 auto upperBoundValue = lookup(forOp.getUpperBound(), scope);
1078 auto stepValue = lookup(forOp.getStep(), scope);
1079 if (!lowerBoundValue) {
1080 return lowerBoundValue.takeError();
1081 }
1082 if (!upperBoundValue) {
1083 return upperBoundValue.takeError();
1084 }
1085 if (!stepValue) {
1086 return stepValue.takeError();
1087 }
1088 auto lowerBound = asIndex(*lowerBoundValue);
1089 if (!lowerBound) {
1090 return lowerBound.takeError();
1091 }
1092 auto upperBound = asIndex(*upperBoundValue);
1093 if (!upperBound) {
1094 return upperBound.takeError();
1095 }
1096 auto step = asIndex(*stepValue);
1097 if (!step) {
1098 return step.takeError();
1099 }
1100 auto iterValuesOrErr = collectOperands(forOp.getInitArgs(), scope);
1101 if (!iterValuesOrErr) {
1102 return iterValuesOrErr.takeError();
1103 }
1104 llvm::SmallVector<WitnessVal> iterValues = std::move(*iterValuesOrErr);
1105
1106 if (usesUnsignedCmp(forOp)) {
1107 // Unsigned comparison directly interprets int64 as unsigned value.
1108 auto lowerBoundUIntValue = static_cast<uint64_t>(*lowerBound);
1109 auto upperBoundUIntValue = static_cast<uint64_t>(*upperBound);
1110 auto stepUInt = static_cast<uint64_t>(*step);
1111 for (uint64_t iv = lowerBoundUIntValue, ub = upperBoundUIntValue, unsignedStep = stepUInt;
1112 iv < ub; iv += unsignedStep) {
1113 auto signedIV = checkedCast<int64_t>(iv);
1114 if (!signedIV) {
1115 return signedIV.takeError();
1116 }
1117 llvm::SmallVector<WitnessVal> regionArgs;
1118 regionArgs.push_back(WitnessVal(*signedIV));
1119 regionArgs.append(iterValues.begin(), iterValues.end());
1120 auto result = runRegion(forOp.getRegion(), regionArgs, scope);
1121 if (!result) {
1122 return result.takeError();
1123 }
1124 iterValues = std::move(result->values);
1125 }
1126 } else {
1127 for (int64_t iv = *lowerBound; iv < *upperBound; iv += *step) {
1128 llvm::SmallVector<WitnessVal> regionArgs;
1129 regionArgs.push_back(WitnessVal(iv));
1130 regionArgs.append(iterValues.begin(), iterValues.end());
1131 auto result = runRegion(forOp.getRegion(), regionArgs, scope);
1132 if (!result) {
1133 return result.takeError();
1134 }
1135 iterValues = std::move(result->values);
1136 }
1137 }
1138 return bind(iterValues);
1139 }
1140
1141 if (auto whileOp = dyn_cast<scf::WhileOp>(op)) {
1142 auto iterValuesOrErr = collectOperands(whileOp.getInits(), scope);
1143 if (!iterValuesOrErr) {
1144 return iterValuesOrErr.takeError();
1145 }
1146 llvm::SmallVector<WitnessVal> iterValues = std::move(*iterValuesOrErr);
1147 while (true) {
1148 auto beforeResult = runRegion(whileOp.getBefore(), iterValues, scope);
1149 if (!beforeResult) {
1150 return beforeResult.takeError();
1151 }
1152 if (!beforeResult->terminated) {
1153 return makeError("scf.while before region must terminate with scf.condition");
1154 }
1155 if (beforeResult->values.empty()) {
1156 return makeError("scf.while before region did not produce a condition");
1157 }
1158
1159 auto condition = asBool(beforeResult->values.front());
1160 if (!condition) {
1161 return condition.takeError();
1162 }
1163
1164 llvm::SmallVector<WitnessVal> nextValues;
1165 nextValues.append(beforeResult->values.begin() + 1, beforeResult->values.end());
1166 if (!*condition) {
1167 return bind(nextValues);
1168 }
1169
1170 auto afterResult = runRegion(whileOp.getAfter(), nextValues, scope);
1171 if (!afterResult) {
1172 return afterResult.takeError();
1173 }
1174 if (!afterResult->terminated) {
1175 return makeError("scf.while after region must terminate with scf.yield");
1176 }
1177 iterValues = std::move(afterResult->values);
1178 }
1179 }
1180
1181 return makeError(llvm::Twine("unsupported op in llzk-witgen: ") + op.getName().getStringRef());
1182 }
1183};
1184
1185} // namespace
1186
1188llvm::Expected<llvm::SmallVector<WitnessVal>>
1189FunctionInterpreter::run(function::FuncDefOp funcOp, ArrayRef<WitnessVal> args) {
1190 return InvocationInterpreter(moduleOp, tables, field, uninitializedBehavior, rng)
1191 .run(funcOp, args);
1192}
1193
1194} // namespace llzk::witgen
This file implements helpers for DynamicAPInt operations and conversions that LLVM does not provide.
This file defines methods symbol lookup across LLZK operations and included files.
Information about the prime finite field used for the interval analysis.
Definition Field.h:36
::mlir::Region & getBody()
Definition Ops.h.inc:871
llvm::Expected< llvm::SmallVector< WitnessVal > > run(llzk::function::FuncDefOp funcOp, mlir::ArrayRef< WitnessVal > args)
Run a function with concrete arguments and return its result values.
FunctionInterpreter(mlir::ModuleOp moduleOp, mlir::SymbolTableCollection &tables, const llzk::Field &field, UninitializedBehavior uninitializedBehavior, std::mt19937_64 rng)
Build an interpreter for one module and field configuration.
std::optional< DynamicAPInt > applyIntToFeltOverflow(const DynamicAPInt &value, const Field &field, OverflowSemantics overflow)
std::optional< DynamicAPInt > applyFeltToIndexOverflow(const DynamicAPInt &value, unsigned indexBitWidth, OverflowSemantics overflow)
llvm::Expected< T > checkedCast(U u)
Definition WitgenUtils.h:28
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< bool > asBool(const WitnessVal &value)
Require a boolean value from the runtime variant.
UninitializedBehavior
Control how witgen materializes uninitialized/default values.
Definition ValueModel.h:55
llvm::Expected< size_t > getStaticShapeElementCount(llvm::ArrayRef< int64_t > shape, llvm::StringRef context)
Return the static element count for one shape, rejecting dynamic sizes.
llvm::Expected< WitnessVal > defaultValue(Type type, SymbolTableCollection &tables, Operation *origin, const Field &field, UninitializedBehavior behavior, std::mt19937_64 *rng)
Build a default value for a supported LLZK type.
llvm::Expected< int64_t > asIndex(const WitnessVal &value)
Require an index value from the runtime variant.
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< 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.
mlir::FailureOr< SymbolLookupResultUntyped > lookupTopLevelSymbol(mlir::SymbolTableCollection &tables, mlir::SymbolRefAttr symbol, mlir::Operation *origin, bool reportMissing=true)
APSInt toAPSInt(const DynamicAPInt &i)
DynamicAPInt modExp(const DynamicAPInt &base, const DynamicAPInt &exp, const DynamicAPInt &mod)
ExpressionValue notOp(const llvm::SMTSolverRef &solver, const ExpressionValue &val)