Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/skills/t27b-loop/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,14 @@ Append one line to the loop ledger memory (`t27b-loop-ledger.md`): the time, wha
- **The lab drops a request whose head moved.** It also dropped requests it had not fetched yet; since #8320 those wait 30 min.
- **An unrelated session can change shared `.git/config`.** Verify the setting; do not trust that it was restored. Pin per worktree instead of fighting over the shared value.
- **Other sessions' spec rewrites can turn master's native ratchet red with UNEXPECTED FAILURE**, even when t27b did not regress (c7570433e and c9d36f314 on 2026-10-10). Before suspecting t27b, check with `git log` whether the spec changed.

## Traps seen (2026-10-19, #10837)

- **A blocked row cannot be added while the ledger sits at `max_not_pass`.** #10380 merged two specs with no rows. One of them is blocked, so its row would be the 140th not-pass row, and the ratchet then reports OVER CAP. Raising the cap is not allowed. Free a slot first: make t27b pass a blocked spec whose blocker is small. In #10837 that was `@truncate` in `specs/tri/utils/utf8.t27`, written as `truncate_plan` in `int_cast_plan.t27`. Count the slots before you file the issue: `grep -c '"t27b": "blocked"'` plus `codegen`, compared with the cap.
- **The fs-lab is x86_64, but the t27b JIT runs only on arm64.** Without `--runner "qemu-aarch64 -L /usr/aarch64-linux-gnu"`, `t27b corpus` reports every spec as `crash: exit 74`, and the ratchet then shows hundreds of UNEXPECTED FAILURE lines that t27b did not cause. Keep `--jobs 16`: at 32 or 40 the reference returns `spawn: Resource temporarily unavailable`, and those blocked rows poison the reference cache. Before reusing a cache, keep only its `pass` and `fail` rows.
- **The lab's `/data` volume can fill to 100%.** Cargo then fails with `failed to select a version ... locked to` rather than a disk error, because the index update in `CARGO_HOME=/data/cargo` cannot write. Set `CARGO_HOME` to a directory on `/root` (the overlay has free space) rather than deleting other lanes' trees.
- **Changing a plan module's behaviour also flips that module's old negative tests.** `assert !from_destination("@truncate")` failed once `@truncate` became a destination cast. Read the module's tests for the construct before you run them.
- **Two sessions can take the same ledger issue.** #10853 (bare `sqrt`) and #10862 (`@truncate`) both closed #10837, and each stayed red alone: one lacked the rows the other had, and the other lacked the slots. Before you open a PR for an issue, run `gh pr list --search "<issue number>"`. If a sibling PR exists, merge its branch into yours (fetch it as `refs/heads/<name>`), and say "Carries #N" in the body.
- **A cap lowered to the exact count leaves no room for rows other PRs still need.** #10853 set `max_not_pass` to 129. eval_hslm and ngram_hslm still needed two blocked rows, so the combined PR set 130. That is still below master's 139, so it does not raise the cap. Recount after every merge: `blocked` + `codegen` = cap.
- **The Mac hook refuses any Bash text that contains the words of a test command**, even inside a heredoc for a PR body. Write PR bodies with the Write tool.
- **A merge of master that changes `lefthook.yml` is refused by the own-language pre-commit hook**, because the gate file is owner-only and the merge commit carries master's edit to it. Never `--no-verify`. Instead, `git merge --abort`, make the branch's side of each conflicting file equal master's (for the ledger: master's rows plus only the lane's rows) in a normal commit, then check `git merge-tree --write-tree HEAD FETCH_HEAD`. rc 0 means GitHub can squash it with no conflict (#10862).
22 changes: 11 additions & 11 deletions .trinity/seals/t27b_T27bIntCastPlan.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
{
"built_by": "t27c-bootstrap@0.5.2+12c6d9140",
"built_by": "t27c-bootstrap@0.5.2+355d44a39",
"closure": [
{
"spec": "specs/tri/t27b/type_text.t27",
"spec_hash": "sha256:f474fe76cd84a302b46401c9a1b7db0b6a7282c0f0c8a462e4a9493415d0d915"
"spec_hash": "sha256:0c2ea71bc30d0865c2fb2206de7a0499e836bba7da6d1340bb1bef116823f473"
}
],
"config": "gen=c,rust,verilog,zig;tests=zig",
"gen_hash_c": "sha256:dd275abc7d4d4b4b1c66d4ba4cb4f68dc77df14aab682b99befb25b5cffcf1c4",
"gen_hash_rust": "sha256:3b9360c9528f90657a1e23bdaeb0e456b07af742a9b7af6d52972b0c4771fbfb",
"gen_hash_verilog": "sha256:7300347f1653e23aad8c9407fdfe45e71114e20a692c8604f2c9a7299a37e8d1",
"gen_hash_zig": "sha256:800d462d250ddc2be3bcc67882b83421d7b897c4f6d2c58a4a0d59b371e6b34f",
"gen_hash_c": "sha256:edc22f81f999522d6399175b14c25fc32903dc8d78159fcf0b665579c8e5743a",
"gen_hash_rust": "sha256:a21600fc43a214f2d011c4330dda78f0b3ad2ac7201a42280ec5e6ccef99c848",
"gen_hash_verilog": "sha256:ae41776f84b010c488e1fa4b736674e0963f874ad9619971f661382ca517e598",
"gen_hash_zig": "sha256:39ba7667eca666138a3ec170d6d17460e840fcfb53a941ab9dba5e27ba88cb44",
"module": "T27bIntCastPlan",
"ring": 12,
"seal_schema": 2,
"sealed_at": "2026-10-10T19:08:25Z",
"sealed_at": "2026-10-19T04:30:30Z",
"sealed_by": "t27c-bootstrap@0.5.2",
"spec_hash": "sha256:bd544b0bb2aaf79e600e974b91eb5c25daeecbb09ee1e4f1d3fd0b3cd3d91946",
"spec_hash": "sha256:9ec7b05df748df422d374d5adb6b23f5f333a84ec30abc2545b2d73b7a2dbf56",
"spec_path": "specs/tri/t27b/int_cast_plan.t27",
"tests": {
"failed": 0,
"failing": [],
"forced": false,
"passed": 13,
"total": 13,
"passed": 14,
"total": 14,
"vacuous": 0
},
"toolchain": {
"test_runner": "sha256:1982ae948e24d00c5d221ccec12dfce1eb01204b53e37b0f857f93b16d64629d",
"test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10",
"zig": "0.16.0"
}
}
18 changes: 9 additions & 9 deletions cli/t27b/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3712,8 +3712,8 @@ impl<'a> Lower<'a> {
}

/// `@intCast(x)` with integer result type `ty` from the context (`@as`, a typed binding, a parameter, a
/// return). What is refused and what the rest lowers to: specs/tri/t27b/int_cast_plan.t27.
fn int_cast(&mut self, n: &Node, ty: Ty) -> R<Val> {
/// return), or with `trunc` `@truncate` (#10837). What is refused and what the rest lowers to: int_cast_plan.t27.
fn int_cast(&mut self, n: &Node, ty: Ty, trunc: bool) -> R<Val> {
self.see(n);
let shape = ic::shape(n.children.len(), ty.is_int());
let v = if shape == ic::GO { self.expr(&n.children[0])? } else { Val::Poison };
Expand All @@ -3723,28 +3723,28 @@ impl<'a> Lower<'a> {
Val::Ct(c) => (ic::K_CT, ty, Some(*c)),
_ => (ic::K_OTHER, ty, None),
};
let (fits, wrap) = (c.is_some_and(|c| ty.fits(c)), self.mode == OverflowMode::Wrap);
let plan = |k| ic::plan(k, from.bits(), from.signed(), ty.bits(), ty.signed(), c.is_some(), fits, wrap);
let (fits, wrap, site) = (c.is_some_and(|c| ty.fits(c)), self.mode == OverflowMode::Wrap, if trunc { ic::TRUNC_SITE } else { ic::SITE });
let plan = |k| if trunc { ic::truncate_plan(k, from.bits(), from.signed(), ty.bits(), ty.signed(), c.is_some()) } else { ic::plan(k, from.bits(), from.signed(), ty.bits(), ty.signed(), c.is_some(), fits, wrap) };
let kind = match (if shape == ic::GO { plan(k) } else { shape }, v) {
(ic::KEEP, v) => return Ok(v),
(ic::FOLD | ic::FOLD_WRAP, _) => ExprKind::Const(ty.wrap(c.unwrap())), // a FOLD fits `ty`: wrap keeps it
(ic::WIDEN, Val::E(e)) => ExprKind::Widen(Box::new(e)),
(a, Val::E(e)) if ic::is_cast(a) => {
let what = format!("{} {} to {}", ic::SITE, from.name(), ty.name());
let what = format!("{} {} to {}", site, from.name(), ty.name());
ExprKind::Cast { arg: Box::new(e), site: if ic::traps(a) { self.site(TrapKind::Cast, what, ty) } else { 0 } }
}
(a, v) => {
let d = match &v { Val::E(e) => e.ty.name().into(), Val::Ct(c) => c.to_string(), _ => self.val_desc(&v) };
let d = if v.is_poison() { format!("{} operands", n.children.len()) } else { d };
return self.reject(ic::WHAT, format!("`@intCast` of {} to {}: {}", d, ty.name(), ic::why(a)));
return self.reject(if trunc { ic::TRUNC_WHAT } else { ic::WHAT }, format!("`{}` of {} to {}: {}", site, d, ty.name(), ic::why(a)));
}
};
Ok(Val::E(Expr { ty, kind }))
}

/// An index or a slice bound, whose result type in Zig is `ic::INDEX_TYPE`: an `@intCast` there converts to it.
fn usize_operand(&mut self, n: &Node) -> R<Val> {
if n.kind == NodeKind::ExprCall && n.name == ic::SITE { self.int_cast(n, Ty::from_name(ic::INDEX_TYPE).unwrap_or(Ty::U64)) } else { self.expr(n) }
if n.kind == NodeKind::ExprCall && n.name == ic::SITE { self.int_cast(n, Ty::from_name(ic::INDEX_TYPE).unwrap_or(Ty::U64), false) } else { self.expr(n) }
}

/// `n` folded as Zig folds an expression that names a module constant of an integer type wider than 64 bits:
Expand Down Expand Up @@ -5223,8 +5223,8 @@ impl<'a> Lower<'a> {
if let (NodeKind::ExprCall, "@floatCast", LTy::S(ty)) = (&n.kind, n.name.as_str(), want) {
return self.float_cast_call(n, *ty);
}
if let (NodeKind::ExprCall, "@intCast", LTy::S(ty)) = (&n.kind, n.name.as_str(), want) {
return self.int_cast(n, *ty);
if let (NodeKind::ExprCall, "@intCast" | "@truncate", LTy::S(ty)) = (&n.kind, n.name.as_str(), want) {
return self.int_cast(n, *ty, n.name == ic::TRUNC_SITE);
}
if let (NodeKind::ExprCall, "@bitCast", LTy::S(ty)) = (&n.kind, n.name.as_str(), want) { return self.bit_cast(n, *ty); }
if let (NodeKind::ExprCall, "@intFromEnum", LTy::S(ty)) = (&n.kind, n.name.as_str(), want) {
Expand Down
17 changes: 8 additions & 9 deletions cli/t27b/src/lower_float.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,21 +72,20 @@ impl<'a> Lower<'a> {
}
}

/// `abs(x)` with no `fn abs` declared: t27c's Zig backend prints the
/// builtin `@abs(x)` (its `declared_fns` guard). A typed f64 / f32 loses
/// its sign bit, so `-0.0` gives `+0.0`; written `x <= 0 ? 0 - x : x`,
/// with `x` evaluated once (`NaN <= 0` is false: a NaN comes back as is).
/// A comptime operand folds. A typed integer is refused: Zig's `@abs`
/// of an `iN` is a `uN`. `None`: not this call.
/// `abs(x)` / `sqrt(x)` with no such fn declared: t27c's Zig backend prints the builtin `@abs(x)` / `@sqrt(x)`
/// (its `declared_fns` guard). A typed f64 / f32 `abs` loses its sign bit, so `-0.0` gives `+0.0`; written
/// `x <= 0 ? 0 - x : x`, with `x` evaluated once (`NaN <= 0` is false: a NaN comes back as is). A comptime
/// operand folds. A typed integer `abs` is refused: Zig's `@abs` of an `iN` is a `uN`. `None`: not this call.
pub(super) fn bare_abs(&mut self, n: &Node) -> R<Option<Val>> {
if n.name != "abs" || self.sigs.contains_key("abs") || self.poison_names.contains("abs") {
let g = n.name.as_str();
if !matches!(g, "abs" | "sqrt") || self.sigs.contains_key(g) || self.poison_names.contains(g) {
return Ok(None);
}
if g == "sqrt" { return self.expr(&Node { name: "@sqrt".into(), ..n.clone() }).map(Some); }
self.builtin_plan(n).map(Some)
}

/// `@abs(x)`, `@max(a, b)`, `@min(a, b)`, `@divTrunc(a, b)` and a bare `abs(x)`: what they take, the
/// result type and the select chain or operator each lowers to are specs/tri/t27b/builtin_plan.t27.
/// `@abs`, `@max`, `@min`, `@divTrunc` and a bare `abs`: operands, result type and select chain/operator are specs/tri/t27b/builtin_plan.t27.
pub(super) fn builtin_plan(&mut self, n: &Node) -> R<Val> {
let b = match n.name.as_str() { "@abs" => bp::ABS, "@max" => bp::MAX, "@min" => bp::MIN, "@divTrunc" => bp::DIV_TRUNC, _ => bp::BARE_ABS };
self.see(n);
Expand Down
8 changes: 4 additions & 4 deletions cli/t27b/tests/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3024,9 +3024,8 @@ fn type_alias_rejections() {

// ------------------------------------------------------------ bare abs

/// `abs(x)` with no `fn abs` declared is Zig's `@abs` (#7227): t27c prints
/// the builtin. A float loses its sign bit (`-0.0` gives `+0.0`), the
/// operand runs once, and a comptime operand folds.
/// `abs(x)` with no `fn abs` declared is Zig's `@abs` (#7227): t27c prints the builtin. A float loses its
/// sign bit (`-0.0` gives `+0.0`), the operand runs once, a comptime operand folds; bare `sqrt` is `@sqrt`.
#[test]
fn bare_abs_is_the_zig_builtin() {
let src = "module a;
Expand Down Expand Up @@ -3059,6 +3058,7 @@ test ok {
assert(CALLS == 1);
assert(abs(-2.5) == 2.5);
assert(abs(-3) == 3);
assert(sqrt(mag(-16.0)) == 4.0);
}

test fails {
Expand All @@ -3079,7 +3079,7 @@ fn bare_abs_rejections() {
let cases: &[(&str, &str)] = &[
("fn f(x: i32) -> u32 { return abs(x); }", "ExprCall(abs of an integer)"),
("fn f(x: f64, y: f64) -> f64 { return abs(x, y); }", "ExprCall(undeclared fn)"),
("fn f(x: f64) -> f64 { return sqrt(x); }", "ExprCall(undeclared fn)"),
("fn f(x: f64) -> f64 { return tanh(x); }", "ExprCall(undeclared fn)"),
];
for (body, construct) in cases {
let m = rejected(&format!("module a;\n\n{}\n", body));
Expand Down
Loading
Loading