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
6 changes: 3 additions & 3 deletions .trinity/seals/ternary_ClockedCounter.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"gen_hash_c": "sha256:27383948b87cfb14a1fe5b5188e45317648d51530c9fe657090d5c6d8011b318",
"gen_hash_rust": "sha256:ad02e62fbac89c519bd4b82b2ec3b8a5a24f76a0426f582ba831ecc965b462ac",
"gen_hash_verilog": "sha256:980820ea0014f837f8fb8c021390947973dec556dfb6ef925c6b6810bce0905b",
"gen_hash_verilog": "sha256:742baa2af5542beb3dd9bdaabf60865eb8324ed71cf707ec25ee685d7af62dd4",
"gen_hash_zig": "sha256:b4847252ab51690dc65b288a7f7d95840a4aa31ff87ee8b9b2d0c268273c13d0",
"module": "ClockedCounter",
"ring": 12,
"sealed_at": "2026-08-06T10:43:39Z",
"spec_hash": "sha256:0bdf30370bf761b681d0745630e46dcab0e8f7cf7b59242041ac815a48f09d80",
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:12a5bb8e6d0975bfd72b3520ac83e9adaa6f52371be8ef101ef5fd39b8fcbf6f",
"spec_path": "specs/ternary/clocked_counter.t27"
}
11 changes: 11 additions & 0 deletions .trinity/seals/ternary_CombBitnetLayer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"gen_hash_c": "sha256:095e6072c93b91f7e343bc517ddfb8063d263ebc78e44ea70b3ec3a40eaddf58",
"gen_hash_rust": "sha256:b3b29fdd87187b06ca8f5e5b93e15676bda76e2f5236322f3f713713e6d2e793",
"gen_hash_verilog": "sha256:e4faca797bec20c0727d344c20df3af8cac748004f86f5da17903b4f6ba32e1e",
"gen_hash_zig": "sha256:cd371cffeb09a87c3fb7bb60d04d3515c1e8d1de9385071fc5a002e92f65f4a9",
"module": "CombBitnetLayer",
"ring": 12,
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:105570e1b9856293381a0dd3a7365177b880aaf561e8659f5d50e240a593b5be",
"spec_path": "specs/ternary/comb_bitnet_layer.t27"
}
11 changes: 11 additions & 0 deletions .trinity/seals/ternary_CombBitnetNeuron.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"gen_hash_c": "sha256:335a4d8f9a1b96197475cc2e0ddcb3ed47a6c4edd7ae2b7222696fb579bfacbb",
"gen_hash_rust": "sha256:6aac52806028636740c4dd181f13cccc1c2bcadce38e097483feeb3766c35446",
"gen_hash_verilog": "sha256:bb061aa2c56bdf386892b89ee8f13e44d73d32eca928f7eec7d3850b588e20bc",
"gen_hash_zig": "sha256:8545d117bb4a1a5a2cd71fb2234ef14fd7b1ad0bfd34ba2ee76e5bf65af984b5",
"module": "CombBitnetNeuron",
"ring": 12,
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:85e9e5f9206e17a924e9ae7f2a7c79a62cce17f06327cf1abdf424efaba6cf41",
"spec_path": "specs/ternary/comb_bitnet_neuron.t27"
}
11 changes: 11 additions & 0 deletions .trinity/seals/ternary_CombTernaryDot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"gen_hash_c": "sha256:fd30c50e9bed7a74880a3b911641d1c8869385fc6ad4a14668ebe0d647b80f3d",
"gen_hash_rust": "sha256:747428b2be42fbd2cc4c14d172e56a2323dc1f1766b1ec688c437a7c39d6f5c2",
"gen_hash_verilog": "sha256:3936e13b234c20b7dc7daf2332adb4e27afeef049ebeaaa392a4c4b4facf6244",
"gen_hash_zig": "sha256:a06c6c9a385a07db9123a69f5b423966aacbac91a7d226744361a2c1b27d5bc1",
"module": "CombTernaryDot",
"ring": 12,
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:1cb1bf87cd3f9cb92778ec47f85d079c6514ad532a7f8ee777481dd031ba4651",
"spec_path": "specs/ternary/comb_ternary_dot.t27"
}
11 changes: 11 additions & 0 deletions .trinity/seals/ternary_GftAddRne.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"gen_hash_c": "sha256:30292b82d55be23fa5952775ceb0dbded3c6f051dc929f937c7e6dfe3909ab77",
"gen_hash_rust": "sha256:bb4c89d4ddddc8892a470ccd47025767e7b66de5dc4ec00fff056e273f8977d4",
"gen_hash_verilog": "sha256:88b310292720be7be01e98c15ae63615547d39646e758de51867612fb14268cc",
"gen_hash_zig": "sha256:c4e780d9cc0139bff7a2b73b4f6ad5bab6103205d8655b4b911ed4695224d44d",
"module": "GftAddRne",
"ring": 12,
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:e900a1f93f9d85ab1c55a4492f61134eee0720787e2cc59a88890e6120a7ee87",
"spec_path": "specs/ternary/gft_add_rne.t27"
}
11 changes: 11 additions & 0 deletions .trinity/seals/ternary_GftDot2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"gen_hash_c": "sha256:9072ef283a9f55147e7fb63f581b53262d520f110ab9d78ac61169a90ab7a5d0",
"gen_hash_rust": "sha256:99b540c4468bd1e8eefc039dfbeabfbd9d7af347ede035242c7a224bb6b3112e",
"gen_hash_verilog": "sha256:14682c77fe6fdec9656828145c2b41e23e30a78524bd9c9fde370424f5ba7276",
"gen_hash_zig": "sha256:b654810ab7356b4b170a6405d1b904ee9c8e4d20efcc98004b430ae7d291022e",
"module": "GftDot2",
"ring": 12,
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:b99c892c8a0fdcd11528ddd0ca8d52d8646e0c9b1ce420ae16610e8ede3de5c4",
"spec_path": "specs/ternary/gft_dot2.t27"
}
11 changes: 11 additions & 0 deletions .trinity/seals/ternary_GftDot2Rne.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"gen_hash_c": "sha256:e6dc470045a17a719de37b6b59ffea70be28dd306b109ba8e1a5356638157409",
"gen_hash_rust": "sha256:d83583ec1d4a03405e2f88e6b67b4fcc1281de2b215a12bf43e9788002133b1a",
"gen_hash_verilog": "sha256:73abfbc1ed5cc595f6677c120dcd05b2c5aa0f4b832060f95f6810b29a96da39",
"gen_hash_zig": "sha256:146a0646e204d523da04197b83989202071cf60c73addfb68291407fc9c131ae",
"module": "GftDot2Rne",
"ring": 12,
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:be0e54b47c9cef2d0f3338635e692dfc3afe66258a0b9c9fdbb95cce022309d8",
"spec_path": "specs/ternary/gft_dot2_rne.t27"
}
11 changes: 11 additions & 0 deletions .trinity/seals/ternary_GftDot4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"gen_hash_c": "sha256:bf46c20be049d23ad8a43674b919d9a3f5f7ae4921e678f753824331d5764c94",
"gen_hash_rust": "sha256:7f94ab7756a758d4cc6869dce46ad17b084d64a9b89620b4dcd6f7610f40ca1f",
"gen_hash_verilog": "sha256:686c9dbf0b86f11498309f986825f59650c6f690b18b3ad9646c691fb4ed8e04",
"gen_hash_zig": "sha256:c764491d15be4c5df0810cf9b59d2dca43d68059f4e3d54a7bf441e3065f8b03",
"module": "GftDot4",
"ring": 12,
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:4e80040a11ab6ae72d46b8d4487959caa396ddde34d00ebe15ecef1f4bbbb9ed",
"spec_path": "specs/ternary/gft_dot4.t27"
}
11 changes: 11 additions & 0 deletions .trinity/seals/ternary_GftDot8.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"gen_hash_c": "sha256:06ba4cad93adba5600c3a3c4d4d97d6347b88cb5e03c5a4a70451c26ba92e012",
"gen_hash_rust": "sha256:5b35b84c39af10f82ac9988c180306a9bd85ae393b06647c9bced6117a2fcc06",
"gen_hash_verilog": "sha256:f8bf84c8a793d9a39bfc2c37be27c8a9fe48c176cbcada92ff8c7d7af2e4e28c",
"gen_hash_zig": "sha256:4d8a3ad5d803c92076d0666838f1f2ec2fbc837662a157714cba8e868b6ef6d6",
"module": "GftDot8",
"ring": 12,
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:2d9ab503fe27887ed2f276cca7e093e3e017547d591f875a457a69bd58647f01",
"spec_path": "specs/ternary/gft_dot8.t27"
}
11 changes: 11 additions & 0 deletions .trinity/seals/ternary_GftLayer2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"gen_hash_c": "sha256:aeff0798c4375a3611554e7435f2f4f8f4a3f01e4da5fd1e6fd8657dd1b43027",
"gen_hash_rust": "sha256:77c1337c41161123a28bca8fc3cb85a521181702c62b78b6c79293b1a01e10be",
"gen_hash_verilog": "sha256:93baf008efbdc1215d2f887945ef2980b76a5f61c4f0e411830948b1a971d969",
"gen_hash_zig": "sha256:0ec32b741911475de3160677c669ecb818c192f52933959e8753663b77859123",
"module": "GftLayer2",
"ring": 12,
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:35a33a43c6ba2669c285f757c838ae44d8cd5028598ff7912370383a41ee7133",
"spec_path": "specs/ternary/gft_layer2.t27"
}
11 changes: 11 additions & 0 deletions .trinity/seals/ternary_GftMulRne.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"gen_hash_c": "sha256:c5dc68c01c9f66810431684dad2ed0dfe6db9267d06afa6e79dbf437aaccc062",
"gen_hash_rust": "sha256:bb1b28ae1841154247b3e794bd359fcdc92c840ed1bf636ca994a24cb4e891d3",
"gen_hash_verilog": "sha256:feee808883e376fdbabeb5fc51639497d91cf0bc2b704adec4383d0e53d81dab",
"gen_hash_zig": "sha256:aff1d546b1386dcfa7ff370d481636c1817382fbf87efd50bac2f21ce6db9c14",
"module": "GftMulRne",
"ring": 12,
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:d16bedfa83646e9e9df60e17c51928fabe4240cb5ca15ac682369f4dcc9fd9a6",
"spec_path": "specs/ternary/gft_mul_rne.t27"
}
11 changes: 11 additions & 0 deletions .trinity/seals/ternary_StreamTernaryMac.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"gen_hash_c": "sha256:f553ebdccd96b21d0ee140c3ede653ad4b13d8b3ee84af5f35743c130b7b5d63",
"gen_hash_rust": "sha256:0f6c4b2bb6cffb751322bc797df1701ac6c5048523dadb22282e2b550e1e395f",
"gen_hash_verilog": "sha256:48a0e07aae8a39a3dd62a323312143cef6efa160ae7da28c6031bf7a7e41685f",
"gen_hash_zig": "sha256:6f659088bd546472e7e3db64ea64ac431a5ab30d7800d04e564b639916e99326",
"module": "StreamTernaryMac",
"ring": 12,
"sealed_at": "2026-08-06T13:47:37Z",
"spec_hash": "sha256:f46c60b0ba669110f8cf4a169987c7c2e794b317bd6889f0e90e40cfcb158632",
"spec_path": "specs/ternary/stream_ternary_mac.t27"
}
125 changes: 119 additions & 6 deletions bootstrap/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4067,6 +4067,10 @@ pub struct VerilogCodegen {
// assignment (`<=`) instead of a blocking one, so a clocked `fn on_clock`
// body lowers to correct sequential logic inside an `always @(posedge clk)`.
clocked_nonblocking: bool,
// Scalar module-level `var`s exposed as `output reg` data ports (only in a
// clocked module -- one with an `on_clock` fn). Their body `reg` declaration
// is suppressed because the ANSI port header already declares them.
exposed_output_vars: std::collections::HashSet<String>,
// Width of the parameters of the function currently being lowered, keyed by
// parameter name. Populated in `gen_verilog_fn`. Used by `ExprCast` lowering
// to skip a redundant truncation mask when the operand is a parameter that is
Expand Down Expand Up @@ -4146,6 +4150,7 @@ impl VerilogCodegen {
current_fn_return_type: String::new(),
hoist_fn_locals: false,
clocked_nonblocking: false,
exposed_output_vars: std::collections::HashSet::new(),
param_widths: std::collections::HashMap::new(),
struct_decls: std::collections::HashMap::new(),
local_types: std::collections::HashMap::new(),
Expand Down Expand Up @@ -5487,6 +5492,7 @@ impl VerilogCodegen {
current_fn_return_type: String::new(),
hoist_fn_locals: false,
clocked_nonblocking: false,
exposed_output_vars: std::collections::HashSet::new(),
param_widths: self.param_widths.clone(),
struct_decls: self.struct_decls.clone(),
local_types: self.local_types.clone(),
Expand Down Expand Up @@ -5655,6 +5661,7 @@ impl VerilogCodegen {
current_fn_return_type: String::new(),
hoist_fn_locals: false,
clocked_nonblocking: false,
exposed_output_vars: std::collections::HashSet::new(),
param_widths: self.param_widths.clone(),
struct_decls: self.struct_decls.clone(),
local_types: self.local_types.clone(),
Expand Down Expand Up @@ -6187,6 +6194,52 @@ impl VerilogCodegen {

// Emit top-level module
let mod_name = self.module_name.clone();
// #1764: in a clocked module (an `on_clock` fn is present) the registered
// scalar `var`s ARE the module's observable state. Expose each as an
// `output reg` data port so the design synthesizes to real flip-flops
// instead of being dead-code-eliminated (yosys DCEs a port-less design to
// zero cells). Gated on `on_clock`, so non-clocked specs -- and every
// existing spec -- keep the byte-identical `(clk,rst_n,en,ready)` header.
let has_on_clock = functions.iter().any(|f| f.name == "on_clock");
self.exposed_output_vars.clear();
let mut exposed_ports: Vec<(String, usize, bool)> = Vec::new();
if has_on_clock {
for c in &consts {
let is_scalar = Self::parse_array_type(&c.extra_type).is_none()
&& !Self::is_primitive_array_type(&c.extra_type)
&& !self.struct_decls.contains_key(&c.extra_type);
if c.extra_mutable && is_scalar {
let w = Self::type_to_width(&c.extra_type) as usize;
let signed = Self::type_is_signed(&c.extra_type);
exposed_ports.push((c.name.clone(), w, signed));
self.exposed_output_vars.insert(c.name.clone());
}
}
}
// #1764: the parameters of `on_clock` (streaming) or `on_comb`
// (combinational) become INPUT data ports, so a spec can consume data fed
// in on real ports (`fn on_clock(x: i16) {...}` -> `input signed [15:0] x`).
// Only present when such a fn takes params -> existing specs unchanged.
let mut input_ports: Vec<(String, u32, bool)> = Vec::new();
if let Some(oc) = functions.iter().find(|f| f.name == "on_clock" || f.name == "on_comb") {
for (pname, ptype) in &oc.params {
let w = Self::type_to_width(ptype);
let signed = Self::type_is_signed(ptype);
input_ports.push((pname.clone(), w, signed));
}
}
// `on_comb` is the combinational counterpart of `on_clock`: its return is a
// continuously-driven `output wire result` (`assign result = on_comb(...)`),
// so a purely combinational spec (dot27, an adder, a whole MLP) synthesizes
// to real LUTs instead of being dead-code-eliminated. Only when defined.
let comb_result: Option<(u32, bool, Vec<String>)> =
functions.iter().find(|f| f.name == "on_comb").map(|f| {
let w = Self::type_to_width(&f.extra_return_type);
let signed = Self::type_is_signed(&f.extra_return_type);
let params: Vec<String> = f.params.iter().map(|(p, _)| p.clone()).collect();
(w, signed, params)
});

self.write_line(&format!("module {} (", mod_name));
self.indent();
self.write_indent();
Expand All @@ -6195,8 +6248,46 @@ impl VerilogCodegen {
self.write_line("input wire rst_n,");
self.write_indent();
self.write_line("input wire en,");
for (name, w, signed) in &input_ports {
let range = Self::range_decl(*w);
let signed_str = if *signed { "signed " } else { "" };
self.write_indent();
if range.is_empty() {
self.write_line(&format!("input wire {}{},", signed_str, name));
} else {
self.write_line(&format!("input wire {}{} {},", signed_str, range, name));
}
}
self.write_indent();
self.write_line("output wire ready");
let n_extra_out = exposed_ports.len() + if comb_result.is_some() { 1 } else { 0 };
if n_extra_out == 0 {
self.write_line("output wire ready");
} else {
self.write_line("output wire ready,");
let mut emitted = 0;
for (name, w, signed) in &exposed_ports {
let range = Self::range_decl(*w as u32);
let signed_str = if *signed { "signed " } else { "" };
emitted += 1;
let comma = if emitted < n_extra_out { "," } else { "" };
self.write_indent();
if range.is_empty() {
self.write_line(&format!("output reg {}{}{}", signed_str, name, comma));
} else {
self.write_line(&format!("output reg {}{} {}{}", signed_str, range, name, comma));
}
}
if let Some((w, signed, _)) = &comb_result {
let range = Self::range_decl(*w);
let signed_str = if *signed { "signed " } else { "" };
self.write_indent();
if range.is_empty() {
self.write_line(&format!("output wire {}result", signed_str));
} else {
self.write_line(&format!("output wire {}{} result", signed_str, range));
}
}
}
self.dedent();
self.write_line(");");
self.write_line("");
Expand Down Expand Up @@ -6329,6 +6420,13 @@ impl VerilogCodegen {
for f in &clocked {
self.gen_verilog_clocked_fn(f, &consts);
}
// `on_comb`: continuously drive the `result` output port from the
// combinational function of the input data ports.
if let Some((_, _, params)) = &comb_result {
self.write_line("");
self.write_indent();
self.write_line(&format!("assign result = on_comb({});", params.join(", ")));
}

// Section: Module-level statements (e.g. calls to array-param functions)
if !module_stmts.is_empty() {
Expand Down Expand Up @@ -7112,11 +7210,17 @@ impl VerilogCodegen {
self.write_line("end");
}
} else {
self.write(&format!(
"reg {}{}{};",
signed_str, range_str, safe_name
));
self.write_line("");
// An exposed output-reg var is already declared in the ANSI port
// header; emit only its power-on initializer here.
if self.exposed_output_vars.contains(&node.name) {
self.write_line(&format!("// {} exposed as output reg port", node.name));
} else {
self.write(&format!(
"reg {}{}{};",
signed_str, range_str, safe_name
));
self.write_line("");
}
if !node.children.is_empty() {
self.write_indent();
self.write_line("initial begin");
Expand Down Expand Up @@ -7390,6 +7494,14 @@ impl VerilogCodegen {
self.current_fn_name = node.name.clone();
self.local_types.clear();
self.param_types.clear();
self.param_widths.clear();
// `on_clock` params are streaming input data ports; register their widths
// and types so body references (casts, width-aware ops) resolve.
for (pname, ptype) in &node.params {
self.param_widths
.insert(pname.clone(), self.packed_width(ptype) as usize);
self.param_types.insert(pname.clone(), ptype.clone());
}
// Mirror gen_verilog_fn: cache any body-local variable types.
for stmt in &node.children {
if stmt.kind == NodeKind::StmtLocal && !stmt.name.is_empty() {
Expand Down Expand Up @@ -7438,6 +7550,7 @@ impl VerilogCodegen {
self.current_fn_name.clear();
self.local_types.clear();
self.param_types.clear();
self.param_widths.clear();
}

/// Emit a Verilog function body statement list, rewriting the
Expand Down
2 changes: 1 addition & 1 deletion bootstrap/stage0/FROZEN_HASH
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6607d41cbc98f18867814aad647587ba3d3c4d50b134c16a9683c4137c82752c
de57378ec0e294016ced91ad6976f19f73cf71ae3cf4db93f210bf754bdc9c6f
34 changes: 34 additions & 0 deletions bootstrap/tests/clocked_counter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,40 @@ fn spec_first_on_clock_registers_state() {
"clocked var was not updated with a nonblocking assignment:\n{}",
verilog
);
// The registered state must be exposed as a data output port -- otherwise a
// synthesizer dead-code-eliminates the whole design to zero cells (nothing
// observable drives an output). This is the difference between a simulation
// artifact and real hardware.
assert!(
verilog.contains("output reg [7:0] count"),
"clocked var `count` was not exposed as an output data port:\n{}",
verilog
);

// If yosys is available, prove the design synthesizes to REAL Artix-7
// hardware -- the 8-bit register must map to flip-flops, not vanish.
if tool_available("yosys") {
let dir = scratch_dir("synth");
fs::create_dir_all(&dir).expect("create synth dir");
fs::write(dir.join("cc.v"), &gen.stdout).expect("write cc.v");
let synth = Command::new("yosys")
.arg("-p")
.arg(format!(
"read_verilog -sv {}; synth_xilinx -top ClockedCounter; stat",
dir.join("cc.v").to_str().unwrap()
))
.output()
.expect("invoke yosys");
let s = String::from_utf8_lossy(&synth.stdout).into_owned()
+ &String::from_utf8_lossy(&synth.stderr);
let _ = fs::remove_dir_all(&dir);
assert!(synth.status.success(), "yosys synth_xilinx failed:\n{}", s);
assert!(
s.contains("FDCE") || s.contains("FDRE") || s.contains("FDPE") || s.contains("FDSE"),
"synth produced no flip-flops -- the register was optimized away:\n{}",
s
);
}

if !tool_available("iverilog") || !tool_available("vvp") {
eprintln!("SKIP: iverilog/vvp not on PATH; skipping clocked simulation");
Expand Down
Loading
Loading