LLZK 3.0.0
An open-source IR for Zero Knowledge (ZK) circuits
Loading...
Searching...
No Matches
ConstraintFlatteningPass.cpp
Go to the documentation of this file.
1//===-- ConstraintFlatteningPass.cpp -----------------------------*- 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 "ConstraintBindings.h"
13#include "ConstraintStorage.h"
14
27
28#include <mlir/Dialect/Arith/IR/Arith.h>
29#include <mlir/Dialect/SCF/IR/SCF.h>
30#include <mlir/IR/IRMapping.h>
31#include <mlir/IR/Verifier.h>
32#include <mlir/Interfaces/SideEffectInterfaces.h>
33
34#include <llvm/ADT/DenseSet.h>
35#include <llvm/ADT/TypeSwitch.h>
38#define GEN_PASS_DEF_CONSTRAINTFLATTENINGPASS
40} // namespace llzk::polymorphic
41
42using namespace mlir;
43using namespace llzk;
44using namespace llzk::component;
45using namespace llzk::function;
46using namespace llzk::polymorphic;
48
49namespace {
50/// Expand structural control flow while leaving ordinary expressions as SSA.
51class ConstraintElaborator {
52 ModuleOp module;
54 OpBuilder builder;
55 SymbolTableCollection tables;
56 /// Detached output is committed only after expansion and validation succeed.
57 Region output;
60 std::optional<ConstraintStorage> storage;
63 llvm::DenseSet<Operation *> nonSignalReads;
64 SmallVector<FuncDefOp> activeCalls;
65
66 static bool aggregate(Type type) { return isa<array::ArrayType, pod::PodType>(type); }
67
69 static Value scalar(Value source, const Environment &frame) {
70 auto found = frame.find(source);
71 assert(found != frame.end() && "missing source binding");
72 const auto *value = std::get_if<Value>(&found->second);
73 assert(value && *value && "expected a scalar SSA binding");
74 return *value;
75 }
76
78 FailureOr<Value> readStorage(Value value, ArrayRef<Attribute> path, Type type, Operation *site) {
79 while (!path.empty()) {
80 if (auto arrayType = dyn_cast<array::ArrayType>(value.getType())) {
81 unsigned rank = arrayType.getShape().size();
82 if (path.size() < rank) {
83 return constraintError(site, "storage path does not identify a scalar leaf");
84 }
85 SmallVector<Value> indices;
86 for (Attribute step : path.take_front(rank)) {
87 auto index = dyn_cast<IntegerAttr>(step);
88 if (!index) {
89 return constraintError(site, "invalid array storage path");
90 }
91 indices.push_back(
92 arith::ConstantIndexOp::create(builder, site->getLoc(), index.getInt())
93 );
94 }
96 builder, site->getLoc(), arrayType.getElementType(), value, indices
97 );
98 path = path.drop_front(rank);
99 continue;
100 }
101 auto podType = dyn_cast<pod::PodType>(value.getType());
102 auto name = dyn_cast<StringAttr>(path.front());
103 if (!podType || !name) {
104 return constraintError(site, "invalid POD storage path");
105 }
106 auto recordType = podType.getRecord(name.getValue(), [site] {
107 return site->emitError(CONSTRAINT_DIAGNOSTIC_PREFIX);
108 });
109 if (failed(recordType)) {
110 return failure();
111 }
112 value = pod::ReadPodOp::create(builder, site->getLoc(), *recordType, value, name.getValue());
113 path = path.drop_front();
114 }
115 if (value.getType() != type) {
116 return constraintError(site, "storage leaf type mismatch");
117 }
118 auto info = storage->describe(value, site);
119 if (failed(info)) {
120 return failure();
121 }
122 if (!isa<StructType>(type) && !info->signal) {
123 nonSignalReads.insert(value.getDefiningOp());
124 }
125 return value;
126 }
127
128 FailureOr<Value> load(AccessPath path, Operation *site) {
129 return bindings.load(
130 path, site, [this](Value root, ArrayRef<Attribute> pathSteps, Type type, Operation *at) {
131 return readStorage(root, pathSteps, type, at);
132 }
133 );
134 }
135
137 FailureOr<BoundValue> copy(BoundValue source, Operation *site, Value input = {}) {
138 auto *path = std::get_if<AccessPath>(&source);
139 if (!path) {
140 return source;
141 }
142 auto target = bindings.create(path->type, input);
143 if (failed(transfer(target, *path, site))) {
144 return failure();
145 }
146 return BoundValue(target);
147 }
148
149 LogicalResult transfer(AccessPath target, AccessPath source, Operation *site) {
150 return bindings.transfer(
151 target, source, site,
152 [this](Value root, ArrayRef<Attribute> pathSteps, Type type, Operation *at) {
153 return readStorage(root, pathSteps, type, at);
154 }
155 );
156 }
157
159 LogicalResult assign(AccessPath target, BoundValue source, Operation *site) {
160 if (auto *value = std::get_if<Value>(&source)) {
161 bindings.store(target, *value);
162 return success();
163 }
164 return transfer(target, std::get<AccessPath>(source), site);
165 }
166
168 FailureOr<AccessPath>
169 arrayPath(Value source, ValueRange indices, Environment &frame, Operation *site) {
170 auto path = std::get<AccessPath>(frame.lookup(source));
171 for (Value index : indices) {
172 auto coordinate = facts.integer(scalar(index, frame));
173 if (!coordinate) {
174 return constraintError(site, "array path requires compile-time indices");
175 }
176 auto selected = bindings.select(path, builder.getIndexAttr(*coordinate), site);
177 if (failed(selected)) {
178 return failure();
179 }
180 path = *selected;
181 }
182 return path;
183 }
184
186 FailureOr<AccessPath> podPath(Value source, StringRef name, Environment &frame, Operation *site) {
187 auto path = std::get<AccessPath>(frame.lookup(source));
188 return bindings.select(path, builder.getStringAttr(name), site);
189 }
190
192 LogicalResult readPath(Value result, AccessPath path, Environment &frame, Operation *site) {
193 if (aggregate(path.type)) {
194 auto copied = copy(BoundValue(path), site);
195 if (failed(copied)) {
196 return failure();
197 }
198 frame[result] = *copied;
199 return success();
200 }
201 auto value = load(path, site);
202 if (failed(value)) {
203 return failure();
204 }
205 frame[result] = *value;
206 return success();
207 }
208
210 LogicalResult writePath(AccessPath path, Value value, Environment &frame, Operation *site) {
211 if (failed(bindings.writable(path, site))) {
212 return failure();
213 }
214 return assign(path, frame.lookup(value), site);
215 }
216
218 LogicalResult arrayNew(array::CreateArrayOp op, Environment &frame) {
219 auto path = bindings.create(op.getType());
220 frame[op.getResult()] = path;
221 if (op.getElements().empty()) {
222 return success();
223 }
224 unsigned next = 0;
225 return bindings.elements(path, op, [this, &op, &frame, &next](AccessPath item) {
226 return assign(item, frame.lookup(op.getElements()[next++]), op);
227 });
228 }
229
231 LogicalResult podNew(pod::NewPodOp op, Environment &frame) {
232 auto path = bindings.create(op.getType());
233 frame[op.getResult()] = path;
234 for (auto record : op.getInitializedRecordValues()) {
235 auto field = podPath(op.getResult(), record.name, frame, op);
236 if (failed(field) || failed(assign(*field, frame.lookup(record.value), op))) {
237 return failure();
238 }
239 }
240 return success();
241 }
242
244 LogicalResult memberRead(MemberReadOp op, Environment &frame) {
245 if (op.getTableOffset()) {
246 return constraintError(op, "member read requires concrete non-column component storage");
247 }
248 IRMapping mapping;
249 mapping.map(op.getComponent(), scalar(op.getComponent(), frame));
250 auto emitted = llvm::cast<MemberReadOp>(builder.clone(*op, mapping));
251 auto info = storage->describe(emitted.getResult(), op);
252 if (failed(info)) {
253 return failure();
254 }
255 if (aggregate(op.getType())) {
256 frame[op.getResult()] = bindings.create(info->memberType, {}, emitted.getResult());
257 } else {
258 frame[op.getResult()] = emitted.getResult();
259 if (!isa<StructType>(op.getType()) && !info->signal) {
260 nonSignalReads.insert(emitted);
261 }
262 }
263 return success();
264 }
265
267 LogicalResult globalRead(global::GlobalReadOp op, Environment &frame) {
269 if (failed(definition) || !definition->get().isConstant() ||
270 !definition->get().getInitialValue()) {
271 return constraintError(op, "global read requires an initialized constant");
272 }
273 Attribute initial = definition->get().getInitialValue();
274 if (!aggregate(op.getType())) {
275 auto value = constant(initial, op);
276 if (failed(value)) {
277 return failure();
278 }
279 frame[op.getResult()] = *value;
280 return success();
281 }
282 auto values = dyn_cast<ArrayAttr>(initial);
283 if (!values || !isa<array::ArrayType>(op.getType())) {
284 return constraintError(op, "unsupported aggregate global initializer");
285 }
286 auto path = bindings.create(op.getType());
287 frame[op.getResult()] = path;
288 unsigned next = 0;
289 return bindings.walk(path, op, [this, &op, values, &next](AccessPath item) -> LogicalResult {
290 if (next >= values.size()) {
291 return constraintError(op, "global initializer does not match aggregate leaves");
292 }
293 auto value = constant(values[next++], op);
294 if (failed(value)) {
295 return failure();
296 }
297 bindings.store(item, *value);
298 return success();
299 });
300 }
301
303 FailureOr<Value> constant(Attribute attribute, Operation *site) {
304 if (auto felt = dyn_cast<felt::FeltConstAttr>(attribute)) {
305 return Value(felt::FeltConstantOp::create(builder, site->getLoc(), felt));
306 }
307 if (auto integer = dyn_cast<IntegerAttr>(attribute)) {
308 return Value(arith::ConstantOp::create(builder, site->getLoc(), integer));
309 }
310 return constraintError(site, "unsupported scalar global initializer");
311 }
312
314 FailureOr<IRMapping> scalarMapping(Operation &op, const Environment &frame) {
315 IRMapping mapping;
316 for (Value operand : op.getOperands()) {
317 if (aggregate(operand.getType())) {
318 return constraintError(&op, "unsupported aggregate operand");
319 }
320 mapping.map(operand, scalar(operand, frame));
321 }
322 return mapping;
323 }
324
327 static bool isFieldExpression(Operation &op) {
328 return isa<
329 felt::FeltConstantOp, felt::AddFeltOp, felt::SubFeltOp, felt::MulFeltOp, felt::NegFeltOp>(
330 op
331 );
332 }
333
336 static bool isIntegerExpression(Operation &op) {
337 return !op.getNumRegions() && op.getNumResults() && isMemoryEffectFree(&op) &&
338 llvm::all_of(op.getResultTypes(), [](Type type) {
339 return isa<IndexType, IntegerType>(type);
340 });
341 }
342
345 LogicalResult emitStructural(Operation &op, IRMapping &mapping, Environment &frame) {
346 Operation *emitted = builder.clone(op, mapping);
347 for (auto [source, result] : llvm::zip_equal(op.getResults(), emitted->getResults())) {
348 if (auto constant = dyn_cast_or_null<IntegerAttr>(facts.known(result))) {
349 frame[source] = Value(arith::ConstantOp::create(builder, op.getLoc(), constant));
350 } else {
351 if (auto conversion = dyn_cast<cast::FeltToIndexOp>(emitted);
352 conversion && !isMemoryEffectFree(conversion)) {
353 return constraintError(&op, "assert cast requires a statically valid index conversion");
354 }
355 frame[source] = result;
356 }
357 }
358 // Keep the temporary operation until final dead-code removal: other folded
359 // results may still depend on it, and the facts cache retains its SSA keys.
360 return success();
361 }
362
365 LogicalResult evaluateNonNative(Operation &op, Environment &frame) {
366 if (op.getNumRegions() || !op.getNumResults() || !isMemoryEffectFree(&op) ||
367 !llvm::all_of(op.getResultTypes(), [](Type type) { return isa<felt::FeltType>(type); })) {
368 return constraintError(&op, "lower non-native operations before constraint flattening");
369 }
370 auto mapping = scalarMapping(op, frame);
371 if (failed(mapping)) {
372 return failure();
373 }
374 // A field left shift with a known amount is multiplication by 2^amount.
375 // Fold that factor using the shift's own modular semantics, including large
376 // unsigned shift representatives. Right shifts retain integer semantics.
377 if (auto shift = dyn_cast<felt::ShlFeltOp>(op); shift &&
378 !facts.known(mapping->lookup(shift.getLhs())) &&
379 facts.known(mapping->lookup(shift.getRhs()))) {
380 Value lhs = mapping->lookup(shift.getLhs());
381 auto one = felt::FeltConstAttr::get(
382 op.getContext(), llvm::DynamicAPInt(1), llvm::cast<felt::FeltType>(shift.getType())
383 );
384 mapping->map(shift.getLhs(), felt::FeltConstantOp::create(builder, op.getLoc(), one));
385 Operation *factor = builder.clone(op, *mapping);
386 auto constant = dyn_cast_or_null<felt::FeltConstAttr>(facts.known(factor->getResult(0)));
387 if (!constant) {
388 return constraintError(&op, "lower non-native operations before constraint flattening");
389 }
390 Value multiplier = felt::FeltConstantOp::create(builder, op.getLoc(), constant);
391 frame[shift.getResult()] =
392 Value(felt::MulFeltOp::create(builder, op.getLoc(), lhs, multiplier));
393 return success();
394 }
395 Operation *emitted = builder.clone(op, *mapping);
396 for (auto [source, result] : llvm::zip_equal(op.getResults(), emitted->getResults())) {
397 auto constant = dyn_cast_or_null<felt::FeltConstAttr>(facts.known(result));
398 if (!constant || constant.getType() != result.getType()) {
399 return constraintError(&op, "lower non-native operations before constraint flattening");
400 }
401 frame[source] = Value(felt::FeltConstantOp::create(builder, op.getLoc(), constant));
402 }
403 // As with structural computations, keep cached SSA keys alive until DCE.
404 return success();
405 }
406
408 LogicalResult emit(Operation &op, Environment &frame) {
409 if (auto assertion = dyn_cast<boolean::AssertOp>(op)) {
410 auto condition = facts.integer(scalar(assertion.getCondition(), frame));
411 return condition && *condition ? success()
412 : constraintError(&op, "assertion must be statically true");
413 }
414 bool equality = isa<constrain::EmitEqualityOp>(op);
415 if (equality && !isa<felt::FeltType>(op.getOperand(0).getType())) {
416 return constraintError(&op, "equality constraints require scalar felt operands");
417 }
418 bool structural = isIntegerExpression(op) || isa<cast::FeltToIndexOp>(op);
419 if (!(equality || structural || isFieldExpression(op) || isa<cast::IntToFeltOp>(op))) {
420 if (op.hasTrait<function::NotFieldNative>() || isa<felt::DivFeltOp>(op)) {
421 return evaluateNonNative(op, frame);
422 }
423 if (isa<constrain::EmitContainmentOp>(op)) {
424 return constraintError(
425 &op, "unsupported operation 'constrain.in': containment constraints require lowering "
426 "before constraint flattening"
427 );
428 }
429 if (llvm::any_of(op.getResultTypes(), [](Type type) { return isa<FloatType>(type); })) {
430 return constraintError(
431 &op, "unsupported: floating-point results are not supported in flattened constraints"
432 );
433 }
434 return constraintError(
435 &op, "unsupported operation '" + op.getName().getStringRef() +
436 "': expected a supported field expression or pure integer computation"
437 );
438 }
439 auto mapping = scalarMapping(op, frame);
440 if (failed(mapping)) {
441 return failure();
442 }
443 if (structural) {
444 return emitStructural(op, *mapping, frame);
445 }
446 Operation *emitted = builder.clone(op, *mapping);
447 for (auto [source, result] : llvm::zip_equal(op.getResults(), emitted->getResults())) {
448 frame[source] = result;
449 }
450 return success();
451 }
452
453 static void bind(ValueRange arguments, ArrayRef<BoundValue> values, Environment &frame) {
454 for (auto [argument, value] : llvm::zip_equal(arguments, values)) {
455 frame[argument] = value;
456 }
457 }
458
461 LogicalResult invoke(
462 FuncDefOp callee, ArrayRef<BoundValue> arguments, Operation *site,
463 SmallVectorImpl<BoundValue> &results
464 ) {
465 if (!callee || !llvm::hasSingleElement(callee.getBody()) || isInTemplate(callee)) {
466 return constraintError(site, "callee must be concrete, defined, and single-block");
467 }
468 if (llvm::is_contained(activeCalls, callee)) {
469 return constraintError(site, "recursive calls are unsupported");
470 }
471 if (callee.getNumArguments() != arguments.size()) {
472 return constraintError(site, "callee argument arity mismatch");
473 }
474 if (activeCalls.size() >= 256) {
475 return constraintError(site, "call nesting limit exceeded");
476 }
477 Environment frame;
478 for (auto [parameter, argument] : llvm::zip_equal(callee.getArguments(), arguments)) {
479 frame[parameter] = argument;
480 if (const auto *path = std::get_if<AccessPath>(&argument)) {
481 bindings.protect(*path);
482 }
483 }
484 activeCalls.push_back(callee);
485 auto status = expand(callee.getBody().front(), frame, results);
486 activeCalls.pop_back();
487 for (const auto &argument : arguments) {
488 if (const auto *path = std::get_if<AccessPath>(&argument)) {
489 bindings.unprotect(*path);
490 }
491 }
492 return status;
493 }
494
496 LogicalResult call(CallOp op, Environment &frame) {
497 SmallVector<BoundValue> arguments;
498 for (Value argument : op.getArgOperands()) {
499 arguments.push_back(frame.lookup(argument));
500 }
501 FuncDefOp target;
502 if (!arguments.empty() && op.calleeIsStructConstrain()) {
503 auto info = storage->describe(std::get<Value>(arguments.front()), op);
504 if (failed(info)) {
505 return failure();
506 }
507 auto owner = info->definition;
508 target = owner ? owner.getConstrainFuncOp() : FuncDefOp();
509 } else {
510 auto found = lookupTopLevelSymbol<FuncDefOp>(tables, op.getCalleeAttr(), op);
511 if (failed(found)) {
512 return failure();
513 }
514 target = found->get();
515 }
516 SmallVector<BoundValue> results;
517 if (failed(invoke(target, arguments, op, results))) {
518 return failure();
519 }
520 if (results.size() != op.getNumResults()) {
521 return constraintError(op, "callee result arity mismatch");
522 }
523 bind(op.getResults(), results, frame);
524 return success();
525 }
526
528 LogicalResult branch(scf::IfOp op, Environment &frame) {
529 auto condition = facts.integer(scalar(op.getCondition(), frame));
530 if (!condition) {
531 return constraintError(
532 op, "symbolic i1 condition requires an explicit field selector representation"
533 );
534 }
535 auto &region = *condition ? op.getThenRegion() : op.getElseRegion();
536 SmallVector<BoundValue> results;
537 if (!region.empty() && failed(expand(region.front(), frame, results))) {
538 return failure();
539 }
540 bind(op.getResults(), results, frame);
541 return success();
542 }
543
545 LogicalResult forLoop(scf::ForOp op, Environment &frame) {
546 auto lower = facts.integer(scalar(op.getLowerBound(), frame));
547 auto upper = facts.integer(scalar(op.getUpperBound(), frame));
548 auto step = facts.integer(scalar(op.getStep(), frame));
549 if (!lower || !upper || !step || *step <= 0 || op.getUnsignedCmp()) {
550 return constraintError(op, "requires signed static for bounds and a positive step");
551 }
552 SmallVector<BoundValue> carried;
553 for (Value value : op.getInitArgs()) {
554 carried.push_back(frame.lookup(value));
555 }
556 for (int64_t i = *lower; i < *upper;) {
557 frame[op.getInductionVar()] = Value(
558 arith::ConstantOp::create(
559 builder, op.getLoc(), builder.getIntegerAttr(op.getInductionVar().getType(), i)
560 )
561 );
562 bind(op.getRegionIterArgs(), carried, frame);
563 carried.clear();
564 if (failed(expand(*op.getBody(), frame, carried))) {
565 return failure();
566 }
567 if (!hasNextStaticIteration(i, *upper, *step)) {
568 break;
569 }
570 i += *step;
571 }
572 bind(op.getResults(), carried, frame);
573 return success();
574 }
575
577 LogicalResult whileLoop(scf::WhileOp op, Environment &frame) {
578 if (isInvariantTrueCondition(op.getConditionOp().getCondition())) {
579 return constraintError(op, "cannot execute an always-true while loop");
580 }
581 SmallVector<BoundValue> carried;
582 for (Value value : op.getInits()) {
583 carried.push_back(frame.lookup(value));
584 }
585 while (true) {
586 bind(op.getBeforeArguments(), carried, frame);
587 SmallVector<BoundValue> condition;
588 if (failed(expand(op.getBefore().front(), frame, condition))) {
589 return failure();
590 }
591 auto test = facts.integer(std::get<Value>(condition.front()));
592 if (!test) {
593 return constraintError(op, "while condition must be statically known");
594 }
595 carried.assign(condition.begin() + 1, condition.end());
596 if (!*test) {
597 break;
598 }
599 bind(op.getAfterArguments(), carried, frame);
600 carried.clear();
601 if (failed(expand(op.getAfter().front(), frame, carried))) {
602 return failure();
603 }
604 }
605 bind(op.getResults(), carried, frame);
606 return success();
607 }
608
610 template <typename ReadOp> LogicalResult arrayRead(ReadOp op, Environment &frame) {
611 auto path = arrayPath(op.getArrRef(), op.getIndices(), frame, op);
612 if (failed(path)) {
613 return failure();
614 }
615 return readPath(op.getResult(), *path, frame, op);
616 }
617
619 template <typename WriteOp> LogicalResult arrayWrite(WriteOp op, Environment &frame) {
620 auto path = arrayPath(op.getArrRef(), op.getIndices(), frame, op);
621 if (failed(path)) {
622 return failure();
623 }
624 return writePath(*path, op.getRvalue(), frame, op);
625 }
626
628 LogicalResult podRead(pod::ReadPodOp op, Environment &frame) {
629 auto path = podPath(op.getPodRef(), op.getRecordName(), frame, op);
630 if (failed(path)) {
631 return failure();
632 }
633 return readPath(op.getResult(), *path, frame, op);
634 }
635
637 LogicalResult podWrite(pod::WritePodOp op, Environment &frame) {
638 auto path = podPath(op.getPodRef(), op.getRecordName(), frame, op);
639 if (failed(path)) {
640 return failure();
641 }
642 return writePath(*path, op.getValue(), frame, op);
643 }
644
646 LogicalResult arrayLength(array::ArrayLengthOp op, Environment &frame) {
647 auto path = std::get<AccessPath>(frame.lookup(op.getArrRef()));
648 auto type = dyn_cast<array::ArrayType>(path.type);
649 auto dimension = facts.integer(scalar(op.getDim(), frame));
650 if (!type || !dimension || *dimension < 0 || uint64_t(*dimension) >= type.getShape().size()) {
651 return constraintError(op, "array dimension must be statically known and in bounds");
652 }
653 int64_t size = type.getShape()[*dimension];
654 if (size < 0) {
655 return constraintError(op, "aggregate shape must be static");
656 }
657 frame[op.getResult()] = Value(arith::ConstantIndexOp::create(builder, op.getLoc(), size));
658 return success();
659 }
660
662 LogicalResult select(arith::SelectOp op, Environment &frame) {
663 auto condition = facts.integer(scalar(op.getCondition(), frame));
664 if (!condition) {
665 return constraintError(
666 op, "symbolic select requires an explicit field selector representation"
667 );
668 }
669 frame[op.getResult()] = frame.lookup(*condition ? op.getTrueValue() : op.getFalseValue());
670 return success();
671 }
672
675 LogicalResult dispatch(Operation &op, Environment &frame) {
676 return llvm::TypeSwitch<Operation *, LogicalResult>(&op)
677 .Case<scf::IfOp>([this, &frame](auto control) { return branch(control, frame); })
678 .Case<scf::ForOp>([this, &frame](auto control) { return forLoop(control, frame); })
679 .Case<scf::WhileOp>([this, &frame](auto control) { return whileLoop(control, frame); })
680 .Case<CallOp>([this, &frame](auto function) { return call(function, frame); })
681 .Case<arith::SelectOp>([this, &frame](auto choice) { return select(choice, frame); })
682 .Case<UnifiableCastOp>([&frame](auto conversion) {
683 frame[conversion.getResult()] = frame.lookup(conversion.getInput());
684 return success();
685 })
686 .Case<global::GlobalReadOp>([this, &frame](auto read) { return globalRead(read, frame); })
687 .Case<MemberReadOp>([this, &frame](auto read) { return memberRead(read, frame); })
688 .Case<MemberWriteOp>([](auto write) {
689 return constraintError(write, "cannot write to original component storage");
690 })
691 .Case<array::CreateArrayOp>([this, &frame](auto allocation) {
692 return arrayNew(allocation, frame);
693 })
694 .Case<NonDetOp>([this, &frame](auto allocation) {
695 if (!aggregate(allocation.getType())) {
696 return emit(*allocation, frame);
697 }
698 // Uninitialized aggregate leaves may be overwritten but never read.
699 frame[allocation.getResult()] = bindings.create(allocation.getType());
700 return success();
701 })
702 .Case<pod::NewPodOp>([this, &frame](auto allocation) { return podNew(allocation, frame); })
703 .Case<array::ReadArrayOp, array::ExtractArrayOp>([this, &frame](auto read) {
704 return arrayRead(read, frame);
705 })
706 .Case<array::WriteArrayOp, array::InsertArrayOp>([this, &frame](auto write) {
707 return arrayWrite(write, frame);
708 })
709 .Case<pod::ReadPodOp>([this, &frame](auto read) { return podRead(read, frame); })
710 .Case<pod::WritePodOp>([this, &frame](auto write) { return podWrite(write, frame); })
711 .Case<array::ArrayLengthOp>([this, &frame](auto length) {
712 return arrayLength(length, frame);
713 }).Default([this, &frame](Operation *ordinary) { return emit(*ordinary, frame); });
714 }
715
717 LogicalResult expand(Block &block, Environment &frame, SmallVectorImpl<BoundValue> &results) {
718 for (Operation &op : block) {
719 if (isa<ReturnOp, scf::YieldOp, scf::ConditionOp>(op)) {
720 for (Value value : op.getOperands()) {
721 results.push_back(frame.lookup(value));
722 }
723 return success();
724 }
725 if (failed(dispatch(op, frame))) {
726 return failure();
727 }
728 }
729 return constraintError(block.getParentOp(), "missing supported terminator");
730 }
731
734 LogicalResult validateOperation(Operation &op) {
735 if (op.getNumRegions()) {
736 return constraintError(&op, "regions are unsupported in flattened constraints");
737 }
738 if (isFieldExpression(op) || isa<constrain::EmitEqualityOp>(op)) {
739 auto fieldType = [](Type type) { return isa<felt::FeltType>(type); };
740 if (llvm::all_of(op.getOperandTypes(), fieldType) &&
741 llvm::all_of(op.getResultTypes(), fieldType)) {
742 return success();
743 }
744 return constraintError(
745 &op, "constraint expressions require scalar felt operands and results"
746 );
747 }
748 if (auto constant = dyn_cast<arith::ConstantOp>(op)) {
749 Type type = constant.getType();
750 if (type.isIndex() || type.isInteger(1)) {
751 return success();
752 }
753 }
754 if (auto conversion = dyn_cast<cast::IntToFeltOp>(op)) {
755 Attribute literal;
756 if (matchPattern(conversion.getValue(), m_Constant(&literal)) && isa<IntegerAttr>(literal)) {
757 if (!isMemoryEffectFree(conversion) && !facts.known(conversion.getResult())) {
758 return constraintError(&op, "assert cast requires a statically valid field conversion");
759 }
760 return success();
761 }
762 return constraintError(&op, "field conversion requires a compile-time integer");
763 }
764 if (isa<array::ReadArrayOp, array::ExtractArrayOp>(op)) {
765 ValueRange indices = llvm::cast<array::ArrayAccessOpInterface>(op).getIndices();
766 for (Value index : indices) {
767 if (!index.getDefiningOp<arith::ConstantOp>()) {
768 return constraintError(&op, "array path requires compile-time indices");
769 }
770 }
771 return success();
772 }
773 if (auto read = dyn_cast<MemberReadOp>(op)) {
774 if (read.getTableOffset()) {
775 return constraintError(&op, "member read requires concrete non-column component storage");
776 }
777 return success();
778 }
779 if (isa<pod::ReadPodOp>(op)) {
780 return success();
781 }
782 if (isIntegerExpression(op)) {
783 return constraintError(&op, "structural operation requires a compile-time value");
784 }
785 return constraintError(
786 &op, "unsupported operation in flattened constraints: '" + op.getName().getStringRef() + "'"
787 );
788 }
789
791 LogicalResult validateOutput() {
792 for (Operation &op : llvm::make_early_inc_range(llvm::reverse(output.front()))) {
793 // A successful cast fold proves its range assertion, so the temporary
794 // cast can be removed despite its conservative effect interface.
795 bool foldedIndexCast = isa<cast::FeltToIndexOp>(op) && facts.known(op.getResult(0));
796 if (op.use_empty() &&
797 (foldedIndexCast || isMemoryEffectFree(&op) ||
798 isa<MemberReadOp, array::ReadArrayOp, array::ExtractArrayOp, pod::ReadPodOp>(op))) {
799 op.erase();
800 }
801 }
802 for (Operation &op : output.front()) {
803 if (failed(validateOperation(op))) {
804 return failure();
805 }
806 if (nonSignalReads.contains(&op)) {
807 return constraintError(
808 &op, "live unresolved non-signal scalar storage; algebraic elimination is required"
809 );
810 }
811 }
812 return success();
813 }
814
816 LogicalResult commit(FuncDefOp source) {
817 Region original;
818 original.takeBody(source.getBody());
819 source.getBody().takeBody(output);
820 if (failed(verify(source))) {
821 output.takeBody(source.getBody());
822 source.getBody().takeBody(original);
823 return failure();
824 }
825 return success();
826 }
827
828public:
829 explicit ConstraintElaborator(ModuleOp root)
830 : module(root), builder(root.getContext()), bindings(root.getContext()) {}
831
833 LogicalResult run() {
834 auto root = getRootStructDef(tables, module);
835 if (failed(root)) {
836 return failure();
837 }
838 if (!*root) {
839 return constraintError(module, "requires llzk.main");
840 }
841 main = root->get();
842 storage.emplace(module, main, builder, tables, facts);
843 auto source = main.getConstrainFuncOp();
844 if (isInTemplate(main) || !source || !llvm::hasSingleElement(source.getBody())) {
845 return constraintError(main, "main must have a concrete single-block constrain function");
846 }
847 output.push_back(new Block());
848 SmallVector<BoundValue> arguments;
849 for (auto [i, argument] : llvm::enumerate(source.getArguments())) {
850 Value emitted = output.front().addArgument(argument.getType(), argument.getLoc());
851 if (aggregate(argument.getType())) {
852 arguments.push_back(bindings.create(argument.getType(), argument, emitted));
853 } else {
854 arguments.push_back(emitted);
855 }
856 }
857 builder.setInsertionPointToEnd(&output.front());
858 SmallVector<BoundValue> results;
859 if (failed(invoke(source, arguments, source, results)) || failed(validateOutput())) {
860 return failure();
861 }
862 ReturnOp::create(builder, source.getLoc());
863 return commit(source);
864 }
865};
866
867class PassImpl : public llzk::polymorphic::impl::ConstraintFlatteningPassBase<PassImpl> {
868 using Base = llzk::polymorphic::impl::ConstraintFlatteningPassBase<PassImpl>;
869 using Base::Base;
870
871 void runOnOperation() override {
872 if (failed(ConstraintElaborator(getOperation()).run())) {
873 signalPassFailure();
874 }
875 }
876};
877} // namespace
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for and distribution as defined by Sections through of this document Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License Legal Entity shall mean the union of the acting entity and all other entities that control are controlled by or are under common control with that entity For the purposes of this definition control direct or to cause the direction or management of such whether by contract or including but not limited to software source documentation and configuration files Object form shall mean any form resulting from mechanical transformation or translation of a Source including but not limited to compiled object generated and conversions to other media types Work shall mean the work of whether in Source or Object made available under the as indicated by a copyright notice that is included in or attached to the whether in Source or Object that is based or other modifications as a an original work of authorship For the purposes of this Derivative Works shall not include works that remain separable or merely the Work and Derivative Works thereof Contribution shall mean any work of including the original version of the Work and any modifications or additions to that Work or Derivative Works that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner For the purposes of this definition
Definition LICENSE.txt:53
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for and distribution as defined by Sections through of this document Licensor shall mean the copyright owner or entity authorized by the copyright owner that is granting the License Legal Entity shall mean the union of the acting entity and all other entities that control are controlled by or are under common control with that entity For the purposes of this definition control direct or to cause the direction or management of such whether by contract or including but not limited to software source documentation source
Definition LICENSE.txt:28
::mlir::TypedValue<::llzk::array::ArrayType > getArrRef()
Definition Ops.h.inc:318
::mlir::TypedValue<::mlir::IndexType > getDim()
Definition Ops.h.inc:322
::mlir::TypedValue<::llzk::array::ArrayType > getResult()
Definition Ops.h.inc:612
::mlir::Operation::operand_range getElements()
Definition Ops.h.inc:592
static ReadArrayOp create(::mlir::OpBuilder &builder, ::mlir::Location location, ::mlir::Type result, ::mlir::Value arr_ref, ::mlir::ValueRange indices)
Definition Ops.cpp.inc:1758
::mlir::TypedValue<::llzk::component::StructType > getComponent()
Definition Ops.h.inc:855
::std::optional<::mlir::Attribute > getTableOffset()
Definition Ops.cpp.inc:1132
static FeltConstantOp create(::mlir::OpBuilder &builder, ::mlir::Location location, ::mlir::Type result, ::llzk::felt::FeltConstAttr value)
Definition Ops.cpp.inc:1274
static MulFeltOp create(::mlir::OpBuilder &builder, ::mlir::Location location, ::mlir::Type result, ::mlir::Value lhs, ::mlir::Value rhs)
Definition Ops.cpp.inc:1922
bool calleeIsStructConstrain()
Return true iff the callee function name is FUNC_NAME_CONSTRAIN within a StructDefOp.
Definition Ops.cpp:1004
::mlir::SymbolRefAttr getCalleeAttr()
Definition Ops.h.inc:456
::mlir::Operation::operand_range getArgOperands()
Definition Ops.h.inc:430
::mlir::Region & getBody()
Definition Ops.h.inc:871
static ReturnOp create(::mlir::OpBuilder &builder, ::mlir::Location location)
Definition Ops.cpp.inc:1666
::mlir::SymbolRefAttr getNameRefAttr()
Definition Ops.h.inc:563
::mlir::SmallVector<::llzk::pod::RecordValue > getInitializedRecordValues()
Definition Ops.cpp:540
::mlir::TypedValue<::llzk::pod::PodType > getResult()
Definition Ops.h.inc:345
::llvm::StringRef getRecordName()
Definition Ops.cpp.inc:769
::mlir::TypedValue<::mlir::Type > getResult()
Definition Ops.h.inc:601
::mlir::TypedValue<::llzk::pod::PodType > getPodRef()
Definition Ops.h.inc:582
static ReadPodOp create(::mlir::OpBuilder &builder, ::mlir::Location location, ::mlir::Type result, ::mlir::Value pod_ref, ::mlir::StringAttr record_name)
Definition Ops.cpp.inc:784
::mlir::TypedValue<::mlir::Type > getValue()
Definition Ops.h.inc:838
::mlir::TypedValue<::llzk::pod::PodType > getPodRef()
Definition Ops.h.inc:834
::llvm::StringRef getRecordName()
Definition Ops.cpp.inc:1171
Bind scalar leaves without constructing a shadow array or POD object.
Evaluate scalar dependencies of structural decisions, assertions, and constant arithmetic.
int main(int argc, char **argv)
Common interface for constant parameter-like symbol operations that can be defined within a TemplateO...
mlir::LogicalResult constraintError(mlir::Operation *site, const llvm::Twine &message)
Emit the common diagnostic prefix for constraint expansion and storage helpers.
std::variant< mlir::Value, AccessPath > BoundValue
Scalar expressions are SSA; aggregates are paths, never emitted allocations.
llvm::DenseMap< mlir::Value, BoundValue > Environment
bool isInvariantTrueCondition(mlir::Value condition)
Prove a Boolean condition true without any loop-carried bindings.
bool hasNextStaticIteration(int64_t current, int64_t upper, int64_t step)
Whether another signed iteration exists after current, for current < upper and a positive step.
Definition StaticLoop.h:19
bool isInTemplate(Operation *op)
Definition Ops.cpp:34
mlir::FailureOr< SymbolLookupResultUntyped > lookupTopLevelSymbol(mlir::SymbolTableCollection &tables, mlir::SymbolRefAttr symbol, mlir::Operation *origin, bool reportMissing=true)
ConstantCapture m_Constant()
Definition Matchers.h:89
FailureOr< SymbolLookupResult< StructDefOp > > getRootStructDef(SymbolTableCollection &tables, Operation *lookupFrom, StringRef symbolPath)