Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 94 additions & 72 deletions include/circt/Dialect/OM/Evaluator/Evaluator.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,60 @@ class EvaluatorValue;
/// primitive Attribute. Further refinement is expected.
using EvaluatorValuePtr = std::shared_ptr<EvaluatorValue>;

/// 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.
Comment on lines +40 to +62
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.

///
/// The evaluation state of a value handle.
enum class ResolutionState {
/// The handle can be used now. For references, this means the whole
/// reference chain leads to a settled value.
Ready,
/// Evaluation is not done yet. The handle itself may still be partial, or a
/// reference in the chain may still be missing.
Pending,
/// Evaluation hit a hard error, such as a reference cycle.
Failure
};

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;
Comment on lines +76 to +81
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.


static ResolvedValue ready(EvaluatorValuePtr value) {
return {ResolutionState::Ready, std::move(value)};
}
static ResolvedValue pending(EvaluatorValuePtr value = nullptr) {
return {ResolutionState::Pending, std::move(value)};
}
static ResolvedValue failure(EvaluatorValuePtr value = nullptr) {
return {ResolutionState::Failure, std::move(value)};
}
};

/// The fields of a composite Object, currently represented as a map. Further
/// refinement is expected.
using ObjectFields = SmallDenseMap<StringAttr, EvaluatorValuePtr>;
Expand All @@ -54,12 +108,14 @@ class EvaluatorValue : public std::enable_shared_from_this<EvaluatorValue> {
Kind getKind() const { return kind; }
MLIRContext *getContext() const { return ctx; }

// Return true the value is fully evaluated.
// Unknown values are considered fully evaluated.
bool isFullyEvaluated() const { return fullyEvaluated; }
void markFullyEvaluated() {
assert(!fullyEvaluated && "should not mark twice");
fullyEvaluated = true;
// Return true if this value object has finished its own local work.
// This is not the same as semantic Ready/Pending state: for example, a
// ReferenceValue can be settled but still point to a pending value.
// Unknown values are considered settled.
bool isSettled() const { return settled; }
void markSettled() {
assert(!settled && "should not mark twice");
settled = true;
}

/// Return true if the value is unknown (has unknown in its fan-in).
Expand All @@ -68,13 +124,13 @@ class EvaluatorValue : public std::enable_shared_from_this<EvaluatorValue> {
bool isUnknown() const { return unknown; }

/// Mark this value as unknown.
/// This also marks the value as fully evaluated if it isn't already, since
/// unknown values are considered fully evaluated. This maintains the
/// invariant that unknown implies fullyEvaluated.
/// This also marks the value as settled if it isn't already, since unknown
/// values are considered settled. This maintains the invariant that unknown
/// implies settled.
void markUnknown() {
unknown = true;
if (!fullyEvaluated)
markFullyEvaluated();
if (!settled)
markSettled();
}

/// Return the associated MLIR context.
Expand All @@ -100,7 +156,7 @@ class EvaluatorValue : public std::enable_shared_from_this<EvaluatorValue> {
const Kind kind;
MLIRContext *ctx;
Location loc;
bool fullyEvaluated = false;
bool settled = false;
bool finalized = false;
bool unknown = false;
};
Expand All @@ -123,24 +179,14 @@ class ReferenceValue : public EvaluatorValue {
EvaluatorValuePtr getValue() const { return value; }
void setValue(EvaluatorValuePtr newValue) {
value = std::move(newValue);
markFullyEvaluated();
markSettled();
}

// Finalize the value.
LogicalResult finalizeImpl();

// Return the first non-reference value that is reachable from the reference.
FailureOr<EvaluatorValuePtr> getStrippedValue() const {
llvm::SmallPtrSet<ReferenceValue *, 4> visited;
auto currentValue = value;
while (auto *v = dyn_cast<ReferenceValue>(currentValue.get())) {
// Detect a cycle.
if (!visited.insert(v).second)
return failure();
currentValue = v->getValue();
}
return success(currentValue);
}
FailureOr<EvaluatorValuePtr> getStrippedValue() const;

private:
EvaluatorValuePtr value;
Expand Down Expand Up @@ -180,7 +226,7 @@ class AttributeValue : public EvaluatorValue {
AttributeValue(PrivateTag, Attribute attr, Location loc)
: EvaluatorValue(attr.getContext(), Kind::Attr, loc), attr(attr),
type(cast<TypedAttr>(attr).getType()) {
markFullyEvaluated();
markSettled();
}

// Constructor for partially evaluated AttributeValue
Expand Down Expand Up @@ -215,12 +261,12 @@ class ListValue : public EvaluatorValue {
Location loc)
: EvaluatorValue(type.getContext(), Kind::List, loc), type(type),
elements(std::move(elements)) {
markFullyEvaluated();
markSettled();
}

void setElements(SmallVector<EvaluatorValuePtr> newElements) {
elements = std::move(newElements);
markFullyEvaluated();
markSettled();
}

// Finalize the value.
Expand Down Expand Up @@ -251,7 +297,7 @@ class ObjectValue : public EvaluatorValue {
ObjectValue(om::ClassLike cls, ObjectFields fields, Location loc)
: EvaluatorValue(cls.getContext(), Kind::Object, loc), cls(cls),
fields(std::move(fields)) {
markFullyEvaluated();
markSettled();
}

// Partially evaluated value.
Expand All @@ -263,7 +309,7 @@ class ObjectValue : public EvaluatorValue {

void setFields(llvm::SmallDenseMap<StringAttr, EvaluatorValuePtr> newFields) {
fields = std::move(newFields);
markFullyEvaluated();
markSettled();
}

/// Return the type of the value, which is a ClassType.
Expand Down Expand Up @@ -393,13 +439,13 @@ class Evaluator {
using ObjectKey = std::pair<Value, ActualParameters>;

private:
bool isFullyEvaluated(Value value, ActualParameters key) {
return isFullyEvaluated({value, key});
bool isSettled(Value value, ActualParameters key) {
return isSettled({value, key});
}

bool isFullyEvaluated(ObjectKey key) {
bool isSettled(ObjectKey key) {
auto val = objects.lookup(key);
return val && val->isFullyEvaluated();
return val && val->isSettled();
}

FailureOr<EvaluatorValuePtr>
Expand All @@ -410,53 +456,25 @@ class Evaluator {
/// Evaluate a Value in a Class body according to the small expression grammar
/// described in the rationale document. The actual parameters are the values
/// supplied at the current instantiation of the Class being evaluated.
FailureOr<EvaluatorValuePtr>
evaluator::ResolvedValue
evaluateValue(Value value, ActualParameters actualParams, Location loc);

/// Evaluator dispatch functions for the small expression grammar.
FailureOr<EvaluatorValuePtr> evaluateParameter(BlockArgument formalParam,
ActualParameters actualParams,
Location loc);

FailureOr<EvaluatorValuePtr>
evaluateConstant(ConstantOp op, ActualParameters actualParams, Location loc);

FailureOr<EvaluatorValuePtr>
evaluateIntegerBinaryArithmetic(IntegerBinaryArithmeticOp op,
ActualParameters actualParams, Location loc);
evaluator::ResolvedValue evaluateParameter(BlockArgument formalParam,
ActualParameters actualParams,
Location loc);

/// Instantiate an Object with its class name and actual parameters.
FailureOr<EvaluatorValuePtr>
evaluateObjectInstance(StringAttr className, ActualParameters actualParams,
Location loc, ObjectKey instanceKey = {});
FailureOr<EvaluatorValuePtr>
evaluator::ResolvedValue
evaluateObjectInstance(ObjectOp op, ActualParameters actualParams);
FailureOr<EvaluatorValuePtr>
evaluateObjectField(ObjectFieldOp op, ActualParameters actualParams,
Location loc);
FailureOr<EvaluatorValuePtr> evaluateListCreate(ListCreateOp op,
ActualParameters actualParams,
Location loc);
FailureOr<EvaluatorValuePtr> evaluateListConcat(ListConcatOp op,
ActualParameters actualParams,
Location loc);
FailureOr<EvaluatorValuePtr>
evaluateStringConcat(StringConcatOp op, ActualParameters actualParams,
Location loc);
FailureOr<EvaluatorValuePtr>
evaluateBinaryEquality(BinaryEqualityOp op, ActualParameters actualParams,
Location loc);
FailureOr<evaluator::EvaluatorValuePtr>
evaluateBasePathCreate(FrozenBasePathCreateOp op,
ActualParameters actualParams, Location loc);
FailureOr<evaluator::EvaluatorValuePtr>
evaluatePathCreate(FrozenPathCreateOp op, ActualParameters actualParams,
Location loc);
FailureOr<evaluator::EvaluatorValuePtr>
evaluateEmptyPath(FrozenEmptyPathOp op, ActualParameters actualParams,
Location loc);
FailureOr<evaluator::EvaluatorValuePtr>
evaluateUnknownValue(UnknownValueOp op, Location loc);
evaluator::ResolvedValue evaluateObjectField(ObjectFieldOp op,
ActualParameters actualParams,
Location loc);
evaluator::ResolvedValue evaluateUnknownValue(UnknownValueOp op,
Location loc);

LogicalResult evaluatePropertyAssert(PropertyAssertOp op,
ActualParameters actualParams);
Expand All @@ -477,12 +495,16 @@ class Evaluator {
std::unique_ptr<SmallVector<std::shared_ptr<evaluator::EvaluatorValue>>>>
actualParametersBuffers;

/// A worklist that tracks values which needs to be fully evaluated.
/// A worklist that tracks values that still need more evaluation work.
std::queue<ObjectKey> worklist;

/// Evaluator value storage. Return an evaluator value for the given
/// instantiation context (a pair of Value and parameters).
DenseMap<ObjectKey, std::shared_ptr<evaluator::EvaluatorValue>> objects;

/// Tracks object instantiations currently being evaluated so recursive
/// object graphs reuse the existing placeholder instead of recursing.
llvm::SmallDenseSet<ObjectKey, 8> activeObjectInstances;
};

/// Helper to enable printing objects in Diagnostics.
Expand Down
1 change: 1 addition & 0 deletions include/circt/Dialect/OM/OMOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,7 @@ class IntegerBinaryArithmeticOp<string mnemonic, list<Trait> traits = []> :
let results = (outs OMIntegerType:$result);

let assemblyFormat = "$lhs `,` $rhs attr-dict `:` type($result)";
let hasFolder = 1;
}

def IntegerAddOp : IntegerBinaryArithmeticOp<"integer.add", [Commutative]> {
Expand Down
1 change: 1 addition & 0 deletions lib/Dialect/OM/Evaluator/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
add_circt_library(CIRCTOMEvaluator
Evaluator.cpp
EvaluatorPatterns.cpp

DEPENDS
MLIROMIncGen
Expand Down
Loading