diff --git a/.trinity/seals/loop_TriLoopLesson.json b/.trinity/seals/loop_TriLoopLesson.json index 24999695c6..4e660a2f84 100644 --- a/.trinity/seals/loop_TriLoopLesson.json +++ b/.trinity/seals/loop_TriLoopLesson.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+0fba674ef", + "built_by": "t27c-bootstrap@0.5.2+42a4860c4", "closure": [ { "spec": "specs/claw/text.t27", @@ -11,23 +11,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:b43f8e5932e50e39962700c70d21c804ecac1296da76238bc38a6d908d1442be", - "gen_hash_rust": "sha256:5b03509aa928f0f6ae57f16317efdbab2ce35120e04299cc7c68ab01c7628a95", - "gen_hash_verilog": "sha256:3d1754b8cb60953c8f360d30ccef8482f51efa3e495137efeab74f35945fd6c0", - "gen_hash_zig": "sha256:3351777f5fc3125f092cf3882414ee7a46582ca37ea287cecbe72c77b27f58e0", + "gen_hash_c": "sha256:d563caba34950b1a7eb7540c3db655b14387e7cb7c5044d91b2b7b5ed5dc44ac", + "gen_hash_rust": "sha256:2c51a212ceca517b28dc3af6c034acbbdafce4278c7076a7f0abe43b83cb4b26", + "gen_hash_verilog": "sha256:4b059e9731893ebc3e894960fa912994555d860e2fee9d2071f03fb864a44060", + "gen_hash_zig": "sha256:dda4a7b05cf7f6cdd974333cc253cf7fd40467fc883e7ba0f4e65d9d5d52925f", "module": "TriLoopLesson", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T16:23:39Z", + "sealed_at": "2026-10-19T17:26:43Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:9964c79e37666e3673d2d32b4d98bb0a4b2d9639f5ff3a9d8372f4f4fa1cadcb", + "spec_hash": "sha256:2a7e5e4e999c5418374f2987c45e6bfbb7b542c05c4f24e0ad185cd65b1bcb53", "spec_path": "specs/tri/loop/lesson.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 21, - "total": 21, + "passed": 22, + "total": 22, "vacuous": 0 }, "toolchain": { diff --git a/gen/rust/tri/loop/lesson.rs b/gen/rust/tri/loop/lesson.rs index ffa88acc0e..a0762502df 100644 --- a/gen/rust/tri/loop/lesson.rs +++ b/gen/rust/tri/loop/lesson.rs @@ -1,6 +1,6 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 21 test(s), 2 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 22 test(s), 2 invariant(s), 0 function(s) with no body. // This backend emits declarations only. The spec's checks live in // the Zig and Verilog outputs; do not read this file as verified. @@ -396,11 +396,39 @@ pub fn fence_char(f: &'static str, at: usize, stop: usize) -> u8 { return 0; } -pub fn fence_after(fence: u8, fc: u8) -> u8 { +pub fn fence_len(f: &'static str, at: usize, stop: usize) -> usize { + let c: u8 = fence_char(f, at, stop); + if (c == 0) { + return 0; + } + let mut k: usize = at; + while (((k < stop) && (k < (at + 3))) && (f.as_bytes()[(k) as usize] == 32)) { + k = (k + 1); + } + let mut n: usize = 0; + while (((k + n) < stop) && (f.as_bytes()[((k + n)) as usize] == c)) { + n = (n + 1); + } + return n; +} + +pub fn fence_bare(f: &'static str, at: usize, stop: usize) -> bool { + let n: usize = fence_len(f, at, stop); + if (n == 0) { + return false; + } + let mut k: usize = at; + while (((k < stop) && (k < (at + 3))) && (f.as_bytes()[(k) as usize] == 32)) { + k = (k + 1); + } + return blank_line(f, (k + n), stop); +} + +pub fn fence_after(fence: u8, fc: u8, closes: bool) -> u8 { if (fence == 0) { return fc; } - if (fc == fence) { + if ((fc == fence) && closes) { return 0; } return fence; @@ -433,13 +461,17 @@ pub fn is_traps(f: &'static str, at: usize, stop: usize) -> bool { pub fn trap_line(f: &'static str) -> usize { let mut at: usize = 0; let mut fence: u8 = 0; + let mut flen: usize = 0; while (at < f.len()) { let stop: usize = line_end(f, at); let fc: u8 = fence_char(f, at, stop); if ((((fence == 0) && (fc == 0)) && (heading_level(f, at, stop) > 0)) && is_traps(f, at, stop)) { return at; } - fence = fence_after(fence, fc); + if (fence == 0) { + flen = fence_len(f, at, stop); + } + fence = fence_after(fence, fc, (fence_bare(f, at, stop) && (fence_len(f, at, stop) >= flen))); at = next_line(f, at); } return f.len(); @@ -449,6 +481,7 @@ pub fn section_end(f: &'static str, h: usize) -> usize { let level: usize = heading_level(f, h, line_end(f, h)); let mut at: usize = next_line(f, h); let mut fence: u8 = 0; + let mut flen: usize = 0; while (at < f.len()) { let stop: usize = line_end(f, at); let fc: u8 = fence_char(f, at, stop); @@ -458,7 +491,10 @@ pub fn section_end(f: &'static str, h: usize) -> usize { return at; } } - fence = fence_after(fence, fc); + if (fence == 0) { + flen = fence_len(f, at, stop); + } + fence = fence_after(fence, fc, (fence_bare(f, at, stop) && (fence_len(f, at, stop) >= flen))); at = next_line(f, at); } return f.len(); @@ -790,6 +826,14 @@ pub const G: &'static str = "## Traps\n\n- 2026-10-19 use curl (#5)\n- tls\n- pl pub const BL: &'static str = "- 2026-10-19 use curl (#5)"; +pub const FX1: &'static str = "intro\n```\ncode\n``` numbers\nmore code\n```\n## Traps\n\n- 2026-10-19 one lesson that is long enough\n"; + +pub const FX2: &'static str = "````\n```\n## Traps\n````\n## Traps\n\n- x lesson that is long enough\n"; + +pub const FX3: &'static str = "```\n``` numbers\ntext\n```\n"; + +pub const FX4: &'static str = "```\n``` numbers\ntext\n```\n\n## Traps\n\n- 2026-10-19 a new lesson that is long enough to keep\n"; + pub fn put_str(dst: &mut [u8], cap: usize, w: usize, s: &'static str, s_len: usize) -> usize { let mut i: usize = 0; while (i < s_len) { diff --git a/specs/tri/loop/lesson.t27 b/specs/tri/loop/lesson.t27 index a6f5aa0e63..f131fe8be1 100644 --- a/specs/tri/loop/lesson.t27 +++ b/specs/tri/loop/lesson.t27 @@ -35,7 +35,9 @@ ; - otherwise ADD, or ADD_SECTION when the file has no Traps section and one is created at the end. ; -- ; The Traps section is the first heading, at any level, whose title opens with `Traps` or `Trap` (so -; `## Traps`, `## Traps that cost hours` and `## Traps met on this Mac`), outside a code fence. It runs to the +; `## Traps`, `## Traps that cost hours` and `## Traps met on this Mac`), outside a code fence (a fence closes +; on a bare line of the same character at least as long as its opener, as on GitHub, so "``` numbers" inside a +; block is text and one odd line cannot hide the section, #11110). It runs to the ; next heading of the same or a higher level, or to the end of the file. A row goes after the last line of ; the section that is not blank; a section with nothing in it gets a blank line first. ; The row is `- ()`, with no ` ()` when the ref is empty, then a newline. @@ -384,10 +386,38 @@ pub fn fence_char(f: string, at: usize, stop: usize) -> u8 { return 0; } -/// The fence state after a line: outside (0) a line that opens a fence enters it, inside a line with the same fence leaves it. -pub fn fence_after(fence: u8, fc: u8) -> u8 { +/// The length of the run of fence characters that opens the line (up to three spaces before it); 0 when the line is no fence line. +pub fn fence_len(f: string, at: usize, stop: usize) -> usize { + const c : u8 = fence_char(f, at, stop); + if (c == 0) { return 0; } + var k : usize = at; + while (k < stop and k < at + 3 and f[k] == 32) { + k = k + 1; + } + var n : usize = 0; + while (k + n < stop and f[k + n] == c) { + n = n + 1; + } + return n; +} + +/// A fence line with nothing after its run but spaces, tabs and carriage returns. A fence line with an info string opens a block and +/// can never close one, so "``` numbers" inside a block is text. +pub fn fence_bare(f: string, at: usize, stop: usize) -> bool { + const n : usize = fence_len(f, at, stop); + if (n == 0) { return false; } + var k : usize = at; + while (k < stop and k < at + 3 and f[k] == 32) { + k = k + 1; + } + return blank_line(f, k + n, stop); +} + +/// The fence state after a line: outside (0) a line that opens a fence enters it; inside, a line leaves it only when it is the same +/// fence (`closes`: bare and at least as long as the opener, as on GitHub). +pub fn fence_after(fence: u8, fc: u8, closes: bool) -> u8 { if (fence == 0) { return fc; } - if (fc == fence) { return 0; } + if (fc == fence and closes) { return 0; } return fence; } @@ -412,11 +442,13 @@ pub fn is_traps(f: string, at: usize, stop: usize) -> bool { pub fn trap_line(f: string) -> usize { var at : usize = 0; var fence : u8 = 0; + var flen : usize = 0; while (at < f.len) { const stop : usize = line_end(f, at); const fc : u8 = fence_char(f, at, stop); if (fence == 0 and fc == 0 and heading_level(f, at, stop) > 0 and is_traps(f, at, stop)) { return at; } - fence = fence_after(fence, fc); + if (fence == 0) { flen = fence_len(f, at, stop); } + fence = fence_after(fence, fc, fence_bare(f, at, stop) and fence_len(f, at, stop) >= flen); at = next_line(f, at); } return f.len; @@ -427,6 +459,7 @@ pub fn section_end(f: string, h: usize) -> usize { const level : usize = heading_level(f, h, line_end(f, h)); var at : usize = next_line(f, h); var fence : u8 = 0; + var flen : usize = 0; while (at < f.len) { const stop : usize = line_end(f, at); const fc : u8 = fence_char(f, at, stop); @@ -434,7 +467,8 @@ pub fn section_end(f: string, h: usize) -> usize { const l : usize = heading_level(f, at, stop); if (l > 0 and l <= level) { return at; } } - fence = fence_after(fence, fc); + if (fence == 0) { flen = fence_len(f, at, stop); } + fence = fence_after(fence, fc, fence_bare(f, at, stop) and fence_len(f, at, stop) >= flen); at = next_line(f, at); } return f.len; @@ -897,6 +931,28 @@ test the_section_is_found_and_bounded { assert add(F, "second lesson that is long enough", "") == O_DUPLICATE; } +const FX1 : string = "intro\n```\ncode\n``` numbers\nmore code\n```\n## Traps\n\n- 2026-10-19 one lesson that is long enough\n"; +const FX2 : string = "````\n```\n## Traps\n````\n## Traps\n\n- x lesson that is long enough\n"; +const FX3 : string = "```\n``` numbers\ntext\n```\n"; +const FX4 : string = "```\n``` numbers\ntext\n```\n\n## Traps\n\n- 2026-10-19 a new lesson that is long enough to keep\n"; + +// Scenario (#11110): a fence line with an info string does not close a fence, a shorter fence line does not close a longer one, so a +// file with one such line keeps its Traps section findable and a second lesson is a plain ADD, not a second heading. +test an_info_string_fence_line_does_not_close_a_fence { + assert fence_len("```rust", 0, 7) == 3 and fence_len(" ~~~~", 0, 6) == 4 and fence_len("text", 0, 4) == 0 and fence_len(" ```", 0, 7) == 0; + assert fence_bare("```", 0, 3) and fence_bare("``` ", 0, 5) and fence_bare("~~~~", 0, 4); + assert !fence_bare("``` numbers", 0, 11) and !fence_bare("```rust", 0, 7) and !fence_bare("text", 0, 4); + assert fence_after(96, 96, false) == 96 and fence_after(96, 96, true) == 0 and fence_after(96, 126, true) == 96 and fence_after(0, 96, false) == 96; + assert trap_line(FX1) == 41 and section_end(FX1, 41) == FX1.len; + assert add(FX1, "another lesson that is different enough", "") == O_ADD; + assert trap_line(FX2) == 23; + assert add(FX2, "another lesson that is different enough", "") == O_ADD; + assert add(FX3, NEW, "") == O_ADD_SECTION; + assert trap_line(FX4) < FX4.len; + assert add(FX4, "another lesson that is different enough", "") == O_ADD; + assert add(FX4, NEW, "") == O_DUPLICATE; +} + // Scenario: the row goes after the last line of the section that is not blank, before its trailing blank line. test the_row_goes_after_the_last_line_of_the_section { assert patch_at(F) == 145;