Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[LowerAnnotations] Resolve legacy wiring annotations as WiringProblems #4496

Merged
merged 12 commits into from Feb 7, 2023

Conversation

sam-shahrestani
Copy link
Contributor

  • Updates annotationRecords with a special applier for legacy Wiring annotations (i.e. firrtl.passes.wiring.SinkAnnotation, firrtl.passes.wiring.SourceAnnotation) that stores these annotations in ApplyState
  • Later in LowerAnnotations, these are processed into specific WiringProblem pairs.
  • "legacy" in names is used to refer to the fact that these annotations are probably going to be depreciated for annotations that map closer to WiringProblem (see issue)

Implements #4482.

Copy link
Member

@seldridge seldridge left a comment

Choose a reason for hiding this comment

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

High level, looking good!

Some comments throughout.

Also, could the PR include some tests?

Comment on lines 250 to 251
llvm::StringMap<Value> legacyWiringSources;
llvm::StringMap<SmallVector<Value>> legacyWiringSinks;
Copy link
Member

Choose a reason for hiding this comment

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

It would be slightly cleaner to define a new struct LegacyWiringProblem as opposed to doing the split of two string maps. Consider:

struct LegacyWiringProblem {
  source Value;
  sinks SmallVector<Value>;
};

DenseMap<LegacyWiringProblem> legacyWiringProblems;

Copy link
Member

Choose a reason for hiding this comment

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

Also, there's usually not a huge benefit in using llvm::StringMap unless you have actual strings. Because you're dealing with StringAttrs (which are wrappers around interned strings), it's usually better to just use a DenseMap<Attribute> (and ignore the fact that this will allow for keys of any attribute type and not just StringAttr). This should be more efficient than llvm::StringMap.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A logical issue I'm having with this is that the order of annotations impacts how we can structure data. If the sink for a pin is defined before a source for it, it's not possible to define a LegacyWiringProblem like this right away, we'd have to store something immediately and update it later. Would

struct LegacyWiringProblem {
  Optional<Value> source;
  SmallVector<Value> sinks;
};

make any sense?

Copy link
Member

@seldridge seldridge Dec 29, 2022

Choose a reason for hiding this comment

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

Optional<Value> source is overkill as Value is nullable. I.e., it's totally fine to just leave Value uninitialized and it has boolean operations defined on it. Stuff like the following is totally fine:

LegacyWiringProblem foo = ...;
if (!foo.source)
  return mlir::emitError() << "no source set!";

Tangential tidbit: llvm::Optional is deprecated (in upstream LLVM) in favor of unifying around std::optional.

Comment on lines 85 to 90
// Get pin field
StringRef pin;
if (auto pinName = anno.getNamed("pin")) {
pin = pinName->getValue().cast<StringAttr>().getValue();
} else {
return mlir::emitError(state.circuit.getLoc())
<< "Annotation does not have an associated pin name: " << anno;
}
Copy link
Member

Choose a reason for hiding this comment

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

A slightly cleaner way to do this using getAs<T>.

auto pin = anno.getAs<StringAttr>("pin");
if (!pin)
  return mlir::emitError(...);

@sam-shahrestani
Copy link
Contributor Author

I've had a crack at everything above except testing. LowerAnnotations as a whole seems to not have any CIRCT level tests, so what would be the best way to test this for now? Adding tests to firtool?

@seldridge
Copy link
Member

The PR can model the tests off of how the Grand Central Taps tests are written:

// SiFive-custom annotations related to the GrandCentral utility. These

An example of testing the error paths is here:

// expected-error @+3 {{unknown/unimplemented DataTapKey class 'sifive.enterprise.grandcentral.LiteralDataTapKey'}}

These are, for the most part, directly testing the wiring problem infra. It isn't necessary to duplicate that, only the novel features that are getting added here. E.g., single-source/multi-sink, real ports bored from source to LCA, and error code paths.

@sam-shahrestani
Copy link
Contributor Author

Had a chance to circle back to this, I've added some simple tests and resolved conflicts.

Copy link
Contributor

@dtzSiFive dtzSiFive left a comment

Choose a reason for hiding this comment

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

Generally, looks good! Bounced through it and added some comments--most pressing are ones regarding ensuring this works properly re:dominance in various scenarios, secondarily ensuring it composes with taps properly (although if can't find a good solution for that here, wiling to be convinced we file an issue for that and tackle separately).

Thanks for updating and the tests!

@@ -0,0 +1,121 @@
//===- LegacyWiring- legacy Wiring annotation resolver ----------*- C++ -*-===//
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:

Suggested change
//===- LegacyWiring- legacy Wiring annotation resolver ----------*- C++ -*-===//
//===- LegacyWiring- legacy Wiring annotation resolver ----------*- C++ -*-===//
//===- LegacyWiring- legacy Wiring annotation resolver --------------------===//

It's tedious but the idea is we only include the "C++" bit for headers to help editors unclear if it's a C or C++ file: https://llvm.org/docs/CodingStandards.html#file-headers . AFAIK only recognized by emacs...

@@ -206,6 +206,19 @@ struct WiringProblem {
/// A base name to use when generating new signals associated with this wiring
/// problem.
std::string newNameHint;

/// Create real type ports instead of ref type when solving this problem.
bool useRealTypePorts;
Copy link
Contributor

Choose a reason for hiding this comment

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

Micro-nit: both values for this boolean will use non-ref ports in some situations (ref type only used for half of the up/down wiring) maybe rename this (as it's essentially the WiringProblem API presently)? "disallowRefTypePorts" but that's not wonderful, "onlyBaseTypePorts", similarly so.

Alternatively, maybe make this an enum with each describing the wiring behavior, something like "PreferRefType" "NoRefType" (or RefTypeUsage::Prefer, RefTypeUsage::Never)? This would make the requested behavior more understandable when instantiating WiringProblem's too, but 🤷‍♂️ .

Just some thoughts, not at all a blocker as-is 👍.

Copy link
Member

Choose a reason for hiding this comment

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

👍 to using an enum class.

auto portNum = portTarget.getImpl().getPortNo();
if (auto module = dyn_cast<FModuleOp>(portTarget.getOp())) {
builder.setInsertionPointToEnd(module.getBodyBlock());
targetsValues.push_back(getValueByFieldID(
Copy link
Contributor

Choose a reason for hiding this comment

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

So, IMO, this is something we need to solve more robustly re:WiringProblem infra improvements, but for now it's the responsibility of the problem creation code to ensure a number of things that are a little tricky. Hopefully we can consolidate/improve things in the future. Anyway.

Here, inserting into the end of the block makes sense if this is a sink, but for the source you probably want the value to dominate everything the target port dominates (so it can be connected to the same sorts of things, for example down through an instance).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it enough to insert before/ at the start for sources?

if (paths.size() > 1) {
mlir::emitError(state.circuit.getLoc())
<< "cannot resolve a unique instance path from the "
"external module target"
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
"external module target"
"external module target "

(I think?)

targetsValues.end());
} else {
// Create a new problem
state.legacyWiringProblems[pin] = {.sinks = targetsValues};
Copy link
Contributor

Choose a reason for hiding this comment

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

I honestly thought this was a GCC extension (I think it is for C?), but looks like maybe it's a C++20 feature? In either case, I'm not sure this is a construct that's valid for all our intended toolchains/platforms (c++17, not necessarily w/gcc extensions).

/// are visited from leaves to roots to apply module modifications. Module
/// modifications include addings ports and connecting things up.
/// Wiring Problem is a mapping from a source to a sink that can be connected
/// via a real Type or RefType as requested. This uses a two-step approach.
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW non-reftype is "base" not "real", but maybe they should've been called real instead 😄. Calling these "real" ports makes sense to me.

auto diag = mlir::emitError(source.getLoc())
<< "Wiring Problem source type " << sourceType
<< " does not match sink type " << sinkType;
diag.attachNote(sink.getLoc()) << "The sink is here.";
Copy link
Contributor

Choose a reason for hiding this comment

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

Great! Thanks for checking this.

%1 = firrtl.subfield %bar_io[out] : !firrtl.bundle<out: uint<1>>
firrtl.strictconnect %0, %1 : !firrtl.uint<1>
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: newline (see red mark on github vs other files)

/// using the pin attribute as newNameHint
LogicalResult LowerAnnotationsPass::legacyToWiringProblems(ApplyState &state) {
for (const auto &problem : state.legacyWiringProblems) {
auto pin = cast<StringAttr>(problem.first);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the DenseMap have StringAttr as its key instead of Attribute?

@sam-shahrestani
Copy link
Contributor Author

Any chance for a review of these latest patches? I don't think anything else was considered a blocker, and I'd rather avoid this PR stagnating.

@seldridge
Copy link
Member

Sorry for the delay! We can take a look.

Copy link
Member

@seldridge seldridge left a comment

Choose a reason for hiding this comment

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

Nice! Thanks for tackling this.

A few nits and a few lingering comments from @dtzSiFive, but this looks good.

Comment on lines 483 to 486
if (!problem.second.source) {
return mlir::emitError(state.circuit.getLoc())
<< "Unable to resolve source for pin: " << problem.first;
}
Copy link
Member

Choose a reason for hiding this comment

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

Mega-nit:

Suggested change
if (!problem.second.source) {
return mlir::emitError(state.circuit.getLoc())
<< "Unable to resolve source for pin: " << problem.first;
}
if (!problem.second.source)
return mlir::emitError(state.circuit.getLoc())
<< "Unable to resolve source for pin: " << problem.first;

Ditto below and for for loop.

test/Dialect/FIRRTL/legacy-wiring-errors.mlir Outdated Show resolved Hide resolved
%1 = firrtl.subfield %bar_io[out] : !firrtl.bundle<out: uint<1>>
firrtl.strictconnect %0, %1 : !firrtl.uint<1>
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Ditto from above: this test can be greatly reduced in complexity.

%1 = firrtl.subfield %bar_io[out] : !firrtl.bundle<out: uint<1>>
firrtl.strictconnect %0, %1 : !firrtl.uint<1>
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit: Test can be reduced by using just ground types.

/// Convert consumed SourceAnnotation and SinkAnnotation into WiringProblems,
/// using the pin attribute as newNameHint
LogicalResult LowerAnnotationsPass::legacyToWiringProblems(ApplyState &state) {
for (const auto &problem : state.legacyWiringProblems) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider structured binding here, if you like it better:

for (const auto &[name, problem] : state.legacyWiringProblems) {

Don't usually use ref for Attribute's but doesn't hurt and no need to copy the problem.

@sam-shahrestani
Copy link
Contributor Author

I've stuck to just emitting an error instead of calling addPortsToModule (which seems to be the location of the invalidation), is that a reasonable decision?

@sam-shahrestani
Copy link
Contributor Author

I also copied the example you provided to a test case in data-taps-errors.fir to validate the output of this reference checking.

Copy link
Contributor

@dtzSiFive dtzSiFive left a comment

Choose a reason for hiding this comment

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

LGTM, checking instances is simpler than all impacted values.
(apologies for slow response, was out sick last week)

I was worried about checking the instance since it changes "all instances" of the module-- but since we don't allow data taps on modules that are multiply instantiated, this does the trick AFAIK.

This is something we should improve (separately!), thank you for handing this as you have!

@@ -834,7 +844,8 @@ LogicalResult circt::firrtl::applyGCTMemTaps(const AnnoPathValue &target,
combMem->getParentOfType<FModuleOp>());
if (path.size() > 1)
return combMem.emitOpError(
"cannot be resolved as source for MemTap, multiple paths from top "
"cannot be resolved as source for MemTap, multiple paths from "
"top "
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: combine this with rest of the message string literal below?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not quite sure why clang-format decided to mess up these strings, but I've reverted it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, that's weird. Especially unfortunate we can't get it to recombine string literals it happily breaks apart.

(thanks!)

@seldridge
Copy link
Member

I force pushed this just now to resolve merge conflicts I created. I'll merge this after tests run. Thanks for the hard work getting this together @sam-shahrestani!

@seldridge seldridge merged commit 05ca657 into llvm:main Feb 7, 2023
@dtzSiFive
Copy link
Contributor

Thanks @sam-shahrestani !!

@sam-shahrestani
Copy link
Contributor Author

Thanks for all the help and advice! Hopefully we'll see more projects moving to CIRCT from the SFC now.

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.

[FIRRTL] Use WiringProblem to handle SFC wiring transform
3 participants