Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@seldridge
Copy link
Member

@seldridge seldridge commented Sep 5, 2025

Add a pass that will lower FIRRTL domains to properties. This has the
effect of removing all domain information from the circuit.

Example

Input:

firrtl.circuit "Foo" {
  firrtl.domain @ClockDomain {}
  firrtl.module @Foo(
    in %A: !firrtl.domain of @ClockDomain,
    in %a: !firrtl.uint<1> domains [%A],
    in %B: !firrtl.domain of @ClockDomain,
    out %b: !firrtl.uint<1> domains [%B]
  ) {
    %0 = firrtl.unsafe_domain_cast %a domains %B : !firrtl.uint<1>
    firrtl.matchingconnect %b, %0 : !firrtl.uint<1>
  }
}

Output:

module {
  firrtl.circuit "Foo" {
    firrtl.class @ClockDomain() {
    }
    firrtl.class @ClockDomain_out(in %domainInfo_in: !firrtl.class<@ClockDomain()>, out %domainInfo_out: !firrtl.class<@ClockDomain()>, in %associations_in: !firrtl.list<path>, out %associations_out: !firrtl.list<path>) {
      firrtl.propassign %domainInfo_out, %domainInfo_in : !firrtl.class<@ClockDomain()>
      firrtl.propassign %associations_out, %associations_in : !firrtl.list<path>
    }
    firrtl.module @Foo(in %A: !firrtl.class<@ClockDomain()>, out %A_out: !firrtl.class<@ClockDomain_out(in domainInfo_in: !firrtl.class<@ClockDomain()>, out domainInfo_out: !firrtl.class<@ClockDomain()>, in associations_in: !firrtl.list<path>, out associations_out: !firrtl.list<path>)>, in %a: !firrtl.uint<1> [{class = "circt.tracker", id = distinct[0]<>}], in %B: !firrtl.class<@ClockDomain()>, out %B_out: !firrtl.class<@ClockDomain_out(in domainInfo_in: !firrtl.class<@ClockDomain()>, out domainInfo_out: !firrtl.class<@ClockDomain()>, in associations_in: !firrtl.list<path>, out associations_out: !firrtl.list<path>)>, out %b: !firrtl.uint<1> [{class = "circt.tracker", id = distinct[1]<>}]) {
      firrtl.matchingconnect %b, %a : !firrtl.uint<1>
      %A_object = firrtl.object @ClockDomain_out(in domainInfo_in: !firrtl.class<@ClockDomain()>, out domainInfo_out: !firrtl.class<@ClockDomain()>, in associations_in: !firrtl.list<path>, out associations_out: !firrtl.list<path>)
      %0 = firrtl.object.subfield %A_object[domainInfo_in] : !firrtl.class<@ClockDomain_out(in domainInfo_in: !firrtl.class<@ClockDomain()>, out domainInfo_out: !firrtl.class<@ClockDomain()>, in associations_in: !firrtl.list<path>, out associations_out: !firrtl.list<path>)>
      firrtl.propassign %0, %A : !firrtl.class<@ClockDomain()>
      %1 = firrtl.object.subfield %A_object[associations_in] : !firrtl.class<@ClockDomain_out(in domainInfo_in: !firrtl.class<@ClockDomain()>, out domainInfo_out: !firrtl.class<@ClockDomain()>, in associations_in: !firrtl.list<path>, out associations_out: !firrtl.list<path>)>
      %2 = firrtl.path reference distinct[0]<>
      %3 = firrtl.list.create %2 : !firrtl.list<path>
      firrtl.propassign %1, %3 : !firrtl.list<path>
      firrtl.propassign %A_out, %A_object : !firrtl.class<@ClockDomain_out(in domainInfo_in: !firrtl.class<@ClockDomain()>, out domainInfo_out: !firrtl.class<@ClockDomain()>, in associations_in: !firrtl.list<path>, out associations_out: !firrtl.list<path>)>
      %B_object = firrtl.object @ClockDomain_out(in domainInfo_in: !firrtl.class<@ClockDomain()>, out domainInfo_out: !firrtl.class<@ClockDomain()>, in associations_in: !firrtl.list<path>, out associations_out: !firrtl.list<path>)
      %4 = firrtl.object.subfield %B_object[domainInfo_in] : !firrtl.class<@ClockDomain_out(in domainInfo_in: !firrtl.class<@ClockDomain()>, out domainInfo_out: !firrtl.class<@ClockDomain()>, in associations_in: !firrtl.list<path>, out associations_out: !firrtl.list<path>)>
      firrtl.propassign %4, %B : !firrtl.class<@ClockDomain()>
      %5 = firrtl.object.subfield %B_object[associations_in] : !firrtl.class<@ClockDomain_out(in domainInfo_in: !firrtl.class<@ClockDomain()>, out domainInfo_out: !firrtl.class<@ClockDomain()>, in associations_in: !firrtl.list<path>, out associations_out: !firrtl.list<path>)>
      %6 = firrtl.path reference distinct[1]<>
      %7 = firrtl.list.create %6 : !firrtl.list<path>
      firrtl.propassign %5, %7 : !firrtl.list<path>
      firrtl.propassign %B_out, %B_object : !firrtl.class<@ClockDomain_out(in domainInfo_in: !firrtl.class<@ClockDomain()>, out domainInfo_out: !firrtl.class<@ClockDomain()>, in associations_in: !firrtl.list<path>, out associations_out: !firrtl.list<path>)>
    }
  }
}

@rwy7 rwy7 added the FIRRTL Involving the `firrtl` dialect label Sep 8, 2025
@seldridge seldridge force-pushed the dev/seldridge/firrtl-erase-domains branch from 9de57e9 to 43844de Compare September 15, 2025 22:36
@seldridge seldridge force-pushed the dev/seldridge/firrtl-erase-domains branch from 43844de to ba5a8e5 Compare September 23, 2025 22:01
@seldridge seldridge marked this pull request as ready for review September 23, 2025 22:28
@seldridge seldridge requested review from dtzSiFive and rwy7 September 23, 2025 23:17
@dtzSiFive
Copy link
Contributor

Consider this example (mutated second test circuit to not have an extmodule), which given current IR I think is close to simplest example with instantiation:

firrtl.circuit "Foo" {
  firrtl.domain @ClockDomain {}
  firrtl.module @Bar(
    in %A: !firrtl.domain of @ClockDomain,
    in %a: !firrtl.uint<1> domains [%A]
  ) { }
  firrtl.module @Foo(
    in %A: !firrtl.domain of @ClockDomain,
    in %a: !firrtl.uint<1> domains [%A]
  ) {
    %bar_A, %bar_a = firrtl.instance bar @Bar(
      in A: !firrtl.domain of @ClockDomain,
      in a: !firrtl.uint<1> domains[A]
    )
    firrtl.matchingconnect %bar_a, %a : !firrtl.uint<1>
  }
}

Looks like this doesn't work in the presence of instances? (no test exists demonstrating otherwise and trivial example does not work)

Error:

/home/will/src/sifive/circt/test/Dialect/FIRRTL/lower-domains.mlir:11:22: error: 'firrtl.instance' op has a wrong number of results; expected 3 but got 1
    %bar_A, %bar_a = firrtl.instance bar @Bar(
                     ^
/home/will/src/sifive/circt/test/Dialect/FIRRTL/lower-domains.mlir:11:22: note: see current operation: %5 = "firrtl.instance"() <{annotations = [], domainInfo = [[]], layers = [], moduleName = @Bar, name = "bar", nameKind = #firrtl<name_kind droppable_name>, portAnnotations = [[]], portDirections = array<i1: false>, portNames = ["a"]}> : () -> !firrtl.uint<1>
/home/will/src/sifive/circt/test/Dialect/FIRRTL/lower-domains.mlir:3:3: note: original module declared here
  firrtl.module @Bar(
  ^

@seldridge seldridge force-pushed the dev/seldridge/firrtl-erase-domains branch from 3b2ed12 to cbfc9c3 Compare September 24, 2025 22:32
@seldridge
Copy link
Member Author

seldridge commented Sep 24, 2025

Something is broken. The lowerInstances member function is supposed to be replacing all instances with the result of the module lowering. Thanks for the test case.

Edit: Typical mistake of walking instances in modules instead of instances of modules.

@seldridge
Copy link
Member Author

seldridge commented Sep 25, 2025

Instances are fixed in 5c42aea. I still need to look at the changes to external modules. (External modeuls are fixed in 9f28b09.)

@seldridge seldridge mentioned this pull request Sep 27, 2025
1 task
@seldridge seldridge force-pushed the dev/seldridge/firrtl-erase-domains branch 2 times, most recently from 76e8bbd to bffc683 Compare October 1, 2025 17:30
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/Debug.h"

#define DEBUG_TYPE "firrtl-lower-domains"
Copy link
Contributor

Choose a reason for hiding this comment

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

It'd be neat if we could magically set this to the pass name, as that's usually what we want!

Copy link
Member Author

Choose a reason for hiding this comment

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

Ugh... this has annoyed me for a while. There's no nice char * readily available. However, I did find a way to do it that looks fine. See what you think about: 4ab6d85

// user needs to provide.
// 2. A second that returns this user-provided information and includes the
// domain information.
// CHECK-LABEL: firrtl.class @ClockDomain
Copy link
Contributor

Choose a reason for hiding this comment

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

I have questions about domains and classes and names -- you probably can guess, the usual kind.

Modulo making domains classlike, insofar as classes are nominally typed it may be important to give consideration here. Probably the associations class is the main question (the other will continue as a domain (?) or at least can re-use the class name, assuming running with same namespace for them), while it's squarely compiler-generated it needs to be the same across SC units.

Something to think about.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, good point. The re-use of the existing domain name for the "input" class works. The output class will need to have an ABI-determined name. While I think it's probably fine to keep this as <name>_out for now, this should likely be something better like <name>$domainOut. Or: something that uses $ to prevent collision and is impossible to collide with. Same thing for the port name.

I won't treat this as blocking.

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.

Looking good! Left a bunch of feedback.

GH started bugging on me so will add a few more comments shortly.

let summary = "lower domain information to properties";
let description = [{
Lower all domain information into FIRRTL properties. This has the effect of
erasing all domain information.
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this pass have any prerequisites? Should these be stated, and can/should they be checked for?
Having neither seems dangerous, but I'm not sure sure what that might entail.

This maybe needs InferDomains to have run (or be equivalent to having run already, maybe, if input is already fully set)? Where in the pipeline is this expected to live -- hopefully part of the overall LowerToHW pipeline (which includes LowerClasses)?

I know there's advice going around (super fair characterization follows!) that passes should yolo their way and crash on folks not swoll enough to use them "properly" but that's basically the same argument against defensive assertions that also is contrary to LLVM ethos and building quality products; if one piece of advice is blindly taken I'd much rather have a compiler (and especially be on a compiler team that works towards) that works every time over ... I'll spare my critique of one blindly following the other way 👍. Of course there are trade-offs and consideration of best engineering best practices at play here... not everything can or should check everything, so on.

Use your judgement, as I'm sure you would regardless 😁 .

Making passes that don't have silent assumptions they crash on really helps a compiler project overall, and is part of being a high-quality pass in general. Pipeline debugging (bugpoint-style), reductions (circt-reduce style), pass rearranging, so on.

Anyway 😄 . I'm not sure this needed so many words, please do what you think is best here 👍 .

I'm honestly partly sincerely interested in what this needs for it to work! So if nothing else please spare words to answer that for me if you would.

Copy link
Member Author

Choose a reason for hiding this comment

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

Known prerequisites: this needs to run after infer domains (which is WIP right now) and before lower classes. If run before infer domains, it won't crash or anything like that, but it will produce incomplete results that differ from what would happen if run after infer domains. E.g., if association information is missing, then it will claim that those ports have no associations.

The current limitations of the representation of domains (that ports are either domain ports or have associations) means that this assumes a post-LOA representation. However, there is no way to actually represent this pre-LOA right now. The pass doesn't require LowerTypes currently, though that is again due to the lack of the ability to have associations on fields. Once per-field associations are available, the pass will need an update.

Clearly, as the representational restrictions are lifted, the pass will need to gain capabilities.

Yes, the pass is expected to be in the LowerToHW pipeline. This adds a lot of distinct attr annotations to ports which will have a detrimental effect to the ability of the module to be optimized. For that reason it needs to be as late as possible.

Copy link
Member Author

Choose a reason for hiding this comment

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

1d3f95a adds top-level commentary summarizing the above comment.

@seldridge seldridge force-pushed the dev/seldridge/firrtl-erase-domains branch from 4ab6d85 to 39455de Compare October 2, 2025 19:00
@seldridge seldridge requested a review from dtzSiFive October 4, 2025 05:41
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.

Only made it through your WONDERFUL comments!! (and have been following responses via email so have a bit of sense elsewhere).

Apologies for slow review, thank you for taking all my feedback!! I'll get to this ASAP 👍 , leaving what I have for now (few typos and answering my own question I asked the other day? haha).

This is looking great!

// input/output properties as its corresponding domain and (2) a class that is
// used to track the associations of the domain. Every input domain port is
// lowered to an input of type (1) and an output of type (2). Every output
// domain port is lowered to an output of type (2).
Copy link
Contributor

Choose a reason for hiding this comment

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

Until we have our domain define connectlike (#9067) maybe this is moot, but....

What drives the added object of type (1) that are now input ports?

Conceptually, I mean, if nothing else. In some cases this can be from the parent's input domain port. When local to a module (whatever "allocate a domain" looks like), it's from that point I'd imagine.

This brings us to the example I was bringing up the other day on this subject re:proposed lowering and asymmetric handling of input/output domain information.

Basically, I'd expect the lowered domain plumbing to mirror that of the domain connections originally:
If a domain input port is (in the FIRRTL) driven by a sibling's output port, where does the domain object of type (1) come from?

By lowering (2) to associations.. ah, there it is.
Okay so they're both (at least) objects of type (1), (2) additionally includes a list of association paths.

So presumably the answer to this question is that we'd grab the domain object out of the output of type (2) from the sibling instance and drive that to the input port (now expecting something of type (1)).

Okay so -- just how I like thinking about these things:

  1. All domain values and connectivity become type (1) and propassign instead of domain define.
  2. Output ports have association information bundled into the required-for-correctness-of-IR threading of type (1) objects (lowered domain values) which implicitly also ties the two together for easy processing agnostic of knowledge of, say, domain port names.

Anyway this breakdown isn't how the code does things, and that's all good, but hopefully you follow. We transform domains to classes, and replace values with objects of that class, and connectivity is basically the same throughout. For output ports we instead drive into the associations object (and grab it back out as needed).

Does this make sense, anything wrong/missing here?
Thanks!

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, this is correct. The lowered class plumbing will mirror that of the original domain plumbing. Things may get optimized, though. Also, aliasing is possible (but not problematic).

Aliasing Example

Consider the following two circuits. The first has two output aliased domains of A:

domain ClockDomain:
  input name_in: String
  output name_out: String

  propassign name_out, name_in

module Foo:
  input A: Domain of ClockDomain
  output B: Domain of ClockDomain
  output C: Domain of ClockDomain
  output a: UInt<1> domains [A]
  output b: UInt<1> domains [B]
  output c: UInt<1> domains [C]

  invalidate a
  invalidate b
  invalidate c
  
  domain_define B = A
  domain_define C = A
domain ClockDomain:
  input name_in: String
  output name_out: String

  propassign name_out, name_in

module Foo:
  input A: Domain of ClockDomain
  output a: UInt<1> domains [A]
  output b: UInt<1> domains [A]
  output c: UInt<1> domains [A]

  invalidate a
  invalidate b
  invalidate c

When this eventually hits the OM APIs in the final MLIR, the shape of the module Foo will look different for the first circuit and for the second. In the first, the user will have to provide a single clock domain object, setting the name_in parameter. They will then get three domains out, each associated with one port. However, the user will need to recognize that all of these are aliases as they have the same name.

There is no aliasing in the latter.

If this is an internal module, it is reasonable that the first module is optimized into the second. If this is a public module, then (I think) it cannot be optimized and it's up to the user to deal with the aliasing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you! This makes sense. I was wondering how much inspect-what-is-there will be done in the consuming API's. I'm a little surprised we're on board with having the set of domain association objects being dependent on optimizations, but that almost takes us to point of preserving a module just to run a property out that says the domain has no associations.

OTOH since we're not splitting these apart at the MLIR level we could gather associations at the top of public modules (like OMIR would do for children nodes, IIRC?).
Nevermind that, mostly, for now 😄 .

That does raise question of how the API's might reason about domains they are provided associations for that they don't know the origin of (similarly but slightly less obvious than needing to provide information for domains not originally part of the module's interface).

I'll move this conversion to a chat 👍 .

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!

Awesome, excellent, work!! I love how very well structured your code always is ❤️ makes it a joy to read. Even tests are aligned (a pattern I think we've started "enforcing"/requesting generally). Comments are fantastic and really help, appreciate it. Thank you!

DistinctAttr id;
for (auto indexAttr : domainAttr.getAsRange<IntegerAttr>()) {
if (!id) {
id = DistinctAttr::create(UnitAttr::get(context));
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we usually reuse distinctattr's? I don't think so, but not sure that that is important. Do you know/remember @youngar ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Talked with Andrew about this offline and, while we don't seem to do this currently, there should be no problem with it.

// input/output properties as its corresponding domain and (2) a class that is
// used to track the associations of the domain. Every input domain port is
// lowered to an input of type (1) and an output of type (2). Every output
// domain port is lowered to an output of type (2).
Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you! This makes sense. I was wondering how much inspect-what-is-there will be done in the consuming API's. I'm a little surprised we're on board with having the set of domain association objects being dependent on optimizations, but that almost takes us to point of preserving a module just to run a property out that says the domain has no associations.

OTOH since we're not splitting these apart at the MLIR level we could gather associations at the top of public modules (like OMIR would do for children nodes, IIRC?).
Nevermind that, mostly, for now 😄 .

That does raise question of how the API's might reason about domains they are provided associations for that they don't know the origin of (similarly but slightly less obvious than needing to provide information for domains not originally part of the module's interface).

I'll move this conversion to a chat 👍 .

@seldridge seldridge merged commit 822157a into main Oct 10, 2025
7 checks passed
@seldridge seldridge deleted the dev/seldridge/firrtl-erase-domains branch October 10, 2025 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

FIRRTL Involving the `firrtl` dialect

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants