From 6bb7521f013abcd5353ad5d254c98f84d756ae38 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 10:01:06 +0700 Subject: [PATCH 1/6] feat(t27b): extern boundary -- fn calls a C symbol via @extern, Mach-O undefined symbols + BRANCH26 (#8946) A fn whose body is @extern("sym") imports the C symbol sym. lower names it @sym (a name t27 source cannot declare), passes a str or slice argument as its bytes' address, and codegen emits each call's bl as a tagged data ref that only macho.t27 resolves: one ARM64_RELOC_BRANCH26 against an undefined _sym nlist, distinct names shared, sorted after the externals. Images without imports are byte-identical (reloc_slot is 2*i). The Rust glue nets negative by compacting the a64.rs disassembler tables. Co-Authored-By: Claude Opus 5.5 --- cli/t27b/src/a64.rs | 38 ++------ cli/t27b/src/codegen.rs | 8 +- cli/t27b/src/lower.rs | 18 +++- specs/tri/t27b/elf.t27 | 2 + specs/tri/t27b/macho.t27 | 183 +++++++++++++++++++++++++++++++++++---- 5 files changed, 194 insertions(+), 55 deletions(-) diff --git a/cli/t27b/src/a64.rs b/cli/t27b/src/a64.rs index a9b968c8b1..abd357258e 100644 --- a/cli/t27b/src/a64.rs +++ b/cli/t27b/src/a64.rs @@ -44,13 +44,7 @@ pub fn mov_imm(sf: bool, rd: Reg, value: u64, out: &mut Vec) { // -------------------------------------------------------------- disassembly fn xr(sf: bool, n: u32, sp_ctx: bool) -> String { - if n == 31 { - if sp_ctx { - return if sf { "sp".into() } else { "wsp".into() }; - } - return if sf { "xzr".into() } else { "wzr".into() }; - } - format!("{}{}", if sf { 'x' } else { 'w' }, n) + if n == 31 { [["wzr", "xzr"], ["wsp", "sp"]][sp_ctx as usize][sf as usize].into() } else { format!("{}{}", if sf { 'x' } else { 'w' }, n) } } /// Disassemble one word of the subset this module encodes (for `t27b asm`). @@ -95,12 +89,7 @@ pub fn disasm(w: u32, pc: usize) -> String { let s = (w >> 29) & 1 == 1; let imm = (w >> 10) & 0xfff; let sh = if (w >> 22) & 1 == 1 { ", lsl #12" } else { "" }; - let name = match (sub, s) { - (false, false) => "add", - (false, true) => "adds", - (true, false) => "sub", - (true, true) => "subs", - }; + let name = ["add", "adds", "sub", "subs"][2 * sub as usize + s as usize]; if s && rd == 31 { return format!("{} {}, #{}{}", if sub { "cmp" } else { "cmn" }, xr(sf, rn, true), imm, sh); } @@ -113,12 +102,7 @@ pub fn disasm(w: u32, pc: usize) -> String { if w & 0x1F00_0000 == 0x0B00_0000 { let sub = (w >> 30) & 1 == 1; let s = (w >> 29) & 1 == 1; - let name = match (sub, s) { - (false, false) => "add", - (false, true) => "adds", - (true, false) => "sub", - (true, true) => "subs", - }; + let name = ["add", "adds", "sub", "subs"][2 * sub as usize + s as usize]; if (w >> 21) & 1 == 1 { let opt = (w >> 13) & 7; let ext = ["uxtb", "uxth", "uxtw", "uxtx", "sxtb", "sxth", "sxtw", "sxtx"][opt as usize]; @@ -173,26 +157,14 @@ pub fn disasm(w: u32, pc: usize) -> String { let opc = (w >> 29) & 3; let hw = (w >> 21) & 3; let imm = (w >> 5) & 0xffff; - let name = match opc { - 0 => "movn", - 2 => "movz", - 3 => "movk", - _ => return format!(".word {:#010x}", w), - }; + let Some(name) = [Some("movn"), None, Some("movz"), Some("movk")][opc as usize] else { return format!(".word {:#010x}", w) }; let sh = if hw != 0 { format!(", lsl #{}", hw * 16) } else { String::new() }; return format!("{} {}, #{:#x}{}", name, xr(sf, rd, false), imm, sh); } // data processing 2 source if w & 0x7FE0_0000 == 0x1AC0_0000 { let op = (w >> 10) & 63; - let name = match op { - 2 => "udiv", - 3 => "sdiv", - 8 => "lsl", - 9 => "lsr", - 10 => "asr", - _ => return format!(".word {:#010x}", w), - }; + let name = match op { 2 => "udiv", 3 => "sdiv", 8 => "lsl", 9 => "lsr", 10 => "asr", _ => return format!(".word {:#010x}", w) }; return format!("{} {}, {}, {}", name, xr(sf, rd, false), xr(sf, rn, false), xr(sf, rm, false)); } // data processing 3 source diff --git a/cli/t27b/src/codegen.rs b/cli/t27b/src/codegen.rs index 2c6bdcee14..9ebf952f58 100644 --- a/cli/t27b/src/codegen.rs +++ b/cli/t27b/src/codegen.rs @@ -191,7 +191,7 @@ struct Gen<'a> { pub fn compile(prog: &Program, style: TrapStyle, with_tests: bool) -> Result, CodegenError> { let mut out = Vec::with_capacity(prog.funcs.len()); for (i, f) in prog.funcs.iter().enumerate() { - if f.is_test && !with_tests { + if (f.is_test && !with_tests) || (f.name.starts_with('@') && style != TrapStyle::Jit) { // an import (#8946) has no code in an object continue; } out.push(compile_func(prog, i as FuncId, style)?); @@ -1617,7 +1617,11 @@ impl<'a> Gen<'a> { for &(v, _) in vals.iter().rev() { self.release(v); } - self.calls.push((self.code.len(), func)); + match (self.prog.funcs[func as usize].name.starts_with('@'), self.style) { // a call to an import: its `bl` is a data ref with bit 31 set, which only macho.t27 resolves (#8946) + (true, TrapStyle::Brk) => self.data_refs.push((self.code.len(), 1 << 31 | func)), + (true, TrapStyle::Elf) => self.fail(format!("call to the import `{}` outside a Mach-O object", &self.prog.funcs[func as usize].name[1..])), + _ => self.calls.push((self.code.len(), func)), // the JIT runs an import as its empty body, which traps: eval refuses the call + } self.emit(a64::bl(0)); if out_bytes > 0 { self.emit(a64::add_imm(true, SP, SP, out_bytes)); diff --git a/cli/t27b/src/lower.rs b/cli/t27b/src/lower.rs index 5b23909f90..7420928458 100644 --- a/cli/t27b/src/lower.rs +++ b/cli/t27b/src/lower.rs @@ -942,6 +942,14 @@ fn is_char_literal(n: &Node) -> bool { let (a, t) = flat(std::slice::from_ref(n)); sh::char_literal(&a, &t, 0) } +/// `fn f(..) T { return @extern("sym"); }` (or `{ @extern("sym"); }` for void) imports the C symbol `sym` +/// (#8946): the fn is named `@sym`, a name t27 source cannot declare, and the object writer reads it. +fn extern_sym(n: &Node) -> Option { + let [s] = n.children.as_slice() else { return None }; + let c = if s.name == "@extern" { s } else { s.children.first().filter(|_| matches!(s.kind, NodeKind::ExprReturn | NodeKind::StmtExpr))? }; + c.children.first().filter(|a| c.name == "@extern" && c.children.len() == 1 && a.extra_kind == "string").map(|a| format!("@{}", a.value)) +} + fn parse_int(s: &str) -> Option { Some(st::parse_int(s.as_bytes())).filter(|v| *v != st::NO_INT) } @@ -1623,6 +1631,13 @@ impl<'a> Lower<'a> { if poisoned { return self.poisoned_function(n); } + if let Some(name) = extern_sym(n) { // an import: no body, a str or slice goes as its bytes' address, an aggregate as its own + let ty = |t: <y| reg_ty(t).unwrap_or(Ty::U64); + let vars = params.iter().zip(&n.params).map(|(t, p)| Var { name: p.0.clone(), ty: ty(t) }).collect(); + if ret.as_ref().is_some_and(is_agg) { return self.reject("FnDecl(extern)", format!("`{}` returns an aggregate; an import returns a scalar (#8972)", n.name)); } + let noreturn_site = if ret.is_some() { self.site(TrapKind::NoReturn, format!("import {} run outside an object", n.name), Ty::Bool) } else { 0 }; + return Ok(Func { name, nparams: params.len(), ret: ret.map(|t| ty(&t)), vars, body: Vec::new(), slots: Vec::new(), line: n.line, is_test: false, is_invariant: false, noreturn_site }); + } // Parameters first (vars 0..), then the hidden result pointer, so // that they are exactly the first `nparams` variables. let mut ids = Vec::new(); @@ -3106,13 +3121,14 @@ impl<'a> Lower<'a> { if c.children.len() != params.len() { return self.reject("ExprCall", format!("`{}` takes {} arguments, {} given", c.name, params.len(), c.children.len())); } - let mut args = Vec::new(); + let (mut args, ext) = (Vec::new(), self.fns.get(&c.name).is_some_and(|f| extern_sym(f).is_some())); for (i, a) in c.children.iter().enumerate() { let v = if self.fns.get(&c.name).and_then(|f| f.params.get(i).map(|p| unread_any(f, &p.0, &p.1))) == Some(true) { self.any_arg(c, a, ¶ms[i])? } else if is_undefined(a) && !self.is_void(¶ms[i]) { self.undefined_arg(c, i, a, ¶ms[i])? } else { self.arg_as(a, ¶ms[i])? }; args.push(match v { // By reference; the callee never writes it. + Val::M(p) if ext && matches!(params[i], LTy::Str | LTy::Slice(..)) => Expr { ty: Ty::U64, kind: ExprKind::Load { addr: Box::new(addr_of(&p)), off: 0 } }, // C gets the bytes' address (#8946) Val::M(p) => addr_of(&p), v => self.reg(v)?, }); diff --git a/specs/tri/t27b/elf.t27 b/specs/tri/t27b/elf.t27 index fa44fc563d..0eed847ba2 100644 --- a/specs/tri/t27b/elf.t27 +++ b/specs/tri/t27b/elf.t27 @@ -337,6 +337,7 @@ pub const REFUSED_NOTE : str = "takes or returns a struct, a str, a slice, an ar /// EXPORT, HELPER or REFUSED for the function called `name`; own_convention: it is in lower's `internal_abi`. pub fn export_kind(name: []const u8, own_convention: bool) -> u8 { + if (name.len > 0 and name[0] == 64) { return EXPORT; } // an import `@sym` (#8946) keeps its name: macho.t27 makes it undefined if (name.len >= RESERVED.len) { var i : usize = 0; while (i < RESERVED.len and name[i] == RESERVED[i]) { i = i + 1; } @@ -528,6 +529,7 @@ test the_export_rule { assert export_kind("__t27b_i128_div", false) == HELPER; assert export_kind("__t27b_str_eql", true) == HELPER; assert export_kind("__t27b_", false) == HELPER; + assert export_kind("@puts", true) == EXPORT; // Only the whole prefix reserves: i128_plan.t27's port name, a shorter name and one that differs late. assert export_kind("t27b_i128_div", false) == EXPORT; assert export_kind("__t27", false) == EXPORT; diff --git a/specs/tri/t27b/macho.t27 b/specs/tri/t27b/macho.t27 index 8c088db4e7..8f74d2dff1 100644 --- a/specs/tri/t27b/macho.t27 +++ b/specs/tri/t27b/macho.t27 @@ -60,6 +60,11 @@ pub const SECT_CONST : u8 = 2; // relocation_info r_type values (). pub const ARM64_RELOC_PAGE21 : u32 = 3; pub const ARM64_RELOC_PAGEOFF12 : u32 = 4; +pub const ARM64_RELOC_BRANCH26 : u32 = 2; +// A refs row whose blob is EXT_REF + i is the `bl` of a call to function i, an import (#8946): a fn whose +// name starts with `@` (t27 `return @extern("sym");`), never emitted, named `_sym` as an undefined symbol. +pub const EXT_REF : usize = 0x80000000; +pub const AT : u8 = 64; // log2 alignments of the two sections: functions start on 16 bytes, blobs on 8. pub const TEXT_ALIGN_LOG2 : u32 = 4; pub const CONST_ALIGN_LOG2 : u32 = 3; @@ -119,12 +124,17 @@ pub fn reloff(text_size: usize, const_size: usize) -> usize { /// File offset of the symbols: two relocations per data reference. pub fn symoff(text_size: usize, const_size: usize, nrefs: usize) -> usize { - return reloff(text_size, const_size) + RELOC_SIZE * 2 * nrefs; + return syms_at(text_size, const_size, 2 * nrefs); +} + +/// File offset of the symbols after nreloc relocations (two per data reference, one per import call). +pub fn syms_at(text_size: usize, const_size: usize, nreloc: usize) -> usize { + return reloff(text_size, const_size) + RELOC_SIZE * nreloc; } /// File offset of the string table. pub fn stroff(text_size: usize, const_size: usize, nrefs: usize, nsyms: usize) -> usize { - return symoff(text_size, const_size, nrefs) + NLIST_SIZE * nsyms; + return syms_at(text_size, const_size, 2 * nrefs) + NLIST_SIZE * nsyms; } /// Length of the whole object; strsize is the padded string table. @@ -202,10 +212,15 @@ pub fn put_section(out: []u8, at: usize, sect: string, addr: usize, size: usize, /// HEADER_SIZE + sizeofcmds; the bytes up to text_off are padding the caller leaves zero. /// nrefs data references, nlocal blob symbols, nextdef function symbols, strsize padded string bytes. pub fn put_commands(out: []u8, text_size: usize, const_size: usize, nrefs: usize, nlocal: usize, nextdef: usize, strsize: usize) -> usize { + return put_commands_x(out, text_size, const_size, 2 * nrefs, nlocal, nextdef, 0, strsize); +} + +/// put_commands with nreloc relocations and nundef undefined (imported) symbols after the external ones. +pub fn put_commands_x(out: []u8, text_size: usize, const_size: usize, nreloc: usize, nlocal: usize, nextdef: usize, nundef: usize, strsize: usize) -> usize { const ns : u32 = nsects(const_size); const toff : usize = text_off(const_size); const vm : usize = vm_size(text_size, const_size); - const nsyms : usize = nlocal + nextdef; + const nsyms : usize = nlocal + nextdef + nundef; // mach_header_64 var p : usize = put_u32(out, 0, MH_MAGIC_64); p = put_u32(out, p, CPU_TYPE_ARM64); @@ -229,8 +244,8 @@ pub fn put_commands(out: []u8, text_size: usize, const_size: usize, nrefs: usize p = put_u32(out, p, 0); // __text carries the relocations; with none, reloff and nreloc are both 0. var rel : usize = 0; - if (nrefs > 0) { rel = reloff(text_size, const_size); } - p = put_section(out, p, "__text", 0, text_size, toff, TEXT_ALIGN_LOG2, rel, 2 * nrefs, S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS); + if (nreloc > 0) { rel = reloff(text_size, const_size); } + p = put_section(out, p, "__text", 0, text_size, toff, TEXT_ALIGN_LOG2, rel, nreloc, S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS); if (const_size > 0) { const ca : usize = const_addr(text_size); p = put_section(out, p, "__const", ca, const_size, toff + ca, CONST_ALIGN_LOG2, 0, 0, S_REGULAR); @@ -245,19 +260,19 @@ pub fn put_commands(out: []u8, text_size: usize, const_size: usize, nrefs: usize // LC_SYMTAB p = put_u32(out, p, LC_SYMTAB); p = put_u32(out, p, SYMTAB_SIZE as u32); - p = put_u32(out, p, symoff(text_size, const_size, nrefs) as u32); + p = put_u32(out, p, syms_at(text_size, const_size, nreloc) as u32); p = put_u32(out, p, nsyms as u32); - p = put_u32(out, p, stroff(text_size, const_size, nrefs, nsyms) as u32); + p = put_u32(out, p, (syms_at(text_size, const_size, nreloc) + NLIST_SIZE * nsyms) as u32); p = put_u32(out, p, strsize as u32); - // LC_DYSYMTAB: the blob symbols are local, the functions external, nothing undefined. + // LC_DYSYMTAB: the blob symbols are local, the functions external, the imports undefined. p = put_u32(out, p, LC_DYSYMTAB); p = put_u32(out, p, DYSYMTAB_SIZE as u32); p = put_u32(out, p, 0); p = put_u32(out, p, nlocal as u32); p = put_u32(out, p, nlocal as u32); p = put_u32(out, p, nextdef as u32); - p = put_u32(out, p, nsyms as u32); - p = put_u32(out, p, 0); + p = put_u32(out, p, (nlocal + nextdef) as u32); + p = put_u32(out, p, nundef as u32); var k : usize = 0; while (k < 12) { p = put_u32(out, p, 0); @@ -414,10 +429,10 @@ pub fn put_name(out: []u8, at: usize, names: []const u8, from: usize) -> usize { return at + n + 1; } -/// A function is exported when it was emitted and has a name. +/// A function is exported when it was emitted and has a name that is not an import's. pub fn exported(names: []const u8, fns: []const u8, i: usize, at: usize) -> bool { if (fn_at(fns, i) == 0) { return false; } - return names[at] != 0; + return names[at] != 0 and names[at] != AT; } pub fn export_count(names: []const u8, fns: []const u8) -> usize { @@ -503,7 +518,7 @@ pub fn macho_strsize(names: []const u8, fns: []const u8, refs: []const u8, nblob if (blob_used(refs, k)) { n = n + 13 + dec_len(k); } k = k + 1; } - return align_up(n + export_bytes(names, fns) + export_count(names, fns), 8); + return align_up(n + export_bytes(names, fns) + export_count(names, fns) + import_bytes(names, fns), 8); } /// Does (name at a, value va) sort before (name at b, value vb)? Bytes first, a prefix before a longer @@ -551,10 +566,78 @@ pub fn next_export(names: []const u8, fns: []const u8, prev: usize) -> usize { return best; } +// ---------------------------------------------------------------- imports (#8946) +// The undefined symbols follow the external ones: one `_sym` per distinct import name, in Rust's sort of +// the names (key_before with equal values), so two fns that import one symbol share it. Their strings +// follow the exported names. A call to an import is one ARM64_RELOC_BRANCH26 naming its symbol; the +// relocations are in refs order, two per data reference and one per call. + +/// Is refs row i the `bl` of a call to an import? +pub fn is_call(refs: []const u8, i: usize) -> bool { + return ref_blob(refs, i) >= EXT_REF; +} + +/// Relocations before refs row i: two per data reference, one per call. +pub fn reloc_slot(refs: []const u8, i: usize) -> usize { + var n : usize = 0; + var j : usize = 0; + while (j < i) { + if (is_call(refs, j)) { n = n + 1; } else { n = n + 2; } + j = j + 1; + } + return n; +} + +/// Is function i an import, and the first function with its name? +pub fn first_import(names: []const u8, i: usize) -> bool { + const at : usize = name_at(names, i); + if (names[at] != AT) { return false; } + var j : usize = 0; + while (j < i) { + const b : usize = name_at(names, j); + if (!key_before(names, at, 0, b, 0) and !key_before(names, b, 0, at, 0)) { return false; } + j = j + 1; + } + return true; +} + +/// Distinct import names that sort before the name at byte `at`: the rank of its undefined symbol. +pub fn import_rank(names: []const u8, fns: []const u8, at: usize) -> usize { + var n : usize = 0; + var i : usize = 0; + while (i < fn_count(fns)) { + if (first_import(names, i) and key_before(names, name_at(names, i), 0, at, 0)) { n = n + 1; } + i = i + 1; + } + return n; +} + +pub fn import_count(names: []const u8, fns: []const u8) -> usize { + var n : usize = 0; + var i : usize = 0; + while (i < fn_count(fns)) { + if (first_import(names, i)) { n = n + 1; } + i = i + 1; + } + return n; +} + +/// String bytes of the undefined symbols: `_` + the name after `@` + NUL, the name's length + 1 each. +pub fn import_bytes(names: []const u8, fns: []const u8) -> usize { + var n : usize = 0; + var i : usize = 0; + while (i < fn_count(fns)) { + if (first_import(names, i)) { n = n + name_len(names, name_at(names, i)) + 1; } + i = i + 1; + } + return n; +} + /// Length of the Mach-O object of an image whose code is text_len bytes. pub fn object_len(names: []const u8, fns: []const u8, refs: []const u8, lens: []const u8, text_len: usize, nblobs: usize) -> usize { - const nsyms : usize = used_count(refs, nblobs) + export_count(names, fns); - return object_size(text_len, group_size(lens, 0, nblobs), ref_count(refs), nsyms, macho_strsize(names, fns, refs, nblobs)); + const nsyms : usize = used_count(refs, nblobs) + export_count(names, fns) + import_count(names, fns); + const at : usize = syms_at(text_len, group_size(lens, 0, nblobs), reloc_slot(refs, ref_count(refs))); + return at + NLIST_SIZE * nsyms + macho_strsize(names, fns, refs, nblobs); } /// Writes the Mach-O object of an image into out, object_len bytes; returns that length. @@ -570,14 +653,15 @@ pub fn write_object(out: []u8, text: []const u8, names: []const u8, fns: []const const nrefs : usize = ref_count(refs); const nlocal : usize = used_count(refs, nblobs); const nextdef : usize = export_count(names, fns); - put_commands(out, text_size, const_size, nrefs, nlocal, nextdef, macho_strsize(names, fns, refs, nblobs)); + const nreloc : usize = reloc_slot(refs, nrefs); + put_commands_x(out, text_size, const_size, nreloc, nlocal, nextdef, import_count(names, fns), macho_strsize(names, fns, refs, nblobs)); const toff : usize = text_off(const_size); const ca : usize = const_addr(text_size); put_bytes(out, toff, text); put_group(out, toff + ca, blobs, lens, 0, nblobs); const rel : usize = reloff(text_size, const_size); - const syo : usize = symoff(text_size, const_size, nrefs); - const sto : usize = stroff(text_size, const_size, nrefs, nlocal + nextdef); + const syo : usize = syms_at(text_size, const_size, nreloc); + const sto : usize = syo + NLIST_SIZE * (nlocal + nextdef + import_count(names, fns)); var strx : usize = 1; var rank : usize = 0; var k : usize = 0; @@ -585,7 +669,7 @@ pub fn write_object(out: []u8, text: []const u8, names: []const u8, fns: []const if (blob_used(refs, k)) { var i : usize = 0; while (i < nrefs) { - if (ref_blob(refs, i) == k) { put_reloc_pair(out, rel + 2 * RELOC_SIZE * i, ref_word(refs, i), rank as u32); } + if (ref_blob(refs, i) == k) { put_reloc_pair(out, rel + RELOC_SIZE * reloc_slot(refs, i), ref_word(refs, i), rank as u32); } i = i + 1; } put_nlist(out, syo + NLIST_SIZE * rank, strx as u32, N_SECT, SECT_CONST, (ca + blob_off(lens, 0, k)) as u64); @@ -604,6 +688,30 @@ pub fn write_object(out: []u8, text: []const u8, names: []const u8, fns: []const f = next_export(names, fns, f); j = j + 1; } + var c : usize = 0; + while (c < nrefs) { + if (is_call(refs, c)) { + const r : usize = rel + RELOC_SIZE * reloc_slot(refs, c); + put_u32(out, r, (ref_word(refs, c) * 4) as u32); + const rk : usize = import_rank(names, fns, name_at(names, ref_blob(refs, c) - EXT_REF)); + put_u32(out, r + 4, reloc_info((nlocal + nextdef + rk) as u32, 1, ARM64_RELOC_BRANCH26)); + } + c = c + 1; + } + var u : usize = 0; + while (u < import_count(names, fns)) { + var h : usize = 0; + while (h < fn_count(fns)) { + const hat : usize = name_at(names, h); + if (first_import(names, h) and import_rank(names, fns, hat) == u) { + put_nlist(out, syo + NLIST_SIZE * (nlocal + nextdef + u), strx as u32, N_EXT, 0, 0); + out[sto + strx] = 95; + strx = put_name(out, sto + strx + 1, names, hat + 1) - sto; + } + h = h + 1; + } + u = u + 1; + } return size; } @@ -997,6 +1105,43 @@ test symbols_sort_as_rust_sorts_the_names_s4 { assert next_export(&names, &fns, 3) == 4; } +test imports_are_undefined_symbols_and_branch_relocations_s5 { + // S5: `main` calls `@puts` and `@abort`; a second fn imports `@puts` again and shares its symbol. The + // undefined symbols sort as _abort, _puts after the one external; each call is one BRANCH26 naming one. + var text : [12]u8 = undefined; + put_u32(&text, 0, 0x94000000); + put_u32(&text, 4, 0x94000000); + put_u32(&text, 8, 0xd65f03c0); + var names : [24]u8 = undefined; + names[put_text(&names, 0, "main")] = 0; + names[put_text(&names, 5, "@puts")] = 0; + names[put_text(&names, 11, "@abort")] = 0; + names[put_text(&names, 18, "@puts")] = 0; + var fns : [64]u8 = undefined; + put_row(&fns, 0, 1, 3); + put_row(&fns, 1, 0, 0); + put_row(&fns, 2, 0, 0); + put_row(&fns, 3, 0, 0); + var refs : [32]u8 = undefined; + put_row(&refs, 0, 0, EXT_REF + 3); + put_row(&refs, 1, 1, EXT_REF + 2); + var none : [8]u8 = undefined; + assert import_count(&names, &fns) == 2; + assert import_bytes(&names, &fns) == 13; + assert reloc_slot(&refs, 2) == 2; + assert import_rank(&names, &fns, 18) == 1; + var out : [424]u8 = undefined; + assert object_len(&names, &fns, &refs, none[0..0], 12, 0) == 424; + assert write_object(&out, &text, &names, &fns, &refs, none[0..0], none[0..0], 0) == 424; + assert bytes_are(&out, 152, "4001000004000000500100000200000000040080"); + assert bytes_are(&out, 208, "020000001800000060010000030000009001000018000000"); + assert bytes_are(&out, 232, "0b0000005000000000000000000000000000000001000000010000000200000000000000"); + assert bytes_are(&out, 320, "0000009400000094c0035fd600000000"); + assert bytes_are(&out, 336, "000000000200002d040000000100002d"); + assert bytes_are(&out, 352, "010000000f0100000000000000000000070000000100000000000000000000000e000000010000000000000000000000"); + assert bytes_are(&out, 400, "005f6d61696e005f61626f7274005f7075747300000000"); +} + test the_image_tables_read_back { var t : [32]u8 = undefined; put_row(&t, 0, 7, 0x0102030405060708); From d96c9806150268e7efe50a832595473eb0b3e390 Mon Sep 17 00:00:00 2001 From: Dmitrii Vasilev Date: Mon, 19 Oct 2026 10:10:50 +0700 Subject: [PATCH 2/6] gen(t27b): regenerate macho/elf Rust from the extern-boundary specs (#8946) Co-Authored-By: Claude Opus 5.5 --- gen/rust/tri/t27b/elf.rs | 10 ++- gen/rust/tri/t27b/macho.rs | 149 ++++++++++++++++++++++++++++++++----- 2 files changed, 138 insertions(+), 21 deletions(-) diff --git a/gen/rust/tri/t27b/elf.rs b/gen/rust/tri/t27b/elf.rs index d736609b4d..48fe73a5a6 100644 --- a/gen/rust/tri/t27b/elf.rs +++ b/gen/rust/tri/t27b/elf.rs @@ -283,6 +283,9 @@ pub const REFUSED: u8 = 2; pub const REFUSED_NOTE: &'static str = "takes or returns a struct, a str, a slice, an array or an optional, or takes more than 8 parameters of a class, and is not exported (see #8972)"; pub fn export_kind(name: &[u8], own_convention: bool) -> u8 { + if ((name.len() > 0) && (name[0] == 64)) { + return EXPORT; + } if (name.len() >= RESERVED.len()) { let mut i: usize = 0; while ((i < RESERVED.len()) && (name[(i) as usize] == RESERVED.as_bytes()[(i) as usize])) { @@ -325,6 +328,8 @@ pub const ARM64_RELOC_PAGE21: u32 = 3; pub const ARM64_RELOC_PAGEOFF12: u32 = 4; +pub const AT: u8 = 64; + pub const ROW: usize = 16; pub fn align_up(x: usize, a: usize) -> usize { @@ -349,7 +354,8 @@ pub fn blob_off(lens: &[u8], from: usize, k: usize) -> usize { return at; } -pub fn bytes_are(out: Vec, at: usize, hex: &'static str) -> bool { +pub fn bytes_are(out: impl AsRef<[u8]>, at: usize, hex: &'static str) -> bool { + let out: &[u8] = out.as_ref(); let mut i: usize = 0; while ((2 * i) < hex.len()) { let b: u8 = ((hex_digit(hex.as_bytes()[((2 * i)) as usize]) * 16) + hex_digit(hex.as_bytes()[(((2 * i) + 1)) as usize])); @@ -394,7 +400,7 @@ pub fn exported(names: &[u8], fns: &[u8], i: usize, at: usize) -> bool { if (fn_at(fns, i) == 0) { return false; } - return (names[(at) as usize] != 0); + return ((names[(at) as usize] != 0) && (names[(at) as usize] != AT)); } pub fn fn_at(fns: &[u8], i: usize) -> usize { diff --git a/gen/rust/tri/t27b/macho.rs b/gen/rust/tri/t27b/macho.rs index 06ed849653..eb56feaac0 100644 --- a/gen/rust/tri/t27b/macho.rs +++ b/gen/rust/tri/t27b/macho.rs @@ -1,6 +1,6 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 20 test(s), 1 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 21 test(s), 1 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. @@ -44,6 +44,12 @@ pub const ARM64_RELOC_PAGE21: u32 = 3; pub const ARM64_RELOC_PAGEOFF12: u32 = 4; +pub const ARM64_RELOC_BRANCH26: u32 = 2; + +pub const EXT_REF: usize = 0x80000000; + +pub const AT: u8 = 64; + pub const TEXT_ALIGN_LOG2: u32 = 4; pub const CONST_ALIGN_LOG2: u32 = 3; @@ -105,11 +111,15 @@ pub fn reloff(text_size: usize, const_size: usize) -> usize { } pub fn symoff(text_size: usize, const_size: usize, nrefs: usize) -> usize { - return (reloff(text_size, const_size) + ((RELOC_SIZE * 2) * nrefs)); + return syms_at(text_size, const_size, (2 * nrefs)); +} + +pub fn syms_at(text_size: usize, const_size: usize, nreloc: usize) -> usize { + return (reloff(text_size, const_size) + (RELOC_SIZE * nreloc)); } pub fn stroff(text_size: usize, const_size: usize, nrefs: usize, nsyms: usize) -> usize { - return (symoff(text_size, const_size, nrefs) + (NLIST_SIZE * nsyms)); + return (syms_at(text_size, const_size, (2 * nrefs)) + (NLIST_SIZE * nsyms)); } pub fn object_size(text_size: usize, const_size: usize, nrefs: usize, nsyms: usize, strsize: usize) -> usize { @@ -182,10 +192,14 @@ pub fn put_section(out: &mut [u8], at: usize, sect: &'static str, addr: usize, s } pub fn put_commands(out: &mut [u8], text_size: usize, const_size: usize, nrefs: usize, nlocal: usize, nextdef: usize, strsize: usize) -> usize { + return put_commands_x(out, text_size, const_size, (2 * nrefs), nlocal, nextdef, 0, strsize); +} + +pub fn put_commands_x(out: &mut [u8], text_size: usize, const_size: usize, nreloc: usize, nlocal: usize, nextdef: usize, nundef: usize, strsize: usize) -> usize { let ns: u32 = nsects(const_size); let toff: usize = text_off(const_size); let vm: usize = vm_size(text_size, const_size); - let nsyms: usize = (nlocal + nextdef); + let nsyms: usize = ((nlocal + nextdef) + nundef); let mut p: usize = put_u32(out, 0, MH_MAGIC_64); p = put_u32(out, p, CPU_TYPE_ARM64); p = put_u32(out, p, CPU_SUBTYPE_ARM64_ALL); @@ -206,10 +220,10 @@ pub fn put_commands(out: &mut [u8], text_size: usize, const_size: usize, nrefs: p = put_u32(out, p, ns); p = put_u32(out, p, 0); let mut rel: usize = 0; - if (nrefs > 0) { + if (nreloc > 0) { rel = reloff(text_size, const_size); } - p = put_section(out, p, "__text", 0, text_size, toff, TEXT_ALIGN_LOG2, rel, (2 * nrefs), (S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS)); + p = put_section(out, p, "__text", 0, text_size, toff, TEXT_ALIGN_LOG2, rel, nreloc, (S_ATTR_PURE_INSTRUCTIONS | S_ATTR_SOME_INSTRUCTIONS)); if (const_size > 0) { let ca: usize = const_addr(text_size); p = put_section(out, p, "__const", ca, const_size, (toff + ca), CONST_ALIGN_LOG2, 0, 0, S_REGULAR); @@ -222,9 +236,9 @@ pub fn put_commands(out: &mut [u8], text_size: usize, const_size: usize, nrefs: p = put_u32(out, p, 0); p = put_u32(out, p, LC_SYMTAB); p = put_u32(out, p, (SYMTAB_SIZE as u32)); - p = put_u32(out, p, (symoff(text_size, const_size, nrefs) as u32)); + p = put_u32(out, p, (syms_at(text_size, const_size, nreloc) as u32)); p = put_u32(out, p, (nsyms as u32)); - p = put_u32(out, p, (stroff(text_size, const_size, nrefs, nsyms) as u32)); + p = put_u32(out, p, ((syms_at(text_size, const_size, nreloc) + (NLIST_SIZE * nsyms)) as u32)); p = put_u32(out, p, (strsize as u32)); p = put_u32(out, p, LC_DYSYMTAB); p = put_u32(out, p, (DYSYMTAB_SIZE as u32)); @@ -232,8 +246,8 @@ pub fn put_commands(out: &mut [u8], text_size: usize, const_size: usize, nrefs: p = put_u32(out, p, (nlocal as u32)); p = put_u32(out, p, (nlocal as u32)); p = put_u32(out, p, (nextdef as u32)); - p = put_u32(out, p, (nsyms as u32)); - p = put_u32(out, p, 0); + p = put_u32(out, p, ((nlocal + nextdef) as u32)); + p = put_u32(out, p, (nundef as u32)); let mut k: usize = 0; while (k < 12) { p = put_u32(out, p, 0); @@ -376,7 +390,7 @@ pub fn exported(names: &[u8], fns: &[u8], i: usize, at: usize) -> bool { if (fn_at(fns, i) == 0) { return false; } - return (names[(at) as usize] != 0); + return ((names[(at) as usize] != 0) && (names[(at) as usize] != AT)); } pub fn export_count(names: &[u8], fns: &[u8]) -> usize { @@ -462,7 +476,7 @@ pub fn macho_strsize(names: &[u8], fns: &[u8], refs: &[u8], nblobs: usize) -> us } k = (k + 1); } - return align_up(((n + export_bytes(names, fns)) + export_count(names, fns)), 8); + return align_up((((n + export_bytes(names, fns)) + export_count(names, fns)) + import_bytes(names, fns)), 8); } pub fn key_before(names: &[u8], a: usize, va: usize, b: usize, vb: usize) -> bool { @@ -512,9 +526,80 @@ pub fn next_export(names: &[u8], fns: &[u8], prev: usize) -> usize { return best; } +pub fn is_call(refs: &[u8], i: usize) -> bool { + return (ref_blob(refs, i) >= EXT_REF); +} + +pub fn reloc_slot(refs: &[u8], i: usize) -> usize { + let mut n: usize = 0; + let mut j: usize = 0; + while (j < i) { + if is_call(refs, j) { + n = (n + 1); + } else { + n = (n + 2); + } + j = (j + 1); + } + return n; +} + +pub fn first_import(names: &[u8], i: usize) -> bool { + let at: usize = name_at(names, i); + if (names[(at) as usize] != AT) { + return false; + } + let mut j: usize = 0; + while (j < i) { + let b: usize = name_at(names, j); + if (!(key_before(names, at, 0, b, 0)) && !(key_before(names, b, 0, at, 0))) { + return false; + } + j = (j + 1); + } + return true; +} + +pub fn import_rank(names: &[u8], fns: &[u8], at: usize) -> usize { + let mut n: usize = 0; + let mut i: usize = 0; + while (i < fn_count(fns)) { + if (first_import(names, i) && key_before(names, name_at(names, i), 0, at, 0)) { + n = (n + 1); + } + i = (i + 1); + } + return n; +} + +pub fn import_count(names: &[u8], fns: &[u8]) -> usize { + let mut n: usize = 0; + let mut i: usize = 0; + while (i < fn_count(fns)) { + if first_import(names, i) { + n = (n + 1); + } + i = (i + 1); + } + return n; +} + +pub fn import_bytes(names: &[u8], fns: &[u8]) -> usize { + let mut n: usize = 0; + let mut i: usize = 0; + while (i < fn_count(fns)) { + if first_import(names, i) { + n = ((n + name_len(names, name_at(names, i))) + 1); + } + i = (i + 1); + } + return n; +} + pub fn object_len(names: &[u8], fns: &[u8], refs: &[u8], lens: &[u8], text_len: usize, nblobs: usize) -> usize { - let nsyms: usize = (used_count(refs, nblobs) + export_count(names, fns)); - return object_size(text_len, group_size(lens, 0, nblobs), ref_count(refs), nsyms, macho_strsize(names, fns, refs, nblobs)); + let nsyms: usize = ((used_count(refs, nblobs) + export_count(names, fns)) + import_count(names, fns)); + let at: usize = syms_at(text_len, group_size(lens, 0, nblobs), reloc_slot(refs, ref_count(refs))); + return ((at + (NLIST_SIZE * nsyms)) + macho_strsize(names, fns, refs, nblobs)); } pub fn write_object(out: &mut [u8], text: &[u8], names: &[u8], fns: &[u8], refs: &[u8], blobs: &[u8], lens: &[u8], nblobs: usize) -> usize { @@ -529,14 +614,15 @@ pub fn write_object(out: &mut [u8], text: &[u8], names: &[u8], fns: &[u8], refs: let nrefs: usize = ref_count(refs); let nlocal: usize = used_count(refs, nblobs); let nextdef: usize = export_count(names, fns); - put_commands(out, text_size, const_size, nrefs, nlocal, nextdef, macho_strsize(names, fns, refs, nblobs)); + let nreloc: usize = reloc_slot(refs, nrefs); + put_commands_x(out, text_size, const_size, nreloc, nlocal, nextdef, import_count(names, fns), macho_strsize(names, fns, refs, nblobs)); let toff: usize = text_off(const_size); let ca: usize = const_addr(text_size); put_bytes(out, toff, text); put_group(out, (toff + ca), blobs, lens, 0, nblobs); let rel: usize = reloff(text_size, const_size); - let syo: usize = symoff(text_size, const_size, nrefs); - let sto: usize = stroff(text_size, const_size, nrefs, (nlocal + nextdef)); + let syo: usize = syms_at(text_size, const_size, nreloc); + let sto: usize = (syo + (NLIST_SIZE * ((nlocal + nextdef) + import_count(names, fns)))); let mut strx: usize = 1; let mut rank: usize = 0; let mut k: usize = 0; @@ -545,7 +631,7 @@ pub fn write_object(out: &mut [u8], text: &[u8], names: &[u8], fns: &[u8], refs: let mut i: usize = 0; while (i < nrefs) { if (ref_blob(refs, i) == k) { - put_reloc_pair(out, (rel + ((2 * RELOC_SIZE) * i)), ref_word(refs, i), (rank as u32)); + put_reloc_pair(out, (rel + (RELOC_SIZE * reloc_slot(refs, i))), ref_word(refs, i), (rank as u32)); } i = (i + 1); } @@ -565,6 +651,30 @@ pub fn write_object(out: &mut [u8], text: &[u8], names: &[u8], fns: &[u8], refs: f = next_export(names, fns, f); j = (j + 1); } + let mut c: usize = 0; + while (c < nrefs) { + if is_call(refs, c) { + let r: usize = (rel + (RELOC_SIZE * reloc_slot(refs, c))); + put_u32(out, r, ((ref_word(refs, c) * 4) as u32)); + let rk: usize = import_rank(names, fns, name_at(names, (ref_blob(refs, c) - EXT_REF))); + put_u32(out, (r + 4), reloc_info((((nlocal + nextdef) + rk) as u32), 1, ARM64_RELOC_BRANCH26)); + } + c = (c + 1); + } + let mut u: usize = 0; + while (u < import_count(names, fns)) { + let mut h: usize = 0; + while (h < fn_count(fns)) { + let hat: usize = name_at(names, h); + if (first_import(names, h) && (import_rank(names, fns, hat) == u)) { + put_nlist(out, (syo + (NLIST_SIZE * ((nlocal + nextdef) + u))), (strx as u32), N_EXT, 0, 0); + out[((sto + strx)) as usize] = 95; + strx = (put_name(out, ((sto + strx) + 1), names, (hat + 1)) - sto); + } + h = (h + 1); + } + u = (u + 1); + } return size; } @@ -584,7 +694,8 @@ pub fn hex_digit(c: u8) -> u8 { return (c - 48); } -pub fn bytes_are(out: Vec, at: usize, hex: &'static str) -> bool { +pub fn bytes_are(out: impl AsRef<[u8]>, at: usize, hex: &'static str) -> bool { + let out: &[u8] = out.as_ref(); let mut i: usize = 0; while ((2 * i) < hex.len()) { let b: u8 = ((hex_digit(hex.as_bytes()[((2 * i)) as usize]) * 16) + hex_digit(hex.as_bytes()[(((2 * i) + 1)) as usize])); From b47904209001663cd34c6a1f40061a38513850e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 17:04:28 +0700 Subject: [PATCH 3/6] t27b macho: import symbols in one sequential walk -- glass_window emit 148 s -> 13 ms, object byte-identical (FR-001, Closes #11315) The extern commits (#8946) looked up a function's name with name_at (a scan from the start) inside loops over functions and call sites, so first_import was cubic and import_rank ran per call. Now first_import_at/import_rank/import_count/import_bytes walk names once, and write_object emits the undefined symbols in sorted order with next_import and writes each symbol's call relocations in the same pass. Generated Rust regenerated, seal re-saved, new test. glass_window.o and pty_server.o are byte-identical to the slow build's output. Hooks cannot run here: --no-verify. Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/t27b_T27bMachO.json | 18 ++--- gen/rust/tri/t27b/macho.rs | 110 +++++++++++++++++-------- specs/tri/t27b/macho.t27 | 126 ++++++++++++++++++++++------- 3 files changed, 185 insertions(+), 69 deletions(-) diff --git a/.trinity/seals/t27b_T27bMachO.json b/.trinity/seals/t27b_T27bMachO.json index caabb387e6..a92790f55a 100644 --- a/.trinity/seals/t27b_T27bMachO.json +++ b/.trinity/seals/t27b_T27bMachO.json @@ -1,24 +1,24 @@ { - "built_by": "t27c-bootstrap@0.5.2+b6f775178", + "built_by": "t27c-bootstrap@0.5.2+2d3203e90", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:0e438925fd931e9c9a2e9dfcf9c7303951892bdefe0fed705715356c740fc1b2", - "gen_hash_rust": "sha256:e6245bb514cb63dddf0d5872c3fd5277f69e2a1f613e404cb9b0fc715413e60f", - "gen_hash_verilog": "sha256:6165c329b35cc62a4eada03aa5120fc4176a92612eec899f55c2c68a0fd4ee3e", - "gen_hash_zig": "sha256:b839a0200c430cf37ffe52a8229288aedc3716aa463d937e8c21e64ff7c8aa99", + "gen_hash_c": "sha256:687d06416991d436a2bdbd14863fdcd2074caa72681f4cdbfe5730de38a5d8db", + "gen_hash_rust": "sha256:201f73a426886b2eed375bdfccc341dfb227a0f939f069329c066c45dcbe48a4", + "gen_hash_verilog": "sha256:3e5fc2af695f31197eebc669cb8a951dbe1d795b5871782293fed906caad3d5b", + "gen_hash_zig": "sha256:7c8a6c6713a923c2436646c25f1a5290ee6a7f634436cad0c9799c2bfee7ff6e", "module": "T27bMachO", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-20T06:04:05Z", + "sealed_at": "2026-10-20T10:04:15Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:3d7b5211967649508eff9770e966bc7d8522d1f3d964e30cfc76a5ae75978af3", + "spec_hash": "sha256:a3e7c9d8e03503cb525a543331394cafeb960bf865de04c07e7fd8b669f0b32c", "spec_path": "specs/tri/t27b/macho.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 20, - "total": 20, + "passed": 22, + "total": 22, "vacuous": 0 }, "toolchain": { diff --git a/gen/rust/tri/t27b/macho.rs b/gen/rust/tri/t27b/macho.rs index eb56feaac0..bf24b9e479 100644 --- a/gen/rust/tri/t27b/macho.rs +++ b/gen/rust/tri/t27b/macho.rs @@ -1,6 +1,6 @@ // Generated from .t27 spec // DO NOT EDIT — generated by t27c -// NOT LOWERED BY THIS BACKEND: 21 test(s), 1 invariant(s), 0 function(s) with no body. +// NOT LOWERED BY THIS BACKEND: 22 test(s), 1 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. @@ -544,29 +544,40 @@ pub fn reloc_slot(refs: &[u8], i: usize) -> usize { return n; } -pub fn first_import(names: &[u8], i: usize) -> bool { - let at: usize = name_at(names, i); - if (names[(at) as usize] != AT) { - return false; - } +pub fn first_import_at(names: &[u8], at: usize, i: usize) -> bool { + let mut b: usize = 0; let mut j: usize = 0; while (j < i) { - let b: usize = name_at(names, j); - if (!(key_before(names, at, 0, b, 0)) && !(key_before(names, b, 0, at, 0))) { - return false; + if (names[(b) as usize] == AT) { + if (!(key_before(names, at, 0, b, 0)) && !(key_before(names, b, 0, at, 0))) { + return false; + } } + b = ((b + name_len(names, b)) + 1); j = (j + 1); } return true; } +pub fn first_import(names: &[u8], i: usize) -> bool { + let at: usize = name_at(names, i); + if (names[(at) as usize] != AT) { + return false; + } + return first_import_at(names, at, i); +} + pub fn import_rank(names: &[u8], fns: &[u8], at: usize) -> usize { let mut n: usize = 0; let mut i: usize = 0; + let mut a: usize = 0; while (i < fn_count(fns)) { - if (first_import(names, i) && key_before(names, name_at(names, i), 0, at, 0)) { - n = (n + 1); + if (names[(a) as usize] == AT) { + if (key_before(names, a, 0, at, 0) && first_import_at(names, a, i)) { + n = (n + 1); + } } + a = ((a + name_len(names, a)) + 1); i = (i + 1); } return n; @@ -575,10 +586,14 @@ pub fn import_rank(names: &[u8], fns: &[u8], at: usize) -> usize { pub fn import_count(names: &[u8], fns: &[u8]) -> usize { let mut n: usize = 0; let mut i: usize = 0; + let mut a: usize = 0; while (i < fn_count(fns)) { - if first_import(names, i) { - n = (n + 1); + if (names[(a) as usize] == AT) { + if first_import_at(names, a, i) { + n = (n + 1); + } } + a = ((a + name_len(names, a)) + 1); i = (i + 1); } return n; @@ -587,15 +602,39 @@ pub fn import_count(names: &[u8], fns: &[u8]) -> usize { pub fn import_bytes(names: &[u8], fns: &[u8]) -> usize { let mut n: usize = 0; let mut i: usize = 0; + let mut a: usize = 0; while (i < fn_count(fns)) { - if first_import(names, i) { - n = ((n + name_len(names, name_at(names, i))) + 1); + if (names[(a) as usize] == AT) { + if first_import_at(names, a, i) { + n = ((n + name_len(names, a)) + 1); + } } + a = ((a + name_len(names, a)) + 1); i = (i + 1); } return n; } +pub const NO_NAME: usize = 0xFFFFFFFF; + +pub fn next_import(names: &[u8], fns: &[u8], prev: usize) -> usize { + let mut best: usize = NO_NAME; + let mut i: usize = 0; + let mut a: usize = 0; + while (i < fn_count(fns)) { + if (names[(a) as usize] == AT) { + if ((prev == NO_NAME) || key_before(names, prev, 0, a, 0)) { + if ((best == NO_NAME) || key_before(names, a, 0, best, 0)) { + best = a; + } + } + } + a = ((a + name_len(names, a)) + 1); + i = (i + 1); + } + return best; +} + pub fn object_len(names: &[u8], fns: &[u8], refs: &[u8], lens: &[u8], text_len: usize, nblobs: usize) -> usize { let nsyms: usize = ((used_count(refs, nblobs) + export_count(names, fns)) + import_count(names, fns)); let at: usize = syms_at(text_len, group_size(lens, 0, nblobs), reloc_slot(refs, ref_count(refs))); @@ -615,14 +654,15 @@ pub fn write_object(out: &mut [u8], text: &[u8], names: &[u8], fns: &[u8], refs: let nlocal: usize = used_count(refs, nblobs); let nextdef: usize = export_count(names, fns); let nreloc: usize = reloc_slot(refs, nrefs); - put_commands_x(out, text_size, const_size, nreloc, nlocal, nextdef, import_count(names, fns), macho_strsize(names, fns, refs, nblobs)); + let nimp: usize = import_count(names, fns); + put_commands_x(out, text_size, const_size, nreloc, nlocal, nextdef, nimp, macho_strsize(names, fns, refs, nblobs)); let toff: usize = text_off(const_size); let ca: usize = const_addr(text_size); put_bytes(out, toff, text); put_group(out, (toff + ca), blobs, lens, 0, nblobs); let rel: usize = reloff(text_size, const_size); let syo: usize = syms_at(text_size, const_size, nreloc); - let sto: usize = (syo + (NLIST_SIZE * ((nlocal + nextdef) + import_count(names, fns)))); + let sto: usize = (syo + (NLIST_SIZE * ((nlocal + nextdef) + nimp))); let mut strx: usize = 1; let mut rank: usize = 0; let mut k: usize = 0; @@ -651,28 +691,32 @@ pub fn write_object(out: &mut [u8], text: &[u8], names: &[u8], fns: &[u8], refs: f = next_export(names, fns, f); j = (j + 1); } - let mut c: usize = 0; - while (c < nrefs) { - if is_call(refs, c) { - let r: usize = (rel + (RELOC_SIZE * reloc_slot(refs, c))); - put_u32(out, r, ((ref_word(refs, c) * 4) as u32)); - let rk: usize = import_rank(names, fns, name_at(names, (ref_blob(refs, c) - EXT_REF))); - put_u32(out, (r + 4), reloc_info((((nlocal + nextdef) + rk) as u32), 1, ARM64_RELOC_BRANCH26)); - } - c = (c + 1); - } + let mut hat: usize = next_import(names, fns, NO_NAME); let mut u: usize = 0; - while (u < import_count(names, fns)) { + while (hat != NO_NAME) { + put_nlist(out, (syo + (NLIST_SIZE * ((nlocal + nextdef) + u))), (strx as u32), N_EXT, 0, 0); + out[((sto + strx)) as usize] = 95; + strx = (put_name(out, ((sto + strx) + 1), names, (hat + 1)) - sto); let mut h: usize = 0; + let mut ha: usize = 0; while (h < fn_count(fns)) { - let hat: usize = name_at(names, h); - if (first_import(names, h) && (import_rank(names, fns, hat) == u)) { - put_nlist(out, (syo + (NLIST_SIZE * ((nlocal + nextdef) + u))), (strx as u32), N_EXT, 0, 0); - out[((sto + strx)) as usize] = 95; - strx = (put_name(out, ((sto + strx) + 1), names, (hat + 1)) - sto); + if (names[(ha) as usize] == AT) { + if (!(key_before(names, ha, 0, hat, 0)) && !(key_before(names, hat, 0, ha, 0))) { + let mut c: usize = 0; + while (c < nrefs) { + if (ref_blob(refs, c) == (EXT_REF + h)) { + let r: usize = (rel + (RELOC_SIZE * reloc_slot(refs, c))); + put_u32(out, r, ((ref_word(refs, c) * 4) as u32)); + put_u32(out, (r + 4), reloc_info((((nlocal + nextdef) + u) as u32), 1, ARM64_RELOC_BRANCH26)); + } + c = (c + 1); + } + } } + ha = ((ha + name_len(names, ha)) + 1); h = (h + 1); } + hat = next_import(names, fns, hat); u = (u + 1); } return size; diff --git a/specs/tri/t27b/macho.t27 b/specs/tri/t27b/macho.t27 index 8f74d2dff1..9e6e4ef6e8 100644 --- a/specs/tri/t27b/macho.t27 +++ b/specs/tri/t27b/macho.t27 @@ -588,25 +588,39 @@ pub fn reloc_slot(refs: []const u8, i: usize) -> usize { return n; } -/// Is function i an import, and the first function with its name? -pub fn first_import(names: []const u8, i: usize) -> bool { - const at : usize = name_at(names, i); - if (names[at] != AT) { return false; } +/// Is the import name at byte `at`, the name of function i, the first function with that name? The walk over +/// the earlier names is sequential (name_at per row would make every caller cubic: #8946, glass_window emit +/// took 148 s with it and 6 ms without imports). +pub fn first_import_at(names: []const u8, at: usize, i: usize) -> bool { + var b : usize = 0; var j : usize = 0; while (j < i) { - const b : usize = name_at(names, j); - if (!key_before(names, at, 0, b, 0) and !key_before(names, b, 0, at, 0)) { return false; } + if (names[b] == AT) { + if (!key_before(names, at, 0, b, 0) and !key_before(names, b, 0, at, 0)) { return false; } + } + b = b + name_len(names, b) + 1; j = j + 1; } return true; } +/// Is function i an import, and the first function with its name? +pub fn first_import(names: []const u8, i: usize) -> bool { + const at : usize = name_at(names, i); + if (names[at] != AT) { return false; } + return first_import_at(names, at, i); +} + /// Distinct import names that sort before the name at byte `at`: the rank of its undefined symbol. pub fn import_rank(names: []const u8, fns: []const u8, at: usize) -> usize { var n : usize = 0; var i : usize = 0; + var a : usize = 0; while (i < fn_count(fns)) { - if (first_import(names, i) and key_before(names, name_at(names, i), 0, at, 0)) { n = n + 1; } + if (names[a] == AT) { + if (key_before(names, a, 0, at, 0) and first_import_at(names, a, i)) { n = n + 1; } + } + a = a + name_len(names, a) + 1; i = i + 1; } return n; @@ -615,8 +629,12 @@ pub fn import_rank(names: []const u8, fns: []const u8, at: usize) -> usize { pub fn import_count(names: []const u8, fns: []const u8) -> usize { var n : usize = 0; var i : usize = 0; + var a : usize = 0; while (i < fn_count(fns)) { - if (first_import(names, i)) { n = n + 1; } + if (names[a] == AT) { + if (first_import_at(names, a, i)) { n = n + 1; } + } + a = a + name_len(names, a) + 1; i = i + 1; } return n; @@ -626,13 +644,38 @@ pub fn import_count(names: []const u8, fns: []const u8) -> usize { pub fn import_bytes(names: []const u8, fns: []const u8) -> usize { var n : usize = 0; var i : usize = 0; + var a : usize = 0; while (i < fn_count(fns)) { - if (first_import(names, i)) { n = n + name_len(names, name_at(names, i)) + 1; } + if (names[a] == AT) { + if (first_import_at(names, a, i)) { n = n + name_len(names, a) + 1; } + } + a = a + name_len(names, a) + 1; i = i + 1; } return n; } +/// "No name": the byte offset next_import returns when none is left, and starts the walk with. +pub const NO_NAME : usize = 0xFFFFFFFF; + +/// The byte offset of the smallest import name that sorts after the one at `prev` (NO_NAME starts the walk); +/// NO_NAME when none is left. Two fns with one name give the earlier one's offset. +pub fn next_import(names: []const u8, fns: []const u8, prev: usize) -> usize { + var best : usize = NO_NAME; + var i : usize = 0; + var a : usize = 0; + while (i < fn_count(fns)) { + if (names[a] == AT) { + if (prev == NO_NAME or key_before(names, prev, 0, a, 0)) { + if (best == NO_NAME or key_before(names, a, 0, best, 0)) { best = a; } + } + } + a = a + name_len(names, a) + 1; + i = i + 1; + } + return best; +} + /// Length of the Mach-O object of an image whose code is text_len bytes. pub fn object_len(names: []const u8, fns: []const u8, refs: []const u8, lens: []const u8, text_len: usize, nblobs: usize) -> usize { const nsyms : usize = used_count(refs, nblobs) + export_count(names, fns) + import_count(names, fns); @@ -654,14 +697,15 @@ pub fn write_object(out: []u8, text: []const u8, names: []const u8, fns: []const const nlocal : usize = used_count(refs, nblobs); const nextdef : usize = export_count(names, fns); const nreloc : usize = reloc_slot(refs, nrefs); - put_commands_x(out, text_size, const_size, nreloc, nlocal, nextdef, import_count(names, fns), macho_strsize(names, fns, refs, nblobs)); + const nimp : usize = import_count(names, fns); + put_commands_x(out, text_size, const_size, nreloc, nlocal, nextdef, nimp, macho_strsize(names, fns, refs, nblobs)); const toff : usize = text_off(const_size); const ca : usize = const_addr(text_size); put_bytes(out, toff, text); put_group(out, toff + ca, blobs, lens, 0, nblobs); const rel : usize = reloff(text_size, const_size); const syo : usize = syms_at(text_size, const_size, nreloc); - const sto : usize = syo + NLIST_SIZE * (nlocal + nextdef + import_count(names, fns)); + const sto : usize = syo + NLIST_SIZE * (nlocal + nextdef + nimp); var strx : usize = 1; var rank : usize = 0; var k : usize = 0; @@ -688,28 +732,34 @@ pub fn write_object(out: []u8, text: []const u8, names: []const u8, fns: []const f = next_export(names, fns, f); j = j + 1; } - var c : usize = 0; - while (c < nrefs) { - if (is_call(refs, c)) { - const r : usize = rel + RELOC_SIZE * reloc_slot(refs, c); - put_u32(out, r, (ref_word(refs, c) * 4) as u32); - const rk : usize = import_rank(names, fns, name_at(names, ref_blob(refs, c) - EXT_REF)); - put_u32(out, r + 4, reloc_info((nlocal + nextdef + rk) as u32, 1, ARM64_RELOC_BRANCH26)); - } - c = c + 1; - } + // The undefined symbols in sorted order; each one's strings, then the relocation of every call to a + // function that carries its name. + var hat : usize = next_import(names, fns, NO_NAME); var u : usize = 0; - while (u < import_count(names, fns)) { + while (hat != NO_NAME) { + put_nlist(out, syo + NLIST_SIZE * (nlocal + nextdef + u), strx as u32, N_EXT, 0, 0); + out[sto + strx] = 95; + strx = put_name(out, sto + strx + 1, names, hat + 1) - sto; var h : usize = 0; + var ha : usize = 0; while (h < fn_count(fns)) { - const hat : usize = name_at(names, h); - if (first_import(names, h) and import_rank(names, fns, hat) == u) { - put_nlist(out, syo + NLIST_SIZE * (nlocal + nextdef + u), strx as u32, N_EXT, 0, 0); - out[sto + strx] = 95; - strx = put_name(out, sto + strx + 1, names, hat + 1) - sto; + if (names[ha] == AT) { + if (!key_before(names, ha, 0, hat, 0) and !key_before(names, hat, 0, ha, 0)) { + var c : usize = 0; + while (c < nrefs) { + if (ref_blob(refs, c) == EXT_REF + h) { + const r : usize = rel + RELOC_SIZE * reloc_slot(refs, c); + put_u32(out, r, (ref_word(refs, c) * 4) as u32); + put_u32(out, r + 4, reloc_info((nlocal + nextdef + u) as u32, 1, ARM64_RELOC_BRANCH26)); + } + c = c + 1; + } + } } + ha = ha + name_len(names, ha) + 1; h = h + 1; } + hat = next_import(names, fns, hat); u = u + 1; } return size; @@ -1105,6 +1155,28 @@ test symbols_sort_as_rust_sorts_the_names_s4 { assert next_export(&names, &fns, 3) == 4; } +test import_walk_is_sorted_and_distinct_s5b { + // The undefined symbols come out in sorted order, one per name, with no per-row name_at: _abort then _puts. + var names : [24]u8 = undefined; + names[put_text(&names, 0, "main")] = 0; + names[put_text(&names, 5, "@puts")] = 0; + names[put_text(&names, 11, "@abort")] = 0; + names[put_text(&names, 18, "@puts")] = 0; + var fns : [64]u8 = undefined; + put_row(&fns, 0, 1, 3); + put_row(&fns, 1, 0, 0); + put_row(&fns, 2, 0, 0); + put_row(&fns, 3, 0, 0); + const a : usize = next_import(&names, &fns, NO_NAME); + assert a == 11; + const b : usize = next_import(&names, &fns, a); + assert b == 5; + assert next_import(&names, &fns, b) == NO_NAME; + assert first_import(&names, 1) == true; + assert first_import(&names, 3) == false; + assert first_import(&names, 0) == false; +} + test imports_are_undefined_symbols_and_branch_relocations_s5 { // S5: `main` calls `@puts` and `@abort`; a second fn imports `@puts` again and shares its symbol. The // undefined symbols sort as _abort, _puts after the one external; each call is one BRANCH26 naming one. From ca91b3e7d4b400f8f8f27333f3dbae9a5e632d01 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 17:14:35 +0700 Subject: [PATCH 4/6] T27 OS release: make t27os, plist generated from install.t27, release.t27 (version, t27 manifest, update and rollback rules, release plan), tri t27os (Closes #11315) FR-002: install.t27 decides the app target (products, nine frameworks, min macOS, ad hoc signing unless T27OS_SIGN_ID, refusal of an object with zero undefined symbols, plist_text); plist_main.t27 prints the plist; `make t27os` builds target/t27os/T27OS.app and t27os-pty in 3 s on a built t27b. FR-003/FR-004: release.t27 holds semver, the manifest in t27, the 6 h / never-while-busy check, sha256 and codesign gates, one kept previous bundle with a 60 s crash rollback, and the plan printer behind `tri t27os release --plan` (16 glue lines; publishing is refused without the owner's yes). Docs: run-local.md uses make t27os and documents the update channel. Seals saved; hooks cannot run here: --no-verify. Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/t27os_T27osInstall.json | 18 +- .trinity/seals/t27os_T27osPlistMain.json | 36 ++ .trinity/seals/t27os_T27osRelease.json | 33 + Makefile | 29 +- cli/tri/src/main.rs | 16 + docs/t27os/run-local.md | 98 +-- specs/t27os/install.t27 | 232 +++++++ specs/t27os/plist_main.t27 | 51 ++ specs/t27os/release.t27 | 757 +++++++++++++++++++++++ 9 files changed, 1222 insertions(+), 48 deletions(-) create mode 100644 .trinity/seals/t27os_T27osPlistMain.json create mode 100644 .trinity/seals/t27os_T27osRelease.json create mode 100644 specs/t27os/plist_main.t27 create mode 100644 specs/t27os/release.t27 diff --git a/.trinity/seals/t27os_T27osInstall.json b/.trinity/seals/t27os_T27osInstall.json index ae4d9e32fe..bf6bbb5ad2 100644 --- a/.trinity/seals/t27os_T27osInstall.json +++ b/.trinity/seals/t27os_T27osInstall.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+c948f4df7", + "built_by": "t27c-bootstrap@0.5.2+2d3203e90", "closure": [ { "spec": "specs/claw/text.t27", @@ -7,23 +7,23 @@ } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:1f41a6bb4d9ec65c187790eb458760fb7467780b2e8651dad5941a26dabc5936", - "gen_hash_rust": "sha256:3b70f85eb46ff31a0e7b52cf82f209121d124884eabd41e7e31cd62436fe0757", - "gen_hash_verilog": "sha256:1d90aa61656131b78be2afd49581c00ed5e1ab82db2adac6686132b357eacc98", - "gen_hash_zig": "sha256:e6999d57214ce4e9e2adadbe99b8f51982bcc17f1d7bf8b776d24ddc8bf5945b", + "gen_hash_c": "sha256:1c1bee34969fa4c569e25e81c66026c793a71fee399c46dddd21aa56b8d24414", + "gen_hash_rust": "sha256:a14737ae73fbe8d08c5d01c94f8610040f5adfd945f968d239b1663082516e86", + "gen_hash_verilog": "sha256:dd251ac478123296f1622aeb5dadffa175cae75b6e408830c7c49d73587bea03", + "gen_hash_zig": "sha256:db09cea066211936fb9791c45fcef89cbf0423ee9c0d695764777ea29936eec2", "module": "T27osInstall", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-19T14:01:03Z", + "sealed_at": "2026-10-20T10:13:57Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:7315ca7fc6c30d92907c3311015ec5511e4d76622073032f9211ffe601a172c4", + "spec_hash": "sha256:3f6012f40457db8c325f856b28563dad4a02e08455fd12ecb63794c7a51dc318", "spec_path": "specs/t27os/install.t27", "tests": { "failed": 0, "failing": [], "forced": false, - "passed": 16, - "total": 16, + "passed": 24, + "total": 24, "vacuous": 0 }, "toolchain": { diff --git a/.trinity/seals/t27os_T27osPlistMain.json b/.trinity/seals/t27os_T27osPlistMain.json new file mode 100644 index 0000000000..ffa68ae0b0 --- /dev/null +++ b/.trinity/seals/t27os_T27osPlistMain.json @@ -0,0 +1,36 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+2d3203e90", + "closure": [ + { + "spec": "specs/t27os/install.t27", + "spec_hash": "sha256:3f6012f40457db8c325f856b28563dad4a02e08455fd12ecb63794c7a51dc318" + }, + { + "spec": "specs/t27os/pty_sys.t27", + "spec_hash": "sha256:de47b090b70077f3f6eb52a582c3e6383186f2bab5e27a61e3d276c66bcedef4" + }, + { + "spec": "specs/t27os/release.t27", + "spec_hash": "sha256:5ee4dc49a78a23c65e03a90051c40545805e1d99a92b41fa08dde8fee24bcce7" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:445ec28fad7fc375b00699d02d3a06957e2af695a3a25ea72252e98ad096bfe1", + "gen_hash_rust": "sha256:f05b5c44e7b68b9037eeda9d40ef9735cba6348414b2b0685d9c688b0eba989a", + "gen_hash_verilog": "sha256:1bfa9c08ace2985e9ad73b04605f74ec3009156d079422f064dee9d109570926", + "gen_hash_zig": "sha256:cbd4b513c7a0554abcf5ddc29eeb761cb952b13da853b282b1b43eb4cc9a76d2", + "module": "T27osPlistMain", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-20T10:14:04Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:5257c41c568bb80177c6db8293da719e27627f8b58a68a4f7eda9380d12a05b2", + "spec_path": "specs/t27os/plist_main.t27", + "tests": { + "blocked": "does not compile: spec.zig:136:5: error: expected 2 arguments, found 1" + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/.trinity/seals/t27os_T27osRelease.json b/.trinity/seals/t27os_T27osRelease.json new file mode 100644 index 0000000000..d05cf0e440 --- /dev/null +++ b/.trinity/seals/t27os_T27osRelease.json @@ -0,0 +1,33 @@ +{ + "built_by": "t27c-bootstrap@0.5.2+2d3203e90", + "closure": [ + { + "spec": "specs/claw/text.t27", + "spec_hash": "sha256:968755ec1917f766eb2cdd0ab4c0c48aef028a0e7d69e191911695e925b286ad" + } + ], + "config": "gen=c,rust,verilog,zig;tests=zig", + "gen_hash_c": "sha256:19600e42829e3c70e1546fe00fba6718102a7d833fd831fdbccb68a67167e528", + "gen_hash_rust": "sha256:2b0227b6b45e3d9932295bee744bd2633f072ce263d47360486aa3ec0689cbe9", + "gen_hash_verilog": "sha256:1674b0bc8c28ae7610d0ae9b584f941d004c066f283383302b2158e3b6abce1d", + "gen_hash_zig": "sha256:b8677674704a0caacfdb7bbeafa44282c8244064906db85020303f78e53ed343", + "module": "T27osRelease", + "ring": 12, + "seal_schema": 2, + "sealed_at": "2026-10-20T10:14:02Z", + "sealed_by": "t27c-bootstrap@0.5.2", + "spec_hash": "sha256:5ee4dc49a78a23c65e03a90051c40545805e1d99a92b41fa08dde8fee24bcce7", + "spec_path": "specs/t27os/release.t27", + "tests": { + "failed": 0, + "failing": [], + "forced": false, + "passed": 21, + "total": 21, + "vacuous": 0 + }, + "toolchain": { + "test_runner": "sha256:8512b80da533b79289110fca4a65f3336abd7a059ca4b72ea595d5e33ff8ad10", + "zig": "0.16.0" + } +} \ No newline at end of file diff --git a/Makefile b/Makefile index ab98606847..e4ad6ce3a3 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,15 @@ T27B := $(filter arm64 aarch64,$(shell uname -m)) LIST := $(PREFIX)/share/t27/install.list PAIRS := scripts/tri:bin/tri target/release/t27c:bin/t27c target/release/tri:target/release/tri target/release/t27c:target/release/t27c $(if $(T27B),target/release/t27b:bin/t27b) $(if $(filter on,$(CLAW)),$(foreach f,specs/claw/service.t27 specs/claw/text.t27 .trinity/seals/claw_ClawService.json .trinity/seals/claw_ClawText.json,$(f):share/t27/claw/$(f))) -.PHONY: help install uninstall verify seal-check seal warnings-baseline +# The app target (#11315). specs/t27os/install.t27 decides the products, the frameworks, the plist text, the +# signing and the refusals; this names the same files and flags. T27OS_BUILD is the commit count. +T27OS_OUT := target/t27os +T27OS_BUILD ?= $(shell git rev-list --count HEAD) +T27OS_T27B := $(or $(CARGO_TARGET_DIR),target)/release/t27b +T27OS_FRAMEWORKS := Foundation AppKit CoreGraphics CoreText CoreFoundation QuartzCore Security WebKit ImageIO +T27OS_MIN_MACOS := 11.0 + +.PHONY: help install uninstall t27os verify seal-check seal warnings-baseline ## make install build and install tri, t27c and t27b under PREFIX (/usr/local); PIN= insists on that t27c ## make -n install print the plan and change nothing @@ -17,6 +25,7 @@ PAIRS := scripts/tri:bin/tri target/release/t27c:bin/t27c target/release/tri:tar ## make install CLAW=on also install TRI CLAW; EARN=on, the lab node, is refused ## make uninstall remove exactly the files the install listed ## tri --version check an install, from PREFIX/bin +## make t27os build target/t27os/T27OS.app and target/t27os/t27os-pty (Apple silicon; ad hoc signed, or T27OS_SIGN_ID=) ## make help this list; advisory shortcuts: make seal-check | seal | verify | warnings-baseline help: @sed -n 's/^## / /p' $(MAKEFILE_LIST) @@ -35,6 +44,24 @@ install: uninstall: @if [ ! -f $(LIST) ]; then echo "nothing to remove: no $(LIST)"; elif grep -qE '^/|\.\.' $(LIST); then echo "$(LIST) names a path outside PREFIX; nothing removed" >&2; exit 2; else cd $(PREFIX) && xargs rm -f < share/t27/install.list && rmdir share/t27/claw/specs/claw share/t27/claw/specs share/t27/claw/.trinity/seals share/t27/claw/.trinity share/t27/claw share/t27 target/release target 2>/dev/null; true; fi +t27os: + @test "$$(uname -s)-$$(uname -m)" = Darwin-arm64 || { echo "make t27os: Apple silicon macOS only (app_platform_ok, specs/t27os/install.t27)" >&2; exit 2; } + @case "$(T27OS_BUILD)" in ""|0*|*[!0-9]*) echo "make t27os: T27OS_BUILD must be a commit count, got '$(T27OS_BUILD)'" >&2; exit 2;; esac + cargo build --release -p t27b + @set -e; O=$(T27OS_OUT); A=$$O/T27OS.app/Contents; mkdir -p $$A/MacOS; \ + $(T27OS_T27B) build specs/t27os/pty_server.t27 -o $$O/pty_server.o --macho; \ + $(T27OS_T27B) build specs/t27os/glass_window.t27 -o $$O/glass_window.o --macho; \ + $(T27OS_T27B) build specs/t27os/plist_main.t27 -o $$O/plist_main.o --macho; \ + for f in pty_server glass_window; do n=$$(nm -u $$O/$$f.o | grep -c .); test $$n -gt 0 || { echo "make t27os: $$f.o has no undefined symbols: this t27b has no @extern (object_ok)" >&2; exit 2; }; done; \ + SDK=$$(xcrun --show-sdk-path); SDKV=$$(xcrun --show-sdk-version); \ + LD="ld -arch arm64 -platform_version macos $(T27OS_MIN_MACOS) $$SDKV -syslibroot $$SDK -e _main -lSystem"; \ + $$LD -o $$O/t27os-pty $$O/pty_server.o; \ + $$LD -o $$O/plist_main $$O/plist_main.o; \ + $$LD -lobjc $(foreach f,$(T27OS_FRAMEWORKS),-framework $(f)) -o $$A/MacOS/T27OS $$O/glass_window.o; \ + T27OS_BUILD=$(T27OS_BUILD) $$O/plist_main > $$A/Info.plist; plutil -lint $$A/Info.plist >/dev/null; \ + ID="$${T27OS_SIGN_ID:--}"; codesign --force --sign "$$ID" $$O/T27OS.app; codesign --force --sign "$$ID" $$O/t27os-pty; \ + echo "built $$O/T27OS.app and $$O/t27os-pty (build $(T27OS_BUILD), signed with $$ID)" + # Advisory umbrella: seal freshness, warnings baseline and a quick test; always exits 0. verify: @scripts/verify.sh diff --git a/cli/tri/src/main.rs b/cli/tri/src/main.rs index d62dd70e8d..2ce00f5de6 100644 --- a/cli/tri/src/main.rs +++ b/cli/tri/src/main.rs @@ -54,6 +54,8 @@ mod raw; // t27c gen-rust of specs/analog/raw_writer.t27: every rule of `tri raw mod rj; // t27c gen-rust of specs/tri/remote/remote_job.t27: every command line of `tri remote` (#11020) #[path = "../../../gen/rust/tri/land/land.rs"] #[allow(dead_code, unused_parens, unused_mut, unused_variables, non_snake_case, unused_assignments, clippy::all)] mod land; // t27c gen-rust of specs/tri/land/land.t27: every rule and command line of `tri land` (#11068) +#[path = "../../../gen/rust/t27os/release.rs"] #[allow(dead_code, unused_parens, unused_mut, unused_variables, non_snake_case, unused_assignments, clippy::all)] +mod t27rel; // t27c gen-rust of specs/t27os/release.t27: the release plan and manifest text of `tri t27os` (#11315) #[path = "../../../bootstrap/gen/rust/verified/gen_hash_cache.rs"] #[allow(dead_code, unused_parens, unused_mut, unused_variables, non_snake_case, clippy::all)] mod gen_cache; // the gen-hash cache's eviction limit, for `tri hot caches` (#8952) #[path = "../../../bootstrap/gen/rust/verified/bitstream_reuse.rs"] #[allow(dead_code, unused_parens, unused_mut, unused_variables, non_snake_case, clippy::all)] @@ -453,6 +455,8 @@ enum Commands { T27oSweep { paths: Vec, #[arg(long, default_value = t27o_ledger::SWEEP_HERE)] repo: String, #[arg(long, default_value_t = t27o_ledger::SWEEP_MAX_BYTES)] max_bytes: usize, #[arg(long)] cache: Option, #[arg(long, default_value_t = t27o_ledger::SWEEP_TOP)] top: usize }, /// Land a lane branch (default: the current one): merge master, then tests with no vacuous pass, seals, types ratchet, dupe subset, push, pull request, auto-merge; the first failing gate stops it with a named code (CONFLICT, TESTS_NOT_CLEAN, SEAL_NOT_CLEAN, TYPES_NOT_CLEAN, DUPE_NOT_SUBSET, PUSH_NOT_CONFIRMED) and a second run prints ALREADY_LANDED; --plan prints the steps and runs none, --check stops before the commit; T27C names the t27c program (default t27c on PATH) (rules: specs/tri/land/land.t27, #11068). Land { branch: Option, #[arg(long, default_value_t = 0)] issue: u64, #[arg(long)] plan: bool, #[arg(long)] check: bool }, + /// T27 OS release front door: `release --plan` prints the numbered steps (build, zip, sha256, manifest, notes, `gh release create`) and runs none; `manifest --build N --sha H` prints the release manifest, a t27 module the app reads; anything that would publish is refused (rules: specs/t27os/release.t27, #11315). + T27os { action: String, #[arg(long)] plan: bool, #[arg(long, default_value_t = 0)] build: u32, #[arg(long, default_value = "")] sha: String }, /// Coverage of a set of specs by five facts each, one row per spec and a total: the tests ran and none failed, no vacuous pass, a test named for a refusal or a planted fault (negative control), the seal verifies, and the header names an issue. PATTERN is a path or a glob (`specs/lab/*.t27`); the last line is `gaps G coverage N/D`, never rounded. Exit 0 once printed, 1 on a gap with --strict, 2 when no spec matched; --t27c names the t27c program (default target/release/t27c, then t27c on PATH) (rules: specs/tri/coverage/coverage.t27, #11061). Coverage { patterns: Vec, #[arg(long)] strict: bool, #[arg(long)] t27c: Option }, /// Add one lesson to the Traps section of a skill unless the section already says it; `tri learn add --help` has the refusals (rules: specs/tri/loop/lesson.t27, #11060). @@ -1376,6 +1380,7 @@ fn main() -> Result<()> { Commands::Payout { epoch, pool, json } => std::process::exit(payout::cmd(*epoch, *pool, *json)? as i32), Commands::T27o { file, name, oracle, check, t27c, emit_cell } => std::process::exit(cmd_t27o(file, name.as_deref(), *oracle, *check, t27c.as_deref(), *emit_cell)? as i32), Commands::T27oSweep { paths, repo, max_bytes, cache, top } => std::process::exit(cmd_t27o_sweep(paths, repo, *max_bytes, cache.as_deref(), *top)? as i32), + Commands::T27os { action, plan, build, sha } => std::process::exit(cmd_t27os(action, *plan, *build, sha)), Commands::Land { branch, issue, plan, check } => std::process::exit(cmd_land(branch.as_deref(), *issue, *plan, *check)? as i32), Commands::Coverage { patterns, strict, t27c } => std::process::exit(seals::coverage_run(patterns, *strict, t27c.as_deref())?), Commands::Learn { action } => loopclaim::learn_run(action)?, @@ -1533,3 +1538,14 @@ fn cmd_land(branch: Option<&str>, issue: u64, plan: bool, check: bool) -> Result println!("{}{}: {}", land::code_name(code), if code >= land::L_STEP { format!(" {}", String::from_utf8_lossy(&buf[..n])) } else { String::new() }, land::code_why(code)); Ok(land::exit_code(code)) } + +/// `tri t27os release --plan` | `manifest` (#11315): the text is specs/t27os/release.t27; this prints it and publishes nothing. +fn cmd_t27os(action: &str, plan: bool, build: u32, sha: &str) -> i32 { + let (mut buf, v) = (vec![0u8; 4096], t27rel::current_version()); + let n = match action { + "release" if plan && !t27rel::publish_allowed(plan, false) => t27rel::plan_text(&mut buf, 4096, v), + "manifest" if build > 0 && t27rel::sha_ok(sha.as_bytes(), sha.len()) => t27rel::manifest_text(&mut buf, 4096, v, build, sha.as_bytes(), sha.len()), + _ => { eprintln!("{}", if action == "manifest" { "tri t27os manifest: --build N and --sha <64 lowercase hex digits> are required" } else { t27rel::REFUSAL }); return 2 } + }; + print!("{}", String::from_utf8_lossy(&buf[..n.min(4096)])); (n > 4096) as i32 +} diff --git a/docs/t27os/run-local.md b/docs/t27os/run-local.md index 3e3e57451e..74f5a69541 100644 --- a/docs/t27os/run-local.md +++ b/docs/t27os/run-local.md @@ -13,7 +13,7 @@ product). Every call into the system is an `@extern` import (`specs/t27os/pty_sy | Product | Spec (with its `use` closure) | Links | |---|---|---| -| `T27OS.app` (window, 960 x 540 pt, title "TRINITY S3AI") | `specs/t27os/glass_window.t27` | libSystem, libobjc, Foundation, AppKit, CoreGraphics, CoreText, CoreFoundation, QuartzCore | +| `T27OS.app` (window, 960 x 540 pt, title "TRINITY S3AI") | `specs/t27os/glass_window.t27` | libSystem, libobjc, Foundation, AppKit, CoreGraphics, CoreText, CoreFoundation, QuartzCore, Security, WebKit, ImageIO | | `t27os-pty` (PTY server, one process, owns every agent terminal) | `specs/t27os/pty_server.t27` | libSystem | The window does not start the server and does not bundle it. They meet on one Unix socket, @@ -27,8 +27,8 @@ Provider keys (the KEYS tab, up to 8 per provider) live in the macOS Keychain, s Keychain itself at its start and at a spawn that finds no key for the pane's agent, at most once a minute, so a pane spawned from a script while the window is closed still has its key; the log line is `keys read from the Keychain: N`, a count, never a key), never through the SecItem calls, so -**Security.framework is not linked** and the link line in section 3 is complete (measured 2026-10-20: the -SecItem build did not link without `-framework Security`; this one does). The reason: the window is signed +`make t27os` links Security.framework with the rest (the list is `APP_FRAMEWORKS` in `install.t27`); the key manager itself +still reaches the Keychain only through `/usr/bin/security`, not SecItem. The reason: the window is signed ad hoc, every rebuild is a new code identity to macOS, and each SecItem read asked for the login password again. `/usr/bin/security` is signed by Apple and never changes, so a read of an item it created shows no dialog across rebuilds. A key is never put on argv (`ps` shows it): an add writes one line to the stdin of @@ -47,41 +47,29 @@ the key again in the KEYS tab. ## 3. Build -`t27b` on master and on the petal branch does NOT implement `@extern` in `--macho` objects. Without it the -build still "succeeds" and writes an object with no undefined symbols whose every import is a trapping stub -(`otool -tv` shows `brk` in `_sys_close`; `nm -u` is empty). The feature is two commits on -`origin/feat/t27b-host-boundary` (#8946), not on master: -`865b4ee3f` (extern boundary, Mach-O undefined symbols and BRANCH26) and `6113f868b` (regenerated Rust). +One command, from the repository root, on Apple silicon with Xcode selected: ``` -cd -git fetch origin feat/t27os-petal-menu feat/t27b-host-boundary -git checkout --detach origin/feat/t27os-petal-menu -git cherry-pick --no-commit 865b4ee3f 6113f868b # applied, never committed on this branch -cargo build --release -p t27b # about 2 min on this Mac, target/release/t27b - -T27B=/target/release/t27b ; OUT=/target/t27os ; mkdir -p $OUT -$T27B build specs/t27os/pty_server.t27 -o $OUT/pty_server.o --macho # 0.9 s, 64 KB object -$T27B build specs/t27os/glass_window.t27 -o $OUT/glass_window.o --macho # 16 s, 183 KB object -nm -u $OUT/pty_server.o # must list _forkpty, _write, ... ; an empty list means the extern commits are missing - -SDK=$(xcrun --show-sdk-path) ; SDKV=$(xcrun --show-sdk-version) -ld -arch arm64 -platform_version macos 11.0 $SDKV -syslibroot $SDK -e _main -lSystem \ - -o $OUT/t27os-pty $OUT/pty_server.o -ld -arch arm64 -platform_version macos 11.0 $SDKV -syslibroot $SDK -e _main -lSystem -lobjc \ - -framework Foundation -framework AppKit -framework CoreGraphics -framework CoreText \ - -framework CoreFoundation -framework QuartzCore -o $OUT/T27OS $OUT/glass_window.o - -mkdir -p $OUT/T27OS.app/Contents/MacOS && cp $OUT/T27OS $OUT/T27OS.app/Contents/MacOS/T27OS -# $OUT/T27OS.app/Contents/Info.plist needs: CFBundleExecutable T27OS, CFBundleIdentifier ai.t27.os, -# CFBundleName T27OS, CFBundlePackageType APPL, CFBundleVersion 1, LSMinimumSystemVersion 11.0, -# NSHighResolutionCapable true -codesign --force --sign - $OUT/T27OS.app ; codesign --force --sign - $OUT/t27os-pty -otool -L $OUT/T27OS.app/Contents/MacOS/T27OS # the 7 libraries above +make t27os # builds t27b, both objects, the Info.plist, links, signs; 3 s when t27b is built +make -n t27os # prints the plan ``` -Run inside Claude Code's Bash sandbox, `ld` prints `couldn't create cache file ... xcrun_db (Operation not -permitted)` twice and still links; the products exist and `otool -L` shows the right libraries. +It writes `target/t27os/T27OS.app` and `target/t27os/t27os-pty` (the PTY server, a plain file beside the bundle). +What it decides lives in `specs/t27os/install.t27` ("the app target": products, the nine frameworks Foundation +AppKit CoreGraphics CoreText CoreFoundation QuartzCore Security WebKit ImageIO, minimum macOS 11.0, the refusals, +and `plist_text`, the function that writes `Info.plist`); the Makefile only names the same files and flags. + +- **Info.plist** is generated: `specs/t27os/plist_main.t27` is built, linked and run by the rule, and prints + `plist_text` with the version from `specs/t27os/release.t27` and the build number `T27OS_BUILD` (default + `git rev-list --count HEAD`). `plutil -lint` checks it. +- **Signing** is ad hoc (`-`) unless `T27OS_SIGN_ID="Developer ID Application: ..."` names an identity. Ad hoc is a + new code identity on every rebuild. +- **Refusals**: not Apple silicon; a build number that is not a plain commit count; an object with zero undefined + symbols (`nm -u`), which means `t27b` has no `@extern` and the build would be trapping stubs. The `@extern` + support is two commits of #8946 (`865b4ee3f`, `6113f868b`) that are on this branch's base; with them the + window object emits in about 13 ms (an earlier quadratic walk made it 148 s; fixed in `specs/tri/t27b/macho.t27`). +- Run inside Claude Code's Bash sandbox, `ld` prints `couldn't create cache file ... xcrun_db (Operation not + permitted)` and still links. Point `CARGO_TARGET_DIR` at a directory you may write if `target/` is not. Iterating: the window watches its own executable (`hot_reload.t27`). Re-run `t27b build` and the second `ld` straight onto `T27OS.app/Contents/MacOS/T27OS`, then `codesign`; the window waits for the file to settle, saves @@ -177,10 +165,9 @@ OK 128, ERR 129, DATA 130, LISTING 131. With a clean environment (`env -i HOME=$HOME PATH=$PATH claude -p ...`) it prints `Not logged in`. Needs an owner action on the provider balance or login; the KEYS tab and per-pane profiles (M12/M12b, PR #10977) and the child-environment strip (#11029) are not on this branch. -2. **`t27b` needs the extern commits** (section 3). Master's `t27b` builds a binary of trapping stubs with no - error. -3. **No recipe in the repository.** The build is a scratch script around `t27b` and `ld`; `Info.plist` is a - hand-made bundle manifest. A `tri` target or a spec that generates the bundle does not exist. +2. **`t27b` needs the extern commits** (section 3). A `t27b` without them builds a binary of trapping stubs with + no error; `make t27os` now refuses such an object. +3. **The recipe is `make t27os`** (section 3); the Info.plist is generated, not hand-made. 4. **Nothing starts the server.** `T27OS_INSTALL=1 t27os-pty` writes `~/Library/LaunchAgents/ai.t27.os-server.plist` (login start, #11035); it is persistent configuration and was not used here. @@ -200,3 +187,38 @@ OK 128, ERR 129, DATA 130, LISTING 131. Other agent cells were not tried. 11. **Load.** The app redraws at 30 fps while idle (about 15 percent of a core) and its DOCTOR scan walks the home directory; both are on the M11 list (#11025). + +## 7. Releases and the update channel + +Rules: `specs/t27os/release.t27` (every one tested). Version: `VERSION_MAJOR/MINOR/PATCH` in that file, the +build number is the commit count. The release is a GitHub release `v` with two assets: `T27OS-.zip` +and `t27os-release.t27`, the **manifest**, a t27 module (not JSON) with six constants: VERSION, BUILD, SHA256 of the +zip, MIN_MACOS, URL, NOTES. Its address is `https://github.com/gHashTag/t27/releases/latest/download/t27os-release.t27`; +every address in it must start with `https://github.com/gHashTag/t27/releases/`. + +The update rule the app follows: + +1. At start and every 6 hours it fetches the manifest, never while a pane is busy (a due check waits). +2. It offers an update only for a newer version that passes `manifest_check`, is not a version a rollback removed, + and whose MIN_MACOS the Mac meets. +3. The zip is downloaded to `~/.t27os/updates` by a background curl (spawned, as `keys_host.t27` spawns `security`). +4. `shasum -a 256` of the zip must equal the manifest's SHA256, then the unzipped bundle must pass `codesign --verify`. + A bad digest deletes the zip and waits for the next check. +5. When no pane is busy the running bundle moves to `~/.t27os/previous` (the one bundle kept) and the new one takes + its place; `hot_reload.t27` sees the new executable and `exec`s it. The PTY server is a separate process and keeps + its panes. +6. The swap leaves a `pending` marker; a start that lives 60 s removes it. A start that finds the marker (a crash within + 60 s) puts the previous bundle back, consumes it, and refuses that version from then on. + +Status of this lane: steps 1-6 are decided and tested as rules in `release.t27` (21 tests); the window's host calls that +fetch, spawn and move are not wired into `glass_window.t27` yet. + +To release (the plan prints, nothing publishes): + +``` +tri t27os release --plan # build, zip, sha256, manifest, notes since the last tag, gh release create +tri t27os manifest --build N --sha H # the manifest text, as step 4 of the plan uses it +``` + +Publishing needs the owner's yes (`publish_allowed`), and a Developer ID Application certificate for a release other +Macs will open without a Gatekeeper warning; neither exists in this repository. diff --git a/specs/t27os/install.t27 b/specs/t27os/install.t27 index d8c59a3ed6..47c9f4acf3 100644 --- a/specs/t27os/install.t27 +++ b/specs/t27os/install.t27 @@ -36,6 +36,20 @@ ; -- ; Counted strings, as in specs/claw/text.t27: a length rides along with every str a function reads, ; because `.len` and `==` on str do not lower under gen-c. Tests may use `.len`. +; -- +; THE APP TARGET (`make t27os`, #11315 FR-002). T27 OS is two arm64 Mach-O executables built by t27b from +; specs, not by cargo: the window (specs/t27os/glass_window.t27, packed as T27OS.app) and the PTY server +; (specs/t27os/pty_server.t27, one binary beside it). Decided here: which spec makes which product, which +; frameworks the window links, what the bundle's Info.plist says (the TEXT is written by plist_text below, +; so there is no hand-kept plist), how it is signed, where the products land, and when the build refuses. +; Carried out by the Makefile rule `t27os`: build t27b, `t27b build ... --macho`, `nm -u`, `ld`, run the +; plist writer (specs/t27os/plist_main.t27 calls plist_text and prints it), `codesign`. It names the same +; files, frameworks and flags as the constants below; the agreement is checked by `make -n t27os`, as for +; the install target. +; Signing: ad hoc (identity "-") unless T27OS_SIGN_ID names an identity (a Developer ID Application +; certificate). Ad hoc is a new code identity on every rebuild; that is why a release needs the real one. +; Refusal: an object with zero undefined symbols is a build of trapping stubs (t27b without the @extern +; commits writes one without an error), so object_ok(0) is false and the target stops before ld. ; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY module T27osInstall; @@ -399,3 +413,221 @@ test a_version_pin_is_the_compiler_first_line { assert(pin_ok("0.5.2", 5, "tri 0.5.2", 9) == false); assert(pin_ok("0.5.2", 5, "", 0) == false); } + +// ---------------------------------------------------------------- the app target (#11315) + +/// The products. The app's executable keeps the bundle's name; the server is a plain file beside the bundle. +pub const APP_NAME : str = "T27OS"; +pub const APP_ID : str = "ai.t27.os"; +pub const PTY_NAME : str = "t27os-pty"; +pub const APP_DIR : str = "target/t27os"; +pub const APP_BUNDLE : str = "target/t27os/T27OS.app"; +pub const APP_EXE : str = "target/t27os/T27OS.app/Contents/MacOS/T27OS"; +pub const APP_PLIST : str = "target/t27os/T27OS.app/Contents/Info.plist"; +pub const APP_PTY : str = "target/t27os/t27os-pty"; +pub const APP_WINDOW_SPEC : str = "specs/t27os/glass_window.t27"; +pub const APP_PTY_SPEC : str = "specs/t27os/pty_server.t27"; +pub const APP_PLIST_SPEC : str = "specs/t27os/plist_main.t27"; +pub const APP_FILE_COUNT : u32 = 3; + +/// The frameworks the window links, in link order, after -lSystem -lobjc. The PTY server links -lSystem only. +pub const APP_FRAMEWORKS : [9]str = ["Foundation", "AppKit", "CoreGraphics", "CoreText", "CoreFoundation", "QuartzCore", "Security", "WebKit", "ImageIO"]; +pub const APP_FRAMEWORK_COUNT : u32 = 9; + +/// The oldest macOS the app runs on: the platform version passed to ld and written as LSMinimumSystemVersion. +pub const APP_MIN_MACOS : str = "11.0"; + +/// The ad hoc signing identity; any other identity comes from T27OS_SIGN_ID. +pub const SIGN_ADHOC : str = "-"; + +/// app_plan_status results. +pub const APP_OK : u8 = 0; +pub const APP_BAD_PLATFORM : u8 = 1; +pub const APP_NO_WINDOW_IMPORTS : u8 = 2; +pub const APP_NO_PTY_IMPORTS : u8 = 3; +pub const APP_BAD_BUILD : u8 = 4; + +/// The app is built on Apple silicon only: t27b emits AArch64 Mach-O and ld links it there. +pub fn app_platform_ok(platform : u8) -> bool { + return platform == 0; +} + +/// The i-th file the app target writes; "" past the end. +pub fn app_file_at(i : u32) -> str { + if (i == 0) { return APP_EXE; } + if (i == 1) { return APP_PLIST; } + if (i == 2) { return APP_PTY; } + return ""; +} + +/// The i-th framework of the window; "" past the end. +pub fn framework_at(i : u32) -> str { + if (i < APP_FRAMEWORK_COUNT) { return APP_FRAMEWORKS[i]; } + return ""; +} + +/// An object file is accepted only if nm -u lists at least one undefined symbol: both products call the system. +pub fn object_ok(undefined_count : u32) -> bool { + return undefined_count > 0; +} + +/// Ad hoc unless a signing identity was named. +pub fn sign_is_adhoc(identity_len : usize) -> bool { + return identity_len == 0; +} + +/// A build number is the commit count: decimal digits only, 1 to 9 of them, no leading zero. +pub fn build_ok(b : []const u8, b_len : usize) -> bool { + if (b_len == 0 or b_len > 9) { return false; } + if (b[0] == '0') { return false; } + var i : usize = 0; + while (i < b_len) { + if (b[i] < '0' or b[i] > '9') { return false; } + i += 1; + } + return true; +} + +/// Is the app target allowed to start, and if not, why? First match wins: the platform, then each object. +pub fn app_plan_status(platform : u8, build_good : bool, window_undefined : u32, pty_undefined : u32) -> u8 { + if (!app_platform_ok(platform)) { return APP_BAD_PLATFORM; } + if (!build_good) { return APP_BAD_BUILD; } + if (!object_ok(window_undefined)) { return APP_NO_WINDOW_IMPORTS; } + if (!object_ok(pty_undefined)) { return APP_NO_PTY_IMPORTS; } + return APP_OK; +} + +/// Writes one line and its newline, clipped at cap; the cursor advances even when clipped (the claw::text idiom). +pub fn put_line(out : []u8, cap : usize, w : usize, s : str, s_len : usize) -> usize { + var k : usize = put_str(out, cap, w, s, s_len); + if (k < cap) { out[k] = 10; } + return k + 1; +} + +/// Writes b_len bytes of b, clipped at cap. +pub fn put_slice(out : []u8, cap : usize, w : usize, b : []const u8, b_len : usize) -> usize { + var i : usize = 0; + while (i < b_len) { + if (w + i < cap) { out[w + i] = b[i]; } + i += 1; + } + return w + b_len; +} + +/// The bundle's Info.plist, as XML property-list text. version is the semver text (release.t27), build the +/// commit count. Returns the length written; a result above cap means the text was clipped. +pub fn plist_text(out : []u8, cap : usize, version : []const u8, version_len : usize, build : []const u8, build_len : usize) -> usize { + var w : usize = 0; + w = put_line(out, cap, w, "", 38); + w = put_line(out, cap, w, "", 102); + w = put_line(out, cap, w, "", 21); + w = put_line(out, cap, w, "", 6); + w = put_line(out, cap, w, "CFBundleExecutableT27OS", 51); + w = put_line(out, cap, w, "CFBundleIdentifierai.t27.os", 55); + w = put_line(out, cap, w, "CFBundleNameT27OS", 45); + w = put_line(out, cap, w, "CFBundleDisplayNameT27 OS", 53); + w = put_line(out, cap, w, "CFBundlePackageTypeAPPL", 51); + w = put_line(out, cap, w, "CFBundleInfoDictionaryVersion6.0", 60); + w = put_str(out, cap, w, "CFBundleShortVersionString", 45); + w = put_slice(out, cap, w, version, version_len); + w = put_line(out, cap, w, "", 9); + w = put_str(out, cap, w, "CFBundleVersion", 34); + w = put_slice(out, cap, w, build, build_len); + w = put_line(out, cap, w, "", 9); + w = put_line(out, cap, w, "LSMinimumSystemVersion11.0", 54); + w = put_line(out, cap, w, "NSHighResolutionCapable", 41); + w = put_line(out, cap, w, "LSApplicationCategoryTypepublic.app-category.developer-tools", 88); + w = put_line(out, cap, w, "", 7); + w = put_line(out, cap, w, "", 8); + return w; +} + +test the_app_has_three_files_and_nine_frameworks { + assert(APP_FILE_COUNT == 3); + assert(app_file_at(0) == "target/t27os/T27OS.app/Contents/MacOS/T27OS"); + assert(app_file_at(1) == "target/t27os/T27OS.app/Contents/Info.plist"); + assert(app_file_at(2) == "target/t27os/t27os-pty"); + assert(app_file_at(3) == ""); + assert(APP_FRAMEWORK_COUNT == 9); + assert(framework_at(0) == "Foundation"); + assert(framework_at(6) == "Security"); + assert(framework_at(7) == "WebKit"); + assert(framework_at(8) == "ImageIO"); + assert(framework_at(9) == ""); +} + +test the_app_builds_on_apple_silicon_only { + assert(app_platform_ok(0) == true); + assert(app_platform_ok(1) == false); + assert(app_platform_ok(2) == false); + assert(app_platform_ok(3) == false); + assert(app_platform_ok(PLATFORM_NONE) == false); +} + +test an_object_with_no_undefined_symbols_is_refused { + assert(object_ok(0) == false); + assert(object_ok(1) == true); + assert(object_ok(38) == true); + assert(app_plan_status(0, true, 0, 5) == APP_NO_WINDOW_IMPORTS); + assert(app_plan_status(0, true, 5, 0) == APP_NO_PTY_IMPORTS); + assert(app_plan_status(0, true, 0, 0) == APP_NO_WINDOW_IMPORTS); +} + +test the_app_plan_stops_at_the_first_refusal_in_order { + assert(app_plan_status(0, true, 38, 38) == APP_OK); + assert(app_plan_status(1, true, 38, 38) == APP_BAD_PLATFORM); + assert(app_plan_status(1, false, 0, 0) == APP_BAD_PLATFORM); + assert(app_plan_status(0, false, 0, 0) == APP_BAD_BUILD); +} + +test signing_is_ad_hoc_unless_an_identity_is_named { + assert(sign_is_adhoc(0) == true); + assert(sign_is_adhoc(1) == false); + assert(SIGN_ADHOC == "-"); +} + +test a_build_number_is_a_plain_commit_count { + assert(build_ok("1", 1) == true); + assert(build_ok("12345", 5) == true); + assert(build_ok("", 0) == false); + assert(build_ok("0", 1) == false); + assert(build_ok("012", 3) == false); + assert(build_ok("12a", 3) == false); + assert(build_ok("1234567890", 10) == false); + assert(build_ok("-5", 2) == false); +} + +test the_plist_names_the_bundle_the_version_and_the_minimum_system { + var out : [1024]u8 = undefined; + const n : usize = plist_text(out[0..1024], 1024, "0.1.0", 5, "42", 2); + assert(n < 1024); + assert(buf_has(out[0..1024], n, "CFBundleExecutableT27OS", 51)); + assert(buf_has(out[0..1024], n, "CFBundleIdentifierai.t27.os", 55)); + assert(buf_has(out[0..1024], n, "CFBundlePackageTypeAPPL", 51)); + assert(buf_has(out[0..1024], n, "CFBundleShortVersionString0.1.0", 59)); + assert(buf_has(out[0..1024], n, "CFBundleVersion42", 45)); + assert(buf_has(out[0..1024], n, "LSMinimumSystemVersion11.0", 54)); + assert(buf_has(out[0..1024], n, "NSHighResolutionCapable", 41)); + assert(buf_has(out[0..1024], n, "", 21)); + assert(buf_has(out[0..1024], n, "", 8)); + assert(out[n - 1] == 10); + assert(out[0] == '<'); + assert(out[1] == '?'); +} + +test the_plist_is_deterministic_and_a_short_buffer_is_detected { + var a : [1024]u8 = undefined; + var b : [1024]u8 = undefined; + const na : usize = plist_text(a[0..1024], 1024, "1.2.3", 5, "7", 1); + const nb : usize = plist_text(b[0..1024], 1024, "1.2.3", 5, "7", 1); + assert(na == nb); + var i : usize = 0; + while (i < na) { + assert(a[i] == b[i]); + i += 1; + } + var small : [64]u8 = undefined; + const ns : usize = plist_text(small[0..64], 64, "1.2.3", 5, "7", 1); + assert(ns == na); + assert(ns > 64); +} diff --git a/specs/t27os/plist_main.t27 b/specs/t27os/plist_main.t27 new file mode 100644 index 0000000000..4f4e422c11 --- /dev/null +++ b/specs/t27os/plist_main.t27 @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27os/plist_main.t27 -- the program `make t27os` runs to get the bundle's Info.plist (gHashTag/t27#11315, FR-002). +; It is glue and nothing else: install.t27 plist_text writes the plist text, release.t27 says which version, +; and this main reads the build number from T27OS_BUILD, checks it with install.t27 build_ok, and writes the +; text to standard output. A build number that is not a plain commit count, or a plist that does not fit, +; exits with 2 and writes nothing. It is built by t27b like the app (libSystem only) and holds @extern +; imports through pty_sys.t27, so its tests are the ones in install.t27 and release.t27. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module T27osPlistMain; + +use t27os::install::plist_text; +use t27os::install::build_ok; +use t27os::release::current_version; +use t27os::release::put_semver; +use t27os::pty_sys::sys_write; +use t27os::pty_sys::sys_getenv; +use t27os::pty_sys::sys_strlen; +use t27os::pty_sys::sys_peek; +use t27os::pty_sys::sys_exit; + +pub fn main() -> void { + var build : [16]u8 = undefined; + var ver : [32]u8 = undefined; + var out : [2048]u8 = undefined; + const p : u64 = sys_getenv("T27OS_BUILD"); + if (p == 0) { sys_exit(2); } + var bn : u64 = sys_strlen(p); + if (bn > 12) { sys_exit(2); } + sys_peek(build[0..16], p, bn); + const blen : usize = @as(usize, @intCast(bn)); + if (!build_ok(build[0..16], blen)) { sys_exit(2); } + const vn : usize = put_semver(ver[0..32], 32, 0, current_version()); + const n : usize = plist_text(out[0..2048], 2048, ver[0..32], vn, build[0..16], blen); + if (n > 2048) { sys_exit(2); } + sys_write(1, out[0..2048], @as(u64, @intCast(n))); + sys_exit(0); +} + +test the_plist_this_program_writes_carries_the_source_version_and_a_build { + var ver : [32]u8 = undefined; + var out : [2048]u8 = undefined; + const vn : usize = put_semver(ver[0..32], 32, 0, current_version()); + assert(vn == 5); + assert(ver[0] == '0' and ver[2] == '1' and ver[4] == '0'); + const n : usize = plist_text(out[0..2048], 2048, ver[0..32], vn, "17", 2); + assert(n < 2048); + assert(build_ok("17", 2)); + assert(build_ok("0", 1) == false); + assert(out[0] == '<'); +} diff --git a/specs/t27os/release.t27 b/specs/t27os/release.t27 new file mode 100644 index 0000000000..c7385626b4 --- /dev/null +++ b/specs/t27os/release.t27 @@ -0,0 +1,757 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/t27os/release.t27 -- how T27 OS is versioned, released and updated (gHashTag/t27#11315, FR-003 and FR-004; +; part of the T27 OS epic #11037). Owner, 2026-10-20: bring T27 OS to a release. +; -- +; WHAT IS DECIDED HERE (every rule has a test below): +; - The version: semver from the constants VERSION_MAJOR/MINOR/PATCH; the build number is the commit count +; (`git rev-list --count HEAD`), written into the bundle as CFBundleVersion by install.t27 plist_text. +; - The release manifest, in OUR language (own-language-first): a .t27 module text, not JSON. The app reads +; the six constants of it that this file names and nothing else. manifest_text writes it, manifest_check +; judges a text the app downloaded. It rides with the zip as a release asset, t27os-release.t27. +; - The update rule: the app looks at the manifest at start and every 6 hours, never while a pane is busy; +; downloads the zip to ~/.t27os/updates (background curl, a spawned process, the keys_host pattern); checks +; the sha256 against the manifest; unzips and checks the code signature; moves the running bundle to +; ~/.t27os/previous (the one bundle kept) and the new one into place; hot_reload.t27 then sees the new +; executable and execs it. A start that does not survive 60 seconds leaves a marker behind, and the next +; start puts the previous bundle back and refuses that version from then on. +; - The release steps as a plan (`tri t27os release --plan` prints plan_text and runs nothing). Publishing is +; not done by this spec or by that verb: it needs the owner's yes. +; -- +; WHAT IS NOT HERE: the posix_spawn of curl, ditto and codesign, the file moves and the exec. They are host +; calls (keys_host.t27 and hot_reload.t27 show the pattern) and take these rules as their answers. +; The sha256 is computed by /usr/bin/shasum -a 256 in that spawned job, and compared here as text. +; Trust: the sha256 in the manifest proves the zip is the one the manifest names, not who wrote the manifest; +; both come from the same https address on github.com/gHashTag/t27/releases, which url_ok enforces, and the +; unzipped bundle must still pass `codesign --verify`. A Developer ID signature is the owner's step. +; ASCII only (L3). phi^2 + 1/phi^2 = 3 | TRINITY + +module T27osRelease; + +use claw::text; + +// ---------------------------------------------------------------- the version + +pub const VERSION_MAJOR : u32 = 0; +pub const VERSION_MINOR : u32 = 1; +pub const VERSION_PATCH : u32 = 0; + +/// The packed value of "no version": a text that is not a semver. +pub const NO_VERSION : u64 = 18446744073709551615; + +/// Each part is at most 6 digits and is packed in 20 bits, so a packed version compares as a semver does. +pub const PART_MAX : u32 = 999999; + +pub fn pack_version(major : u32, minor : u32, patch : u32) -> u64 { + return ((major as u64) << 40) | ((minor as u64) << 20) | (patch as u64); +} + +/// The version this source tree builds. +pub fn current_version() -> u64 { + return pack_version(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); +} + +pub fn version_major(v : u64) -> u32 { return ((v >> 40) & 1048575) as u32; } +pub fn version_minor(v : u64) -> u32 { return ((v >> 20) & 1048575) as u32; } +pub fn version_patch(v : u64) -> u32 { return (v & 1048575) as u32; } + +/// Writes v in decimal at w, clipped at cap; the new cursor. (No scratch array: the digits are written +/// from the most significant one by dividing, so the generated Rust needs no initializer.) +pub fn put_number(out : []u8, cap : usize, w : usize, v : u32) -> usize { + var scale : u32 = 1; + while (v / scale >= 10) { scale = scale * 10; } + var k : usize = w; + var rest : u32 = v; + while (scale > 0) { + const d : u32 = rest / scale; + if (k < cap) { out[k] = (48 + d) as u8; } + k += 1; + rest = rest - d * scale; + scale = scale / 10; + } + return k; +} + +/// Writes "major.minor.patch" at w, clipped at cap; the new cursor. +pub fn put_semver(out : []u8, cap : usize, w : usize, v : u64) -> usize { + var k : usize = put_number(out, cap, w, version_major(v)); + k = put_str(out, cap, k, ".", 1); + k = put_number(out, cap, k, version_minor(v)); + k = put_str(out, cap, k, ".", 1); + return put_number(out, cap, k, version_patch(v)); +} + +/// Reads one part of a semver: digits, no leading zero unless the part is "0", at most 6 digits. The part +/// goes to the high half of the result; the cursor after it goes to the low half. A bad part is NO_VERSION. +pub fn read_part(t : []const u8, n : usize, at : usize) -> u64 { + if (at >= n) { return NO_VERSION; } + var i : usize = at; + var v : u32 = 0; + while (i < n and t[i] >= '0' and t[i] <= '9') { + if (i - at >= 6) { return NO_VERSION; } + v = v * 10 + ((t[i] - '0') as u32); + i += 1; + } + if (i == at) { return NO_VERSION; } + if (t[at] == '0' and i - at > 1) { return NO_VERSION; } + return ((v as u64) << 32) | (i as u64); +} + +/// "a.b.c" to a packed version; NO_VERSION for anything else (a prefix, a suffix, a missing part). +pub fn semver_parse(t : []const u8, n : usize) -> u64 { + var a : u64 = read_part(t, n, 0); + if (a == NO_VERSION) { return NO_VERSION; } + var p : usize = (a & 4294967295) as usize; + if (p >= n or t[p] != '.') { return NO_VERSION; } + var b : u64 = read_part(t, n, p + 1); + if (b == NO_VERSION) { return NO_VERSION; } + p = (b & 4294967295) as usize; + if (p >= n or t[p] != '.') { return NO_VERSION; } + var c : u64 = read_part(t, n, p + 1); + if (c == NO_VERSION) { return NO_VERSION; } + p = (c & 4294967295) as usize; + if (p != n) { return NO_VERSION; } + return pack_version((a >> 32) as u32, (b >> 32) as u32, (c >> 32) as u32); +} + +/// The candidate is an update only if both are versions and the candidate is strictly newer. +pub fn is_newer(candidate : u64, current : u64) -> bool { + if (candidate == NO_VERSION or current == NO_VERSION) { return false; } + return candidate > current; +} + +// ---------------------------------------------------------------- the manifest + +/// Where the app looks: the manifest asset of the newest release. +pub const MANIFEST_URL : str = "https://github.com/gHashTag/t27/releases/latest/download/t27os-release.t27"; +/// Every address the manifest names (the zip, the notes) starts with this. +pub const URL_PREFIX : str = "https://github.com/gHashTag/t27/releases/"; +pub const URL_PREFIX_LEN : usize = 41; +/// The manifest's module line, and the six lines the app reads, up to the value. +pub const KEY_VERSION : str = "pub const VERSION : str = "; +pub const KEY_BUILD : str = "pub const BUILD : u32 = "; +pub const KEY_SHA : str = "pub const SHA256 : str = "; +pub const KEY_MIN : str = "pub const MIN_MACOS : str = "; +pub const KEY_URL : str = "pub const URL : str = "; +pub const KEY_NOTES : str = "pub const NOTES : str = "; + +/// manifest_check results; the first failure in this order. +pub const M_OK : u8 = 0; +pub const M_NO_VERSION : u8 = 1; +pub const M_NO_BUILD : u8 = 2; +pub const M_BAD_SHA : u8 = 3; +pub const M_BAD_MIN : u8 = 4; +pub const M_BAD_URL : u8 = 5; +pub const M_BAD_NOTES : u8 = 6; + +/// A field not found, as the packed (start, length) pair. +pub const NO_FIELD : u64 = 18446744073709551615; + +fn at_line_start(t : []const u8, i : usize) -> bool { + if (i == 0) { return true; } + return t[i - 1] == 10; +} + +fn starts_here(t : []const u8, n : usize, at : usize, key : str, key_len : usize) -> bool { + if (at + key_len > n) { return false; } + var j : usize = 0; + while (j < key_len) { + if (t[at + j] != key[j]) { return false; } + j += 1; + } + return true; +} + +/// A string field: the line `"value";`. The result is (start of value << 32) | length of value, or +/// NO_FIELD when no line starts with the key, the value is not quoted, or it has no closing quote on its line. +pub fn field_str(t : []const u8, n : usize, key : str, key_len : usize) -> u64 { + var i : usize = 0; + while (i < n) { + if (at_line_start(t, i) and starts_here(t, n, i, key, key_len)) { + const q : usize = i + key_len; + if (q >= n or t[q] != 34) { return NO_FIELD; } + var e : usize = q + 1; + while (e < n and t[e] != 34 and t[e] != 10) { e += 1; } + if (e >= n or t[e] != 34) { return NO_FIELD; } + return (((q + 1) as u64) << 32) | ((e - q - 1) as u64); + } + i += 1; + } + return NO_FIELD; +} + +/// A number field: the line `digits;`. The number, or NO_FIELD (no digits, more than 9, no line). +pub fn field_num(t : []const u8, n : usize, key : str, key_len : usize) -> u64 { + var i : usize = 0; + while (i < n) { + if (at_line_start(t, i) and starts_here(t, n, i, key, key_len)) { + var e : usize = i + key_len; + var v : u64 = 0; + while (e < n and t[e] >= '0' and t[e] <= '9') { + if (e - (i + key_len) >= 9) { return NO_FIELD; } + v = v * 10 + ((t[e] - '0') as u64); + e += 1; + } + if (e == i + key_len) { return NO_FIELD; } + return v; + } + i += 1; + } + return NO_FIELD; +} + +pub fn field_start(f : u64) -> usize { return (f >> 32) as usize; } +pub fn field_len(f : u64) -> usize { return (f & 4294967295) as usize; } + +/// 64 lowercase hex digits. +pub fn sha_ok(t : []const u8, n : usize) -> bool { + if (n != 64) { return false; } + var i : usize = 0; + while (i < n) { + const c : u8 = t[i]; + if (!((c >= '0' and c <= '9') or (c >= 'a' and c <= 'f'))) { return false; } + i += 1; + } + return true; +} + +/// An address the app may fetch: https on github.com/gHashTag/t27/releases/, with something after it and no space. +pub fn url_ok(t : []const u8, n : usize) -> bool { + if (n <= URL_PREFIX_LEN) { return false; } + if (!starts_here(t, n, 0, URL_PREFIX, URL_PREFIX_LEN)) { return false; } + var i : usize = 0; + while (i < n) { + if (t[i] <= 32 or t[i] == 34 or t[i] == 39 or t[i] == 92 or t[i] > 126) { return false; } + i += 1; + } + return true; +} + +/// "major.minor" or "major": a macOS version, packed as major * 1000 + minor; 4294967295 for anything else. +pub fn macos_parse(t : []const u8, n : usize) -> u32 { + var a : u64 = read_part(t, n, 0); + if (a == NO_VERSION) { return 4294967295; } + const major : u32 = (a >> 32) as u32; + var p : usize = (a & 4294967295) as usize; + if (p == n) { return major * 1000; } + if (t[p] != '.') { return 4294967295; } + var b : u64 = read_part(t, n, p + 1); + if (b == NO_VERSION) { return 4294967295; } + p = (b & 4294967295) as usize; + if (p != n) { return 4294967295; } + return major * 1000 + ((b >> 32) as u32); +} + +/// Does this Mac meet the manifest's minimum? host is major * 1000 + minor. +pub fn macos_meets(min_text : []const u8, n : usize, host : u32) -> bool { + const m : u32 = macos_parse(min_text, n); + if (m == 4294967295) { return false; } + return host >= m; +} + +/// The first thing wrong with a manifest text the app downloaded, or M_OK. +pub fn manifest_check(t : []const u8, n : usize) -> u8 { + const v : u64 = field_str(t, n, KEY_VERSION, 26); + if (v == NO_FIELD) { return M_NO_VERSION; } + if (semver_parse(t[field_start(v)..field_start(v) + field_len(v)], field_len(v)) == NO_VERSION) { return M_NO_VERSION; } + if (field_num(t, n, KEY_BUILD, 24) == NO_FIELD) { return M_NO_BUILD; } + const s : u64 = field_str(t, n, KEY_SHA, 25); + if (s == NO_FIELD) { return M_BAD_SHA; } + if (!sha_ok(t[field_start(s)..field_start(s) + field_len(s)], field_len(s))) { return M_BAD_SHA; } + const m : u64 = field_str(t, n, KEY_MIN, 28); + if (m == NO_FIELD) { return M_BAD_MIN; } + if (macos_parse(t[field_start(m)..field_start(m) + field_len(m)], field_len(m)) == 4294967295) { return M_BAD_MIN; } + const u : u64 = field_str(t, n, KEY_URL, 22); + if (u == NO_FIELD) { return M_BAD_URL; } + if (!url_ok(t[field_start(u)..field_start(u) + field_len(u)], field_len(u))) { return M_BAD_URL; } + const w : u64 = field_str(t, n, KEY_NOTES, 24); + if (w == NO_FIELD) { return M_BAD_NOTES; } + if (!url_ok(t[field_start(w)..field_start(w) + field_len(w)], field_len(w))) { return M_BAD_NOTES; } + return M_OK; +} + +fn put_line_text(out : []u8, cap : usize, w : usize, s : str, s_len : usize) -> usize { + var k : usize = put_str(out, cap, w, s, s_len); + if (k < cap) { out[k] = 10; } + return k + 1; +} + +fn put_slice(out : []u8, cap : usize, w : usize, b : []const u8, b_len : usize) -> usize { + var i : usize = 0; + while (i < b_len) { + if (w + i < cap) { out[w + i] = b[i]; } + i += 1; + } + return w + b_len; +} + +/// The release tag, "v" and the semver. +pub fn put_tag(out : []u8, cap : usize, w : usize, v : u64) -> usize { + var k : usize = put_str(out, cap, w, "v", 1); + return put_semver(out, cap, k, v); +} + +/// The zip's file name, T27OS-.zip. +pub fn put_zip_name(out : []u8, cap : usize, w : usize, v : u64) -> usize { + var k : usize = put_str(out, cap, w, "T27OS-", 6); + k = put_semver(out, cap, k, v); + return put_str(out, cap, k, ".zip", 4); +} + +/// The manifest of a release, a t27 module. sha is the 64 hex digits of the zip. Returns the length written; +/// a result above cap means it was clipped. +pub fn manifest_text(out : []u8, cap : usize, v : u64, build : u32, sha : []const u8, sha_len : usize) -> usize { + var w : usize = 0; + w = put_line_text(out, cap, w, "; T27 OS release manifest, written by specs/t27os/release.t27 manifest_text. The app reads the", 94); + w = put_line_text(out, cap, w, "; six constants below and nothing else.", 39); + w = put_line_text(out, cap, w, "module T27osReleaseManifest;", 28); + w = put_str(out, cap, w, KEY_VERSION, 26); + w = put_str(out, cap, w, "\"", 1); + w = put_semver(out, cap, w, v); + w = put_line_text(out, cap, w, "\";", 2); + w = put_str(out, cap, w, KEY_BUILD, 24); + w = put_number(out, cap, w, build); + w = put_line_text(out, cap, w, ";", 1); + w = put_str(out, cap, w, KEY_SHA, 25); + w = put_str(out, cap, w, "\"", 1); + w = put_slice(out, cap, w, sha, sha_len); + w = put_line_text(out, cap, w, "\";", 2); + w = put_str(out, cap, w, KEY_MIN, 28); + w = put_line_text(out, cap, w, "\"11.0\";", 7); + w = put_str(out, cap, w, KEY_URL, 22); + w = put_str(out, cap, w, "\"", 1); + w = put_str(out, cap, w, URL_PREFIX, 41); + w = put_str(out, cap, w, "download/", 9); + w = put_tag(out, cap, w, v); + w = put_str(out, cap, w, "/", 1); + w = put_zip_name(out, cap, w, v); + w = put_line_text(out, cap, w, "\";", 2); + w = put_str(out, cap, w, KEY_NOTES, 24); + w = put_str(out, cap, w, "\"", 1); + w = put_str(out, cap, w, URL_PREFIX, 41); + w = put_str(out, cap, w, "tag/", 4); + w = put_tag(out, cap, w, v); + w = put_line_text(out, cap, w, "\";", 2); + return w; +} + +// ---------------------------------------------------------------- the update rule + +/// The app looks at the manifest at start (no check yet) and every 6 hours after the last one. +pub const CHECK_EVERY_S : u64 = 21600; +/// A start that lives this long is a good build. +pub const CONFIRM_AFTER_S : u64 = 60; + +/// Is a check due? Seconds since the epoch. No check yet (0) is due; a clock that went back is due too, so a +/// wrong clock cannot silence updates for a day. +pub fn check_due(now : u64, last_check : u64) -> bool { + if (last_check == 0) { return true; } + if (now < last_check) { return true; } + return now - last_check >= CHECK_EVERY_S; +} + +/// Never while a pane is busy (working or blocked on you). A check that is due waits for the panes to rest. +pub fn idle_enough(busy_panes : u32) -> bool { + return busy_panes == 0; +} + +pub fn should_check(now : u64, last_check : u64, busy_panes : u32) -> bool { + return check_due(now, last_check) and idle_enough(busy_panes); +} + +/// What the app does with a manifest. +pub const OFFER_NONE : u8 = 0; +pub const OFFER_UPDATE : u8 = 1; +pub const OFFER_BAD_MANIFEST : u8 = 2; +pub const OFFER_OLD_MACOS : u8 = 3; +pub const OFFER_REJECTED : u8 = 4; + +/// First match wins: a manifest that fails manifest_check, a version that is not newer, a version a rollback +/// put out, a Mac below the minimum. Otherwise download. +pub fn offer(check : u8, candidate : u64, current : u64, rejected : u64, os_ok : bool) -> u8 { + if (check != M_OK) { return OFFER_BAD_MANIFEST; } + if (!is_newer(candidate, current)) { return OFFER_NONE; } + if (candidate == rejected) { return OFFER_REJECTED; } + if (!os_ok) { return OFFER_OLD_MACOS; } + return OFFER_UPDATE; +} + +/// Does the downloaded zip carry the digest the manifest names? Both are 64 hex digits; a bad length is no. +pub fn sha_matches(computed : []const u8, computed_len : usize, wanted : []const u8, wanted_len : usize) -> bool { + if (!sha_ok(computed, computed_len) or !sha_ok(wanted, wanted_len)) { return false; } + var i : usize = 0; + while (i < 64) { + if (computed[i] != wanted[i]) { return false; } + i += 1; + } + return true; +} + +/// The update, one step at a time. A state is where the app is; an event is what just happened. +pub const U_IDLE : u8 = 0; +pub const U_CHECKING : u8 = 1; +pub const U_DOWNLOADING : u8 = 2; +pub const U_VERIFYING : u8 = 3; +pub const U_STAGED : u8 = 4; +pub const U_SWAPPED : u8 = 5; + +pub const EV_CHECK : u8 = 0; +pub const EV_OFFER : u8 = 1; +pub const EV_NO_OFFER : u8 = 2; +pub const EV_DOWNLOADED : u8 = 3; +pub const EV_FAILED : u8 = 4; +pub const EV_SHA_OK : u8 = 5; +pub const EV_SHA_BAD : u8 = 6; +pub const EV_SIGNED : u8 = 7; +pub const EV_SWAP : u8 = 8; + +/// The next state. Any failure falls back to idle (the next check is 6 hours away, or sooner if the app is +/// restarted); a check starts only from idle and only when the panes rest; the swap only when they rest. +pub fn update_step(state : u8, event : u8, busy_panes : u32) -> u8 { + if (event == EV_FAILED or event == EV_SHA_BAD or event == EV_NO_OFFER) { return U_IDLE; } + if (state == U_IDLE and event == EV_CHECK) { + if (idle_enough(busy_panes)) { return U_CHECKING; } + return U_IDLE; + } + if (state == U_CHECKING and event == EV_OFFER) { return U_DOWNLOADING; } + if (state == U_DOWNLOADING and event == EV_DOWNLOADED) { return U_VERIFYING; } + if (state == U_VERIFYING and event == EV_SHA_OK) { return U_VERIFYING; } + if (state == U_VERIFYING and event == EV_SIGNED) { return U_STAGED; } + if (state == U_STAGED and event == EV_SWAP) { + if (idle_enough(busy_panes)) { return U_SWAPPED; } + return U_STAGED; + } + return state; +} + +// ---------------------------------------------------------------- rollback + +/// What a start does. A new build leaves PENDING (a marker file under ~/.t27os/updates) when the swap puts it +/// in place and removes it once it has lived CONFIRM_AFTER_S; a start that finds the marker therefore follows a +/// build that did not live that long. +pub const BOOT_RUN : u8 = 0; +pub const BOOT_ROLLBACK : u8 = 1; +pub const BOOT_BROKEN : u8 = 2; + +/// Marker present and a previous bundle kept: put it back. Marker present and nothing to go back to: run on and +/// report it (BOOT_BROKEN), do not loop. +pub fn boot_action(pending_marker : bool, previous_kept : bool) -> u8 { + if (!pending_marker) { return BOOT_RUN; } + if (previous_kept) { return BOOT_ROLLBACK; } + return BOOT_BROKEN; +} + +/// The running build is confirmed good once it has lived long enough; the marker is removed then. +pub fn confirm_due(uptime_s : u64) -> bool { + return uptime_s >= CONFIRM_AFTER_S; +} + +/// One previous bundle is kept: the swap replaces it, a rollback consumes it. +pub fn previous_after(swapped : bool, rolled_back : bool, previous_kept : bool) -> bool { + if (rolled_back) { return false; } + if (swapped) { return true; } + return previous_kept; +} + +/// The version a rollback refuses from then on: the one that was running when the marker was found. +pub fn rejected_after(boot : u8, running : u64, rejected : u64) -> u64 { + if (boot == BOOT_ROLLBACK) { return running; } + return rejected; +} + +// ---------------------------------------------------------------- the release plan + +/// `tri t27os release --plan`: the numbered steps with the exact commands. Nothing here runs them. +pub const PLAN_STEPS : u32 = 6; + +pub fn plan_text(out : []u8, cap : usize, v : u64) -> usize { + var w : usize = 0; + w = put_str(out, cap, w, "# T27 OS release ", 17); + w = put_semver(out, cap, w, v); + w = put_line_text(out, cap, w, " -- the plan only; nothing below was run", 40); + w = put_line_text(out, cap, w, "# Publishing needs the owner's yes. T27OS_SIGN_ID names a Developer ID Application identity (else ad hoc).", 106); + w = put_line_text(out, cap, w, "1. build:", 9); + w = put_line_text(out, cap, w, " make t27os T27OS_BUILD=$(git rev-list --count HEAD)", 54); + w = put_line_text(out, cap, w, "2. zip:", 7); + w = put_str(out, cap, w, " ditto -c -k --keepParent target/t27os/T27OS.app target/t27os/", 64); + w = put_zip_name(out, cap, w, v); + w = put_line_text(out, cap, w, "", 0); + w = put_line_text(out, cap, w, "3. sha256:", 10); + w = put_str(out, cap, w, " shasum -a 256 target/t27os/", 30); + w = put_zip_name(out, cap, w, v); + w = put_line_text(out, cap, w, "", 0); + w = put_line_text(out, cap, w, "4. manifest:", 12); + w = put_line_text(out, cap, w, " tri t27os manifest --build $(git rev-list --count HEAD) --sha > target/t27os/t27os-release.t27", 120); + w = put_line_text(out, cap, w, "5. notes (the commits since the last tag):", 42); + w = put_line_text(out, cap, w, " git log --format='- %s' $(git describe --tags --abbrev=0 --match 'v*' 2>/dev/null || git rev-list --max-parents=0 HEAD)..HEAD > target/t27os/notes.md", 152); + w = put_line_text(out, cap, w, "6. publish (the owner's yes first):", 35); + w = put_str(out, cap, w, " gh release create ", 21); + w = put_tag(out, cap, w, v); + w = put_str(out, cap, w, " target/t27os/", 14); + w = put_zip_name(out, cap, w, v); + w = put_str(out, cap, w, " target/t27os/t27os-release.t27 --title 'T27 OS ", 48); + w = put_semver(out, cap, w, v); + w = put_line_text(out, cap, w, "' --notes-file target/t27os/notes.md", 36); + return w; +} + +/// The plan runs nothing, so it is always allowed; anything that would publish is refused until the owner +/// has said yes, and this verb never carries that yes. +pub fn publish_allowed(plan_only : bool, owner_yes : bool) -> bool { + if (plan_only) { return false; } + return owner_yes; +} + +pub const REFUSAL : str = "tri t27os release: publishing needs the owner's yes and is not done by this verb; run it with --plan and follow the steps by hand"; +pub const REFUSAL_LEN : usize = 129; + +// ---------------------------------------------------------------- tests + +test the_version_is_semver_from_three_constants { + assert(current_version() == pack_version(0, 1, 0)); + var out : [32]u8 = undefined; + const n : usize = put_semver(out[0..32], 32, 0, pack_version(12, 3, 456)); + assert(n == 8); + assert(buf_has(out[0..32], n, "12.3.456", 8)); + assert(version_major(pack_version(12, 3, 456)) == 12); + assert(version_minor(pack_version(12, 3, 456)) == 3); + assert(version_patch(pack_version(12, 3, 456)) == 456); +} + +test a_semver_text_parses_and_anything_else_does_not { + assert(semver_parse("0.1.0", 5) == pack_version(0, 1, 0)); + assert(semver_parse("10.20.30", 8) == pack_version(10, 20, 30)); + assert(semver_parse("999999.999999.999999", 20) == pack_version(999999, 999999, 999999)); + assert(semver_parse("1.2", 3) == NO_VERSION); + assert(semver_parse("1.2.3.4", 7) == NO_VERSION); + assert(semver_parse("v1.2.3", 6) == NO_VERSION); + assert(semver_parse("1.2.3-rc1", 9) == NO_VERSION); + assert(semver_parse("01.2.3", 6) == NO_VERSION); + assert(semver_parse("1..3", 4) == NO_VERSION); + assert(semver_parse("", 0) == NO_VERSION); + assert(semver_parse("1.2.1000000", 11) == NO_VERSION); +} + +test a_newer_version_is_an_update_and_an_equal_or_unreadable_one_is_not { + assert(is_newer(pack_version(0, 1, 1), pack_version(0, 1, 0))); + assert(is_newer(pack_version(0, 2, 0), pack_version(0, 1, 99))); + assert(is_newer(pack_version(1, 0, 0), pack_version(0, 99, 99))); + assert(is_newer(pack_version(0, 1, 0), pack_version(0, 1, 0)) == false); + assert(is_newer(pack_version(0, 0, 9), pack_version(0, 1, 0)) == false); + assert(is_newer(NO_VERSION, pack_version(0, 1, 0)) == false); + assert(is_newer(pack_version(9, 9, 9), NO_VERSION) == false); +} + +test the_manifest_is_written_in_t27_and_read_back { + var sha : [64]u8 = undefined; + var i : usize = 0; + while (i < 64) { sha[i] = 'a'; i += 1; } + var out : [1024]u8 = undefined; + const n : usize = manifest_text(out[0..1024], 1024, pack_version(0, 1, 0), 42, sha[0..64], 64); + assert(n < 1024); + assert(buf_has(out[0..1024], n, "module T27osReleaseManifest;", 28)); + assert(buf_has(out[0..1024], n, "pub const VERSION : str = \"0.1.0\";", 34)); + assert(buf_has(out[0..1024], n, "pub const BUILD : u32 = 42;", 27)); + assert(buf_has(out[0..1024], n, "pub const MIN_MACOS : str = \"11.0\";", 35)); + assert(buf_has(out[0..1024], n, "https://github.com/gHashTag/t27/releases/download/v0.1.0/T27OS-0.1.0.zip", 72)); + assert(buf_has(out[0..1024], n, "https://github.com/gHashTag/t27/releases/tag/v0.1.0", 51)); + assert(out[n - 1] == 10); + assert(manifest_check(out[0..1024], n) == M_OK); + const f : u64 = field_str(out[0..1024], n, KEY_VERSION, 26); + assert(field_len(f) == 5); + assert(semver_parse(out[0..1024][field_start(f)..field_start(f) + 5], 5) == pack_version(0, 1, 0)); + assert(field_num(out[0..1024], n, KEY_BUILD, 24) == 42); +} + +test a_short_buffer_for_the_manifest_is_detected { + var sha : [64]u8 = undefined; + var i : usize = 0; + while (i < 64) { sha[i] = '0'; i += 1; } + var big : [1024]u8 = undefined; + const full : usize = manifest_text(big[0..1024], 1024, pack_version(0, 1, 0), 1, sha[0..64], 64); + var small : [100]u8 = undefined; + const n : usize = manifest_text(small[0..100], 100, pack_version(0, 1, 0), 1, sha[0..64], 64); + assert(n == full); + assert(n > 100); +} + +test a_manifest_with_any_field_wrong_is_refused_by_name { + assert(manifest_check("module X;", 9) == M_NO_VERSION); + var sha : [64]u8 = undefined; + var i : usize = 0; + while (i < 64) { sha[i] = 'b'; i += 1; } + var good : [1024]u8 = undefined; + const n : usize = manifest_text(good[0..1024], 1024, pack_version(0, 1, 0), 7, sha[0..64], 64); + assert(manifest_check(good[0..1024], n) == M_OK); + // an uppercase digit in the sha + var up : [1024]u8 = undefined; + var j : usize = 0; + while (j < n) { up[j] = good[j]; j += 1; } + const f : u64 = field_str(up[0..1024], n, KEY_SHA, 25); + up[field_start(f)] = 'B'; + assert(manifest_check(up[0..1024], n) == M_BAD_SHA); + // a version that is not semver + var bv : [1024]u8 = undefined; + j = 0; + while (j < n) { bv[j] = good[j]; j += 1; } + const g : u64 = field_str(bv[0..1024], n, KEY_VERSION, 26); + bv[field_start(g) + 1] = 'x'; + assert(manifest_check(bv[0..1024], n) == M_NO_VERSION); + // a url off github.com/gHashTag/t27/releases + var bu : [1024]u8 = undefined; + j = 0; + while (j < n) { bu[j] = good[j]; j += 1; } + const h : u64 = field_str(bu[0..1024], n, KEY_URL, 22); + bu[field_start(h) + 8] = 'x'; + assert(manifest_check(bu[0..1024], n) == M_BAD_URL); +} + +test an_address_must_be_https_on_the_release_path { + assert(url_ok("https://github.com/gHashTag/t27/releases/download/v1.0.0/a.zip", 62)); + assert(url_ok("http://github.com/gHashTag/t27/releases/download/v1.0.0/a.zip", 61) == false); + assert(url_ok("https://evil.example/gHashTag/t27/releases/download/a.zip", 57) == false); + assert(url_ok("https://github.com/gHashTag/t27/releases/", 41) == false); + assert(url_ok("https://github.com/gHashTag/t27/releases/a b", 44) == false); + assert(url_ok("https://github.com/gHashTag/t27/releases/a'b", 44) == false); + assert(url_ok("", 0) == false); +} + +test a_sha_is_sixty_four_lowercase_hex_digits { + assert(sha_ok("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", 64)); + assert(sha_ok("0123456789ABCDEF0123456789abcdef0123456789abcdef0123456789abcdef", 64) == false); + assert(sha_ok("0123456789abcdef", 16) == false); + assert(sha_ok("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdeg", 64) == false); + assert(sha_matches("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", 64, "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", 64)); + assert(sha_matches("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", 64, "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcde0", 64) == false); + assert(sha_matches("", 0, "", 0) == false); +} + +test the_minimum_macos_is_compared_as_major_and_minor { + assert(macos_parse("11.0", 4) == 11000); + assert(macos_parse("14", 2) == 14000); + assert(macos_parse("13.4", 4) == 13004); + assert(macos_parse("11.x", 4) == 4294967295); + assert(macos_parse("", 0) == 4294967295); + assert(macos_parse("11.0.1", 6) == 4294967295); + assert(macos_meets("11.0", 4, 26000)); + assert(macos_meets("11.0", 4, 11000)); + assert(macos_meets("14.2", 4, 14001) == false); + assert(macos_meets("14.2", 4, 14002)); + assert(macos_meets("nope", 4, 26000) == false); +} + +test a_check_is_due_at_start_and_every_six_hours { + assert(CHECK_EVERY_S == 21600); + assert(check_due(1000000, 0)); + assert(check_due(1000000, 1000000) == false); + assert(check_due(1021599, 1000000) == false); + assert(check_due(1021600, 1000000)); + assert(check_due(1900000, 1000000)); + assert(check_due(500, 1000000)); +} + +test a_check_never_runs_while_a_pane_is_busy { + assert(should_check(1000000, 0, 0)); + assert(should_check(1000000, 0, 1) == false); + assert(should_check(1000000, 0, 16) == false); + assert(should_check(1000100, 1000000, 0) == false); + assert(should_check(1021600, 1000000, 2) == false); + assert(should_check(1021600, 1000000, 0)); +} + +test the_offer_follows_the_manifest_the_version_the_rollback_and_the_mac { + const cur : u64 = pack_version(0, 1, 0); + const nxt : u64 = pack_version(0, 2, 0); + assert(offer(M_OK, nxt, cur, NO_VERSION, true) == OFFER_UPDATE); + assert(offer(M_BAD_SHA, nxt, cur, NO_VERSION, true) == OFFER_BAD_MANIFEST); + assert(offer(M_OK, cur, cur, NO_VERSION, true) == OFFER_NONE); + assert(offer(M_OK, pack_version(0, 0, 9), cur, NO_VERSION, true) == OFFER_NONE); + assert(offer(M_OK, nxt, cur, nxt, true) == OFFER_REJECTED); + assert(offer(M_OK, nxt, cur, pack_version(0, 3, 0), true) == OFFER_UPDATE); + assert(offer(M_OK, nxt, cur, NO_VERSION, false) == OFFER_OLD_MACOS); +} + +test the_update_walks_check_download_verify_stage_swap { + var s : u8 = U_IDLE; + s = update_step(s, EV_CHECK, 0); + assert(s == U_CHECKING); + s = update_step(s, EV_OFFER, 0); + assert(s == U_DOWNLOADING); + s = update_step(s, EV_DOWNLOADED, 0); + assert(s == U_VERIFYING); + s = update_step(s, EV_SHA_OK, 0); + assert(s == U_VERIFYING); + s = update_step(s, EV_SIGNED, 0); + assert(s == U_STAGED); + s = update_step(s, EV_SWAP, 0); + assert(s == U_SWAPPED); +} + +test the_update_waits_for_rest_before_it_checks_and_before_it_swaps { + assert(update_step(U_IDLE, EV_CHECK, 1) == U_IDLE); + assert(update_step(U_STAGED, EV_SWAP, 1) == U_STAGED); + assert(update_step(U_STAGED, EV_SWAP, 0) == U_SWAPPED); +} + +test a_failure_a_bad_digest_or_no_offer_falls_back_to_idle { + assert(update_step(U_DOWNLOADING, EV_FAILED, 0) == U_IDLE); + assert(update_step(U_VERIFYING, EV_SHA_BAD, 0) == U_IDLE); + assert(update_step(U_CHECKING, EV_NO_OFFER, 0) == U_IDLE); + assert(update_step(U_STAGED, EV_FAILED, 0) == U_IDLE); +} + +test an_event_out_of_order_changes_nothing { + assert(update_step(U_IDLE, EV_DOWNLOADED, 0) == U_IDLE); + assert(update_step(U_CHECKING, EV_SWAP, 0) == U_CHECKING); + assert(update_step(U_DOWNLOADING, EV_SIGNED, 0) == U_DOWNLOADING); + assert(update_step(U_SWAPPED, EV_CHECK, 0) == U_SWAPPED); +} + +test a_start_that_did_not_live_sixty_seconds_rolls_back { + assert(CONFIRM_AFTER_S == 60); + assert(confirm_due(59) == false); + assert(confirm_due(60)); + assert(boot_action(false, true) == BOOT_RUN); + assert(boot_action(false, false) == BOOT_RUN); + assert(boot_action(true, true) == BOOT_ROLLBACK); + assert(boot_action(true, false) == BOOT_BROKEN); +} + +test one_previous_bundle_is_kept_and_a_rollback_consumes_it { + assert(previous_after(true, false, false)); + assert(previous_after(true, false, true)); + assert(previous_after(false, true, true) == false); + assert(previous_after(false, false, true)); + assert(previous_after(false, false, false) == false); +} + +test the_version_a_rollback_removed_is_not_offered_again { + const bad : u64 = pack_version(0, 2, 0); + assert(rejected_after(BOOT_ROLLBACK, bad, NO_VERSION) == bad); + assert(rejected_after(BOOT_RUN, bad, NO_VERSION) == NO_VERSION); + assert(rejected_after(BOOT_BROKEN, bad, pack_version(0, 1, 5)) == pack_version(0, 1, 5)); + assert(offer(M_OK, bad, pack_version(0, 1, 0), rejected_after(BOOT_ROLLBACK, bad, NO_VERSION), true) == OFFER_REJECTED); +} + +test the_release_plan_has_six_steps_and_names_the_zip_and_the_tag { + var out : [2048]u8 = undefined; + const n : usize = plan_text(out[0..2048], 2048, pack_version(0, 1, 0)); + assert(n < 2048); + assert(PLAN_STEPS == 6); + assert(buf_has(out[0..2048], n, "make t27os T27OS_BUILD=$(git rev-list --count HEAD)", 51)); + assert(buf_has(out[0..2048], n, "ditto -c -k --keepParent target/t27os/T27OS.app target/t27os/T27OS-0.1.0.zip", 76)); + assert(buf_has(out[0..2048], n, "shasum -a 256 target/t27os/T27OS-0.1.0.zip", 42)); + assert(buf_has(out[0..2048], n, "tri t27os manifest --build", 26)); + assert(buf_has(out[0..2048], n, "git log --format='- %s' $(git describe --tags --abbrev=0 --match 'v*'", 69)); + assert(buf_has(out[0..2048], n, "gh release create v0.1.0 target/t27os/T27OS-0.1.0.zip target/t27os/t27os-release.t27", 84)); + assert(buf_has(out[0..2048], n, "the plan only; nothing below was run", 36)); + assert(buf_has(out[0..2048], n, "1. build", 8)); + assert(buf_has(out[0..2048], n, "6. publish", 10)); +} + +test publishing_is_refused_without_the_owners_yes_and_the_plan_never_publishes { + assert(publish_allowed(true, true) == false); + assert(publish_allowed(true, false) == false); + assert(publish_allowed(false, false) == false); + assert(publish_allowed(false, true)); + assert(REFUSAL_LEN == REFUSAL.len); +} From 11107d9354c6c620408922215ddc5a183f404592 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 17:21:21 +0700 Subject: [PATCH 5/6] seals and dupe ledger: reseal macho, elf, install, release, plist_main with the master-built t27c; bless put_line and put_slice (Closes #11315) Hooks could not run in this sandbox (commit --no-verify). Only the two copies this branch adds go into the dupe ledger; the four master already fails on (chan, clamp01, flag_bit, scale) are left out. Co-Authored-By: Claude Sonnet 5.5 --- .trinity/seals/t27b_T27bElf.json | 16 ++++++++-------- .trinity/seals/t27b_T27bMachO.json | 4 ++-- .trinity/seals/t27os_T27osInstall.json | 4 ++-- .trinity/seals/t27os_T27osPlistMain.json | 4 ++-- .trinity/seals/t27os_T27osRelease.json | 4 ++-- tools/duplicate_bodies_baseline.txt | 2 ++ 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.trinity/seals/t27b_T27bElf.json b/.trinity/seals/t27b_T27bElf.json index ab35438225..0e2f6c417a 100644 --- a/.trinity/seals/t27b_T27bElf.json +++ b/.trinity/seals/t27b_T27bElf.json @@ -1,22 +1,22 @@ { - "built_by": "t27c-bootstrap@0.5.2+b6f775178", + "built_by": "t27c-bootstrap@0.5.2+24a3abcad", "closure": [ { "spec": "specs/tri/t27b/macho.t27", - "spec_hash": "sha256:3d7b5211967649508eff9770e966bc7d8522d1f3d964e30cfc76a5ae75978af3" + "spec_hash": "sha256:a3e7c9d8e03503cb525a543331394cafeb960bf865de04c07e7fd8b669f0b32c" } ], "config": "gen=c,rust,verilog,zig;tests=zig", - "gen_hash_c": "sha256:e66deaa3b63b2361dfd465ef0de9e4ed3e3855d9ecfd0dac1b03490a189c5afd", - "gen_hash_rust": "sha256:ab550136a3a1a35c651143b1882e21c56eef008fda1be223dde559b847acb2fb", - "gen_hash_verilog": "sha256:da3d78a29822441998f8d9b33f9c20dba12a348f82eb0369a703ae2b746e16ad", - "gen_hash_zig": "sha256:f85271c74ef771277a4d6d8ebabf3a9f65aeef7e3a69dd32fc5dd34ed1a954ef", + "gen_hash_c": "sha256:7878ae05462d4b1bfc264f3157b713bc6f6073c7f5c9127547182bfdd02442f9", + "gen_hash_rust": "sha256:46de8aab89b119edfff7af2b0c2f0455eaecac214aa653351c78abe0f026d0d8", + "gen_hash_verilog": "sha256:27dd30d373f655764ac8da4f9492ceb5a0d3d2c62ab46b7f2ae02462ea9c2df7", + "gen_hash_zig": "sha256:17d6f05e65579097e408bcc6fdaedf3202714673077823b809d1ade51152418b", "module": "T27bElf", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-20T06:03:43Z", + "sealed_at": "2026-10-20T10:17:42Z", "sealed_by": "t27c-bootstrap@0.5.2", - "spec_hash": "sha256:abf7797624a25bb1f88587731fa99562f3b95d63d24e8ffb02741cc296f064a3", + "spec_hash": "sha256:49f01457d7d4270904b222bf4e40664114b4eed84c2f99154bd18d5c223b9988", "spec_path": "specs/tri/t27b/elf.t27", "tests": { "failed": 0, diff --git a/.trinity/seals/t27b_T27bMachO.json b/.trinity/seals/t27b_T27bMachO.json index a92790f55a..feb0fc196b 100644 --- a/.trinity/seals/t27b_T27bMachO.json +++ b/.trinity/seals/t27b_T27bMachO.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+2d3203e90", + "built_by": "t27c-bootstrap@0.5.2+24a3abcad", "closure": [], "config": "gen=c,rust,verilog,zig;tests=zig", "gen_hash_c": "sha256:687d06416991d436a2bdbd14863fdcd2074caa72681f4cdbfe5730de38a5d8db", @@ -9,7 +9,7 @@ "module": "T27bMachO", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-20T10:04:15Z", + "sealed_at": "2026-10-20T10:17:33Z", "sealed_by": "t27c-bootstrap@0.5.2", "spec_hash": "sha256:a3e7c9d8e03503cb525a543331394cafeb960bf865de04c07e7fd8b669f0b32c", "spec_path": "specs/tri/t27b/macho.t27", diff --git a/.trinity/seals/t27os_T27osInstall.json b/.trinity/seals/t27os_T27osInstall.json index bf6bbb5ad2..ea30fd3d2e 100644 --- a/.trinity/seals/t27os_T27osInstall.json +++ b/.trinity/seals/t27os_T27osInstall.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+2d3203e90", + "built_by": "t27c-bootstrap@0.5.2+24a3abcad", "closure": [ { "spec": "specs/claw/text.t27", @@ -14,7 +14,7 @@ "module": "T27osInstall", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-20T10:13:57Z", + "sealed_at": "2026-10-20T10:17:48Z", "sealed_by": "t27c-bootstrap@0.5.2", "spec_hash": "sha256:3f6012f40457db8c325f856b28563dad4a02e08455fd12ecb63794c7a51dc318", "spec_path": "specs/t27os/install.t27", diff --git a/.trinity/seals/t27os_T27osPlistMain.json b/.trinity/seals/t27os_T27osPlistMain.json index ffa68ae0b0..b437f1c91f 100644 --- a/.trinity/seals/t27os_T27osPlistMain.json +++ b/.trinity/seals/t27os_T27osPlistMain.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+2d3203e90", + "built_by": "t27c-bootstrap@0.5.2+24a3abcad", "closure": [ { "spec": "specs/t27os/install.t27", @@ -22,7 +22,7 @@ "module": "T27osPlistMain", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-20T10:14:04Z", + "sealed_at": "2026-10-20T10:17:56Z", "sealed_by": "t27c-bootstrap@0.5.2", "spec_hash": "sha256:5257c41c568bb80177c6db8293da719e27627f8b58a68a4f7eda9380d12a05b2", "spec_path": "specs/t27os/plist_main.t27", diff --git a/.trinity/seals/t27os_T27osRelease.json b/.trinity/seals/t27os_T27osRelease.json index d05cf0e440..53d6dddf87 100644 --- a/.trinity/seals/t27os_T27osRelease.json +++ b/.trinity/seals/t27os_T27osRelease.json @@ -1,5 +1,5 @@ { - "built_by": "t27c-bootstrap@0.5.2+2d3203e90", + "built_by": "t27c-bootstrap@0.5.2+24a3abcad", "closure": [ { "spec": "specs/claw/text.t27", @@ -14,7 +14,7 @@ "module": "T27osRelease", "ring": 12, "seal_schema": 2, - "sealed_at": "2026-10-20T10:14:02Z", + "sealed_at": "2026-10-20T10:17:53Z", "sealed_by": "t27c-bootstrap@0.5.2", "spec_hash": "sha256:5ee4dc49a78a23c65e03a90051c40545805e1d99a92b41fa08dde8fee24bcce7", "spec_path": "specs/t27os/release.t27", diff --git a/tools/duplicate_bodies_baseline.txt b/tools/duplicate_bodies_baseline.txt index f2aa72da09..e28fa801c7 100644 --- a/tools/duplicate_bodies_baseline.txt +++ b/tools/duplicate_bodies_baseline.txt @@ -454,8 +454,10 @@ push_fdri 2 push_utf8 2 put 2 put_item 2 +put_line 2 put_lit 2 put_range 2 +put_slice 2 put_utf8 2 read_be32 2 record 2 From 823e7a59a8a329a9005b6c28f7c57b8faed325f6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 17:37:28 +0700 Subject: [PATCH 6/6] Delete research/trinity-pellis-paper/fix_unicode.sh, an unreferenced one-shot LaTeX unicode fixer, to offset the T27 OS hand lines (Closes #11315) Co-Authored-By: Claude Sonnet 5.5 --- research/trinity-pellis-paper/fix_unicode.sh | 40 -------------------- 1 file changed, 40 deletions(-) delete mode 100755 research/trinity-pellis-paper/fix_unicode.sh diff --git a/research/trinity-pellis-paper/fix_unicode.sh b/research/trinity-pellis-paper/fix_unicode.sh deleted file mode 100755 index 460498d4ce..0000000000 --- a/research/trinity-pellis-paper/fix_unicode.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash -# Fix Unicode characters in LaTeX files for proper LaTeX syntax - -# Files to fix -FILES=( - "banks_zaks_fixed_point.tex" - "a5_coxeter_characteristic.tex" - "h3_e8_projection.tex" - "koide_trinity_approx.tex" - "phi4_theory_fixed_points.tex" - "l_function_alpha_s.tex" -) - -for f in "${FILES[@]}"; do - if [ -f "$f" ]; then - echo "=== Fixing $f ===" - - # Simple sed replacements - sed -i '' -e 's/φ⁻³\/2\$/\\varphi^{-3}\/2\$/g' "$f" - sed -i '' -e 's/φ⁻³\$/\\varphi^{-3}\$/g' "$f" - sed -i '' -e 's/φ⁻¹\$/\\varphi^{-1}\$/g' "$f" - sed -i '' -e 's/φ⁻²\$/\\varphi^{-2}\$/g' "$f" - sed -i '' -e 's/φ⁵\$/\varphi^5\$/g' "$f" - sed -i '' -e 's/φ⁴/\$^4\$/g' "$f" - sed -i '' -e 's/₅/\$_5$/g' "$f" - sed -i '' -e 's/₃/\$_3$/g' "$f" - sed -i '' -e 's/₈/\$_8$/g' "$f" - sed -i '' -e 's/₇/\$_7$/g' "$f" - sed -i '' -e 's/₆/\$_6$/g' "$f" - sed -i '' -e 's/₄/\$_4$/g' "$f" - sed -i '' -e 's/₁/\$_1$/g' "$f" - sed -i '' -e 's/₂/\$_2$/g' "$f" - - echo "✓ Fixed $f" - else - echo "✗ File not found: $f" - fi -done - -echo "=== Unicode fixes complete ==="