From 990965c78d140e52db01edb1d4ed1013c23df4e7 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Sun, 18 Oct 2026 15:35:27 +0700 Subject: [PATCH] claw: write the text helpers once in specs/claw/text.t27 (Closes #10317) put_str, digit_byte, put_count, buf_has, str_eq and has_prefix lived as copies in gates, sarif, suggest, service, preview and sarif_pins. They now live in specs/claw/text.t27 and every importer says `use claw::text;`. The sarif.t27 comment claiming the copies exist because the file must lower alone was wrong: use_resolve splices the imported helpers in before gen-c runs. preview.t27 swaps its `use claw::recipes;` for `use claw::text;`: it needs a single use, since recipes::str_eq and text::str_eq are reported ambiguous together and the qualified references stop resolving. Its tests compare strings with == instead. Measured on the Railway lab: test-report passes on text, gates, sarif, sarif_pins, suggest, service, preview, github_surface and recipes; the gate-topology claw audit still builds from gen/c/claw/gates.c and reports a current matrix; check_seal_currency reports 0 stale. gen/c/claw/gates.c is regenerated and the ClawGates, ClawGatesPins and ClawService seals re-saved in this commit. The dupe ledger moves down in the same commit (bytes_eq 3 -> 2; buf_has, digit_byte, has_prefix, put_count, put_str, str_eq removed). log_frame, which master's ledger lacks since particle_engine, is left to #10316. Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/claw_ClawGates.json | 21 ++- .trinity/seals/claw_ClawGatesPins.json | 6 +- .trinity/seals/claw_ClawService.json | 21 ++- gen/c/claw/gates.c | 216 ++++++++++++------------- specs/claw/gates.t27 | 86 +--------- specs/claw/preview.t27 | 33 ++-- specs/claw/sarif.t27 | 70 +------- specs/claw/sarif_pins.t27 | 21 +-- specs/claw/service.t27 | 28 ++-- specs/claw/suggest.t27 | 14 +- specs/claw/text.t27 | 177 ++++++++++++++++++++ tools/duplicate_bodies_baseline.txt | 8 +- 12 files changed, 348 insertions(+), 353 deletions(-) create mode 100644 specs/claw/text.t27 diff --git a/.trinity/seals/claw_ClawGates.json b/.trinity/seals/claw_ClawGates.json index 0cb0fee79b..201b9d97bb 100644 --- a/.trinity/seals/claw_ClawGates.json +++ b/.trinity/seals/claw_ClawGates.json @@ -1,17 +1,22 @@ { - "built_by": "t27c-bootstrap@0.5.2+043cdea36", - "closure": [], + "built_by": "t27c-bootstrap@0.5.2+9da2c5f", + "closure": [ + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:eab393c71eb760e092ff07c51489fb2e5d67a6e8b25262ae7ebf5c95fbfb19a1" + } + ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:97188898c4d8cda3d8bbb551de2a41cc67db142ee15f724bbfee30bb4cedd0c4", - "gen_hash_rust": "sha256:6192bfeb11dcb93a415eb8ef5f30a07d2588e58b0ea832f8ac99f4428a6829ee", - "gen_hash_verilog": "sha256:4ec80fa7604787bbb30bfee71cc6e838f4ac24e33348cbfbe46682182ff43b5e", - "gen_hash_zig": "sha256:3f6afb1b574d5a4346abf72b7a313ccae1316f56c04ef486c999fa2e160e4ee9", + "gen_hash_c": "sha256:31f5535824c04d63b77b8df6e0a72e96fef46ff1609043a46abedd3df21684c5", + "gen_hash_rust": "sha256:811db936e27fb63042662fb804f32f6cd4bd9dfff0d816d3f597c0cdc1c178fa", + "gen_hash_verilog": "sha256:cd9116aae7e4cd6315743c6ebc05c08579284821cfbfd363c33986cf0fd96b7a", + "gen_hash_zig": "sha256:83f1b79f148d10b88c18858c57630b06540be83be4335fc8b8fb0b57bf2651f8", "module": "ClawGates", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T07:27:06Z", + "sealed_at": "2026-10-18T08:30:55Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:2b49b0ada4cf5323472026378a33a07831bcc750ccc3a5a7b649e115dbcb6ab6", + "spec_hash": "sha256:3ae3db764f44ce4e0c45b8c7af6f17db39755e3c23be43c17a75409394a0a32d", "spec_path": "specs/claw/gates.t27", "tests": { "failed": 0, diff --git a/.trinity/seals/claw_ClawGatesPins.json b/.trinity/seals/claw_ClawGatesPins.json index 1e219126e4..db0de001a1 100644 --- a/.trinity/seals/claw_ClawGatesPins.json +++ b/.trinity/seals/claw_ClawGatesPins.json @@ -1,9 +1,9 @@ { - "built_by": "t27c-bootstrap@0.5.2+043cdea36", + "built_by": "t27c-bootstrap@0.5.2+9da2c5f", "closure": [ { "spec": "specs/claw/gates.t27", - "spec_hash": "sha256:2b49b0ada4cf5323472026378a33a07831bcc750ccc3a5a7b649e115dbcb6ab6" + "spec_hash": "sha256:3ae3db764f44ce4e0c45b8c7af6f17db39755e3c23be43c17a75409394a0a32d" } ], "config": "gen=c,rust,verilog,zig;tests=zig", @@ -14,7 +14,7 @@ "module": "ClawGatesPins", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T07:27:08Z", + "sealed_at": "2026-10-18T08:30:58Z", "sealed_by": "t27c-bootstrap@0.5.2", "spec_hash": "sha256:e18a7800770e75607f68b089aaa38880e490276fbe6706372b0e302bb990238c", "spec_path": "specs/claw/gates_pins.t27", diff --git a/.trinity/seals/claw_ClawService.json b/.trinity/seals/claw_ClawService.json index 53889e1b18..cf4abaeaa4 100644 --- a/.trinity/seals/claw_ClawService.json +++ b/.trinity/seals/claw_ClawService.json @@ -1,17 +1,22 @@ { - "built_by": "t27c-bootstrap@0.5.2+043cdea36", - "closure": [], + "built_by": "t27c-bootstrap@0.5.2+9da2c5f", + "closure": [ + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:eab393c71eb760e092ff07c51489fb2e5d67a6e8b25262ae7ebf5c95fbfb19a1" + } + ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:de9f70b57e80b4aa791a51d8ea356605a0c1c99611c7d25017654742fb5a6a39", - "gen_hash_rust": "sha256:5c38af81d488436c4cb4db98ade0c82a4a63a4fff1b9a4cfa6a24e68f89e391e", - "gen_hash_verilog": "sha256:4c83cf81d17f778a8a07ee15277e7a4685b479851f036f8048f9e8f35a46dbdf", - "gen_hash_zig": "sha256:23cdedf78a2b4e9a324ad4653ea746379a3beacfea7ded12fd1d4cd4cbf91f5a", + "gen_hash_c": "sha256:c27d8844f0ea70fe6dc6e9efd9a7dadb0d79b5b3ac0ba662e4512b4bdbfb607f", + "gen_hash_rust": "sha256:b0ab966055933dd64fe05f683cf39318dfb48efb28742611e3bd930afe95992e", + "gen_hash_verilog": "sha256:ceb8e2e602bbeef6fac81113c46ffe8a6cf765a722c3242dc2dcf46303b8a941", + "gen_hash_zig": "sha256:d565e6d8882e0661bb513be2c68b83443495d1960941c73b6dcdba753f4846e9", "module": "ClawService", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-18T07:27:09Z", + "sealed_at": "2026-10-18T08:30:58Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:e704897a637b01492a2608aad832cb63461cbdf6f45222cd0c46e8728764381e", + "spec_hash": "sha256:ad9dab3f1f4d513036c68cf6a866289cf78a87b814e4b0644cb3d06aa4d26a03", "spec_path": "specs/claw/service.t27", "tests": { "failed": 0, diff --git a/gen/c/claw/gates.c b/gen/c/claw/gates.c index 1e9bc51427..28d5a59adf 100644 --- a/gen/c/claw/gates.c +++ b/gen/c/claw/gates.c @@ -54,19 +54,19 @@ bool is_blocking(const char* name, size_t name_len); const char* events_of(const char* name, size_t name_len); uint32_t count_class(uint8_t c); bool every_row_filled(void); -bool str_eq(const char* a, size_t alen, const char* b, size_t blen); bool all_names_unique(void); bool all_classes_known(void); size_t line_len(uint8_t* dir_text, size_t dir_len, size_t start); bool line_eq_name(uint8_t* dir_text, size_t start, size_t len, const char* name, size_t name_len); bool dir_has(uint8_t* dir_text, size_t dir_len, const char* name, size_t name_len); bool line_in_names(uint8_t* dir_text, size_t start, size_t len); -size_t put_str(uint8_t* dst, size_t cap, size_t w, const char* s, size_t s_len); -uint8_t digit_byte(uint32_t d); -size_t put_count(uint8_t* dst, size_t cap, size_t w, uint32_t v); -bool buf_has(uint8_t* buf, size_t buf_len, const char* needle, size_t needle_len); int32_t claw_status(uint8_t* dir_text, size_t dir_len, uint8_t* out, size_t out_cap); size_t claw_coverage_report(uint8_t* out, size_t out_cap); +bool buf_has(uint8_t* buf, size_t buf_len, const char* needle, size_t needle_len); +uint8_t digit_byte(uint32_t d); +size_t put_count(uint8_t* dst, size_t cap, size_t w, uint32_t v); +size_t put_str(uint8_t* dst, size_t cap, size_t w, const char* s, size_t s_len); +bool str_eq(const char* a, size_t alen, const char* b, size_t blen); /* ------------------------------------------------------- Function implementations @@ -116,21 +116,6 @@ bool every_row_filled(void) { return true; } -bool str_eq(const char* a, size_t alen, const char* b, size_t blen) { - if ((alen != blen)) { - return false; - } - size_t diff = 0; - size_t i = 0; - while ((i < alen)) { - if ((a[i] != b[i])) { - diff += 1; - } - i += 1; - } - return (diff == 0); -} - bool all_names_unique(void) { for (size_t i = 0; i < 67; i++) { for (size_t j = (i + 1); j < 67; j++) { @@ -202,94 +187,6 @@ bool line_in_names(uint8_t* dir_text, size_t start, size_t len) { return false; } -size_t put_str(uint8_t* dst, size_t cap, size_t w, const char* s, size_t s_len) { - size_t i = 0; - while ((i < s_len)) { - if (((w + i) < cap)) { - dst[(w + i)] = s[i]; - } - i += 1; - } - return (w + s_len); -} - -uint8_t digit_byte(uint32_t d) { - uint8_t b = '0'; - if ((d == 1)) { - b = '1'; - } - if ((d == 2)) { - b = '2'; - } - if ((d == 3)) { - b = '3'; - } - if ((d == 4)) { - b = '4'; - } - if ((d == 5)) { - b = '5'; - } - if ((d == 6)) { - b = '6'; - } - if ((d == 7)) { - b = '7'; - } - if ((d == 8)) { - b = '8'; - } - if ((d == 9)) { - b = '9'; - } - return b; -} - -size_t put_count(uint8_t* dst, size_t cap, size_t w, uint32_t v) { - uint8_t tmp[16] = {0}; - size_t n = 0; - uint32_t x = v; - if ((x == 0)) { - tmp[0] = digit_byte(0); - n = 1; - } - while ((x > 0)) { - tmp[n] = digit_byte((x % 10)); - n += 1; - x = (x / 10); - } - size_t k = 0; - while ((k < n)) { - if (((w + k) < cap)) { - dst[(w + k)] = tmp[((n - 1) - k)]; - } - k += 1; - } - return (w + n); -} - -bool buf_has(uint8_t* buf, size_t buf_len, const char* needle, size_t needle_len) { - if ((needle_len > buf_len)) { - return false; - } - size_t start = 0; - while (((start + needle_len) <= buf_len)) { - size_t k = 0; - size_t diff = 0; - while ((k < needle_len)) { - if ((buf[(start + k)] != needle[k])) { - diff += 1; - } - k += 1; - } - if ((diff == 0)) { - return true; - } - start += 1; - } - return false; -} - int32_t claw_status(uint8_t* dir_text, size_t dir_len, uint8_t* out, size_t out_cap) { size_t w = 0; int32_t problems = 0; @@ -376,6 +273,109 @@ size_t claw_coverage_report(uint8_t* out, size_t out_cap) { return w; } +bool buf_has(uint8_t* buf, size_t buf_len, const char* needle, size_t needle_len) { + if ((needle_len > buf_len)) { + return false; + } + size_t start = 0; + while (((start + needle_len) <= buf_len)) { + size_t k = 0; + size_t diff = 0; + while ((k < needle_len)) { + if ((buf[(start + k)] != needle[k])) { + diff += 1; + } + k += 1; + } + if ((diff == 0)) { + return true; + } + start += 1; + } + return false; +} + +uint8_t digit_byte(uint32_t d) { + uint8_t b = '0'; + if ((d == 1)) { + b = '1'; + } + if ((d == 2)) { + b = '2'; + } + if ((d == 3)) { + b = '3'; + } + if ((d == 4)) { + b = '4'; + } + if ((d == 5)) { + b = '5'; + } + if ((d == 6)) { + b = '6'; + } + if ((d == 7)) { + b = '7'; + } + if ((d == 8)) { + b = '8'; + } + if ((d == 9)) { + b = '9'; + } + return b; +} + +size_t put_count(uint8_t* dst, size_t cap, size_t w, uint32_t v) { + uint8_t tmp[16] = {0}; + size_t n = 0; + uint32_t x = v; + if ((x == 0)) { + tmp[0] = digit_byte(0); + n = 1; + } + while ((x > 0)) { + tmp[n] = digit_byte((x % 10)); + n += 1; + x = (x / 10); + } + size_t k = 0; + while ((k < n)) { + if (((w + k) < cap)) { + dst[(w + k)] = tmp[((n - 1) - k)]; + } + k += 1; + } + return (w + n); +} + +size_t put_str(uint8_t* dst, size_t cap, size_t w, const char* s, size_t s_len) { + size_t i = 0; + while ((i < s_len)) { + if (((w + i) < cap)) { + dst[(w + i)] = s[i]; + } + i += 1; + } + return (w + s_len); +} + +bool str_eq(const char* a, size_t alen, const char* b, size_t blen) { + if ((alen != blen)) { + return false; + } + size_t diff = 0; + size_t i = 0; + while ((i < alen)) { + if ((a[i] != b[i])) { + diff += 1; + } + i += 1; + } + return (diff == 0); +} + /* ------------------------------------------------------- Tests ------------------------------------------------------- */ diff --git a/specs/claw/gates.t27 b/specs/claw/gates.t27 index f90a82dae0..066636f444 100644 --- a/specs/claw/gates.t27 +++ b/specs/claw/gates.t27 @@ -28,6 +28,8 @@ module ClawGates; +use claw::text; + /// A workflow class. G_UNKNOWN is deliberately far from the real classes so a /// partial read cannot mistake it for a verdict. pub const G_BLOCKING : u8 = 0; @@ -156,20 +158,6 @@ pub fn every_row_filled() -> bool { return true; } -/// Byte equality for two counted strings -- the same loop as affected.t27's -/// `same`. `==` on str does not lower element-to-element, and `.len` does not -/// lower under gen-c at all, so both lengths ride along as parameters. -pub fn str_eq(a: str, alen: usize, b: str, blen: usize) -> bool { - if (alen != blen) { return false; } - var diff : usize = 0; - var i : usize = 0; - while (i < alen) { - if (a[i] != b[i]) { diff += 1; } - i += 1; - } - return diff == 0; -} - pub fn all_names_unique() -> bool { for i in 0..67 { for j in (i + 1)..67 { @@ -273,76 +261,6 @@ fn line_in_names(dir_text : []const u8, start : usize, len : usize) -> bool { return false; } -/// Copy s_len bytes of s into dst at w, clipped at cap, and return the new -/// write cursor -- the gen/c/policy write idiom: a clipped write still -/// advances the cursor so the caller's arithmetic stays honest, only the -/// bytes past cap are dropped. -fn put_str(dst : []u8, cap : usize, w : usize, s : str, s_len : usize) -> usize { - var i : usize = 0; - while (i < s_len) { - if (w + i < cap) { dst[w + i] = s[i]; } - i += 1; - } - return w + s_len; -} - -fn digit_byte(d : u32) -> u8 { - var b : u8 = '0'; - if (d == 1) { b = '1'; } - if (d == 2) { b = '2'; } - if (d == 3) { b = '3'; } - if (d == 4) { b = '4'; } - if (d == 5) { b = '5'; } - if (d == 6) { b = '6'; } - if (d == 7) { b = '7'; } - if (d == 8) { b = '8'; } - if (d == 9) { b = '9'; } - return b; -} - -/// Decimal digits of a count, into the out buffer, snprintf-style. Written -/// with digit_byte rather than '0' + digit because the gen-c backend lowers -/// str to const char* and a u8 arithmetic assignment has no precedent to -/// lean on; every construct here is one gates.t27 already lowers. -fn put_count(dst : []u8, cap : usize, w : usize, v : u32) -> usize { - var tmp : [16]u8 = undefined; - var n : usize = 0; - var x : u32 = v; - if (x == 0) { - tmp[0] = digit_byte(0); - n = 1; - } - while (x > 0) { - tmp[n] = digit_byte(x % 10); - n += 1; - x = x / 10; - } - var k : usize = 0; - while (k < n) { - if (w + k < cap) { dst[w + k] = tmp[n - 1 - k]; } - k += 1; - } - return w + n; -} - -/// Byte-scan of a written buffer for a needle -- the same loop as -/// line_eq_name, over the whole report instead of one line. -fn buf_has(buf : []const u8, buf_len : usize, needle : str, needle_len : usize) -> bool { - if (needle_len > buf_len) { return false; } - var start : usize = 0; - while (start + needle_len <= buf_len) { - var k : usize = 0; - var diff : usize = 0; - while (k < needle_len) { - if (buf[start + k] != needle[k]) { diff += 1; } - k += 1; - } - if (diff == 0) { return true; } - start += 1; - } - return false; -} - /// The audit: diff the directory listing against this inventory. dir_text is /// `ls` output, one workflow stem per line; the report text lands in out; the /// return value is the problem count -- the runner's exit code, 0 clean diff --git a/specs/claw/preview.t27 b/specs/claw/preview.t27 index 3916785c4c..0677d47c17 100644 --- a/specs/claw/preview.t27 +++ b/specs/claw/preview.t27 @@ -38,7 +38,7 @@ module ClawPreview; -use claw::recipes; +use claw::text; /// Deployment states. S_NONE is "no deployment yet" and also the answer to an illegal move, /// kept far from the real states so a missed check cannot pass for a state. @@ -125,17 +125,6 @@ pub fn description_ok(d : str) -> bool { return d.len > 0 && d.len <= DESCRIPTION_MAX; } -/// Does `s` start with `p`? -pub fn starts_with(s : str, p : str) -> bool { - if (s.len < p.len) { return false; } - var i : usize = 0; - while (i < p.len) { - if (s[i] != p[i]) { return false; } - i += 1; - } - return true; -} - /// Does `s` end with `p`? pub fn ends_with(s : str, p : str) -> bool { if (s.len < p.len) { return false; } @@ -150,7 +139,7 @@ pub fn ends_with(s : str, p : str) -> bool { /// Is `path` a .t27 file under specs/? The name part must be non-empty (".t27" alone is not). pub fn is_spec_path(path : str) -> bool { - return starts_with(path, "specs/") && ends_with(path, ".t27") && path.len > 10; + return has_prefix(path, path.len, "specs/", 6) && ends_with(path, ".t27") && path.len > 10; } /// How many lines of `list` are spec paths. `list` is the changed files, one path per line. @@ -194,7 +183,7 @@ pub fn env_name_ok(pr : u32, name : str) -> bool { if (pr == 0) { return false; } const d : usize = digits_of(pr); if (name.len != ENV_PREFIX.len + d) { return false; } - if (!starts_with(name, ENV_PREFIX)) { return false; } + if (!has_prefix(name, name.len, ENV_PREFIX, ENV_PREFIX.len)) { return false; } var p : u32 = 1; var k : usize = 1; while (k < d) { @@ -251,7 +240,7 @@ pub fn sha_at(s : str, at : usize) -> bool { /// A preview URL is the page, with repo=, a 40-hex sha= and path= in the query. A URL that does /// not pin the commit is refused: a branch name would show tomorrow's spec under today's check. pub fn url_ok(url : str) -> bool { - if (!starts_with(url, PAGE)) { return false; } + if (!has_prefix(url, url.len, PAGE, PAGE.len)) { return false; } if (find_from(url, "repo=", PAGE.len) < 0) { return false; } if (find_from(url, "path=specs/", PAGE.len) < 0) { return false; } const at : i32 = find_from(url, "sha=", PAGE.len); @@ -301,7 +290,7 @@ test the_lifecycle_walks_queued_to_success { assert s == S_SUCCESS; s = next_state(s, E_SUPERSEDE); assert s == S_INACTIVE; - assert recipes::str_eq(state_name(s), "inactive") == true; + assert state_name(s) == "inactive"; } test a_failed_probe_ends_in_failure_and_failure_can_retire { @@ -436,10 +425,10 @@ test descriptions_fit_the_api_limit { } test the_surface_slug_and_state_names_are_the_api_spelling { - assert recipes::str_eq(SURFACE, "t27f-preview") == true; - assert recipes::str_eq(state_name(S_QUEUED), "queued") == true; - assert recipes::str_eq(state_name(S_IN_PROGRESS), "in_progress") == true; - assert recipes::str_eq(state_name(S_SUCCESS), "success") == true; - assert recipes::str_eq(state_name(S_FAILURE), "failure") == true; - assert recipes::str_eq(state_name(S_NONE), "") == true; + assert SURFACE == "t27f-preview"; + assert state_name(S_QUEUED) == "queued"; + assert state_name(S_IN_PROGRESS) == "in_progress"; + assert state_name(S_SUCCESS) == "success"; + assert state_name(S_FAILURE) == "failure"; + assert state_name(S_NONE) == ""; } diff --git a/specs/claw/sarif.t27 b/specs/claw/sarif.t27 index 53deb54e6d..ca5406e4f6 100644 --- a/specs/claw/sarif.t27 +++ b/specs/claw/sarif.t27 @@ -32,8 +32,9 @@ ; -- ; gen-c lowers str to const char* and cannot express .len, so every string carries its length ; explicitly (the specs/claw/gates.t27 idiom); the pins that tie each length to the bytes live in -; specs/claw/sarif_pins.t27, which is never passed to gen-c. put_str, digit_byte and put_count -; are the gates.t27 writers, copied because they are private there and this file must lower alone. +; specs/claw/sarif_pins.t27, which is never passed to gen-c. put_str, digit_byte, put_count and +; buf_has are the shared writers of specs/claw/text.t27; `use claw::text;` splices them in before +; gen-c runs, so this file still lowers alone without a second copy of them. ; The honest limit: this shapes a document that follows the SARIF 2.1.0 fields GitHub reads. It ; does not prove GitHub accepts a given upload (size and result limits are GitHub's), and t27c ; does not print the TSV above today -- the adapter from its output is the wiring step. @@ -41,6 +42,8 @@ module ClawSarif; +use claw::text; + /// Severity classes, in SARIF level order. SEV_NONE is deliberately far from the real values so /// an unknown word cannot be mistaken for a level. pub const SEV_ERROR : u8 = 0; @@ -79,17 +82,6 @@ pub const DEFAULT_MAX_RESULTS : u32 = 1000; /// Longest message kept, in bytes. pub const MSG_MAX : usize = 500; -/// Copy s_len bytes of s into dst at w, clipped at cap; the cursor still advances, so a clipped -/// document is detected by comparing the final cursor with cap (sarif_fits). -fn put_str(dst : []u8, cap : usize, w : usize, s : str, s_len : usize) -> usize { - var i : usize = 0; - while (i < s_len) { - if (w + i < cap) { dst[w + i] = s[i]; } - i += 1; - } - return w + s_len; -} - fn put_byte(dst : []u8, cap : usize, w : usize, b : u8) -> usize { if (w < cap) { dst[w] = b; } return w + 1; @@ -110,41 +102,6 @@ fn put_key(dst : []u8, cap : usize, w : usize, s : str, n : usize) -> usize { return x; } -fn digit_byte(d : u32) -> u8 { - var b : u8 = '0'; - if (d == 1) { b = '1'; } - if (d == 2) { b = '2'; } - if (d == 3) { b = '3'; } - if (d == 4) { b = '4'; } - if (d == 5) { b = '5'; } - if (d == 6) { b = '6'; } - if (d == 7) { b = '7'; } - if (d == 8) { b = '8'; } - if (d == 9) { b = '9'; } - return b; -} - -fn put_count(dst : []u8, cap : usize, w : usize, v : u32) -> usize { - var tmp : [16]u8 = undefined; - var n : usize = 0; - var x : u32 = v; - if (x == 0) { - tmp[0] = digit_byte(0); - n = 1; - } - while (x > 0) { - tmp[n] = digit_byte(x % 10); - n += 1; - x = x / 10; - } - var k : usize = 0; - while (k < n) { - if (w + k < cap) { dst[w + k] = tmp[n - 1 - k]; } - k += 1; - } - return w + n; -} - /// Message text, escaped: `"` and `\` get a backslash, every byte outside printable ASCII /// becomes `?`, and at most maxn bytes are read; a longer message ends in `...`. fn put_json_text(dst : []u8, cap : usize, w : usize, text : []const u8, s : usize, e : usize, maxn : usize) -> usize { @@ -476,23 +433,6 @@ pub fn sarif_fits(needed : usize, out_cap : usize) -> bool { return needed <= out_cap; } -/// Byte-scan of a written buffer for a needle (the gates.t27 buf_has loop). -fn buf_has(buf : []const u8, buf_len : usize, needle : str, needle_len : usize) -> bool { - if (needle_len > buf_len) { return false; } - var start : usize = 0; - while (start + needle_len <= buf_len) { - var k : usize = 0; - var diff : usize = 0; - while (k < needle_len) { - if (buf[start + k] != needle[k]) { diff += 1; } - k += 1; - } - if (diff == 0) { return true; } - start += 1; - } - return false; -} - /// Brackets and braces of a written document pair up outside strings, and it ends closed: the /// cheap structural check a valid-JSON claim can stand on without a parser. fn balanced(buf : []const u8, n : usize) -> bool { diff --git a/specs/claw/sarif_pins.t27 b/specs/claw/sarif_pins.t27 index 61d0e2271e..a9f310334e 100644 --- a/specs/claw/sarif_pins.t27 +++ b/specs/claw/sarif_pins.t27 @@ -12,16 +12,7 @@ module ClawSarifPins; use claw::sarif; - -fn same(a : str, b : str) -> bool { - if (a.len != b.len) { return false; } - var i : usize = 0; - while (i < a.len) { - if (a[i] != b[i]) { return false; } - i += 1; - } - return true; -} +use claw::text; fn len_is(n : usize, s : str) -> bool { return n == s.len; @@ -46,18 +37,12 @@ test url_lens_pin_their_strings { assert len_is(sarif::INFO_URL_LEN, sarif::INFO_URL); } -test same_tells_equal_from_different { - assert same("abc", "abc") == true; - assert same("abc", "abd") == false; - assert same("abc", "ab") == false; -} - test rule_ids_and_slugs_are_unique_so_an_alert_key_is_one_rule { for i in 0..4 { for j in 0..4 { if (i != j) { - assert same(sarif::RULE_IDS[i], sarif::RULE_IDS[j]) == false; - assert same(sarif::RULE_SLUGS[i], sarif::RULE_SLUGS[j]) == false; + assert str_eq(sarif::RULE_IDS[i], sarif::RULE_IDS[i].len, sarif::RULE_IDS[j], sarif::RULE_IDS[j].len) == false; + assert str_eq(sarif::RULE_SLUGS[i], sarif::RULE_SLUGS[i].len, sarif::RULE_SLUGS[j], sarif::RULE_SLUGS[j].len) == false; } } } diff --git a/specs/claw/service.t27 b/specs/claw/service.t27 index 85701115d7..f963136952 100644 --- a/specs/claw/service.t27 +++ b/specs/claw/service.t27 @@ -20,6 +20,8 @@ ; phi^2 + 1/phi^2 = 3 | TRINITY module ClawService; +use claw::text; + /// The four modes of the service. audit: diff the consumer's workflows directory against the gate /// inventory, exit code below. report: the markdown table of the inventory, for a PR body or a step /// summary. matrix: the raw class/name/events rows. t27f: the seal-gated T27F check-run the App posts @@ -55,19 +57,9 @@ pub fn mode_valid(m : str) -> bool { return m == MODE_AUDIT or m == MODE_REPORT or m == MODE_MATRIX or m == MODE_T27F; } -pub fn has_prefix(s : str, p : str) -> bool { - if (s.len < p.len) { return false; } - var i : usize = 0; - while (i < p.len) { - if (s[i] != p[i]) { return false; } - i += 1; - } - return true; -} - /// A consumer pins the reusable workflow to a tag of this family; anything else is not a release. pub fn is_release_tag(t : str) -> bool { - return has_prefix(t, TAG_PREFIX); + return has_prefix(t, t.len, TAG_PREFIX, TAG_PREFIX.len); } /// Audit mode's verdict from the three diff counts. Unreadable wins over drift: a missing @@ -111,10 +103,10 @@ test default_mode_is_audit_and_audit_is_valid { test has_prefix_matches_and_rejects { const tag : str = "claw-v0.1"; - assert(has_prefix(tag, TAG_PREFIX) == true); - assert(has_prefix("v0.1", TAG_PREFIX) == false); - assert(has_prefix("", TAG_PREFIX) == false); - assert(has_prefix("claw", "claw-v") == false); + assert(has_prefix(tag, tag.len, TAG_PREFIX, TAG_PREFIX.len) == true); + assert(has_prefix("v0.1", 4, TAG_PREFIX, TAG_PREFIX.len) == false); + assert(has_prefix("", 0, TAG_PREFIX, TAG_PREFIX.len) == false); + assert(has_prefix("claw", 4, "claw-v", 6) == false); } test is_release_tag_accepts_only_the_tag_family { @@ -163,7 +155,7 @@ test class_labels_cover_every_class_and_fail_closed { test check_prefix_namespaced { const check_run : str = "claw/matrix-drift"; - assert(has_prefix(check_run, CHECK_PREFIX) == true); - assert(has_prefix("matrix-drift", CHECK_PREFIX) == false); - assert(has_prefix("claw/t27f", CHECK_PREFIX) == true); + assert(has_prefix(check_run, check_run.len, CHECK_PREFIX, CHECK_PREFIX.len) == true); + assert(has_prefix("matrix-drift", 12, CHECK_PREFIX, CHECK_PREFIX.len) == false); + assert(has_prefix("claw/t27f", 9, CHECK_PREFIX, CHECK_PREFIX.len) == true); } diff --git a/specs/claw/suggest.t27 b/specs/claw/suggest.t27 index 1a9bd8c5a4..32bfc227bf 100644 --- a/specs/claw/suggest.t27 +++ b/specs/claw/suggest.t27 @@ -40,6 +40,8 @@ module ClawSuggest; +use claw::text; + /// The only review event this module ever lets through. pub const EVENT : str = "COMMENT"; /// The only side a suggestion anchors on. @@ -64,18 +66,6 @@ pub const V_CAP : u8 = 2; pub const V_TOO_BIG : u8 = 3; pub const V_UNKNOWN : u8 = 255; -/// Byte equality for two counted strings (the idiom of specs/claw/gates.t27). -pub fn str_eq(a : str, alen : usize, b : str, blen : usize) -> bool { - if (alen != blen) { return false; } - var diff : usize = 0; - var i : usize = 0; - while (i < alen) { - if (a[i] != b[i]) { diff += 1; } - i += 1; - } - return diff == 0; -} - /// Law 1. The event a review may carry: "COMMENT" when COMMENT was asked for, "" otherwise. /// "" means the caller posts nothing. Nothing else can be returned, so no input reaches APPROVE. pub fn event_for(requested : str, requested_len : usize) -> str { diff --git a/specs/claw/text.t27 b/specs/claw/text.t27 new file mode 100644 index 0000000000..564d87f9b9 --- /dev/null +++ b/specs/claw/text.t27 @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/claw/text.t27 -- the byte-text helpers the TRI CLAW specs share, written once (#10317). +; Before this file gates.t27 and sarif.t27 each carried put_str, digit_byte, put_count and +; buf_has; gates.t27 and suggest.t27 each carried str_eq; service.t27 and preview.t27 each carried +; a prefix test; sarif_pins.t27 carried a byte-equality loop. tools/dupe_scan.py counted them as +; copies of the same bodies (#10311), and sarif.t27 said it copied them "because this file must +; lower alone". It lowers alone still: `use claw::text;` is spliced in by use_resolve before every +; backend (gen-c, gen-rust, gen) runs, so the importer's C holds the helpers it names and nothing +; else, and no hand-written second copy is needed. +; -- +; Every string here is COUNTED: the length rides along as a parameter, never read as `.len`, +; because gen-c lowers str to const char* and cannot express `.len` (the specs/claw/gates.t27 +; idiom). That is also why this file lowers under gen-c on its own. A caller that holds a literal +; passes its length; a caller in a zig-runner-only spec may pass `s.len`. +; -- +; The writers share one contract with the gen/c/policy write idiom: a write returns the new +; cursor, a clipped write still advances the cursor, only the bytes past cap are dropped, so the +; caller detects truncation by comparing the final cursor with cap. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module ClawText; + +/// Copy s_len bytes of s into dst at w, clipped at cap, and return the new write cursor -- the +/// gen/c/policy write idiom: a clipped write still advances the cursor so the caller's arithmetic +/// stays honest, only the bytes past cap are dropped. +pub fn put_str(dst : []u8, cap : usize, w : usize, s : str, s_len : usize) -> usize { + var i : usize = 0; + while (i < s_len) { + if (w + i < cap) { dst[w + i] = s[i]; } + i += 1; + } + return w + s_len; +} + +pub fn digit_byte(d : u32) -> u8 { + var b : u8 = '0'; + if (d == 1) { b = '1'; } + if (d == 2) { b = '2'; } + if (d == 3) { b = '3'; } + if (d == 4) { b = '4'; } + if (d == 5) { b = '5'; } + if (d == 6) { b = '6'; } + if (d == 7) { b = '7'; } + if (d == 8) { b = '8'; } + if (d == 9) { b = '9'; } + return b; +} + +/// Decimal digits of a count, into the out buffer, snprintf-style. Written with digit_byte rather +/// than '0' + digit because the gen-c backend lowers str to const char* and a u8 arithmetic +/// assignment has no precedent to lean on; every construct here is one gates.t27 already lowers. +pub fn put_count(dst : []u8, cap : usize, w : usize, v : u32) -> usize { + var tmp : [16]u8 = undefined; + var n : usize = 0; + var x : u32 = v; + if (x == 0) { + tmp[0] = digit_byte(0); + n = 1; + } + while (x > 0) { + tmp[n] = digit_byte(x % 10); + n += 1; + x = x / 10; + } + var k : usize = 0; + while (k < n) { + if (w + k < cap) { dst[w + k] = tmp[n - 1 - k]; } + k += 1; + } + return w + n; +} + +/// Byte-scan of a written buffer for a needle -- the same loop as line_eq_name, over the whole +/// report instead of one line. +pub fn buf_has(buf : []const u8, buf_len : usize, needle : str, needle_len : usize) -> bool { + if (needle_len > buf_len) { return false; } + var start : usize = 0; + while (start + needle_len <= buf_len) { + var k : usize = 0; + var diff : usize = 0; + while (k < needle_len) { + if (buf[start + k] != needle[k]) { diff += 1; } + k += 1; + } + if (diff == 0) { return true; } + start += 1; + } + return false; +} + +/// Byte equality for two counted strings -- the same loop as affected.t27's `same`. `==` on str +/// does not lower element-to-element, and `.len` does not lower under gen-c at all, so both +/// lengths ride along as parameters. +pub fn str_eq(a : str, alen : usize, b : str, blen : usize) -> bool { + if (alen != blen) { return false; } + var diff : usize = 0; + var i : usize = 0; + while (i < alen) { + if (a[i] != b[i]) { diff += 1; } + i += 1; + } + return diff == 0; +} + +/// Does s start with p? Both counted. A prefix longer than the string is never a prefix, and the +/// empty prefix is a prefix of everything. +pub fn has_prefix(s : str, s_len : usize, p : str, p_len : usize) -> bool { + if (s_len < p_len) { return false; } + var i : usize = 0; + while (i < p_len) { + if (s[i] != p[i]) { return false; } + i += 1; + } + return true; +} + +test digit_byte_is_the_ascii_digit { + assert digit_byte(0) == '0'; + assert digit_byte(7) == '7'; + assert digit_byte(9) == '9'; +} + +test put_str_copies_and_returns_the_cursor { + var out : [16]u8 = undefined; + var w : usize = put_str(&out, 16, 0, "abc", 3); + assert w == 3; + w = put_str(&out, 16, w, "de", 2); + assert w == 5; + assert buf_has(&out, w, "abcde", 5) == true; + assert buf_has(&out, w, "abcdf", 5) == false; +} + +test put_str_clipped_write_still_advances_the_cursor { + var out : [4]u8 = undefined; + var w : usize = put_str(&out, 4, 0, "abcdef", 6); + // The cursor says six bytes were asked for; only four fit, and the caller can tell. + assert w == 6; + assert buf_has(&out, 4, "abcd", 4) == true; +} + +test put_count_writes_decimal_digits { + var out : [16]u8 = undefined; + var w : usize = put_count(&out, 16, 0, 0); + assert w == 1; + assert buf_has(&out, w, "0", 1) == true; + w = put_count(&out, 16, 0, 7); + assert w == 1; + assert buf_has(&out, w, "7", 1) == true; + w = put_count(&out, 16, 0, 1050); + assert w == 4; + assert buf_has(&out, w, "1050", 4) == true; + assert buf_has(&out, w, "1051", 4) == false; +} + +test buf_has_scans_every_start_and_rejects_a_long_needle { + var out : [16]u8 = undefined; + var w : usize = put_str(&out, 16, 0, "n=62 n=3", 8); + assert buf_has(&out, w, "n=62", 4) == true; + assert buf_has(&out, w, "n=3", 3) == true; + assert buf_has(&out, w, "n=63", 4) == false; + assert buf_has(&out, w, "n=62 n=3 more", 13) == false; +} + +test str_eq_tells_equal_from_different { + assert str_eq("abc", 3, "abc", 3) == true; + assert str_eq("abc", 3, "abd", 3) == false; + assert str_eq("abc", 3, "ab", 2) == false; + assert str_eq("", 0, "", 0) == true; +} + +test has_prefix_matches_and_rejects { + assert has_prefix("claw-v0.1.0", 11, "claw-v", 6) == true; + assert has_prefix("v0.1", 4, "claw-v", 6) == false; + assert has_prefix("claw", 4, "claw-v", 6) == false; + assert has_prefix("", 0, "claw-v", 6) == false; + assert has_prefix("anything", 8, "", 0) == true; +} diff --git a/tools/duplicate_bodies_baseline.txt b/tools/duplicate_bodies_baseline.txt index 13a97657b5..151dae65ca 100644 --- a/tools/duplicate_bodies_baseline.txt +++ b/tools/duplicate_bodies_baseline.txt @@ -35,7 +35,6 @@ match_at 4 neuron4 4 append 3 assign 3 -bytes_eq 3 category 3,2 check_balanced_braces 3 copy_bytes_at 3 @@ -79,10 +78,10 @@ bit_at 2 bits 2 booth_mul_i16 2 booth_mul_u32_inner 2 -buf_has 2 build_report_frame 2 bytes32_eq 2 bytes64_eq 2 +bytes_eq 2 carries_src2 2 charged 2 concat 2,2 @@ -94,7 +93,6 @@ cordic_z_next 2 counter_24bit 2 debug_state_bits 2 deque_clear 2 -digit_byte 2 dot4 2 effect_wanted 2 encode 2 @@ -105,7 +103,6 @@ generate_next_token_from_logits 2 get 2 hard_sigmoid 2 has_at 2 -has_prefix 2 has_star_inner 2 hex_digit 2 hw_place_before 2 @@ -134,10 +131,8 @@ ordinary_if 2 pipeline_f32 2 price_tokens 2 put 2 -put_count 2 put_lit 2 put_range 2 -put_str 2 remove 2 runRailwayLogsCommand 2 run_die_level_with 2 @@ -152,7 +147,6 @@ slices_equal 2 starts_with 2 startup_config 2 step_ef 2 -str_eq 2 text_equal 2 trit_add 2 trit_compare 2