Skip to content

The fork's LLVM gives a loop counter's recurrence only the wrap flags proven for it, a sysroot whose compilers lose a loop's last exit is refused, and the ScalarEvolution issue is closed - #790

Merged
Japabu merged 11 commits into
mainfrom
wt/toyos-scevfix
Oct 9, 2026

Conversation

@Japabu

@Japabu Japabu commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

No function that lost a loop exit was found in what ToyOS builds. What was compared, per function, between the compiler before the fix and the one that lands, each building with the sysroot it built: every crate of both architectures' images and of the SMP test's image, as an image build compiles them (48 functions of 79,803 differ) and again with each crate one unit and its optimised IR kept (59 of 48,579, the 51 of them in the images text for text the ones round 1 read); every archive of the two sysroots, the Rust libraries, the C library and the C++ runtime (15 of 32,638); and doomgeneric's C (none of 848 in the build's objects, 2 of 847 in IR). In none does the old compiler's code have an exit fewer that a program can take. The reproducers, and the port test as main had it before #780, are the only code seen to go wrong.

The defect

The compiler that builds every ToyOS kernel, loader and program turned correct safe Rust into an endless loop: an inclusive range that ends at its integer type's maximum, for aarch64-unknown-toyos over u16 and u128, and for x86_64-unknown-toyos over u128.

The fault is LLVM's ScalarEvolution, on every target. createSimpleAffineAddRec and createAddRecFromPHI copied the nuw/nsw of a loop's increment onto the add recurrence of its header phi. Those flags say only that the increment is poison where it wraps, and poison nothing observes is no undefined behaviour. SCEV expressions are uniqued without their flags, so the flag became a claim about every value sharing the expression. In m1:

header:
  %next = phi i16 [ -3, %entry ], [ %nextnext, %latch ]
  %iter = phi i16 [ -4, %entry ], [ %next, %latch ]
  %done = icmp eq i16 %iter, -1
  br i1 %done, label %exit, label %latch
latch:
  %nextnext = add nuw i16 %next, 1   ; poison on the third iteration, never used
  br label %header

%next was {-3,+,1}<nuw>, whose range excludes 0; %iter + 1 is the same expression; indvars folded %done to false. CorrelatedValuePropagation and LoopRotate make this shape of RangeInclusive::next as rust-lang/rust#155114 wrote it. Upstream knows the fault as llvm/llvm-project#175729, open; its proposed fix, llvm/llvm-project#118959, has been open and unmerged since 2024-12.

The fix, and why this shape

ToyOSOrg/llvm-project 81b496be0342 and b7420fe534bf, appended to toyos-rustc-22.1-2026-05-19: ScalarEvolution.cpp +78 −37 and ScalarEvolution.h +10 −4 against ceaf0fbb8440, and tests. The phi's recurrence takes the increment's flags only where they are proven for the recurrence itself, on every backedge the loop takes:

  • the program is undefined if the phi is poison (programUndefinedIfPoison, which reads the header from the phi on): the phi of any iteration but the first is the increment of the one before, so that increment did not wrap; or
  • a poison increment causes undefined behaviour in a block that dominates the latch: every path to the latch passes that block after the increment, in the same iteration, so on a backedge that is taken the increment is not poison.

Otherwise the phi gets no flag but what SCEV proves from ranges, then and later. The post-increment recurrence keeps the condition it had, renamed isPostIncAddRecNeverPoison.

Nothing ships for a test or a measurement. Round 1's commit carried a hidden option, -scev-unconditional-preinc-nowrap-flags, that restored the old behaviour; b7420fe534bf removes it and the test lines that passed it. The control of every measurement below is the compiler before the fix: the one main builds with, LLVM ceaf0fbb8440, compiler key 3d1cb62e54e18406.

This is candidate A, upstream's #118959, and not B, dropping the flags outright. The brief made B the default unless A is sound and B's cost material. Both hold:

  • A is sound, by the argument above, which the round-1 review read independently and agreed with; that is two readers of one argument and no independent checker. Both conditions establish the fact for the recurrence on every iteration the loop enters, whatever uses the expression, which is what a flag on a uniqued expression has to mean. #118959's own thread has no comment and no review (fetched, User-Agent: toyos-build, no credentials). The review it descends from, D148931, has one objection, from mkazantsev: two recurrences with the same start and step are one node, a flag inferred for it is proven "regardless of uses", and inference "should not rely on users at all" because the expander makes new uses. The first half is the premise the miscompile refutes: the flag was copied, never proven. The second does not reach this condition: it reads the uses to prove a fact about the loop's iterations, and the fact stays true of a use the expander adds later. A maintainer's comment on #175729 calls #118959 "the fix for this issue" and says it "has some problematic impact", which is lost optimisation, measured below. Upstream has not accepted it in 22 months.
  • B's cost is material. Measured in round 1, before the tests were touched, by one development opt that carried the old behaviour, A and B behind a switch (LLVM's Analysis, Transforms, CodeGen, DebugInfo, Other and Feature tests, 43,385 discovered, each run all three ways); that opt and its switch were never committed in that form and are not what lands:
A B
LLVM tests whose expectation changes, seven targets built 43 182
of those, opt aborting on an assertion 0 at least 10
autogenerated tests regenerated 33 files, +255 −412 148 files, +3086 −5432, script formatting included
functions that differ from the old behaviour, ToyOS corpus 11 39
text of that corpus (5,805,358 bytes) −80 −988

Under B, print<scalar-evolution> aborts on ten of Analysis/ScalarEvolution's tests in an assertions build; in nsw.ll, the one read, on PrintLoopInfo's "Different predicated BTC, but no predicates". So B is not a two-site edit: an LLVM carrying it needs a further change to pass its own tests. The corpus is the unoptimised bitcode of the x86-64 kernel and nine programs with their dependencies (134 crates), through opt -O2 and llc -O2 in each mode.

What A costs is in the census and in the tests table: where a loop counter's wrap is ruled out by a check the program makes and not by undefined behaviour, as with Rust's overflow checks, the recurrence loses a flag that was true, and with it a peel, a widened counter or a vectoriser's free pass. One limit is the helper's and not the argument's: programUndefinedIfPoison scans 32 instructions, and a header whose first observing use is further on loses its flag (mve-reg-pressure-vmla.ll).

The fork chain

repository branch commits
ToyOSOrg/llvm-project toyos-rustc-22.1-2026-05-19 81b496be0342cef58d9d7e480f7c78265d2a4b78, the fix, then b7420fe534bf063b78a61c18fe6fb2bbcf4340e0, on ceaf0fbb8440
ToyOSOrg/rust main 9e7b17b52f955a9462a55c46a8ba2440c51a9c9f, then b9cc8392f0eb21330160352e0abd85602b419b23, on 6d6ad8c71906: the src/llvm-project gitlink and nothing else
this repository wt/toyos-scevfix the rust gitlink, at b9cc8392f0eb

Each fork commit is a fast-forward of its branch. library/core and library/alloc are untouched: #155114's next is right, and reverting it would leave the fault for any other code of the shape.

This moves the LLVM key, so every other branch's CI goes cold once it lands: LLVM 90d48821e80c5da1, compiler a1a1399eacc6661a, freestanding libraries 42125dcfe1c67e4a, sysroot 843dffb798581666. A branch that merges main afterwards builds or restores all four. On the development host the first build at these pins, of an LLVM, a compiler, the freestanding libraries and a sysroot from nothing, took 28 minutes, LLVM 17 of them, at 1-minute load 25 to 115. The sysroot's own check ran in that build (Checking that the compilers of … keep a loop's last exit, then the C library's build) and in the one that made 843dffb798581666.

ToyOS's own check

src/miscompile.rs, called from sysroot::build once a sysroot's libraries are in place and before anything else is built into it. It holds the loop twice, by the two compilers a toolchain carries of one LLVM:

  • src/miscompile/last_exit.ll, no front end in it: m1 as IR, walked a thousand passes, through the toolchain's clang at -O2 for both userland targets; refused unless caller in the output is one block, ret i1 true. A thousand, because m1's four are evaluated and folded right before indvars meets the loop, by a compiler with the fault too (measured: m1 itself through the base clang is ret i1 true). The shape of core's RangeInclusive::next, which upstream may well rework because of this very miscompile, is not in it. The IR names no target and the passes that matter are the middle end's; clang knows neither UEFI triple of AArch64 nor the kernel's soft-float one, so the two userland targets are the two architectures.
  • src/miscompile/last_exit.rs, the Rust that was seen to make it: the port test's loop over u128, by the toolchain's rustc for all six guest targets (GUEST_TARGETS), each wrong under the base compiler. u16 is gone: for the three x86-64 targets the base compiler compiles it right, and one type whose every cell has a red arm is simpler than two with a table. The no-op .env_remove("RUSTFLAGS") is gone.

sysroot::RECIPE moves, so every sysroot key does.

the check (refuse, through a driver test a checked patch adds and reverses), at 5b4592e88 exit
against main's sysroot 8618c089fa736cb0, the base compiler 101: its clang "miscompiles a loop that ends at its counter's maximum for x86_64-unknown-toyos", caller is br label %header to itself
against the sysroot that lands, 843dffb798581666 0
the IR case taken out (a second patch), against 8618c089fa736cb0 101: its rustc, caller is br label %bb4.i.backedge.i to itself
the same against 843dffb798581666 0

Case by case, by the check's own command lines: the base clang makes caller an endless loop for both userland targets and the base rustc for all six guest targets, eight of eight; the fixed toolchain makes all eight ret i1 true.

  • Why a check and not a sentence. What a compiler does to a loop is the compiler's bytes; no reading of the tree shows it, and the next move of the fork to a later upstream brings an LLVM that does not have this fix unless it is carried again.
  • Why there. It needs the tree's compilers and core for a ToyOS target, which exist only once a sysroot is made; the host job has neither. It runs wherever a sysroot is built, the bootstrap job and every development host, once per key, and costs two clang runs of a 20-line file and six rustc runs of a 48-line crate. No QEMU.
  • What ships for it. Nothing in an image: both are compiled to IR in a scratch directory that is removed.
  • IR, not behaviour. The wrong outcome is an endless loop, which a run could only bound with a timeout; ret i1 true is exact.
  • The IR oracle itself (returns_true) has a host test whose negative control is the endless caller verbatim.

The census

One measurement, kept nowhere in the tree; its scripts and patches are in the comments below. It was made at 35ee6ca4d, this branch at its merge of main's 975d5702a (#791), by the compilers that land. The head has since merged main's 965e62bb1 (#792, #794), which changes five guest crates (toyos-net-ip, toyos-net-shard, toyos-net-udp, toyos-tco, netstack's node) and the metal harness, and no key of the toolchain; nothing of the census was run again for it.

The two arms. Fixed: this branch, compiler a1a1399eacc6661a, sysroot 843dffb798581666. Base: main at 975d5702a itself, whose fork pin is the compiler before the fix (3d1cb62e54e18406, LLVM 1670055c5e508eba), with one line of sysroot::RECIPE changed in the worktree so that its sysroot's key was one nobody had made (290b50c419514a6f, freestanding 88b7ae8e10853b24). That is because a sysroot's std, toyos and toyos_abi take a crate hash from the path they were built at: against the store's own sysroot of that tree, made in another worktree, every symbol that names one of the three differs, shared generics are taken from other crates, and 2,821 functions "differ". Built at one path the two arms name every function alike. Neither sysroot was built with anything added to a compile. Every build is cargo run -- --build-only --arch <arch> (or cargo test --test toyos-build -- virt_el1_smp, 1 passed in each of its four runs) from no guest target directory of that architecture, each exit 0, at 1-minute load 24 to 76.

Two ways to build, because round 1's way is not how an image is built. Round 1 added --emit=llvm-ir to every guest compile to have IR to read. That makes rustc compile a crate as one codegen unit: 366 objects for 337 crates, where the image build cuts the same crates into 3,314 units (the kernel into 280) and optimises each apart. So round 1 compared, and its sizes were of, binaries no image holds. Both are measured here:

as an image build compiles it (-Csave-temps alone added) x86-64 AArch64 SMP test's image
crates, codegen units 337, 3,314 324, 3,021 124, 1,662
functions, by crate and symbol 34,828 31,948 13,027
functions whose object code differs 21 21 6
linked text, base 24,415,136 19,982,885 5,793,206
linked text, fixed 24,414,656 (−480) 19,982,437 (−448) 5,793,006 (−200)
each crate one unit, its IR kept (round 1's way) x86-64 AArch64 SMP test's image
crates, functions in their IR 337, 21,529 324, 19,528 124, 7,522
functions whose IR differs 26 25 8
functions whose object code differs 25 of 21,716 26 of 19,674 8 of 7,574
of those, fewer exiting blocks in the base arm's loops (opt -passes='print<loops>') 15 15 8
of those, a loop with no exiting block in the base arm 0 0 0

An object's functions are its disassembly with relocations (llvm-objdump -d -r), keyed by crate and symbol; a function's IR is its text less metadata and debug records. In the x86-64 fixed arm 21 crates of a test program (tls-cranelift) were built that the base arm did not build, by a harness command of mine that ran beside that arm; they pair with nothing and are in no count. Linked binaries are no longer compared byte for byte: with two sysroots only the loaders are identical.

The numbers did not change with the arms. Round 1 found 26, 25 and 8 with the switch; the base compiler finds 26, 25 and 8, the same functions. Each of the 51 in the two images, extracted from both arms as round 1 extracted it (llvm-extract --func | opt --strip-debug), is text for text the function round 1 read, in the base arm its "unfixed" and in the fixed arm its "fixed": 26 of 26 and 25 of 25. So what round 1 read stands for the one-unit build at 35ee6ca4d, and the switch did reproduce the old compiler in every function that differs:

where (one-unit build) functions what the base compiler did that the fixed does not lost exit
rustc_demangle v0::Printer (print_sep_list ×4, in_binder, a print_type closure, print_path_maybe_open_generics), linked into the kernel and symbolize 7, twice peeled the loop's first iteration, which moves that iteration's exits out of the loop: 14 of the 15 candidates. Each arm has the same panics, panic_const_add_overflow among them no
uefi::fs::path::Components::next, in the uefi crate the loader depends on 1 dropped count's add-overflow panic. The counter runs from 0 beside a slice index that starts no lower and is bounded by the slice's length, so the panic is dead and dropping it right: the fifteenth candidate. The loaders are byte-identical between the arms, so the function is not in them no
toyos_fat32::fs::Fat32::ensure_capacity ×2 in fileserver, and inlined into update::write_volume 3 widened the u32 cluster index to 64 bits. Same six panics no
base64ct's Encoding::decode and encode ×2, instantiated in password_hash 3 vectorised without the run-time overflow check the fix adds (vector.scevcheck). Same panics no
internal_russh_forked_ssh_key ECDSA try_sign ×3 3 wrote one comparison with its operands the other way round (icmp ult a, b for icmp ugt b, a) no
kurbo::simplify::SimplifyState::flush 1 named values differently; llvm-diff finds no difference and the object code is identical no
snake's event-loop closure, x86-64 only 1 peeled a loop no
rubato NeonSample::get_sinc_interpolated_unsafe, f32 (in soundserver) and f64, AArch64, object code only 2 counted the loop up against 2n (4n) where the fix counts down from n − 1; n is a length shifted right by 3. Every bounds check is in both no

As shipped, the functions that differ are nearly the same ones, and none of them is the same code: no arm's disassembly of any of the 48 equals its one-unit counterpart's, so the IR readings above do not carry to them, and they were read apart, as object code. For each, both arms' conditional branches, returns, every relocation target (each call and each panic) and the instruction mix were tabulated, and the differences read where the table did not explain them:

where (as shipped) functions what the base compiler's code has that the fixed does not lost exit
rustc_demangle v0::Printer: print_sep_list ×4, the print_type closure, print_path_maybe_open_generics (in_binder no longer differs), in the kernel and in symbolize, and the kernel's again in the SMP image 6, five times two conditional branches more (12 against 10, 54 against 52, 23 against 21) and the same relocation targets with the same counts: the peel no
Fat32::ensure_capacity ×2 in fileserver, Fat32<Cached>::write in update 3 per architecture a 64-bit compare and increment where the fixed has 32-bit ones (cmpq/incq against cmpl, a cmn on AArch64). Same conditional branches (22 in each, 38 or 39 in write), same targets no
base64ct decode, encode ×2 in password_hash 3 per architecture one conditional branch fewer, the vectoriser's overflow check the fix adds. Same targets no
kurbo::fit::fit_to_cubic 1 per architecture a branch to core::result::unwrap_failed that cannot be taken: on AArch64 cmp x21, #0x15; b.eq A; add …; b.eq <unwrap_failed>, the second b.eq on the flags of the first, with an add that sets none between. The fixed compiler deletes it; every other target is in both no
libm rem_pio2_large, x86-64 only 1 one conditional branch and a few scalar floating-point instructions more: a peeled iteration. Same target set. Its 564 differing lines, mostly register assignment, were not read line by line no
snake's App::window_event, x86-64 only 1 four conditional branches and one call to fill_rect more: the peel no
rubato get_sinc_interpolated_unsafe ×2, AArch64 2 a compare where the fixed counts down (subs); 14 and 22 conditional branches in both no

So in the 48 the base compiler's code has at least as many conditional branches as the fixed one's everywhere but in password_hash's three, where the one it lacks is a check the old code did without, and the same calls and panics everywhere but in kurbo, where the one it has is dead. ssh_key's three and uefi's one do not differ as shipped.

The sysroot's libraries, all 138 archives of each sysroot unpacked (3,634 objects each, in 128 of them) and compared by archive and symbol: 32,638 functions, 15 differ.

where functions what the base compiler did that the fixed does not lost exit
std::fs::DirBuilder::_create, in libstd for both userland and both UEFI targets 4 tested, after its loop, the byte offset 16·n for zero where the fixed tests the count n. On AArch64 the two arms differ in that one register (cbnz x27 against cbnz x23). n is the length of a vector of 16-byte elements, so the two are zero together no
libunwind's CFI_Parser::parseFDEInstructions and parseCIE, DwarfInstructions::evaluateExpression, LocalAddressSpace::getEncodedP, in the C++ runtime (libc++.a), both architectures 8 kept LEB128's shift count in 64 bits where the fixed keeps it in 32 (mov x10, #-7; add x10, x10, #7; cmp x10, #0x39 against the w registers). The count, bit, is an int that grows with the input and is not bounded in the source; the two arms differ only past its signed overflow, which C leaves undefined. Same conditional branches, returns and targets in each no
libc++'s _Large_integer_to_chars (both) and __barrier_algorithm_base::__arrive (x86-64) 3 a 64-bit index where the fixed has a 32-bit one. Same shape no
the C library (libtoyos_c.a, userland/libc, Rust) 0 of 2,961 and 2,931

A third comparison, the sysroot's own std build run twice into a scratch directory with --emit=llvm-ir (bootstrap stage 0, library, the six guest targets), is round 1's: 2,594 IR files and 21,666 functions, 2 differ, test::cli::parse_opts for the two UEFI targets, in libtest, which nothing ships; llvm-diff finds no difference in it. It does not see DirBuilder::_create, whose IR is the same and whose code is not.

doomgeneric, compiled by clang (userland/doom/build.rs), the base arm's by the base clang: the image build's own 83 objects (82 of doomgeneric, 848 functions), 0 differ. Its 82 files compiled again by each arm's clang to IR with the build's options, for both userland targets: 847 functions, 2 differ on each, ST_loadGraphics and ST_unloadGraphics, where the base clang writes the face table's address as a sign-extended index with an inbounds nuw and the fixed as the pointer it has; same two loops, same exits, and identical object code. Doom is built for x86-64 only.

test_rs_counters_read is not among them. The harness's own virt_el1_smp (tests/virtsmpcase: unmap_touch, test_rs_counters_read, test_rs_trace_read on 8 CPUs under TCG) is the third column above. Its 124 crates differ, as shipped, in the kernel's six rustc_demangle functions and nowhere else; counters_read, trace_read, test-runner, supervisor, logkeeper, toybox and kernelprobe have no function that differs, and std's one is DirBuilder::_create. A counters read calls no demangler and creates no directory. The fault this pull request fixes is not that issue's cause.

"Not a lost exit" is my reading of each difference, by IR for the one-unit build and by the tabulated shape of the object code for the shipped one; it is not a proof.

The defect issue is closed here

#778 landed issues/the-forks-llvm-deletes-a-loops-exit-on-a-no-wrap-flag-scalar-evolution-gives-the-wrong-value.md on main. This branch merges main and deletes the file, its exit met, by number, under LLVM 90d48821e80c5da1 and sysroot 843dffb798581666:

  1. m1.ll through that LLVM's opt -passes=indvars -S: no br i1 false, and @f returns. The store keeps no opt. A development opt built at b7420fe534bf passes Transforms/IndVarSimplify/preinc-nowrap-flags-unobserved-poison.ll, which holds m1, m2, m7, four widths and r1, each keeping its exit, and fails it with the two source files as ceaf0fbb8440 has them (below). The sysroot's own clang, which is that LLVM, compiles m1 at a thousand passes to ret i1 true where the base clang leaves br label %header to itself: the check's first case.

  2. caller is ret i1 true by the issue's own command over its own minns.rs (the u16 file, the issue's text byte for byte), for the four targets it names, by the sysroot 843dffb798581666's rustc (rustc 1.99.0-dev, LLVM 22.1.8). The two ToyOS targets take that sysroot's libraries; the other two take the freestanding key 42125dcfe1c67e4a's, which the sysroot carries as clones (diff -r of each against freestanding/42125dcfe1c67e4a/<target>, exit 0, both). Run at 8f55e8b90, output under the round's logs directory only:

    <store>/sysroots/843dffb798581666/bin/rustc --edition 2021 --crate-type lib --emit llvm-ir,asm --target <target> -C opt-level=2 minns.rs

    target libraries exit caller, IR caller, assembly
    aarch64-unknown-toyos 843dffb798581666 0 start: ret i1 true frame setup, mov w0, #1, frame teardown, ret
    x86_64-unknown-toyos 843dffb798581666 0 start: ret i1 true pushq %rbp; movq %rsp, %rbp; movb $1, %al; popq %rbp; retq
    aarch64-unknown-none-softfloat 42125dcfe1c67e4a 0 start: ret i1 true mov w0, #1; ret
    aarch64-unknown-uefi 42125dcfe1c67e4a 0 start: ret i1 true mov w0, #1; ret

    The issue recorded, for the same file and command under the base compiler, no ret and b .LBB0_1 to itself for the three AArch64 targets, and ret i1 true for x86_64-unknown-toyos. The issue's c_u128.rs for the two ToyOS targets is the check's Rust case, which runs over u128 for all six guest targets: an endless loop under the base compiler, ret i1 true under this one.

  3. The test binary, built with that compiler on Apple silicon without incremental state, exits 0, and this one has a red arm that is the recorded failure itself. CARGO_INCREMENTAL=0 cargo test --locked -p toyos-userbound --test firmware, the binary run whole and ended by PID if still running after 120 s:

tree the base compiler (sysroot 8618c089fa736cb0) the fixed compiler (a1a1399eacc6661a, in the sysroot it had then, 5f582f969b8a4b2e)
main before #780 (an archive of b3322379c) hung: 20 tests ok, then a_port_answers_as_its_declaration_says has been running for over 60 seconds, ended after 120 s exit 0, 21 passed
this branch at 14bfdeebe exit 0, 24 passed exit 0, 24 passed

Since #780 changed toyos-userbound, the test no longer gives the loop the shape, under either compiler: which is why the check does not rest on it.
4. The tree's own functions read: the census above. No hit to name.

The change is to ScalarEvolution and to no client of it. The file's citations stand without it: issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md says the fault is measured not to reach test_rs_counters_read, by the two compilers and no longer by a switch; issues/the-nightlys-macos-job-pins-rustc-1-98-1-for-a-hang-its-test-no-longer-shows.md stays open and states the cause itself. The rule the file carried, that whoever moves the fork to a later upstream reruns its measurements, is src/miscompile.rs's header and the check itself.

The macOS pin's premise is stale

nightly.yml's portability-macos installs 1.98.1 because stable 1.99.0 hung the port test (#778). Stable 1.99.0 (b940084d7 2026-09-28), installed with rustup for this and removed after (rustup toolchain list as it was before), the pin issue's second table row on this Apple-silicon machine:

tree 1.99.0
this branch at 07a317192, whose toyos-userbound, toyos-abi and toyos-bootmap are main's since #780 exit 0, 24 passed, a_port_answers_as_its_declaration_says ... ok
main before #780 (an archive of 805ba7978, the three crates as b3322379c has them) hung, ended by PID after 120 s: the control

1.99.0 no longer hangs the test, and has not since #780, which landed before #778 did. It has the fault as it had: the check's Rust reproducer over u16 and over u128, compiled by it for aarch64-apple-darwin, is a branch to itself. So the pin holds a test green that no longer needs it, and says nothing about the rest of the host suite, which either stable compiles with a faulty LLVM. The issue's record is corrected to this, and its slug with it: issues/rustc-1-99-0-makes-an-endless-loop-of-a-port-test-on-apple-silicon.md is renamed issues/the-nightlys-macos-job-pins-rustc-1-98-1-for-a-hang-its-test-no-longer-shows.md, and its one citation, a comment in nightly.yml, moves; nothing else in the workflow changes. The weakness that outlives the pin, that every host binary is compiled by an upstream rustc whose LLVM has the fault, is written into issues/the-host-job-runs-the-toolchain-the-runner-ships.md, which owns the host's toolchain. The pin itself goes in a follow-up after this lands: one change puts stable back, and the issue is deleted when a nightly on main is green with it.

Gates, at the head 8f55e8b90

The gates ran at 75d5cf822, whose tree 8f55e8b90 is byte for byte (git diff 75d5cf822 8f55e8b90 is empty; the amend changed a commit-message trailer only).

8f55e8b90 is 5b4592e88 and one commit: the pin issue's rename and its citation in nightly.yml, the host issue's paragraph, a filed issue, and src/sysroot.rs's check scratch removed through keystore::remove instead of a discarded fs::remove_dir_all. It touches no guest crate, no fork pin and not sysroot::RECIPE. main is not merged: git merge-tree --write-tree origin/main HEAD against main at 1621281ae (#793) exits 0, no conflict.

gate, at 8f55e8b90 exit
cargo run -- --build-only --arch x86_64 0, at 1-minute load 32
cargo run -- --build-only --arch aarch64 0
cargo run -- --ci host 0, "Host: 78 step(s), all green", at 1-minute load 33 to 51
cargo test --lib sourcegate 0, 10 passed
the issue's command over minns.rs, four targets (the defect issue's item 2 above) 0, 0, 0, 0

No toolchain key moved. Neither build made anything: after them the worktree's target/.deps-stamp names sysroot 843dffb798581666 for both ToyOS targets and freestanding 42125dcfe1c67e4a for the other four, as before. The stamp's compiler af2e2dc427e9f649 is the compiler's identity (Compiler::identity, from its driver), not its key; the LLVM and compiler keys follow from the sysroot key, which hashes the freestanding key (sysroot::key_of), which hashes the compiler's key (freestanding_key_of), which hashes the LLVM's (compiler::key). So 843dffb798581666 unmoved is a1a1399eacc6661a and 90d48821e80c5da1 unmoved, and the store's use marks for compilers/a1a1399eacc6661a and sysroots/843dffb798581666 carry the builds' minute. No key reads src/sysroot.rs but its RECIPE, which this commit does not touch. No guest test and no census was rerun for this commit.

The gates at 5b4592e88, which the T14 pass below was made from. main at 965e62bb1 (#794) is merged, its tip when the gates began. Run from no guest target directory a census arm left, in this order:

gate exit
cargo run -- --build-only --arch x86_64 0
cargo run -- --build-only --arch aarch64 0
cargo run -- --ci host 0, 78 steps all green, at 1-minute load 50 to 39
cargo test, the whole guest suite, both architectures' tests 0, 37 passed of 37, at 1-minute load 39 to 47
the check against main's sysroot and against the one that lands (the table above) 101 and 0
the build that made the toolchain's four keys, the check in its log 0
cargo test --test toyos-build -- --metal --metal-readback <dir>, the whole list: images built, no machine touched 2, the staging mode's own exit, which is never 0 because nothing was judged: "staged 23 image(s)", the 23 boots --metal --list names

The guest suite is cargo test with no filter, the virt_* AArch64 tests under TCG among them, 142 s. The first build's target/.deps-stamp names the sysroot and freestanding keys above; the LLVM and compiler keys are in the build's own log lines (Building sysroot 843dffb798581666: … the compiler …/compilers/a1a1399eacc6661a/stage2 …).

The T14, at 5b4592e88

The orchestrator's reading (the round-2 review's BLOCKER 6; his comments on this pull request). The whole metal list, 23 boots, every image built by compiler a1a1399eacc6661a on LLVM 90d48821e80c5da1, each image's SHA-256 checked against the staged list before it was flashed, every toyos-metal run exit 0 with the stick's FAT check on. Judged with cargo test --test toyos-build -- --metal --metal-readback <dir> over the whole list: exit 0, 293 passed, 0 failed, 23 boots.

  • Measured names: 70 numbers, 0 past their record, 0 whose owner failed. Timing verdicts come only from metal; these are the ones this compiler has.
  • The kernel's six (the demangler's list printers): the hard-lockup boot's sealed record printed its symbolised frames whole, <kernel::sync::Lock<bool>>::lock (twice, with the lock's source line), kernel::deadline::this_cpu (six CPUs) and kernel::hardlockup::probe::hold_and_sample, name for name the frames of three earlier readings of that boot on main's compiler; the USB-load boot printed <kernel::drivers::xhci::XhciController>::wait_transfer and <kernel::hw::KernelHw as kernel::sched::hw::Machine>::idle_wait, each closed.
  • What it does not show: no metal row printed a frame with a comma-separated list (two or more generic arguments, a tuple, a const list). Every list the demangler printed on the machine had one element or was a <T as Trait> path, so the loop's first pass, the one the two compilers treat differently, ran there; the second element of a demangled list is held by no execution: the host suites run rustc_demangle compiled by upstream's rustc, not by this compiler, so what holds it is round 2's reading of both arms' object code.
  • fileserver's ensure_capacity and std's DirBuilder::_create: PASS mkdir_cap on testcases-mkdir; the file-growing members of shared and testcases all exit=0; no boot's FAT check failed.
  • No bound fired on a boot not staged to wedge.

The pass measured this compiler on the tree at 5b4592e88. 8f55e8b90 changes no guest crate, fork pin or sysroot::RECIPE, the condition the review gave for it to stand. How it relates to what main has landed since (#793, #795, and what follows) is the orchestrator's and the next review's.

The independent oracles for the LLVM change: upstream's own report #175729 and its reduction (the same fold, the same code path); upstream's #118959, whose source change this is: of the 22 tests it had updated, this tree's regenerated expectations hold every check line it adds in 18, and in the other four (nowrap-preinc-limits.ll, pr27315.ll, lftr.ll, wrong_assert_in_peeling.ll) the same output under other FileCheck names or in hand-written form; LLVM's, clang's and lld's whole test suites; and a recorded real failure, the port test at main before #780, which the compiler before the fix hangs and the fixed one passes. No third-party checker was run (below).

LLVM's own tests

At b7420fe534bf, in a development tree (Release, assertions on, every target, clang and lld), llvm-lit over each project's whole test directory, the binaries linked at that commit:

suite discovered passed failed exit
LLVM (check-llvm's tests, its unit tests among them) 71,556 69,961 1 1
clang (check-clang's) 49,762 49,026 0 0
lld (check-lld's) 3,180 3,141 0 0
  • The one failure is the host's: LLVM-Unit :: TargetParser/./TargetParserTests/HostTest/getMacOSHostVersion compares the product version sw_vers gives, major 27 on this macOS, with the one LLVM derives from the Darwin kernel's release, 28 (unittests/TargetParser/Host.cpp:548; run alone it exits 1 with that message). It reads no optimiser.
  • CodeGen/PowerPC/git_revision.ll passes in that run. Round 1 claimed it passed after a relink and had no log of it; its only whole-suite run at 81b496be0342 was exit 1 with that test failed, its llc linked one commit earlier.
  • Polly is not run: the toolchain's build does not enable it (src/clang.rs's LLVM_CONFIG names clang and no polly).
  • The red arm: ScalarEvolution.cpp and ScalarEvolution.h as ceaf0fbb8440 has them, a checked patch on that tree, opt rebuilt: the two new tests fail (lit exit 1) and iv-select-cmp.ll passes; the patch reversed and opt rebuilt, the three pass (exit 0), the checkout clean.
  • 44 tests change and 2 are new, as in round 1; b7420fe534bf touches three of them, the two new ones and iv-select-cmp.ll.

One by one

Every row is a test under llvm/test whose expectation or input the two LLVM commits change. "Unobserved" means what the fix asks: the phi being poison is not shown to be undefined behaviour, and the increment's poison reaches no undefined behaviour in a block dominating the latch, so the increment's flag is not proven for the phi's recurrence.

New. Both fail with ScalarEvolution.cpp and .h as ceaf0fbb8440 has them and pass at b7420fe534bf (the red arm above):

test what it holds
Transforms/IndVarSimplify/preinc-nowrap-flags-unobserved-poison.ll m1 (@only_exit), m2 (@second_exit), m7 (@ranged_start), m2 at i8, i32, i64, i128, and r1 (@probe): every %done keeps its exit (@only_exit, which has no side effect, leaves at once: br i1 true); the source before the fix makes each br i1 false.
Analysis/ScalarEvolution/preinc-nowrap-flags-unobserved-poison.ll {-3,+,1} is not <nuw> where the poison is unobserved (U: [-3,1), not [-3,0)); it keeps <nuw> where the header passes the phi as noundef and where a block dominating the latch passes the increment as noundef; it loses it where that use is on one path to the latch only.

Regenerated by the script each names (update_test_checks.py, update_analyze_test_checks.py, update_llc_test_checks.py), each diff read. The same regeneration with the old behaviour as the default changed 12 lines of script formatting in four of them (pr27133.ll, scev-expander-preserve-lcssa.ll, invalidate-scev-dispositions.ll, runtime-exit-phi-scev-invalidation.ll); those lines, and one more of the kind in the AMDGPU test, are taken back out, so each diff is the fix's alone.

test what changes, and why
Analysis/Delinearization/gcd_multiply_expr.ll the outer {(%1 * %2),+,1} loses <nw> in nine access functions; unobserved. Still "failed to delinearize", as before.
Analysis/DependenceAnalysis/monotonicity-invariant.ll @invariant_plus_monotonic0: {%x,+,1} loses <nsw>: %offset is read only in an inner loop its guard may skip. Monotonicity is Unknown and the dependence "confused".
Analysis/DependenceAnalysis/monotonicity-no-wrap-flags.ll @conditional_store1: the inner {..,+,1} loses <nsw>: the store that would observe it is conditional. Unknown, "confused".
Analysis/ScalarEvolution/decrementing_addrecs.ll %j: {(-1 + %n),+,-1} is <nw> (proven), no longer <nsw>: %j.next feeds the phi alone.
Analysis/ScalarEvolution/different-loops-recs.ll @test_04: %tmp {2,+,1} loses <nuw><nsw> and its range: the loop leaves on an opaque i1 and %tmp4 feeds the phi alone.
Analysis/ScalarEvolution/flags-from-poison.ll @test-add-not-header5: the loop is br label %loop; nothing observes %nexti. {0,+,1} and what is built on it lose their flags.
Analysis/ScalarEvolution/infer-prestart-no-wrap.ll @infer.sext.1, .sext.2, .zext.1: the loops leave on a loaded i1; the phi's flag goes and the extension is no longer folded into the recurrence.
Analysis/ScalarEvolution/iv-poison.ll the functions whose increment is unobserved (iv_nsw_poison2, iv_*_extra_use*, iv_nuw_poison2, iv_nuw_poison_extra_use): the phi loses the flag, and so does the sibling phi that shared its expression, which is the defect itself. @use there takes no noundef.
Analysis/ScalarEvolution/max-backedge-taken-count-guard-info.ll @crash: {null,+,-1} loses <nw> (taken from an inbounds GEP that feeds the phi alone), and the counts built on it.
Analysis/ScalarEvolution/max-expr-cache.ll @smax, @umax: the outer %tmp5 {0,+,1} loses <nuw><nsw> and its range; its increment feeds the phi alone.
Analysis/ScalarEvolution/range-signedness.ll %idx {0,+,1} loses <nuw><nsw>: the loop leaves on a volatile load.
Analysis/ScalarEvolution/smin-smax-folds.ll %i.011 {%n,+,-1} is <nw>, no longer <nsw>: a call that may not return stands between the phi and the branch that reads it. Exit counts unchanged.
Analysis/ScalarEvolution/trip-count-andor-selectform.ll unsimplified_and2, unsimplified_or3, reversed_and2, reversed_or3: the exit reads the increment through a select whose condition is a constant, an operand propagatesPoison does not follow. The exact count becomes a predicated one (<nusw>).
Analysis/ScalarEvolutionDivision/sdiv.ll the numerator {0,+,%step} loses <nuw><nsw> in three functions, and one quotient <nsw>. Quotients and remainders are otherwise the same.
CodeGen/PowerPC/no-ctr-loop-if-exit-in-nested-loop.ll loop strength reduction picks other induction variables for the inner loop; still no CTR loop, which is what the test holds.
Transforms/IndVarSimplify/X86/pr27133.ll the i32 counter is not widened: it reaches an invoke argument that is not noundef.
Transforms/IndVarSimplify/iv-poison.ll the old expectations were the miscompile: icmp ult i4 [[IV_0]], [[ARG]] had become icmp ult i4 1, [[ARG]] (or [[START]]) on a flag that belonged to the other phi. The exit test reads the induction variable again, and add nuw nsw is add nsw where nuw was inferred from it.
Transforms/IndVarSimplify/lftr-pr31181.ll switch_to_different_iv_*_poison: %iv2.inc no longer gains nsw.
Transforms/IndVarSimplify/lftr.ll @test_udiv_as_shift: a volatile store, which may not return, stands between the phi and the branch that reads it, so the exit test is not rewritten to a trip count.
Transforms/IndVarSimplify/no-iv-rewrite.ll @phiUsesTrunc: not widened; the loop leaves on an opaque i1.
Transforms/IndVarSimplify/pr30806-phi-scev.ll %inx.06 is not widened to i64: its increment reaches a call argument that is not noundef.
Transforms/IndVarSimplify/pr55925.ll not widened (an invoke argument, not noundef); the dead GEP the old output had removed stays.
Transforms/IndVarSimplify/scev-expander-preserve-lcssa.ll @test6: add nuw nsw is add nsw.
Transforms/IndVarSimplify/simplify-icmp-operands-order.ll two functions: the second loop is br label %exit.loop and its counter reaches a call argument that is not noundef; neither it nor the outer counter it starts from is widened.
Transforms/IndVarSimplify/widen-nonnegative-countdown.ll sext_preinc*: a call that may not return stands between the phi and the branch that reads it; not widened.
Transforms/LoopIdiom/pr80954.ll the memset's pointer is computed from the induction variable instead of a second pointer recurrence. The memset is still made.
Transforms/LoopSimplifyCFG/invalidate-scev-dispositions.ll add nuw nsw is add nsw in the INDVARS run.
Transforms/LoopStrengthReduce/AMDGPU/lsr-invalid-ptr-extend.ll @scaledregtest: the first loop never leaves (br i1 false) and %inc feeds its phi alone; strength reduction keeps integer induction variables where it had made pointer ones. The test's subject, that it does not assert, stands. Line for line upstream's update.
Transforms/LoopStrengthReduce/X86/sibling-loops.ll two increments lose nuw.
Transforms/LoopUnroll/runtime-exit-phi-scev-invalidation.ll @pr56282: one increment loses nuw.
Transforms/LoopUnroll/wrong_assert_in_peeling.ll the loop is no longer peeled: the peel count rested on %tmp3's nsw, from an increment that feeds the phi and a comparison with undef. The test still runs the pass over the input that once asserted.
Transforms/LoopVectorize/ARM/mve-reg-pressure-vmla.ll the stores through %ptr.iv.2 come 37 instructions after the phi, past the 32 programUndefinedIfPoison scans, so its flag is unproven and the vectoriser adds a run-time overflow check. Still vectorised.
Transforms/LoopVectorize/iv-select-cmp.ll input and expectation of one function: @select_icmp_min_valid_iv_start leaves its loop on %inc3, the increment of the counter its select reads, so that increment's nsw is observed, %iv.j keeps the range that excludes the sentinel, and the loop is vectorised as the test's name says, its trip count %n + INT64_MAX where it was %n. Round 1 had regenerated it to "not vectorised". Nothing else in the file differs from ceaf0fbb8440's, and the test passes with the old source too.
Transforms/LoopVectorize/reuse-lcssa-phi-scev-expansion.ll one increment loses nuw.

Hand-written, changed by hand:

test what changes, and why
Analysis/ScalarEvolution/pr27315.ll expects {0,+,1}<%loop>, {1,+,1}<%loop> and the unfolded sext, as upstream's PR #118959 does. What the test holds, that %iv.inc.maywrap is not <nsw>, stands.
Analysis/ScalarEvolution/nowrap-preinc-limits.ll @f expects (zext i32 {2,+,1}<%loop> to i64), as upstream's PR does; @g, whose increment a load's address observes, is unchanged.
Transforms/LoopIdiom/introduce-memset-in-outerloop.ll the outer count is i32 5, found by evaluating the loop, where it was i64 1, derived from an nsw the loop violates on its fifth iteration. The test's subject, that the count is the same before and after loop-idiom, holds on both RUN lines.
Transforms/SampleProfile/pseudo-probe-twoaddr.ll the ADD64ri32 the test looks for carries no nuw; its subject is the two-address form.
Transforms/IndVarSimplify/2011-09-10-widen-nsw.ll, pr25578.ll input: @use takes its argument noundef, so the counter's poison is observed and the widening each test is about still happens. Expectations unchanged.
Transforms/LoopStrengthReduce/X86/2008-08-14-ShadowIV.ll, preserving-debugloc-phi-binop.ll input: @foo takes %tmp1 as noundef in the functions that expect a shadow induction variable (foobar6, foobar8). Expectations unchanged. Without it the shadow variable is legal for that one use and ScalarEvolution has no way to say so: a transform the fix costs.
Analysis/LoopCacheAnalysis/compute-cost.ll, PowerPC/compute-cost.ll input: @handle_to_ptr_2's three loops leave on icmp eq i16 %iv, 99 instead of an opaque i1, which makes each trip count the 100 the old costs took as the default for an unknown one. Expectations unchanged.

Not measured, and what I am unsure of

  • A third-party checker. Alive2 over indvars on the two new tests was not run. Neither Z3 nor re2c is on this machine, and Alive2 needs an LLVM built with RTTI and exceptions, a third build tree beside the two this round already built; with the machine at a 1-minute load of 24 to 146 through the round, that did not fit the hour the brief allowed for it. The soundness of A stays an argument its readers agree on, and no mechanised proof.
  • The T14 at a later head. The pass above is of 5b4592e88; nothing ran on the machine since. A demangled list with a second element ran on no metal row.
  • Speed. The census counts functions and bytes; no QEMU run times anything. A loop that lost its peel or its widened counter is slower by an amount nobody measured.
  • The census is one merge of main behind the head. It was made at 35ee6ca4d; toyos-net-udp: a datagram waits for its next hop in its own sender; [ip] takes none it cannot frame and holds only its own messages; a failed hop drops what waited; the node reads a refused query in the opportunity that refused it #792 and A metal boot's list bound and deadline follow from who rides it, at 300 and 100 ms a member: shared-2 and the three bounds rows ride shared, 25 boots to 23 #794 changed five guest crates since (toyos-net-ip, toyos-net-shard, toyos-net-udp, toyos-tco, netstack's node). The compilers and all four keys are the head's.
  • main has moved. 55e4e1dd2 (calc, snake and doom are on the AArch64 ROOT: the list that left them out is deleted, and the owner's word of 2026-10-09 ships doom there #795) puts calc, snake and doom on the AArch64 ROOT and 1621281ae (toyos-mdns claims its name before it uses it: three probes, the tie-break, a conflict's outcome, RFC 6762 §8.1's bound on what a peer's messages cost, and the link's return in both callers #793) changes the shipped netstack's mDNS responder; neither is merged here, and the branch merges main at 1621281ae without conflict (git merge-tree, exit 0). Their AArch64 builds are in no column of the census: doom's C for that target was compared as IR by hand and not as the build's objects, and the two toolkit apps not at all.
  • How the shipped build's 48 functions were read. By what each arm's object code has (conditional branches, returns, relocation targets, instruction mix), and line by line only where that did not explain a difference (kurbo's dead branch, std's one register, the AArch64 C++ runtime). libm's rem_pio2_large and libunwind's four on x86-64 differ mostly in register assignment over hundreds of lines; they were read by shape.
  • A compiler built on Linux. Every measurement here is of compilers built on this Apple-silicon host. CI's toolchain / build builds the four keys on Linux; nothing here says what that compiler emits beyond its own check passing.
  • The gaps in the A-against-B count. The three-way run had seven targets and not yet every tool: 1,056 of its tests could not run, so B's 182 is a floor. A's count is exact, 44 with every target: at b7420fe534bf the LLVM suite's one failure is the host-version unit test above.
  • B's ten aborts: one was read (nsw.ll); the other nine are "Abort trap" in round 1's regeneration log and may be other assertions.
  • Host-side Rust. toyos-build, the harness and every host test are compiled by the host's stable rustc, whose LLVM has the fault. issues/the-host-job-runs-the-toolchain-the-runner-ships.md now says so and owns it.
  • programUndefinedIfPoison's 32-instruction scan is taken as upstream has it. Widening the first condition to any block that every path from the header reaches would keep more flags; it needs its own soundness argument around calls that do not return, and is not attempted.
  • Round 1's census compared one-unit builds and called them the images; its −528 and −464 bytes were of those. The shipped build's are −480 and −448.

🤖 Generated with Claude Code

https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C

Japabu and others added 3 commits October 9, 2026 06:39
…oot whose compiler does not is refused

The fork's LLVM gave a header phi's recurrence the wrap flags of its
increment, which say only that the increment is poison where it wraps, and
`indvars` folded the exit an inclusive range takes at its integer type's
maximum to `false`: safe Rust became an endless loop for
`aarch64-unknown-toyos` (`u16`, `u128`) and `x86_64-unknown-toyos` (`u128`).
Upstream knows it as llvm/llvm-project#175729, open.

- `rust` moves to 9e7b17b52f9, whose `src/llvm-project` is 81b496be0342:
  ScalarEvolution transfers the flags only where they are proven for the
  recurrence, behind the hidden `-scev-unconditional-preinc-nowrap-flags`.
- `src/miscompile.rs` compiles the reproducer over `u16` and `u128` for both
  userland targets with every sysroot's compiler before the sysroot is
  published, and refuses one whose `caller` is not `ret i1 true`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
`issues/the-forks-llvm-deletes-a-loops-exit-on-a-no-wrap-flag-scalar-evolution-gives-the-wrong-value.md`
goes, with its three citations rewritten to stand without it.

Its exit, under LLVM 2b9f1f003570f5b2 and sysroot d45d2461afc0b940:

1. `m1.ll` through that LLVM's own `opt -passes=indvars -S`: no `br i1
   false`, `@f` returns; one with the switch.
2. `caller` is `ret i1 true` in `minns.rs` and `c_u128.rs` for
   `aarch64-unknown-toyos`, `x86_64-unknown-toyos`,
   `aarch64-unknown-none-softfloat` and `aarch64-unknown-uefi`.
3. `cargo test --locked -p toyos-userbound --test firmware` built by that
   compiler without incremental state on Apple silicon: exit 0, 24 passed.
4. The tree built twice with the one compiler, the switch given and
   withheld, compared function by function: no function lost a loop exit.

The rule the file carried is `src/miscompile.rs`'s: every sysroot's compiler
compiles the reproducer right or the sysroot is refused, which is the
measurement the file asked of whoever moves the fork to a later upstream.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Mutation patches and census scripts

$TREE is this branch's worktree, $SCRATCH a scratch directory outside it, $STORE the host's toolchain store. Nothing here is in the tree.

The check's negative control

m0 adds a driver test that runs miscompile::refuse against a real sysroot; m1 is m0 with the switch added to the check's rustc command. Each was applied with git apply --check && git apply, run as MISCOMPILE_SYSROOT=$STORE/sysroots/<key> cargo test --lib miscompile::tests::the_check_against_a_real_sysroot, and reversed with git apply -R in the same loop; git status --porcelain printed nothing after each.

m0-driver-only.patch
--- a/src/miscompile.rs
+++ b/src/miscompile.rs
@@ -120,4 +120,12 @@
         assert!(!returns_true(&right.replace("ret i1 true", "ret i1 false"), "caller"));
         assert!(!returns_true(&right.replace("@caller(", "@other("), "caller"), "a module with no `caller` answers nothing");
     }
+
+    /// Mutation driver, not part of the tree: the check itself against the sysroot `MISCOMPILE_SYSROOT` names.
+    #[test]
+    fn the_check_against_a_real_sysroot() {
+        let sysroot = std::env::var("MISCOMPILE_SYSROOT").expect("MISCOMPILE_SYSROOT");
+        let scratch = toyos_tmpdir::TempDir::new("miscompile-control");
+        refuse(Path::new(&sysroot), &scratch.join("scratch"));
+    }
 }
m1-switch-off.patch
--- a/src/miscompile.rs
+++ b/src/miscompile.rs
@@ -44,6 +44,7 @@
                 .args(["-C", "opt-level=2", "-C", "codegen-units=1", "--emit", "llvm-ir", "-o"])
                 .arg(&ir)
                 .arg(&source)
+                .args(["-C", "llvm-args=-scev-unconditional-preinc-nowrap-flags"])
                 .env_remove("RUSTFLAGS")
                 .output()
                 .unwrap_or_else(|e| panic!("run {}: {e}", rustc.display()));
@@ -120,4 +121,12 @@
         assert!(!returns_true(&right.replace("ret i1 true", "ret i1 false"), "caller"));
         assert!(!returns_true(&right.replace("@caller(", "@other("), "caller"), "a module with no `caller` answers nothing");
     }
+
+    /// Mutation driver, not part of the tree: the check itself against the sysroot `MISCOMPILE_SYSROOT` names.
+    #[test]
+    fn the_check_against_a_real_sysroot() {
+        let sysroot = std::env::var("MISCOMPILE_SYSROOT").expect("MISCOMPILE_SYSROOT");
+        let scratch = toyos_tmpdir::TempDir::new("miscompile-control");
+        refuse(Path::new(&sysroot), &scratch.join("scratch"));
+    }
 }
cases.sh: the check's compile per case, with or without extra rustc arguments
#!/bin/sh
# cases.sh <sysroot> <out-dir> [extra rustc args]: the miscompile check's own compile of
# src/miscompile/last_exit.rs, per integer type and target, and whether `caller` is one
# block that returns true.
S=$1; O=$2; shift 2
W=$TREE
mkdir -p "$O"; bad=0
for port in u16 u128; do
  sed "s/^type Port = u16;/type Port = $port;/" $W/src/miscompile/last_exit.rs > "$O/last_exit_$port.rs"
  for target in $TARGETS; do
    ir="$O/last_exit_$port-$target.ll"
    "$S/bin/rustc" --edition 2021 --crate-type lib --target $target -C opt-level=2 -C codegen-units=1 --emit llvm-ir -o "$ir" "$@" "$O/last_exit_$port.rs" || { echo "$port $target: rustc failed"; bad=1; continue; }
    body=$(sed -n '/^define .*@caller(/,/^}/p' "$ir" | sed '1d;$d' | sed 's/^ *//' | grep -v '^$' | grep -v ':$')
    if [ "$body" = "ret i1 true" ]; then echo "$port $target: ret i1 true"; else echo "$port $target: WRONG: $(echo "$body" | tr '\n' ';')"; bad=1; fi
  done
done
exit $bad

The census

rustc-wrapper.sh
#!/bin/sh
# cargo's rustc wrapper for the census: a guest crate's compile gets --emit=llvm-ir and
# -Csave-temps (its object file stays beside its rlib or binary), and
# whatever census/arm-flags holds (nothing, or the switch). The command line cargo hashes
# is the same in both arms, so file names, symbol names and type ids are too.
rustc=$1; shift
case " $* " in
  *" --print"*|*" -vV "*) exec "$rustc" "$@";;
  *"-unknown-toyos "*|*"-unknown-none "*|*"-unknown-none-softfloat "*|*"-unknown-uefi "*)
    exec "$rustc" "$@" --emit=llvm-ir -Csave-temps $(cat $SCRATCH/census/arm-flags);;
  *) exec "$rustc" "$@";;
esac
run-arm.sh
#!/bin/sh
# run-arm.sh <arch> <unfixed|fixed>: one --build-only of the whole image from no guest
# artifact of that architecture, every guest crate compiled through census/rustc-wrapper.sh
# (--emit=llvm-ir, and in the unfixed arm -Cllvm-args=-scev-unconditional-preinc-nowrap-flags),
# then its IR and binaries copied out.
set -e
W=$TREE
X=$SCRATCH
arch=$1; arm=$2
case $arm in
  unfixed) echo "-Cllvm-args=-scev-unconditional-preinc-nowrap-flags" > $X/census/arm-flags;;
  fixed) : > $X/census/arm-flags;;
  *) echo "arm?"; exit 2;;
esac
printf '[build]\nrustc-wrapper = "%s"\n' $X/census/rustc-wrapper.sh > $W/.cargo/local.toml
log=$X/logs/30-census-$arch-$arm.log
{ date; uptime; cat $W/.cargo/local.toml; echo "arm-flags: $(cat $X/census/arm-flags)"; } > $log
cd $W
# no guest artifact of this architecture survives into the arm: every crate is compiled in it
find . -path ./rust -prune -o -type d -name "$arch-unknown-*" -path '*/target/*' -print -prune > $X/census/removed-$arch-$arm.txt
while read -r d; do rm -rf "$d"; done < $X/census/removed-$arch-$arm.txt
echo "removed $(wc -l < $X/census/removed-$arch-$arm.txt) guest target directories" >> $log
rc=0; cargo run -- --build-only --arch $arch >> $log 2>&1 || rc=$?
echo "BUILD EXIT=$rc $(date)" >> $log; uptime >> $log
[ $rc = 0 ] && $X/census/collect.sh $arch $arm >> $log 2>&1
echo "ARM DONE rc=$rc" >> $log
exit $rc
run-virtsmp-arm.sh
#!/bin/sh
# run-virtsmp-arm.sh <unfixed|fixed>: the harness's own build and run of virt_el1_smp
# (tests/virtsmpcase: unmap_touch, test_rs_counters_read, test_rs_trace_read on 8 CPUs under
# TCG), from no AArch64 guest artifact, every guest crate compiled through the census wrapper;
# then its IR, objects and binaries copied out as architecture "virtsmp".
set -e
W=$TREE
X=$SCRATCH
arm=$1
case $arm in
  unfixed) echo "-Cllvm-args=-scev-unconditional-preinc-nowrap-flags" > $X/census/arm-flags;;
  fixed) : > $X/census/arm-flags;;
  *) echo "arm?"; exit 2;;
esac
printf '[build]\nrustc-wrapper = "%s"\n' $X/census/rustc-wrapper.sh > $W/.cargo/local.toml
log=$X/logs/32-census-virtsmp-$arm.log
{ date; uptime; echo "arm-flags: $(cat $X/census/arm-flags)"; } > $log
cd $W
find . -path ./rust -prune -o -type d -name "aarch64-unknown-*" -path '*/target/*' -print -prune > $X/census/removed-virtsmp-$arm.txt
while read -r d; do rm -rf "$d"; done < $X/census/removed-virtsmp-$arm.txt
echo "removed $(wc -l < $X/census/removed-virtsmp-$arm.txt) guest target directories" >> $log
rc=0; cargo test --test toyos-build -- virt_el1_smp >> $log 2>&1 || rc=$?
echo "TEST EXIT=$rc $(date)" >> $log; uptime >> $log
rm -rf $X/census/virtsmp/$arm; $X/census/collect.sh aarch64 $arm >> $log 2>&1 && mkdir -p $X/census/virtsmp && mv $X/census/aarch64/$arm $X/census/virtsmp/$arm
echo "ARM DONE rc=$rc" >> $log
exit $rc
std-arm.sh
#!/bin/sh
# std-arm.sh <unfixed|fixed>: the sysroot's own std build (src/sysroot.rs's build_std: bootstrap
# stage-0 local rebuild of `library` for the six guest targets by the store's compiler), into a
# build directory in the scratchpad, with RUSTFLAGS carrying --emit=llvm-ir and, in the
# unfixed arm, the switch. The fork's two lockfiles are put back as they were.
set -e
X=$SCRATCH
F=$TREE/rust
C=$STORE/compilers/2c79bce70f85e4fa/stage2
arm=$1; B=$X/census/std/$arm/build; log=$X/logs/31-census-std-$arm.log
case $arm in
  unfixed) flags="--emit=llvm-ir -Cllvm-args=-scev-unconditional-preinc-nowrap-flags";;
  fixed) flags="--emit=llvm-ir";;
  *) echo "arm?"; exit 2;;
esac
host=aarch64-apple-darwin
rm -rf $X/census/std/$arm; mkdir -p $B
cat > $B/bootstrap.toml <<TOML
change-id = "ignore"
profile = "compiler"

[build]
rustc = "$C/bin/rustc"
cargo = "$RUSTUP_HOME/toolchains/nightly-2026-07-22-$host/bin/cargo"
local-rebuild = true
build-dir = "$B"
host = ["$host"]
target = ["x86_64-unknown-toyos", "x86_64-unknown-none", "x86_64-unknown-uefi", "aarch64-unknown-toyos", "aarch64-unknown-none-softfloat", "aarch64-unknown-uefi"]

[llvm]
download-ci-llvm = false
ninja = false

[rust]
lld = false
debug-assertions-std = false

[target.x86_64-unknown-toyos]
linker = "$C/lib/rustlib/$host/bin/rust-lld"
rpath = false

[target.aarch64-unknown-toyos]
linker = "$C/lib/rustlib/$host/bin/rust-lld"
rpath = false
TOML
cd $F
cp Cargo.lock $X/census/std/$arm/Cargo.lock.kept; cp library/Cargo.lock $X/census/std/$arm/library-Cargo.lock.kept
{ date; uptime; echo "RUSTFLAGS=$flags"; } > $log
rc=0
env -u GITHUB_ACTIONS -u CI BOOTSTRAP_SKIP_TARGET_SANITY=1 RUSTFLAGS="$flags" ./x build library --stage 0 --config $B/bootstrap.toml --warnings warn \
  --target x86_64-unknown-toyos,x86_64-unknown-none,x86_64-unknown-uefi,aarch64-unknown-toyos,aarch64-unknown-none-softfloat,aarch64-unknown-uefi >> $log 2>&1 || rc=$?
cp $X/census/std/$arm/Cargo.lock.kept Cargo.lock; cp $X/census/std/$arm/library-Cargo.lock.kept library/Cargo.lock
echo "BUILD EXIT=$rc $(date)" >> $log; uptime >> $log
git status --porcelain --ignore-submodules=none >> $log
mkdir -p $X/census/std/$arm/ll
( cd $B/$host/stage0-std && find . -name '*.ll' -path '*/dist/build/*/out/*' ) > $X/census/std/$arm/ll.list
# one directory per target; cargo's file-name hash and rustc's per-session part of a codegen
# unit's name, which differ between the arms, are taken out of each name
while read -r f; do
  t=$(echo "$f" | cut -d/ -f2)
  n=$(basename "$f" | sed -e 's/-[0-9a-f]\{16\}\././' -e 's/\.[0-9a-z]\{7\}\.rcgu\.ll$/.rcgu.ll/')
  mkdir -p "$X/census/std/$arm/ll/$t"
  [ -e "$X/census/std/$arm/ll/$t/$n" ] && { echo "two files under $t/$n" >> $log; exit 1; }
  cp "$B/$host/stage0-std/$f" "$X/census/std/$arm/ll/$t/$n"
done < $X/census/std/$arm/ll.list
echo "std $arm: $(wc -l < $X/census/std/$arm/ll.list) IR files" >> $log
exit $rc
collect.sh
#!/bin/sh
# collect.sh <arch> <arm>: copy what the last guest build emitted out of the worktree.
# IR: every crate's optimised .ll under a guest target's toyos/deps. Binaries: every
# file directly under a guest target's toyos/ that is ELF or PE.
set -e
W=$TREE
C=$SCRATCH/census
arch=$1; arm=$2; out=$C/$arch/$arm
rm -rf "$out"; mkdir -p "$out/ll" "$out/bin"
cd $W
find . -path ./rust -prune -o -type f -name '*.ll' -path "*/$arch-unknown-*/toyos/deps/*" -print > "$out/ll.list"
while read -r f; do d="$out/ll/$(dirname "$f")"; mkdir -p "$d"; cp "$f" "$d/"; done < "$out/ll.list"
find . -path ./rust -prune -o -type f -name '*.rcgu.o' -path "*/$arch-unknown-*/toyos/deps/*" -print > "$out/obj.list"
mkdir -p "$out/obj"
while read -r f; do d="$out/obj/$(dirname "$f")"; mkdir -p "$d"; cp "$f" "$d/"; done < "$out/obj.list"
find . -path ./rust -prune -o -type f -path "*/$arch-unknown-*/toyos/*" -not -path '*/toyos/*/*' -print > "$out/bin.cand"
while read -r f; do
  m=$(head -c 4 "$f" | od -An -tx1 | tr -d ' \n')
  case $m in 7f454c46|4d5a*) d="$out/bin/$(dirname "$f")"; mkdir -p "$d"; cp "$f" "$d/";; esac
done < "$out/bin.cand"
echo "$arch $arm: $(wc -l < "$out/ll.list") IR files, $(wc -l < "$out/obj.list") objects, $(find "$out/bin" -type f | wc -l) binaries"
irdiff.py
# irdiff.py <unfixed-ll-dir> <fixed-ll-dir>: per-function comparison of two builds' IR.
# A function body is its text with metadata references, debug records and comments
# removed. Prints one line per function that differs: file, symbol, and for each arm
# (lines, conditional branches, terminators).
import sys, re, os
DEF = re.compile(r'^define .*?@("[^"]+"|[\w.$]+)\(')
def norm(line):
    s = line.rstrip()
    if s.lstrip().startswith('#dbg_'): return None
    s = re.sub(r'\s*;.*$', '', s)
    s = re.sub(r',? ![\w.]+ !\d+', '', s)
    s = re.sub(r'!\d+', '!N', s)
    return s if s.strip() else None
def fns(path):
    out = {}; name = None; body = []
    with open(path, errors='replace') as f:
        for line in f:
            if name is None:
                m = DEF.match(line)
                if m: name = m.group(1).strip('"'); body = []
            elif line.startswith('}'):
                out[name] = body; name = None
            else:
                s = norm(line)
                if s is not None: body.append(s)
    return out
def shape(body):
    return (len(body), sum(1 for l in body if l.lstrip().startswith('br i1 ')),
            sum(1 for l in body if re.match(r'\s*(br|switch|ret|unreachable|resume|invoke|callbr|indirectbr) ', l) or ' invoke ' in l))
# With a third argument, files pair by their path less cargo's -<16 hex> file-name hash,
# which reads RUSTFLAGS where the symbol hash does not; two files of one arm under one such
# key are refused.
a_root, b_root = sys.argv[1], sys.argv[2]
strip = len(sys.argv) > 3
def key(rel): return re.sub(r'-[0-9a-f]{16}\.ll$', '.ll', rel) if strip else rel
def index(root):
    out = {}
    for d, _, fs in os.walk(root):
        for f in fs:
            # -Csave-temps leaves a one-unit crate's unit beside the crate's own IR: the same
            # functions again. A crate of several units has only its units' files.
            if f.endswith('.rcgu.ll') and f.split('.')[0] + '.ll' in fs: continue
            if f.endswith('.ll'):
                rel = os.path.relpath(os.path.join(d, f), root)
                if key(rel) in out: raise SystemExit('two files under %s in %s' % (key(rel), root))
                out[key(rel)] = rel
    return out
ia, ib = index(a_root), index(b_root)
files = set(ia) | set(ib)
total = differ = 0; only = []
for rel in sorted(files):
    if rel not in ia or rel not in ib:
        only.append(rel); continue
    pa, pb = os.path.join(a_root, ia[rel]), os.path.join(b_root, ib[rel])
    a, b = fns(pa), fns(pb)
    for n in sorted(set(a) | set(b)):
        total += 1
        if a.get(n) != b.get(n):
            differ += 1
            print('DIFF\t%s\t%s\t%s\t%s\t%s\t%s' % (ia[rel], ib[rel], n, shape(a.get(n, [])), shape(b.get(n, [])), 'only-unfixed' if n not in b else 'only-fixed' if n not in a else ''))
for rel in only: print('ONLY-IN-ONE\t' + rel)
print('TOTAL\tfiles=%d\tfunctions=%d\tdiffer=%d\tfiles-in-one-arm=%d' % (len(files), total, differ, len(only)))
objdiff.py
# objdiff.py <llvm-objdump> <unfixed-obj-dir> <fixed-obj-dir>: per-function comparison of two
# builds' object files, before linking: each function's disassembly with its relocations
# (llvm-objdump -d -r --no-show-raw-insn --no-leading-addr), keyed by object file and symbol.
# Prints one line per function whose instructions differ, with each arm's instruction count.
import sys, re, os, subprocess
objdump, a_root, b_root = sys.argv[1:4]
HEAD = re.compile(r'^(?:[0-9a-f]+ )?<(.+)>:$')
def fns(path):
    out = subprocess.run([objdump, '-d', '-r', '--no-show-raw-insn', '--no-leading-addr', path], capture_output=True, text=True)
    if out.returncode != 0: raise SystemExit('objdump failed on %s: %s' % (path, out.stderr[:300]))
    res = {}; name = None
    for line in out.stdout.splitlines():
        m = HEAD.match(line)
        if m: name = m.group(1); res.setdefault(name, []); continue
        if line.startswith('Disassembly of section'): name = None; continue
        if name is not None and line.strip(): res[name].append(line.strip())
    return res
# An object is <crate>-<cargo's hash>.<codegen unit>.<session>.rcgu.o; the last part
# is not the same from build to build, so objects pair by the rest, and two under one such
# name are refused.
def index(root):
    out = {}
    for d, _, fs in os.walk(root):
        for f in fs:
            if f.endswith('.rcgu.o'):
                rel = os.path.relpath(os.path.join(d, f), root)
                k = re.sub(r'\.[^./]*\.rcgu\.o$', '.rcgu.o', rel)
                if k in out: raise SystemExit('two objects under %s in %s' % (k, root))
                out[k] = rel
    return out
ia, ib = index(a_root), index(b_root)
files = set(ia) | set(ib)
total = differ = 0; only = []
for rel in sorted(files):
    if rel not in ia or rel not in ib: only.append(rel); continue
    pa, pb = os.path.join(a_root, ia[rel]), os.path.join(b_root, ib[rel])
    if open(pa, 'rb').read() == open(pb, 'rb').read():
        total += len(fns(pa)); continue
    a, b = fns(pa), fns(pb)
    for n in sorted(set(a) | set(b)):
        total += 1
        if a.get(n) != b.get(n):
            differ += 1
            insn = lambda l: sum(1 for x in l if not re.match(r'^[0-9a-f]+:\s+R_|^[0-9a-f]+:\s+IMAGE_REL', x))
            print('OBJDIFF\t%s\t%s\t%d\t%d' % (rel, n, insn(a.get(n, [])), insn(b.get(n, []))))
for rel in only: print('ONLY-IN-ONE\t' + rel)
print('TOTAL\tobjects=%d\tfunctions=%d\tdiffer=%d\tobjects-in-one-arm=%d' % (len(files), total, differ, len(only)))
binsize.py
# binsize.py <llvm-size> <llvm-nm> <unfixed-bin-dir> <fixed-bin-dir>: per linked binary, the
# text size of each arm (llvm-size's first column), and the defined symbols whose size differs.
import sys, os, subprocess
size, nm, a_root, b_root = sys.argv[1:5]
def text(path):
    out = subprocess.run([size, path], capture_output=True, text=True, check=True).stdout.splitlines()
    return int(out[1].split()[0])
def syms(path):
    out = subprocess.run([nm, '--print-size', '--defined-only', path], capture_output=True, text=True)
    res = {}
    for line in out.stdout.splitlines():
        p = line.split()
        if len(p) == 4 and p[2] in 'tTwW':
            res.setdefault(p[3], []).append(int(p[1], 16))
    return {k: sorted(v) for k, v in res.items()}
ta = tb = 0; nsym = ndiff = 0
for d, _, fs in os.walk(a_root):
    for f in sorted(fs):
        pa = os.path.join(d, f); rel = os.path.relpath(pa, a_root); pb = os.path.join(b_root, rel)
        if not os.path.exists(pb): print('ONLY-UNFIXED', rel); continue
        a, b = text(pa), text(pb); ta += a; tb += b
        sa, sb = syms(pa), syms(pb)
        changed = sorted(n for n in set(sa) | set(sb) if sa.get(n) != sb.get(n))
        nsym += len(set(sa) | set(sb)); ndiff += len(changed)
        same = open(pa, 'rb').read() == open(pb, 'rb').read()
        print('%s\ttext unfixed=%d fixed=%d delta=%+d\tsymbols=%d differing-size=%d\t%s' % (rel, a, b, b - a, len(set(sa) | set(sb)), len(changed), 'identical bytes' if same else 'bytes differ'))
        for n in changed: print('\tSYM\t%s\t%s\t%s\t%s' % (rel, n, sa.get(n), sb.get(n)))
print('TOTAL\ttext unfixed=%d fixed=%d delta=%+d (%.4f%%)\tsymbols=%d differing-size=%d' % (ta, tb, tb - ta, 100.0 * (tb - ta) / ta, nsym, ndiff))
exits.py
# exits.py <opt> <unfixed-ll-dir> <fixed-ll-dir> <irdiff-output>: for every function irdiff
# found different, the loops each arm has and how many blocks leave each, from
# opt -passes='print<loops>'. Prints a function where the unfixed arm has a loop with no
# exiting block, or fewer exiting blocks in total than the fixed arm.
import sys, re, subprocess, collections
opt, a_root, b_root, diff = sys.argv[1:5]
want = collections.defaultdict(set)
for line in open(diff):
    p = line.rstrip('\n').split('\t')
    if p[0] == 'DIFF': want[(p[1], p[2])].add(p[3])
def loops(path):
    out = subprocess.run([opt, '-passes=print<loops>', '-disable-output', path], capture_output=True, text=True)
    if out.returncode != 0: raise SystemExit('opt failed on %s: %s' % (path, out.stderr[:400]))
    res = {}; cur = None
    for line in (out.stdout + out.stderr).splitlines():
        m = re.match(r"Loop info for function '(.*)':", line)
        if m: cur = m.group(1); res[cur] = []; continue
        if cur is not None and line.lstrip().startswith('Loop at depth'):
            res[cur].append(line.count('<exiting>'))
    return res
for ra, rb in sorted(want):
    a, b = loops(a_root + '/' + ra), loops(b_root + '/' + rb)
    rel = ra
    for fn in sorted(want[(ra, rb)]):
        la, lb = a.get(fn, []), b.get(fn, [])
        endless = sum(1 for n in la if n == 0) > sum(1 for n in lb if n == 0)
        fewer = sum(la) < sum(lb)
        tag = 'ENDLESS' if endless else 'FEWER-EXITS' if fewer else 'same-or-more'
        print('%s\t%s\t%s\tunfixed loops=%d exiting=%d\tfixed loops=%d exiting=%d' % (tag, rel, fn, len(la), sum(la), len(lb), sum(lb)))

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 1 — wt/toyos-scevfix at 1b5192454, ToyOSOrg/rust 9e7b17b52f95, ToyOSOrg/llvm-project 81b496be0342

Read, nothing run: git log/git diff origin/main...1b5192454, every changed file whole, the LLVM commit as new code against ScalarEvolution.cpp at that commit, upstream's llvm/llvm-project#118959 and #175729 as they stand today, the step logs and the census results.

Net lines, ToyOS: 8 files, +197 −519. Production +145 −2 (src/miscompile.rs 90, its reproducer 48, src/sysroot.rs +6 −2, src/lib.rs 1), tests +33, issues/ +18 −516, the gitlink. LLVM fork: 48 files, +1093 −482, of them source +108 −45. The production growth is accepted in kind (what a compiler emits is not readable from the tree) and not as it stands: BLOCKER 2.

What the brief asked, judged

  • Soundness of the condition: I find it sound, and I find no third site. The source hunks are #118959's, line for line, plus the option. The change is monotone: PreIncFlags is IRFlags or nothing, so every flag the fixed compiler sets the old one set, and the only question is whether what is kept is true. First arm: programUndefinedIfPoison(phi) makes the phi non-poison on every iteration whose header runs, the phi of iteration k is the increment of k−1, and an increment that wrapped is poison; so no step wrapped on a taken backedge, which is exactly the recurrence's domain (values 0..BTC). Second arm: the use that must trigger UB is not a phi, so the increment dominates it, it dominates the latch, and all three lie in the loop; within one iteration the increment therefore precedes it, and a taken backedge means its block ran to its end. No abnormal-exit condition is needed there, because only taken backedges are claimed. The other writers of an addrec's flags (getPreStartForExtend at :1380, the extension proofs at :1668 to :2108, proveNoWrapViaConstantRanges and the two induction proofs at :5799 and :5935, the exit-count sites at :9351, :9369 and :13148) prove from ranges, guards or the exit; none reads the IR increment. The one route from a post-increment node to a pre-increment node, :1380, starts from a post-increment flag that isPostIncAddRecNeverPoison gives only where the increment is non-poison on every iteration the header runs, the last included, so a sibling phi that shares that node inherits a true fact. SCEVShiftRewriter builds the shifted recurrence with no flags.
  • The Phabricator objection: the body's reading holds. The fact proved is about the loop's executions, not about a use; a use the expander adds later, or one a later pass deletes, does not unprove it. The maintainer's "problematic impact" on #175729 is lost optimisation; his next sentence proposes per-use flags as the mitigation.
  • A over B: accepted on the argument above, with the unmerged status recorded as it is. B's abort is PrintLoopInfo's consistency assertion, in a printer no compile runs; it says a later query can find a better count once flags are inferred lazily, not that B is unsound and not that A is incomplete. Upstream has not accepted A in 22 months and its author left it a draft: "as upstream would accept it" is unproven by upstream's own record, and stands here on the soundness reading alone.
  • LLVM tests, spot-checked (16): Analysis/ScalarEvolution/iv-poison.ll, pr27315.ll, nowrap-preinc-limits.ll, smin-smax-folds.ll, trip-count-andor-selectform.ll; IndVarSimplify/iv-poison.ll, lftr.ll, 2011-09-10-widen-nsw.ll, pr25578.ll; LoopStrengthReduce/X86/sibling-loops.ll, 2008-08-14-ShadowIV.ll, AMDGPU/lsr-invalid-ptr-extend.ll; LoopVectorize/ARM/mve-reg-pressure-vmla.ll, iv-select-cmp.ll; LoopUnroll/wrong_assert_in_peeling.ll, LoopIdiom/pr80954.ll. Each loses a flag, a range, a widening or a transform; none gains a fold. IndVarSimplify/iv-poison.ll's old lines (icmp ult i4 1, %arg) were the miscompile. The two new tests hold m1, m2, m7, four widths and r1, wrong with the switch and right without.
  • Fork chain: both commits are one-commit fast-forwards of their branches and are on them; the rust commit changes the src/llvm-project gitlink and nothing else; .gitmodules names the same branch; NOTICE and no manifest name an LLVM revision. Clean.
  • Cache hit: a restored or already-made sysroot does not rerun the check, and that is right: RECIPE moved, so every key made from here on was checked when it was made.
  • Census, read by me (10 of 51): the seven kernel functions (rustc_demangle v0::Printer, x86-64), Fat32::ensure_capacity, uefi Components::next, base64ct encode. Kernel: in each the old behaviour peels iteration 0, which removes the in-loop i == 0 test and moves that iteration's exits out of the loop; the i == −1 panic and every other exit are in both arms. ensure_capacity: the u32 index is widened and its overflow test becomes == 4294967295; same exits. Components::next: the old behaviour drops count's overflow panic; the count runs beside a slice index that starts no lower and is bounded by the slice's length, so the panic is dead and the drop was right. encode: the fix adds vector.scevcheck. I see no lost exit a program can take in the ten. The text deltas add up from the per-binary rows (−112 −224 −120 −16 −56 = −528; −32 −8 −216 +8 −216 = −464). The method sees inlined callees (the caller's body differs), codegen-only changes (the object comparison found rubato's two), and there is no LTO to hide behind; what it does not see is BLOCKER 3.
  • CI at the head that lands. toolchain / build must have built, not restored, all four keys on its Linux runner (nothing here has been measured for a compiler built on Linux): llvm, compiler, freestanding and sysroot each built in the bootstrap step's outputs, the sysroot's build log passing through the miscompile check, conclusion success. host: success, not skipped (it skips a draft). guest / suite: success under KVM with the sysroot key toolchain output. The orchestrator may land on reading those three at the merge queue's head once every BLOCKER below is closed, the T14 reading among them; a skipped check is not a reading.

BLOCKER

  1. llvm/lib/Analysis/ScalarEvolution.cpp:172 and :7505 (fork) — -scev-unconditional-preinc-nowrap-flags ships in the compiler for tests and measurement alone, and is a switch upstream would not take — the commit message says it "exists to measure what the transfer bought and to show the new tests failing"; reviewer.md: nothing ships for tests alone; "A fork": written as upstream would accept it. Root CLAUDE.md's negative control is the whole change reverted, and the store holds that compiler (LLVM ceaf0fbb8440). Append a commit that removes the option and the UNCONDITIONAL run lines of the two new tests, and take the check's red arm from the base compiler. The deleted issue's exit item 4 asked for the option for the census; it did not say the option lands. If the owner rules that the lever stays, that ruling is what closes this, recorded in the body as his.
  2. src/miscompile.rs:18, :38 — the check holds the fix only through a shape core's RangeInclusive::next and rustc happen to produce today, so it cannot fail for a faulty LLVM once that shape moves — the deleted issue says so itself ("a compiler that merely stops making them meets those two and not the exit; item 1 has no front end in it"), and this head shows it: step log 66-final-firmware-switch has the port test green with the fault switched on after two unrelated commits to the crate, and log 64-final-cases has u16 on all three x86-64 targets green with the fault on. Upstream Rust reworking that next because of this very miscompile is the likeliest way the shape goes, and it would go at exactly the fork move the check exists for. Add a case with no front end in it (m1/m2 as IR through the sysroot's own clang, or an equivalent the implementer measures), shown red under the faulty compiler and green under the fixed one, exit codes and logs in the body.
  3. Body, "The census" — "every function of both architectures' images" is not what was compared: userland/doom/build.rs compiles doomgeneric at -O2 with the toolchain's clang (src/build.rs:817), and the sysroot's C++ runtime is clang's too; the switch reached rustc only (rustc-wrapper.sh), so both arms compiled that C identically and x86-64 doom's "identical bytes" is true by construction. The lead sentence's "none in ... what ToyOS ships" is unmeasured for every clang-compiled function. Cheap to measure: doom's objects and the C++ runtime built by the fixed clang and by the faulty one, compared the same way.
  4. Body, "Not measured" — check-clang and check-lld were not run on a commit that changes what the optimiser emits, and the toolchain builds and ships both (src/llvm.rs:50, clang::provision), clang compiling shipped code; the fork branch carries clang test changes of its own, so its clang suite is a suite the fork keeps. Run both at the LLVM commit that lands, command, exit and log. Polly only if the toolchain's build enables it.
  5. Body, "LLVM's own tests" — "llc relinked, it passes" has no log: 67-llc-relink is six build steps and no test run, and the only whole-suite run at 81b496be0342 (61-lit-whole-final) is exit 1 with CodeGen/PowerPC/git_revision.ll failed. A claimed measurement that is absent. Run that test, or the suite, after the relink and record the exit.
  6. T14, the orchestrator's — a reading is owed before landing: the tree records this machine's timings (tests/metal/lenovo-20w0003amz.toml), timing verdicts come only from metal, and this changes the compiler of the kernel and every driver that runs there. One whole --metal pass on images built at the head that lands, by the compiler key that lands. Read: every row's verdict; every [measured] name within its bound (boot.*.complete_ms, panel_us, panel_max_us, latency.p99_us, tlb.latencycase.p50_ns/p99_ns); and, since the x86-64 kernel differs from the faulty arm in rustc_demangle's seven functions and nowhere else, a symbolised kernel backtrace printed whole in a row that panics by design, and a row that grows a file on FAT through fileserver (ensure_capacity). No audio verdict is moved: x86-64 soundserver is byte-identical between the arms.

NOTE

  • issues/rustc-1-99-0-makes-an-endless-loop-of-a-port-test-on-apple-silicon.md:109-111 (as rewritten here) — "the fork's compiler with that report's proposed fix compiles this test right, 24 passed" no longer tells the fix from its absence: at this head the test passes with the fault switched on too. And whether stable 1.99.0 still hangs the test on Apple silicon at this head is unmeasured, so the premise of nightly.yml:120's pin may already be stale. Say what the control was and at which head (805ba7978), and the orchestrator reruns the table's second row under 1.99.0 at this head and corrects the pin's record by what it shows.
  • src/miscompile.rs:38-39 — only the two userland targets are compiled; the kernel's and the loader's four were measured once by hand (log 64-final-cases, twelve of twelve) and are held by nothing. The front-end-free case of BLOCKER 2 is target-independent and would cover them; otherwise compile GUEST_TARGETS, whose libraries are already in partial at that point.
  • src/miscompile.rs:47 — .env_remove("RUSTFLAGS") does nothing: rustc reads no RUSTFLAGS, cargo does. Delete it.
  • src/miscompile.rs:25 — u16 for x86_64-unknown-toyos has no red arm (green with the fault on); one of the four compiles tests nothing. Drop it when the case of BLOCKER 2 arrives.
  • Census review — the 51 diffs that were read are the pre-merge run's (805ba7978, compiler b313341e24f1d050); the head's run is shown to find the same function names, not the same bodies. Show the head run's 51 bodies equal to the ones read, or reread the ones that differ.
  • llvm/test/Transforms/LoopVectorize/iv-select-cmp.ll (fork) — @select_icmp_min_valid_iv_start is regenerated to "not vectorised", so the test no longer holds what its name says; the ShadowIV and widening tests were kept on their subject by making the increment observed. Do the same here.
  • Soundness rests on an argument, upstream's, the implementer's and now mine: a second reader is not independence. A third-party checker exists; alive-tv over indvars on the two new tests, both ways, would be one.
  • Body, "The gaps in the A-against-B count" — "the whole suite passes" contradicts "2 failed, exit 1" two sections up. (Prose.)

SEND BACK

Japabu and others added 7 commits October 9, 2026 10:11
…over every guest target

The check held the fix only through the shape `core`'s `RangeInclusive::next`
and rustc give the reproducer today. `src/miscompile/last_exit.ll` is the
loop itself, compiled by the toolchain's clang, which links the LLVM its
rustc does: no front end stands between it and `indvars`. It walks a thousand
passes because a loop short enough to be evaluated is folded right before
`indvars` meets it, by a compiler with the fault too.

The Rust reproducer stays where a compiler with the fault gets it wrong:
over `u128`, for all six guest targets. Over `u16` the x86-64 targets were
compiled right with the fault, so that type goes. `rustc` reads no
`RUSTFLAGS`; the `env_remove` goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
… switch

`rust` moves to ToyOSOrg/rust b9cc8392f0eb, whose `src/llvm-project` is
ToyOSOrg/llvm-project b7420fe534bf: the fix as it was, less
`-scev-unconditional-preinc-nowrap-flags`. Nothing ships in the compiler for
a test or a measurement; the control of every measurement is the compiler
before the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…ounters issue names the compilers compared

Under stable 1.99.0 the port test has passed on Apple silicon since #780
changed its crate: measured at this branch, with the row hanging again at
main before #780 as its control. 1.99.0 still compiles the reproducer to an
endless loop. The pin is the orchestrator's to decide.

The counters issue cited the switch the fork no longer has: its comparison
is now the compiler before the fix against the fixed one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
@Japabu Japabu changed the title The fork's LLVM gives a loop counter's recurrence only the wrap flags proven for it, a sysroot whose compiler loses a loop's last exit is refused, and the ScalarEvolution issue is closed The fork's LLVM gives a loop counter's recurrence only the wrap flags proven for it, a sysroot whose compilers lose a loop's last exit is refused, and the ScalarEvolution issue is closed Oct 9, 2026
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2: the round-1 review answered, finding by finding

Head 5b4592e88; ToyOSOrg/rust b9cc8392f0eb; ToyOSOrg/llvm-project b7420fe534bf. The fix's condition is unchanged. Every measurement is in the body with its exit; the scripts and patches are in the next comment.

BLOCKERs

  1. The switch ships for tests alone: fixed. b7420fe534bf, appended to the LLVM branch, removes -scev-unconditional-preinc-nowrap-flags and the UNCONDITIONAL run and check lines of the two new tests; the rust gitlink and this repository's pin follow, each a fast-forward. The control of every measurement is now the compiler before the fix, main's own (3d1cb62e54e18406, LLVM ceaf0fbb8440), or the development opt with ScalarEvolution.cpp and .h as that commit has them: under it the two new tests fail (lit exit 1), restored they pass (0).
  2. The check held the fix only through a shape rustc happens to produce: fixed. src/miscompile/last_exit.ll is the loop as IR through the sysroot's own clang at -O2, measured before it was written: m1 as it stands is folded right by the base clang too, its four passes evaluated before indvars meets them, and m2's wrong answer is an unrolled body no simple oracle reads; so the loop is m1 walking a thousand passes, where the base clang leaves br label %header to itself and the fixed one ret i1 true. The check through a driver test: 101 against main's sysroot, 0 against the one that lands. u16 is dropped (the x86-64 targets had no red arm); the Rust case stays over u128, where all six guest targets are wrong under the base compiler, and is compiled for GUEST_TARGETS; with the IR case patched out it alone answers 101 against the base sysroot. The env_remove is deleted.
  3. "Every function of both architectures' images" was not what was compared: fixed, and the comparison found more to correct than the clang-compiled code.
    • doomgeneric: the build's own objects, the base arm's by the base clang, 0 of 848 functions differ; as IR, 2 of 847 (a GEP's flags), identical object code.
    • The C++ runtime: 5 and 6 of about 2,500 functions differ, a counter kept in 64 bits or 32; read.
    • Round 1 compiled the sysroot's own libraries with one compiler in both arms, so its census could not see them. Compared now, archive by archive: std's DirBuilder::_create differs on four targets; the C library in nothing.
    • Round 1's --emit=llvm-ir made every crate one codegen unit, 366 objects where an image build makes 3,314, so it compared binaries no image holds. The shipped build is compared too, as object code: 21, 21 and 6 functions differ, nearly the same ones, none of them the same code as its one-unit counterpart, each read by its shape. In one, kurbo's fit_to_cubic, the old compiler kept a branch to a panic that cannot be taken; in none has it an exit fewer.
    • The lead sentence says exactly what was compared.
  4. check-clang and check-lld not run: fixed. At b7420fe534bf: clang 49,762 discovered, exit 0; lld 3,180, exit 0; LLVM 71,556, exit 1 on one test, HostTest.getMacOSHostVersion, which compares two readings of the host's macOS version (27 and 28) and reads no optimiser. Polly is not in the toolchain's build and was not run.
  5. "llc relinked, it passes" had no log: fixed. CodeGen/PowerPC/git_revision.ll passes in the whole-suite run above, of binaries linked at b7420fe534bf. The body's "the whole suite passes" is gone.
  6. A T14 reading: the orchestrator's. Staged: every image of the whole metal list built at this head by the compiler that lands; the request is named at the end of my report.

NOTEs

What this round did that the review did not ask

  • The base arm's sysroot is built at this worktree's path, by one changed line of RECIPE in the base tree: a sysroot built elsewhere gives std a different crate hash, and 2,821 functions "differ" by name alone.
  • The sysroot's check says one line when it runs, so a build log shows it ran.

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2: mutation patches and measurement scripts

$TREE is this branch's worktree, $SCRATCH a scratch directory outside it, $STORE the host's toolchain store. Nothing here is in the tree. The base arm of every measurement is the compiler before the fix (LLVM ceaf0fbb8440); the switch round 1's scripts passed is gone.

The sysroot check's controls

m0-driver-only.patch: a driver test that runs the check against a stored sysroot
diff --git a/src/miscompile.rs b/src/miscompile.rs
index ef8151066..f41eb2d1a 100644
--- a/src/miscompile.rs
+++ b/src/miscompile.rs
@@ -121,4 +121,12 @@ start:
         assert!(!returns_true(&right.replace("ret i1 true", "ret i1 false"), "caller"));
         assert!(!returns_true(&right.replace("@caller(", "@other("), "caller"), "a module with no `caller` answers nothing");
     }
+
+    /// Mutation driver, not part of the tree: the check itself against the sysroot `MISCOMPILE_SYSROOT` names.
+    #[test]
+    fn the_check_against_a_real_sysroot() {
+        let sysroot = std::env::var("MISCOMPILE_SYSROOT").expect("MISCOMPILE_SYSROOT");
+        let scratch = toyos_tmpdir::TempDir::new("miscompile-control");
+        refuse(Path::new(&sysroot), &scratch.join("scratch"));
+    }
 }
m1-driver-and-no-ir-case.patch: the same with the IR case taken out, so the Rust case answers
diff --git a/src/miscompile.rs b/src/miscompile.rs
index 0d3b44d2c..8b57ff434 100644
--- a/src/miscompile.rs
+++ b/src/miscompile.rs
@@ -33,7 +33,7 @@ pub(crate) fn refuse(toolchain: &Path, scratch: &Path) {
     fs::create_dir_all(scratch).unwrap_or_else(|e| panic!("create {}: {e}", scratch.display()));
     let loop_ir = scratch.join("last_exit.ll");
     fs::write(&loop_ir, LAST_EXIT_IR).unwrap_or_else(|e| panic!("write {}: {e}", loop_ir.display()));
-    for arch in Arch::ALL {
+    for arch in [] as [Arch; 0] {
         let c = CSysroot::of(toolchain, arch);
         let ir = scratch.join(format!("last_exit-clang-{}.ll", c.target));
         let mut clang = Command::new(&c.clang);
@@ -122,4 +122,12 @@ start:
         assert!(!returns_true(&right.replace("ret i1 true", "ret i1 false"), "caller"));
         assert!(!returns_true(&right.replace("@caller(", "@other("), "caller"), "a module with no `caller` answers nothing");
     }
+
+    /// Mutation driver, not part of the tree: the check itself against the sysroot `MISCOMPILE_SYSROOT` names.
+    #[test]
+    fn the_check_against_a_real_sysroot() {
+        let sysroot = std::env::var("MISCOMPILE_SYSROOT").expect("MISCOMPILE_SYSROOT");
+        let scratch = toyos_tmpdir::TempDir::new("miscompile-control");
+        refuse(Path::new(&sysroot), &scratch.join("scratch"));
+    }
 }
control.sh
#!/bin/sh
# control.sh <log> <sysroot-key>...: the sysroot check (src/miscompile.rs's `refuse`) run against each
# stored sysroot by a driver test a checked patch adds and the same script takes back out; then each
# case by the check's own command line.
log=$1; shift
W=$TREE; S=$STORE/sysroots; X=$(dirname "$0")
cd $W
{ git rev-parse HEAD; git status --porcelain; uptime; } > $log
git apply --check $X/m0-driver-only.patch && git apply $X/m0-driver-only.patch || { echo "patch does not apply" >> $log; exit 2; }
for k in "$@"; do
  echo "== refuse() against sysroot $k" >> $log
  MISCOMPILE_SYSROOT=$S/$k cargo test --lib miscompile::tests::the_check_against_a_real_sysroot >> $log 2>&1
  echo "EXIT=$? (sysroot $k)" >> $log
done
git apply -R $X/m0-driver-only.patch; echo "patch reversed; git status --porcelain:" >> $log; git status --porcelain >> $log
host=$(rustc -vV | sed -n 's/^host: //p')
O=$(mktemp -d)
for k in "$@"; do
  echo "== per case, sysroot $k" >> $log
  for t in x86_64-unknown-toyos aarch64-unknown-toyos; do
    $S/$k/lib/rustlib/$host/bin/clang --target=$t -O2 -S -emit-llvm -o $O/c.ll $W/src/miscompile/last_exit.ll 2>/dev/null
    echo "clang last_exit.ll $t: exit $?: $(sed -n '/^define .*@caller(/,/^}/p' $O/c.ll | sed '1d;$d' | grep -v '^$' | grep -v ':' | sed 's/^ *//' | tr '\n' ';')" >> $log
  done
  for t in x86_64-unknown-toyos x86_64-unknown-none x86_64-unknown-uefi aarch64-unknown-toyos aarch64-unknown-none-softfloat aarch64-unknown-uefi; do
    $S/$k/bin/rustc --edition 2021 --crate-type lib --target $t -C opt-level=2 -C codegen-units=1 --emit llvm-ir -o $O/r.ll $W/src/miscompile/last_exit.rs
    echo "rustc last_exit.rs $t: exit $?: $(sed -n '/^define .*@caller(/,/^}/p' $O/r.ll | sed '1d;$d' | grep -v '^$' | grep -v ':' | sed 's/^ *//' | tr '\n' ';')" >> $log
  done
done
rm -rf $O
control-m1.sh
#!/bin/sh
# control-m1.sh <log> <sysroot-key>...: the check with its IR case taken out (and the driver test
# added), so that its Rust case is what answers; applied as a checked patch and reversed.
log=$1; shift
W=$TREE; S=$STORE/sysroots; X=$(dirname "$0")
cd $W
{ git rev-parse HEAD; git status --porcelain; uptime; } > $log
git apply --check $X/m1-driver-and-no-ir-case.patch && git apply $X/m1-driver-and-no-ir-case.patch || { echo "patch does not apply" >> $log; exit 2; }
for k in "$@"; do
  echo "== refuse(), Rust case alone, against sysroot $k" >> $log
  MISCOMPILE_SYSROOT=$S/$k cargo test --lib miscompile::tests::the_check_against_a_real_sysroot >> $log 2>&1
  echo "EXIT=$? (sysroot $k)" >> $log
done
git apply -R $X/m1-driver-and-no-ir-case.patch; echo "patch reversed; git status --porcelain:" >> $log; git status --porcelain >> $log

LLVM's new tests against the source before the fix

llvm-red-arm.sh
#!/bin/sh
# llvm-red-arm.sh: the fix's whole source change reverted in the development tree (ScalarEvolution.cpp
# and .h as LLVM ceaf0fbb8440 has them) as a checked patch, opt rebuilt, the two new tests and the
# vectoriser test run, the patch reversed, opt rebuilt and the three run again.
X=$(cd $(dirname "$0")/..; pwd); L=$X/logs/r2-15-llvm-red-arm.log
W=$TREE/rust/src/llvm-project
T="llvm/test/Transforms/IndVarSimplify/preinc-nowrap-flags-unobserved-poison.ll llvm/test/Analysis/ScalarEvolution/preinc-nowrap-flags-unobserved-poison.ll llvm/test/Transforms/LoopVectorize/iv-select-cmp.ll"
cd $W
{ echo "llvm-project $(git rev-parse HEAD)"; git status --porcelain; uptime; } > $L
git diff HEAD ceaf0fbb8440 -- llvm/lib/Analysis/ScalarEvolution.cpp llvm/include/llvm/Analysis/ScalarEvolution.h > $X/r2/llvm-base-source.patch
git apply --check $X/r2/llvm-base-source.patch && git apply $X/r2/llvm-base-source.patch || { echo "patch does not apply" >> $L; exit 2; }
git diff --stat >> $L
( cd $X/llvm-dev && ninja -j10 opt ) >> $L 2>&1; echo "BUILD (source of ceaf0fbb8440) EXIT=$?" >> $L
$X/llvm-dev/bin/llvm-lit $T >> $L 2>&1; echo "LIT, source of ceaf0fbb8440: EXIT=$?" >> $L
git apply -R $X/r2/llvm-base-source.patch; echo "patch reversed; git status --porcelain:" >> $L; git status --porcelain >> $L
( cd $X/llvm-dev && ninja -j10 opt ) >> $L 2>&1; echo "BUILD (restored) EXIT=$?" >> $L
$X/llvm-dev/bin/llvm-lit $T >> $L 2>&1; echo "LIT, restored: EXIT=$?" >> $L
uptime >> $L
llvm-base-source.patch: ScalarEvolution.cpp and .h as ceaf0fbb8440 has them
diff --git a/llvm/include/llvm/Analysis/ScalarEvolution.h b/llvm/include/llvm/Analysis/ScalarEvolution.h
index c6286ead3..dcd11daf9 100644
--- a/llvm/include/llvm/Analysis/ScalarEvolution.h
+++ b/llvm/include/llvm/Analysis/ScalarEvolution.h
@@ -2272,16 +2272,10 @@ private:
   bool isSCEVExprNeverPoison(const Instruction *I);
 
   /// This is like \c isSCEVExprNeverPoison but it specifically works for
-  /// instructions that will get mapped to SCEV post-inc add recurrences.
-  /// Return true if \p I will never generate poison under the assumption that
-  /// \p I is an add recurrence on the loop \p L.
-  bool isPostIncAddRecNeverPoison(const Instruction *I, const Loop *L);
-
-  /// Check whether nowrap flags from the IR increment operation can be
-  /// transferred to the pre-inc addrec.
-  bool canPreservePreIncAddRecNoWrapFlags(const Instruction *PreIncI,
-                                          const Instruction *PostIncI,
-                                          const Loop *L);
+  /// instructions that will get mapped to SCEV add recurrences.  Return true
+  /// if \p I will never generate poison under the assumption that \p I is an
+  /// add recurrence on the loop \p L.
+  bool isAddRecNeverPoison(const Instruction *I, const Loop *L);
 
   /// Similar to createAddRecFromPHI, but with the additional flexibility of
   /// suggesting runtime overflow checks in case casts are encountered.
diff --git a/llvm/lib/Analysis/ScalarEvolution.cpp b/llvm/lib/Analysis/ScalarEvolution.cpp
index 4a6272a76..e19de5f46 100644
--- a/llvm/lib/Analysis/ScalarEvolution.cpp
+++ b/llvm/lib/Analysis/ScalarEvolution.cpp
@@ -5773,19 +5773,14 @@ const SCEV *ScalarEvolution::createSimpleAffineAddRec(PHINode *PN,
   if (!Accum)
     return nullptr;
 
-  SCEV::NoWrapFlags IRFlags = SCEV::FlagAnyWrap;
+  SCEV::NoWrapFlags Flags = SCEV::FlagAnyWrap;
   if (BO->IsNUW)
-    IRFlags = setFlags(IRFlags, SCEV::FlagNUW);
+    Flags = setFlags(Flags, SCEV::FlagNUW);
   if (BO->IsNSW)
-    IRFlags = setFlags(IRFlags, SCEV::FlagNSW);
+    Flags = setFlags(Flags, SCEV::FlagNSW);
 
-  auto *BEInst = dyn_cast<Instruction>(BEValueV);
-  SCEV::NoWrapFlags PreIncFlags =
-      BEInst && canPreservePreIncAddRecNoWrapFlags(PN, BEInst, L)
-          ? IRFlags
-          : SCEV::FlagAnyWrap;
   const SCEV *StartVal = getSCEV(StartValueV);
-  const SCEV *PHISCEV = getAddRecExpr(StartVal, Accum, L, PreIncFlags);
+  const SCEV *PHISCEV = getAddRecExpr(StartVal, Accum, L, Flags);
   insertValueToMap(PN, PHISCEV);
 
   if (auto *AR = dyn_cast<SCEVAddRecExpr>(PHISCEV)) {
@@ -5797,11 +5792,11 @@ const SCEV *ScalarEvolution::createSimpleAffineAddRec(PHINode *PN,
   // We can add Flags to the post-inc expression only if we
   // know that it is *undefined behavior* for BEValueV to
   // overflow.
-  if (BEInst) {
+  if (auto *BEInst = dyn_cast<Instruction>(BEValueV)) {
     assert(isLoopInvariant(Accum, L) &&
            "Accum is defined outside L, but is not invariant?");
-    if (isPostIncAddRecNeverPoison(BEInst, L))
-      (void)getAddRecExpr(getAddExpr(StartVal, Accum), Accum, L, IRFlags);
+    if (isAddRecNeverPoison(BEInst, L))
+      (void)getAddRecExpr(getAddExpr(StartVal, Accum), Accum, L, Flags);
   }
 
   return PHISCEV;
@@ -5881,14 +5876,14 @@ const SCEV *ScalarEvolution::createAddRecFromPHI(PHINode *PN) {
       if (isLoopInvariant(Accum, L) ||
           (isa<SCEVAddRecExpr>(Accum) &&
            cast<SCEVAddRecExpr>(Accum)->getLoop() == L)) {
-        SCEV::NoWrapFlags IRFlags = SCEV::FlagAnyWrap;
+        SCEV::NoWrapFlags Flags = SCEV::FlagAnyWrap;
 
         if (auto BO = MatchBinaryOp(BEValueV, getDataLayout(), AC, DT, PN)) {
           if (BO->Opcode == Instruction::Add && BO->LHS == PN) {
             if (BO->IsNUW)
-              IRFlags = setFlags(IRFlags, SCEV::FlagNUW);
+              Flags = setFlags(Flags, SCEV::FlagNUW);
             if (BO->IsNSW)
-              IRFlags = setFlags(IRFlags, SCEV::FlagNSW);
+              Flags = setFlags(Flags, SCEV::FlagNSW);
           }
         } else if (GEPOperator *GEP = dyn_cast<GEPOperator>(BEValueV)) {
           if (GEP->getOperand(0) == PN) {
@@ -5896,13 +5891,13 @@ const SCEV *ScalarEvolution::createAddRecFromPHI(PHINode *PN) {
             // If the increment has any nowrap flags, then we know the address
             // space cannot be wrapped around.
             if (NW != GEPNoWrapFlags::none())
-              IRFlags = setFlags(IRFlags, SCEV::FlagNW);
+              Flags = setFlags(Flags, SCEV::FlagNW);
             // If the GEP is nuw or nusw with non-negative offset, we know that
             // no unsigned wrap occurs. We cannot set the nsw flag as only the
             // offset is treated as signed, while the base is unsigned.
             if (NW.hasNoUnsignedWrap() ||
                 (NW.hasNoUnsignedSignedWrap() && isKnownNonNegative(Accum)))
-              IRFlags = setFlags(IRFlags, SCEV::FlagNUW);
+              Flags = setFlags(Flags, SCEV::FlagNUW);
           }
 
           // We cannot transfer nuw and nsw flags from subtraction
@@ -5911,12 +5906,7 @@ const SCEV *ScalarEvolution::createAddRecFromPHI(PHINode *PN) {
         }
 
         const SCEV *StartVal = getSCEV(StartValueV);
-        auto *BEInst = dyn_cast<Instruction>(BEValueV);
-        SCEV::NoWrapFlags PreIncFlags =
-            BEInst && canPreservePreIncAddRecNoWrapFlags(PN, BEInst, L)
-                ? IRFlags
-                : SCEV::FlagAnyWrap;
-        const SCEV *PHISCEV = getAddRecExpr(StartVal, Accum, L, PreIncFlags);
+        const SCEV *PHISCEV = getAddRecExpr(StartVal, Accum, L, Flags);
 
         // Okay, for the entire analysis of this edge we assumed the PHI
         // to be symbolic.  We now need to go back and purge all of the
@@ -5933,9 +5923,9 @@ const SCEV *ScalarEvolution::createAddRecFromPHI(PHINode *PN) {
         // We can add Flags to the post-inc expression only if we
         // know that it is *undefined behavior* for BEValueV to
         // overflow.
-        if (BEInst && isLoopInvariant(Accum, L) &&
-            isPostIncAddRecNeverPoison(BEInst, L))
-          (void)getAddRecExpr(getAddExpr(StartVal, Accum), Accum, L, IRFlags);
+        if (auto *BEInst = dyn_cast<Instruction>(BEValueV))
+          if (isLoopInvariant(Accum, L) && isAddRecNeverPoison(BEInst, L))
+            (void)getAddRecExpr(getAddExpr(StartVal, Accum), Accum, L, Flags);
 
         return PHISCEV;
       }
@@ -7441,16 +7431,29 @@ bool ScalarEvolution::isSCEVExprNeverPoison(const Instruction *I) {
   return isGuaranteedToTransferExecutionTo(DefI, I);
 }
 
-/// Check whether there is an instruction in a block dominating \p BB that would
-/// cause undefined behavior if \p I is poison. (The caller is responsible for
-/// making sure the instruction actually executes.)
-static bool poisonCausesDominatingUB(const Instruction *I, const BasicBlock *BB,
-                                     const Loop *L, const DominatorTree &DT) {
+bool ScalarEvolution::isAddRecNeverPoison(const Instruction *I, const Loop *L) {
+  // If we know that \c I can never be poison period, then that's enough.
+  if (isSCEVExprNeverPoison(I))
+    return true;
+
+  // If the loop only has one exit, then we know that, if the loop is entered,
+  // any instruction dominating that exit will be executed. If any such
+  // instruction would result in UB, the addrec cannot be poison.
+  //
+  // This is basically the same reasoning as in isSCEVExprNeverPoison(), but
+  // also handles uses outside the loop header (they just need to dominate the
+  // single exit).
+
+  auto *ExitingBB = L->getExitingBlock();
+  if (!ExitingBB || !loopHasNoAbnormalExits(L))
+    return false;
+
   SmallPtrSet<const Value *, 16> KnownPoison;
   SmallVector<const Instruction *, 8> Worklist;
 
-  // We start by assuming \c I is poison. Only things that are known to be
-  // poison under that assumption go on the Worklist.
+  // We start by assuming \c I, the post-inc add recurrence, is poison.  Only
+  // things that are known to be poison under that assumption go on the
+  // Worklist.
   KnownPoison.insert(I);
   Worklist.push_back(I);
 
@@ -7460,7 +7463,7 @@ static bool poisonCausesDominatingUB(const Instruction *I, const BasicBlock *BB,
     for (const Use &U : Poison->uses()) {
       const Instruction *PoisonUser = cast<Instruction>(U.getUser());
       if (mustTriggerUB(PoisonUser, KnownPoison) &&
-          DT.dominates(PoisonUser->getParent(), BB))
+          DT.dominates(PoisonUser->getParent(), ExitingBB))
         return true;
 
       if (propagatesPoison(U) && L->contains(PoisonUser))
@@ -7472,50 +7475,6 @@ static bool poisonCausesDominatingUB(const Instruction *I, const BasicBlock *BB,
   return false;
 }
 
-bool ScalarEvolution::isPostIncAddRecNeverPoison(const Instruction *PostIncI,
-                                                 const Loop *L) {
-  // If we know that \c PostIncI can never be poison period, then that's enough.
-  if (isSCEVExprNeverPoison(PostIncI))
-    return true;
-
-  // If the loop only has one exit, then we know that, if the loop is entered,
-  // any instruction dominating that exit will be executed. If any such
-  // instruction would result in UB, the addrec cannot be poison.
-  //
-  // This is basically the same reasoning as in isSCEVExprNeverPoison(), but
-  // also handles uses outside the loop header (they just need to dominate the
-  // single exit).
-
-  auto *ExitingBB = L->getExitingBlock();
-  if (!ExitingBB || !loopHasNoAbnormalExits(L))
-    return false;
-
-  return poisonCausesDominatingUB(PostIncI, ExitingBB, L, DT);
-}
-
-bool ScalarEvolution::canPreservePreIncAddRecNoWrapFlags(
-    const Instruction *PreIncI, const Instruction *PostIncI, const Loop *L) {
-  // The flags of the increment say that it is poison where it wraps, and a
-  // poison value that nothing observes is no undefined behavior. SCEV
-  // expressions are uniqued without their flags, so a flag on the pre-inc
-  // addrec is a claim about every value that shares the expression, and must
-  // hold on every iteration the loop enters, whatever uses the phi.
-
-  // The pre-inc value of an iteration other than the first is the post-inc
-  // value of the previous one. If the program is undefined whenever the phi is
-  // poison, the increment did not wrap on any backedge that was taken.
-  if (programUndefinedIfPoison(PreIncI))
-    return true;
-
-  // Likewise if a poison post-inc value would cause UB in a block dominating
-  // the latch: that block has been left by the time the backedge is taken.
-  auto *Latch = L->getLoopLatch();
-  if (!Latch)
-    return false;
-
-  return poisonCausesDominatingUB(PostIncI, Latch, L, DT);
-}
-
 ScalarEvolution::LoopProperties
 ScalarEvolution::getLoopProperties(const Loop *L) {
   using LoopProperties = ScalarEvolution::LoopProperties;

The port test under each compiler

row2-fork.sh
#!/bin/sh
# row2-fork.sh <tree-dir> <sysroot-key> <target-dir> <log>: the pin issue's second row under the fork's
# toolchain of that sysroot: the firmware test binary built for the host without incremental state,
# run whole, ended by PID if still running after 120 s.
D=$1; S=$STORE/sysroots/$2; T=$3; log=$4
cd $D
{ echo "tree: $(cat .tree-name 2>/dev/null || git rev-parse HEAD)"; echo "sysroot $2"; uptime; } > $log
CARGO_INCREMENTAL=0 CARGO_TARGET_DIR=$T RUSTC=$S/bin/rustc $S/bin/cargo test --locked -p toyos-userbound --test firmware --no-run --message-format=json 2>>$log > $T.json
echo "BUILD EXIT=$?" >> $log
bin=$(sed -n 's/.*"executable":"\([^"]*firmware-[^"]*\)".*/\1/p' $T.json)
$bin >> $log 2>&1 &
pid=$!
n=0; while kill -0 $pid 2>/dev/null && [ $n -lt 120 ]; do sleep 1; n=$((n+1)); done
if kill -0 $pid 2>/dev/null; then kill $pid; wait $pid 2>/dev/null; echo "STILL RUNNING after ${n}s, ended by PID; RUN EXIT=hung" >> $log
else wait $pid; echo "RUN EXIT=$? after ${n}s" >> $log; fi
uptime >> $log

The census

chain-final.sh
#!/bin/sh
# Both censuses at the head that lands. Fixed arms at the branch's head; base arms at the commit of
# main that head merged (census/BASE), with the fork checkout at the pin that commit names and one
# line of the sysroot's RECIPE changed, so that the base compiler builds its sysroot at this
# worktree's path as the fixed one was. Each sysroot is built by a build with no wrapper in it.
# Then, per arm: the images as they ship (rustc-wrapper-objects.sh: -Csave-temps alone), and the
# images with every crate as optimised IR (rustc-wrapper.sh), which makes each crate one unit.
C=$(dirname "$0"); L=$C/../../logs; S=$L/r2-42-census-chain-final.log
W=$TREE; BASE=$(cat $C/BASE)
cd $W
echo "head $(git rev-parse HEAD), base $BASE, $(date)" > $S
[ -z "$(git status --porcelain --ignore-submodules=none)" ] || { echo "tree not clean" >> $S; exit 2; }
arms() { # <suffix> <wrapper>
  WRAPPER=$2 sh $C/run-virtsmp-arm.sh $1; echo "virtsmp $1 rc=$? $(date +%T)" >> $S
  WRAPPER=$2 sh $C/run-arm.sh x86_64 $1; echo "x86_64 $1 rc=$? $(date +%T)" >> $S
  WRAPPER=$2 sh $C/run-arm.sh aarch64 $1; echo "aarch64 $1 rc=$? $(date +%T)" >> $S
  rm -f .cargo/local.toml
}
rm -f .cargo/local.toml
{ echo "head $(git rev-parse HEAD), fork $(git -C rust rev-parse HEAD), llvm $(git -C rust/src/llvm-project rev-parse HEAD)"; date; uptime; cargo run -- --build-only --arch x86_64; echo "EXIT=$? $(date)"; uptime; cat target/.deps-stamp; } > $L/r2-40-toolchain-final.log 2>&1; echo "fixed sysroot, no wrapper: $(grep '^EXIT' $L/r2-40-toolchain-final.log)" >> $S
cp target/.deps-stamp $C/deps-stamp.fixed
arms fixed-shipped $C/rustc-wrapper-objects.sh
arms fixed $C/rustc-wrapper.sh
git checkout -q --detach $BASE && git -C rust checkout -q --detach 6d6ad8c71906 && git -C rust/src/llvm-project checkout -q --detach ceaf0fbb8440
git apply --check $C/base-recipe-3.patch && git apply $C/base-recipe-3.patch || { echo "patch does not apply" >> $S; exit 2; }
echo "moved to base: tree $(git rev-parse HEAD), fork $(git -C rust rev-parse HEAD), llvm $(git -C rust/src/llvm-project rev-parse HEAD); status: $(git status --porcelain --ignore-submodules=none | tr '\n' ';')" >> $S
{ date; uptime; cargo run -- --build-only --arch x86_64; echo "EXIT=$? $(date)"; cat target/.deps-stamp; } > $L/r2-41-base-sysroot-no-wrapper.log 2>&1; echo "base sysroot, no wrapper: $(grep '^EXIT' $L/r2-41-base-sysroot-no-wrapper.log)" >> $S
cp target/.deps-stamp $C/deps-stamp.base
arms base-shipped $C/rustc-wrapper-objects.sh
arms base $C/rustc-wrapper.sh
git apply -R $C/base-recipe-3.patch
git -C rust/src/llvm-project checkout -q --detach b7420fe534bf && git -C rust checkout -q --detach b9cc8392f0eb && git checkout -q wt/toyos-scevfix
echo "restored: tree $(git rev-parse HEAD) on $(git branch --show-current), fork $(git -C rust rev-parse HEAD), llvm $(git -C rust/src/llvm-project rev-parse HEAD); status: $(git status --porcelain --ignore-submodules=none | tr '\n' ';')" >> $S
echo "done $(date +%T)" >> $S
run-arm.sh
#!/bin/sh
# run-arm.sh <arch> <base|fixed>: one --build-only of the whole image from no guest artifact of that
# architecture, by the compiler the worktree's tree names as it stands (fixed: the branch; base:
# origin/main's tree, whose fork pin is the compiler before the fix), every guest crate compiled
# through census/rustc-wrapper.sh; then its IR, objects and binaries copied out.
W=$TREE
C=$(dirname "$0"); L=$C/../../logs
arch=$1; arm=$2
printf '[build]\nrustc-wrapper = "%s"\n' ${WRAPPER:-$C/rustc-wrapper.sh} > $W/.cargo/local.toml
log=$L/r2-30-census-$arch-$arm.log
cd $W
{ date; uptime; echo "tree $(git rev-parse HEAD), fork $(git -C rust rev-parse HEAD), LLVM gitlink $(git -C rust ls-tree HEAD src/llvm-project)"; } > $log
find . -path ./rust -prune -o -type d -name "$arch-unknown-*" -path '*/target/*' -print -prune > $C/removed-$arch-$arm.txt
while read -r d; do rm -rf "$d"; done < $C/removed-$arch-$arm.txt
echo "removed $(wc -l < $C/removed-$arch-$arm.txt) guest target directories" >> $log
rc=0; cargo run -- --build-only --arch $arch >> $log 2>&1 || rc=$?
echo "BUILD EXIT=$rc $(date)" >> $log; uptime >> $log; cat target/.deps-stamp >> $log
[ $rc = 0 ] && sh $C/collect.sh $arch $arm >> $log 2>&1
echo "ARM DONE rc=$rc" >> $log
exit $rc
run-virtsmp-arm.sh
#!/bin/sh
# run-virtsmp-arm.sh <base|fixed>: the harness's own build and run of virt_el1_smp (tests/virtsmpcase:
# unmap_touch, test_rs_counters_read, test_rs_trace_read on 8 CPUs under TCG), from no AArch64 guest
# artifact, every guest crate compiled through the census wrapper; then its IR, objects and binaries
# copied out as architecture "virtsmp".
W=$TREE
C=$(dirname "$0"); L=$C/../../logs
arm=$1
printf '[build]\nrustc-wrapper = "%s"\n' ${WRAPPER:-$C/rustc-wrapper.sh} > $W/.cargo/local.toml
log=$L/r2-32-census-virtsmp-$arm.log
cd $W
{ date; uptime; echo "tree $(git rev-parse HEAD), fork $(git -C rust rev-parse HEAD)"; } > $log
find . -path ./rust -prune -o -type d -name "aarch64-unknown-*" -path '*/target/*' -print -prune > $C/removed-virtsmp-$arm.txt
while read -r d; do rm -rf "$d"; done < $C/removed-virtsmp-$arm.txt
echo "removed $(wc -l < $C/removed-virtsmp-$arm.txt) guest target directories" >> $log
rc=0; cargo test --test toyos-build -- virt_el1_smp >> $log 2>&1 || rc=$?
echo "TEST EXIT=$rc $(date)" >> $log; uptime >> $log
rm -rf $C/virtsmp/$arm; sh $C/collect.sh aarch64 $arm >> $log 2>&1 && mkdir -p $C/virtsmp && mv $C/aarch64/$arm $C/virtsmp/$arm
echo "ARM DONE rc=$rc" >> $log
exit $rc
collect.sh
#!/bin/sh
# collect.sh <arch> <arm>: copy what the last guest build emitted out of the worktree.
# IR: every crate's optimised .ll under a guest target's toyos/deps. Objects: every crate's
# codegen-unit object there, and every object a build script's C compiler left in its out/.
# Binaries: every file directly under a guest target's toyos/ that is ELF or PE.
set -e
W=$TREE
C=$(dirname "$0")
arch=$1; arm=$2; out=$C/$arch/$arm
rm -rf "$out"; mkdir -p "$out/ll" "$out/bin" "$out/obj" "$out/cobj"
cd $W
find . -path ./rust -prune -o -type f -name '*.ll' -path "*/$arch-unknown-*/toyos/deps/*" -print > "$out/ll.list"
while read -r f; do d="$out/ll/$(dirname "$f")"; mkdir -p "$d"; cp "$f" "$d/"; done < "$out/ll.list"
find . -path ./rust -prune -o -type f -name '*.rcgu.o' -path "*/$arch-unknown-*/toyos/deps/*" -print > "$out/obj.list"
while read -r f; do d="$out/obj/$(dirname "$f")"; mkdir -p "$d"; cp "$f" "$d/"; done < "$out/obj.list"
find . -path ./rust -prune -o -type f -name '*.o' -path "*/$arch-unknown-*/toyos/build/*/out/*" -print > "$out/cobj.list"
while read -r f; do
  # cargo's hash of the build script's run, which is not the same from arm to arm, goes from the path
  d="$out/cobj/$(dirname "$f" | sed 's#/build/\([^/]*\)-[0-9a-f]\{16\}/out#/build/\1/out#')"; mkdir -p "$d"; cp "$f" "$d/"
done < "$out/cobj.list"
find . -path ./rust -prune -o -type f -name output -path "*/$arch-unknown-*/toyos/build/doom-*/output" -print > "$out/ccmd.list"
while read -r f; do cp "$f" "$out/doom-build-output.txt"; done < "$out/ccmd.list"
find . -path ./rust -prune -o -type f -path "*/$arch-unknown-*/toyos/*" -not -path '*/toyos/*/*' -print > "$out/bin.cand"
while read -r f; do
  m=$(head -c 4 "$f" | od -An -tx1 | tr -d ' \n')
  case $m in 7f454c46|4d5a*) d="$out/bin/$(dirname "$f")"; mkdir -p "$d"; cp "$f" "$d/";; esac
done < "$out/bin.cand"
echo "$arch $arm: $(wc -l < "$out/ll.list") IR files, $(wc -l < "$out/obj.list") Rust objects, $(wc -l < "$out/cobj.list") C objects, $(find "$out/bin" -type f | wc -l) binaries"
rustc-wrapper-objects.sh
#!/bin/sh
# cargo's rustc wrapper for the census of the build as it ships, the same in both arms: a guest
# crate's compile gets -Csave-temps and nothing else, so its codegen units' object files stay
# beside its rlib or binary and it is cut into units and optimised exactly as an image build does.
rustc=$1; shift
case " $* " in
  *" --print"*|*" -vV "*) exec "$rustc" "$@";;
  *"-unknown-toyos "*|*"-unknown-none "*|*"-unknown-none-softfloat "*|*"-unknown-uefi "*)
    exec "$rustc" "$@" -Csave-temps;;
  *) exec "$rustc" "$@";;
esac
rustc-wrapper.sh
#!/bin/sh
# cargo's rustc wrapper for the census, the same in both arms: a guest crate's compile gets
# --emit=llvm-ir and -Csave-temps (its object file stays beside its rlib or binary).
rustc=$1; shift
case " $* " in
  *" --print"*|*" -vV "*) exec "$rustc" "$@";;
  *"-unknown-toyos "*|*"-unknown-none "*|*"-unknown-none-softfloat "*|*"-unknown-uefi "*)
    exec "$rustc" "$@" --emit=llvm-ir -Csave-temps;;
  *) exec "$rustc" "$@";;
esac
std-arm.sh
#!/bin/sh
# std-arm.sh <base|fixed> <compiler-key>: the sysroot's own std build (src/sysroot.rs's build_std:
# bootstrap stage-0 local rebuild of `library` for the six guest targets) by the store's compiler
# of that key, into a build directory in the scratchpad, with RUSTFLAGS=--emit=llvm-ir in both
# arms. The fork's two lockfiles are put back as they were.
set -e
X=$(cd $(dirname "$0")/../..; pwd)
F=$TREE/rust
C=$STORE/compilers/$2/stage2
arm=$1; B=$X/r2/census/std/$arm/build; log=$X/logs/r2-31-census-std-$arm.log
flags="--emit=llvm-ir"
host=aarch64-apple-darwin
rm -rf $X/r2/census/std/$arm; mkdir -p $B
cat > $B/bootstrap.toml <<TOML
change-id = "ignore"
profile = "compiler"

[build]
rustc = "$C/bin/rustc"
cargo = "$RUSTUP_HOME/toolchains/nightly-2026-07-22-$host/bin/cargo"
local-rebuild = true
build-dir = "$B"
host = ["$host"]
target = ["x86_64-unknown-toyos", "x86_64-unknown-none", "x86_64-unknown-uefi", "aarch64-unknown-toyos", "aarch64-unknown-none-softfloat", "aarch64-unknown-uefi"]

[llvm]
download-ci-llvm = false
ninja = false

[rust]
lld = false
debug-assertions-std = false

[target.x86_64-unknown-toyos]
linker = "$C/lib/rustlib/$host/bin/rust-lld"
rpath = false

[target.aarch64-unknown-toyos]
linker = "$C/lib/rustlib/$host/bin/rust-lld"
rpath = false
TOML
cd $F
cp Cargo.lock $X/r2/census/std/$arm/Cargo.lock.kept; cp library/Cargo.lock $X/r2/census/std/$arm/library-Cargo.lock.kept
{ date; uptime; echo "compiler $2, fork $(git rev-parse HEAD), RUSTFLAGS=$flags"; } > $log
rc=0
env -u GITHUB_ACTIONS -u CI BOOTSTRAP_SKIP_TARGET_SANITY=1 RUSTFLAGS="$flags" ./x build library --stage 0 --config $B/bootstrap.toml --warnings warn \
  --target x86_64-unknown-toyos,x86_64-unknown-none,x86_64-unknown-uefi,aarch64-unknown-toyos,aarch64-unknown-none-softfloat,aarch64-unknown-uefi >> $log 2>&1 || rc=$?
cp $X/r2/census/std/$arm/Cargo.lock.kept Cargo.lock; cp $X/r2/census/std/$arm/library-Cargo.lock.kept library/Cargo.lock
echo "BUILD EXIT=$rc $(date)" >> $log; uptime >> $log
git status --porcelain --ignore-submodules=none >> $log
mkdir -p $X/r2/census/std/$arm/ll
( cd $B/$host/stage0-std && find . -name '*.ll' -path '*/dist/build/*/out/*' ) > $X/r2/census/std/$arm/ll.list
# one directory per target; cargo's file-name hash and rustc's per-session part of a codegen
# unit's name, which differ between the arms, are taken out of each name
while read -r f; do
  t=$(echo "$f" | cut -d/ -f2)
  n=$(basename "$f" | sed -e 's/-[0-9a-f]\{16\}\././' -e 's/\.[0-9a-z]\{7\}\.rcgu\.ll$/.rcgu.ll/')
  mkdir -p "$X/r2/census/std/$arm/ll/$t"
  [ -e "$X/r2/census/std/$arm/ll/$t/$n" ] && { echo "two files under $t/$n" >> $log; exit 1; }
  cp "$B/$host/stage0-std/$f" "$X/r2/census/std/$arm/ll/$t/$n"
done < $X/r2/census/std/$arm/ll.list
echo "std $arm: $(wc -l < $X/r2/census/std/$arm/ll.list) IR files" >> $log
exit $rc
sysroot-members.sh
#!/bin/sh
# sysroot-members.sh <llvm-ar> <base-sysroot-key> <fixed-sysroot-key>: every archive each sysroot carries
# for a guest target (the Rust libraries' rlibs under lib/, and the C sysroot's archives under c/lib/:
# the C library and the C++ runtime), unpacked, one object per member.
C=$(dirname "$0"); S=$STORE/sysroots; ar=$1
for pair in "base $2" "fixed $3"; do set -- $pair; arm=$1; key=$2
  rm -rf $C/sysroot/$arm; mkdir -p $C/sysroot/$arm
  ( cd $S/$key/lib/rustlib && find . \( -path './*-unknown-*/lib/*.rlib' -o -path './*-unknown-*/c/lib/*.a' \) -type f ) | sort > $C/sysroot/$arm.archives
  while read -r a; do
    out=$C/sysroot/$arm/$a; mkdir -p "$out"
    # a member name can repeat in an archive; llvm-ar's N-th extraction keeps them apart
    $ar t "$S/$key/lib/rustlib/$a" | sort | uniq -c | while read -r n m; do
      case $m in *.o) ;; *) continue;; esac
      i=1; while [ $i -le $n ]; do ( cd "$out" && $ar xN $i "$S/$key/lib/rustlib/$a" "$m" && mv "$m" "$i-$m" ); i=$((i+1)); done
    done
  done < $C/sysroot/$arm.archives
  echo "$arm $key: $(wc -l < $C/sysroot/$arm.archives) archives, $(find $C/sysroot/$arm -name '*.o' | wc -l) objects"
done
doom-ir.sh
#!/bin/sh
# doom-ir.sh: doomgeneric's C as optimised IR by each arm's clang, for both userland targets, with
# the options userland/doom/build.rs gives the cc crate (-O2, its include directories, FEATURE_SOUND,
# its forced include, no warnings) and cc's own -ffunction-sections -fdata-sections -fPIC, against
# each arm's own C sysroot. The objects the image build itself made are compared apart from this.
C=$(dirname "$0"); W=$TREE; D=$W/userland/doom
host=$(rustc -vV | sed -n 's/^host: //p')
for arm in base fixed; do
  for t in x86_64-unknown-toyos aarch64-unknown-toyos; do
    S=$STORE/sysroots/$(sed -n "s/^$t //p" $C/deps-stamp.$arm)
    out=$C/doom-ir/$arm/$t; rm -rf $out; mkdir -p $out
    for f in $(sed -n 's/^ *"\([a-z0-9_]*\.c\)",$/\1/p' $D/build.rs); do
      $S/lib/rustlib/$host/bin/clang --target=$t --sysroot=$S/lib/rustlib/$t/c -O2 -ffunction-sections -fdata-sections -fPIC \
        -I $D/include -I $D/doomgeneric -DFEATURE_SOUND -include $D/include/doomtype.h -w -S -emit-llvm -o $out/${f%.c}.ll $D/doomgeneric/$f || { echo "$arm $t $f: clang failed"; exit 1; }
    done
    echo "$arm $t: $(ls $out | wc -l) IR files by $(sed -n "s/^$t //p" $C/deps-stamp.$arm)'s clang"
  done
done
rereview.sh
#!/bin/sh
# rereview.sh <arch>: each function round 1 read (kept as review2/<arch>-<n>.{unfixed,fixed}.ll, made by
# llvm-extract --func | opt --strip-debug, with <arch>.list naming file and symbol) made again the same
# way from this run's base and fixed arms, and compared text for text.
X=$(cd $(dirname "$0")/../..; pwd); C=$X/r2/census; B=$X/llvm-dev/bin; O=$X/census-results/review2; a=$1
out=$X/r2/results/$a/review; rm -rf $out; mkdir -p $out
n=0; same=0
while IFS="$(printf '\t')" read -r f sym; do
  n=$((n+1)); ok=1
  for pair in "unfixed base" "fixed fixed"; do set -- $pair
    $B/llvm-extract -S --func="$sym" "$C/$a/$2/ll/$f" | $B/opt --strip-debug -passes=verify -S -o "$out/$a-$n.$2.ll" || { echo "extract failed: $a-$n $2"; ok=0; continue; }
    cmp -s "$O/$a-$n.$1.ll" "$out/$a-$n.$2.ll" || { ok=0; echo "DIFFERS	$a-$n	$2 arm against round 1's $1	$(diff "$O/$a-$n.$1.ll" "$out/$a-$n.$2.ll" | grep -c '^[<>]') lines"; }
  done
  diff -U5 "$out/$a-$n.base.ll" "$out/$a-$n.fixed.ll" > "$out/$a-$n.diff"
  same=$((same+ok))
done < $O/$a.list
echo "$a: $same of $n functions are, in both arms, text for text what round 1 read"
base-recipe-3.patch: the base arm's one changed line
--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -100,3 +100,3 @@
                       runtimes' sources of the compiler's LLVM, the freestanding libraries cloned \
-                      from their key's; 13";
+                      from their key's; 13, built again at this worktree's path for the census, 3";
 
shipdiff.py
# shipdiff.py <llvm-objdump> <base-obj-dir> <fixed-obj-dir>: per-function comparison of two builds'
# object files as an image build makes them, where a crate is many codegen units. A function is its
# disassembly with its relocations (llvm-objdump -d -r --no-show-raw-insn --no-leading-addr), keyed
# by crate (the object's path up to cargo's hash) and symbol; the suffix LLVM gives a local symbol it
# promotes across units (.llvm.<number>) is taken out of every name. Where a crate holds a name more
# than once its bodies are compared as a sorted list. Prints one line per function that differs, with
# each arm's instruction counts, and per crate its unit counts where they are not the same.
import sys, re, os, subprocess, collections
from concurrent.futures import ThreadPoolExecutor
objdump, a_root, b_root = sys.argv[1:4]
HEAD = re.compile(r'^(?:[0-9a-f]+ )?<(.+)>:$')
PROMOTED = re.compile(r'\.llvm\.\d+')
CRATE = re.compile(r'^(.*-[0-9a-f]{16})\..*\.rcgu\.o$')
def fns(path):
    out = subprocess.run([objdump, '-d', '-r', '--no-show-raw-insn', '--no-leading-addr', path], capture_output=True, text=True)
    if out.returncode != 0: raise SystemExit('objdump failed on %s: %s' % (path, out.stderr[:300]))
    res = []; body = None
    for line in out.stdout.splitlines():
        m = HEAD.match(line)
        if m: body = []; res.append((PROMOTED.sub('', m.group(1)), body)); continue
        if line.startswith('Disassembly of section'): body = None; continue
        if body is not None and line.strip(): body.append(PROMOTED.sub('', line.strip()))
    return res
def crates(root):
    out = collections.defaultdict(list)
    for d, _, fs in os.walk(root):
        for f in fs:
            m = CRATE.match(f)
            if m: out[os.path.join(os.path.relpath(d, root), m.group(1))].append(os.path.join(d, f))
    return out
def crate(paths):
    into = collections.defaultdict(list)
    with ThreadPoolExecutor(8) as pool:
        for res in pool.map(fns, sorted(paths)):
            for name, body in res: into[name].append(tuple(body))
    return {k: sorted(v) for k, v in into.items()}
insn = lambda bodies: '+'.join(str(sum(1 for x in b if not re.match(r'^[0-9a-f]+:\s+R_|^[0-9a-f]+:\s+IMAGE_REL', x))) for b in bodies) or '-'
ca, cb = crates(a_root), crates(b_root)
total = differ = units_a = units_b = 0
for rel in sorted(set(ca) | set(cb)):
    if rel not in ca or rel not in cb: print('ONLY-IN-ONE\t' + rel); continue
    units_a += len(ca[rel]); units_b += len(cb[rel])
    if len(ca[rel]) != len(cb[rel]): print('UNITS\t%s\tbase=%d fixed=%d' % (rel, len(ca[rel]), len(cb[rel])))
    a, b = crate(ca[rel]), crate(cb[rel])
    for n in sorted(set(a) | set(b)):
        total += 1
        if a.get(n) != b.get(n):
            differ += 1
            print('OBJDIFF\t%s\t%s\t%s\t%s' % (rel, n, insn(a.get(n, [])), insn(b.get(n, []))))
print('TOTAL\tcrates=%d\tunits base=%d fixed=%d\tfunctions=%d\tdiffer=%d' % (len(set(ca) | set(cb)), units_a, units_b, total, differ))
archdiff.py
# archdiff.py <llvm-objdump> <base-dir> <fixed-dir>: per-function comparison of two sysroots' library
# archives, unpacked one directory per archive (sysroot-members.sh). A function is its disassembly with
# its relocations (llvm-objdump -d -r --no-show-raw-insn --no-leading-addr), keyed by archive and
# symbol and not by member: a crate's codegen units are not cut the same way by two compilers. The
# suffix LLVM gives a local symbol it promotes across units (.llvm.<number>), which names the unit, is
# taken out of every name. Where one archive holds a name more than once, its bodies are compared as a
# sorted list. Prints one line per function that differs, with each arm's instruction counts.
import sys, re, os, subprocess, collections
objdump, a_root, b_root = sys.argv[1:4]
HEAD = re.compile(r'^(?:[0-9a-f]+ )?<(.+)>:$')
PROMOTED = re.compile(r'\.llvm\.\d+')
def fns(path, into):
    out = subprocess.run([objdump, '-d', '-r', '--no-show-raw-insn', '--no-leading-addr', path], capture_output=True, text=True)
    if out.returncode != 0: raise SystemExit('objdump failed on %s: %s' % (path, out.stderr[:300]))
    name = None; body = None
    for line in out.stdout.splitlines():
        m = HEAD.match(line)
        if m:
            name = PROMOTED.sub('', m.group(1)); body = []; into[name].append(body); continue
        if line.startswith('Disassembly of section'): name = None; continue
        if name is not None and line.strip(): body.append(PROMOTED.sub('', line.strip()))
def archives(root):
    out = set()
    for d, ds, fs in os.walk(root):
        if any(f.endswith('.o') for f in fs): out.add(os.path.relpath(d, root))
    return out
def archive(path):
    into = collections.defaultdict(list); n = 0
    for f in sorted(os.listdir(path)):
        if f.endswith('.o'): fns(os.path.join(path, f), into); n += 1
    return {k: sorted(tuple(b) for b in v) for k, v in into.items()}, n
insn = lambda bodies: '+'.join(str(sum(1 for x in b if not re.match(r'^[0-9a-f]+:\s+R_|^[0-9a-f]+:\s+IMAGE_REL', x))) for b in bodies) or '-'
aa, ab = archives(a_root), archives(b_root)
total = differ = 0
for rel in sorted(aa | ab):
    if rel not in aa or rel not in ab: print('ONLY-IN-ONE\t' + rel); continue
    (a, na), (b, nb) = archive(os.path.join(a_root, rel)), archive(os.path.join(b_root, rel))
    d = 0
    for n in sorted(set(a) | set(b)):
        total += 1
        if a.get(n) != b.get(n):
            differ += 1; d += 1
            print('OBJDIFF\t%s\t%s\t%s\t%s' % (rel, n, insn(a.get(n, [])), insn(b.get(n, []))))
    print('ARCHIVE\t%s\tobjects base=%d fixed=%d\tfunctions=%d\tdiffer=%d' % (rel, na, nb, len(set(a) | set(b)), d))
print('TOTAL\tarchives=%d\tfunctions=%d\tdiffer=%d' % (len(aa | ab), total, differ))
cobjdiff.py
# cobjdiff.py <llvm-objdump> <base-dir> <fixed-dir>: per-function comparison of two builds' C and C++
# object files (*.o, paired by path): each function's disassembly with its relocations
# (llvm-objdump -d -r --no-show-raw-insn --no-leading-addr). Prints one line per function whose
# instructions differ, with each arm's instruction count.
import sys, re, os, subprocess
objdump, a_root, b_root = sys.argv[1:4]
HEAD = re.compile(r'^(?:[0-9a-f]+ )?<(.+)>:$')
def fns(path):
    out = subprocess.run([objdump, '-d', '-r', '--no-show-raw-insn', '--no-leading-addr', path], capture_output=True, text=True)
    if out.returncode != 0: raise SystemExit('objdump failed on %s: %s' % (path, out.stderr[:300]))
    res = {}; name = None; sect = ''
    for line in out.stdout.splitlines():
        m = HEAD.match(line)
        if m: name = sect + m.group(1); res.setdefault(name, []); continue
        if line.startswith('Disassembly of section'):
            # a static function's name can repeat within one object; its section tells them apart
            sect = line.split()[-1].rstrip(':') + ':' if '.text.' not in line else ''; name = None; continue
        if name is not None and line.strip(): res[name].append(line.strip())
    return res
def index(root):
    out = {}
    for d, _, fs in os.walk(root):
        for f in fs:
            if f.endswith('.o'): out[os.path.relpath(os.path.join(d, f), root)] = 1
    return out
ia, ib = index(a_root), index(b_root)
files = set(ia) | set(ib)
total = differ = same = 0; only = []
for rel in sorted(files):
    if rel not in ia or rel not in ib: only.append(rel); continue
    pa, pb = os.path.join(a_root, rel), os.path.join(b_root, rel)
    if open(pa, 'rb').read() == open(pb, 'rb').read():
        total += len(fns(pa)); same += 1; continue
    a, b = fns(pa), fns(pb)
    for n in sorted(set(a) | set(b)):
        total += 1
        if a.get(n) != b.get(n):
            differ += 1
            insn = lambda l: sum(1 for x in l if not re.match(r'^[0-9a-f]+:\s+R_|^[0-9a-f]+:\s+IMAGE_REL', x))
            print('OBJDIFF\t%s\t%s\t%d\t%d' % (rel, n, insn(a.get(n, [])), insn(b.get(n, []))))
for rel in only: print('ONLY-IN-ONE\t' + rel)
print('TOTAL\tobjects=%d\tbyte-identical=%d\tfunctions=%d\tdiffer=%d\tobjects-in-one-arm=%d' % (len(files), same, total, differ, len(only)))
irdiff.py
# irdiff.py <unfixed-ll-dir> <fixed-ll-dir>: per-function comparison of two builds' IR.
# A function body is its text with metadata references, debug records and comments
# removed. Prints one line per function that differs: file, symbol, and for each arm
# (lines, conditional branches, terminators).
import sys, re, os
DEF = re.compile(r'^define .*?@("[^"]+"|[\w.$]+)\(')
def norm(line):
    s = line.rstrip()
    if s.lstrip().startswith('#dbg_'): return None
    s = re.sub(r'\s*;.*$', '', s)
    s = re.sub(r',? ![\w.]+ !\d+', '', s)
    s = re.sub(r'!\d+', '!N', s)
    return s if s.strip() else None
def fns(path):
    out = {}; name = None; body = []
    with open(path, errors='replace') as f:
        for line in f:
            if name is None:
                m = DEF.match(line)
                if m: name = m.group(1).strip('"'); body = []
            elif line.startswith('}'):
                out[name] = body; name = None
            else:
                s = norm(line)
                if s is not None: body.append(s)
    return out
def shape(body):
    return (len(body), sum(1 for l in body if l.lstrip().startswith('br i1 ')),
            sum(1 for l in body if re.match(r'\s*(br|switch|ret|unreachable|resume|invoke|callbr|indirectbr) ', l) or ' invoke ' in l))
# With a third argument, files pair by their path less cargo's -<16 hex> file-name hash,
# which reads RUSTFLAGS where the symbol hash does not; two files of one arm under one such
# key are refused.
a_root, b_root = sys.argv[1], sys.argv[2]
strip = len(sys.argv) > 3
def key(rel): return re.sub(r'-[0-9a-f]{16}\.ll$', '.ll', rel) if strip else rel
def index(root):
    out = {}
    for d, _, fs in os.walk(root):
        for f in fs:
            # -Csave-temps leaves a one-unit crate's unit beside the crate's own IR: the same
            # functions again. A crate of several units has only its units' files.
            if f.endswith('.rcgu.ll') and f.split('.')[0] + '.ll' in fs: continue
            if f.endswith('.ll'):
                rel = os.path.relpath(os.path.join(d, f), root)
                if key(rel) in out: raise SystemExit('two files under %s in %s' % (key(rel), root))
                out[key(rel)] = rel
    return out
ia, ib = index(a_root), index(b_root)
files = set(ia) | set(ib)
total = differ = 0; only = []
for rel in sorted(files):
    if rel not in ia or rel not in ib:
        only.append(rel); continue
    pa, pb = os.path.join(a_root, ia[rel]), os.path.join(b_root, ib[rel])
    a, b = fns(pa), fns(pb)
    for n in sorted(set(a) | set(b)):
        total += 1
        if a.get(n) != b.get(n):
            differ += 1
            print('DIFF\t%s\t%s\t%s\t%s\t%s\t%s' % (ia[rel], ib[rel], n, shape(a.get(n, [])), shape(b.get(n, [])), 'only-unfixed' if n not in b else 'only-fixed' if n not in a else ''))
for rel in only: print('ONLY-IN-ONE\t' + rel)
print('TOTAL\tfiles=%d\tfunctions=%d\tdiffer=%d\tfiles-in-one-arm=%d' % (len(files), total, differ, len(only)))
objdiff.py
# objdiff.py <llvm-objdump> <unfixed-obj-dir> <fixed-obj-dir>: per-function comparison of two
# builds' object files, before linking: each function's disassembly with its relocations
# (llvm-objdump -d -r --no-show-raw-insn --no-leading-addr), keyed by object file and symbol.
# Prints one line per function whose instructions differ, with each arm's instruction count.
import sys, re, os, subprocess
objdump, a_root, b_root = sys.argv[1:4]
HEAD = re.compile(r'^(?:[0-9a-f]+ )?<(.+)>:$')
def fns(path):
    out = subprocess.run([objdump, '-d', '-r', '--no-show-raw-insn', '--no-leading-addr', path], capture_output=True, text=True)
    if out.returncode != 0: raise SystemExit('objdump failed on %s: %s' % (path, out.stderr[:300]))
    res = {}; name = None
    for line in out.stdout.splitlines():
        m = HEAD.match(line)
        if m: name = m.group(1); res.setdefault(name, []); continue
        if line.startswith('Disassembly of section'): name = None; continue
        if name is not None and line.strip(): res[name].append(line.strip())
    return res
# An object is <crate>-<cargo's hash>.<codegen unit>.<session>.rcgu.o; the last part
# is not the same from build to build, so objects pair by the rest, and two under one such
# name are refused.
def index(root):
    out = {}
    for d, _, fs in os.walk(root):
        for f in fs:
            if f.endswith('.rcgu.o'):
                rel = os.path.relpath(os.path.join(d, f), root)
                k = re.sub(r'\.[^./]*\.rcgu\.o$', '.rcgu.o', rel)
                if k in out: raise SystemExit('two objects under %s in %s' % (k, root))
                out[k] = rel
    return out
ia, ib = index(a_root), index(b_root)
files = set(ia) | set(ib)
total = differ = 0; only = []
for rel in sorted(files):
    if rel not in ia or rel not in ib: only.append(rel); continue
    pa, pb = os.path.join(a_root, ia[rel]), os.path.join(b_root, ib[rel])
    if open(pa, 'rb').read() == open(pb, 'rb').read():
        total += len(fns(pa)); continue
    a, b = fns(pa), fns(pb)
    for n in sorted(set(a) | set(b)):
        total += 1
        if a.get(n) != b.get(n):
            differ += 1
            insn = lambda l: sum(1 for x in l if not re.match(r'^[0-9a-f]+:\s+R_|^[0-9a-f]+:\s+IMAGE_REL', x))
            print('OBJDIFF\t%s\t%s\t%d\t%d' % (rel, n, insn(a.get(n, [])), insn(b.get(n, []))))
for rel in only: print('ONLY-IN-ONE\t' + rel)
print('TOTAL\tobjects=%d\tfunctions=%d\tdiffer=%d\tobjects-in-one-arm=%d' % (len(files), total, differ, len(only)))
binsize.py
# binsize.py <llvm-size> <llvm-nm> <unfixed-bin-dir> <fixed-bin-dir>: per linked binary, the
# text size of each arm (llvm-size's first column), and the defined symbols whose size differs.
import sys, os, subprocess
size, nm, a_root, b_root = sys.argv[1:5]
def text(path):
    out = subprocess.run([size, path], capture_output=True, text=True, check=True).stdout.splitlines()
    return int(out[1].split()[0])
def syms(path):
    out = subprocess.run([nm, '--print-size', '--defined-only', path], capture_output=True, text=True)
    res = {}
    for line in out.stdout.splitlines():
        p = line.split()
        if len(p) == 4 and p[2] in 'tTwW':
            res.setdefault(p[3], []).append(int(p[1], 16))
    return {k: sorted(v) for k, v in res.items()}
ta = tb = 0; nsym = ndiff = 0
for d, _, fs in os.walk(a_root):
    for f in sorted(fs):
        pa = os.path.join(d, f); rel = os.path.relpath(pa, a_root); pb = os.path.join(b_root, rel)
        if not os.path.exists(pb): print('ONLY-UNFIXED', rel); continue
        a, b = text(pa), text(pb); ta += a; tb += b
        sa, sb = syms(pa), syms(pb)
        changed = sorted(n for n in set(sa) | set(sb) if sa.get(n) != sb.get(n))
        nsym += len(set(sa) | set(sb)); ndiff += len(changed)
        same = open(pa, 'rb').read() == open(pb, 'rb').read()
        print('%s\ttext unfixed=%d fixed=%d delta=%+d\tsymbols=%d differing-size=%d\t%s' % (rel, a, b, b - a, len(set(sa) | set(sb)), len(changed), 'identical bytes' if same else 'bytes differ'))
        for n in changed: print('\tSYM\t%s\t%s\t%s\t%s' % (rel, n, sa.get(n), sb.get(n)))
print('TOTAL\ttext unfixed=%d fixed=%d delta=%+d (%.4f%%)\tsymbols=%d differing-size=%d' % (ta, tb, tb - ta, 100.0 * (tb - ta) / ta, nsym, ndiff))
exits.py
# exits.py <opt> <unfixed-ll-dir> <fixed-ll-dir> <irdiff-output>: for every function irdiff
# found different, the loops each arm has and how many blocks leave each, from
# opt -passes='print<loops>'. Prints a function where the unfixed arm has a loop with no
# exiting block, or fewer exiting blocks in total than the fixed arm.
import sys, re, subprocess, collections
opt, a_root, b_root, diff = sys.argv[1:5]
want = collections.defaultdict(set)
for line in open(diff):
    p = line.rstrip('\n').split('\t')
    if p[0] == 'DIFF': want[(p[1], p[2])].add(p[3])
def loops(path):
    out = subprocess.run([opt, '-passes=print<loops>', '-disable-output', path], capture_output=True, text=True)
    if out.returncode != 0: raise SystemExit('opt failed on %s: %s' % (path, out.stderr[:400]))
    res = {}; cur = None
    for line in (out.stdout + out.stderr).splitlines():
        m = re.match(r"Loop info for function '(.*)':", line)
        if m: cur = m.group(1); res[cur] = []; continue
        if cur is not None and line.lstrip().startswith('Loop at depth'):
            res[cur].append(line.count('<exiting>'))
    return res
for ra, rb in sorted(want):
    a, b = loops(a_root + '/' + ra), loops(b_root + '/' + rb)
    rel = ra
    for fn in sorted(want[(ra, rb)]):
        la, lb = a.get(fn, []), b.get(fn, [])
        endless = sum(1 for n in la if n == 0) > sum(1 for n in lb if n == 0)
        fewer = sum(la) < sum(lb)
        tag = 'ENDLESS' if endless else 'FEWER-EXITS' if fewer else 'same-or-more'
        print('%s\t%s\t%s\tunfixed loops=%d exiting=%d\tfixed loops=%d exiting=%d' % (tag, rel, fn, len(la), sum(la), len(lb), sum(lb)))
fndis.py
# fndis.py <llvm-objdump> <dir-of-objects> <symbol>: one function's disassembly with relocations, from
# whichever object of the directory defines it (the first, by name), promoted-local suffixes removed.
import sys, re, os, subprocess
objdump, root, sym = sys.argv[1:4]
HEAD = re.compile(r'^(?:[0-9a-f]+ )?<(.+)>:$'); PROMOTED = re.compile(r'\.llvm\.\d+')
for f in sorted(os.listdir(root)):
    if not f.endswith('.o'): continue
    out = subprocess.run([objdump, '-d', '-r', '--no-show-raw-insn', '--no-leading-addr', os.path.join(root, f)], capture_output=True, text=True).stdout
    name = None; hit = []
    for line in out.splitlines():
        m = HEAD.match(line)
        if m: name = PROMOTED.sub('', m.group(1)); continue
        if line.startswith('Disassembly of section'): name = None; continue
        if name == sym and line.strip(): hit.append(PROMOTED.sub('', line.strip()))
    if hit: print('\n'.join(hit)); break

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

T14 pass staged for the orchestrator (the review's BLOCKER 6)

cargo test --test toyos-build -- --metal --metal-readback <dir> with no filter, at 5b4592e88, a committed tree: exit 2, the staging mode's own ("staged 23 image(s) … The machine was not touched, so this run establishes nothing about it"). Every image is built by compiler a1a1399eacc6661a with sysroot 843dffb798581666. <dir>/request.txt lists the 23 boots --metal --list names (61 registrations and 232 shared members); no judge words, the whole list. Each image's SHA-256, also in <dir>/sha256.txt:

45c36f9fb3c8e69207f378b9d144ef025fa2c1b3b8f741efa09067b847724e11  acpicase/image.img
d6e3315168963402771c496f29d1a86d476b2b9300cca9796f4a06a134567996  ccorpus/image.img
798d2a779faa3c4defa01e5f2ef1a14b5849fd66a336bbd88e0d8548e25cc3e0  deadlinewedge/image.img
eb14047ec720a97af87a7061eaa459153046109eb6c85b45540608bb3fda4668  foreignrecord/image.img
b81f0c48b7a7880154c4684608ad10bc50f5c46142d427b37b3b74b953f7ff24  hardlockup/image.img
e46681873ccec38f30ee47f2240ccc38139a336db274333a9f9958e8a6fbc66f  iommu-no-remap/image.img
b99421293fc02ad9f4bb6c82dd114cef9558c1ac1689448712333597b9fa7755  isa-withheld/image.img
326ea7c117ca21894a9958d31c78f947fddb9370a130ac93642ddddce499061f  latencycase/image.img
1e0114e6f4d80775e3c666689bb5e261ae26240d6316b5ac777bf03d95c6faf7  logstallcase/image.img
081ff1bf3dea095f20d126773b31d4cbe27aa20b3c4a665f605bef275b48833a  metalcase/image.img
b4eebd726802e2b1a70107684675d6fa50e0bbad0bd6e825f8143c1c5a3047e0  metaldevicecase/image.img
95213a38d7fd7bb02d48e5f3b95d4b8941fdfd7296bbe8fb48346bd00a60ea1f  proctreecase/image.img
4359816ad2b9e9aba37c42596d093b784a33ebceca89f672d388a66a7a177d75  shared-debug/image.img
70594625261a42f5567b1716ba4fbb5c80190adb0751de3577d6a4f1203ab434  shared/image.img
b288ee3535de7442126ce98816e2f3b8bec6e66410d61162dfb7c4aa8a054bbe  testcases-deaf/image.img
1205489769ea10d32f909ae73db62510b1bb0eb05b81c54c753d3965a7b110e4  testcases-debug/image.img
98ec43d46dd55d7c338e916412f6effed27085c1da36234cdb6465d4e23edd1e  testcases-mkdir/image.img
5b3922d0bf0cd5f6f6b09bb722ee1b41c251e88cdbd22837356b159bc6bd9fb2  testcases-readdir/image.img
7562a2a92f70b31572e4a403aac011fae3f68a31841f4cbde8d6f215c9cce672  testcases-watchdog/image.img
f105b14ffa2de821ef29fcccf61b59ddc8b70bd2cc6e13c019ca72c7193faaad  testcases/image.img
57d31c1a99cb8b6799bac4d41247547dc97a632c4d4324bd2ea14e8097d5e3d0  usbbreak/image.img
f8e758943e95397737614cb2f4e46edad7ee888e5be97f904d60b4f550ab99c2  usbload/image.img
0b776ec31adde1e5425ca191bc5787e233eaa5f7841ee74b8a7cb183740fb79c  windowscase/image.img

What the review asks to be read off the pass: every row's verdict; every [measured] name within its bound; a symbolised kernel backtrace printed whole in a row that panics by design (the kernel's rustc_demangle differs from the old compiler's in six functions as shipped); and a row that grows a file on FAT through fileserver (ensure_capacity). Since this round also found std's fs::DirBuilder::_create differing, testcases-mkdir's test_rs_mkdir_cap is a third row worth reading.

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2: the scripts the first comment left out

How the shipped build's differing functions were extracted and tabulated, the run that makes every comparison, and the check that the two arms name each crate alike.

shipread.py
# shipread.py <llvm-objdump> <census-dir> <arch> <shipdiff.txt> <out-dir>: for every function shipdiff
# found different, both arms' disassembly as shipped written to <out-dir>, and whether each arm's
# is, instruction for instruction, what the one-unit build of the same crate (the arms whose IR
# was read) has under that symbol.
import sys, re, os, subprocess, glob
objdump, C, arch, diff, out = sys.argv[1:6]
HEAD = re.compile(r'^(?:[0-9a-f]+ )?<(.+)>:$'); PROMOTED = re.compile(r'\.llvm\.\d+')
cache = {}
def crate(arm, rel):
    key = (arm, rel)
    if key not in cache:
        res = {}
        for path in sorted(glob.glob(os.path.join(C, arch, arm, 'obj', rel + '.*.rcgu.o'))):
            text = subprocess.run([objdump, '-d', '-r', '--no-show-raw-insn', '--no-leading-addr', path], capture_output=True, text=True).stdout
            name = None
            for line in text.splitlines():
                m = HEAD.match(line)
                if m: name = PROMOTED.sub('', m.group(1)); res.setdefault(name, []).append([]); continue
                if line.startswith('Disassembly of section'): name = None; continue
                if name is not None and line.strip(): res[name][-1].append(PROMOTED.sub('', line.strip()))
        cache[key] = res
    return cache[key]
os.makedirs(out, exist_ok=True)
n = 0
for line in open(diff):
    p = line.rstrip('\n').split('\t')
    if p[0] != 'OBJDIFF': continue
    n += 1; rel, sym = p[1], p[2]
    verdict = []
    for arm in ('base', 'fixed'):
        shipped = crate(arm + '-shipped', rel).get(sym, [])
        one = crate(arm, rel).get(sym, [])
        open(os.path.join(out, '%s-%d.%s.s' % (arch, n, arm)), 'w').write('\n\n'.join('\n'.join(b) for b in shipped) + '\n')
        verdict.append('%s: %s' % (arm, 'same as one-unit' if shipped and sorted(shipped) == sorted(one) else 'no such symbol in one-unit' if not one else 'differs from one-unit'))
    print('%s-%d\t%s\t%s\t%s' % (arch, n, '; '.join(verdict), os.path.basename(rel), sym[:100]))
shape.py
# shape.py <arch-kind> <base.s> <fixed.s>: what of a function's control flow each arm's disassembly has:
# instructions, conditional branches, returns, and every relocation target (calls, panics and data alike)
# with its count; the targets one arm has and the other does not.
import sys, re, collections
kind, a, b = sys.argv[1:4]
COND = re.compile(r'^(j(?!mp)\w+|b\.\w+|cbn?z|tbn?z)\s') 
def shape(path):
    insn = cond = ret = 0; targets = collections.Counter()
    for l in open(path):
        l = l.strip()
        if not l: continue
        m = re.match(r'^(?:[0-9a-f]+:\s+)?(R_\S+|IMAGE_REL\S+)\s+(\S+)', l)
        if m: targets[re.sub(r'[+-]0x[0-9a-f]+$', '', m.group(2))] += 1; continue
        insn += 1
        if COND.match(l): cond += 1
        if re.match(r'^ret', l): ret += 1
    return insn, cond, ret, targets
(ia, ca, ra, ta), (ib, cb, rb, tb) = shape(a), shape(b)
only_a = sorted((ta - tb).elements()); only_b = sorted((tb - ta).elements())
print('insn %d -> %d\tcond %d -> %d\tret %d -> %d\treloc targets %d -> %d\tonly base: %s\tonly fixed: %s' % (
    ia, ib, ca, cb, ra, rb, sum(ta.values()), sum(tb.values()), ' '.join(t[:60] for t in only_a) or '-', ' '.join(t[:60] for t in only_b) or '-'))
cratehashes.py
# cratehashes.py <base-dir> <fixed-dir> <suffix>: the crate disambiguators (v0 mangling's Cs<hash>_<len><name>)
# each arm's files of that suffix mention, and for each crate name the ones only one arm has.
import sys, os, re, collections
PAT = re.compile(rb'Cs([0-9A-Za-z]+)_(\d+)')
def scan(root, suffix):
    out = collections.defaultdict(set)
    for d, _, fs in os.walk(root):
        for f in fs:
            if not f.endswith(suffix): continue
            data = open(os.path.join(d, f), 'rb').read()
            for m in PAT.finditer(data):
                n = int(m.group(2)); name = data[m.end():m.end() + n]
                if re.fullmatch(rb'[A-Za-z0-9_]+', name): out[name.decode()].add(m.group(1).decode())
    return out
a, b = scan(sys.argv[1], sys.argv[3]), scan(sys.argv[2], sys.argv[3])
for name in sorted(set(a) | set(b)):
    oa, ob = a[name] - b[name], b[name] - a[name]
    if oa or ob: print('%s\tbase-only=%s\tfixed-only=%s\tshared=%d' % (name, ','.join(sorted(oa)), ','.join(sorted(ob)), len(a[name] & b[name])))
print('crates named: base %d, fixed %d' % (len(a), len(b)))
analyse.sh
#!/bin/sh
# analyse.sh: every comparison of the two arms chain-final.sh left, into r2/results.
X=$(cd $(dirname "$0")/../..; pwd); C=$X/r2/census; R=$X/r2/results; B=$X/llvm-dev/bin
rm -rf $R; mkdir -p $R
for a in x86_64 aarch64 virtsmp; do
  mkdir -p $R/$a
  python3 -I $C/cratehashes.py $C/$a/base/ll $C/$a/fixed/ll .ll > $R/$a/cratehashes.txt
  python3 -I $C/irdiff.py $C/$a/base/ll $C/$a/fixed/ll > $R/$a/irdiff.txt; echo "$a irdiff EXIT=$?"
  python3 -I $C/objdiff.py $B/llvm-objdump $C/$a/base/obj $C/$a/fixed/obj > $R/$a/objdiff.txt; echo "$a objdiff EXIT=$?"
  python3 -I $C/binsize.py $B/llvm-size $B/llvm-nm $C/$a/base/bin $C/$a/fixed/bin > $R/$a/binsize.txt; echo "$a binsize EXIT=$?"
  python3 -I $C/exits.py $B/opt $C/$a/base/ll $C/$a/fixed/ll $R/$a/irdiff.txt > $R/$a/exits.txt; echo "$a exits EXIT=$?"
  python3 -I $C/shipdiff.py $B/llvm-objdump $C/$a/base-shipped/obj $C/$a/fixed-shipped/obj > $R/$a/shipdiff.txt; echo "$a shipdiff EXIT=$?"
  python3 -I $C/binsize.py $B/llvm-size $B/llvm-nm $C/$a/base-shipped/bin $C/$a/fixed-shipped/bin > $R/$a/shipped-binsize.txt; echo "$a shipped binsize EXIT=$?"
done
for a in x86_64 aarch64; do sh $C/rereview.sh $a > $R/$a/rereview.txt 2>&1; echo "$a rereview EXIT=$?"; done
mkdir -p $R/doom $R/sysroot
python3 -I $C/cobjdiff.py $B/llvm-objdump $C/x86_64/base-shipped/cobj $C/x86_64/fixed-shipped/cobj > $R/doom/cobjdiff-x86_64.txt; echo "doom cobjdiff EXIT=$?"
sh $C/doom-ir.sh > $R/doom/doom-ir.log 2>&1; echo "doom-ir EXIT=$?"
for t in x86_64-unknown-toyos aarch64-unknown-toyos; do
  python3 -I $C/irdiff.py $C/doom-ir/base/$t $C/doom-ir/fixed/$t > $R/doom/irdiff-$t.txt; echo "doom irdiff $t EXIT=$?"
  python3 -I $C/exits.py $B/opt $C/doom-ir/base/$t $C/doom-ir/fixed/$t $R/doom/irdiff-$t.txt > $R/doom/exits-$t.txt
done
bk=$(sed -n 's/^x86_64-unknown-toyos //p' $C/deps-stamp.base); fk=$(sed -n 's/^x86_64-unknown-toyos //p' $C/deps-stamp.fixed)
sh $C/sysroot-members.sh $B/llvm-ar $bk $fk > $R/sysroot/members.log 2>&1; echo "sysroot members EXIT=$?"
python3 -I $C/archdiff.py $B/llvm-objdump $C/sysroot/base $C/sysroot/fixed > $R/sysroot/archdiff.txt; echo "sysroot archdiff EXIT=$?"
the shape of each of the 48, as tabulated
x86_64-1 rustc_demangle insn 68 -> 67	cond 12 -> 10	ret 1 -> 1	reloc targets 6 -> 6	only base: -	only fixed: -
x86_64-2 rustc_demangle insn 78 -> 72	cond 12 -> 10	ret 1 -> 1	reloc targets 6 -> 6	only base: -	only fixed: -
x86_64-3 rustc_demangle insn 76 -> 70	cond 12 -> 10	ret 1 -> 1	reloc targets 6 -> 6	only base: -	only fixed: -
x86_64-4 rustc_demangle insn 66 -> 65	cond 12 -> 10	ret 1 -> 1	reloc targets 6 -> 6	only base: -	only fixed: -
x86_64-5 rustc_demangle insn 284 -> 282	cond 54 -> 52	ret 1 -> 1	reloc targets 35 -> 35	only base: -	only fixed: -
x86_64-6 rustc_demangle insn 161 -> 156	cond 23 -> 21	ret 1 -> 1	reloc targets 14 -> 14	only base: -	only fixed: -
x86_64-7 fileserver insn 263 -> 251	cond 22 -> 22	ret 1 -> 1	reloc targets 20 -> 20	only base: -	only fixed: -
x86_64-8 fileserver insn 263 -> 251	cond 22 -> 22	ret 1 -> 1	reloc targets 20 -> 20	only base: -	only fixed: -
x86_64-9 kurbo insn 859 -> 847	cond 24 -> 23	ret 1 -> 1	reloc targets 69 -> 65	only base: .data.rel.ro..Lanon.dced8c3102f8acf416ea5aab6810b48d.1 .data.rel.ro..Lanon.dced8c3102f8acf416ea5aab6810b48d.19 .rodata..Lanon.dced8c3102f8acf416ea5aab6810b48d.20 _RNvNtCs9VSTO83uhuD_4core6result13unwrap_failed	only fixed: -
x86_64-10 libm insn 896 -> 886	cond 139 -> 138	ret 1 -> 1	reloc targets 110 -> 108	only base: .LCPI5_13 .LCPI5_14	only fixed: -
x86_64-11 password_hash insn 823 -> 832	cond 30 -> 31	ret 1 -> 1	reloc targets 33 -> 33	only base: -	only fixed: -
x86_64-12 password_hash insn 545 -> 554	cond 11 -> 12	ret 1 -> 1	reloc targets 28 -> 28	only base: -	only fixed: -
x86_64-13 password_hash insn 635 -> 644	cond 11 -> 12	ret 1 -> 1	reloc targets 31 -> 31	only base: -	only fixed: -
x86_64-14 rustc_demangle insn 62 -> 67	cond 12 -> 10	ret 1 -> 1	reloc targets 6 -> 6	only base: -	only fixed: -
x86_64-15 rustc_demangle insn 73 -> 67	cond 12 -> 10	ret 1 -> 1	reloc targets 6 -> 6	only base: -	only fixed: -
x86_64-16 rustc_demangle insn 71 -> 65	cond 12 -> 10	ret 1 -> 1	reloc targets 6 -> 6	only base: -	only fixed: -
x86_64-17 rustc_demangle insn 61 -> 65	cond 12 -> 10	ret 1 -> 1	reloc targets 6 -> 6	only base: -	only fixed: -
x86_64-18 rustc_demangle insn 288 -> 287	cond 54 -> 52	ret 1 -> 1	reloc targets 35 -> 35	only base: -	only fixed: -
x86_64-19 rustc_demangle insn 142 -> 137	cond 23 -> 21	ret 1 -> 1	reloc targets 14 -> 14	only base: -	only fixed: -
x86_64-20 snake insn 698 -> 663	cond 58 -> 54	ret 1 -> 1	reloc targets 74 -> 72	only base: .data.rel.ro..Lanon.4fb9a59d25c9792deb7361e97fd60bcf.47 .text._RNvCscsDUJCwY8Fa_5snake9fill_rect	only fixed: -
x86_64-21 update insn 406 -> 404	cond 38 -> 38	ret 1 -> 1	reloc targets 35 -> 35	only base: -	only fixed: -
aarch64-1 rustc_demangle insn 57 -> 51	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
aarch64-2 rustc_demangle insn 67 -> 55	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
aarch64-3 rustc_demangle insn 65 -> 53	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
aarch64-4 rustc_demangle insn 56 -> 49	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
aarch64-5 rustc_demangle insn 242 -> 237	cond 54 -> 52	ret 1 -> 1	reloc targets 50 -> 50	only base: -	only fixed: -
aarch64-6 rustc_demangle insn 138 -> 130	cond 23 -> 21	ret 1 -> 1	reloc targets 19 -> 19	only base: -	only fixed: -
aarch64-7 fileserver insn 225 -> 221	cond 22 -> 22	ret 1 -> 1	reloc targets 27 -> 27	only base: -	only fixed: -
aarch64-8 fileserver insn 225 -> 221	cond 22 -> 22	ret 1 -> 1	reloc targets 27 -> 27	only base: -	only fixed: -
aarch64-9 kurbo insn 690 -> 671	cond 24 -> 23	ret 1 -> 1	reloc targets 57 -> 50	only base: .data.rel.ro..Lanon.b925e34d63e7ef4eeab1d841ca690023.1 .data.rel.ro..Lanon.b925e34d63e7ef4eeab1d841ca690023.1 .data.rel.ro..Lanon.b925e34d63e7ef4eeab1d841ca690023.19 .data.rel.ro..Lanon.b925e34d63e7ef4eeab1d841ca690023.19 .rodata..Lanon.b925e34d63e7ef4eeab1d841ca690023.20 .rodata..Lanon.b925e34d63e7ef4eeab1d
aarch64-10 password_hash insn 893 -> 903	cond 34 -> 35	ret 1 -> 1	reloc targets 19 -> 19	only base: -	only fixed: -
aarch64-11 password_hash insn 322 -> 331	cond 11 -> 12	ret 1 -> 1	reloc targets 13 -> 13	only base: -	only fixed: -
aarch64-12 password_hash insn 384 -> 393	cond 11 -> 12	ret 1 -> 1	reloc targets 13 -> 13	only base: -	only fixed: -
aarch64-13 rubato insn 99 -> 97	cond 14 -> 14	ret 1 -> 1	reloc targets 27 -> 27	only base: -	only fixed: -
aarch64-14 rubato insn 148 -> 146	cond 22 -> 22	ret 1 -> 1	reloc targets 43 -> 43	only base: -	only fixed: -
aarch64-15 rustc_demangle insn 57 -> 51	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
aarch64-16 rustc_demangle insn 67 -> 55	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
aarch64-17 rustc_demangle insn 65 -> 53	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
aarch64-18 rustc_demangle insn 56 -> 49	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
aarch64-19 rustc_demangle insn 246 -> 241	cond 54 -> 52	ret 1 -> 1	reloc targets 49 -> 49	only base: -	only fixed: -
aarch64-20 rustc_demangle insn 130 -> 122	cond 23 -> 21	ret 1 -> 1	reloc targets 19 -> 19	only base: -	only fixed: -
aarch64-21 update insn 360 -> 358	cond 39 -> 39	ret 1 -> 1	reloc targets 44 -> 44	only base: -	only fixed: -
virtsmp-1 rustc_demangle insn 57 -> 51	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
virtsmp-2 rustc_demangle insn 67 -> 55	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
virtsmp-3 rustc_demangle insn 65 -> 53	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
virtsmp-4 rustc_demangle insn 56 -> 49	cond 12 -> 10	ret 1 -> 1	reloc targets 8 -> 8	only base: -	only fixed: -
virtsmp-5 rustc_demangle insn 242 -> 237	cond 54 -> 52	ret 1 -> 1	reloc targets 50 -> 50	only base: -	only fixed: -
virtsmp-6 rustc_demangle insn 138 -> 130	cond 23 -> 21	ret 1 -> 1	reloc targets 19 -> 19	only base: -	only fixed: -

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 2 — wt/toyos-scevfix at 5b4592e88, ToyOSOrg/rust b9cc8392f0eb, ToyOSOrg/llvm-project b7420fe534bf

Read, no build, test or QEMU run: git diff 1b5192454 5b4592e88 less the merges of main; every changed file whole at the head; in the LLVM fork git diff 81b496be0342 b7420fe534bf and ceaf0fbb8440..b7420fe534bf; the round's step logs, control patches and census scripts; both arms' disassembly of the functions listed below. For the C++ runtime, whose extracted functions the implementer had not kept, I disassembled libc++.a of the two sysroots still in the store (8618c089fa736cb0, the base clang's, and 843dffb798581666) with a disassembler and compared them myself; that is a reading of store files and nothing in the worktree.

Net lines, ToyOS: 9 files, +270 −544. Production +167 −2 (src/miscompile.rs 92, its two reproducers 68, src/sysroot.rs +6 −2, src/lib.rs 1), tests +33, issues/ +69 −541, the gitlink. LLVM fork against ceaf0fbb8440: source +88 −41 in two files, tests +851 −344 in 46. The production growth is accepted: what a compiler emits is not readable from the tree, and the front-end-free case is what round 1 asked for.

Round 1's BLOCKERs

  1. CLOSED. git diff 81b496be0342 b7420fe534bf -- llvm/lib is ten deleted lines: the cl::opt and the if (UnconditionalPreIncNoWrapFlags) return true;. No parameter, declaration or run line is left (git grep for the option's two spellings over llvm, clang, lld at b7420fe534bf: nothing; no UNCONDITIONAL prefix in the two new tests). ceaf0fbb8440..b7420fe534bf under llvm/lib and llvm/include is the fix alone, and canPreservePreIncAddRecNoWrapFlags, poisonCausesDominatingUB and both call sites are byte for byte what round 1 judged, so the soundness reading stands and was not redone. Both fork commits are single-parent on the previous tip and are their branches' tips on GitHub; b9cc8392f0eb changes the src/llvm-project gitlink and nothing else. The red arm is now the whole revert: the checked patch is git diff b7420fe534bf ceaf0fbb8440 of the two source files (compared, identical), under it the two new tests fail (lit exit 1), reversed the three pass (exit 0).
  2. CLOSED. src/miscompile/last_exit.ll has no front end in it: it goes through the sysroot's clang at -O2, and nothing of rustc or core reaches it. I checked the IR by hand: %iter runs −1000..−1, %nextnext wraps on the pass before the last, the poison reaches %next and no branch, ret i1 true is right. Measured at 5b4592e88 through a driver test added and reversed by a checked patch: exit 101 against 8618c089fa736cb0 (clang, caller a branch to itself), 0 against 843dffb798581666; with the IR case patched out, 101 on the Rust case and 0; per case, 8 of 8 wrong under the base toolchain and 8 of 8 ret i1 true under the fixed one. Both architectures and not one: the fold is the middle end's, but the two data layouts differ in native integer widths, which indvars reads, each is measured red, and it costs one clang run. The oracle fails closed: a later LLVM that leaves the loop in any other form is refused, not passed. .env_remove and the red-arm-less u16 are gone; the Rust case covers GUEST_TARGETS. Where it runs is as round 1 accepted; its line is in both builds' logs that made a sysroot this round.
  3. CLOSED. doomgeneric: the build's 83 objects, base clang against fixed, 0 of 848 functions differ; as IR 2 of 847, same loops and exiting blocks. C++ runtime: 5 functions for AArch64 and 6 for x86-64. I read nine of those eleven (below).
  4. CLOSED. At b7420fe534bf: clang 49,762 discovered, exit 0; lld 3,180, exit 0; LLVM 71,556, exit 1 on one test, LLVM-Unit :: TargetParser/./TargetParserTests/HostTest/getMacOSHostVersion, which compares two readings of the host's macOS major version (27 against 28) and touches no optimiser; run alone it fails with that message. Unrelated, and the host's.
  5. CLOSED. PASS: LLVM :: CodeGen/PowerPC/git_revision.ll is in that whole-suite log. The body's contradiction is gone.
  6. OPEN, the orchestrator's. 23 boots are staged from 5b4592e88 (exit 2, the staging mode's own); no machine has been read. What to read is restated below.

Round 1's NOTEs

  • iv-select-cmp.ll: closed. @select_icmp_min_valid_iv_start leaves on %inc3, is vectorised again, and is the file's only difference from ceaf0fbb8440.
  • Alive2: accepted as not run. The body says what the argument's status is (two readers, no mechanised proof); that sentence must stay in the merge commit.
  • The pin issue's record: closed by measurement. Stable 1.99.0 on Apple silicon, without incremental state: exit 0, 24 passed at this branch; hung, ended after 120 s, at the tree before A byte the acpi claim's holder writes to SMI_CMD is a firmware call the kernel makes on the boot processor, refused for the FADT's own values and past eight a second #780; and it still compiles the reproducer over u16 and u128 to a branch to itself. #780 is below #778 on main's first-parent history, so the pin's premise was stale when it landed. Everything the issue now says is true of those logs. Its slug is not: BLOCKER 7.
  • The 51 one-unit bodies: closed. 26 of 26 and 25 of 25 are text for text what round 1 read, by the base and the fixed compilers.
  • The other three (targets, env_remove, u16): closed with BLOCKER 2.

The redone census

Method, judged. A function is its disassembly with relocations, keyed by crate and symbol, a crate's codegen units pooled because two compilers cut them differently, bodies under one name compared as a sorted list. A symbol present in one arm only compares unequal and is counted, so a function inlined away in one arm shows, and an inlining difference shows in the caller's body. The two arms name every crate alike (243, 234 and 79 crates, no disambiguator in one arm only), which is what building the base sysroot at the worktree's path bought. It over-reports and does not under-report control flow. What it does not see: anything outside text (constant pools, tables), and it was not asked why none of doom's 83 objects is byte-identical while none of their functions differs. A lost exit cannot hide in either. Sound for the claim it carries.

One merge behind the head: acceptable. The merge since brought #792 and #794: five guest crates (toyos-net-ip, toyos-net-shard, toyos-net-udp, toyos-tco, netstack's node) and the metal harness; not harness alone. The census asks what the old compiler did to the tree; code that arrived after it is compiled by the fixed compiler from the day this lands, and the fix only withholds flags the old one gave. The body says where it was made; that sentence stays.

Read by me, as shipped object code, both arms:

  • every kernel function that differs: the six of rustc_demangle's v0::Printer for x86-64 (print_sep_list ×4, the print_type closure, print_path_maybe_open_generics) and the six for AArch64, whose two arms are byte for byte the SMP image's six. In all twelve the base compiler peels the list loop's first pass; each arm has the same exits (parser in error, the closing E, the separator's and the element's error) and the same one panic, panic_const_add_overflow, the closure also its unwrap_failed;
  • Fat32::ensure_capacity in fileserver, x86-64: the base compares the widened index against 2^32, the fixed tests the u32 for −1; same branches and panics;
  • kurbo::fit::fit_to_cubic, AArch64, the one place the base has a target more: cmp x21, #0x15; b.eq; add; b.eq <unwrap_failed>; the second branch reads the first's flags and cannot be taken;
  • base64ct encode, AArch64: the fixed adds an add; cmp; b.lo before the vector loop and the base has nothing less;
  • std::fs::DirBuilder::_create for all four targets: one register in the test after the loop (16·n against n), on x86-64 toyos with stack slots renumbered around it;
  • the C++ runtime, nine of eleven: all five for AArch64 (parseFDEInstructions, parseCIE, evaluateExpression, getEncodedP, _Large_integer_to_chars) and four for x86-64 (getEncodedP, parseCIE, _Large_integer_to_chars, __barrier_algorithm_base::__arrive). Each is a counter in 64 bits in the base and 32 in the fixed, with identical control flow.

No lost exit in any of them. Not read by me: x86-64 libunwind's parseFDEInstructions and evaluateExpression, libm's rem_pio2_large, snake, update's write, rubato's two, symbolize's six, five of password_hash's six, three of fileserver's four, and kurbo beyond that branch. For those the evidence is the tabulated shape (the base never has fewer conditional branches or relocation targets except the base64ct check) and the body's own statement that it is a reading and no proof.

BLOCKER

  1. (round 1's, open) The T14 reading, the orchestrator's. Read, from the 23 staged images whose SHA-256 are beside the request:
    • every row's verdict, and every [measured] name within its bound;
    • the kernel's six: a row whose kernel panics by design, its symbolised backtrace printed whole. The six are the demangler's list printers, so the frames to read are the ones with a generic argument list, a tuple or a const list in their names: each complete, comma-separated, closed, and no frame missing against the same row's last reading on main;
    • fileserver's ensure_capacity (both instantiations): a row that grows a file on FAT through fileserver, and every boot's --fat32-check clean;
    • std's DirBuilder::_create: testcases-mkdir's test_rs_mkdir_cap, new against round 1's list;
    • the rest are reachable only where a row happens to run them and need no row of their own: password_hash's base64, kurbo, libm's rem_pio2_large, update's write, snake. libunwind's four are reached by a C++ throw alone, since Rust on ToyOS unwinds through the unwinding crate. No audio verdict moves on x86-64: rubato differs for AArch64 only.
    • Sent back by: any red row; a measured name outside its bound; a backtrace frame garbled, truncated or absent; a FAT check that fails after a growing row; test_rs_mkdir_cap red.
    • Across main's 55e4e1dd2 (calc, snake and doom are on the AArch64 ROOT: the list that left them out is deleted, and the owner's word of 2026-10-09 ships doom there #795): the reading stands. calc, snake and doom are on the AArch64 ROOT: the list that left them out is deleted, and the owner's word of 2026-10-09 ships doom there #795 deletes a list in src/build.rs that kept three programs off the AArch64 ROOT, a test of it and issue text; it changes no source an x86-64 image ships and no key. It also stands across a commit that closes 7 and 8 below, provided it touches no guest crate, no fork pin and not sysroot::RECIPE.
  2. issues/rustc-1-99-0-makes-an-endless-loop-of-a-port-test-on-apple-silicon.md — the slug and heading claim what this branch's own body edit refutes — issues/README.md, "Slugs": a slug the tree has refuted "is renamed in the commit that corrects the body, with every citation moved". d01c42e24 corrects the body to "since A byte the acpi claim's holder writes to SMI_CMD is a firmware call the kernel makes on the boot processor, refused for the FADT's own values and past eight a second #780 the test passes under 1.99.0 too" and keeps the name. Rename it to what is true (the nightly's macOS job pins 1.98.1 for a hang its test no longer shows; the host's rustc has the fault) and move its one citation, .github/workflows/nightly.yml:121, a comment.
  3. Body, "The defect issue is closed here", item 2 — not met by its letter under the compiler that lands. The item names minns.rs, the u16 file, for aarch64-unknown-toyos, x86_64-unknown-toyos, aarch64-unknown-none-softfloat and aarch64-unknown-uefi; the body answers with the check's u128 case. The only run of the issue's own files under a fixed compiler is round 1's, of another compiler key and another sysroot. Four rustc runs: the issue's command over minns.rs for those four targets from 843dffb798581666 and 42125dcfe1c67e4a, command, exit and caller in the body.

NOTE

  • The macOS pin. By the project's rules it goes: it is a workaround that holds nothing, and code that does not earn its keep is deleted. Not in this pull request: its issue's exit is a green portability-macos on stable in a nightly on main, which only a nightly can show, and the job is off this branch's task. The orchestrator's order is right: this lands, then one change puts stable back, and the issue is deleted when that nightly is green. What must not be lost with it is the weakness that remains: every host binary is compiled by an upstream rustc whose LLVM has the fault. That sentence belongs in issues/the-host-job-runs-the-toolchain-the-runner-ships.md, which the pin issue already calls its owner.
  • src/sysroot.rs:770 — let _ = fs::remove_dir_all(&miscompiles) discards the result of removing a directory refuse has just created — an exit status nobody reads; fail on it.
  • CI at the head that lands (the three checks are SKIPPED on this draft, which is no reading). toolchain / build: success, with llvm, compiler, freestanding and sysroot each built and not restored on the Linux runner, their keys 90d48821e80c5da1, a1a1399eacc6661a, 42125dcfe1c67e4a, 843dffb798581666 unless the closing commit moves a source they read, and Checking that the compilers of … keep a loop's last exit in the sysroot's build log before the C library's build. That run is the first measurement of this compiler built on Linux. host: success, not skipped, 78 steps or however many main then has. guest / suite: success under KVM, with the sysroot key toolchain output. The orchestrator may land on reading those three at the merge queue's head, with the T14 pass read as BLOCKER 6 says and 7 and 8 closed; a skipped or restored-only toolchain is not that reading.
  • Gates at 5b4592e88, read from their logs: --build-only 0 for both architectures, --ci host 0 with 78 steps green, cargo test 0 with 37 passed of 37 at 1-minute load 39 to 47.
  • Body, "Gates": "The first build's target/.deps-stamp names the four keys above" — the stamp names the sysroot and freestanding keys and a compiler hash that is not a1a1399eacc6661a; the LLVM and compiler keys are in the build's log lines. (Prose.)
  • Body, the sysroot table: libunwind's shift count is not "bounded by 63 in the source"; bit is an int that grows with the input, and the arms differ only past its signed overflow. (Prose.)

SEND BACK

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

The T14 on the compiler that lands, at 5b4592e88 (the orchestrator's reading; round 1's sixth BLOCKER). The whole metal list, 23 boots, every image built by compiler a1a1399eacc6661a on LLVM 90d48821e80c5da1; worktree clean before every boot and after; each image's sha256 checked against the staged list in the command that flashed it; every toyos-metal run exit 0 with the stick's FAT check on. Judged with --metal --metal-readback <dir> over the whole list: exit 0, 293 passed, 0 failed, 23 boots.

  • Measured names: 70 number(s), 0 past its record, 0 whose owner failed.
  • The kernel's six (the demangler's list printers): the hard-lockup boot's sealed record prints its symbolised frames whole: <kernel::sync::Lock<bool>>::lock (twice, with the lock's source line), kernel::deadline::this_cpu (six CPUs), kernel::hardlockup::probe::hold_and_sample. The same nine frames, name for name, as three earlier readings of that boot on main's compiler that are still on record here. The USB-load boot prints <kernel::drivers::xhci::XhciController>::wait_transfer and <kernel::hw::KernelHw as kernel::sched::hw::Machine>::idle_wait, each closed.
    • What this does not show: no row on the T14 printed a frame with a comma-separated list (two or more generic arguments, a tuple, a const list). Every list the demangler printed on the machine had one element or was a <T as Trait> path. The loop's first pass, which is the pass the two compilers treat differently, is what ran; a second element is exercised by the host suites and by no metal row.
  • fileserver's ensure_capacity and std's DirBuilder::_create: PASS mkdir_cap on testcases-mkdir; the file-growing members of shared and testcases all exit=0; no boot's FAT check failed.
  • No bound fired on any boot that was not staged to wedge; the staged-wedge boots returned as their rows expect (all rows green).

This reading stands for a head that merges main's landings since (#795 and later change no source an x86-64 image ships and no toolchain key) and for a commit that closes round 2's items 7 and 8 without touching a guest crate, a fork pin or the sysroot's recipe.

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Correction to the last paragraph of my T14 reading above: "#795 and later change no source an x86-64 image ships" is true of #795 and false of what has landed or is landing after it (#793 changes the shipped netstack's mDNS responder; #796 its virtio driver and the Intel driver's crate). Round 2 said the reading stands across #795 and across a commit that closes its items 7 and 8 without touching a guest crate, a fork pin or the sysroot's recipe. Whether it also stands across a merge of main that brings those other, separately reviewed and separately measured changes is for the next round to say; the pass measured this compiler on the tree at 5b4592e88.

…he weakness, and the check's scratch removal fails loudly

Round 2's review of #790.

The pin issue's slug and heading claimed that rustc 1.99.0 makes an endless
loop of the port test; its body has said since d01c42e that the test passes
under 1.99.0 since #780. It is renamed to what is true, the nightly's macOS
job pins 1.98.1 for a hang its test no longer shows, and its one citation, a
comment in nightly.yml, moves with it. Nothing else in the workflow changes.

The weakness that outlives the pin goes to the issue that owns the host's
toolchain: every host binary is compiled by an upstream rustc whose LLVM has
the ScalarEvolution fault.

sysroot::build discarded the result of removing the miscompile check's
scratch directory; it goes through keystore::remove, which fails. No key
reads src/sysroot.rs outside RECIPE, so no toolchain key moves. The same
discard one statement on, of libc's scratch target, is main's and outside
this round's fence: filed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 answered, at 8f55e8b90

8f55e8b90 is 5b4592e88 and one commit. main is not merged: git merge-tree --write-tree origin/main HEAD against 1621281ae exits 0, with no conflict.

  • BLOCKER 6. This is the orchestrator's T14 reading, now in the body's own section with what the pass does not show: no metal row printed a demangled list with a second element, so the host suites alone cover that element. This commit touches no guest crate, no fork pin and not sysroot::RECIPE.
  • BLOCKER 7. The issue is renamed to issues/the-nightlys-macos-job-pins-rustc-1-98-1-for-a-hang-its-test-no-longer-shows.md and its heading changed to match. Its one citation, the comment at nightly.yml:121, moves with it, and nothing else in the workflow changes (git grep on the old slug finds nothing). issues/the-host-job-runs-the-toolchain-the-runner-ships.md gains the sentence that every host binary is compiled by an upstream rustc whose LLVM has the fault.
  • BLOCKER 8. The issue's command was run over its own minns.rs (identical to the issue's text) from sysroot 843dffb798581666, for aarch64-unknown-toyos, x86_64-unknown-toyos, aarch64-unknown-none-softfloat and aarch64-unknown-uefi. All four exit 0 and give caller = start: ret i1 true. The two freestanding targets' libraries are 42125dcfe1c67e4a's (diff -r exit 0). The command, exits and assembly are in the body.
  • NOTE src/sysroot.rs:770. Fixed: the removal goes through keystore::remove, which panics on failure. No key reads src/sysroot.rs except through RECIPE. After both builds the stamp still names sysroot 843dffb798581666 and freestanding 42125dcfe1c67e4a. The sysroot key hashes the freestanding key, which hashes the compiler key a1a1399eacc6661a, which hashes the LLVM key 90d48821e80c5da1, so none of the four moved. The same discard one statement later (libc_target) is main's and was outside this round's fence. It is filed as issues/the-sysroot-build-discards-the-result-of-removing-libcs-scratch-target.md.
  • NOTE, the macOS pin. The body says it goes in a follow-up after this lands.
  • NOTEs, prose. Two body corrections: the stamp names only the sysroot and freestanding keys, and libunwind's bit is an unbounded int, so the two arms differ only past its signed overflow.
  • Alive2 not run. The body still says the soundness is an argument agreed by its readers, with no mechanised proof.

Gates (run at 75d5cf822, whose tree is identical): --build-only --arch x86_64 0, --arch aarch64 0, --ci host 0 (78 steps green, 1-minute load 33 to 51), cargo test --lib sourcegate 0 (10 passed). Guest suite and census were not rerun because no key moved.

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review, round 3: wt/toyos-scevfix at 8f55e8b90

Read only, with no build, test or QEMU run. I read git diff 5b4592e88 8f55e8b90 and every changed file whole at the head, plus the body. I also read the round's r3-* step logs, the staging log at 5b4592e88, and the T14 readback: the request, the hash list, every boot's exit and the judge's log. I checked the backtrace lines myself in the hard-lockup and USB-load boots' loader logs.

git diff 75d5cf822 8f55e8b90 is empty, so the gates' tree is the head's. Net lines this round: 5 files, +33 −3. Production is +1 −1 (src/sysroot.rs), issues/ is +31 −1 (one rename, one paragraph, one new file), and one workflow comment changed. The branch against main is 12 files, +302 −546.

Round 2's BLOCKERs

  1. CLOSED. The judge's log reads [metal] 293 passed, 0 failed, 23 boot(s) and "70 number(s) … 0 past its record, 0 whose owner failed".
    • All 23 boots exit 0. Each staged command carries --fat32-check, and each flashed image's SHA-256 matches the staged list, 23 of 23. The staging log names 5b4592e88 and exits 2, the staging mode's own exit.
    • PASS mkdir_cap is present, and no row is red.
    • The symbolised frames are the ones the reading names. The hard-lockup boot prints <kernel::sync::Lock<bool>>::lock+0x152 (on cpu7 with the lock's source line, and once more on its own), kernel::deadline::this_cpu on six CPUs and kernel::hardlockup::probe::hold_and_sample. The USB-load boot prints wait_transfer and <KernelHw as Machine>::idle_wait. Every frame is closed.
    • I searched the guest suite's log at 5b4592e88 and every metal boot's kernel and loader log for a symbolised frame with a comma-separated list. There are none, so the stated limit is real.
    • The reading is enough. In all twelve list printers, round 2 read both arms' object code: the same exits and panics, and the base compiler peeling the first pass. The fix makes the compiler give strictly fewer no-wrap flags, so the fixed compiler's output for the second pass can be wrong only through a fault this change does not touch.
    • The machine ran the first pass, which is the one the two compilers treat differently. The second element has the static reading and no execution under this compiler.
    • The three boot.testcases-window.* numbers the run did not measure are main's, tracked in issues/the-t14s-record-keeps-three-rows-of-a-boot-nothing-stages.md.
  2. CLOSED.
    • The issue is renamed to issues/the-nightlys-macos-job-pins-rustc-1-98-1-for-a-hang-its-test-no-longer-shows.md, and its heading changed with it.
    • In nightly.yml, only the cited path at line 121 changed.
    • git grep at 8f55e8b90 for rustc-1-99-0 and endless-loop-of-a-port returns nothing (exit 1).
    • issues/the-host-job-runs-the-toolchain-the-runner-ships.md now carries the weakness, with an exit something can read: a stable rustc that compiles src/miscompile.rs's reproducers right.
  3. CLOSED. The issue's command was run over its own minns.rs with 843dffb798581666's rustc (1.99.0-dev, LLVM 22.1.8).
    • All four targets exit 0: aarch64-unknown-toyos, x86_64-unknown-toyos, aarch64-unknown-none-softfloat and aarch64-unknown-uefi.
    • Each caller is start: ret i1 true in the IR, and its assembly sets 1 and returns.
    • The two freestanding targets' libraries match 42125dcfe1c67e4a (diff -r exit 0, both).
    • The command, the exits and caller are in the body.

Round 2's NOTEs

  • src/sysroot.rs:770: closed.
    • The removal now goes through keystore::remove. It returns on NotFound, retries only on DirectoryNotEmpty and panics on any other error.
    • The key correctly did not move. No key hashes src/sysroot.rs as a source:
      • sysroot::key_of hashes three things: build_text(), witness(root) and the freestanding key. build_text() covers the targets, libcxx::OPTIONS, clang::CMAKE and libc's two invocations. witness(root) covers SYSROOT_SOURCES and SYSROOT_MANIFESTS.
      • The freestanding key hashes RECIPE, std's configuration, the fork and the compiler key.
      • RECIPE's contract is what changes how sources become the published libraries. Deleting a scratch directory after the check changes no published byte, so leaving RECIPE alone is right.
      • r3-24 shows the stamp still naming 843dffb798581666 and 42125dcfe1c67e4a, with the store's use marks for compilers/a1a1399eacc6661a and sysroots/843dffb798581666 at the builds' minute.
    • The sibling libc_target discard is filed. It has an owner (the build system) and an exit a build can show.
  • The macOS pin: closed. The body says a follow-up removes it after this lands, and the host issue now owns the weakness that outlives it.
  • The two prose items: closed.
  • Alive2: closed. The body still says the argument is two readers and no mechanised proof. That sentence has to stay in the merge commit.

Gates at 75d5cf822 (same tree as the head)

gate result
--build-only --arch x86_64 EXIT=0
--build-only --arch aarch64 EXIT=0
--ci host EXIT=0, "Host: 78 step(s), all green"
cargo test --lib sourcegate EXIT=0, 10 passed

The guest suite and the census were not rerun. No toolchain key and no guest source moved, so that is acceptable.

The T14 reading across #793, #796 and #797

It stands for a landing head that merges them, and nothing more is owed from the machine.

What CI must show at the head that lands, and whether that is enough to land

A correction to round 2's CI note. On the Linux runner the four keys will not be 90d48821e80c5da1, a1a1399eacc6661a, 42125dcfe1c67e4a and 843dffb798581666. llvm::key hashes host_triple() and the host tools' identity (src/llvm.rs:130). compiler::build_text hashes host_triple() (src/compiler.rs:71). The freestanding and sysroot keys hash the compiler key in turn. Those four values are this macOS host's. Read in CI whether each key was built, not what its value is.

  • toolchain / build (the merge_group run):
    • It concludes success.
    • The bootstrap step's outputs llvm, compiler, freestanding and sysroot are each built. No restore step hit, which is expected: no entry for these keys exists that a merge group can restore.
    • The sysroot's build log has Checking that the compilers of … keep a loop's last exit before libc's build, with no refusal after it.
    • A timeout (the job has 350 minutes for LLVM and stage 2) is red, not a reading.
    • This is the first build of this compiler on Linux. Only its own check measures it.
  • host: success, not skipped, ending "Host: N step(s), all green" with N as many as main then has.
  • guest / suite: success under KVM. Its restore has to hit the sysroot key that toolchain gave as output (fail-on-cache-miss refuses a miss), and --ci guest has to exit 0.

The orchestrator may land by reading those three at the merge queue's head, with the T14 reading above. A skipped check, or a toolchain that restored any of the four, is not that reading.

BLOCKER

None open.

NOTE

  • Body, "The T14", "What it does not show": "the second element of a demangled list is held by the host suites alone" is false. The host suites run rustc_demangle compiled by upstream's rustc, not by this compiler. What holds the second element for this compiler is round 2's reading of both arms' object code, and no execution. (Prose.)

LAND

@Japabu

Japabu commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Cold CI at 8f55e8b90, read from each job's log. toolchain / build: all four keys built (llvm b6e43eac6576ea60, compiler 5675f032e009946d, freestanding 331c957b4e6b7c70, sysroot 932651db8fdb76d3) and cached; the sysroot step ran the miscompile check ("Checking that the compilers of … keep a loop's last exit"). host: 79 steps all green. guest suite: 37/37 ok on the restored sysroot. Merges clean with main. Queued.

@Japabu
Japabu added this pull request to the merge queue Oct 9, 2026
Merged via the queue into main with commit c757a86 Oct 9, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-scevfix branch October 9, 2026 17:51
Japabu added a commit that referenced this pull request Oct 9, 2026
…timing, into the stop's hold of the console wire

The counters-stall issue keeps main's ScalarEvolution paragraph for the
stalls it compared and this branch's fifth and sixth, which ran this
branch's kernel at aa7448db0 under the compiler before the fix and were
not compared; the paragraph citing the issue main closed goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
Japabu added a commit that referenced this pull request Oct 9, 2026
…in the store

`--hosted-clang` read the LLVM commit from the primary checkout's
`.git/modules/rust/modules/src/llvm-project`, a repository no build fetches
into: after #790 moved the gitlink to ToyOSOrg/llvm-project b7420fe it held
no such commit (its origin is rust-lang's), and the build died with
`unable to read tree`.

The normal build obtains the commit one way: bootstrap's `Llvm` step checks
the gitlink's commit out in the fork checkout and fetches it there, and
`llvm::place` writes what builds read of it into `llvm/<key>/src` from that
checkout; the C++ runtime is built from there. The LLVM now keeps the hosted
clang's pathspecs there too, and the hosted clang builds from that directory
of the LLVM it holds in use: the commit is the one the host LLVM's key names
on any host, whether the LLVM was built here or found in the store, and no
checkout is read.

The pathspecs an LLVM keeps are now part of its key, so a change to either
list moves it; the recipe moves to 5, and every LLVM key with it. The hosted
clang's key names the LLVM's key, which now names its sources, so it drops
its own copy of them; its recipe moves to 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cSFvbD35xJ2kGANVdm23C
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant