From 28f69c639f6b62794d68246cd2c2cc2eb5fc7775 Mon Sep 17 00:00:00 2001 From: SSD DDD Date: Sun, 9 Aug 2026 09:56:37 +0700 Subject: [PATCH] fix(tri_settle): wrapping round_add + add gen/zig (last execution orphan closed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tri_settle was the final spec still missing an execution backend beyond icarus/C: it panicked under zig on two counts. The narrowing u64->u32 cast in reward_weighted was fixed compiler-side (t27#1999); the second was round_add's wrap-detect saturating idiom (let sum = round_total + epoch_bytes; if (sum < round_total) ...) on a plain '+', which zig-safe arithmetic aborts on before the guard runs. Switch that site to '+%' (wrapping) -- a no-op for C and Verilog, which already wrap by width, and explicit for zig. tri_settle now passes all 17 test blocks under zig, with icarus (17/17) and C still green. Regenerate its gens: gen/rust flips to .wrapping_add() (rust-drift, dynamic since #366), and gen/zig/tri_settle.zig is committed for the first time -- the dynamic Zig-drift leg (#365) picks it up automatically, no list edit. gen/c is byte-identical ('+%' lowers to '+'). With this, every spec in the corpus executes in at least the icarus and C backends, and tri_settle joins Rust/Zig too. gen/ regeneration via the no-gen-edits escape (LEFTHOOK=0). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- gen/rust/tri_settle.rs | 3 +- gen/zig/tri_settle.zig | 147 +++++++++++++++++++++++++++++++++++++++++ specs/tri_settle.t27 | 2 +- 3 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 gen/zig/tri_settle.zig diff --git a/gen/rust/tri_settle.rs b/gen/rust/tri_settle.rs index 0c259831..1713ea2f 100644 --- a/gen/rust/tri_settle.rs +++ b/gen/rust/tri_settle.rs @@ -8,7 +8,7 @@ pub const SNR_FLOOR: u32 = 3; pub const SNR_CEIL: u32 = 40; pub fn round_add(round_total: u32, epoch_bytes: u32) -> u32 { - let sum: u32 = (round_total + epoch_bytes); + let sum: u32 = (round_total).wrapping_add(epoch_bytes); if (sum < round_total) { return 0xFFFFFFFF; } else { @@ -65,4 +65,3 @@ pub fn snr_to_quality(snr_db: u32) -> u32 { } } } - diff --git a/gen/zig/tri_settle.zig b/gen/zig/tri_settle.zig new file mode 100644 index 00000000..9be66656 --- /dev/null +++ b/gen/zig/tri_settle.zig @@ -0,0 +1,147 @@ +// Generated from t27 spec: TriSettle (module name) +// DO NOT EDIT — generated by t27c +// phi^2 + 1/phi^2 = 3 | TRINITY + +const std = @import("std"); + +// use types: no references in this module +const PPM: u32 = 1000000; +const SNR_FLOOR: u32 = 3; +const SNR_CEIL: u32 = 40; +fn round_add(round_total: u32, epoch_bytes: u32) u32 { + const sum: u32 = round_total +% epoch_bytes; + if (sum < round_total) { + return 0xFFFFFFFF; + } else { + return sum; + } +} +fn reward_share_ppm(node_bytes: u32, round_bytes: u32) u32 { + if (round_bytes == 0) { + return 0; + } else { + const scaled: u64 = @as(u64, @intCast(node_bytes)) * @as(u64, @intCast(PPM)); + return @as(u32, @truncate(scaled / @as(u64, @intCast(round_bytes)))); + } +} +fn reward_units(node_bytes: u32, round_bytes: u32, pool: u32) u64 { + if (round_bytes == 0) { + return 0; + } else { + const num: u64 = @as(u64, @intCast(pool)) * @as(u64, @intCast(node_bytes)); + return num / @as(u64, @intCast(round_bytes)); + } +} +test "share_proportional" { + const half = reward_share_ppm(100, 200); + const quarter = reward_share_ppm(50, 200); + if (!(half == 500000)) @panic("half"); + if (!(quarter == 250000)) @panic("quarter"); + if (!(half == (quarter + quarter))) @panic("2x bytes = 2x share"); +} +test "share_full" { + if (!(reward_share_ppm(200, 200) == PPM)) @panic("full share"); +} +test "share_zero_guard" { + if (!(reward_share_ppm(5, 0) == 0)) @panic("empty round pays nobody"); + if (!(reward_share_ppm(0, 200) == 0)) @panic("no work no share"); +} +test "units_no_free_mint" { + if (!(reward_units(0, 200, 1000) == 0)) @panic("no free mint"); +} +test "units_proportional" { + if (!(reward_units(100, 200, 1000) == 500)) @panic("half of pool"); +} +test "units_conservation" { + const a = reward_units(100, 300, 1000); + const b = reward_units(200, 300, 1000); + if (!(a == 333)) @panic("node a"); + if (!(b == 666)) @panic("node b"); + if (!((a + b) <= @as(u64, @intCast(1000)))) @panic("sum <= pool"); +} +test "round_monotonic" { + const r0 = round_add(0, 19974); + const r1 = round_add(r0, 40000); + if (!(r1 > r0)) @panic("monotonic"); + if (!(r0 == 19974)) @panic("epoch1"); + if (!(r1 == 59974)) @panic("epoch1+2"); +} +test "round_saturates" { + if (!(round_add(0xFFFFFF00, 0x0000FFFF) == 0xFFFFFFFF)) @panic("saturates"); +} +fn weighted_contrib(node_bytes: u32, quality: u32) u64 { + return @as(u64, @intCast(node_bytes)) * @as(u64, @intCast(quality)); +} +fn round_add_weighted(wtotal: u64, node_bytes: u32, quality: u32) u64 { + return wtotal + weighted_contrib(node_bytes, quality); +} +fn reward_weighted(node_bytes: u32, quality: u32, weighted_total: u64, pool: u32) u64 { + const hi: u32 = @as(u32, @truncate(weighted_total >> 32)); + const lo: u32 = @as(u32, @truncate(weighted_total)); + if ((hi == 0) and (lo == 0)) { + return 0; + } else { + const contrib: u64 = @as(u64, @intCast(node_bytes)) * @as(u64, @intCast(quality)); + const ppm: u64 = (contrib * 1000000) / weighted_total; + return (@as(u64, @intCast(pool)) * ppm) / 1000000; + } +} +test "weighted_quality_rewards_coverage" { + const wt = round_add_weighted(round_add_weighted(0, 1000, 47), 1000, 10); + const a = reward_weighted(1000, 47, wt, 1000); + const b = reward_weighted(1000, 10, wt, 1000); + if (!(a > b)) @panic("better link earns more per byte"); + if (!(wt == @as(u64, @intCast(57000)))) @panic("weighted total = 47000 + 10000"); +} +test "weighted_equal_quality_is_proportional" { + const wt = round_add_weighted(round_add_weighted(0, 100, 5), 200, 5); + const a = reward_weighted(100, 5, wt, 1000); + const b = reward_weighted(200, 5, wt, 1000); + if (!(a == 333)) @panic("third"); + if (!(b == 666)) @panic("two thirds"); +} +test "weighted_conservation" { + const wt = round_add_weighted(round_add_weighted(0, 1000, 47), 500, 30); + const a = reward_weighted(1000, 47, wt, 10000); + const b = reward_weighted(500, 30, wt, 10000); + if (!((a + b) <= @as(u64, @intCast(10000)))) @panic("sum <= pool"); +} +test "weighted_zero_quality" { + const wt = round_add_weighted(0, 1000, 47); + if (!(reward_weighted(1000, 0, wt, 1000) == 0)) @panic("dead link no reward"); +} +test "weighted_large_total_not_zero" { + const big = @as(u64, @intCast(4294967296)); + const r = reward_weighted(1000000, 1000, big, 1000); + if (!(r > @as(u64, @intCast(0)))) @panic("large total not read as zero"); +} +fn snr_to_quality(snr_db: u32) u32 { + if (snr_db <= SNR_FLOOR) { + return 0; + } else if (snr_db >= SNR_CEIL) { + return SNR_CEIL - SNR_FLOOR; + } else { + return snr_db - SNR_FLOOR; + } +} +test "snr_quality_floor" { + if (!(snr_to_quality(1) == 0)) @panic("1 dB unusable"); + if (!(snr_to_quality(3) == 0)) @panic("at floor unusable"); +} +test "snr_quality_measured" { + if (!(snr_to_quality(34) == 31)) @panic("strong link (.13->.12 -10 dB)"); + if (!(snr_to_quality(5) == 2)) @panic("weak link (.13->.12 -50 dB)"); + if (!(snr_to_quality(34) > snr_to_quality(5))) @panic("stronger link -> higher quality"); +} +test "snr_quality_ceiling" { + if (!(snr_to_quality(100) == 37)) @panic("saturates at CEIL-FLOOR"); + if (!(snr_to_quality(40) == 37)) @panic("at ceiling"); +} +test "snr_drives_reward" { + const qa = snr_to_quality(34); + const qb = snr_to_quality(5); + const wt = round_add_weighted(round_add_weighted(0, 10000, qa), 10000, qb); + const ra = reward_weighted(10000, qa, wt, 1000); + const rb = reward_weighted(10000, qb, wt, 1000); + if (!(ra > rb)) @panic("better coverage earns more $TRI"); +} diff --git a/specs/tri_settle.t27 b/specs/tri_settle.t27 index dfda5487..2931dae6 100644 --- a/specs/tri_settle.t27 +++ b/specs/tri_settle.t27 @@ -14,7 +14,7 @@ module TriSettle { // Fold one epoch's verified byte count into the running round total. // Saturating so an overflow can never wrap the round total DOWNWARD. fn round_add(round_total: u32, epoch_bytes: u32) -> u32 { - let sum: u32 = round_total + epoch_bytes; + let sum: u32 = round_total +% epoch_bytes; if (sum < round_total) { return 0xFFFFFFFF; // saturate } else {