Skip to content

Conversation

@seldridge
Copy link
Member

@seldridge seldridge commented Aug 12, 2025

This is an alternative approach to implementing FIRRTL domains from #8830.
This represents domains as additional operands to operations instead of
capturing which domain something is colored by in its type.

At present, the actual kind of a domain, e.g., if a domain is a clock
domain, a reset domain, or something else, is not linked to the underlying
domain values.

This PR is broken up into individual logical commits making the following
changes:

  • Add firrtl.domain
  • Add domain type
  • Add domains to ports
  • Add unsafe_domain_cast operation

@seldridge seldridge force-pushed the dev/seldridge/firrtl-domains-operation-approach branch 2 times, most recently from 7ca003f to e846e78 Compare August 12, 2025 03:55
@seldridge seldridge force-pushed the dev/seldridge/firrtl-domains-operation-approach branch from f7706fe to c1de2b9 Compare August 20, 2025 17:07
@seldridge seldridge marked this pull request as ready for review August 20, 2025 19:55
@seldridge seldridge requested a review from darthscsi as a code owner August 20, 2025 19:55
Copy link
Member

@uenoku uenoku left a comment

Choose a reason for hiding this comment

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

DomainInfoAttr is a 2d integer array which has port indices, correct? In that case how do we keep that invariant through the pipeline? Is it legal to define a port before Domain?

firrtl.module @Domains(
in %A: !firrtl.domain,
in %B: !firrtl.domain,
in %a: !firrtl.uint<1> domains [%A],
Copy link
Member

Choose a reason for hiding this comment

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

Does this work with anonymous block arguments?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, it does not. This probably needs to use an alternative code path when there is a block arg that doesn't have a name.

Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW FIRRTL allows duplicate port names, previously had verifier that needed to be rolled back: #3976 .

Consider:

FIRRTL version 5.1.0
circuit Domains:
  domain ClockDomain:

  public module Domains:
    input a_b: Domain of ClockDomain
    input a : { b: String }
    output b: UInt<5> domains [a_b]
    invalidate b

With current PR this input errors:

oops.fir:5:10: error: 'firrtl.module' op domain information for domain port "a_b" must be a 'FlatSymbolRefAttr'
  public module Domains:
         ^
oops.fir:5:10: note: see current operation:
"firrtl.module"() <{annotations = [], convention = #firrtl<convention scalarized>, domainInfo = [[], [], [0 : ui32]], layers = [], portAnnotations = [], portDirections = array<i1: false, false, true>, portLocations = [loc("oops.fir":6:11), loc("oops.fir":7:11), loc("oops.fir":8:12)], portNames = ["a_b", "a_b", "b"], portSymbols = [], portTypes = [!firrtl.domain, !firrtl.string, !firrtl.uint<5>], sym_name = "Domains"}> ({
^bb0(%arg0: !firrtl.domain, %arg1: !firrtl.string, %arg2: !firrtl.uint<5>):
  %0 = "firrtl.invalidvalue"() : () -> !firrtl.uint<5>
  "firrtl.matchingconnect"(%arg2, %0) : (!firrtl.uint<5>, !firrtl.uint<5>) -> ()
}) : () -> ()

@seldridge
Copy link
Member Author

DomainInfoAttr is a 2d integer array which has port indices, correct?

Yes, 2D integer array with port indices. Mechanically this is being enforced in a verifier for modules and instances (with slight tweaks between them). For modules, this must be either an empty ArrayAttr or an ArrayAttr of ArrayAttrs where the outer dimension is the number of ports. This is the same requirements for port annotations.

For instances, this is the same as modules, except it cannot be an empty ArrayAttr. This is also the same as port annotations on instances, though I don't think port annotations on instances are used.

In that case how do we keep that invariant through the pipeline?

Two ways (first is a non-way):

  1. We don't. After the forthcoming EraseDomains pass, all domain information is removed and the domain info on modules and instances is empty.
  2. We do enforce it and we will probably need either new operations or tracking a bit that we are after domain inference and we need stricter checks.

Is it legal to define a port before Domain?

Currently, no. This is only being checked in the FIRRTL parser and MLIR parser, though, not with a verifier. There's no actual problem with, though, just we've never allowed FIRRTL text to ever refer to an identifier that comes later. The underlying storage would allow this, though.

Copy link
Member

@uenoku uenoku left a comment

Choose a reason for hiding this comment

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

Two ways (first is a non-way):
We don't. After the forthcoming EraseDomains pass, all domain information is removed and the domain info on modules and instances is empty.
We do enforce it and we will probably need either new operations or tracking a bit that we are after domain inference and we need stricter checks.

As you mentioned offline we anyway need to use insertPorts for port modifications so I agree that it's feasible to maintain the correct indexes. Regardless it would be nice to check and add tests for LowerTypes/LowerSignature doesn't mess up the domain port indices.

Another way to represent this is to (ab?)use inner symbols for domain ports and use them as identifiers instead of indexes. This will certainly avoid nasty index management though using inner symbols for non-hardware stuffs might be not appropriate though.

AnnotationArrayAttr:$annotations,
DefaultValuedAttr<LayerArrayAttr, "{}">:$layers
DefaultValuedAttr<LayerArrayAttr, "{}">:$layers,
DefaultValuedAttr<ArrayRefAttr, "{}">:$domainInfo
Copy link
Member

Choose a reason for hiding this comment

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

Allowing intrinsics to specify domains for operands/results make sense but certainly that will require special handling for each operation in infer domain (or maybe we can prepare op interface to specify domain inference). For now could you reject intrinsics with domain info in LowerIntrinsic or somewhere?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is going to be the same as with external modules. They can define domains, but they need to be completely specified. Same thing for public modules, no inferred domains. WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

Yes that makes sense .

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.

Here we go!

Left some feedback 👍 .

Does this need a connect operation? I suggest adding one that behaves similarly to define and propassign (static single connect).

Some interesting inputs to consider:

This should error (symbol used as domain is not domain):

FIRRTL version 5.1.0
circuit Domains:
  public module ClockDomain:

  public module Domains:
    input A: Domain of ClockDomain
    input b: UInt<5> domains [A]

LowerOpenAggs lowers the open bundle to put domain in its own port causing an error. Putting b in same domain twice is needed, FWIW.
Generally, where can "Domain" types be used? From what I'm seeing in the syntax, probably Domain is not meant to be valid in a port type if it's not top-level?

FIRRTL version 5.1.0
circuit Domains:
  domain ClockDomain:

  public module Domains:
    input A: Domain of ClockDomain
    input x: { y: Domain }
    output b: UInt<5> domains [A, A]
    invalidate b

Instantiation involving domains (this example doesn't make sense yet but just wanted to see an instance):

FIRRTL version 5.1.0
circuit Domains:
  domain ClockDomain:

  module Child:
    output B: Domain of ClockDomain
    input b: UInt<5> domains [B]

  public module Domains:
    input A: Domain of ClockDomain
    input a: UInt<5> domains [A]

    inst c of Child
    connect c.b, a

firrtl.module @Domains(
in %A: !firrtl.domain,
in %B: !firrtl.domain,
in %a: !firrtl.uint<1> domains [%A],
Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW FIRRTL allows duplicate port names, previously had verifier that needed to be rolled back: #3976 .

Consider:

FIRRTL version 5.1.0
circuit Domains:
  domain ClockDomain:

  public module Domains:
    input a_b: Domain of ClockDomain
    input a : { b: String }
    output b: UInt<5> domains [a_b]
    invalidate b

With current PR this input errors:

oops.fir:5:10: error: 'firrtl.module' op domain information for domain port "a_b" must be a 'FlatSymbolRefAttr'
  public module Domains:
         ^
oops.fir:5:10: note: see current operation:
"firrtl.module"() <{annotations = [], convention = #firrtl<convention scalarized>, domainInfo = [[], [], [0 : ui32]], layers = [], portAnnotations = [], portDirections = array<i1: false, false, true>, portLocations = [loc("oops.fir":6:11), loc("oops.fir":7:11), loc("oops.fir":8:12)], portNames = ["a_b", "a_b", "b"], portSymbols = [], portTypes = [!firrtl.domain, !firrtl.string, !firrtl.uint<5>], sym_name = "Domains"}> ({
^bb0(%arg0: !firrtl.domain, %arg1: !firrtl.string, %arg2: !firrtl.uint<5>):
  %0 = "firrtl.invalidvalue"() : () -> !firrtl.uint<5>
  "firrtl.matchingconnect"(%arg2, %0) : (!firrtl.uint<5>, !firrtl.uint<5>) -> ()
}) : () -> ()

AnnotationArrayAttr:$annotations,
DefaultValuedAttr<LayerArrayAttr, "{}">:$layers
DefaultValuedAttr<LayerArrayAttr, "{}">:$layers,
DefaultValuedAttr<ArrayRefAttr, "{}">:$domainInfo
Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is going to be the same as with external modules. They can define domains, but they need to be completely specified. Same thing for public modules, no inferred domains. WDYT?

@seldridge
Copy link
Member Author

Does this need a connect operation? I suggest adding one that behaves similarly to define and propassign (static single connect).

It will. Though that can come in a subsequent PR.

Some interesting inputs to consider:

This should error (symbol used as domain is not domain):

FIRRTL version 5.1.0
circuit Domains:
  public module ClockDomain:

  public module Domains:
    input A: Domain of ClockDomain
    input b: UInt<5> domains [A]

Fixed in b171092.

Generally, where can "Domain" types be used? From what I'm seeing in the syntax, probably Domain is not meant to be valid in a port type if it's not top-level?

I don't see a reason to not allow it on any module (or a corresponding instantiation). Domains need to be valid in any port, wire, or bundle. Support for bundles is not rolled out yet, but will be added.

Instantiation involving domains (this example doesn't make sense yet but just wanted to see an instance)

FIRRTL version 5.1.0
circuit Domains:
domain ClockDomain:

module Child:
output B: Domain of ClockDomain
input b: UInt<5> domains [B]

public module Domains:
input A: Domain of ClockDomain
input a: UInt<5> domains [A]

inst c of Child
connect c.b, a

Your example is correct and how it should work. This is not super interesting without the connection operator for domains. However, this will force the question on if you even have to connect it or if the connection can be inferred.

@dtzSiFive
Copy link
Contributor

dtzSiFive commented Aug 28, 2025

Your example is correct and how it should work. This is not super interesting without the connection operator for domains.

Awesome! I meant this was maybe weird but is just the first way I came up with to instantiate a child with domain ports, and wasn't meant to be anything valid or not. That wasn't the point.

I forgot to mention this had a verifier error at the time of review.

However, this will force the question on if you even have to connect it or if the connection can be inferred.

Whoa, fascinating!

@seldridge
Copy link
Member Author

FWIW FIRRTL allows duplicate port names, previously had verifier that needed to be rolled back: #3976 .

I don't think this has been true for a while as the FIRRTL spec states how to handle the collisions (and provides an example of this exact case): https://github.com/chipsalliance/firrtl-spec/blob/main/spec.md#the-scalarized-convention The problem is that the type lowering passes don't properly implement the spec. The second a_b should be a_b_0 and CIRCT is relying on the Verilog emitter to do this uniquification when it should happen eagerly.

@dtzSiFive
Copy link
Contributor

dtzSiFive commented Aug 29, 2025 via email

@seldridge
Copy link
Member Author

Looking at that example again, the error seems to be due to something unrelated to the FIRRTL Dialect name collision. This is actually just dropping the domain information of the domain port. This will trip the verifier, but it looks unrelated to naming.

@seldridge
Copy link
Member Author

Looking at that example again, the error seems to be due to something unrelated to the FIRRTL Dialect name collision. This is actually just dropping the domain information of the domain port. This will trip the verifier, but it looks unrelated to naming.

Fixed in: 84f502f A port insertion utility was dropping the FlatSymbolRefAttr from a Domain type port when it should have passed it through. That commit also fixes issues identified with port insertion needing to not generate <<NULL ATTRIBUTE>> for ports. There's a messy discontinuity between the storage for PortInfo which tracks "no domains" as a null pointer and the underlying operation storage for domains which needs to be an empty ArrayAttr for this.

Add a new `firrtl.domain` op which defines the existence of a kind of
domain.  E.g., this can be used to declare abstract notions of a clock or
reset domain.

Signed-off-by: Schuyler Eldridge <[email protected]>
Add a type for representing a value that is a domain, e.g., a clock or
reset domain.  This is currnetly not creatable from any Chisel operations.

Signed-off-by: Schuyler Eldridge <[email protected]>
Add domain information to all FIRRTL module and instance ports.

Signed-off-by: Schuyler Eldridge <[email protected]>
Add a new operation, `firrtl.unsafe_domain_cast`, which is used to do
unsafe conversions to domains.  Conceptually, this acts like a node whose
input has unknown domain type and result has a known domain type.  This is
not a generic type ascription, as the domain to which the input is casted
does not affect the input at all.  The domain type is only assigned to the
result.

Add a sipmle canonicalizer that will replace an `unsafe_domain_cast` which
has no domains specified.  This operation is a no-op and there's no point
in keeping it around.

Signed-off-by: Schuyler Eldridge <[email protected]>
Copy link
Member

@uenoku uenoku left a comment

Choose a reason for hiding this comment

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

The IR design looks solid and good to me, great work! The change looks already large so I'm fine with merging the current state and incrementally fixing up remaining things.

In the follow could you add tests for Transforms to make sure these passes don't drop domain info? Also LowerTypes and LowerSignature could insert ports so the currently IR will be broken when there is a domain port in the middle of the ports. Could you open tracking issues for that?

AnnotationArrayAttr:$annotations,
DefaultValuedAttr<LayerArrayAttr, "{}">:$layers
DefaultValuedAttr<LayerArrayAttr, "{}">:$layers,
DefaultValuedAttr<ArrayRefAttr, "{}">:$domainInfo
Copy link
Member

Choose a reason for hiding this comment

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

Yes that makes sense .

node b = cat()

;// -----
FIRRTL version 5.1.0
Copy link
Member

Choose a reason for hiding this comment

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

Just a question but what FIRRTL version do you target for domain?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd target this for 6.0. However, this is currently nextFIRVersion which is 5.1.0. I'll deal with updating all this once the FIRRTL spec version solidifies.

Change domain ports to include a mandatory domain sort/kind, e.g., a
ClockDomain or a ResetDomain.

Signed-off-by: Schuyler Eldridge <[email protected]>
@seldridge seldridge force-pushed the dev/seldridge/firrtl-domains-operation-approach branch from 84f502f to df67311 Compare August 30, 2025 03:33
Update port updating methods to work with domains.

Signed-off-by: Schuyler Eldridge <[email protected]>
@seldridge seldridge force-pushed the dev/seldridge/firrtl-domains-operation-approach branch from df67311 to 7e543de Compare August 30, 2025 03:42
@seldridge
Copy link
Member Author

In the follow could you add tests for Transforms to make sure these passes don't drop domain info? Also LowerTypes and LowerSignature could insert ports so the currently IR will be broken when there is a domain port in the middle of the ports. Could you open tracking issues for that?

Good idea. Right now, this is only suitable for firtool -parse-only, circt-opt, and circt-translate -export-firrtl. Tracking issue to fix the bugs with insertion/deletion (and thereby get this working with the rest of the pipeline) here: #8906

@seldridge seldridge merged commit 7e543de into main Aug 30, 2025
7 checks passed
@seldridge seldridge deleted the dev/seldridge/firrtl-domains-operation-approach branch August 30, 2025 19:36
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.

4 participants