diff --git a/.claude/skills/ci-gates/SKILL.md b/.claude/skills/ci-gates/SKILL.md index 201cdb82d5..d7471ddef6 100644 --- a/.claude/skills/ci-gates/SKILL.md +++ b/.claude/skills/ci-gates/SKILL.md @@ -11245,3 +11245,88 @@ so a genuinely RED mutant -- where the total splits across `passed` and count has to add up every bucket the count can land in**, which is the parts-sum-to-the-whole rule from the other side. Total = passed + failed; compare that. + +## 441. Your new marker is only yours if the BASE count is zero + +I added a comment the emitter prints where a `break` cannot be lowered, then +counted it in the regenerated corpus to see how often that happens: + +``` +files with NOT LOWERED 535 +``` + +535 of 581 -- for a change that alters **7** files. Two measurements of the same +run, flatly contradicting each other. + +The emitter already prints `// NOT LOWERED BY THIS BACKEND`, for something +unrelated, in 535 files. My marker fired **zero** times. The grep was not wrong +about anything; it answered the question I asked, and I had asked for a phrase +that was not mine. + +**The control for "my change introduced N of these" is the count on the BASE.** +It costs one more grep and it is the only thing that distinguishes a marker from +a substring of somebody else's sentence. A base count of zero is what earns the +word "mine". + +The same rule survives into the instrument: `tri jumps census` keys its refusal +count on `t27#2988:` -- the issue number -- and not on the English, precisely +because the English collides. A marker you intend to count later should carry +something no prose would contain. + +## 442. A two-part construct needs its two parts asserted as a pair + +A guard flag is a `reg` the loop declares and an assignment the `break` writes. +I had five tests on the lowering, killed five mutants with them, added a sixth +test that counted declarations -- *"only the inner loop owns a flag"* -- and +then ran the mutant that binds `break` to the OUTERMOST enclosing loop instead +of the innermost. + +It **survived**. Of course it did: the declarations are emitted by the loop and +the mutation is in the jump, so exactly one flag is still declared per loop that +needs one. Every count is satisfied. What breaks is the *pairing* -- the flag is +declared, cleared, and never set, and the `break` prints the "no guard flag in +this scope" refusal for a scope that has one. + +The assertion that kills it is one line and does not count anything: + +```rust +assert_eq!(declared_ids, ids_that_are_written_with(" = 1'b1;")); +``` + +**Where a construct has two halves that must name each other -- declare/use, +open/close, charge/refund, allocate/free -- assert the CORRESPONDENCE.** A count +of either half is satisfied by every mutant that keeps the arithmetic and breaks +the binding, and those are the interesting ones. This repository already learned +the same shape in money: a refund equal to the *tariff* passes every total, and +only reconciling against the *actual charge* finds the divergence. + +## 443. When only the bytes moved, only the moved bytes need the expensive ruler + +The change regenerates 581 Verilog files and **7** of them differ. The question +was whether it costs anything under `yosys` -- a ruler slow enough that nobody +runs it over the corpus casually. + +574 files are byte-identical, and `yosys read_verilog` is a deterministic +function of the bytes, so their verdicts cannot move. Running the ruler on 7 +files answers the question for 581: **+2 pass, 0 lost**. + +Two conditions make this legitimate, and both need saying out loud rather than +assuming: + +* the ruler is **per-file** (no whole-corpus state, no ordering effect), and +* the byte comparison covered the **whole population**, not a sample. + +Say which one you did. A bare "+2 under yosys" and "yosys, on the 7 files that +changed, +2 with no losses; the other 574 are byte-identical" are the same fact, +but only the second one survives a reader asking how long that took -- and only +the second one refuses to be read as a whole-corpus absolute that was never +taken. + +And carry the population to EVERY consumer, not just the one you were looking at. +I had "7 generated files differ" and still shipped without a reseal, because I +never asked what 7 files are in the units the seal gate counts. They are **19 +seal files** -- specs here are twinned, several `.trinity/seals/*.json` per spec +-- and `coverage` said so on the first CI round. The rule "the reseal belongs in +the same commit" is already written down three times on this page; what was +missing this time was not the rule but the arithmetic that connects a delta in +one unit to a gate that counts in another. diff --git a/.trinity/seals/AspSolver.json b/.trinity/seals/AspSolver.json index d0079fdc37..07e18fc7e2 100644 --- a/.trinity/seals/AspSolver.json +++ b/.trinity/seals/AspSolver.json @@ -1,7 +1,7 @@ { "gen_hash_c": "sha256:6853fa2f0ade52dabfe66d11846ce96374ef244b644ef8bf1a49b5ded8c53d6b", "gen_hash_rust": "sha256:55933339b5fe7bf29aca7771520157d43d838784b249e3518dbeaa84a5ad89b6", - "gen_hash_verilog": "sha256:4a3aac88230f26e414bd206d0168d8063b891db31ae52d7ed63c02186a0d6944", + "gen_hash_verilog": "sha256:ab0ce745df6cc1f1e0227ac10fb13fc07517ddac3b15065d04240d86b1232615", "gen_hash_zig": "sha256:eceaded4f26af6e30cd424ffcc9318a85eaf61562f0c1de85d85260f4c45437f", "module": "AspSolver", "ring": 12, diff --git a/.trinity/seals/BaseTypes.json b/.trinity/seals/BaseTypes.json index 62296bb1da..0437ce1be0 100644 --- a/.trinity/seals/BaseTypes.json +++ b/.trinity/seals/BaseTypes.json @@ -2,7 +2,7 @@ "conformance_hash": "sha256:3b60977a2e6ef7a89d9c0e7ae9c8a64cd928a2bdc65d0fd46b57baebd0e9ad39", "gen_hash_c": "sha256:cd2ad734d4c56c11d76f00941351fbb1b7afbe3c31672171d1831d8b6edc92aa", "gen_hash_rust": "sha256:fbcebfa1583296107b8765cc921b3d459d2c90b2e00303f930d243313f4aa20f", - "gen_hash_verilog": "sha256:d8d7c843ee5f2ce5883920ed6fdc87998a332b3a3e481dbdccaeb4ce1dd278c6", + "gen_hash_verilog": "sha256:fb66d07df7fad1fd638df94287b7d44457cac0d32729f93d54e88bee7b902f94", "gen_hash_zig": "sha256:0286c2d52b3521ae781dab3c27e977bdef20c218bca8ff4d81af17cb525a5b71", "module": "BaseTypes", "ring": 25, diff --git a/.trinity/seals/Lexing.json b/.trinity/seals/Lexing.json index 7a16f700e3..dc48043f1f 100644 --- a/.trinity/seals/Lexing.json +++ b/.trinity/seals/Lexing.json @@ -1,7 +1,7 @@ { "gen_hash_c": "sha256:d54d21bda496461e70febcf8855037a45164dc0a2212cc71411580a23d4da74f", "gen_hash_rust": "sha256:6a0e73f0d133ae0a90cf5b68b139de266a5d32b6fb2dafe1df7e62047cff6ad7", - "gen_hash_verilog": "sha256:88761f053fa391f6d6f86fa180b7011c0e6f82547d8dd32cab658fe4f54d7f84", + "gen_hash_verilog": "sha256:5807318b282900d93a6563366b253d985000e27818282d4253c1d24c0167f6a6", "gen_hash_zig": "sha256:7da42e5e458cb1a4fe300a2001f5edeb792cc9ff8278efd87ab589562ebbb2ee", "module": "Lexing", "ring": 12, diff --git a/.trinity/seals/MAC_Testbench.json b/.trinity/seals/MAC_Testbench.json index 5bbf866b50..fa35d2c2d2 100644 --- a/.trinity/seals/MAC_Testbench.json +++ b/.trinity/seals/MAC_Testbench.json @@ -1,7 +1,7 @@ { "gen_hash_c": "sha256:33cf769212351db3c992bd82921dd80b938755b6da543061eaa4e7c39b552add", "gen_hash_rust": "sha256:e8bc2d3de487ecbc6d2d0c43767dcb87c4fee84004d2dafb973f55e6f8e009d2", - "gen_hash_verilog": "sha256:85a771ac9b82d59dc7ffd9e310e7cbddc5566a0e82357d93d299aa38725b16d6", + "gen_hash_verilog": "sha256:9cab81dcdc6b62afe3b481d26ee1ea47ab2c776af8358624acdb6a0433f29e09", "gen_hash_zig": "sha256:8928e5f9b62c3721e845fdc7acef8d518e8ea0c343572ca3e5a38303b5e1cffb", "module": "MAC_Testbench", "ring": 12, diff --git a/.trinity/seals/PhiUniversalAttractor.json b/.trinity/seals/PhiUniversalAttractor.json index 1d4c1d9607..158b4a771e 100644 --- a/.trinity/seals/PhiUniversalAttractor.json +++ b/.trinity/seals/PhiUniversalAttractor.json @@ -1,7 +1,7 @@ { "gen_hash_c": "sha256:f87af83c0ff6c847d32612b230569d4c0035683bb05e56f51a180d2f11570eed", "gen_hash_rust": "sha256:f70dfdf4a3fe4e99053c462a0fba0b1be779ae2a7d9d269818c7ca4c1246851c", - "gen_hash_verilog": "sha256:1c50cc362846e4d205dbc32445535b01ec463b50d96dc061eb8039974f8e6ba1", + "gen_hash_verilog": "sha256:57cc672e29d6b8b23a4b66343155520fe6d890ffb46c38ec621e66ca6d857a5b", "gen_hash_zig": "sha256:dbba7f928af7d8a9672028e095e782314e7bccd0a5cee09290c602384d3ee036", "module": "PhiUniversalAttractor", "ring": 12, diff --git a/.trinity/seals/SacredAttention.json b/.trinity/seals/SacredAttention.json index 965bf61566..b0f36de642 100644 --- a/.trinity/seals/SacredAttention.json +++ b/.trinity/seals/SacredAttention.json @@ -1,7 +1,7 @@ { "gen_hash_c": "sha256:f4454df1de407b648599bab4486b3a2161b55cb98e8bf08c3dccce823ebfd3ac", "gen_hash_rust": "sha256:f2d59715911ebcf18c9614c38e5822ebd4929b151a6b0f869945a552e4de89d5", - "gen_hash_verilog": "sha256:19e519134c8b225290175e4723a340a2ffde8b3e5fd392192b91c6d0c1676248", + "gen_hash_verilog": "sha256:f10591dc966a8ac3535e358d35427cb7b03128d6095bad3d76f51b5ca8514f2f", "gen_hash_zig": "sha256:04869b3b6648bcb78f992e671734c17295760ccce99c82281f6969a55ff4a5b9", "module": "SacredAttention", "ring": 12, diff --git a/.trinity/seals/ar_AspSolver.json b/.trinity/seals/ar_AspSolver.json index d0079fdc37..07e18fc7e2 100644 --- a/.trinity/seals/ar_AspSolver.json +++ b/.trinity/seals/ar_AspSolver.json @@ -1,7 +1,7 @@ { "gen_hash_c": "sha256:6853fa2f0ade52dabfe66d11846ce96374ef244b644ef8bf1a49b5ded8c53d6b", "gen_hash_rust": "sha256:55933339b5fe7bf29aca7771520157d43d838784b249e3518dbeaa84a5ad89b6", - "gen_hash_verilog": "sha256:4a3aac88230f26e414bd206d0168d8063b891db31ae52d7ed63c02186a0d6944", + "gen_hash_verilog": "sha256:ab0ce745df6cc1f1e0227ac10fb13fc07517ddac3b15065d04240d86b1232615", "gen_hash_zig": "sha256:eceaded4f26af6e30cd424ffcc9318a85eaf61562f0c1de85d85260f4c45437f", "module": "AspSolver", "ring": 12, diff --git a/.trinity/seals/ar_asp_solver.json b/.trinity/seals/ar_asp_solver.json index 1e4d63f1f7..70280cf986 100644 --- a/.trinity/seals/ar_asp_solver.json +++ b/.trinity/seals/ar_asp_solver.json @@ -1,11 +1,11 @@ { "gen_hash_c": "sha256:6853fa2f0ade52dabfe66d11846ce96374ef244b644ef8bf1a49b5ded8c53d6b", "gen_hash_rust": "sha256:55933339b5fe7bf29aca7771520157d43d838784b249e3518dbeaa84a5ad89b6", - "gen_hash_verilog": "sha256:4a3aac88230f26e414bd206d0168d8063b891db31ae52d7ed63c02186a0d6944", + "gen_hash_verilog": "sha256:ab0ce745df6cc1f1e0227ac10fb13fc07517ddac3b15065d04240d86b1232615", "gen_hash_zig": "sha256:eceaded4f26af6e30cd424ffcc9318a85eaf61562f0c1de85d85260f4c45437f", "module": "asp_solver", "ring": 12, - "sealed_at": "2026-08-30T18:16:28Z", + "sealed_at": "2026-09-03T10:13:51Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:eca06ff812c61e2693f706b7389c71dcd769c30dfbb2cfb958e0416a5118b404", "spec_path": "specs/ar/asp_solver.t27" diff --git a/.trinity/seals/asp_solver.json b/.trinity/seals/asp_solver.json index 2de79bef67..fd0cf93785 100644 --- a/.trinity/seals/asp_solver.json +++ b/.trinity/seals/asp_solver.json @@ -1,7 +1,7 @@ { "gen_hash_c": "sha256:6853fa2f0ade52dabfe66d11846ce96374ef244b644ef8bf1a49b5ded8c53d6b", "gen_hash_rust": "sha256:55933339b5fe7bf29aca7771520157d43d838784b249e3518dbeaa84a5ad89b6", - "gen_hash_verilog": "sha256:4a3aac88230f26e414bd206d0168d8063b891db31ae52d7ed63c02186a0d6944", + "gen_hash_verilog": "sha256:ab0ce745df6cc1f1e0227ac10fb13fc07517ddac3b15065d04240d86b1232615", "gen_hash_zig": "sha256:eceaded4f26af6e30cd424ffcc9318a85eaf61562f0c1de85d85260f4c45437f", "module": "asp_solver", "ring": 12, diff --git a/.trinity/seals/base_tritype-base.json b/.trinity/seals/base_tritype-base.json index c3b2d9ea4d..56496c7132 100644 --- a/.trinity/seals/base_tritype-base.json +++ b/.trinity/seals/base_tritype-base.json @@ -1,11 +1,11 @@ { "gen_hash_c": "sha256:cd2ad734d4c56c11d76f00941351fbb1b7afbe3c31672171d1831d8b6edc92aa", "gen_hash_rust": "sha256:fbcebfa1583296107b8765cc921b3d459d2c90b2e00303f930d243313f4aa20f", - "gen_hash_verilog": "sha256:d8d7c843ee5f2ce5883920ed6fdc87998a332b3a3e481dbdccaeb4ce1dd278c6", + "gen_hash_verilog": "sha256:fb66d07df7fad1fd638df94287b7d44457cac0d32729f93d54e88bee7b902f94", "gen_hash_zig": "sha256:0286c2d52b3521ae781dab3c27e977bdef20c218bca8ff4d81af17cb525a5b71", "module": "tritype-base", "ring": 12, - "sealed_at": "2026-09-03T07:22:13Z", + "sealed_at": "2026-09-03T10:13:51Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:12d9ca3f44b081970a88c4d24c10ee9dbf36eb614cf18404ff571475552c10db", "spec_path": "specs/base/types.t27" diff --git a/.trinity/seals/cli_gen_commands.json b/.trinity/seals/cli_gen_commands.json index dedefcf77c..586b05e89c 100644 --- a/.trinity/seals/cli_gen_commands.json +++ b/.trinity/seals/cli_gen_commands.json @@ -1,11 +1,11 @@ { "gen_hash_c": "sha256:dd4db6dff0df04da9fbb36636078a2fc5083b33f34c6e1f1a7ec14fb574b8c41", "gen_hash_rust": "sha256:78df35165688d2fa0c365f459dce87e18600316530a24526ac175b587d5c67eb", - "gen_hash_verilog": "sha256:f01c4379a2a5ecb08570d34f9a10090380f2be767568a7e81d2c5835ad7181c0", + "gen_hash_verilog": "sha256:9438160852d64b29290d043b8901105bae4848dd9275bd4e34f558e06d70c985", "gen_hash_zig": "sha256:7b6328077fae13ac3d8cdd17bc883c31b8e9ba99cd56daf86957f330338db7f2", "module": "gen_commands", "ring": 12, - "sealed_at": "2026-08-30T19:43:01Z", + "sealed_at": "2026-09-03T10:13:51Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:4dd1bf05316f1a0ed952457e8fbbe7e03c51d51b2891c1dfe603db0425b4f2a3", "spec_path": "compiler/cli/gen.t27" diff --git a/.trinity/seals/compiler_Lexing.json b/.trinity/seals/compiler_Lexing.json index 12ecd7be47..6eb2652387 100644 --- a/.trinity/seals/compiler_Lexing.json +++ b/.trinity/seals/compiler_Lexing.json @@ -1,11 +1,11 @@ { "gen_hash_c": "sha256:d54d21bda496461e70febcf8855037a45164dc0a2212cc71411580a23d4da74f", "gen_hash_rust": "sha256:6a0e73f0d133ae0a90cf5b68b139de266a5d32b6fb2dafe1df7e62047cff6ad7", - "gen_hash_verilog": "sha256:88761f053fa391f6d6f86fa180b7011c0e6f82547d8dd32cab658fe4f54d7f84", + "gen_hash_verilog": "sha256:5807318b282900d93a6563366b253d985000e27818282d4253c1d24c0167f6a6", "gen_hash_zig": "sha256:7da42e5e458cb1a4fe300a2001f5edeb792cc9ff8278efd87ab589562ebbb2ee", "module": "Lexing", "ring": 12, - "sealed_at": "2026-08-30T18:16:28Z", + "sealed_at": "2026-09-03T10:13:51Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:4bc5143fba34174835090dd6b44f8b82feeb5462e3c52407d60d85efb3d6988a", "spec_path": "specs/compiler/lexer.t27" diff --git a/.trinity/seals/gen_commands.json b/.trinity/seals/gen_commands.json index bf808c516a..7980e91e4a 100644 --- a/.trinity/seals/gen_commands.json +++ b/.trinity/seals/gen_commands.json @@ -1,7 +1,7 @@ { "gen_hash_c": "sha256:dd4db6dff0df04da9fbb36636078a2fc5083b33f34c6e1f1a7ec14fb574b8c41", "gen_hash_rust": "sha256:78df35165688d2fa0c365f459dce87e18600316530a24526ac175b587d5c67eb", - "gen_hash_verilog": "sha256:f01c4379a2a5ecb08570d34f9a10090380f2be767568a7e81d2c5835ad7181c0", + "gen_hash_verilog": "sha256:9438160852d64b29290d043b8901105bae4848dd9275bd4e34f558e06d70c985", "gen_hash_zig": "sha256:7b6328077fae13ac3d8cdd17bc883c31b8e9ba99cd56daf86957f330338db7f2", "module": "gen_commands", "ring": 12, diff --git a/.trinity/seals/math_PhiUniversalAttractor.json b/.trinity/seals/math_PhiUniversalAttractor.json index 9e5edbacc8..8c50c8337c 100644 --- a/.trinity/seals/math_PhiUniversalAttractor.json +++ b/.trinity/seals/math_PhiUniversalAttractor.json @@ -1,11 +1,11 @@ { "gen_hash_c": "sha256:f87af83c0ff6c847d32612b230569d4c0035683bb05e56f51a180d2f11570eed", "gen_hash_rust": "sha256:f70dfdf4a3fe4e99053c462a0fba0b1be779ae2a7d9d269818c7ca4c1246851c", - "gen_hash_verilog": "sha256:1c50cc362846e4d205dbc32445535b01ec463b50d96dc061eb8039974f8e6ba1", + "gen_hash_verilog": "sha256:57cc672e29d6b8b23a4b66343155520fe6d890ffb46c38ec621e66ca6d857a5b", "gen_hash_zig": "sha256:dbba7f928af7d8a9672028e095e782314e7bccd0a5cee09290c602384d3ee036", "module": "PhiUniversalAttractor", "ring": 12, - "sealed_at": "2026-08-30T21:18:16Z", + "sealed_at": "2026-09-03T10:13:51Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:3a5a1d0bf24c0a354559c859ce379e3fa65b8aad313da8627e341daa273d4adf", "spec_path": "specs/math/phi_universal_attractor.t27" diff --git a/.trinity/seals/nn_SacredAttention.json b/.trinity/seals/nn_SacredAttention.json index 47d09816b0..bf88799e5f 100644 --- a/.trinity/seals/nn_SacredAttention.json +++ b/.trinity/seals/nn_SacredAttention.json @@ -1,11 +1,11 @@ { "gen_hash_c": "sha256:f4454df1de407b648599bab4486b3a2161b55cb98e8bf08c3dccce823ebfd3ac", "gen_hash_rust": "sha256:f2d59715911ebcf18c9614c38e5822ebd4929b151a6b0f869945a552e4de89d5", - "gen_hash_verilog": "sha256:19e519134c8b225290175e4723a340a2ffde8b3e5fd392192b91c6d0c1676248", + "gen_hash_verilog": "sha256:f10591dc966a8ac3535e358d35427cb7b03128d6095bad3d76f51b5ca8514f2f", "gen_hash_zig": "sha256:04869b3b6648bcb78f992e671734c17295760ccce99c82281f6969a55ff4a5b9", "module": "SacredAttention", "ring": 12, - "sealed_at": "2026-08-30T21:18:16Z", + "sealed_at": "2026-09-03T10:13:51Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:9d16a7efd14f69af62f5d85612ed1c99f0caf6dda097f403eda5c4f865fdf125", "spec_path": "specs/nn/attention.t27" diff --git a/.trinity/seals/sync-schema.json b/.trinity/seals/sync-schema.json index f2cde85e63..69fc6f29df 100644 --- a/.trinity/seals/sync-schema.json +++ b/.trinity/seals/sync-schema.json @@ -1,7 +1,7 @@ { "gen_hash_c": "sha256:139f23c2090f084346521cfe837e4ffa8d37f08b3e842d4716fbdb3190091cc0", "gen_hash_rust": "sha256:8738a44fa55571fa8453b760e3e32aa6c79313f43a95f627cb5696bcd58ce19e", - "gen_hash_verilog": "sha256:f7fd8fe95e20ee934f3827616c8a04dda78dab92848cac2ee3a587b54f3c4822", + "gen_hash_verilog": "sha256:0c5b53a94b6fc4cd73c9da958ac20aa162c1490b095e695a63b1c4d236d78085", "gen_hash_zig": "sha256:48dd28714d3e6c9c8218f67b308c5bffcdd7c264da631e7b33d1844c86ca1ccc", "module": "sync-schema", "ring": 12, diff --git a/.trinity/seals/sync_sync-schema.json b/.trinity/seals/sync_sync-schema.json index 07c891c88d..1d02321757 100644 --- a/.trinity/seals/sync_sync-schema.json +++ b/.trinity/seals/sync_sync-schema.json @@ -1,11 +1,11 @@ { "gen_hash_c": "sha256:139f23c2090f084346521cfe837e4ffa8d37f08b3e842d4716fbdb3190091cc0", "gen_hash_rust": "sha256:8738a44fa55571fa8453b760e3e32aa6c79313f43a95f627cb5696bcd58ce19e", - "gen_hash_verilog": "sha256:f7fd8fe95e20ee934f3827616c8a04dda78dab92848cac2ee3a587b54f3c4822", + "gen_hash_verilog": "sha256:0c5b53a94b6fc4cd73c9da958ac20aa162c1490b095e695a63b1c4d236d78085", "gen_hash_zig": "sha256:48dd28714d3e6c9c8218f67b308c5bffcdd7c264da631e7b33d1844c86ca1ccc", "module": "sync-schema", "ring": 12, - "sealed_at": "2026-09-03T07:22:13Z", + "sealed_at": "2026-09-03T10:13:51Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:553424a2c0d86517157fb167bbc0664b24a5073e93e5b86e8a91cefd6cc9b07b", "spec_path": "specs/sync/schema.t27" diff --git a/.trinity/seals/testbench_MAC_Testbench.json b/.trinity/seals/testbench_MAC_Testbench.json index 7e97de2cb0..a28d757571 100644 --- a/.trinity/seals/testbench_MAC_Testbench.json +++ b/.trinity/seals/testbench_MAC_Testbench.json @@ -1,11 +1,11 @@ { "gen_hash_c": "sha256:33cf769212351db3c992bd82921dd80b938755b6da543061eaa4e7c39b552add", "gen_hash_rust": "sha256:e8bc2d3de487ecbc6d2d0c43767dcb87c4fee84004d2dafb973f55e6f8e009d2", - "gen_hash_verilog": "sha256:85a771ac9b82d59dc7ffd9e310e7cbddc5566a0e82357d93d299aa38725b16d6", + "gen_hash_verilog": "sha256:9cab81dcdc6b62afe3b481d26ee1ea47ab2c776af8358624acdb6a0433f29e09", "gen_hash_zig": "sha256:8928e5f9b62c3721e845fdc7acef8d518e8ea0c343572ca3e5a38303b5e1cffb", "module": "MAC_Testbench", "ring": 12, - "sealed_at": "2026-08-30T19:43:01Z", + "sealed_at": "2026-09-03T10:13:51Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:af060b6e5cdb2ffe1de0a47995b2c621139367174cf85237d8217aa929af10f7", "spec_path": "specs/fpga/testbench/mac_tb.t27" diff --git a/.trinity/seals/tritype-base.json b/.trinity/seals/tritype-base.json index 9c9ec3834b..8b8119ce65 100644 --- a/.trinity/seals/tritype-base.json +++ b/.trinity/seals/tritype-base.json @@ -1,7 +1,7 @@ { "gen_hash_c": "sha256:cd2ad734d4c56c11d76f00941351fbb1b7afbe3c31672171d1831d8b6edc92aa", "gen_hash_rust": "sha256:fbcebfa1583296107b8765cc921b3d459d2c90b2e00303f930d243313f4aa20f", - "gen_hash_verilog": "sha256:d8d7c843ee5f2ce5883920ed6fdc87998a332b3a3e481dbdccaeb4ce1dd278c6", + "gen_hash_verilog": "sha256:fb66d07df7fad1fd638df94287b7d44457cac0d32729f93d54e88bee7b902f94", "gen_hash_zig": "sha256:0286c2d52b3521ae781dab3c27e977bdef20c218bca8ff4d81af17cb525a5b71", "module": "tritype-base", "ring": 12, diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 971a7c7ba4..f456eced13 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -10271,6 +10271,53 @@ impl Codegen { // Verilog Code Generator // ============================================================================ +/// #2988: the guard flags ONE loop owns. +/// +/// Verilog has no `break` and no `continue`. The classic translation for a +/// target without a jump is a flag: set it where the jump was, test it +/// everywhere the jump would have skipped. `disable` is the other candidate and +/// this is deliberately NOT it -- `disable ` unwinds a named block, and +/// it cannot express `continue` at all without wrapping every iteration in a +/// block of its own. +/// +/// A flag is only allocated when the body actually contains the statement that +/// sets it, so a loop with no jump emits byte for byte what it emitted before. +/// +/// SCOPE. `return` inside a loop is the OTHER half of this defect (#2989) and +/// is deliberately not here: `__t27_ret` already exists at function scope, it +/// is set correctly today, and what is missing is only that no loop tests it. +/// That repair moves loops which contain no jump at all, so it is measured and +/// landed on its own. +#[derive(Clone, Copy)] +struct LoopGuard { + /// Drawn from `while_fuel_counter`, the same counter W700's `__t27_fuel_N` + /// uses, so two loops in one module never declare the same name. + id: u32, + /// The body contains a `break` that binds to THIS loop. + brk: bool, + /// ... a `continue`. + cnt: bool, +} + +impl LoopGuard { + /// The terms that stop the LOOP. `continue` is not one of them: it ends an + /// iteration, not the loop. + fn loop_cond(&self) -> String { + let mut t: Vec = Vec::new(); + if self.brk { + t.push(format!("!__t27_brk_{}", self.id)); + } + t.join(" && ") + } + + /// True when this loop needs a `reg` of its own -- and therefore a NAMED + /// block to declare it in, because Verilog-2001 allows a declaration only + /// at the top of one. + fn needs_block(&self) -> bool { + self.brk || self.cnt + } +} + pub struct VerilogCodegen { output: String, indent: u32, @@ -10279,6 +10326,10 @@ pub struct VerilogCodegen { /// Verilog rejects the redeclaration -- turning a synthesis repair into a /// compile failure. while_fuel_counter: u32, + /// #2988: the enclosing loops that own a guard flag, innermost LAST. + /// A `break` needs to know which loop it binds to, and nothing else in this + /// emitter carries that; the stack is the only new state the repair adds. + loop_guards: Vec, module_name: String, current_fn_name: String, current_fn_return_type: String, @@ -10382,6 +10433,7 @@ impl VerilogCodegen { pub fn with_options(emit_test_assertions: bool) -> Self { Self { while_fuel_counter: 0, + loop_guards: Vec::new(), output: String::new(), indent: 0, module_name: String::new(), @@ -12116,6 +12168,7 @@ impl VerilogCodegen { output: String::new(), indent: 0, while_fuel_counter: 0, + loop_guards: Vec::new(), module_name: String::new(), current_fn_name: String::new(), current_fn_return_type: String::new(), @@ -12333,6 +12386,7 @@ impl VerilogCodegen { for sub in outer.iter().take(current_dim) { let mut tmp = VerilogCodegen { while_fuel_counter: 0, + loop_guards: Vec::new(), output: String::new(), indent: 0, module_name: String::new(), @@ -14992,6 +15046,136 @@ impl VerilogCodegen { node.children.iter().any(Self::subtree_has_return) } + /// #2988: does this subtree contain a `break`/`continue` that binds to the + /// ENCLOSING loop? A nested loop captures its own, so the scan STOPS at + /// one -- otherwise an inner `break` would set the outer loop's flag and + /// the repair would be a second wrong answer. + fn subtree_has_loop_exit(node: &Node, kind: &NodeKind) -> bool { + if node.kind == *kind { + return true; + } + if matches!( + node.kind, + NodeKind::StmtWhile | NodeKind::StmtFor | NodeKind::StmtForRange + ) { + return false; + } + node.children + .iter() + .any(|c| Self::subtree_has_loop_exit(c, kind)) + } + + fn stmts_have_loop_exit(stmts: &[Node], kind: &NodeKind) -> bool { + stmts.iter().any(|s| Self::subtree_has_loop_exit(s, kind)) + } + + /// #2988: decide which flags this loop body needs, allocate an id if any, + /// and push the guard. The CALLER emits the declarations, because only the + /// caller knows where its named block starts, and the caller must call + /// `pop_loop_guard` after the body. + /// `preset_id` is for the W700 branch, which has ALREADY drawn an id for + /// its `__t27_loop_N` / `__t27_fuel_N` pair. Drawing a second one there + /// would renumber every later loop in the module and churn generated files + /// that contain no jump at all. + fn push_loop_guard(&mut self, body: &[Node], preset_id: Option) -> LoopGuard { + // A clocked (`<=`) body cannot carry a blocking control flag -- a + // nonblocking write does not settle inside the time step that reads it. + // The early-return machinery refuses there for exactly this reason (see + // `__t27_ret`), so this refuses too, and the `break` arm SAYS SO in the + // output instead of emitting something that looks like a lowering. + let live = !self.clocked_nonblocking; + let brk = live && Self::stmts_have_loop_exit(body, &NodeKind::StmtBreak); + let cnt = live && Self::stmts_have_loop_exit(body, &NodeKind::StmtContinue); + let mut g = LoopGuard { id: 0, brk, cnt }; + if let Some(id) = preset_id { + g.id = id; + } else if g.needs_block() { + g.id = self.while_fuel_counter; + self.while_fuel_counter += 1; + } + self.loop_guards.push(g); + g + } + + fn pop_loop_guard(&mut self) { + self.loop_guards.pop(); + } + + /// Emit this loop's `reg` declarations and the one initialisation that + /// belongs OUTSIDE the loop. Legal only at the top of a named block. + fn emit_loop_guard_decls(&mut self, g: LoopGuard) { + // Every declaration FIRST. Verilog forbids a declaration after a + // procedural statement even inside a named block (#1741), so the + // `= 1'b0` cannot be interleaved with the `reg`s. + if g.brk { + self.write_indent(); + self.write_line(&format!("reg __t27_brk_{};", g.id)); + } + if g.cnt { + self.write_indent(); + self.write_line(&format!("reg __t27_cnt_{};", g.id)); + } + if g.brk { + self.write_indent(); + self.write_line(&format!("__t27_brk_{} = 1'b0;", g.id)); + } + // `__t27_cnt_N` is cleared at the TOP of each iteration instead: that + // is what makes it per-iteration and `__t27_brk_N` persistent. + } + + /// Emit `__t27_cnt_N = 1'b0;` as the first statement of an iteration. + fn emit_continue_reset(&mut self, g: LoopGuard) { + if g.cnt { + self.write_indent(); + self.write_line(&format!("__t27_cnt_{} = 1'b0;", g.id)); + } + } + + /// The condition under which the REST of this iteration still runs, given + /// that `stmt` has just been emitted. Empty when `stmt` cannot jump, which + /// is why a loop body with no jump is unchanged. + fn iteration_guard_cond(&self, stmt: &Node) -> String { + let Some(g) = self.loop_guards.last() else { + return String::new(); + }; + let mut t: Vec = Vec::new(); + if g.brk && Self::subtree_has_loop_exit(stmt, &NodeKind::StmtBreak) { + t.push(format!("!__t27_brk_{}", g.id)); + } + if g.cnt && Self::subtree_has_loop_exit(stmt, &NodeKind::StmtContinue) { + t.push(format!("!__t27_cnt_{}", g.id)); + } + t.join(" && ") + } + + /// Emit a statement sequence that lives inside a loop. After a statement + /// that can jump, the remainder of the iteration is wrapped in the guard -- + /// the same barrier `gen_verilog_fn_body` already builds for `__t27_ret`, + /// extended to the two flags a loop owns. With no guard in scope this is + /// exactly the `for stmt in .. { gen_verilog_stmt }` it replaces, character + /// for character, which is what keeps the corpus delta down to the loops + /// that actually jump. + fn gen_verilog_stmt_seq(&mut self, stmts: &[Node]) { + for (idx, stmt) in stmts.iter().enumerate() { + self.gen_verilog_stmt(stmt); + if idx + 1 >= stmts.len() { + continue; + } + let cond = self.iteration_guard_cond(stmt); + if cond.is_empty() { + continue; + } + self.write_indent(); + self.write_line(&format!("if ({cond}) begin")); + self.indent(); + self.gen_verilog_stmt_seq(&stmts[idx + 1..]); + self.dedent(); + self.write_indent(); + self.write_line("end"); + return; + } + } + // W551: hoist probe registers and block-local variable declarations for // any simulation block (test or deterministic bench) that contains // assert_eq statements. Returns the names of block-local variables whose @@ -16294,10 +16478,50 @@ impl VerilogCodegen { self.gen_verilog_for_range_stmt(node); } NodeKind::StmtBreak => { - self.write_line("disable fork;"); + // t27#2988: this emitted `disable fork;`. `disable fork` kills + // processes spawned by a `fork` in the CURRENT SCOPE, and the + // token `fork` occurs nowhere in the 581 generated .v files + // except inside that very line -- so all fourteen sites were + // no-ops, the loop ran to completion, and later iterations + // overwrote whatever the break was meant to preserve. + // `iverilog -g2012` accepts it silently. + // + // It was also emitted at COLUMN 0: `write_line` without + // `write_indent`. Both are fixed here. + let g = self.loop_guards.last().copied(); + match g { + Some(g) if g.brk => { + self.write_indent(); + self.write_line(&format!("__t27_brk_{} = 1'b1;", g.id)); + } + _ => { + // No flag in scope: a clocked (`<=`) body, where a + // blocking guard would not settle. Say it, do not fake + // it -- a comment that names the issue is findable; a + // `disable fork;` that reads like a lowering is not. + self.write_indent(); + self.write_line( + "// t27#2988: `break` NOT LOWERED -- no guard flag in this scope.", + ); + } + } } NodeKind::StmtContinue => { - self.write_line("/* continue */;"); + // t27#2988: this emitted `/* continue */;` -- a comment and an + // empty statement. Same repair, same reason. + let g = self.loop_guards.last().copied(); + match g { + Some(g) if g.cnt => { + self.write_indent(); + self.write_line(&format!("__t27_cnt_{} = 1'b1;", g.id)); + } + _ => { + self.write_indent(); + self.write_line( + "// t27#2988: `continue` NOT LOWERED -- no guard flag in this scope.", + ); + } + } } NodeKind::StmtExpr => { self.write_indent(); @@ -16324,9 +16548,7 @@ impl VerilogCodegen { self.indent(); if node.children.len() > 1 { - for stmt in &node.children[1].children { - self.gen_verilog_stmt(stmt); - } + self.gen_verilog_stmt_seq(&node.children[1].children); } self.dedent(); @@ -16341,9 +16563,7 @@ impl VerilogCodegen { self.write_indent(); self.write_line("end else begin"); self.indent(); - for stmt in &else_block.children { - self.gen_verilog_stmt(stmt); - } + self.gen_verilog_stmt_seq(&else_block.children); self.dedent(); self.write_indent(); self.write_line("end"); @@ -16363,9 +16583,7 @@ impl VerilogCodegen { self.indent(); if node.children.len() > 1 { - for stmt in &node.children[1].children { - self.gen_verilog_stmt(stmt); - } + self.gen_verilog_stmt_seq(&node.children[1].children); } self.dedent(); @@ -16379,9 +16597,7 @@ impl VerilogCodegen { self.write_indent(); self.write_line("end else begin"); self.indent(); - for stmt in &else_block.children { - self.gen_verilog_stmt(stmt); - } + self.gen_verilog_stmt_seq(&else_block.children); self.dedent(); self.write_indent(); self.write_line("end"); @@ -16454,30 +16670,57 @@ impl VerilogCodegen { // yosys: Left hand side of 1st expression of procedural // for-loop is not a register! // Louder, and still broken. The named block fixes both. + // t27#2988: this loop's guard flags. The trip count MUST stay + // static -- that is the whole point of W700 -- so the guard goes on + // the inner `if`, never on the `for` header. + let body: &[Node] = if node.children.len() > 1 { + &node.children[node.children.len() - 1].children + } else { + &[] + }; + let g = self.push_loop_guard(body, Some(n)); self.write_indent(); self.write_line(&format!("begin : {blk}")); self.indent(); self.write_indent(); self.write_line(&format!("integer {fuel};")); + self.emit_loop_guard_decls(g); self.write_indent(); self.write_line(&format!("for ({fuel} = 0; {fuel} < {bound}; {fuel} = {fuel} + 1) begin")); self.indent(); self.write_indent(); self.write("if ("); + let lc = g.loop_cond(); + if !lc.is_empty() { + self.write(&format!("{lc} && ")); + } if !node.children.is_empty() { self.gen_verilog_expr(&node.children[0]); } self.write_line(") begin"); self.indent(); if node.children.len() > 1 { - for stmt in &node.children[node.children.len() - 1].children { - self.gen_verilog_stmt(stmt); - } + self.emit_continue_reset(g); + self.gen_verilog_stmt_seq(body); // W-27: body is the LAST child; a `while (c) : (step)` puts the step at [1]. + // t27#2988: the step runs on `continue` (Zig semantics), so it + // sits OUTSIDE the iteration barrier and is guarded only by the + // flags that stop the loop. if node.children.len() > 2 { - self.gen_verilog_stmt(unwrap_single(&node.children[1])); + if lc.is_empty() { + self.gen_verilog_stmt(unwrap_single(&node.children[1])); + } else { + self.write_indent(); + self.write_line(&format!("if ({lc}) begin")); + self.indent(); + self.gen_verilog_stmt(unwrap_single(&node.children[1])); + self.dedent(); + self.write_indent(); + self.write_line("end"); + } } } + self.pop_loop_guard(); self.dedent(); self.write_indent(); self.write_line("end"); @@ -16536,8 +16779,32 @@ impl VerilogCodegen { ); self.write_indent(); self.write_line("// on data is sequential; naming it combinational is the error."); + // t27#2988: this loop has NO static trip count, so a `break` guard on + // the body would stop the writes and not the spinning -- the flag has to + // be in the CONDITION or the loop never ends. That is also why `disable` + // cannot serve here: there is nothing to disable until a block exists, + // and a block per iteration is exactly the cost this shape avoids. + // + // The same argument applies to a `return` inside this loop and is NOT + // acted on here; see #2989 and the note on `LoopGuard`. + let body: &[Node] = if node.children.len() > 1 { + &node.children[node.children.len() - 1].children + } else { + &[] + }; + let g = self.push_loop_guard(body, None); + if g.needs_block() { + self.write_indent(); + self.write_line(&format!("begin : __t27_loop_{}", g.id)); + self.indent(); + self.emit_loop_guard_decls(g); + } self.write_indent(); self.write("while ("); + let lc = g.loop_cond(); + if !lc.is_empty() { + self.write(&format!("{lc} && ")); + } if !node.children.is_empty() { self.gen_verilog_expr(&node.children[0]); } @@ -16545,22 +16812,53 @@ impl VerilogCodegen { self.indent(); if node.children.len() > 1 { - for stmt in &node.children[node.children.len() - 1].children { - self.gen_verilog_stmt(stmt); - } + self.emit_continue_reset(g); + self.gen_verilog_stmt_seq(body); // W-27: body is the LAST child; a `while (c) : (step)` puts the step at [1]. if node.children.len() > 2 { - self.gen_verilog_stmt(unwrap_single(&node.children[1])); + if lc.is_empty() { + self.gen_verilog_stmt(unwrap_single(&node.children[1])); + } else { + self.write_indent(); + self.write_line(&format!("if ({lc}) begin")); + self.indent(); + self.gen_verilog_stmt(unwrap_single(&node.children[1])); + self.dedent(); + self.write_indent(); + self.write_line("end"); + } } } + self.pop_loop_guard(); self.dedent(); self.write_indent(); self.write_line("end"); + if g.needs_block() { + self.dedent(); + self.write_indent(); + self.write_line("end"); + } } fn gen_verilog_for_stmt(&mut self, node: &Node) { // Emit as integer for loop: for (i = 0; i < N; i = i + 1) let body_idx = node.children.len().saturating_sub(1); + // t27#2988: allocate this loop's guard flags before anything is + // written, because a `reg` may only be declared at the top of a NAMED + // block and that block has to open first. + let body: &[Node] = if node.children.is_empty() { + &[] + } else { + &node.children[body_idx].children + }; + let g = self.push_loop_guard(body, None); + let lc = g.loop_cond(); + if g.needs_block() { + self.write_indent(); + self.write_line(&format!("begin : __t27_loop_{}", g.id)); + self.indent(); + self.emit_loop_guard_decls(g); + } // Use capture variable name if available, else default to __i let iter_var_raw = if !node.params.is_empty() { @@ -16617,21 +16915,49 @@ impl VerilogCodegen { } else { self.write(&format!("for ({0} = 0; {0} < 1; {0} = {0} + 1)", iter_var)); } - self.write_line(" begin"); + // t27#2988: the guard rides the loop BODY, not the `for` header. A + // data-dependent header would cost the static trip count yosys needs to + // unroll this; `for (..) if (guard) begin` keeps it and adds no nesting + // level, so the body's indentation -- and its bytes -- do not move. + if lc.is_empty() { + self.write_line(" begin"); + } else { + self.write_line(&format!(" if ({lc}) begin")); + } self.indent(); if !node.children.is_empty() { - for stmt in &node.children[body_idx].children { - self.gen_verilog_stmt(stmt); - } + self.emit_continue_reset(g); + self.gen_verilog_stmt_seq(&node.children[body_idx].children); } + self.pop_loop_guard(); self.dedent(); self.write_indent(); self.write_line("end"); + if g.needs_block() { + self.dedent(); + self.write_indent(); + self.write_line("end"); + } } fn gen_verilog_for_range_stmt(&mut self, node: &Node) { let var = Self::verilog_safe_identifier(&node.name); + // t27#2988: same shape as gen_verilog_for_stmt -- guard the body, + // keep the header's trip count static. + let body: &[Node] = if node.children.len() > 2 { + &node.children[2].children + } else { + &[] + }; + let g = self.push_loop_guard(body, None); + let lc = g.loop_cond(); + if g.needs_block() { + self.write_indent(); + self.write_line(&format!("begin : __t27_loop_{}", g.id)); + self.indent(); + self.emit_loop_guard_decls(g); + } self.write_indent(); if node.children.len() >= 2 { self.write(&format!("for ({var} = ")); @@ -16640,16 +16966,25 @@ impl VerilogCodegen { self.gen_verilog_expr(&node.children[1]); self.write(&format!("; {var} = {var} + 1)")); } - self.write_line(" begin"); + if lc.is_empty() { + self.write_line(" begin"); + } else { + self.write_line(&format!(" if ({lc}) begin")); + } self.indent(); if node.children.len() > 2 { - for stmt in &node.children[2].children { - self.gen_verilog_stmt(stmt); - } + self.emit_continue_reset(g); + self.gen_verilog_stmt_seq(body); } + self.pop_loop_guard(); self.dedent(); self.write_indent(); self.write_line("end"); + if g.needs_block() { + self.dedent(); + self.write_indent(); + self.write_line("end"); + } } fn gen_verilog_expr(&mut self, node: &Node) { @@ -40399,6 +40734,211 @@ fn read_it() -> u16 { assert_eq!(super::Codegen::sized_int_type("[]i8"), None); } + // ---- t27#2988: the loop-guard lowering ------------------------------- + // + // Every assertion below FAILS on the emitter these tests were written + // against: `break` was `disable fork;` (a no-op -- the corpus has no + // `fork`) and `continue` was `/* continue */;`. + + #[test] + fn break_lowers_to_a_guard_flag_not_disable_fork() { + let src = r#"module M { + pub fn f(n: i64) -> i64 { + var i: i64 = 0; + var out: i64 = -1; + while (i < n) { + if (i > 2) { out = i; break; } + out = -2; + i = i + 1; + } + return out; + } + }"#; + let v = Compiler::compile_verilog(src).expect("compile should succeed"); + assert!( + !v.contains("disable fork"), + "`disable fork` is a no-op without a `fork`; it must be gone:\n{}", + v + ); + assert!( + v.contains("reg __t27_brk_0;") && v.contains("__t27_brk_0 = 1'b0;"), + "the loop must declare and clear its guard flag:\n{}", + v + ); + assert!( + v.contains("__t27_brk_0 = 1'b1;"), + "`break` must SET the flag:\n{}", + v + ); + // Stopping the loop, and stopping the rest of the iteration, are two + // different things and both are required: without the second, `out` + // is overwritten by the statement that follows the break. + assert!( + v.contains("while (!__t27_brk_0 &&"), + "the flag must stop the LOOP, in the condition:\n{}", + v + ); + assert!( + v.contains("if (!__t27_brk_0) begin"), + "the flag must stop the REST OF THE ITERATION:\n{}", + v + ); + } + + #[test] + fn continue_lowers_to_a_per_iteration_flag() { + let src = r#"module M { + pub fn f(n: i64) -> i64 { + var i: i64 = 0; + var acc: i64 = 0; + while (i < n) { + i = i + 1; + if (i == 3) { continue; } + acc = acc + i; + } + return acc; + } + }"#; + let v = Compiler::compile_verilog(src).expect("compile should succeed"); + assert!( + !v.contains("/* continue */"), + "a comment plus an empty statement is not a lowering:\n{}", + v + ); + assert!( + v.contains("__t27_cnt_0 = 1'b1;") && v.contains("if (!__t27_cnt_0) begin"), + "`continue` must set a flag the iteration tail tests:\n{}", + v + ); + // The continue flag is per-ITERATION -- it must be cleared at the top + // of the body, or the first `continue` skips every later iteration too. + // Written as ONE assertion on purpose: the first draft said + // `contains() || contains()`, and the first + // disjunct implies the second, so the `||` tested nothing. + assert!( + v.contains("__t27_cnt_0 = 1'b0;"), + "the continue flag must be cleared each iteration:\n{}", + v + ); + // A `continue` must NOT stop the loop. + assert!( + !v.contains("while (!__t27_cnt_0"), + "`continue` ends an iteration, not the loop:\n{}", + v + ); + } + + #[test] + fn a_for_loop_keeps_its_static_trip_count() { + // The guard rides the BODY of a `for`, never the header: a + // data-dependent header costs the static trip count yosys needs to + // unroll it, and W702 already documents what yosys does then. + let src = r#"module M { + pub fn f(n: usize) -> usize { + var out: usize = 9; + for i in 0..8 { + if (i >= n) { out = i; break; } + } + return out; + } + }"#; + let v = Compiler::compile_verilog(src).expect("compile should succeed"); + assert!( + v.contains("for (i = 0; i < 8; i = i + 1) if (!__t27_brk_0) begin"), + "the guard belongs on the body, not in the `for` header:\n{}", + v + ); + } + + #[test] + fn a_break_binds_to_the_INNERMOST_loop_only() { + // The scan for a jump STOPS at a nested loop. Without that stop the + // outer loop allocates a flag its body can never set: the emission is + // still correct, so no runtime probe can see it, and the only witness + // is the declaration that should not be there. This is also what keeps + // the corpus delta to the loops that actually jump. + let src = r#"module M { + pub fn f(n: i64) -> i64 { + var acc: i64 = 0; + var i: i64 = 0; + while (i < n) { + var j: i64 = 0; + while (j < n) { + j = j + 1; + if (j > 2) { break; } + acc = acc + 1; + } + i = i + 1; + } + return acc; + } + }"#; + let v = Compiler::compile_verilog(src).expect("compile should succeed"); + assert_eq!( + v.matches("reg __t27_brk_").count(), + 1, + "only the INNER loop owns a break flag; the outer must allocate \ + nothing:\n{}", + v + ); + // ... and the flag the `break` SETS is the flag that was DECLARED. + // Counting declarations does not say that: a mutant binding `break` to + // the OUTERMOST guard instead of the innermost still declares exactly + // one flag, sets nothing, and emits the "no guard flag in this scope" + // refusal for a scope that has one. It survived until this assertion. + let ids = |marker: &str| -> Vec { + v.match_indices(marker) + .map(|(i, m)| { + v[i + m.len()..] + .chars() + .take_while(char::is_ascii_digit) + .collect() + }) + .collect() + }; + assert_eq!( + ids("reg __t27_brk_"), + ids("__t27_brk_") + .into_iter() + .filter(|id| v.contains(&format!("__t27_brk_{id} = 1'b1;"))) + .take(1) + .collect::>(), + "the flag the `break` sets must be the flag the loop declared:\n{}", + v + ); + assert!( + !v.contains("NOT LOWERED"), + "a `break` with a guard in scope must not report that it has none:\n{}", + v + ); + } + + #[test] + fn a_loop_without_a_jump_is_unchanged() { + // The population that must NOT move: the corpus delta is 37 of 581 + // generated .v files precisely because a loop with no `break`, no + // `continue` and no `return` allocates nothing and tests nothing. + let src = r#"module M { + pub fn f(n: i64) -> i64 { + var i: i64 = 0; + var acc: i64 = 0; + while (i < n) { acc = acc + i; i = i + 1; } + return acc; + } + }"#; + let v = Compiler::compile_verilog(src).expect("compile should succeed"); + assert!( + !v.contains("__t27_brk_") && !v.contains("__t27_cnt_"), + "a jump-free loop must allocate no guard:\n{}", + v + ); + assert!( + v.contains("while ((i < n)) begin") || v.contains("while (($signed(i) < $signed(n))) begin"), + "a jump-free loop's condition must be untouched:\n{}", + v + ); + } + #[test] fn float_param_emits_real() { let src = r#"module M { pub const X : f32 = 0.1; }"#; diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index a3cbe200c6..378eb28b67 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -1b52250fc530d28943107efc7289eba769f8e113c10ce7b3b0603ca1564153a4 \ No newline at end of file +2a95b1f7335731bcdfcaf768e476fa3066d66327fe07804fd1b7faf3d8963b90 \ No newline at end of file diff --git a/cli/tri/src/jumps.rs b/cli/tri/src/jumps.rs new file mode 100644 index 0000000000..cc91f6fc2a --- /dev/null +++ b/cli/tri/src/jumps.rs @@ -0,0 +1,324 @@ +//! `tri jumps census` -- what happened to every `break` and `continue` the +//! Verilog emitter had to lower? +//! +//! WHY THIS EXISTS +//! --------------- +//! `break` was emitted as `disable fork;` and `continue` as `/* continue */;`. +//! Both are legal Verilog. `iverilog -g2012` accepts them, `yosys` accepts +//! them, the seal hashes are stable over them, and every one of them is a +//! NO-OP: `disable fork` kills processes spawned by a `fork` in the current +//! scope, and the token `fork` occurs nowhere in the generated corpus except +//! inside that very line. Sixteen `break`s and one `continue` ran off the end +//! of their loops for as long as the emitter has existed, and no instrument in +//! this repository could say so, because every instrument asked whether the +//! output PARSES. +//! +//! So this one asks a different question: for each jump the source wrote, what +//! did the backend put there, and does it do anything? +//! +//! WHAT IT CHECKS THAT A COUNT CANNOT +//! ---------------------------------- +//! A guard flag is two halves -- a `reg` the loop declares and an assignment +//! the `break` writes. A lowering that declares the flag and binds the jump to +//! the WRONG loop still declares exactly one flag per loop that needs one, so +//! any count of declarations is satisfied. It is the PAIRING that fails. That +//! mutant survived a five-test suite until the pairing was asserted, so the +//! pairing is asserted here too, over the whole corpus. +//! +//! WHAT IT REFUSES +//! --------------- +//! With no compiler it says so and exits non-zero. "0 unlowered jumps" taken +//! with no binary is a statement about this machine and reads exactly like +//! health. +//! +//! It reports and never gates. See #2988; `return` inside a loop is #2989 and +//! is a different question with a different answer. + +use anyhow::{bail, Context, Result}; +use clap::Subcommand; +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// The dead lowerings, and what each one was written as. +const NO_OPS: &[(&str, &str)] = &[("disable fork;", "break"), ("/* continue */;", "continue")]; + +#[derive(Subcommand)] +pub enum JumpsCmd { + /// Name every `break`/`continue` site in the generated Verilog and say + /// what it lowered to. + Census { + /// Stop after this many specs. The population line still names the + /// whole tree, so a truncated run cannot read as a complete one. + #[arg(long)] + limit: Option, + }, +} + +pub fn run(cmd: &JumpsCmd) -> Result<()> { + match cmd { + JumpsCmd::Census { limit } => census(*limit), + } +} + +/// The ids in `` across a file, in order of appearance. +fn ids(text: &str, prefix: &str) -> Vec { + text.match_indices(prefix) + .map(|(i, m)| { + text[i + m.len()..] + .chars() + .take_while(|c| c.is_ascii_digit()) + .collect() + }) + .filter(|s: &String| !s.is_empty()) + .collect() +} + +fn census(limit: Option) -> Result<()> { + let root = repo_root()?; + let t27c = root.join("target/release/t27c"); + if !t27c.exists() { + bail!( + "no compiler at {}\n \ + Build it first: cargo build --release -p t27c\n \ + A jump census taken with no binary reports zero unlowered jumps,\n \ + and that reads exactly like health.", + t27c.display() + ); + } + + // Every `.t27` in the tree, not just `specs/`. Two of the seventeen sites + // this was built for live in `compiler/cli/gen.t27`, and a census scoped to + // `specs/` reported fifteen -- twice, in two different sessions. + let mut specs = Vec::new(); + collect_specs(&root, &root, &mut specs); + specs.sort(); + let population = specs.len(); + let walked: Vec<&String> = match limit { + Some(n) => specs.iter().take(n).collect(), + None => specs.iter().collect(), + }; + + let mut generated = 0usize; + let mut dead: Vec<(String, String, usize)> = Vec::new(); + let mut refused: Vec<(String, usize)> = Vec::new(); + // Two different quantities, and conflating them is how a report says 14 + // where the source wrote 17: several `break`s in one loop share ONE flag. + let mut flags: Vec<(String, usize)> = Vec::new(); + let mut jumps: Vec<(String, usize)> = Vec::new(); + let mut unpaired: Vec<(String, String)> = Vec::new(); + + for spec in &walked { + let out = Command::new(&t27c) + .arg("gen-verilog") + .arg(root.join(spec)) + .current_dir(&root) + .output(); + let v = match out { + Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).into_owned(), + _ => continue, + }; + generated += 1; + + for (marker, what) in NO_OPS { + let n = v.matches(marker).count(); + if n > 0 { + dead.push(((*spec).clone(), (*what).to_string(), n)); + } + } + // Only this crate's own marker. The emitter already prints an + // unrelated `// NOT LOWERED BY THIS BACKEND` in 535 files, and a grep + // for the shorter phrase reports every one of them as a refusal. + let n = v.matches("t27#2988:").count(); + if n > 0 { + refused.push(((*spec).clone(), n)); + } + for (decl, set) in [ + ("reg __t27_brk_", "__t27_brk_"), + ("reg __t27_cnt_", "__t27_cnt_"), + ] { + let declared: BTreeSet = ids(&v, decl).into_iter().collect(); + if declared.is_empty() { + continue; + } + flags.push(((*spec).clone(), declared.len())); + let written: BTreeSet = ids(&v, set) + .into_iter() + .filter(|id| v.contains(&format!("{set}{id} = 1'b1;"))) + .collect(); + // The JUMPS: one per `= 1'b1;` assignment, which is one per + // `break`/`continue` the source wrote. + let n = declared + .iter() + .map(|id| v.matches(&format!("{set}{id} = 1'b1;")).count()) + .sum::(); + if n > 0 { + jumps.push(((*spec).clone(), n)); + } + for id in declared.difference(&written) { + unpaired.push(((*spec).clone(), format!("{set}{id}"))); + } + } + } + + println!(); + println!(" .t27 in the tree {population}"); + if walked.len() != population { + println!(" WALKED THIS RUN {} (--limit)", walked.len()); + } + println!(" generated Verilog {generated}"); + println!(); + println!( + " jumps lowered {:>3} site(s) in {} file(s)", + jumps.iter().map(|(_, n)| n).sum::(), + jumps.len() + ); + println!( + " ... sharing {:>3} guard flag(s)", + flags.iter().map(|(_, n)| n).sum::() + ); + println!( + " jumps left as a NO-OP {:>3} site(s) in {} file(s)", + dead.iter().map(|(_, _, n)| n).sum::(), + dead.len() + ); + for (s, what, n) in &dead { + println!(" {s} {n} x {what}"); + } + println!( + " jumps REFUSED, and said {:>3} site(s) in {} file(s)", + refused.iter().map(|(_, n)| n).sum::(), + refused.len() + ); + for (s, n) in &refused { + println!(" {s} {n}"); + } + println!(" flags declared, never set {:>2}", unpaired.len()); + for (s, f) in &unpaired { + println!(" {s} {f}"); + } + println!(); + if dead.is_empty() && unpaired.is_empty() { + println!( + " Every jump the source wrote reached a flag, and every flag a loop\n \ + declared is written somewhere. That is a result and not a silence:\n \ + {generated} file(s) were generated and read in full." + ); + } else { + println!( + " A NO-OP above is a `break` or `continue` the hardware never takes;\n \ + a flag declared and never set is a jump bound to the wrong loop.\n \ + Both parse. See #2988. This command reports; it does not gate." + ); + } + println!(); + Ok(()) +} + +fn collect_specs(dir: &Path, root: &Path, out: &mut Vec) { + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for e in entries.flatten() { + let p = e.path(); + let name = e.file_name(); + let name = name.to_string_lossy(); + if p.is_dir() { + if name == "target" || name == ".git" || name == "node_modules" { + continue; + } + collect_specs(&p, root, out); + } else if p.extension().and_then(|s| s.to_str()) == Some("t27") { + if let Ok(rel) = p.strip_prefix(root) { + out.push(rel.to_string_lossy().into_owned()); + } + } + } +} + +fn repo_root() -> Result { + let out = Command::new("git") + .args(["rev-parse", "--show-toplevel"]) + .output() + .context("running `git rev-parse --show-toplevel`")?; + if !out.status.success() { + bail!("not inside a git repository"); + } + Ok(PathBuf::from( + String::from_utf8_lossy(&out.stdout).trim().to_string(), + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_declared_flag_that_is_never_set_is_visible_to_the_pairing() { + // The mutant that survived a five-test suite: the loop declares the + // flag, the `break` binds elsewhere, nothing writes it. + let v = "reg __t27_brk_0;\n__t27_brk_0 = 1'b0;\n"; + let declared: BTreeSet = ids(v, "reg __t27_brk_").into_iter().collect(); + let written: BTreeSet = ids(v, "__t27_brk_") + .into_iter() + .filter(|id| v.contains(&format!("__t27_brk_{id} = 1'b1;"))) + .collect(); + assert_eq!(declared.len(), 1); + assert!(written.is_empty(), "the clear is not the set"); + assert_eq!(declared.difference(&written).count(), 1); + } + + #[test] + fn a_flag_that_is_declared_and_set_pairs() { + let v = "reg __t27_brk_3;\n__t27_brk_3 = 1'b0;\n__t27_brk_3 = 1'b1;\n"; + let declared: BTreeSet = ids(v, "reg __t27_brk_").into_iter().collect(); + let written: BTreeSet = ids(v, "__t27_brk_") + .into_iter() + .filter(|id| v.contains(&format!("__t27_brk_{id} = 1'b1;"))) + .collect(); + assert_eq!(declared, written); + } + + #[test] + fn the_two_dead_lowerings_are_both_watched() { + // A no-op missing from this list is a no-op this census silently does + // not count -- the shape `tri mods orphan` had when it watched two + // crates of five. + for m in ["disable fork;", "/* continue */;"] { + assert!( + NO_OPS.iter().any(|(marker, _)| *marker == m), + "`{m}` is not watched" + ); + } + } + + #[test] + fn the_refusal_marker_is_not_the_backends_own_phrase() { + // 535 generated files carry `// NOT LOWERED BY THIS BACKEND` for an + // unrelated reason. A census keyed on "NOT LOWERED" reports all 535 as + // refusals; this one is keyed on the issue number. + let unrelated = " // NOT LOWERED BY THIS BACKEND\n"; + assert_eq!(unrelated.matches("t27#2988:").count(), 0); + let mine = " // t27#2988: `break` NOT LOWERED -- no guard flag in this scope.\n"; + assert_eq!(mine.matches("t27#2988:").count(), 1); + } + + #[test] + fn ids_reads_the_number_and_stops() { + assert_eq!(ids("reg __t27_brk_12;", "reg __t27_brk_"), vec!["12"]); + assert_eq!(ids("reg __t27_brk_;", "reg __t27_brk_").len(), 0); + } + + #[test] + fn a_census_with_no_compiler_refuses() { + // Reachable only when `target/release/t27c` is absent; when it is + // present the walk runs and is equally correct. Either way this must + // never print a clean table without having generated anything. + if let Err(e) = census(Some(0)) { + assert!( + e.to_string().contains("no compiler at"), + "the refusal must name what is missing: {e}" + ); + } + } +} diff --git a/cli/tri/src/main.rs b/cli/tri/src/main.rs index 5d447333fc..099897ac17 100644 --- a/cli/tri/src/main.rs +++ b/cli/tri/src/main.rs @@ -41,6 +41,7 @@ mod sweep; mod types_dup; mod vectors; mod gendet; +mod jumps; mod vsim; mod synth; @@ -265,6 +266,17 @@ enum Commands { #[command(subcommand)] action: gendet::EmitCmd, }, + /// What happened to every `break` and `continue` the Verilog emitter had + /// to lower? + /// + /// `break` was `disable fork;` and `continue` was `/* continue */;`. Both + /// parse, both synthesise, both are no-ops -- so every instrument that + /// asks whether the output PARSES said yes for as long as the emitter has + /// existed. See #2988. + Jumps { + #[command(subcommand)] + action: jumps::JumpsCmd, + }, /// How far each spec gets when its generated Verilog is actually RUN. /// /// The one arm that can catch a defect whose nature is that it compiles, @@ -929,6 +941,7 @@ fn main() -> Result<()> { Commands::Issues { action } => issues::run(action)?, Commands::Unparsed { action } => unparsed::run(action, std::env::current_dir()?)?, Commands::Emit { action } => gendet::run(action)?, + Commands::Jumps { action } => jumps::run(action)?, Commands::Vsim { action } => vsim::run(action)?, Commands::Seals { action } => seals::run(action)?, Commands::Hooks { action } => hooks::run(action)?, diff --git a/docs/now/2026-09-03-break-was-disable-fork-and-the-corpus-has-no-fork.md b/docs/now/2026-09-03-break-was-disable-fork-and-the-corpus-has-no-fork.md new file mode 100644 index 0000000000..9300bb94d2 --- /dev/null +++ b/docs/now/2026-09-03-break-was-disable-fork-and-the-corpus-has-no-fork.md @@ -0,0 +1,32 @@ +# NOW -- `break` was `disable fork;`, and the corpus has no `fork` (2026-09-03) + +## Sixteen `break`s and one `continue` were no-ops, and every ruler said fine (Closes #2988) + +- the Verilog emitter wrote `disable fork;` for `break` and `/* continue */;` for `continue`; `disable fork` kills processes spawned by a `fork` in the current scope, and the token `fork` occurs nowhere in the generated corpus except inside that very line +- **17 sites in 8 `.t27` files** -- 16 `break`, 1 `continue`. Two of them are in `compiler/cli/gen.t27`, outside `specs/`; a census scoped to `specs/` reports 15 in 7, which is what I published twice +- both lowerings PARSE. `iverilog -g2012` accepts them, `yosys` accepts them, the seal hashes are stable over them. Every instrument this repository owns asked whether the output parses, so the defect was invisible for as long as the emitter has existed + +## What is measured + +- a probe with 8 declared tests, whose expected values are confirmed by the **Zig** backend (8/8) -- an oracle independent of the Verilog emitter. Under `icarus-simulate`: **master 1 of 8**, after **8 of 8**. The one master passes is the jump-free control +- **581 of 650** specs generate Verilog, before and after +- **7 of 581** generated files differ +- `yosys read_verilog -sv -DSIMULATION` + `hierarchy`, run on those 7: **+2** (`specs/ar/asp_solver`, `specs/compiler/lexer` go FAIL -> PASS), **0 lost**. The other 574 are byte-identical, so their verdicts cannot move +- `iverilog`: unchanged on all 7 (all seven already fail for other reasons), so the corpus acceptance columns do not move +- the "no guard flag in this scope" refusal fires **0** times in the corpus -- every one of the 17 sites got a real lowering + +## The lowering + +- a flag per loop that needs one: `reg __t27_brk_N` persists and joins the loop CONDITION, `reg __t27_cnt_N` is cleared at the top of each iteration and gates only the iteration tail +- allocated only when the body actually contains the jump, which is why the delta is 7 files and not 581 +- the scan for a jump **stops at a nested loop**, so an inner `break` binds to the inner loop +- `disable` was the other candidate and is not this: it cannot express `continue` without a block per iteration, and `disable ` is a **parse error** under `yosys` in all three modes this repo uses + +## Scope + +- `return` inside a loop is the OTHER half and is **not here**. `__t27_ret` already exists and is set correctly; what is missing is that no loop tests it. That repair moves loops containing no jump at all, so it gets its own measurement -- #2989 + +## Instrument + +- `tri jumps census` -- names every `break`/`continue` site in the generated Verilog and says what it lowered to: a flag, a no-op, or a refusal. Walks every `.t27` in the tree, not `specs/`, because that is the mistake this entry corrects +- it also asserts the **pairing**: every flag a loop declares must be written somewhere. A count of declarations cannot see a jump bound to the wrong loop; only the correspondence can