From 8d9a05dbf712ef1f00f574cd7fc4216dfbbaec1b Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Sun, 9 Aug 2026 07:15:32 +0700 Subject: [PATCH] fix(gen-verilog): signed-aware ordered comparison Verilog makes an ordered comparison (< <= > >=) UNSIGNED if either operand is unsigned. A signed i8 (e.g. trend = -16 = 8'hF0) meeting an unsigned const read as 240, so `trend > THRESHOLD` diverged from the Rust/Zig/C backends, which promote both operands to a signed int before comparing. When at least one operand is signed, emit the comparison in the signed domain with C integer-promotion semantics: the signed operand is wrapped $signed(x) (Verilog sign-extends it into the wider signed comparison context) and the unsigned operand is zero-extended one bit, $signed({1'b0, x}), so its value stays non-negative. Unsigned/unsigned pairs are emitted unchanged. tri-net corpus: link_quality_monitor's degradation_detection flips to passing -- the last codegen-side runtime divergence. Full 105-spec icarus gate: 0 regressions. FROZEN_HASH resealed. Refs #1948 Co-Authored-By: Claude Fable 5 --- NOW.md | 7 +++++ bootstrap/src/compiler.rs | 60 +++++++++++++++++++++++++++++++++--- bootstrap/stage0/FROZEN_HASH | 2 +- docs/NOW.md | 7 +++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/NOW.md b/NOW.md index dff7c5cbf..c94b6c6c0 100644 --- a/NOW.md +++ b/NOW.md @@ -2,6 +2,13 @@ Last updated: 2026-08-09 +## gen-verilog: signed-aware ordered comparison (Refs #1948) + +- Verilog makes an ordered comparison (`< <= > >=`) UNSIGNED if either operand is unsigned, so a signed i8 (`trend = -16 = 8'hF0`) meeting an unsigned const read as 240 -- `trend > THRESHOLD` diverged from Rust/Zig/C (signed-int promotion) +- When at least one operand is signed, emit in the signed domain with C-promotion semantics: signed operand -> `$signed(x)` (sign-extends into the wider signed context), unsigned operand -> `$signed({1'b0, x})` (zero-extend one bit, stays non-negative). Unsigned/unsigned pairs untouched +- tri-net corpus: link_quality_monitor flips to passing (last codegen RT divergence); full 105-spec icarus gate: 0 regressions. Only rti_security remains out -- a spec-side assertion arithmetic bug, not codegen +- FROZEN_HASH resealed + ## gen-verilog: test-block call temps re-materialized after a rebinding (Refs #1948) - A call-return temp is CSE'd by call TEXT, but a test block mutates its bindings between statements (`st = on_ack(st);` repeated). Caching the temp across a reassignment reused a STALE value, so the state never advanced -- every step tested the pre-mutation snapshot diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index b4a8afb0a..2073bc158 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -5287,6 +5287,23 @@ impl VerilogCodegen { /// W539: infer the scalar bit width and signedness of an expression in the /// Icarus-lowerable subset. Returns None for non-scalar or unresolvable /// expressions (structs, arrays, strings, etc.). + /// W565: emit one operand of a signed-aware ordered comparison. A signed + /// operand is wrapped in `$signed(...)` (Verilog sign-extends it into the + /// wider signed comparison context); an unsigned operand is zero-extended by + /// one bit and reinterpreted as signed, `$signed({1'b0, ...})`, so its value + /// stays non-negative and the comparison follows C integer-promotion rules. + fn emit_verilog_signed_rel_operand(&mut self, node: &Node, is_signed: bool) { + if is_signed { + self.write("$signed("); + self.gen_verilog_expr(node); + self.write(")"); + } else { + self.write("$signed({1'b0, "); + self.gen_verilog_expr(node); + self.write("})"); + } + } + fn expr_width_signed(&self, node: &Node) -> Option<(u32, bool)> { match node.kind { NodeKind::ExprLiteral => { @@ -9763,11 +9780,44 @@ impl VerilogCodegen { ">>" => ">>", other => other, }; - self.write("("); - self.gen_verilog_expr(&node.children[0]); - self.write(&format!(" {} ", op)); - self.gen_verilog_expr(&node.children[1]); - self.write(")"); + // W565: signed-aware ordered comparison. Verilog makes an + // ordered comparison UNSIGNED if either operand is unsigned, + // so a signed i8 (e.g. trend = -16 = 8'hF0) meeting an + // unsigned const reads as 240 and the test diverges from + // Rust/Zig/C (which promote both to a signed int). When at + // least one operand is signed, compare in the signed domain + // with C-promotion semantics: the signed operand is wrapped + // in $signed() (Verilog sign-extends it into the wider signed + // context) and the unsigned operand is zero-extended one bit, + // $signed({1'b0, x}), so it stays non-negative. + let ordered_rel = matches!(op, "<" | "<=" | ">" | ">="); + let rel_signed = ordered_rel + && match ( + self.expr_width_signed(&node.children[0]), + self.expr_width_signed(&node.children[1]), + ) { + (Some((_, ls)), Some((_, rs))) => ls || rs, + _ => false, + }; + if rel_signed { + let (_, ls) = self + .expr_width_signed(&node.children[0]) + .unwrap_or((0, false)); + let (_, rs) = self + .expr_width_signed(&node.children[1]) + .unwrap_or((0, false)); + self.write("("); + self.emit_verilog_signed_rel_operand(&node.children[0], ls); + self.write(&format!(" {} ", op)); + self.emit_verilog_signed_rel_operand(&node.children[1], rs); + self.write(")"); + } else { + self.write("("); + self.gen_verilog_expr(&node.children[0]); + self.write(&format!(" {} ", op)); + self.gen_verilog_expr(&node.children[1]); + self.write(")"); + } } } } diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index 29734be72..d3ff269bf 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -512b60aa836b260e2cbb2b8ae3b33fb1b329eb68764535df8e21f18bfacade73 +274e2e4be8dd8bc505e656cdb24542f0534c0857bf50214b5c92e6a29595353b diff --git a/docs/NOW.md b/docs/NOW.md index 02afa2d79..014c4f9de 100644 --- a/docs/NOW.md +++ b/docs/NOW.md @@ -2,6 +2,13 @@ Last updated: 2026-08-09 +## gen-verilog: signed-aware ordered comparison (Refs #1948) + +- Verilog makes an ordered comparison (`< <= > >=`) UNSIGNED if either operand is unsigned. A signed `i8` (e.g. `trend = -16 = 8'hF0`) meeting an unsigned const read as 240, so `trend > THRESHOLD` diverged from Rust/Zig/C (which promote both to a signed int) +- When at least one operand is signed, the comparison is now emitted in the signed domain with C-promotion semantics: the signed operand is wrapped `$signed(x)` (Verilog sign-extends it into the wider signed context) and the unsigned operand is zero-extended one bit `$signed({1'b0, x})` so it stays non-negative. Unsigned/unsigned pairs are untouched +- tri-net corpus: link_quality_monitor's `degradation_detection` flips to passing (the last *codegen* runtime divergence); full 105-spec icarus gate: 0 regressions +- FROZEN_HASH resealed + ## gen-verilog: test-block call temps re-materialized after a rebinding (Refs #1948) - A call-return temp is CSE'd by call TEXT, but a test block mutates its bindings between statements (`st = on_ack(st);` repeated). Caching the temp across a reassignment reused a STALE value, so the modelled state never advanced -- every step re-tested the pre-mutation snapshot. Verilog-path-only (Rust/Zig/C evaluate the calls directly)