Skip to content

[OM] Introduce explicit evaluator state and op patterns#10265

Draft
uenoku wants to merge 3 commits into
llvm:mainfrom
uenoku:dev/hidetou/evaluator-framework
Draft

[OM] Introduce explicit evaluator state and op patterns#10265
uenoku wants to merge 3 commits into
llvm:mainfrom
uenoku:dev/hidetou/evaluator-framework

Conversation

@uenoku
Copy link
Copy Markdown
Member

@uenoku uenoku commented Apr 18, 2026

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

@uenoku uenoku force-pushed the dev/hidetou/evaluator-framework branch 2 times, most recently from 3483b6c to 75fd19d Compare April 18, 2026 05:45
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.
@uenoku uenoku force-pushed the dev/hidetou/evaluator-framework branch from af62281 to 55bdc02 Compare April 18, 2026 07:54
@uenoku uenoku force-pushed the dev/hidetou/evaluator-framework branch from 55bdc02 to a82bb7b Compare April 18, 2026 08:24
Comment on lines +765 to +768
auto result = createUnknownValue(op.getType(), loc);
if (failed(result))
return ResolvedValue::failure();
return resolveValueState(result.value());
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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(),
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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, "
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment for link.

@uenoku
Copy link
Copy Markdown
Member Author

uenoku commented Apr 19, 2026

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.

@uenoku uenoku force-pushed the dev/hidetou/evaluator-framework branch from 651b934 to a82bb7b Compare April 20, 2026 09:07
Comment on lines +40 to +62
/// 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.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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). 😅

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +76 to +81
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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread lib/Dialect/OM/OMOps.cpp
Comment on lines +661 to +663
auto result = op.evaluateIntegerOperation(lhsVal, rhsVal);
if (failed(result))
return {};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1764 to +1773
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";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +1790 to +1796
auto fieldValue = llvm::cast<evaluator::ObjectValue>(result.value().get())
->getField("result")
.value();

ASSERT_EQ("Hello", llvm::cast<evaluator::AttributeValue>(fieldValue.get())
->getAs<StringAttr>()
.getValue());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. 😉

uenoku added a commit that referenced this pull request Apr 22, 2026
…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
uenoku added a commit that referenced this pull request Apr 23, 2026
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[domaintool][OM] Assertion failure when bouncing through extra object

2 participants