From c6d42c04dd98f96d77d37a9a7f1b998798cd5d70 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 01:28:59 +0700 Subject: [PATCH] t27c: accept uN casts, 1 <= N <= 64, decided in int_type_names.t27 (Closes #10072) `x as u3` and `x as u12` were refused at parse time by a hand-written whitelist in compiler.rs. The decision now lives in specs/compiler/int_type_names.t27 (`uint_bits`, with tests and negative controls: u0, u65, u012, u3x, banana stay refused). The parser consults the generated copy; zig_uint_bits defers to it past usize, so the reference truncates a narrowing cast exactly as t27b verilog's t27_uN does. Hand-written Rust: 3 lines added, 3 removed, in compiler.rs (the call site, the doc line, the error text). FROZEN_HASH resealed. New conformance spec verilog_odd_cast.t27 carries the Build-A-Chip-Bench moving average (u12 samples, `as u12`), narrowing and widening casts, and a module-level narrowing in on_clock. Refs #10066. Co-Authored-By: Claude Opus 5.5 --- .trinity/seals/compiler_IntTypeNames.json | 20 ++--- ...nformance_T27bConformanceOddWidthInts.json | 6 +- ...ormance_T27bConformanceVerilogOddCast.json | 28 +++++++ bootstrap/gen/rust/compiler/int_type_names.rs | 53 +++++++++---- bootstrap/src/compiler.rs | 6 +- bootstrap/stage0/FROZEN_HASH | 2 +- docs/reports/t27b_expectations.json | 1 + specs/compiler/int_type_names.t27 | 79 ++++++++++++++++--- specs/tri/t27b/conformance/odd_width_int.t27 | 2 +- .../tri/t27b/conformance/verilog_odd_cast.t27 | 70 ++++++++++++++++ 10 files changed, 224 insertions(+), 43 deletions(-) create mode 100644 .trinity/seals/conformance_T27bConformanceVerilogOddCast.json create mode 100644 specs/tri/t27b/conformance/verilog_odd_cast.t27 diff --git a/.trinity/seals/compiler_IntTypeNames.json b/.trinity/seals/compiler_IntTypeNames.json index a8a07909c7..372d903b14 100644 --- a/.trinity/seals/compiler_IntTypeNames.json +++ b/.trinity/seals/compiler_IntTypeNames.json @@ -1,29 +1,29 @@ { - "built_by": "t27c-bootstrap@0.5.2+ea1bccd", + "built_by": "t27c-bootstrap@0.5.2+ee3da59", "closure": [ { "spec": "specs/tri/t27b/type_text.t27", - "spec_hash": "sha256:f474fe76cd84a302b46401c9a1b7db0b6a7282c0f0c8a462e4a9493415d0d915" + "spec_hash": "sha256:0c2ea71bc30d0865c2fb2206de7a0499e836bba7da6d1340bb1bef116823f473" } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:1740064d2823e46c3785fbf7c43b442b0c15d0c4aecdc0012d19b7865f1b5b83", - "gen_hash_rust": "sha256:64c77221dd68b3e9c3e79bf611e5c9426b298faf7a08fb10b6e3926a92e076f3", - "gen_hash_verilog": "sha256:57571437b59aac3631bc9f2f850fe6dc4e98583987304cd2c0c639e39b3eb898", - "gen_hash_zig": "sha256:907e2194aa712faa772ed63a88b511c8567f09e2a923dd8a2eca840a8e7f8300", + "gen_hash_c": "sha256:1171f7cf9a3ba01dd60e34f57012d097d601729ec447efd62327f6695cf22513", + "gen_hash_rust": "sha256:bd09f357f64b7eab3a5c587dbf069c6c1561ec44f6c479e0acf0beb3667a2107", + "gen_hash_verilog": "sha256:057d18d09c0a9b8ba81e95f118b75f7a1de579312fce1ccfc57bb1fffd155645", + "gen_hash_zig": "sha256:b3608b162eef8df3ae7b6cdece1b868009fba678624a6cdd3e5b63fd06cdd051", "module": "IntTypeNames", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-11T13:03:09Z", + "sealed_at": "2026-10-17T18:22:57Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:5f7c1268c8c7a377d710513790be27dd4bd3c85fb2a6445af99a58211bb1e84d", + "spec_hash": "sha256:21600d9f4c201e54953fd0c27f059993b0939041dd4750944e51eb85ddc11275", "spec_path": "specs/compiler/int_type_names.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 4, - "total": 4, + "passed": 6, + "total": 6, "vacuous": 0 }, "toolchain": { diff --git a/.trinity/seals/conformance_T27bConformanceOddWidthInts.json b/.trinity/seals/conformance_T27bConformanceOddWidthInts.json index 053de006f2..ac468790d5 100644 --- a/.trinity/seals/conformance_T27bConformanceOddWidthInts.json +++ b/.trinity/seals/conformance_T27bConformanceOddWidthInts.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+cc19d1fcd", + "built_by": "t27c-bootstrap@0.5.2+ee3da59", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", "gen_hash_c": "sha256:4e37f84a8bb97a92df3c8b4cb25f6c44e882cf89fc9324e83065f81e426a8713", @@ -9,9 +9,9 @@ "module": "T27bConformanceOddWidthInts", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-11T10:29:43Z", + "sealed_at": "2026-10-17T18:23:00Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:e0ae99328f30f3c92eb64fc81fbc94ac4a730692dc40e53a941ce97fb7d65310", + "spec_hash": "sha256:1e679330ea9b8074f0399a50a7a1f108d44afa7b9de046a68eac353c699a149f", "spec_path": "specs/tri/t27b/conformance/odd_width_int.t27", "tests": { "failed": 0, diff --git a/.trinity/seals/conformance_T27bConformanceVerilogOddCast.json b/.trinity/seals/conformance_T27bConformanceVerilogOddCast.json new file mode 100644 index 0000000000..3cde519e08 --- /dev/null +++ b/.trinity/seals/conformance_T27bConformanceVerilogOddCast.json @@ -0,0 +1,28 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+ee3da59", + "closure": [], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:7aeec07814506d88186d27ed7c4c09888b396c27ebd50872922fd05606bb27b0", + "gen_hash_rust": "sha256:b5e5134185aef3f5e1c35f31e37e7bf7bef596627f4091a2c30ffeb52bc3db64", + "gen_hash_verilog": "sha256:c239e304111bb7da816ebe1005f352146bac9b3d01f079eeef3546e1739d878b", + "gen_hash_zig": "sha256:481bb9ba2b3a3317acd7d52c8332c795c3a720df9f3e03810f92695d64cf29a4", + "module": "T27bConformanceVerilogOddCast", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-17T18:22:59Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:1b073e86e2f4ec29ecd7693ac67cf1c2fadf77682afc25f798bdf7ef9a63f540", + "spec_path": "specs/tri/t27b/conformance/verilog_odd_cast.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 4, + "total": 4, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/bootstrap/gen/rust/compiler/int_type_names.rs b/bootstrap/gen/rust/compiler/int_type_names.rs index ff712230c3..dfef37c174 100644 --- a/bootstrap/gen/rust/compiler/int_type_names.rs +++ b/bootstrap/gen/rust/compiler/int_type_names.rs @@ -1,28 +1,45 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 4 test(s), 0 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 6 test(s), 0 invariant(s), 0 function(s) with no body. // This backend emits declarations only. The spec's checks live in // the Zig and Verilog outputs; do not read this file as verified. +pub fn uint_bits(ty: &[u8]) -> Option { + let lo: usize = trim_lo(ty, 0, ty.len()); + let hi: usize = trim_hi(ty, lo, ty.len()); + if (((hi - lo) < 2) || ((hi - lo) > 3)) { + return None; + } + if (ty[(lo) as usize] != 117) { + return None; + } + let p1: usize = (lo + 1); + if (ty[(p1) as usize] == 48) { + return None; + } + let mut w: u32 = 0; + let mut j: usize = p1; + while (j < hi) { + if !(digit(ty[(j) as usize])) { + return None; + } + let dv: u32 = ((ty[(j) as usize] - 48) as _); + w = ((w * 10) + dv); + j = (j + 1); + } + if ((w < 1) || (w > 64)) { + return None; + } + return Some(w); +} + pub fn zig_uint_bits(ty: &[u8]) -> Option { let lo: usize = trim_lo(ty, 0, ty.len()); let t = &ty[(lo) as usize..(trim_hi(ty, lo, ty.len())) as usize]; - if same(t, "u8") { - return Some(8); + if same(t, "usize") { + return Some(64); } else { - if same(t, "u16") { - return Some(16); - } else { - if same(t, "u32") { - return Some(32); - } else { - if (same(t, "u64") || same(t, "usize")) { - return Some(64); - } else { - return None; - } - } - } + return uint_bits(t); } } @@ -108,6 +125,10 @@ pub const B_SPACE: u8 = 32; pub const B_TAB: u8 = 9; +pub fn digit(c: u8) -> bool { + return ((c >= 48) && (c <= 57)); +} + pub fn same(s: &[u8], w: &'static str) -> bool { if (s.len() != w.len()) { return false; diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index ffacd2c4fd..5672034df3 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -5020,7 +5020,7 @@ the parser used to read it as `{}` followed by a negation", /// Consume the type that follows an `as` cast, e.g. `u32`, `i8`, or an /// array form like `u8[4]`. Returns the textual type for diagnostics. /// The base type name is validated against the known scalar type set so - /// that a typo like `x as u3` or `x as banana` is rejected at parse time + /// that `x as u0` or `x as banana` (uN: specs/compiler/int_type_names.t27) is rejected at parse time /// instead of silently lowering to a 32-bit default. fn parse_cast_target_type(&mut self) -> Result { if self.current.kind != TokenKind::Ident { @@ -5046,7 +5046,7 @@ the parser used to read it as `{}` followed by a negation", "bool", "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64", "usize", "f32", "f64", ]; - if !VALID_CAST_TYPES.contains(&base.as_str()) { + if !VALID_CAST_TYPES.contains(&base.as_str()) && int_type_names::uint_bits(base.as_bytes()).is_none() { // f32/f64 parse in DECLARATIONS, so "unknown type" would be a lie here. // The truth is narrower: no backend lowers float arithmetic -- the C // generator emits the literal token `f32`, which is not a C type, and @@ -5063,7 +5063,7 @@ the parser used to read it as `{}` followed by a negation", )); } return Err(format!( - "unknown cast target type `{}`; expected one of {:?}", + "unknown cast target type `{}`; expected one of {:?} or uN, 1 <= N <= 64", base, VALID_CAST_TYPES )); } diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index a5bb7cb394..df0d30ff7f 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -e769ef0217136b9bfdcb3dd783f02d8a8e23e91b70367416d02cdf61af4727e8 bootstrap/src/compiler.rs +3b53d48ddb5201c33b594b583beace6cc8e6a5a1dadee345a2b37dd38476e2be bootstrap/src/compiler.rs diff --git a/docs/reports/t27b_expectations.json b/docs/reports/t27b_expectations.json index 143113021d..2bf2a57fa9 100644 --- a/docs/reports/t27b_expectations.json +++ b/docs/reports/t27b_expectations.json @@ -1298,6 +1298,7 @@ {"path": "specs/tri/t27b/conformance/value_ignored.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/conformance/value_params.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/conformance/verilog_lib.t27", "t27b": "pass"}, + {"path": "specs/tri/t27b/conformance/verilog_odd_cast.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/conformance/verilog_prim.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/conformance/verilog_use.t27", "t27b": "pass"}, {"path": "specs/tri/t27b/conformance/verilog_widths.t27", "t27b": "pass"}, diff --git a/specs/compiler/int_type_names.t27 b/specs/compiler/int_type_names.t27 index eb2b072d62..c42d2d5b33 100644 --- a/specs/compiler/int_type_names.t27 +++ b/specs/compiler/int_type_names.t27 @@ -14,6 +14,8 @@ ; trim_lo / trim_hi (Rust's White_Space trim). ; - t27o compares with tri::mutate::plant's same_text, which gen-rust lowers to (&'static str, &'static str) ; while a sliced text here is &[u8]; tri::t27b::type_text's `same` has the slice signature. +; uint_bits (#10072) is written here, not converted. The parser's cast whitelist consults it, and +; zig_uint_bits defers to it past `usize`. ; Nothing here is written twice. ; -- ; The tests call each function by name. Their expected values are what the hand Rust returned on the lab @@ -26,22 +28,41 @@ module IntTypeNames; use tri::t27b::type_text; +/// The width of an unsigned type name `uN`, 1 <= N <= 64, or null (#10072). These are the widths `t27b +/// verilog` lowers (specs/tri/t27b/verilog_plan.t27, "Widths"), so they are the cast targets the parser +/// accepts beyond its standard names: `x as u3`, `avg as u12`. A name, not a number: no leading zero +/// (`u012`), no sign, no `usize`. u0 holds nothing and u65 is past the 64 bits a `t27b verilog` value +/// carries, so both stay refused, and so does a typo (`u3x`). +pub fn uint_bits(ty: []const u8) -> ?u32 { + const lo : usize = type_text::trim_lo(ty, 0, ty.len); + const hi : usize = type_text::trim_hi(ty, lo, ty.len); + if (hi - lo < 2 or hi - lo > 3) { return null; } + if (ty[lo] != 117) { return null; } + const p1 : usize = lo + 1; + if (ty[p1] == 48) { return null; } + var w : u32 = 0; + var j : usize = p1; + while (j < hi) { + if (!type_text::digit(ty[j])) { return null; } + const dv : u32 = @intCast(ty[j] - 48); + w = w * 10 + dv; + j = j + 1; + } + if (w < 1 or w > 64) { return null; } + return w; +} + /// W566: bit-width of a t27 UNSIGNED integer type, or null otherwise. Restricted to unsigned because /// Zig's `@truncate` only lowers a narrowing cast cleanly when both sides are unsigned; signed narrowing -/// stays on the checked `@intCast` path. +/// stays on the checked `@intCast` path. Every `uN` counts (#10072): `x as u3` keeps the low three bits +/// here as `t27_u3` does in `t27b verilog`, where `@intCast` would panic on 13. pub fn zig_uint_bits(ty: []const u8) -> ?u32 { const lo : usize = type_text::trim_lo(ty, 0, ty.len); const t = ty[lo .. type_text::trim_hi(ty, lo, ty.len)]; - if (type_text::same(t, "u8")) { - return 8; - } else if (type_text::same(t, "u16")) { - return 16; - } else if (type_text::same(t, "u32")) { - return 32; - } else if (type_text::same(t, "u64") or type_text::same(t, "usize")) { + if (type_text::same(t, "usize")) { return 64; } else { - return null; + return uint_bits(t); } } @@ -130,6 +151,46 @@ test zig_uint_bits_reads_unsigned_widths_only { assert zig_uint_bits("") == null; assert zig_uint_bits("u") == null; assert zig_uint_bits("i") == null; + assert zig_uint_bits("u3").? == 3; + assert zig_uint_bits(" u12 ").? == 12; + assert zig_uint_bits("u0") == null; + assert zig_uint_bits("u65") == null; + assert zig_uint_bits("i3") == null; +} + +test uint_bits_names_every_unsigned_width { + assert uint_bits("u1").? == 1; + assert uint_bits("u3").? == 3; + assert uint_bits("u8").? == 8; + assert uint_bits("u12").? == 12; + assert uint_bits("u21").? == 21; + assert uint_bits("u63").? == 63; + assert uint_bits("u64").? == 64; + assert uint_bits(" u12").? == 12; + assert uint_bits("u3\n").? == 3; + assert uint_bits("\tu9 ").? == 9; +} + +test uint_bits_refuses_what_is_not_a_width { + assert uint_bits("u0") == null; + assert uint_bits("u65") == null; + assert uint_bits("u99") == null; + assert uint_bits("u128") == null; + assert uint_bits("u012") == null; + assert uint_bits("u03") == null; + assert uint_bits("u00") == null; + assert uint_bits("u") == null; + assert uint_bits("") == null; + assert uint_bits(" ") == null; + assert uint_bits("i3") == null; + assert uint_bits("U3") == null; + assert uint_bits("u3x") == null; + assert uint_bits("x3") == null; + assert uint_bits("u 3") == null; + assert uint_bits("u-3") == null; + assert uint_bits("usize") == null; + assert uint_bits("bool") == null; + assert uint_bits("banana") == null; } test int_value_bits_counts_value_bits { diff --git a/specs/tri/t27b/conformance/odd_width_int.t27 b/specs/tri/t27b/conformance/odd_width_int.t27 index 5aa2d23567..2868467c50 100644 --- a/specs/tri/t27b/conformance/odd_width_int.t27 +++ b/specs/tri/t27b/conformance/odd_width_int.t27 @@ -8,7 +8,7 @@ ; operators, `~` on a narrow unsigned type, shifts that drop the bits that ; leave the type, products that pass 2^32 before they are checked, widening ; into wider types, `as` casts out of an odd width, and struct fields of odd -; widths. (t27c's `as` names only the standard widths as a target.) +; widths. Casts into an odd width are verilog_odd_cast.t27 (#10072). ; phi^2 + phi^-2 = 3 | TRINITY module T27bConformanceOddWidthInts; diff --git a/specs/tri/t27b/conformance/verilog_odd_cast.t27 b/specs/tri/t27b/conformance/verilog_odd_cast.t27 new file mode 100644 index 0000000000..115e592211 --- /dev/null +++ b/specs/tri/t27b/conformance/verilog_odd_cast.t27 @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/tri/t27b/conformance/verilog_odd_cast.t27 -- a cast into an odd unsigned width (gHashTag/t27#10072). +; t27c's parser refused `x as u3` and `x as u12` until uN, 1 <= N <= 64, was decided in +; specs/compiler/int_type_names.t27. `t27b verilog` lowers a narrowing cast through t27_uW and a widening one +; as a zero fill (verilog_plan.t27, "Widths"); the reference truncates with Zig's @truncate. Each test below +; passes in the reference, in t27b and in the generated test bench: +; - the 8-sample moving average the Build-A-Chip-Bench proxy run needed (epic #10066): u12 samples, a sum +; formed in 32 bits, and `as u12` back to the sample width; +; - `as u3` keeps the low three bits of a byte, `as u1` the low bit; +; - a cast out of u12 into u16 is a zero fill, and one into u8 keeps the low byte; +; - the same narrowing inside on_clock, in the module. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module T27bConformanceVerilogOddCast; + +pub fn avg8(a: u12, b: u12, c: u12, d: u12, e: u12, f: u12, g: u12, h: u12) -> u12 { + return (((a as u32) + (b as u32) + (c as u32) + (d as u32) + (e as u32) + (f as u32) + (g as u32) + (h as u32)) >> 3) as u12; +} + +pub fn low3(x: u8) -> u3 { + return x as u3; +} + +pub fn low_bit(x: u8) -> u1 { + return x as u1; +} + +pub fn wide(x: u12) -> u16 { + return x as u16; +} + +pub fn low_byte(x: u12) -> u8 { + return x as u8; +} + +pub var out : u12 = 0; + +fn on_clock(x: u16) { + out = (x >> 2) as u12; +} + +test the_average_comes_back_as_u12 { + assert_eq(avg8(8, 8, 8, 8, 8, 8, 8, 8), 8); + assert_eq(avg8(4095, 4095, 4095, 4095, 4095, 4095, 4095, 4095), 4095); + assert_eq(avg8(16, 0, 0, 0, 0, 0, 0, 0), 2); + assert_eq(avg8(1, 2, 3, 4, 5, 6, 7, 8), 4); +} + +test a_narrowing_keeps_the_low_bits { + assert_eq(low3(13), 5); + assert_eq(low3(255), 7); + assert_eq(low3(8), 0); + assert_eq(low_bit(6), 0); + assert_eq(low_bit(7), 1); + assert_eq(low_byte(4095), 255); + assert_eq(low_byte(300), 44); +} + +test a_widening_is_a_zero_fill { + assert_eq(wide(4095), 4095); + assert_eq(wide(0), 0); +} + +test the_module_narrows_at_its_width { + out = 0; + on_clock(65535); + assert_eq(out, 4095); + on_clock(400); + assert_eq(out, 100); +}