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
130 changes: 130 additions & 0 deletions abi.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,136 @@ bind Bar Bar_Layer1_Layer2 layer1_layer2(.notA(Bar.layer1.notA));
The `` `ifdef ``{.verilog} guards enable any combination of the bind files to be included while still producing legal SystemVerilog.
I.e., the end user may safely include none, either, or both of the bindings files.

## On Targets

A target will result in the creation of specialization files.
One specialization file per public module per option for a given target will be created.

Including a specialization file in the elaboration of Verilog produced by a FIRRTL compiler specializes the instantiation hierarchy under the associated public module with that option.

Each specialization file must have the following filename where `module` is the name of the public module, `target` is the name of the target, and `option` is the name of the option:

``` ebnf
filename = "targets_" , module , "_" , target , "_", option , ".vh" ;
```
Comment on lines +323 to +325
Copy link
Member Author

Choose a reason for hiding this comment

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

Switch to - delimiters to avoid conflicts with FIRRTL identifiers. This is the same trick we played with layer files.


Each specialization file will guard for multiple inclusion.
Namely, if two files that specialize the same target are included, this must produce an error.
If the file is included after a module which relies on the specialization, this must produce an error.

### Example

The following circuit is an example implementation of how a target can be lowered to align with the ABI defined above.
This circuit has one target named `Target` with two options, `A` and `B`.
Module `Foo` may be specialized using an instance choice that will instantiate `Bar` by default and `Baz` if `Target` is set to `A`.
If `Target` is set to `B`, then the default instantiation, `Bar`, will occur.
Module `Foo` includes a probe whose define is inside the instance choice.

``` firrtl
FIRRTL version 4.1.0
circuit Foo:

option Target:
case A
case B

module Baz:
output a: Probe<UInt<1>>

node c = UInt<1>(1)
define a = probe(c)

module Bar:
output a: Probe<UInt<1>>

node b = UInt<1>(0)
define a = probe(b)

public module Foo:
output a: Probe<UInt<1>>

instchoice x of Bar, Target:
A => Baz

define a = x.a
```

To align with the ABI, this must produce the following files to specialize the circuit for option `A` or option `B`, respectively:

- `targets_Foo_Target_A.sv`
- `targets_Foo_Target_B.sv`

Comment on lines +368 to +372
Copy link
Collaborator

@uenoku uenoku Mar 3, 2026

Choose a reason for hiding this comment

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

We may need to generate a header for default target as well since there would be a macro dependency from ref_*.sv to targets_*.sv generally (somehow integration tests in llvm/circt#9815 passed, but I feel it's very tool dependent, maybe verilator is lazily expanding macro?).

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, having a default is fine. That would need to have some filename that can't collide with the other options to make it work. Not a problem.

Thank you for thinking about this deeply!

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think i have three options:

  1. Reserve "Default" option (we could generate Default case in parser)
  2. Replace the concept of "Default Module" with "Default Option Case". Basically remove a default module from firrtl.instance_choice, and consider the first Option defined in OptionOp as default (if we want current behaivor Chisel can simply generate "Default" option case for the existing default module).
  3. Actually there is a hacky implementation that doesn't require header file for default case (basically we can get real verilog name instance for default case with inner symbol).

What follows describes a possible implementation that aligns with the ABI.
Note that the internal details are not part of the ABI.

When compiled, this produces the following Verilog:

``` systemverilog
module Baz(output a);
assign a = 1'h1;
endmodule

module Bar(output a);
assign a = 1'h0;
endmodule

// Defines for the instance choices
`ifndef __target_Target_foo_x
`define __target_Target_foo_x Bar
`endif
Copy link
Collaborator

Choose a reason for hiding this comment

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

comment for link.


module Foo();

`__target_Target_foo_x x();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need this macro replacement for module name (which requires a additional very specialized operation to HW)? Can we do that with a plain if-def chains?


endmodule
```

The contents of the two option enabling files are shown below:

``` systemverilog
// Contents of "targets_Target_A.vh"
`ifdef __target_Target_foo_x
`ERROR__target_Target_foo_x__must__not__be__set
`else
`define __target_Target_foo_x Baz
`endif

`ifdef __targetref_Foo_x_a a
`ERROR__targetref_Foo_x_a__must__not__be__set
`else
`define __targetref_Foo_x_a a
Copy link
Collaborator

@uenoku uenoku Feb 28, 2026

Choose a reason for hiding this comment

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

Note that we cannot do emit __targetref in a header if we want to support nested instance choices.

FIRRTL version 5.1.0
circuit Top :
  option Platform :
    FPGA
  option Optimization : 
    Fast
  

  module DefaultTarget : 
    output probe : Probe<UInt<8>> 

    wire r : UInt<8> 
    connect r, UInt<8>(0) 
    define probe = probe(r) 

  module FastTarget : 
    output probe : Probe<UInt<8>> 
    wire w : UInt<8> 
    connect w, UInt<8>(1) 
    define probe = probe(w) 
   
  module FPGATarget : 
    output probe : Probe<UInt<8>> 

    instchoice opt_inst of DefaultTarget, Optimization:
      Fast => FastTarget
    define probe = opt_inst.probe


  public module Top : 
    output probe: Probe<UInt<8>> 
    instchoice platform_inst of DefaultTarget, Platform:
      FPGA => FPGATarget
    define probe = platform_inst.probe
// in targets_Platform_FPGA.svh
`define __targetref_Top_platform_inst_probe `__target_Platform_Top_platform_inst.`__targetref_FPGATarget_opt_inst_probe

__targetref_FPGATarget_opt_inst_probe is defined in targets_Optimization_Fast.svh, and cyclic dependency could be easily introduced if there is another nested instance choice op that went through Optimization -> Platform option path.

Copy link
Member Author

Choose a reason for hiding this comment

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

Nice find! Can we break the file inclusion problem by requiring the ordering of the files to be:

  1. Individual instance choice headers which only set instances
  2. and a unified ref header which sets all refs based on instance choices.

Since we've backtracked on the need to hide all instance choices, and instead can handle that with a dedicated tool, this might work. It does mean that the ref header is going to be complex and likely have to mirror the instantiation hierarchy and the options along the way.

Alternatively, (1) could include partial ref paths which are assembled into the final paths in (2).

Copy link
Collaborator

Choose a reason for hiding this comment

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

Individual instance choice headers which only set instances
and a unified ref header which sets all refs based on instance choices.

That is close to what I've implemented so far (= simply inlining paths in ref_*_*.sv files with if-def guards https://gist.github.com/uenoku/d47bd68320dc2cd424ff42df27ed91a4).

The issue with this approach it's necessary to hide specific options/probes from ref files, but yes, it's possible to do with post-process.

`endif
```

``` systemverilog
// Contents of "targets_Target_B.vh"
`ifdef __target_Target_foo_x
`ERROR__target_Target_foo_x__must__not__be__set
`endif

// This file has no defines.
```

Additionally, probe on public module `Foo` requires that the following file is produced:

``` systemverilog
// Contents of "refs_Foo.sv"
`ifndef __targetref_Foo_x_a
`define __targetref_Foo_x_a b
`endif

`define ref_Foo_x x.`__targetref_Foo_x_a
```

If neither of the option enabling files are included, then `Bar` will by default be instantiated.
If `targets_Foo_Target_A.vh` is included before elaboration of `Foo`, then `Baz` will be instantiated.
If `targets_Foo_Target_B.vh` is included before elaboration of `Foo`, then `Bar` will be instantiated.
If both `targets_Foo_Target_A.vh` and `targets_Foo_Target_B.vh` are included, then an error (by means of an undefined macro error) will be produced.
If either `targets_Foo_Target_A.vh` or `targets_Foo_Target_B.vh` are included after `Foo` is elaborated, then an error will be produced.

If `ref_Foo.vh` is included before either `targets_Foo_Target_A.vh` or `targets_Foo_Target_B.vh`, then an error will be produced.

## On Types

Types are only guaranteed to follow this lowering when the Verilog type is on an element which is part of the ABI defined public elements.
Expand Down
3 changes: 3 additions & 0 deletions include/firrtl.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<item>extmodule</item>
<item>layer</item>
<item>formal</item>
<item>option</item>
</list>
<list name="keywords">
<item>input</item>
Expand All @@ -28,6 +29,8 @@
<item>public</item>
<item>enablelayer</item>
<item>bound</item>
<item>instchoice</item>
<item>option</item>
</list>
<list name="types">
<item>UInt</item>
Expand Down
2 changes: 2 additions & 0 deletions revision-history.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ revisionHistory:
# additions to the specification should append entries here.
thisVersion:
spec:
- Add instance choice.
abi:
- Add instance choice.
# Information about the old versions. This should be static.
oldVersions:
- version: 4.0.0
Expand Down
87 changes: 87 additions & 0 deletions spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,39 @@ circuit Foo :
assume(clk, gt(a, UInt(2)), UInt<1>(h1), "input a > 2")

formal testFoo of FooTest :

## Targets

A `target`{.firrtl} describes one way that a FIRRTL circuit may be specialized for a certain use case.
Here, specialization means choosing a specific option for a target.

It is often desirable to have one FIRRTL circuit that has different logic when simulated, synthesized and mapped to a field-programmable gate array (FPGA), or synthesized to a given process technology and standard cell library.
While this per-target customizability can be expressed and specialized in a frontend language that produces FIRRTL, it is often desirable to expose the target specialization in the FIRRTL.
By delaying the specialization, the specialization can either be done by a FIRRTL compiler or exposed in the artifacts of a FIRRTL compiler for specialization by the consumer.

Practically, targets describe a limited form of parameterization and, if not specialized by a FIRRTL compiler, allow for FIRRTL compilers to generate parametric artifacts (e.g., parametric Verilog).

The `option`{.firrtl} keyword declares an option group, which contains `case`{.firrtl} declarations naming the settings allotted to that option.
The circuit can be specialized for a single case of a given option at any time.
Multiple option groups can be declared to capture orthogonal dimensions of configuration.

Specialization can occur either in the compiler or it can be materialized in the lowering.
For details, consult the FIRRTL ABI specification.
Specialization is not mandatory: options can be left unspecified, resorting to explicitly-defined default behaviour.

A target may be declared using the `target`{.firrtl} keyword.
The available options for which a target may take are listed using the `option`{.firrtl} keyword.
An example FIRRTL circuit showing two targets, `Platform` and `Performance`, and their allowable options is shown below:

``` firrtl
circuit:
target Platform:
option FPGA
option ASIC

target Performance:
option Slow
option Fast
```

# Circuit Components
Expand Down Expand Up @@ -518,6 +551,47 @@ circuit Foo:
;; snippetend
```

#### Instance Choice

An instance choice is a submodule instance where the choice of submodule is conditioned based on the value of a `target`{.firrtl}.
This enables per-instance specialization for different targets.
Additionally, this is a mechanism for module replacement.

An instance choice declaration specifies the instance name and names the option group based on which the choices are selected.
A default module must be provided.
The default module is instantiated by the instance choice when an option for its associated target is not specified.
Subsequently, modules can be specified for the known choices of the selected option group.
An instance choice does not need to specify modules for all cases.
The instantiated modules must be either modules or external modules.

An example of an instance choice is shown below.
This instance choice is conditioned on the `Platform` target.
By default, it will instantiate `DefaultClockGate` and when `Platform` is `FPGA` it will instantiate `FPGAClockGate`.

``` firrtl
circuit:
target Platform:
option FPGA
option ASIC

module DefaultClockGate:
input clock_in: Clock
output clock_out: Clock
input enable: UInt<1>

extmodule FPGAClockGate:
input clock_in: Clock
output clock_out: Clock
input enable: UInt<1>

module InstanceChoice:
instchoice clock_gate of DefaultClockGate, Platform:
FPGA => FPGAClockGate
```

The type of an instance choice is the same as an instantiation of the default module.
The ports of all module choices must be the same as the default module.

### Memories

Memories are stateful elements of a design.
Expand Down Expand Up @@ -4270,6 +4344,7 @@ decl =
| decl_extmodule
| decl_layer
| decl_formal
| decl_option
| decl_type_alias ;

decl_module =
Expand Down Expand Up @@ -4302,6 +4377,12 @@ decl_formal_param =
| "[" , [ decl_formal_param , { "," , decl_formal_param } ] , "]"
| "{" , [ id , "=" , decl_formal_param , { "," , id , "=" , decl_formal_param } ] , "}"

decl_option =
"option" , id , ":" , [info] , newline, indent ,
{ "case" , id , ":" , [ info ] , newline } ,
dedent ;


decl_type_alias = "type", id, "=", type ;

port = ( "input" | "output" ) , id , ":" , (type | type_property) , [ info ] ;
Expand All @@ -4323,11 +4404,17 @@ circuit_component =
| circuit_component_wire
| circuit_component_reg
| circuit_component_inst
| circuit_component_instchoice
| circuit_component_mem ;

circuit_component_node = "node" , id , "=" , expr , [ info ] ;
circuit_component_wire = "wire" , id , ":" , type , [ info ] ;
circuit_component_inst = "inst" , id , "of" , id , [ info ] ;
circuit_component_instchoice =
"instchoice" , id , "of" , id , "," , id , ":" , newline ,
indent ,
{ id , "=>" , id , newline } ,
dedent;

circuit_component_reg =
"reg" , id , ":" , type , "," , expr , [ info ]
Expand Down