[OM] Introduce explicit evaluator state and op patterns#10265
Conversation
3483b6c to
75fd19d
Compare
Refactor the OM evaluator around explicit Ready/Pending/Failure state tracking and a shared operation pattern framework. Centralize reference resolution, operand readiness/unknown propagation, and placeholder creation, while moving op-specific logic into typed patterns. Also add the supporting evaluator regressions for unknown nested fields and reference/value propagation.
75fd19d to
6123679
Compare
af62281 to
55bdc02
Compare
55bdc02 to
a82bb7b
Compare
| auto result = createUnknownValue(op.getType(), loc); | ||
| if (failed(result)) | ||
| return ResolvedValue::failure(); | ||
| return resolveValueState(result.value()); |
There was a problem hiding this comment.
| auto result = createUnknownValue(op.getType(), loc); | |
| if (failed(result)) | |
| return ResolvedValue::failure(); | |
| return resolveValueState(result.value()); | |
| return createUnknownValue(op.getType(), loc); |
| return evaluateObjectInstance(op.getClassNameAttr(), params.value(), loc, | ||
| {op, actualParams}); | ||
| return ResolvedValue::failure(); | ||
| auto result = evaluateObjectInstance(op.getClassNameAttr(), params.value(), |
There was a problem hiding this comment.
| auto result = evaluateObjectInstance(op.getClassNameAttr(), params.value(), | |
| return evaluateObjectInstance(op.getClassNameAttr(), params.value(), |
| foldedAttr = attr; | ||
| } else | ||
| return op->emitError( | ||
| "folder returned operands even though all operands are constant, " |
|
Another direction might be to switch to clone IR and actually mutate/flatten IR instead of using in-memory graph. In this approach we can completely eliminate the notion of scheduling (after flattening all objects there should not be any reference). In this case we can implement the evaluation system with (greedy) pattern rewriter nicely. |
651b934 to
a82bb7b
Compare
| /// The evaluator tracks two different things: | ||
| /// | ||
| /// 1. Local state of one value object: `isSettled()` | ||
| /// | ||
| /// false -> this value object may still change | ||
| /// true -> this value object has finished its own local work | ||
| /// | ||
| /// 2. State of using a value handle: `ResolutionState` | ||
| /// | ||
| /// Pending -> the handle still cannot be used | ||
| /// Ready -> the handle can be used now | ||
| /// Failure -> evaluation hit a hard error | ||
| /// | ||
| /// These are not the same thing. A reference may itself be settled, but using | ||
| /// the handle may still be pending: | ||
| /// | ||
| /// ReferenceValue (settled = true) | ||
| /// | | ||
| /// v | ||
| /// pending target | ||
| /// | ||
| /// So `isSettled()` is about one value object, while `ResolvedValue` is about | ||
| /// whether the whole handle is usable. |
There was a problem hiding this comment.
I'm not following this... So, if a value is settled and it has a single user, under what circumstances would the mean that the user is not ready? Is this only an accounting issue of the user needs to be updated from pending (it's current state) to ready (it's next state)?
There was a problem hiding this comment.
Yes, that's possible under the current complicated state transition, unfortunately mostly because of Referencevalue.
Fox example if reference value point to add(x, y) reference value itself would be settled when the reference value reaches add(x, y). However add(x, y) may not be fully evaluated, so referecen(add(x, y)) is settled but not ready.
I think the majority of complexity came from (1) the current in-memory representation about reference value, (2) object field allowing nested fields.
So I'm inclined to pursue the approach to actually mutate the IR, instead of using in-memory representation (#10265 (comment)) since we cannot avoid this complicated scheduling otherwise. This file is the IR I'm experimenting (https://github.com/uenoku/circt/blob/dev/hidetou/evaluator-framework-2/test/Dialect/OM/elaborate-object.mlir) that basically introduces om.elaborated_object operation (https://github.com/uenoku/circt/blob/ebc4122876cb184e193cd7e02631d51b08a6dbcb/include/circt/Dialect/OM/OMOps.td#L209).
There was a problem hiding this comment.
I kind of like the IR mutation strategy only because it's simpler and I can therefore understand it (and it doesn't require lots of debugging logging to figure out when it goes wrong). 😅
There was a problem hiding this comment.
Yes, I think that's better. Though it's necessary to check that the IR mutation approach properly scales for large design, as essentially it flattens entire objects into a single block.
There was a problem hiding this comment.
Yes, fair. It has to visit every object uniquely. It is possible that the final structure could be very compact, though, as long as unused/unknown structure is discarded.
| struct ResolvedValue { | ||
| /// `state` says whether `value` can be used. `value` keeps the original | ||
| /// handle so callers can keep passing it around even when it is still pending | ||
| /// or has already failed. | ||
| ResolutionState state; | ||
| EvaluatorValuePtr value; |
There was a problem hiding this comment.
This is looking a lot like llvm::PointerUnion or llvm::PointerIntPair.
I realize that EvaluatorValuePtr is using std::shared_ptr which makes this likely not workable. There may be an alternative formulation that can be more memory efficient here.
| auto result = op.evaluateIntegerOperation(lhsVal, rhsVal); | ||
| if (failed(result)) | ||
| return {}; |
There was a problem hiding this comment.
Maybe the indirection through foldIntegerBinaryArithmetic and evaluateIntegerOperation could just be removed here in favor of directly evaluating the fold member function, similar to how FIRRTL IMCP is implemented.
This is going beyond the more pure refactor nature of this PR, though.
| TEST(EvaluatorTests, StringConcatSingleOperand) { | ||
| const char *mod = R"MLIR( | ||
| module { | ||
| om.class @Test() -> (result: !om.string) { | ||
| %0 = om.constant "Hello" : !om.string | ||
| %1 = om.string.concat %0 : !om.string | ||
| om.class.fields %1 : !om.string | ||
| } | ||
| } | ||
| )MLIR"; |
There was a problem hiding this comment.
Nit/thought: this isn't consistent with the other tests in this file, but it may be cleaner to batch all of the string concat tests into one Google Test TEST (StringConcat) and then do multiple ASSERT_* calls in that.
| auto fieldValue = llvm::cast<evaluator::ObjectValue>(result.value().get()) | ||
| ->getField("result") | ||
| .value(); | ||
|
|
||
| ASSERT_EQ("Hello", llvm::cast<evaluator::AttributeValue>(fieldValue.get()) | ||
| ->getAs<StringAttr>() | ||
| .getValue()); |
There was a problem hiding this comment.
Nit: the pattern of llvm::cast<evaluator::ObjectValue>(...)->getField(...).value() followed by a whole additional cast to an attribute value, feels like there is a missing templated getFieldAs<T> to allow for getFieldAs<StringAttr> to avoid some of the long cast chains.
Unrelated to the PR, though.
| om.class @Domain(%in: !om.string) -> (out: !om.string) { | ||
| om.class.fields %in : !om.string | ||
| } | ||
| om.class @Foo_Class(%basepath: !om.frozenbasepath) -> (test1: i1, test2: i1) { |
There was a problem hiding this comment.
Nit: This can just be @Foo. The @Foo_Class is only really relevant if testing the "FIRRTL Property ABI". I do realize that this is my copy-pasted issue. 😉
…ot (#10293) This fixes an issue that object field is considered as evaluated even when it's still pending. The fix itself was only: ``` if (!finalField->isFullyEvaluated()) return objectFieldValue; ``` However this change forces a more proper cycle detection mechanism for dataflow cycle, so this PR also added a counter that tracks the number of fully evaluated values. I'm hoping to replace these fixes with more simpler solution that actually mutates the IR as mentioned in #10265 Fixes #10264. Assisted-by: Augment: claude-sonnet-4.5
…or, extend StringConcat folder (#10269) This extends IntegerBinaryArithmeticOp folder using evaluation rule in Evaluator. Also changes Evaluator to use folder to avoid code duplication. Also extends a folder of StringConcat. Separated from #10265, which proposes to a pattern to use Op folder not only IntegerArithmeticOp. Assisted-by: Codex: GPT 5.4
Refactor the OM evaluator around explicit Ready/Pending/Failure state tracking and a declarative operation pattern framework which follows similar structure as mlir's RewritePattern.
Previously each evaluation needed to prevent every corner cases (attributes/reference being unknown, reference, pending) etc. This tries to prevents that by using OpReadyOperandsPattern.
This data structure/framework should make it difficult to introduce a bug, and as a side effect it fixes #10264.
My intention is mostly NFCI except for fixing missing error/partially evaluated value handling, but will need tests for real designs.
Also add the tests for unknown nested fields and reference/value propagation.
Assisted-by: Codex: GPT-5.4