From 1c00afecb17bf23ccf7af1c8735096dec9c40549 Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Mon, 31 Aug 2026 01:09:13 +0700 Subject: [PATCH 1/2] fix(tests): key the corpus-column scratch dir per call, not per process Closes #2968 `tri harness scratch --gate` has been red on master since `994f3c8a3`. `bootstrap/tests/corpus_zig_bodies.rs:86` keys its scratch directory by `std::process::id()` -- one value for the whole test binary -- and the test that uses it ends with `remove_dir_all(&dir)`. Measured, and stated as measured: only one of that file's three tests touches the directory today, so nothing collides on this tree. The gate is structural and right to be: the second test to use that key would collide, and it would pass the first time it ran. Fixed as the gate's own message prescribes -- an `AtomicUsize` counter, unique per call. `tri harness scratch --gate` prints `none` and exits 0; the three tests in that file still pass. Co-Authored-By: Claude Opus 5 --- bootstrap/src/compiler.rs | 44 ++++++++++++++++--- bootstrap/stage0/FROZEN_HASH | 2 +- bootstrap/tests/corpus_zig_bodies.rs | 7 ++- ...ent-red-on-master-the-hour-it-was-wired.md | 8 ++++ 4 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 docs/now/2026-08-31-the-scratch-gate-went-red-on-master-the-hour-it-was-wired.md diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 51176297e7..51d1a38846 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -18360,11 +18360,18 @@ impl CCodegen { let child = &node.children[0]; // Simple literal → #define if child.kind == NodeKind::ExprLiteral { - self.write_line(&format!( - "#define {} {}", - node.name, - Self::c_literal(&child.value) - )); + // `c_literal` takes a &str and so cannot see the lexer's + // `extra_kind == "string"` tag, which lives on the NODE. A + // string constant therefore emitted `#define NAME trinity` -- + // and that is worse than a compile error, because a #define of + // a bare word is valid C. It lands in the output and fails + // wherever it is used, or expands to something else entirely. + let text = if child.extra_kind == "string" { + format!("\"{}\"", Codegen::zig_escape(&child.value)) + } else { + Self::c_literal(&child.value) + }; + self.write_line(&format!("#define {} {}", node.name, text)); } else { // Complex expression → static const self.write(&format!("static const {} {} = ", c_type, node.name)); @@ -19564,6 +19571,25 @@ impl CCodegen { fn gen_c_expr(&mut self, node: &Node) { match node.kind { NodeKind::ExprLiteral => { + // The lexer strips the surrounding quotes and stores the raw + // text, tagging the node `extra_kind == "string"`. Zig reads + // that tag (W560), Verilog reads it, the typechecker reads it + // -- and this emitter never did, so every string literal was + // written back as a bare identifier: + // + // let s = "hello world" -> __auto_type s = hello world; + // const NAME : str = "trinity" -> #define NAME trinity + // + // The second is worse than a compile error: a `#define` of a + // bare word is valid C, so it lands in the output and fails + // wherever it is used, or silently expands to something else. + // + // The lexer UNESCAPES as it reads, so the value here holds real + // newlines and quotes; they have to be written back escaped. + if node.extra_kind == "string" { + self.write(&format!("\"{}\"", Codegen::zig_escape(&node.value))); + return; + } // Zig and Rust write `100_000_000`; C does not. The separator // reached the output verbatim and `cc` reads it as a suffix: // `invalid suffix '_000_000' on integer constant`. 97 lines @@ -23963,6 +23989,14 @@ impl RustCodegen { fn expr_to_rust(&self, node: &Node) -> String { match node.kind { + // Same hole as the C emitter had, in the same place: the lexer's + // `extra_kind == "string"` tag is on the node and this arm returned + // the raw value, so `let s = "hello world"` became + // `let s = hello world;` and `const NAME : str = "trinity"` became + // `pub const NAME: String = trinity;`. + NodeKind::ExprLiteral if node.extra_kind == "string" => { + format!("\"{}\"", Codegen::zig_escape(&node.value)) + } NodeKind::ExprLiteral => node.value.clone(), NodeKind::ExprIdentifier => node.name.clone(), NodeKind::ExprBinary => { diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index 2993540699..f9a52b321e 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -69d5f0a49f872ffd608be86df0423f4c4df0f2cd30bf5f1e705eba52adffa04e +4eccc2f5d22862da1ff78f07c76d232814c87af70a3927c9a997e603afbae31c diff --git a/bootstrap/tests/corpus_zig_bodies.rs b/bootstrap/tests/corpus_zig_bodies.rs index 22515dc4fd..818e923a96 100644 --- a/bootstrap/tests/corpus_zig_bodies.rs +++ b/bootstrap/tests/corpus_zig_bodies.rs @@ -83,7 +83,12 @@ fn per_spec_carries_three_zig_digits_and_says_so_in_its_header() { eprintln!("zig not on PATH -- SKIPPED"); return; } - let dir = std::env::temp_dir().join(format!("t27-corpuscols-{}", std::process::id())); + // Unique per CALL, not per process: a pid is one value for the whole test + // binary, so every test in this file would resolve the same directory and + // the `remove_dir_all` below would erase a sibling's input mid-read. + static NTH: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + let nth = NTH.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let dir = std::env::temp_dir().join(format!("t27-corpuscols-{}-{nth}", std::process::id())); std::fs::create_dir_all(&dir).expect("temp dir"); let path = dir.join("per-spec.txt"); let out = Command::new(env!("CARGO_BIN_EXE_t27c")) diff --git a/docs/now/2026-08-31-the-scratch-gate-went-red-on-master-the-hour-it-was-wired.md b/docs/now/2026-08-31-the-scratch-gate-went-red-on-master-the-hour-it-was-wired.md new file mode 100644 index 0000000000..15e3e22963 --- /dev/null +++ b/docs/now/2026-08-31-the-scratch-gate-went-red-on-master-the-hour-it-was-wired.md @@ -0,0 +1,8 @@ +# NOW -- The scratch gate went red on master the hour it was wired (2026-08-31) + +## The scratch gate went red on master the hour it was wired (Closes #2968) + +- tri harness scratch --gate has been red on master since 994f3c8a3: bootstrap/tests/corpus_zig_bodies.rs keys its scratch directory by std::process::id(), which is one value for the whole test binary. +- Measured, and stated as measured: only one of that file's three tests creates or deletes the directory today, so nothing collides on this tree. The gate is structural, and right to be -- the second test to use that key would collide, and would pass the first time it ran. +- Fixed as the gate's own message prescribes: an AtomicUsize counter, unique per call. Gate now prints none and exits 0; the three tests still pass. +- The gate found this within an hour of being wired, on a file written after it. That is the reading worth keeping: it went red on new code rather than on history. From 336c328b78d38c2c886ef677027e9e1359284f2b Mon Sep 17 00:00:00 2001 From: Vasilev Dmitrii Date: Mon, 31 Aug 2026 01:17:27 +0700 Subject: [PATCH 2/2] revert: drop another session's work-in-progress from this branch Refs #2968 `git add -A` in this branch's first commit swept in 44 lines of `bootstrap/src/compiler.rs` and its `FROZEN_HASH` that I did not write: a C and Rust emitter fix for string literals (`extra_kind == "string"`), belonging to another session. How it got into a clean worktree: the stash stack is **shared across every worktree of a repository**. An earlier command of mine ran `git stash -q -u` on a tree with nothing to stash -- which pushes nothing -- and then `git stash pop -q`, which popped the top of the shared stack: somebody else's work in progress. Both files are restored to master's version, so this branch is again the one line it claims to be. Their work is not lost: it is the diff of this branch's first commit `1c00afecb`, and a copy is attached to #2968. Measured before and after: with a release build of this branch, `tri seals drift` read 152 drifted seals; on master it reads 0. After this revert it reads 0 -- the drift was the swept-in emitter change, not my test file. Co-Authored-By: Claude Opus 5 --- bootstrap/src/compiler.rs | 44 ++++-------------------------------- bootstrap/stage0/FROZEN_HASH | 2 +- 2 files changed, 6 insertions(+), 40 deletions(-) diff --git a/bootstrap/src/compiler.rs b/bootstrap/src/compiler.rs index 51d1a38846..51176297e7 100644 --- a/bootstrap/src/compiler.rs +++ b/bootstrap/src/compiler.rs @@ -18360,18 +18360,11 @@ impl CCodegen { let child = &node.children[0]; // Simple literal → #define if child.kind == NodeKind::ExprLiteral { - // `c_literal` takes a &str and so cannot see the lexer's - // `extra_kind == "string"` tag, which lives on the NODE. A - // string constant therefore emitted `#define NAME trinity` -- - // and that is worse than a compile error, because a #define of - // a bare word is valid C. It lands in the output and fails - // wherever it is used, or expands to something else entirely. - let text = if child.extra_kind == "string" { - format!("\"{}\"", Codegen::zig_escape(&child.value)) - } else { - Self::c_literal(&child.value) - }; - self.write_line(&format!("#define {} {}", node.name, text)); + self.write_line(&format!( + "#define {} {}", + node.name, + Self::c_literal(&child.value) + )); } else { // Complex expression → static const self.write(&format!("static const {} {} = ", c_type, node.name)); @@ -19571,25 +19564,6 @@ impl CCodegen { fn gen_c_expr(&mut self, node: &Node) { match node.kind { NodeKind::ExprLiteral => { - // The lexer strips the surrounding quotes and stores the raw - // text, tagging the node `extra_kind == "string"`. Zig reads - // that tag (W560), Verilog reads it, the typechecker reads it - // -- and this emitter never did, so every string literal was - // written back as a bare identifier: - // - // let s = "hello world" -> __auto_type s = hello world; - // const NAME : str = "trinity" -> #define NAME trinity - // - // The second is worse than a compile error: a `#define` of a - // bare word is valid C, so it lands in the output and fails - // wherever it is used, or silently expands to something else. - // - // The lexer UNESCAPES as it reads, so the value here holds real - // newlines and quotes; they have to be written back escaped. - if node.extra_kind == "string" { - self.write(&format!("\"{}\"", Codegen::zig_escape(&node.value))); - return; - } // Zig and Rust write `100_000_000`; C does not. The separator // reached the output verbatim and `cc` reads it as a suffix: // `invalid suffix '_000_000' on integer constant`. 97 lines @@ -23989,14 +23963,6 @@ impl RustCodegen { fn expr_to_rust(&self, node: &Node) -> String { match node.kind { - // Same hole as the C emitter had, in the same place: the lexer's - // `extra_kind == "string"` tag is on the node and this arm returned - // the raw value, so `let s = "hello world"` became - // `let s = hello world;` and `const NAME : str = "trinity"` became - // `pub const NAME: String = trinity;`. - NodeKind::ExprLiteral if node.extra_kind == "string" => { - format!("\"{}\"", Codegen::zig_escape(&node.value)) - } NodeKind::ExprLiteral => node.value.clone(), NodeKind::ExprIdentifier => node.name.clone(), NodeKind::ExprBinary => { diff --git a/bootstrap/stage0/FROZEN_HASH b/bootstrap/stage0/FROZEN_HASH index f9a52b321e..2993540699 100644 --- a/bootstrap/stage0/FROZEN_HASH +++ b/bootstrap/stage0/FROZEN_HASH @@ -1 +1 @@ -4eccc2f5d22862da1ff78f07c76d232814c87af70a3927c9a997e603afbae31c +69d5f0a49f872ffd608be86df0423f4c4df0f2cd30bf5f1e705eba52adffa04e