From 04683fa7235c6f7744e9dd6fa8b7037d53b563b5 Mon Sep 17 00:00:00 2001 From: lab Date: Tue, 8 Sep 2026 19:15:36 +0700 Subject: [PATCH] gen-c: a string's `.len` is strlen, in both spellings Closes #3489 Five passes named the `len` family and moved on, each for a stated reason. Splitting it by WHAT THE BASE ACTUALLY IS shows that a third of it needs no decision at all: `string` lowers to `const char*`, and C's answer for its length is `strlen`. THREE SPELLINGS, NOT ONE: `s.len()` ONE `ExprCall` named `s.len` 1322 in the specs `s.len` an `ExprFieldAccess` 687 `len(s)` a call to a free function 142 diagnostics The fourth time in this campaign that one rule had several spellings and only one was taught. The first two now go through ONE helper, so a change to the rule cannot reach one and miss the other. AND THE BASE DECIDES WHETHER AN ANSWER EXISTS: `string` 223 sites -> strlen(s) slice `[]T` 449 sites -> NEEDS A REPRESENTATION (#3464) Rust's `.len()` on `&str`, Zig's `.len` on `[]const u8` and C's `strlen` all count bytes, so the three agree and nothing is invented. MEASURED, whole corpus, -ferror-limit=0: errors 11 642 -> 11 401 (-241) files better/worse 14 / 0 `strlen(` emitted 0 -> 247, in 15 files igla_coder_eval 178 -> 103 `#include ` IS DECIDED BY THE SAME TWO HELPERS THE EMITTER USES. A missing include is an undeclared function -- the very family this repair shrinks -- and deciding it in a second place is how that happens. The mutant that drops it dies on five tests. THE THIRD SPELLING WAS MEASURED AND NOT HANDLED. Of the 302 argument shapes of `len(x)`, ZERO are a `string` parameter: 171 identifiers that are not parameters, 89 not plain identifiers, 19 slices, 14 `u32`. A branch for it would be unreachable today. A MUTANT THAT DROPPED THE PER-ITEM RESET SURVIVED, AND THE FIXTURE WAS THE REASON. The leak test declared an `i32` named `s` in a test block and asserted `s == 3` -- but a leaked set only shows up where the rule would FIRE. Rewritten to `assert(s.len() == 0)`, which becomes `strlen(s)` on an `i32` if the set leaks; the mutant dies. 1337 `.len` sites remain (886 calls, 451 fields), left loud on purpose: a slice lowers to a bare pointer, and `strlen` on one would read past the end of anything that is not NUL-terminated bytes. Tests: 6 new, full suite 3543 passed 0 failed. Six mutants, all killed. 14 stale seals refreshed. Co-Authored-By: Claude Opus 5 --- .trinity/seals/Backend.json | 4 +- .trinity/seals/RTL.json | 4 +- .trinity/seals/coder_igla-coder-arch.json | 4 +- .../seals/coder_igla-coder-benchmark.json | 4 +- .trinity/seals/coder_igla-coder-dataset.json | 4 +- .trinity/seals/coder_igla-coder-eval.json | 4 +- .trinity/seals/coder_igla-coder-pipeline.json | 4 +- .trinity/seals/coder_igla-coder-prm.json | 4 +- .../seals/coder_igla-coder-tokenizer.json | 4 +- .trinity/seals/coder_igla-coder-training.json | 4 +- .trinity/seals/coder_igla-coder-weights.json | 4 +- .trinity/seals/race_igla-race-backend.json | 4 +- .trinity/seals/race_igla-race-eda.json | 4 +- .trinity/seals/race_igla-race-formal.json | 4 +- .trinity/seals/race_igla-race-rtl.json | 4 +- .trinity/seals/race_igla-race-yosys.json | 4 +- bootstrap/src/compiler.rs | 102 ++++++++++ bootstrap/stage0/FROZEN_HASH | 2 +- bootstrap/tests/c_string_len.rs | 174 ++++++++++++++++++ .../2026-09-08-the-decidable-third-of-len.md | 11 ++ 20 files changed, 320 insertions(+), 33 deletions(-) create mode 100644 bootstrap/tests/c_string_len.rs create mode 100644 docs/now/2026-09-08-the-decidable-third-of-len.md diff --git a/.trinity/seals/Backend.json b/.trinity/seals/Backend.json index 151345367..4ada27a8a 100644 --- a/.trinity/seals/Backend.json +++ b/.trinity/seals/Backend.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:c14ee9123cacf6eed3b47fc3e6761cf8c44b1999d965380ed951763230d556f7", + "gen_hash_c": "sha256:d44b86294e7fdcaa56dd16e77fc9c93b2308b213b1cc8e8d8f286fe93fe00957", "gen_hash_rust": "sha256:c103921e09e51a6b1c7cc7e7cfa6a43b7651dfdc5397915293e7975a6fb1eaac", "gen_hash_verilog": "sha256:f365308297d87d4fa7268b80ec6d90ad91a2ca026d8ea6cf56716a75efcc3277", "gen_hash_zig": "sha256:270af45cd5c6748b9b8dc5cbfd1358d872d9b10ec22ec245eaa98021839ced41", "module": "Backend", "ring": 32, - "sealed_at": "2026-09-08T11:16:06Z", + "sealed_at": "2026-09-08T12:14:24Z", "spec_hash": "sha256:62f35791f856137a862caf7c205d54de386d8494bf2536fcab561741f08010e1", "spec_path": "specs/igla/race/backend.t27" } \ No newline at end of file diff --git a/.trinity/seals/RTL.json b/.trinity/seals/RTL.json index b1e4fe3ab..db463d482 100644 --- a/.trinity/seals/RTL.json +++ b/.trinity/seals/RTL.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:9538b96c35da3a13dbc6480ffe677c04fe58155b95f4576e30fe2b8f3a6885d6", + "gen_hash_c": "sha256:c9d16b1d8c345469327dd5d286165a2e94bc10142a778e88226f028f03ea7a53", "gen_hash_rust": "sha256:78753bc84edf9c0715982b590dc8acf77012445af5cba89db256fa7be1e457b6", "gen_hash_verilog": "sha256:d211220b876e0ff3a84b44de3a618dc7a8eeb8e5b6e7a54294fadf371e12af6d", "gen_hash_zig": "sha256:35e90b4869e68454c87458f55510d22690aaa056d3069c218b9a797a693d4059", "module": "RTL", "ring": 32, - "sealed_at": "2026-09-08T11:16:07Z", + "sealed_at": "2026-09-08T12:14:24Z", "spec_hash": "sha256:d062a67d5cea57f6533fdc506780b589c98ff079f673b59676d7062dc3ae2b56", "spec_path": "specs/igla/race/rtl.t27" } \ No newline at end of file diff --git a/.trinity/seals/coder_igla-coder-arch.json b/.trinity/seals/coder_igla-coder-arch.json index abcb7f5aa..210f4ab33 100644 --- a/.trinity/seals/coder_igla-coder-arch.json +++ b/.trinity/seals/coder_igla-coder-arch.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:6bcb78cf02d5354cfe0eee13db0c63c12700c31c05fb5f7de657879d09d039e5", + "gen_hash_c": "sha256:e40cd29e2a987dbc61d5955b6a59f02344eff3a2566e31b692516f510bb65d51", "gen_hash_rust": "sha256:c74ffd16d6a53e65bbce0f62af1e6611a44710dacff344b3ea28401fb5c6aad2", "gen_hash_verilog": "sha256:7ad38e94255fb3442588369b348b61df4d302df3e783ebf8fd0a203ec5e28e1c", "gen_hash_zig": "sha256:b901b1daef237e8ab4ab5171e4546dceb3426bdf6c008f2077776b97bab785bc", "module": "igla-coder-arch", "ring": 12, - "sealed_at": "2026-09-08T11:16:05Z", + "sealed_at": "2026-09-08T12:14:22Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:01353d9b489c0adbf88cda433d9b37fa974ab73977195601a6dea9feec74d740", "spec_path": "specs/igla/coder/arch.t27" diff --git a/.trinity/seals/coder_igla-coder-benchmark.json b/.trinity/seals/coder_igla-coder-benchmark.json index b9d9c02a1..5b0b51143 100644 --- a/.trinity/seals/coder_igla-coder-benchmark.json +++ b/.trinity/seals/coder_igla-coder-benchmark.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:12d569d3bec550d5302ceface69e4061b5f9ccd8d8f15c8294ca278875b17d24", + "gen_hash_c": "sha256:a60e2287009a5a83e24ea4417252138cbed8fd4e6f8a56ed693639a474414432", "gen_hash_rust": "sha256:a3fca9ee8ea5a922318c415c78dfddc14a93b29fdfc693e88d21ab451e9a36f4", "gen_hash_verilog": "sha256:829aa193ca03b2c33b60c4038c77e06f24939bb9e3dd52a88348d8f9be62aefd", "gen_hash_zig": "sha256:3b10b64c4d2487cfcefc130f4d1ac1020f47d5294957c4717df407278aeefb6a", "module": "igla-coder-benchmark", "ring": 12, - "sealed_at": "2026-09-08T11:16:05Z", + "sealed_at": "2026-09-08T12:14:23Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:3bc0b4f043a561e151081570bb5035fa4e54b3a62b061619056f8a13ecdcb1d8", "spec_path": "specs/igla/coder/benchmark.t27" diff --git a/.trinity/seals/coder_igla-coder-dataset.json b/.trinity/seals/coder_igla-coder-dataset.json index 03068b92d..b725db6fd 100644 --- a/.trinity/seals/coder_igla-coder-dataset.json +++ b/.trinity/seals/coder_igla-coder-dataset.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:474aba79af25f47c6266c5c6315161141bf943105c85f1820cee43ac0bcf4521", + "gen_hash_c": "sha256:65a5b0e24b06b31aebfd56fff667a19cece8900b33063848dae7c0f095d2b21b", "gen_hash_rust": "sha256:b725d5db75bc0e7e668119d0967b03e581cbebaff0115e640762bef3f532d62b", "gen_hash_verilog": "sha256:ca4b5edae76903601e57b78b3ad9892a1f37b85d377653ba816ef26460527529", "gen_hash_zig": "sha256:9cc02f8da315f4d5ecbe7ab6023af027e33f4e0572a9b2bab55e73151511d99f", "module": "igla-coder-dataset", "ring": 12, - "sealed_at": "2026-09-08T11:16:06Z", + "sealed_at": "2026-09-08T12:14:23Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:b9a67a36ec91ba9f2df7afb43a264d152ae0c5577e29f11371fcb39668aad0d2", "spec_path": "specs/igla/coder/dataset.t27" diff --git a/.trinity/seals/coder_igla-coder-eval.json b/.trinity/seals/coder_igla-coder-eval.json index 0bdee39f7..92e1d06cb 100644 --- a/.trinity/seals/coder_igla-coder-eval.json +++ b/.trinity/seals/coder_igla-coder-eval.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:bdc92613d9482b42c31782b8b989a3b84f658132ac709cdc0dae5d138515d85c", + "gen_hash_c": "sha256:741f540bfafa15d053aa7c1c8d4d22dd6b58c428ac766d3a383f9bd36e0a29b5", "gen_hash_rust": "sha256:f8ccfb7b1aaf5cfa0c7c10290463cb1b12e1a36935670fb83db3629146a2340e", "gen_hash_verilog": "sha256:a8b606f5eaf20caaf68306139b4b50009c29fd846c0a488869a52a3c7ec16946", "gen_hash_zig": "sha256:86f8c3f2cd967e405ef4437bf8a8ed5c9c2bd3e98eb30043cbd957f06a7a3ff7", "module": "igla-coder-eval", "ring": 12, - "sealed_at": "2026-09-08T11:16:06Z", + "sealed_at": "2026-09-08T12:14:23Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:c79668668fd5c4248c5be928bfb4965bf3ae44158fbfc3c56d89f396d573172d", "spec_path": "specs/igla/coder/eval.t27" diff --git a/.trinity/seals/coder_igla-coder-pipeline.json b/.trinity/seals/coder_igla-coder-pipeline.json index 5fc734a7f..fe9541556 100644 --- a/.trinity/seals/coder_igla-coder-pipeline.json +++ b/.trinity/seals/coder_igla-coder-pipeline.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:317445bec3f45e0dc0d6b8ee456e946516e48da8511ada1a9ca517c3dd5a6834", + "gen_hash_c": "sha256:1151911dce49988d805a635f856ee87a2f18c0af2140d4660fb053e7f6c76c2b", "gen_hash_rust": "sha256:eb4c6582020de55d0301b9a7f47d4770a59907a5f81318e8c5054a4bc66b00eb", "gen_hash_verilog": "sha256:a05bd90f9cff0b53f50cac2a23715caf215032d0c723ba57637cfd4210293aa9", "gen_hash_zig": "sha256:db388c8f28e2796ece2eb1b66b6c4cc26759ffb5504a71c5e7c16a990b1aaa1e", "module": "igla-coder-pipeline", "ring": 12, - "sealed_at": "2026-09-08T11:16:06Z", + "sealed_at": "2026-09-08T12:14:23Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:7ced47ba19d0e638422a4fb841c25e3abd45b561787e18f3d62f2bcad90321da", "spec_path": "specs/igla/coder/pipeline.t27" diff --git a/.trinity/seals/coder_igla-coder-prm.json b/.trinity/seals/coder_igla-coder-prm.json index 2e2fd00d3..f23bd9dca 100644 --- a/.trinity/seals/coder_igla-coder-prm.json +++ b/.trinity/seals/coder_igla-coder-prm.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:92412e73ab112811ec9dea56be3f6bd912074337caa2724c27a85ee271f5da38", + "gen_hash_c": "sha256:5cd2d918a14bd1903b3fba75511bd48411effb613d93726164c43d5cf7ea0086", "gen_hash_rust": "sha256:efdee3a1ab6c5ca9a5c1f2abf5af10ea99f9b8ac3c01e3c92ab37f3c2808d68d", "gen_hash_verilog": "sha256:bf1b217f773539abf6bce685e982cc54199da697e41253801a04fb4450275467", "gen_hash_zig": "sha256:cf646f30a60f5e79071b622165e3861170a11d13aa52498b17728d114f3bb87e", "module": "igla-coder-prm", "ring": 12, - "sealed_at": "2026-09-08T10:55:58Z", + "sealed_at": "2026-09-08T12:14:23Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:3eee11bf8df22d370b2a4d29b3a2ddb4bcff529c01e3a23212962b6f7db44ded", "spec_path": "specs/igla/coder/prm.t27" diff --git a/.trinity/seals/coder_igla-coder-tokenizer.json b/.trinity/seals/coder_igla-coder-tokenizer.json index d45cbafba..066397c44 100644 --- a/.trinity/seals/coder_igla-coder-tokenizer.json +++ b/.trinity/seals/coder_igla-coder-tokenizer.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:36807f21c9d060f0d3a80909fcb8e22c405041cc04688752fbf3dfd643ace64d", + "gen_hash_c": "sha256:2fac9a88fd7e71bbe60518b7310e16742bf85019a4a03133468de808d34a82f9", "gen_hash_rust": "sha256:c0f00ace59cd9774faa64bbe5b7e54872c0d5bccc56159f5361833b67cc17193", "gen_hash_verilog": "sha256:c172ed1393dcbaa31d882b80ff1bc6fe0e4629a021de15653f8b47140df35775", "gen_hash_zig": "sha256:27ab8c368cd75f327f1acaeea4daaf6855be4db5e5d3440d4773c59dd1bcfd79", "module": "igla-coder-tokenizer", "ring": 12, - "sealed_at": "2026-09-08T11:16:06Z", + "sealed_at": "2026-09-08T12:14:24Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:5a4913e61e90b9be2632619b3eb79b7a93a445d36fa724601fac5f95e3c7a283", "spec_path": "specs/igla/coder/tokenizer.t27" diff --git a/.trinity/seals/coder_igla-coder-training.json b/.trinity/seals/coder_igla-coder-training.json index 940944e51..bb35c641b 100644 --- a/.trinity/seals/coder_igla-coder-training.json +++ b/.trinity/seals/coder_igla-coder-training.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:67f9554bc72128688dd9103bcefffd54ea00659830e5217da60c9943fde430a6", + "gen_hash_c": "sha256:3a02d7eb5cdd04a2669a5f0ca07fc6c465a1890aa71e5a4b3ffdac0c0504981f", "gen_hash_rust": "sha256:759befd74dc3dae89f9a48dd0f74432e8d3c2d33f7cab17b0987d10ec26b0efc", "gen_hash_verilog": "sha256:dd7901d16fd6b3f3479374459e3a819cc448fc3c28b55582ad0bb550eb6967bd", "gen_hash_zig": "sha256:f4311a46769be6e4fffee985c5ecfd91573150569ef770a3b3d483065c93c604", "module": "igla-coder-training", "ring": 12, - "sealed_at": "2026-09-08T11:16:06Z", + "sealed_at": "2026-09-08T12:14:24Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:ad23bb061da98f151cca70bbc2e6be16536f23f09d867aac11c2040a2427ac82", "spec_path": "specs/igla/coder/training.t27" diff --git a/.trinity/seals/coder_igla-coder-weights.json b/.trinity/seals/coder_igla-coder-weights.json index 85f1462cf..a8a9a07ba 100644 --- a/.trinity/seals/coder_igla-coder-weights.json +++ b/.trinity/seals/coder_igla-coder-weights.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:35b7a17d4a11be5d76ae5fc5c66c8e1493c95e22caf18aad1a5b6a8ef897ff25", + "gen_hash_c": "sha256:395d3292902a906c3a8994f0ed15c3a498afe05749eb83fe4258b454e1fe4579", "gen_hash_rust": "sha256:b21bd972ff362927011d1e76f2d9841cf7fff8fd81c9a80053c3167364b03eb2", "gen_hash_verilog": "sha256:4f92a4528c6de26a76d0bac3bbc3456ff058cdc71dff06463ffa8f0fc473fa9e", "gen_hash_zig": "sha256:b6906990ff354d8f893c6f12558706bbd8037af4b3e6b872758c99ec392e10ba", "module": "igla-coder-weights", "ring": 12, - "sealed_at": "2026-09-08T10:55:59Z", + "sealed_at": "2026-09-08T12:14:24Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:ff49a27495a6de1e3fa7c6a16ed855385c7f772a67591d593199659d5387f814", "spec_path": "specs/igla/coder/weights.t27" diff --git a/.trinity/seals/race_igla-race-backend.json b/.trinity/seals/race_igla-race-backend.json index e03404993..513fd58b1 100644 --- a/.trinity/seals/race_igla-race-backend.json +++ b/.trinity/seals/race_igla-race-backend.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:c14ee9123cacf6eed3b47fc3e6761cf8c44b1999d965380ed951763230d556f7", + "gen_hash_c": "sha256:d44b86294e7fdcaa56dd16e77fc9c93b2308b213b1cc8e8d8f286fe93fe00957", "gen_hash_rust": "sha256:c103921e09e51a6b1c7cc7e7cfa6a43b7651dfdc5397915293e7975a6fb1eaac", "gen_hash_verilog": "sha256:f365308297d87d4fa7268b80ec6d90ad91a2ca026d8ea6cf56716a75efcc3277", "gen_hash_zig": "sha256:270af45cd5c6748b9b8dc5cbfd1358d872d9b10ec22ec245eaa98021839ced41", "module": "igla-race-backend", "ring": 12, - "sealed_at": "2026-09-08T11:16:06Z", + "sealed_at": "2026-09-08T12:14:24Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:62f35791f856137a862caf7c205d54de386d8494bf2536fcab561741f08010e1", "spec_path": "specs/igla/race/backend.t27" diff --git a/.trinity/seals/race_igla-race-eda.json b/.trinity/seals/race_igla-race-eda.json index 6951b0bc1..9c45c4af9 100644 --- a/.trinity/seals/race_igla-race-eda.json +++ b/.trinity/seals/race_igla-race-eda.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:f9a086ea353449b26c75cad0a756e42e9e6b6d3488868b840be979e8ac6892e0", + "gen_hash_c": "sha256:354b227a1b2afa6eafd1796476f021b94ed931ff509686b45b7b0fbaa0518ab2", "gen_hash_rust": "sha256:6a8bb2e8126478ab79cdddad05919a6f6808b4bad9042a03e6edb1695cf26e79", "gen_hash_verilog": "sha256:067413bf2da4752214c260dd88f7bb1421280afc2a936b6e5fd8c0c870294c6b", "gen_hash_zig": "sha256:17ca4c1597f3edcc6ee0ed88df0c63a7ca7d162c981394cdfd4e867f4d248099", "module": "igla-race-eda", "ring": 12, - "sealed_at": "2026-09-08T11:16:07Z", + "sealed_at": "2026-09-08T12:14:24Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:d690f4e6293ffb745638b1a134bfddb550b6a4dd96f358abebb240e8c5190f27", "spec_path": "specs/igla/race/eda.t27" diff --git a/.trinity/seals/race_igla-race-formal.json b/.trinity/seals/race_igla-race-formal.json index 21c1d4d61..1bd45743f 100644 --- a/.trinity/seals/race_igla-race-formal.json +++ b/.trinity/seals/race_igla-race-formal.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:30bed10f7e42da69a60ea07ec70f5d91473024c1d15d02dc4d059d303ad7f8a3", + "gen_hash_c": "sha256:cb3dcb775a85ca093608697069c1e01913dede3ff7ee8f13461e9454821e80b1", "gen_hash_rust": "sha256:76a75680c7ec27fd3ec66cf2e6c4e8a63c6306f0fdded4402ed0260737b2f7f9", "gen_hash_verilog": "sha256:c42bd135a1cb4345d85f79513fc7bcf157387d7ccde398f40007e9f2a54db3ea", "gen_hash_zig": "sha256:d8057987114e327da5de81113ec6e7759edd33a5c29f70cfef8ef0cd17df2dbc", "module": "igla-race-formal", "ring": 12, - "sealed_at": "2026-09-08T11:16:07Z", + "sealed_at": "2026-09-08T12:14:24Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:fbc99bc261c348711aaa19ffb14812698eed8b47ae7e3d64a3922fb7d418ec95", "spec_path": "specs/igla/race/formal.t27" diff --git a/.trinity/seals/race_igla-race-rtl.json b/.trinity/seals/race_igla-race-rtl.json index e62c87739..5f826fdae 100644 --- a/.trinity/seals/race_igla-race-rtl.json +++ b/.trinity/seals/race_igla-race-rtl.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:9538b96c35da3a13dbc6480ffe677c04fe58155b95f4576e30fe2b8f3a6885d6", + "gen_hash_c": "sha256:c9d16b1d8c345469327dd5d286165a2e94bc10142a778e88226f028f03ea7a53", "gen_hash_rust": "sha256:78753bc84edf9c0715982b590dc8acf77012445af5cba89db256fa7be1e457b6", "gen_hash_verilog": "sha256:d211220b876e0ff3a84b44de3a618dc7a8eeb8e5b6e7a54294fadf371e12af6d", "gen_hash_zig": "sha256:35e90b4869e68454c87458f55510d22690aaa056d3069c218b9a797a693d4059", "module": "igla-race-rtl", "ring": 12, - "sealed_at": "2026-09-08T11:16:07Z", + "sealed_at": "2026-09-08T12:14:24Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:d062a67d5cea57f6533fdc506780b589c98ff079f673b59676d7062dc3ae2b56", "spec_path": "specs/igla/race/rtl.t27" diff --git a/.trinity/seals/race_igla-race-yosys.json b/.trinity/seals/race_igla-race-yosys.json index 443cea57e..cdee18974 100644 --- a/.trinity/seals/race_igla-race-yosys.json +++ b/.trinity/seals/race_igla-race-yosys.json @@ -1,11 +1,11 @@ { - "gen_hash_c": "sha256:6d2378a3f363e4e0ef0e9ad874700b6340086d88488bb3169b17daf149a2374e", + "gen_hash_c": "sha256:04064359f0d2f1e401c91c005b1283cfe77a83702e307f263ae855bc399c46d4", "gen_hash_rust": "sha256:902e4fda0e6b2fa1a46877da0a6d6d7568619b05f3c4d0c30e72cd12bde7b24d", "gen_hash_verilog": "sha256:824a0d62f756931a5711d0d94d822a09ea2b2b7a1f39ff2e5a83fd32b8ecac6d", "gen_hash_zig": "sha256:fc5e0cab1d1a52085b720bcda46524531936a5e04a755bf9eb0b5379d8006476", "module": "igla-race-yosys", "ring": 12, - "sealed_at": "2026-09-08T11:16:08Z", + "sealed_at": "2026-09-08T12:14:24Z", "sealed_by": "t27c-bootstrap@0.2.0", "spec_hash": "sha256:1c2c4ed75fb636b5111a9da51fa55a1fa03fc689daab0159c3d2dfce66984c99", "spec_path": "specs/igla/race/yosys.t27" diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 600153721..cfc9ce1f9 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -18129,6 +18129,9 @@ pub struct CCodegen { /// C struct typedef name of the current function's [T; N] return type, /// so a returned array literal can be cast to the right compound literal. current_ret_array_type: Option, + /// Identifiers (params + annotated locals) declared `string` in the item + /// being emitted: `s.len` on one of them is `strlen(s)`, in both spellings. + string_typed_names: std::collections::HashSet, /// Identifiers (params + locals) declared `*T` in the item being emitted: /// a field access on them is `->`, not `.`. C is the only backend that /// distinguishes the two, and 152 sites reached it with a dot. @@ -18155,6 +18158,7 @@ impl CCodegen { scaffold_locals_c: std::collections::HashMap::new(), current_ret_tuple_type: None, current_ret_array_type: None, + string_typed_names: std::collections::HashSet::new(), pointer_typed_names: std::collections::HashSet::new(), array_typed_names: std::collections::HashSet::new(), const_defs: std::collections::HashMap::new(), @@ -18458,6 +18462,14 @@ impl CCodegen { self.write_line("#include "); self.write_line("#include "); + // `s.len` on a `string` becomes `strlen(s)`. Decided by the SAME two + // helpers the emitter uses, so the include and the call site cannot + // disagree -- a missing include is an undeclared function, which is + // the very family this repair is shrinking. + if Self::module_uses_strlen(ast) { + self.write_line("#include "); + } + // Check if tests exist — add assert.h let has_tests = ast.children.iter().any(|d| d.kind == NodeKind::TestBlock); if has_tests { @@ -19179,6 +19191,83 @@ impl CCodegen { } } + /// Names declared `string` in one item: parameters, and locals that carry + /// an annotation. ONE source of truth, used both by the pre-scan that + /// decides whether `` is included and by the emitter that writes + /// `strlen` -- if those two ever disagree, the header does not compile. + fn collect_string_typed(item: &Node) -> std::collections::HashSet { + let mut out = std::collections::HashSet::new(); + for (pname, ptype) in &item.params { + if ptype.trim() == "string" { + out.insert(pname.clone()); + } + } + fn walk(nodes: &[Node], out: &mut std::collections::HashSet) { + for n in nodes { + if n.kind == NodeKind::StmtLocal && n.extra_type.trim() == "string" { + out.insert(n.name.clone()); + } + walk(&n.children, out); + } + } + walk(&item.children, &mut out); + out + } + + /// The base of a `.len` taken on a `string`, in EITHER spelling. + /// + /// The specs write both: `s.len()` parses as one `ExprCall` named `s.len`, + /// and `s.len` as an `ExprFieldAccess`. 1322 of the first and 687 of the + /// second across the corpus -- the third time in this campaign that one + /// rule had two spellings and only one was taught. + /// + /// `string` lowers to `const char*`, which carries no length, so the C + /// answer is `strlen`. That is the WHOLE of what is decidable here: on a + /// slice `[]T` the same expression needs a representation that does not + /// exist yet (#3464), and this returns None for it. + fn string_len_base<'a>( + node: &'a Node, + strings: &std::collections::HashSet, + ) -> Option<&'a str> { + match node.kind { + NodeKind::ExprCall if node.children.is_empty() => { + let (base, field) = node.name.rsplit_once('.')?; + if field == "len" && strings.contains(base) { + return Some(base); + } + None + } + NodeKind::ExprFieldAccess if node.name == "len" => { + let base = node.children.first()?; + if base.kind == NodeKind::ExprIdentifier && strings.contains(&base.name) { + return Some(base.name.as_str()); + } + None + } + _ => None, + } + } + + /// Does any item in this module take `.len` on a `string`? Decides the + /// `` include, using the same two helpers the emitter uses. + fn module_uses_strlen(ast: &Node) -> bool { + fn any(nodes: &[Node], strings: &std::collections::HashSet) -> bool { + nodes.iter().any(|n| { + CCodegen::string_len_base(n, strings).is_some() || any(&n.children, strings) + }) + } + fn items(node: &Node) -> bool { + node.children.iter().any(|c| { + let is_item = matches!( + c.kind, + NodeKind::FnDecl | NodeKind::TestBlock | NodeKind::BenchBlock + ); + (is_item && any(&c.children, &CCodegen::collect_string_typed(c))) || items(c) + }) + } + items(ast) + } + /// The C constant for `base`'s enum member `member`, if this module /// declares that enum AND that member. /// @@ -19759,6 +19848,7 @@ impl CCodegen { self.c_array_info_r(&node.extra_return_type).map(|(n, _, _)| n); self.array_typed_names.clear(); self.pointer_typed_names.clear(); + self.string_typed_names = Self::collect_string_typed(node); for (pname, ptype) in &node.params { if Self::c_array_info(ptype).is_some() { self.array_typed_names.insert(pname.clone()); @@ -19813,6 +19903,7 @@ impl CCodegen { self.current_ret_array_type = None; self.array_typed_names.clear(); self.pointer_typed_names.clear(); + self.string_typed_names = Self::collect_string_typed(node); self.write_line(&format!("void {}(void) {{", fn_name)); self.indent(); @@ -20038,6 +20129,7 @@ impl CCodegen { // cell;` was then written `cell->scope`, +38 errors in one file. self.pointer_typed_names.clear(); self.array_typed_names.clear(); + self.string_typed_names = Self::collect_string_typed(node); self.write_line(&format!("void {}(void) {{", fn_name)); self.indent(); self.write_indent(); @@ -20936,6 +21028,11 @@ impl CCodegen { self.write(&node.name.to_uppercase()); } NodeKind::ExprCall => { + // `s.len()` -- one of the two spellings; see `string_len_base`. + if let Some(base) = Self::string_len_base(node, &self.string_typed_names) { + self.write(&format!("strlen({})", base)); + return; + } let fname = &node.name; // The third call-site of the scaffold class. `default_input()` // and `valid_input()` are TEMPLATE SCAFFOLD, not functions: 571 @@ -21141,6 +21238,11 @@ impl CCodegen { // their language; C was the only backend without an answer. // The `::` spelling of the SAME member reference is handled in // the identifier arm -- one rule, one helper, two spellings. + // `s.len` -- the other spelling of the same rule. + if let Some(base) = Self::string_len_base(node, &self.string_typed_names) { + self.write(&format!("strlen({})", base)); + return; + } if let Some(base) = node.children.first() { if base.kind == NodeKind::ExprIdentifier { if let Some(c) = self.c_enum_constant(&base.name, &node.name) { diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index 2bdc1da41..a6b8ca897 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -d5f667361f3d536baf954d1886e4205da03f0857c06df60565bbf31626501520 bootstrap/src/compiler.rs +d958bcfcb5e431e14cb1cf79e74ea2916c9ff21886462da73badef476d2e3e99 bootstrap/src/compiler.rs diff --git a/bootstrap/tests/c_string_len.rs b/bootstrap/tests/c_string_len.rs new file mode 100644 index 000000000..f02b0cc62 --- /dev/null +++ b/bootstrap/tests/c_string_len.rs @@ -0,0 +1,174 @@ +//! `s.len` on a `string` is `strlen(s)`, in both spellings. +//! +//! `string` lowers to `const char*`, which carries no length, and the specs +//! write the query three ways: +//! +//! `s.len()` parses as ONE `ExprCall` named `s.len` -- 1322 in the specs +//! `s.len` parses as an `ExprFieldAccess` -- 687 +//! `len(s)` parses as a call to a free function -- 142 diagnostics +//! +//! The first two are handled here through ONE helper, so a change to the rule +//! cannot reach one spelling and miss the other -- the fourth time in this +//! campaign that one rule had several spellings and only one was taught. +//! +//! The third spelling is NOT handled, and the reason is measured rather than +//! assumed: of its 302 argument shapes, **zero** are a `string` parameter (171 +//! are identifiers that are not parameters, 89 are not plain identifiers, 19 +//! are slices). A branch for it would be unreachable. +//! +//! WHAT IS DELIBERATELY LEFT ALONE: `xs.len` where `xs` is a slice `[]T`. A +//! slice lowers to a bare pointer, so the length is not recoverable without a +//! representation that does not exist yet (#3464). 1337 sites remain, and they +//! are the subject of that decision, not of this repair. + +use std::process::Command; +use std::sync::atomic::{AtomicUsize, Ordering}; + +static N: AtomicUsize = AtomicUsize::new(0); + +fn cc_present() -> bool { + Command::new("cc").arg("--version").output().map(|o| o.status.success()).unwrap_or(false) +} + +fn gen_c(spec: &str, tag: &str) -> (String, std::path::PathBuf) { + let d = std::env::temp_dir().join(format!( + "t27c-strlen-{tag}-{}-{}", + std::process::id(), + N.fetch_add(1, Ordering::Relaxed) + )); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).expect("dir"); + let p = d.join("in.t27"); + std::fs::write(&p, spec).expect("write"); + let out = Command::new(env!("CARGO_BIN_EXE_t27c")) + .arg("gen-c") + .arg(&p) + .output() + .expect("t27c"); + assert!(out.status.success(), "gen-c failed: {}", String::from_utf8_lossy(&out.stderr)); + let h = String::from_utf8_lossy(&out.stdout).to_string(); + assert!(!h.is_empty(), "gen-c produced an EMPTY header"); + (h, d) +} + +fn errors(h: &str, d: &std::path::Path) -> usize { + let p = d.join("h.h"); + std::fs::write(&p, h).expect("write"); + let out = Command::new("cc") + .args(["-std=c11", "-ferror-limit=0", "-fsyntax-only", "-x", "c"]) + .arg(&p) + .output() + .expect("cc"); + String::from_utf8_lossy(&out.stderr) + .lines() + .filter(|l| { + let mut it = l.splitn(4, ':'); + it.next().is_some() + && it.next().map_or(false, |s| s.trim().parse::().is_ok()) + && it.next().map_or(false, |s| s.trim().parse::().is_ok()) + && it.next().map_or(false, |s| s.trim_start().starts_with("error: ")) + }) + .count() +} + +#[test] +fn the_call_spelling_becomes_strlen() { + let (h, d) = gen_c( + "module S {\n fn a(s: string) -> u32 { return s.len(); }\n}\n", + "call", + ); + assert!(h.contains("return strlen(s);"), "got:\n{h}"); + if cc_present() { + assert_eq!(errors(&h, &d), 0, "and it must compile:\n{h}"); + } +} + +#[test] +fn both_spellings_produce_the_same_call() { + // The point of the shared helper. If these diverge, one call site was + // changed without the other. + let (h, d) = gen_c( + "module S {\n fn a(s: string) -> u32 { return s.len(); }\n\ + \x20 fn b(s: string) -> u32 { return s.len; }\n}\n", + "both", + ); + assert_eq!( + h.matches("return strlen(s);").count(), + 2, + "both spellings must lower the same way:\n{h}" + ); + if cc_present() { + assert_eq!(errors(&h, &d), 0, "and it must compile:\n{h}"); + } +} + +#[test] +fn a_slice_is_left_alone() { + // THE DISCRIMINATING CASE, and the deliberate limitation. A slice lowers to + // a bare pointer; `strlen` on it would read past the end of an array of + // anything that is not NUL-terminated bytes. The loud form names the real + // problem (#3464). + let (h, _d) = gen_c( + "module S {\n fn c(xs: []u32) -> u32 { return xs.len(); }\n}\n", + "slice", + ); + assert!(h.contains("xs.len()"), "a slice keeps its own spelling:\n{h}"); + assert!(!h.contains("strlen(xs)"), "and must NOT become strlen:\n{h}"); +} + +#[test] +fn an_annotated_string_local_counts_too() { + let (h, d) = gen_c( + "module S {\n fn d(v: i32) -> u32 { var s: string = \"abc\"; return s.len(); }\n}\n", + "local", + ); + assert!(h.contains("strlen(s)"), "an annotated local is a string too:\n{h}"); + if cc_present() { + assert_eq!(errors(&h, &d), 0, "and it must compile:\n{h}"); + } +} + +#[test] +fn string_h_is_included_exactly_when_it_is_used() { + // A missing include is an undeclared function -- the very family this + // repair shrinks -- so the include and the call site are decided by the + // SAME two helpers, and this is the test that says so. + let (used, _d) = gen_c( + "module S {\n fn a(s: string) -> u32 { return s.len(); }\n}\n", + "inc-yes", + ); + assert!(used.contains("#include "), "used, so included:\n{used}"); + let (unused, _d2) = gen_c( + "module S {\n fn a(v: i32) -> i32 { return v; }\n}\n", + "inc-no", + ); + assert!( + !unused.contains("#include "), + "unused, so not included:\n{unused}" + ); +} + +#[test] +fn the_string_set_does_not_leak_into_a_test_block() { + // The boundary lesson from #3477: the set is per ITEM, and there are three + // item emitters. A test block's own `s` of another type must not acquire + // `strlen`. + // The test block must USE `.len` on its own `s`, or the fixture proves + // nothing: a leaked set only shows up where the rule would fire. The first + // version asserted `s == 3` and a mutant that dropped the reset survived it. + let (h, _d) = gen_c( + "module S {\n fn a(s: string) -> u32 { return s.len(); }\n\ + \x20 test \"same name, not a string\" { var s: i32 = 3; assert(s.len() == 0); }\n}\n", + "leak", + ); + assert!(h.contains("return strlen(s);"), "the fn still lowers:\n{h}"); + assert!( + h.contains("s.len()"), + "the test block's own `s` is an i32 and keeps its spelling:\n{h}" + ); + assert_eq!( + h.matches("strlen(s)").count(), + 1, + "exactly one strlen -- the function's, not the test block's:\n{h}" + ); +} diff --git a/docs/now/2026-09-08-the-decidable-third-of-len.md b/docs/now/2026-09-08-the-decidable-third-of-len.md new file mode 100644 index 000000000..a65cb3deb --- /dev/null +++ b/docs/now/2026-09-08-the-decidable-third-of-len.md @@ -0,0 +1,11 @@ +# NOW -- The decidable third of `len` (2026-09-08) + +## The decidable third of `len` (Closes #3489) + +- Five passes named the `len` family and moved on. Splitting it by **what the base actually is** shows a third of it needs no decision: `string` lowers to `const char*`, and C's answer for its length is `strlen`. **223 sites have a `string` parameter as the base; 449 have a slice**, which is the part that genuinely needs a representation (#3464). +- **Three spellings, not one**: `s.len()` parses as ONE `ExprCall` named `s.len` (**1322** in the specs), `s.len` as an `ExprFieldAccess` (**687**), and `len(s)` as a free function (142 diagnostics). The fourth time in this campaign that one rule had several spellings and only one was taught. The first two now go through **one helper**. +- Errors **11 642 -> 11 401**, **14 files better and none worse**, 247 `strlen(` emitted across 15 files. `igla_coder_eval` 178 -> 103. +- **`#include ` is decided by the same two helpers the emitter uses.** A missing include is an undeclared function -- the very family this repair shrinks -- and deciding it in a second place is how that happens. A mutant that drops the include is killed by five tests. +- **The third spelling was measured and NOT handled.** Of the 302 argument shapes of `len(x)`, **zero** are a `string` parameter: 171 are identifiers that are not parameters, 89 are not plain identifiers, 19 are slices, 14 are `u32`. A branch for it would be unreachable today, so it is filed rather than written. +- **A mutant that dropped the per-item reset survived, and the fixture was the reason.** The leak test declared an `i32` named `s` in a test block and asserted `s == 3` -- a leaked set only shows up where the rule would FIRE, so nothing caught it. Rewritten to `assert(s.len() == 0)`, which becomes `strlen(s)` on an `i32` if the set leaks; the mutant dies. +- 1337 `.len` sites remain (886 calls, 451 fields) and are left loud on purpose: `strlen` on a slice would read past the end of anything that is not NUL-terminated bytes.