Skip to content
Merged
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
98 changes: 98 additions & 0 deletions tests/fixtures/circuits/alu_4bit_multibit.circ
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// 4-bit Nand2Tetris (Hack) ALU — multi-bit version of alu_4bit.circ.
//
// Same control surface and same logic as the scalar fixture, but the data
// path runs on width-4 buses x and y. Scalar control bits broadcast into
// the lanes via {c, c, c, c} concat. The ripple-carry adder stays serial
// because the carry between bit lanes can't be expressed as a single
// width-4 primitive.
//
// Total inputs: 6 control + 4 (x bus) + 4 (y bus) = 14 → 16,384 rows.
// The truth-table column layout differs from alu_4bit.circ because x, y,
// and out render as multi-bit values, but every row's logical mapping
// (zx, nx, zy, ny, f, no, x, y) → out is identical.

input zx, nx, zy, ny, f, no
input[4] x, y

// ----- broadcast scalar control bits to width-4 buses ------------------
// The macros (xor / or) and primitives accept width-4 ports when written
// `xor name[4](...)`; passing a {c, c, c, c} concat directly into a macro
// port currently confuses the validator's width inference (the destination
// reads as width 1 before the macro is specialised at the call site), so
// we route every scalar-broadcast input through a width-4 wire first.
wire[4] nx_b(in={nx, nx, nx, nx})
wire[4] ny_b(in={ny, ny, ny, ny})
wire[4] no_b(in={no, no, no, no})

// ----- pre-set x: x' = (NOT zx) AND x; x'' = nx XOR x' ----------------
not zx_inv(in=zx)
and[4] xz(a={zx_inv.out, zx_inv.out, zx_inv.out, zx_inv.out}, b=x)
xor xn[4](a=nx_b.out, b=xz.out)

// ----- pre-set y: same shape -------------------------------------------
not zy_inv(in=zy)
and[4] yz(a={zy_inv.out, zy_inv.out, zy_inv.out, zy_inv.out}, b=y)
xor yn[4](a=ny_b.out, b=yz.out)

// ----- compute ADD: 4-bit ripple-carry, drop final carry-out ----------
// Bit-parallel propagate (XOR) and generate (AND) terms across all 4 bits.
//
// The propagate is hand-expanded from primitives instead of using
// `xor add_p[4]` — the xor[W] macro produces incorrect simulation
// values for some input combinations when its output flows through a
// wire-buffered slice chain into the per-bit scalar macros below.
// Logic: xor(a, b) = NOT(AND(NOT(AND(a, NOT b)), NOT(AND(NOT a, b))))
// The generate stays as the primitive `and[4]`.
not[4] xn_inv(in=xn.out)
not[4] yn_inv(in=yn.out)
and[4] p1(a=xn.out, b=yn_inv.out)
and[4] p2(a=xn_inv.out, b=yn.out)
not[4] p1_inv(in=p1.out)
not[4] p2_inv(in=p2.out)
and[4] p_nor(a=p1_inv.out, b=p2_inv.out)
not[4] add_p(in=p_nor.out)
and[4] add_g(a=xn.out, b=yn.out)

// Wire-buffer each slice that feeds a scalar macro. Slice-into-scalar-
// macro (`signal[i]` directly into `xor`/`or`/`nand`/...) trips an
// InternalError in the topology builder; routing through a scalar wire
// avoids it. Primitives (`and add1_pcy`) don't need this, but using the
// buffered slices uniformly keeps the bit-N adder readable.
wire add_p0(in=add_p.out[0])
wire add_p1(in=add_p.out[1])
wire add_p2(in=add_p.out[2])
wire add_p3(in=add_p.out[3])
wire add_g0(in=add_g.out[0])
wire add_g1(in=add_g.out[1])
wire add_g2(in=add_g.out[2])

// Bit 0: half adder is "free" — sum is add_p0, carry is add_g0.
// Bit 1: full adder fed by add_g0
xor add1_sum(a=add_p1.out, b=add_g0.out)
and add1_pcy(a=add_p1.out, b=add_g0.out)
or add1_cy (a=add_g1.out, b=add1_pcy.out)

// Bit 2: full adder fed by add1_cy
xor add2_sum(a=add_p2.out, b=add1_cy.out)
and add2_pcy(a=add_p2.out, b=add1_cy.out)
or add2_cy (a=add_g2.out, b=add2_pcy.out)

// Bit 3: sum only — final carry-out is intentionally dropped per ALU spec.
xor add3_sum(a=add_p3.out, b=add2_cy.out)

// Pack the 4 sum bits (low-on-left) into a width-4 bus.
wire[4] add(in={add_p0.out, add1_sum.out, add2_sum.out, add3_sum.out})

// ----- compute AND: bitwise across all 4 bits --------------------------
and[4] and_result(a=xn.out, b=yn.out)

// ----- mux per bit: out = (f AND add) OR ((NOT f) AND and_result) -----
not f_inv(in=f)
and[4] mux_l(a={f, f, f, f}, b=add.out)
and[4] mux_r(a={f_inv.out, f_inv.out, f_inv.out, f_inv.out}, b=and_result.out)
or mux[4](a=mux_l.out, b=mux_r.out)

// ----- post-set: if no then out := NOT out -----------------------------
xor result[4](a=no_b.out, b=mux.out)

output[4] out(in=result.out)
56 changes: 56 additions & 0 deletions tests/fixtures/projects/alu_parametric/alu.circ
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Parametric Hack-style ALU, generic over width W.
//
// Implements the bit-parallel parts of the Nand2Tetris (Hack) ALU at any
// width W:
// 1. pre-set x: if zx then x := 0; if nx then x := !x
// 2. pre-set y: if zy then y := 0; if ny then y := !y
// 3. compute: if f then out := x + y else out := x & y
// 4. post-set: if no then out := !out
//
// The ripple-carry adder for stage 3 is intrinsically serial and can't be
// expressed parametrically (the language has no way to iterate over bit
// positions), so the adder is *external*: the caller computes x+y at
// width W and feeds the result in via `add_in`. To support this, the ALU
// exposes the pre-set operands `xn_out` and `yn_out` so the caller's
// adder has the right inputs.
//
// The six scalar control bits also can't be broadcast from inside this
// file (no replicate operator), so the caller pre-broadcasts each scalar
// `c` to a width-W bus `c_b` (typically via `wire[W] c_b(in={c, c, ...})`)
// and passes the buses in here.
//
// CAVEAT — feedback path: the caller's adder consumes xn_out/yn_out and
// feeds the result back to add_in. The current topology builder rejects
// such feedback when the chain contains any macro at width > 1 or any
// sub-circuit instance, so the caller's adder must use only primitives
// (and / not / wire) on the bit-parallel paths. See alu_<W>bit.circ for
// the hand-expanded XOR pattern this implies.

input<W>[W] zx_b, nx_b, zy_b, ny_b, f_b, no_b
input<W>[W] x, y, add_in

// ----- pre-set x: x' = (NOT zx) AND x; x'' = nx XOR x' ----------------
not[W] zx_inv(in=zx_b)
and[W] xz(a=zx_inv.out, b=x)
xor xn[W](a=nx_b, b=xz.out)

// ----- pre-set y: same shape -------------------------------------------
not[W] zy_inv(in=zy_b)
and[W] yz(a=zy_inv.out, b=y)
xor yn[W](a=ny_b, b=yz.out)

// ----- compute AND: bitwise across all W bits --------------------------
and[W] and_result(a=xn.out, b=yn.out)

// ----- mux per bit: out = (f AND add_in) OR ((NOT f) AND and_result) ---
not[W] f_inv(in=f_b)
and[W] mux_l(a=f_b, b=add_in)
and[W] mux_r(a=f_inv.out, b=and_result.out)
or mux[W](a=mux_l.out, b=mux_r.out)

// ----- post-set: if no then out := NOT out -----------------------------
xor result[W](a=no_b, b=mux.out)

output[W] xn_out(in=xn.out)
output[W] yn_out(in=yn.out)
output[W] out(in=result.out)
47 changes: 47 additions & 0 deletions tests/fixtures/projects/alu_parametric/alu_2bit.circ
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// 2-bit instantiation of the parametric Hack ALU in alu.circ.
// Total inputs: 6 control + 2 (x) + 2 (y) = 10 bits → 1024 vectors.

import alu "alu.circ"

input zx, nx, zy, ny, f, no
input[2] x, y

// ----- broadcast scalar controls to width 2 ----------------------------
wire[2] zx_b(in={zx, zx})
wire[2] nx_b(in={nx, nx})
wire[2] zy_b(in={zy, zy})
wire[2] ny_b(in={ny, ny})
wire[2] f_b (in={f, f })
wire[2] no_b(in={no, no})

// ----- 2-bit adder over inst.xn_out and inst.yn_out --------------------
// Hand-expanded XOR for the bit-parallel propagate (primitives only).
not[2] xn_inv(in=inst.xn_out)
not[2] yn_inv(in=inst.yn_out)
and[2] p1(a=inst.xn_out, b=yn_inv.out)
and[2] p2(a=xn_inv.out, b=inst.yn_out)
not[2] p1_inv(in=p1.out)
not[2] p2_inv(in=p2.out)
and[2] p_nor(a=p1_inv.out, b=p2_inv.out)
not[2] add_p(in=p_nor.out)
and[2] add_g(a=inst.xn_out, b=inst.yn_out)

// Wire-buffer each slice that feeds a scalar macro.
wire add_p0(in=add_p.out[0])
wire add_p1(in=add_p.out[1])
wire add_g0(in=add_g.out[0])

// Per-bit carry chain. Bit 0 sum = add_p[0]; bit 1 = add_p[1] XOR add_g[0]
// (final carry dropped).
xor add1_sum(a=add_p1.out, b=add_g0.out)

wire[2] adder_result(in={add_p0.out, add1_sum.out})

alu inst[2](
zx_b=zx_b.out, nx_b=nx_b.out, zy_b=zy_b.out, ny_b=ny_b.out,
f_b=f_b.out, no_b=no_b.out,
x=x, y=y,
add_in=adder_result.out
)

output[2] out(in=inst.out)
75 changes: 75 additions & 0 deletions tests/fixtures/projects/alu_parametric/alu_4bit.circ
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 4-bit instantiation of the parametric Hack ALU in alu.circ.
//
// This root file wires up the per-width pieces the parametric core can't
// express on its own:
// - 6 scalar controls broadcast to width-4 buses
// - 4-bit ripple-carry adder built from primitives over alu.xn_out and
// alu.yn_out (must use only primitives in the feedback path back to
// alu.add_in, see alu.circ's CAVEAT block)
//
// Total inputs: 6 control + 4 (x) + 4 (y) = 14 bits → 16,384 vectors.

import alu "alu.circ"

input zx, nx, zy, ny, f, no
input[4] x, y

// ----- broadcast scalar controls to width 4 ----------------------------
wire[4] zx_b(in={zx, zx, zx, zx})
wire[4] nx_b(in={nx, nx, nx, nx})
wire[4] zy_b(in={zy, zy, zy, zy})
wire[4] ny_b(in={ny, ny, ny, ny})
wire[4] f_b (in={f, f, f, f })
wire[4] no_b(in={no, no, no, no})

// ----- 4-bit adder over inst.xn_out and inst.yn_out --------------------
// Hand-expanded XOR for the bit-parallel propagate. Pure primitives so
// the feedback chain back into inst.add_in doesn't trip the topology
// builder (xor[W] macros and sub-circuit instances in this path break).
// Logic: xor(a, b) = NOT(AND(NOT(AND(a, NOT b)), NOT(AND(NOT a, b))))
not[4] xn_inv(in=inst.xn_out)
not[4] yn_inv(in=inst.yn_out)
and[4] p1(a=inst.xn_out, b=yn_inv.out)
and[4] p2(a=xn_inv.out, b=inst.yn_out)
not[4] p1_inv(in=p1.out)
not[4] p2_inv(in=p2.out)
and[4] p_nor(a=p1_inv.out, b=p2_inv.out)
not[4] add_p(in=p_nor.out)

// Generate: bitwise AND of xn and yn (primitive, no expansion needed).
and[4] add_g(a=inst.xn_out, b=inst.yn_out)

// Wire-buffer each slice that feeds a scalar macro (slice-into-scalar-
// macro is a separate validator/topology papercut).
wire add_p0(in=add_p.out[0])
wire add_p1(in=add_p.out[1])
wire add_p2(in=add_p.out[2])
wire add_p3(in=add_p.out[3])
wire add_g0(in=add_g.out[0])
wire add_g1(in=add_g.out[1])
wire add_g2(in=add_g.out[2])

// Per-bit carry chain. Bit 0 is "free" — sum is add_p[0], carry is
// add_g[0]. Bits 1..2 are full adders; bit 3 is sum only (final carry
// dropped per ALU spec).
xor add1_sum(a=add_p1.out, b=add_g0.out)
and add1_pcy(a=add_p1.out, b=add_g0.out)
or add1_cy (a=add_g1.out, b=add1_pcy.out)
xor add2_sum(a=add_p2.out, b=add1_cy.out)
and add2_pcy(a=add_p2.out, b=add1_cy.out)
or add2_cy (a=add_g2.out, b=add2_pcy.out)
xor add3_sum(a=add_p3.out, b=add2_cy.out)

// Pack the 4 sum bits (low-on-left) into a width-4 bus.
wire[4] adder_result(in={add_p0.out, add1_sum.out, add2_sum.out, add3_sum.out})

// Instantiate the parametric ALU at W=4. Declared last so the topology
// builder doesn't see a forward chain through the instance.
alu inst[4](
zx_b=zx_b.out, nx_b=nx_b.out, zy_b=zy_b.out, ny_b=ny_b.out,
f_b=f_b.out, no_b=no_b.out,
x=x, y=y,
add_in=adder_result.out
)

output[4] out(in=inst.out)
69 changes: 69 additions & 0 deletions tests/fixtures/projects/alu_parametric/alu_6bit.circ
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// 6-bit instantiation of the parametric Hack ALU in alu.circ.
// Total inputs: 6 control + 6 (x) + 6 (y) = 18 bits → 262,144 vectors,
// over the default --truth-table cap; use --truth-table-cap=18 to render.

import alu "alu.circ"

input zx, nx, zy, ny, f, no
input[6] x, y

// ----- broadcast scalar controls to width 6 ----------------------------
wire[6] zx_b(in={zx, zx, zx, zx, zx, zx})
wire[6] nx_b(in={nx, nx, nx, nx, nx, nx})
wire[6] zy_b(in={zy, zy, zy, zy, zy, zy})
wire[6] ny_b(in={ny, ny, ny, ny, ny, ny})
wire[6] f_b (in={f, f, f, f, f, f })
wire[6] no_b(in={no, no, no, no, no, no})

// ----- 6-bit adder over inst.xn_out and inst.yn_out --------------------
// Hand-expanded XOR for the bit-parallel propagate (primitives only).
not[6] xn_inv(in=inst.xn_out)
not[6] yn_inv(in=inst.yn_out)
and[6] p1(a=inst.xn_out, b=yn_inv.out)
and[6] p2(a=xn_inv.out, b=inst.yn_out)
not[6] p1_inv(in=p1.out)
not[6] p2_inv(in=p2.out)
and[6] p_nor(a=p1_inv.out, b=p2_inv.out)
not[6] add_p(in=p_nor.out)
and[6] add_g(a=inst.xn_out, b=inst.yn_out)

// Wire-buffer slices.
wire add_p0(in=add_p.out[0])
wire add_p1(in=add_p.out[1])
wire add_p2(in=add_p.out[2])
wire add_p3(in=add_p.out[3])
wire add_p4(in=add_p.out[4])
wire add_p5(in=add_p.out[5])
wire add_g0(in=add_g.out[0])
wire add_g1(in=add_g.out[1])
wire add_g2(in=add_g.out[2])
wire add_g3(in=add_g.out[3])
wire add_g4(in=add_g.out[4])

// Per-bit carry chain (bits 1..4 are full adders; bit 5 is sum only).
xor add1_sum(a=add_p1.out, b=add_g0.out)
and add1_pcy(a=add_p1.out, b=add_g0.out)
or add1_cy (a=add_g1.out, b=add1_pcy.out)
xor add2_sum(a=add_p2.out, b=add1_cy.out)
and add2_pcy(a=add_p2.out, b=add1_cy.out)
or add2_cy (a=add_g2.out, b=add2_pcy.out)
xor add3_sum(a=add_p3.out, b=add2_cy.out)
and add3_pcy(a=add_p3.out, b=add2_cy.out)
or add3_cy (a=add_g3.out, b=add3_pcy.out)
xor add4_sum(a=add_p4.out, b=add3_cy.out)
and add4_pcy(a=add_p4.out, b=add3_cy.out)
or add4_cy (a=add_g4.out, b=add4_pcy.out)
xor add5_sum(a=add_p5.out, b=add4_cy.out)

wire[6] adder_result(in={
add_p0.out, add1_sum.out, add2_sum.out, add3_sum.out, add4_sum.out, add5_sum.out
})

alu inst[6](
zx_b=zx_b.out, nx_b=nx_b.out, zy_b=zy_b.out, ny_b=ny_b.out,
f_b=f_b.out, no_b=no_b.out,
x=x, y=y,
add_in=adder_result.out
)

output[6] out(in=inst.out)
Loading
Loading