From hardware IRs to application-specific arithmetic
ACM Europe School on MLIR 2026 · A Coruña
INSA Lyon · INRIA, Emeraude
University of Rennes · IRISA · INRIA, Taran
10–14 August 2026
$ whoami ~5 min430.1 MiB download · 1.46 GiB unpacked · course files included in /workspace
73.5 MiB Git clone · additional downloads depend on the Docker cache
Favorite IDE? Clone anywhere, then add -v "$PWD:/workspace" to docker run.
$ whoamiNot an MLIR expert … :')

But also an artist.
Why does everyone make them square?
MLIR
LLVM
OpenROAD placement as a visual medium.
This section explains why modern compiler techniques must reach into hardware.
Layers: follow arithmetic choices from programs toward circuits.
Scaling limits: understand why performance no longer arrives for free.
Specialization: connect GPUs, TPUs, and new number formats.
One problem, many representations
Each layer exposes different decisions.
Software and hardware are intertwined and multi-level.
1. Optimize a fixed machine
Algebraic simplification: \(x \times 1 \rightarrow x\).
Strength reduction: \(x \times 2 \rightarrow x \ll 1\).
Instruction selection: fused multiply-add.
A compiler usually sees a local window.
2. Build the machine
High-level synthesis crosses the ISA boundary.
Specialize operators: \(x^2\), \(x/3\).
Fuse operators and map them to LUTs or DSPs.
More hardware freedom means more opportunities.
3. Traverse the stack
MLIR dialects keep domain intent explicit.
Lowerings connect many domain-specific representations.
One infrastructure can move from problem to circuit.
MLIR offers a larger view.
This presentation lives here!
Logic / RTL: hardware structure, datapaths, and state.
Circuits: Boolean networks, synthesis, and cost.
Hands-on: lower arithmetic operations into explicit circuits.
Quiz time
Who has seen a version of this plot before?
Fifty years of microprocessor trends. (Rupp 2022)
Fifty years of microprocessor trends. (Rupp 2022)
Quiz time
Who has heard these ADAges?
“Dark silicon era”
“Memory wall”
“Communication dominates arithmetic”
“End of the laws”
Quiz time
Who knows these machines?

GPU: matrix-multiply instructions backed by dedicated Tensor Core hardware. (NVIDIA 2022)

TPU: a processor built around matrix-multiply hardware. (Dean and Hölzle 2017; Jouppi et al. 2020)
Specialization puts more of the problem directly into the machine.
New accelerators, new compiler stacks: one origin of MLIR.
These concepts become CIRCT vocabulary in the next section.
Logic: combinational operations and bit widths.
Time: registers, clocks, and timing.
Structure: modules, instances, and repeated hardware.
Targets: FPGA fabrics and ASIC geometry.
Quick show of hands
Who has already:
Essential follow-up
Who has never heard any of those words?
So I know exactly how much to panic.
We will use tiny SystemVerilog examples only to make the hardware visible.
Question
Which line runs first?
Neither line runs first.
product reaches it.Compiler takeaway
Dependency analysis recovers the graph, independent of source order.
Question
For a = 15 and b = 1, what values reach wrapped and exact?
q change?Question
This register samples d on a rising edge. If d changes between two edges, when can q change?
Basic D-Q flip-flop: sample d on each rising edge.
Falling-edge flip-flop: move the sampling instant.
Synchronous reset: observe reset on the clock edge.
Asynchronous, active-low reset: reset immediately when rst_n falls.
At the next rising edge. Between edges, q stores its previous value.
Changing posedge to negedge changes when the state may update.
Reset may assert between edges, but this flip-flop applies it only at the next rising edge.
Edge, reset timing, and reset polarity are hardware choices.
Compiler takeaway
Analyses can match these semantics to flip-flops in the target library.
Question
A processor is clocked at 2 GHz. How much time separates two rising edges?
\[ T = \frac{1}{f} = \frac{1}{2\,\text{GHz}} = 0.5\,\text{ns} = \mathbf{500\,\text{ps}} \]
Example cell delays
OR2_1: 61 / 189 ps (rise / fall).LUT6: 119 ps (no routing).\[ t_{\text{clk} \rightarrow Q} + t_{\text{logic}} + t_{\text{routing}} + t_{\text{setup}} + t_{\text{skew}} < 500\,\text{ps} \]
The period is a budget for the longest register-to-register path.
Compiler takeaway
Analyze the critical path \(\rightarrow\) insert registers \(\rightarrow\) balance stages.
Question
Software calls a function. What is the equivalent mechanism in hardware?
Software: call a function. Hardware: instantiate a module.
mac4 becomes a reusable hardware definition.u_mul names this instance of mul4.Question
Nine calls to one Processing Element (PE), or nine independent PEs?
Nine independent PEs, operating in parallel.
generate repeats structure during elaboration, before the circuit operates.
Field-Programmable Gate Array
(the soft/easy hardware)

Question
Which two-input gate is stored here?
Truth table
a |
b |
stored output |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 1 |
OR. A LUT2 stores \(2^2=4\) bits; a LUT6 stores \(2^6=64\) bits.
Compiler takeaway
Synthesis decomposes the function, placement chooses the resources, and routing connects them into one circuit.

Virtex UltraScale+ VU19P
Millions of configurable resources, memories, DSPs, and wires become the circuit described by one bitstream.
Compiler takeaway
Compilation can take several days.
Application-Specific Integrated Circuit
(the hard hardware)
Question
What hardware object is shown here?


RTL becomes geometry through a sequence of compiler passes and physical-design algorithms.
They are friends: just knock on their doors. Cheap, modern paths to a real chip.
TL;DR: Apply MLIR and LLVM compiler techniques to hardware-design tools.
T may be Tool, Translator, Team, Technology, Target, Tree, Type, …
CIRCT Core:
hw: structure and hierarchycomb: combinational logicseq: registers and stateBut also many other dialects:
tensor, linalg → func, scf, arithNext: use the CIRCT tools.
This section follows the ARITH 2026 CIRCT Tutorial by Samuel Coward and me.
Basics: circt-verilog, circt-opt, circt-lec, and firtool.
Synthesis & Reports: circt-synth, AIGER export, and area estimation.
Exercise 1: Compile a SystemVerilog design to CIRCT IR. (\(\approx\) 3 min.)
Exercise 2: Narrow a combinational datapath with range analysis. (\(\approx\) 5 min.)
Exercise 3: Verify that a transformation preserved the circuit. (\(\approx\) 5 min.)
Exercise 4: Generate SystemVerilog for downstream EDA tools. (\(\approx\) 5 min.)
First tool: circt-verilog parses SystemVerilog and prints CIRCT IR.
Let’s inspect the “FMA” module.
(SystemVerilog support remains an ongoing effort documented by Chips Alliance sv-tests.)
… and its MLIR/CIRCT counterpart
Unlike C or SystemVerilog, comb does not implicitly promote arithmetic operands.
The operands and result of comb.mul and comb.add have the same width.
comb.concat makes each extension explicit in the IR.
A later analysis can therefore prove that those extra bits are unnecessary and narrow the circuit. (Exercise 2 ? Maybe ? Who knows ?)
Question
The imported IR uses i9 throughout the datapath. How many bits can these values actually require?
| Value | Possible unsigned values | Required width |
|---|---|---|
%a, %b, %c |
\([0, 15]\) | 4 bits |
%a * %b |
\([0, 225]\) | 8 bits |
%a * %b + %c |
\([0, 240]\) | 8 bits |
The ninth bit belongs to the module interface, not to the internal arithmetic.
comb-int-range-narrowing makes that range argument mechanically:
def CombIntRangeNarrowing : Pass<"comb-int-range-narrowing"> {
let summary = "Reduce comb op bitwidth based on integer range analysis.";
let description = [{
Compute a basic value range analysis, by propagating integer intervals
through the domain.
The analysis is limited by a lack of sign-extension operator in the comb
dialect, leading to an over-approximation.
Particularly for signed arithmetic, a single interval is often an
over-approximation, a more precise analysis would require a union of
intervals.
}];
}For this zero-extended datapath, one interval per SSA value is precise enough; the signed-arithmetic limitation does not apply yet.
The order is intentional:
comb.extract or comb.concat where narrow operations meet wider values.--comb-int-range-narrowinghw.module @ex1_fma(
in %a : i4, in %b : i4, in %c : i4,
out d : i9) {
%false = hw.constant false
%c0_i5 = hw.constant 0 : i5
%0 = comb.concat %c0_i5, %a : i5, i4
%1 = comb.concat %c0_i5, %b : i5, i4
%2 = comb.extract %0 from 0 : (i9) -> i8
%3 = comb.extract %1 from 0 : (i9) -> i8
%4 = comb.mul %2, %3 : i8
%5 = comb.concat %false, %4 : i1, i8
%6 = comb.concat %c0_i5, %c : i5, i4
%7 = comb.extract %5 from 0 : (i9) -> i8
%8 = comb.extract %6 from 0 : (i9) -> i8
%9 = comb.add %7, %8 : i8
%10 = comb.concat %false, %9 : i1, i8
hw.output %10 : i9
}+ --canonicalizehw.module @ex1_fma(
in %a : i4, in %b : i4, in %c : i4,
out d : i9) {
%false = hw.constant false
%c0_i4 = hw.constant 0 : i4
%0 = comb.concat %c0_i4, %a : i4, i4
%1 = comb.concat %c0_i4, %b : i4, i4
%2 = comb.mul %0, %1 : i8
%3 = comb.concat %c0_i4, %c : i4, i4
%4 = comb.add %2, %3 : i8
%5 = comb.concat %false, %4 : i1, i8
hw.output %5 : i9
}Now reverse the two passes:
Before reading the diff: will the semantics change? Will the operation count change? What single extra pass would recover the first output?
Answer: the arithmetic still narrows to i8, but late narrowing inserts comb.extract and comb.concat operations after canonicalization has already finished. The semantics are unchanged, the IR is larger, and a final --canonicalize recovers the first output.
Of course, you do not trust research tools.
And industrial tools are always exact, except when their documentation lists IEEE-754 partial compliance and deviations.
CIRCT lets us mark our own homework with a logical equivalence checker: circt-lec.
circt-leccirct-lec work?Quiz time
Who knows what a miter is?

Construct a circuit miter: drive both circuits with the same inputs and compare their outputs.
Lower the miter and both circuits to SMT: Satisfiability Modulo Theories.
Ask Z3 (an SMT solver) whether an input can make the outputs differ. unsat means there is no counterexample: c1 == c2.
For our small circuits: same or not same :)
Introduce a functional bug while keeping the circuit valid and compilable. Then check that circt-lec reports c1 != c2.
Answer: Change the multiply to an add, alter a constant, or disconnect an operand. The current tool reports inequivalence but does not conveniently print the counterexample.
firtoolA useful CIRCT flow is:
circt-opt and verify the transformations.firtool for downstream FPGA or ASIC tools.exercises/ex2_fma_optimized.mlir is the output saved in Exercise 2.
firtool command to save the generated Verilog.circt-verilog.circt-lec to prove that the round trip preserved equivalence.AIGs · equivalence · structural cost
comb to an AIGhw + comb \(\xrightarrow{\texttt{circt-synth}}\) synth.aig + reports
Logic synthesis lowers adders, muxes, and other operations into Boolean logic, then simplifies and shares that logic.
An And-Inverter Graph (AIG) contains:
The synth dialect represents the graph. AIGER is only a file format used to exchange it. (Mishchenko et al. 2006)
All operations are modulo \(2^8\).
Create exercises/ex5_aig_optimized.sv with the same module name and ports.
Baseline: 149 AIG nodes · 13 logic levels
Task: preserve c1 == c2, but build a smaller and shallower AIG.
All arithmetic is eight-bit modulo \(2^8\):
\[ -(x + 1) = \mathord{\sim}x \qquad -x = \mathord{\sim}x + 1 \]
Apply these identities to the two negative rows.
Original table
a |
b |
out |
|---|---|---|
| 0 | 0 | x |
| 0 | 1 | x + 1 |
| 1 | 0 | -(x + 1) |
| 1 | 1 | -x |
Simplified table
a |
b |
out |
|---|---|---|
| 0 | 0 | x |
| 0 | 1 | x + 1 |
| 1 | 0 | ~x |
| 1 | 1 | ~x + 1 |
Question
Which input selects x or ~x? Which input selects +0 or +1?
Simplified behavior
a |
b |
out |
|---|---|---|
| 0 | 0 | x |
| 0 | 1 | x + 1 |
| 1 | 0 | ~x |
| 1 | 1 | ~x + 1 |
Question
How can a select x or ~x, while b selects +0 or +1?
1. Replicate a into an eight-bit mask
{8{a}} is 8'b00000000 when a = 0, and 8'b11111111 when a = 1.
2. Use XOR to select x or ~x
base = x ^ {8{a}}
3. Let b add zero or one
out = base + b
Import your implementation, then check equivalence and measure both AIGs:
The optimization removes 95 AIG nodes and 5 logic levels without changing the function.
“Be fruitful, and multiply.”
Until now, we used existing CIRCT passes. Now we write a lowering.
arith.mulf : f8E4M3FN \(\longrightarrow\) Python rewrite \(\longrightarrow\) hw + comb circuit
Exercise 6 — warm-up: go from upstream func + arith MLIR to a hardware circuit.
Exercise 7 — build: construct and test an E4M3 multiplier datapath.
Exercise 8 — specialize (optional): detect x * x and generate a cheaper squarer.
To reach that objective, two things must change:
func.func and return must become a hardware module boundary.arith.muli must become a combinational multiplier.Does upstream circt-opt provide both transformations?
| File | Boundary | Multiplication |
|---|---|---|
ex6_arith_muli.mlir |
func.func |
arith.muli |
ex6_hw_arith.mlir |
hw.module |
arith.muli |
ex6_hw_comb.mlir |
hw.module |
comb.mul |
Boundary first; multiplication second.
firtool.Run the same two-stage pipeline on exercises/ex6_arith_mulf.mlir:
map-arith-to-comb draws a deliberate linedef MapArithToCombPass : Pass<"map-arith-to-comb"> {
let summary = "Map arith ops to combinational logic";
let description = [{
A pass which does a simple `arith` to `comb` mapping wherever possible.
This pass will not convert:
* floating point operations
* operations using `vector`-typed values
This does not intend to be the definitive lowering/HLS pass of `arith`
operations in CIRCT (hence the name "map" instead of e.g. "lower").
Rather, it provides a simple way (mostly for testing purposes) to map
`arith` operations.
}];
}CIRCT Transforms/Passes.td, pinned firtool-1.147.0 source
The failure is the documented boundary of the pass. Our exercise supplies one concrete floating-point lowering.
f8E4M3FN| Part | Meaning |
|---|---|
f8 |
8-bit floating-point type |
E4 |
4 exponent bits |
M3 |
3 stored mantissa/fraction bits |
FN |
F = finite; N = NaN. No infinity encoding. |
Overflow therefore produces NaN instead of infinity.
s
[7]
sign · 1 bit
eeee
[6:3]
biased exponent · 4 bits
mmm
[2:0]
stored fraction · 3 bits
| Regime | Encoding | Value |
|---|---|---|
| zero | e = 0000, m = 000 |
\(0\) |
| subnormal | e = 0000, m != 000 |
\((-1)^s 2^{-6}(m/8)\) |
| normal | e != 0000 |
\((-1)^s 2^{e-7}(1+m/8)\) |
| NaN | e = 1111, m = 111 |
not a number |
f8E4M3FN has bias 7, finite values up to 448, and no infinity. (Micikevicius et al. 2022)
You may remember binary scientific notation:
Subnormal: keep \(e=-6\) and allow \(0.mmm_2 \times 2^{-6}\); seven values fill the gap gradually.
Exercise 7: exact normal finite products only.
Set aside: zero, subnormals, NaNs, overflow/underflow, and rounding.
| I need to… | Use |
|---|---|
| Edit locally; run in Docker | docker run -it -v "$PWD:/workspace" ghcr.io/bynaryman/mlir-summer-school-2026-circt:latest |
| Build and run the supplied pipeline | ./scripts/build.sh && ./scripts/run.sh |
| SystemVerilog -> MLIR | circt-verilog in.sv -o out.mlir |
| Apply passes | circt-opt in.mlir --PASS -o out.mlir |
| MLIR -> SystemVerilog | firtool in.mlir -o out.sv |
| Prove equivalent | circt-lec --c1 TOP before.mlir --c2 TOP after.mlir |
| Compare synthesis statistics | ./scripts/compare-aig.py BEFORE.mlir AFTER.mlir TOP |
| Run the Python lowering | python scripts/lower-e4m3fn.py in.mlir -o out.mlir |
| Create XOR | x = comb.XorOp.create(a, b).result |
| Replace an operation | rewriter.replace_op(op, [value]) |
| Test the E4M3 normal path | ./scripts/test-e4m3-all.py --pass --normal-path |
Input excerpt: exercises/ex7_e4m3fn_mul.mlir
Root the rewrite at the final arith.bitcast: the matched island and its replacement then have the same i8 boundary.
Pass skeleton: scripts/lower-e4m3fn.py
Supplied: matcher and integer type aliases · API: official CIRCT Python bindings · Scope: exact normal finite products; no rounding.
For a normal, finite value, let \(E\) be the stored exponent, \(B=7\), and \(M\) the significand:
\[x=\color{#7c3aed}{(-1)^s}\, \color{#2563eb}{2^{E-B}}\, \color{#0f766e}{M}, \qquad \color{#0f766e}{M=(1.\mathtt{mmm})_2}\]
Multiplying two values exposes the three datapaths we must build:
\[ab= \color{#7c3aed}{\underbrace{(-1)^{s_a\oplus s_b}}_{\text{sign}}} \;\color{#2563eb}{\underbrace{2^{(E_a-B)+(E_b-B)}}_{\text{exponent}}} \;\color{#0f766e}{\underbrace{(M_aM_b)}_{\text{significand}}}\]
The significand product may need normalization; that correction is then added to the result exponent.
\[(-1)^{s_a}(-1)^{s_b}=(-1)^{s_a+s_b} \quad\Longrightarrow\quad s_r=s_a\oplus s_b\]
First make the rewrite compile. Extract both signs, XOR them, and set the remaining seven output bits to zero.
Sign path
The long purple wire is now a real SSA value from both inputs to the packed result.
def lower_e4m3_mul(out_cast, rewriter):
matched = match_e4m3_island(out_cast)
if matched is None:
return True
lhs, rhs = matched
with rewriter.ip:
sa = comb.ExtractOp.create(7, i1, lhs).result
sb = comb.ExtractOp.create(7, i1, rhs).result
sign = comb.XorOp.create(sa, sb).result
zero7 = hw.ConstantOp.create(i7, 0).result
result = comb.ConcatOp.create(sign, zero7).result
rewriter.replace_op(out_cast, [result])
return False
Normalized scientific notation keeps one non-zero digit before the point. In binary that digit can only be 1, so every normal significand is 1.mmm₂. Because this leading 1 is predictable, E4M3 stores only mmm.
\[M=1+\frac{(\mathtt{mmm})_2}{2^3}=(1.\mathtt{mmm})_2, \qquad \boxed{P=M_aM_b}\]
Your step: prepend the implicit 1, form both 4-bit significands, then produce the full-width %product : i8. comb.mul returns the operand width.
Reconstruct 1.mmm₂, zero-extend both 4-bit significands to the result width, then multiply them as integers.
\[M=1+\frac{(\mathtt{mmm})_2}{2^3}=(1.\mathtt{mmm})_2, \qquad P=M_aM_b\]
Solution
fa = comb.ExtractOp.create(0, i3, lhs).result
fb = comb.ExtractOp.create(0, i3, rhs).result
one = hw.ConstantOp.create(i1, 1).result
ma4 = comb.ConcatOp.create(one, fa).result
mb4 = comb.ConcatOp.create(one, fb).result
zero4 = hw.ConstantOp.create(i4, 0).result
ma8 = comb.ConcatOp.create(zero4, ma4).result
mb8 = comb.ConcatOp.create(zero4, mb4).result
product = comb.MulOp.create(ma8, mb8).result
def lower_e4m3_mul(out_cast, rewriter):
matched = match_e4m3_island(out_cast)
if matched is None:
return True
lhs, rhs = matched
with rewriter.ip:
sa = comb.ExtractOp.create(7, i1, lhs).result
sb = comb.ExtractOp.create(7, i1, rhs).result
sign = comb.XorOp.create(sa, sb).result
fa = comb.ExtractOp.create(0, i3, lhs).result
fb = comb.ExtractOp.create(0, i3, rhs).result
one = hw.ConstantOp.create(i1, 1).result
ma4 = comb.ConcatOp.create(one, fa).result
mb4 = comb.ConcatOp.create(one, fb).result
zero4 = hw.ConstantOp.create(i4, 0).result
ma8 = comb.ConcatOp.create(zero4, ma4).result
mb8 = comb.ConcatOp.create(zero4, mb4).result
product = comb.MulOp.create(ma8, mb8).result
zero7 = hw.ConstantOp.create(i7, 0).result
result = comb.ConcatOp.create(sign, zero7).result
rewriter.replace_op(out_cast, [result])
return False
ma4 and mb4 encode 1.mmm₂: four stored bits with three fractional positions. Their eight-bit integer product therefore has six fractional positions; the binary point is implicit, not stored.
Stored %product : i8 |
Test | Keep | Exponent |
|---|---|---|---|
01xxxxxx₂ |
%product[7] = 0 |
%product[6:3] |
+ 0 |
1xxxxxxx₂ |
%product[7] = 1 |
%product[7:4] |
+ 1 |
| Product: stored bits (UQ2.6) | Decimal | Shift |
|---|---|---|
1100 × 1000 = 01100000 (01.100000) |
→ 1.5 | no |
1100 × 1100 = 10010000 (10.010000) |
→ 2.25 | yes |
1111 × 1111 = 11100001 (11.100001) |
→ 3.515625 | yes |
%product[7] is 1 when the product is greater than or equal to \(2\), outside the normalized significand interval \([1,2)\). Selecting the upper four bits divides it by two; incrementing the exponent preserves the value.
Your step: extract both four-bit windows, select one with %product[7], produce %frac : i3, and retain that bit as %norm : i1 for the exponent.
\[n=\begin{cases}0 & P<2\\1 & P\ge 2\end{cases} \qquad M_r=P\,2^{-n}\]
%product[7] = 0 keeps %product[6:3]; %product[7] = 1 keeps %product[7:4]. Retain that same bit as the exponent correction.
def lower_e4m3_mul(out_cast, rewriter):
matched = match_e4m3_island(out_cast)
if matched is None:
return True
lhs, rhs = matched
with rewriter.ip:
sa = comb.ExtractOp.create(7, i1, lhs).result
sb = comb.ExtractOp.create(7, i1, rhs).result
sign = comb.XorOp.create(sa, sb).result
fa = comb.ExtractOp.create(0, i3, lhs).result
fb = comb.ExtractOp.create(0, i3, rhs).result
one = hw.ConstantOp.create(i1, 1).result
ma4 = comb.ConcatOp.create(one, fa).result
mb4 = comb.ConcatOp.create(one, fb).result
zero4 = hw.ConstantOp.create(i4, 0).result
ma8 = comb.ConcatOp.create(zero4, ma4).result
mb8 = comb.ConcatOp.create(zero4, mb4).result
product = comb.MulOp.create(ma8, mb8).result
norm = comb.ExtractOp.create(7, i1, product).result
direct = comb.ExtractOp.create(3, i4, product).result
shifted = comb.ExtractOp.create(4, i4, product).result
mant = comb.MuxOp.create(
norm.value, shifted.value, direct.value).result
frac = comb.ExtractOp.create(0, i3, mant).result
result = comb.ConcatOp.create(sign, zero4, frac).result
rewriter.replace_op(out_cast, [result])
return False
Notation reminder: \(E\) is the stored exponent field, \(B=7\) is the bias, and \(e=E-B\) is the real exponent.
\[ E_a+E_b=(e_a+B)+(e_b+B)=e_a+e_b+2B \]
But the result must store only \(e_a+e_b+B\). The sum therefore contains one bias too many, so we subtract \(B\) once:
\[E_{\text{base}}=E_a+E_b-B.\]
Example: \(2.0\) stores \(E_a=8\) and \(0.5\) stores \(E_b=6\). Their product is \(1.0\), whose stored exponent is \(7\): \(8+6-7=7\).
Unbiasing both inputs is also correct: \((E_a-7)+(E_b-7)+7\). It simplifies to the same expression above, which exposes the smaller datapath directly.
The blue box receives the two stored exponents and the normalization bit. It must produce %exp : i4:
\[ E_r=E_a+E_b-7+\mathtt{\%norm} \]
Your step: extract %ea and %eb, compute the expression above with five-bit intermediates, then extract the low four bits as %exp.
\[ E_{\text{base}}=E_a+E_b-7, \qquad E_r=E_{\text{base}}+\mathtt{\%norm} \]
%norm is the bit computed in the previous stage: it is 1 exactly when the significand product was shifted right by one. Use five-bit intermediates to preserve the carry.
Exponent path
ea = comb.ExtractOp.create(3, i4, lhs).result
eb = comb.ExtractOp.create(3, i4, rhs).result
zero1 = hw.ConstantOp.create(i1, 0).result
ea5 = comb.ConcatOp.create(zero1, ea).result
eb5 = comb.ConcatOp.create(zero1, eb).result
sum_ = comb.AddOp.create(ea5, eb5).result
bias = hw.ConstantOp.create(i5, 7).result
base = comb.SubOp.create(sum_, bias).result
norm5 = comb.ConcatOp.create(zero4, norm).result
adjusted = comb.AddOp.create(base, norm5).result
exp = comb.ExtractOp.create(0, i4, adjusted).result
def lower_e4m3_mul(out_cast, rewriter):
matched = match_e4m3_island(out_cast)
if matched is None:
return True
lhs, rhs = matched
with rewriter.ip:
sa = comb.ExtractOp.create(7, i1, lhs).result
sb = comb.ExtractOp.create(7, i1, rhs).result
sign = comb.XorOp.create(sa, sb).result
fa = comb.ExtractOp.create(0, i3, lhs).result
fb = comb.ExtractOp.create(0, i3, rhs).result
one = hw.ConstantOp.create(i1, 1).result
ma4 = comb.ConcatOp.create(one, fa).result
mb4 = comb.ConcatOp.create(one, fb).result
zero4 = hw.ConstantOp.create(i4, 0).result
ma8 = comb.ConcatOp.create(zero4, ma4).result
mb8 = comb.ConcatOp.create(zero4, mb4).result
product = comb.MulOp.create(ma8, mb8).result
norm = comb.ExtractOp.create(7, i1, product).result
direct = comb.ExtractOp.create(3, i4, product).result
shifted = comb.ExtractOp.create(4, i4, product).result
mant = comb.MuxOp.create(
norm.value, shifted.value, direct.value).result
frac = comb.ExtractOp.create(0, i3, mant).result
ea = comb.ExtractOp.create(3, i4, lhs).result
eb = comb.ExtractOp.create(3, i4, rhs).result
zero1 = hw.ConstantOp.create(i1, 0).result
ea5 = comb.ConcatOp.create(zero1, ea).result
eb5 = comb.ConcatOp.create(zero1, eb).result
sum_ = comb.AddOp.create(ea5, eb5).result
bias = hw.ConstantOp.create(i5, 7).result
base = comb.SubOp.create(sum_, bias).result
norm5 = comb.ConcatOp.create(zero4, norm).result
adjusted = comb.AddOp.create(base, norm5).result
exp = comb.ExtractOp.create(0, i4, adjusted).result
result = comb.ConcatOp.create(sign, exp, frac).result
rewriter.replace_op(out_cast, [result])
return False
The final value has the same i8 type as the output bitcast being replaced.
\[R=\{s,e,f\}\qquad\text{(Verilog notation)}\]
Finish the callback
The floating-point island is dead; the greedy rewrite removes it.
def lower_e4m3_mul(out_cast, rewriter):
matched = match_e4m3_island(out_cast)
if matched is None:
return True
lhs, rhs = matched
with rewriter.ip:
sa = comb.ExtractOp.create(7, i1, lhs).result
sb = comb.ExtractOp.create(7, i1, rhs).result
sign = comb.XorOp.create(sa, sb).result
fa = comb.ExtractOp.create(0, i3, lhs).result
fb = comb.ExtractOp.create(0, i3, rhs).result
one = hw.ConstantOp.create(i1, 1).result
ma4 = comb.ConcatOp.create(one, fa).result
mb4 = comb.ConcatOp.create(one, fb).result
zero4 = hw.ConstantOp.create(i4, 0).result
ma8 = comb.ConcatOp.create(zero4, ma4).result
mb8 = comb.ConcatOp.create(zero4, mb4).result
product = comb.MulOp.create(ma8, mb8).result
norm = comb.ExtractOp.create(7, i1, product).result
direct = comb.ExtractOp.create(3, i4, product).result
shifted = comb.ExtractOp.create(4, i4, product).result
mant = comb.MuxOp.create(
norm.value, shifted.value, direct.value).result
frac = comb.ExtractOp.create(0, i3, mant).result
ea = comb.ExtractOp.create(3, i4, lhs).result
eb = comb.ExtractOp.create(3, i4, rhs).result
zero1 = hw.ConstantOp.create(i1, 0).result
ea5 = comb.ConcatOp.create(zero1, ea).result
eb5 = comb.ConcatOp.create(zero1, eb).result
sum_ = comb.AddOp.create(ea5, eb5).result
bias = hw.ConstantOp.create(i5, 7).result
base = comb.SubOp.create(sum_, bias).result
norm5 = comb.ConcatOp.create(zero4, norm).result
adjusted = comb.AddOp.create(base, norm5).result
exp = comb.ExtractOp.create(0, i4, adjusted).result
result = comb.ConcatOp.create(sign, exp, frac).result
rewriter.replace_op(out_cast, [result])
return False
patterns.add(arith.BitcastOp, lower_e4m3_mul)How many normal \(\times\) normal cases does our restricted checker validate?
| Outcome for normal \(\times\) normal | Pairs |
|---|---|
| normal finite result | 42,084 |
| \(\hookrightarrow\) exact, no rounding | 11,892 |
| \(\hookrightarrow\) rounding required | 30,192 |
| subnormal result | 3,916 |
| underflow to zero | 524 |
| overflow to E4M3FN NaN | 10,120 |
| total | 56,644 |
Our datapath still lacks:
The 11,892 cases are exactly those for which none of this extra machinery is needed.
A complete reference is provided, but we will not implement it today.
It handles:
Complete RTL is provided in exercises/.
HAriCo can now generate an IEEE-754 single-precision adder directly as CIRCT Core IR:
Generate hardware in hw + comb, then benefit from existing MLIR/CIRCT verification, optimization, and lowering passes.
The complete reference expands numerical coverage.
We will instead exploit structure already visible in the IR.
Recognize %x * %x \(\longrightarrow\) generate a specialized squarer.
%x * %x cost the same?What becomes simpler when both SSA operands are identical?
For a square, the lower triangle duplicates the upper triangle and the diagonal does not require an AND gate (Dinechin and Kumm 2024, sec. 14.2).
Matcher excerpt: scripts/lower-e4m3fn-square.py
The rewrite continues only when both operands are the same SSA value. A combined production pass would give this pattern a higher PatternBenefit than the generic multiplier.
Edit only: build_square4 in scripts/lower-e4m3fn-square.py.
This generic multiplier is the code to replace.
Task: return the exact 8-bit square of the 4-bit 1.mmm significand.
i8 result.comb.mul.Everything outside this helper remains unchanged.
When your helper is ready:
The generated file must be created, pass MLIR verification, and contain no comb.mul. Functional equivalence and circuit cost are checked after the solution.
For every bit \(x_i\):
\[x_i^2=x_i\]
Its weight changes from \(i\) to \(2i\). Extending one bit to i8 and shifting it by a constant describes that wiring.
At this checkpoint, terms contains only the four diagonal wires.
def build_square4(value):
i1 = IntegerType.get_signless(1)
i7 = IntegerType.get_signless(7)
i8 = IntegerType.get_signless(8)
bits = [
comb.ExtractOp.create(i, i1, value).result
for i in range(4)
]
zero7 = hw.ConstantOp.create(i7, 0).result
def at_weight(bit, weight):
wide = comb.ConcatOp.create(zero7, bit).result
shift = hw.ConstantOp.create(i8, weight).result
return comb.ShlOp.create(wide, shift).result
terms = [at_weight(bits[i], 2 * i) for i in range(4)]For each pair \(j<i\):
\[2x_ix_j=x_ix_j\,2^1\]
Compute one AND and place it at weight \(i+j+1\). The nested loop visits the six distinct cross terms; comb.add collects all ten weighted terms.
def build_square4(value):
i1 = IntegerType.get_signless(1)
i7 = IntegerType.get_signless(7)
i8 = IntegerType.get_signless(8)
bits = [
comb.ExtractOp.create(i, i1, value).result
for i in range(4)
]
zero7 = hw.ConstantOp.create(i7, 0).result
def at_weight(bit, weight):
wide = comb.ConcatOp.create(zero7, bit).result
shift = hw.ConstantOp.create(i8, weight).result
return comb.ShlOp.create(wide, shift).result
terms = [at_weight(bits[i], 2 * i) for i in range(4)]
for i in range(4):
for j in range(i):
cross = comb.AndOp.create(bits[i], bits[j]).result
terms.append(at_weight(cross, i + j + 1))
return comb.AddOp.create(*terms).resultGenerate the generic Exercise 7 baseline:
Compare it with the specialized circuit:
| Lowering checkpoint | AIG nodes | Levels |
|---|---|---|
| generic Exercise 7 multiplier | 57 | 14 |
| supplied square intermediate | 51 | 14 |
| dedicated significand squarer | 50 | 13 |
The same rewrite strategy extends beyond one squarer:
| Recognized arithmetic | Possible datapath |
|---|---|
%x * %x |
dedicated squarer |
%x * C |
constant multiplier or shift-add network |
%a * %b + %c |
fused multiply-add with one final rounding |
| \(\sum_i x_i^2\) | fused sum-of-squares datapath |
| dot product or polynomial | target-sized fused operator |
MLIR preserves enough intent to recognize the pattern; CIRCT lets the pass materialize the chosen circuit. This is the bridge to higher-level hardware generation.
Compilation becomes hardware design…
or the other way around?
Started from the motivation: the end of free scaling and the power, memory, and communication walls demand specialization all the way down to hardware — calling for compiler techniques that help design the machine.
Practised digital design with CIRCT: represented hierarchy, combinational logic, and state; then transformed, verified, synthesized, and emitted the design.
Built an application-specific circuit: lowered arith.mulf to an E4M3 comb datapath, then recognized %x * %x to generate a cheaper squarer.
Yes: we can do digital design with MLIR and CIRCT — and it is interesting!
My research asks how to connect a complete HLS flow to an automated design-space exploration loop:
Full HLS flow: lower tensor / linalg through scheduling, memory, control, and arithmetic decisions into CIRCT, RTL, and physical implementation.
Design-space exploration (DSE): generate architectural variants, prove their behavior, compare accuracy, area, timing, and power, and feed those measurements back into the compiler.




Louis Ledoux
Pierre Cochard
Florent de Dinechin
Progressive Arithmetic Lowering from Tensor Kernels to Synthesizable Datapaths
MLIR Workshop @ Euro LLVM Developers’ Meeting
13 April 2026
class LlamaFfnSublayer(nn.Module):
"""Llama FFN sublayer using SwiGLU."""
def __init__(
self,
dim: int = 512,
hidden_dim: int | None = None,
multiple_of: int = 256,
):
super().__init__()
if hidden_dim is None:
hidden_dim = 4 * dim
hidden_dim = int(2 * hidden_dim / 3)
hidden_dim = multiple_of * (
(hidden_dim + multiple_of - 1)
)
self.w_gate = nn.Linear(dim, hidden_dim, bias=False)
self.w_up = nn.Linear(dim, hidden_dim, bias=False)
self.w_down = nn.Linear(hidden_dim, dim, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
gate = F.silu(self.w_gate(x))
up = self.w_up(x)
return self.w_down(gate * up)

Llama ASIC routing


TinyTapeout GF0P02 wafer.space
Problem to silicon crosses many intermediate arithmetic levels.
matmul, batched GEMM)Arithmetic motivations of different natures
PPA objectives: area, power, performance…
… or budgets / constraints
Hardware constraints of different natures
RealArith and FixedPointArith.--realarith-to-fixed_pt_arith for polynomial approximation lowering.2 × degree-16
32 × degree-4
Example 1: \(f(x)=\tanh(3x)/\tanh(3)\)
--realarith-to-fixed_pt_arith="approximation-method=uniform_piecewise_poly polynomial-degree=10 coeff-storage=switch"
index_switch: 8 intervals (cases 0..7)
\(\left[\ldots\right]\)
EuroLLVM 2025 poster
https://hal.science/hal-05063466
High-level optimization potential:
linear algebra kernels & nonlinear functions.
Final floating-point implementation is delegated to vendor HLS or IP/core generators.
“float and double … are synthesized with IEEE-754 standard partial compliance.”
“complies with much of the IEEE-754 Standard … deviations generally provide a better trade-off of resources.”
[1] Ye et al., HPCA 2022 · [2] Ye & Chen, MICRO 2025 · [3] Xu et al., FPGA 2023 · [4] Friebel et al., HEART 2023
Partial compliance + hidden vendor IP ⇒ opaque arithmetic behavior and limited control.
circt-translate <synth.mlir> --export-aiger -o 90-final.aighls-driver/scripts/export-aig-via-yosys-abc.sh <circt-opt> <in.mlir> 90-final.aig forwardmodule {
hw.module @forward(in %arg0 : !hw.array<16xi32>, in %arg1 : !hw.array<16xi32>, in %clk : !seq.clock, in %reset : i1, out arg1_out : !hw.array<16xi32>) {
...
%111 = comb.extract %local_mem_2_rdata from 31 : (i32) -> i1
%112 = comb.extract %local_mem_3_rdata from 31 : (i32) -> i1
%113 = comb.xor %111, %112 : i1
%114 = comb.extract %local_mem_2_rdata from 23 : (i32) -> i8
%115 = comb.extract %local_mem_3_rdata from 23 : (i32) -> i8
%118 = comb.concat %c1_i25, %116 : i25, i23
%120 = comb.mul %118, %119 : i48
%123 = comb.add %114, %115, %122, %c-127_i8 : i8
...
}
}module forward(
input [15:0][31:0] arg0,
arg1,
input clk,
reset,
output [15:0][31:0] arg1_out
);
wire [511:0] _GEN_57 = arg0;
reg [31:0] arg1_mem[0:15];
always_ff @(posedge clk) begin
if (!reset) begin
if (_GEN_5) arg1_mem[_GEN_1] <= _GEN_0;
if (_GEN_9) arg1_mem[_GEN_6] <= 32'h0;
end
end
...
endmodule\[ \frac{1}{\sqrt{x^2+y^2}} \qquad \sin(\omega t + \varphi) \]
harico-arith-to-combemeraude-mlir-opt in.mlir --harico-arith-to-comb="lowering-mode=per-op target=VirtexUltraScale target-frequency=2.5e7"
\[ \frac{1}{\sqrt{x^2+y^2}} \qquad \sin(\omega t + \varphi) \]
emeraude-mlir-opt in.mlir --harico-arith-to-comb="lowering-mode=fused-graph-datapath target=sky130 target-frequency=2.5e7"
emeraude-mlir-opt in.mlir --harico-arith-to-comb="lowering-mode=specialized specializations=enable=s3fdp,s3fdp.chunk_size=32,s3fdp.ovf=10,s3fdp.msb=10,s3fdp.lsb=-20 target=tsmc7 target-frequency=2.5e7"
#include <HAriCo/Context.hpp>
#include <HAriCo/Operators/FPAddSinglePath.hpp>
using namespace HAriCo;
auto ctx = Context::new_default();
ctx.options["target"] = Targets::VirtexUltraScalePlus;
ctx.options["frequency"] = 500 // MHz;
// Build operator:
auto op = FPAdd();
auto options = op.interface(ctx);
options["wE"] = 8;
options["wF"] = 23;
Module mod = op.build(ctx, options);
// Before pass:
VerilogCodeGenerator gen;
gen.build(mod).fwrite("fpadd.sv");
PassManager pm;
pm.add<TimingAnalysisPass>();
pm.add<SchedulePass>();
pm.add<PipelineizePass>();
pm.run(mod);
gen.build(mod).fwrite("fpadd_pipeline.sv");
import harico as hrc
def main():
ctx = hrc.Context()
ctx.set_target("VirtexUltrascalePlus", frequency_mhz=500)
op = hrc.Operator("FPAdd")
mod = op.build_ir(
ctx,
wE=8,
wF=23,
sub=False,
dualPath=False,
onlyPositiveIO=False,
)
# Before pass:
mod.emit_vhdl("fpadd_before_pipelineize.vhd")
mod.emit_verilog("fpadd_before_pipelineize.v")
# Analysis passes first:
pm_analysis = hrc.PassManager(["timing", "schedule"])
pm_analysis.run(mod, ctx)
# Transform pass:
pm_pipelineize = hrc.PassManager(["pipelineize"])
pm_pipelineize.run(mod, ctx)

MPW + TT submission (TTg0p2) on Wafer.space — Utilisation 47.106% · Wire length 377542 um


ASIC SKY130HD Llama
| Metric | Value |
|---|---|
| Area | 1.7233 mm² |
| Die Dimensions | 1.312 × 1.312 mm |
| Power Breakdown | |
| Combinational | 0.117 W |
| Sequential | 0.044 W |
| Clock | 0.028 W |
| Total Power | 0.189 W |

Llama 1.5 mm² routing congestion heatmap
FPGA VU19P Llama
| Metric | Value |
|---|---|
| LUTs | 26,928 |
| Flip-Flops | 12,221 |
| DSP Blocks | 17 |

Llama FPGA vu19p P&R
module {
func.func @forward(%arg0: tensor<1x2x8xf32>) -> tensor<1x2x8xf32> {
%7 = linalg.batch_matmul ... -> tensor<1x2x16xf32>
%8 = linalg.generic ... ins(%7 : tensor<1x2x16xf32>) outs(%5 : tensor<1x2x16xf32>) {
^bb0(%in: f32, %out: f32):
%neg = arith.negf %in : f32
%exp = math.exp %neg : f32
%gate = arith.divf %cst_1, %den : f32
%silu = arith.mulf %in, %gate : f32
linalg.yield %silu : f32
} -> tensor<1x2x16xf32>
return %18 : tensor<1x2x8xf32>
}
}
Llama FFN Sublayer. Area vs. Delay
#map = affine_map<(d0, d1) -> (d0, d1)>
module {
func.func @forward(%arg0: tensor<2x4xf32>) -> tensor<2x4xf32> {
%mm = linalg.matmul ... -> tensor<2x4xf32>
%sig = linalg.generic ... ins(%mm : tensor<2x4xf32>) outs(%empty : tensor<2x4xf32>) {
^bb0(%in: f32, %acc: f32):
%neg = arith.negf %in : f32
%exp = math.exp %neg : f32
%gate = arith.divf %c1, %den : f32
linalg.yield %gate : f32
} -> tensor<2x4xf32>
%out = linalg.generic ... ins(%sig, %mm : tensor<2x4xf32>, tensor<2x4xf32>) outs(%empty : tensor<2x4xf32>)
return %out : tensor<2x4xf32>
}
}
MatMul + SiLU. Area vs. Delay
#map = affine_map<(d0, d1) -> (d0, d1)>
module {
func.func @forward(%arg0: tensor<2x4xf32>) -> tensor<2x4xf32> {
%mm = linalg.matmul ... -> tensor<2x4xf32>
%out = linalg.generic ... ins(%mm : tensor<2x4xf32>) outs(%empty : tensor<2x4xf32>) {
^bb0(%in: f32, %acc: f32):
%neg = arith.negf %in : f32
%exp = math.exp %neg : f32
%sig = arith.divf %c1, %den : f32
linalg.yield %sig : f32
} -> tensor<2x4xf32>
return %out : tensor<2x4xf32>
}
}
MatMul + Sigmoid. Area vs. Delay
#map = affine_map<(d0, d1) -> (d0, d1)>
module {
func.func @forward(%query: tensor<2x4xf32>) -> tensor<2x2xf32> {
%score = linalg.matmul ... -> tensor<2x2xf32>
%out = linalg.generic ... ins(%score : tensor<2x2xf32>) outs(%out_empty : tensor<2x2xf32>) {
^bb0(%in: f32, %acc: f32):
%res = math.exp %in : f32
linalg.yield %res : f32
} -> tensor<2x2xf32>
return %out : tensor<2x2xf32>
}
}
Attention Softmax Exponential. Area vs. Delay
#map = affine_map<(d0, d1) -> (d0, d1)>
module {
func.func @forward(%arg0: tensor<2x8xf32>) -> tensor<2x8xf32> {
%mm = linalg.matmul ... -> tensor<2x8xf32>
%out = linalg.generic ... ins(%mm : tensor<2x8xf32>) outs(%empty : tensor<2x8xf32>) {
^bb0(%in: f32, %acc: f32):
%neg = arith.negf %in : f32
%exp = math.exp %neg : f32
%res = arith.divf %c1, %den : f32
linalg.yield %res : f32
} -> tensor<2x8xf32>
return %out : tensor<2x8xf32>
}
}
MatMul + Sigmoid Heavy-Tail. Area vs. Delay
module {
func.func @forward(%arg0: tensor<1x4xf32>) -> tensor<1x4xf32> {
%w = arith.constant dense<...> : tensor<4x4xf32>
%init = linalg.fill ins(%c0 : f32) outs(%empty : tensor<1x4xf32>) -> tensor<1x4xf32>
%out = linalg.matmul ins(%arg0, %w : tensor<1x4xf32>, tensor<4x4xf32>) outs(%init : tensor<1x4xf32>)
-> tensor<1x4xf32>
return %out : tensor<1x4xf32>
}
}
GEMV Accumulation. Area vs. Delay
module {
func.func @forward(%query: tensor<2x4xf32>) -> tensor<2x2xf32> {
%kt = arith.constant dense<...> : tensor<4x2xf32>
%init = linalg.fill ins(%c0 : f32) outs(%empty : tensor<2x2xf32>) -> tensor<2x2xf32>
%score = linalg.matmul ins(%query, %kt : tensor<2x4xf32>, tensor<4x2xf32>) outs(%init : tensor<2x2xf32>)
-> tensor<2x2xf32>
return %score : tensor<2x2xf32>
}
}
Attention Score. Area vs. Delay
module {
func.func @forward(%arg0: tensor<2x4xf32>) -> tensor<2x4xf32> {
%w = arith.constant dense<...> : tensor<4x4xf32>
%init = linalg.fill ins(%c0 : f32) outs(%empty : tensor<2x4xf32>) -> tensor<2x4xf32>
%mm = linalg.matmul ins(%arg0, %w : tensor<2x4xf32>, tensor<4x4xf32>) outs(%init : tensor<2x4xf32>)
-> tensor<2x4xf32>
return %mm : tensor<2x4xf32>
}
}
MatMul Accumulation. Area vs. Delay
#map = affine_map<(d0, d1) -> (d0, d1)>
module {
func.func @forward(%arg0: tensor<1x4xf32>) -> tensor<1x4xf32> {
%mm = linalg.matmul ... -> tensor<1x4xf32>
%out = linalg.generic ... ins(%mm, %arg0 : tensor<1x4xf32>, tensor<1x4xf32>) outs(%empty_out : tensor<1x4xf32>) {
^bb0(%mmv: f32, %in: f32, %acc: f32):
%bias = arith.mulf %beta, %in : f32
%res = arith.addf %mmv, %bias : f32
linalg.yield %res : f32
} -> tensor<1x4xf32>
return %out : tensor<1x4xf32>
}
}
Polybench GESUMMV-Like. Area vs. Delay
module {
func.func @forward(%arg0: tensor<1x8xf32>) -> tensor<1x8xf32> {
%w = arith.constant dense<...> : tensor<8x8xf32>
%init = linalg.fill ins(%c0 : f32) outs(%empty : tensor<1x8xf32>) -> tensor<1x8xf32>
%mm = linalg.matmul ins(%arg0, %w : tensor<1x8xf32>, tensor<8x8xf32>) outs(%init : tensor<1x8xf32>)
-> tensor<1x8xf32>
return %mm : tensor<1x8xf32>
}
}
DSP FIR Accumulation. Area vs. Delay
module {
func.func @forward(%arg0: tensor<1x8xf32>) -> tensor<1x8xf32> {
%w = arith.constant dense<...> : tensor<8x8xf32>
%init = linalg.fill ins(%c0 : f32) outs(%empty : tensor<1x8xf32>) -> tensor<1x8xf32>
%mm = linalg.matmul ins(%arg0, %w : tensor<1x8xf32>, tensor<8x8xf32>) outs(%init : tensor<1x8xf32>)
-> tensor<1x8xf32>
return %mm : tensor<1x8xf32>
}
}
Polybench SYRK-Like. Area vs. Delay

Internal Emeraude-MLIR transforms pushed upstream.
1 Hanchen Ye et al., “ScaleHLS: A New Scalable High-Level Synthesis Framework on Multi-Level Intermediate Representation,” HPCA 2022.
2 Hanchen Ye and Deming Chen, “StreamTensor: Make Tensors Stream in Dataflow Accelerators for LLMs,” MICRO 2025.
I will just execute a command while speaking
Thank you
Q&A
| Category | Cells | Count |
|---|---|---|
| Fill | fillcap fill |
10280 |
| NAND | nand2 nand4 nand3 |
2627 |
| Combo Logic | aoi21 oai21 oai22 oai31 aoi211 oai211 aoi22 oai32 oai221 aoi221 oai33 |
1645 |
| Buffer | clkbuf dlyb buf dlya |
1258 |
| Flip Flops | dffq |
1179 |
| NOR | nor2 nor4 nor3 xnor2 xnor3 |
1024 |
| OR | or2 or3 or4 xor2 xor3 |
1020 |
| Clock | clkinv |
367 |
| AND | and2 and3 and4 |
323 |
| Multiplexer | mux2 |
316 |
| Inverter | inv |
55 |
The complete precheck runs in GitHub Actions.
| Check | Result |
|---|---|
| Magic DRC | ✓ |
| KLayout pin label overlapping drawing | ✓ |
| KLayout zero area | ✓ |
| KLayout Checks | ✓ |
| Pin check | ✓ |
| Boundary check | ✓ |
| Power pin check | ✓ |
| Layer check | ✓ |
| Cell name check | ✓ |
| Analog pin check | ✓ |
Many transforms from one Faust line of code passing all checks :)