v-TP3-INTERRUPT-RESTORE — Session summary ======================================== Tag: v-TP3-INTERRUPT-RESTORE Commit: (the commit that adds this file) Fixes: ff7b2dd "tests: nonvacuity restores Compiler.mod when the run is interrupted" and 5e0312e "tests: the nonvacuity trap also restores the mod=11 probe sources" — the two commits immediately before this one Covers: the nonvacuity harness's interrupt safety `v-TP3-STRCAT` (tag `v-TP3-STRCAT`, commit `1bed3bb`) is the string concatenation milestone this session began with; its write-up is `v-TP3-STRCAT.md`. This document covers the test-harness repair that finishing that milestone turned up, which was small in code and turned out to be two problems deep. The bug ------- `shell/tests/nonvacuity.sh` mutates sources on purpose and has to put them back. It had an `EXIT` trap that restored `Runtime.mod`, `Exec86.mod`, `Shell.mod`, `tests/comimage.py` and `tests/comtest.py` — but not `Compiler.mod`, the file it mutates more than any other, because `SAVED_C` was defined at the BP-displacement section halfway down the file, well **after** the trap was set at line 153. An interrupted run therefore left `Compiler.mod` half-mutated, and `git status` showed a source that no case had finished with — the exact confusion that two agents colliding on one tree produces. Finding one: the trap had never fired for an interruption --------------------------------------------------------- Moving `SAVED_C` above the trap was the obvious half. Testing it disproved the comment next to it: ``` bash -c 'trap "echo EXIT-TRAP" EXIT; sleep 30' & kill -TERM $! # prints nothing ``` An **unhandled signal does not run an `EXIT` trap at all** — bash dies on the spot. So `trap restore_all EXIT` had only ever protected against an `exit` *inside* the script; `Runtime.mod` and the rest were never protected against interruption either, despite the comment promising "an interrupted run cannot leave the layout half-written". The fix is that a handler which only `exit`s is enough, because `exit` from a trap **does** fire the `EXIT` trap — and it has to exit, or the run would go on using a tree it has just put back: ``` trap restore_all EXIT trap 'exit 129' HUP trap 'exit 130' INT trap 'exit 143' TERM ``` Finding two: not every signal behaves the same, and Ctrl-C does not stop the run ------------------------------------------------------------------------------- Measured on bash 5.2.37, not assumed: | signal | does the handler fire? | |---|---| | `TERM` | yes — restores everything | | `HUP` | yes — restores everything | | `INT` | **usually never arrives** | For `INT` the rule is: a foreground child that dies of `SIGINT` makes bash treat the signal as the *child's* and **continue the script instead**. So **Ctrl-C does not abort `nonvacuity.sh` at all** — it kills the current case's child and the next case starts. `SIGTERM` is what actually stops it. The first version of the comment claimed `INT` worked; the interrupt test disproved it, and the comment now records what was measured. `tests/probe/modrm11.{py,s}` --------------------------- The last file group, committed separately. The mod=11 table section mutates the probe's own sources and puts them back after each case, so a run that died between the two left them edited. `SAVED_PY` / `SAVED_S` moved to the top with the other six copies. `restore_all` copies them **directly** rather than calling `restore_probe`, which is defined halfway down the file: a trap firing before that definition would abort the rest of the restore with `command not found`, putting back the copies before it and silently leaving the ones after it. Same failure mode the whole exercise is about. The trap now covers all eight: Runtime.mod Exec86.mod Shell.mod Compiler.mod tests/comimage.py tests/comtest.py tests/probe/modrm11.py tests/probe/modrm11.s Evidence -------- Each claim came from killing a live run at the moment a given file was dirty: | file live | before | after | |---|---|---| | `Compiler.mod` | `DIRTY`, byte-different | `clean`, **byte-identical** | | `tests/probe/modrm11.py` | `DIRTY` | both probe sources **byte-identical** | The healthy path is unchanged: `non-vacuity: 78 ok, 0 failed`, and `run_all.sh` reports `OVERALL: ALL PASS` across all 15 suites. How to verify this work ----------------------- cd shell && tests/nonvacuity.sh # expect: non-vacuity: 78 ok, 0 failed cd shell && tests/run_all.sh # expect: OVERALL: ALL PASS To reproduce the interrupt evidence — kill a run while a mutation is live and check the file comes back byte-identical: cd shell cp Compiler.mod /tmp/before ./tests/nonvacuity.sh > /dev/null 2>&1 & PID=$! # wait until Compiler.mod differs, then: kill -TERM $PID wait $PID 2>/dev/null cmp /tmp/before Compiler.mod && echo restored Note that `Compiler.mod` is restored by the trap but is a `.mod` source, so the trap's trailing `make` also rebuilds `Compiler.o` and `tpshell` from it — a restore that left a binary built from the mutation would still be reporting on the mutation. Next work --------- Item 2 (the large one) is untouched: nested procedures / recursion, `var` parameters (the `SEG:OFF` push, RESUME-TP3.md 3.11), range/index checks (`TU_RANGE_CHECK`, `TU_INDEX_CHECK`), typed constants (3.14), `array` at its point of use (`t14`), `case` with subrange labels. Still `ENoLib` as unknown identifiers: the string standard functions `length`, `copy`, `pos`, `concat`, `delete`, `insert`, `str`. `+` and the six comparisons are done (`v-TP3-STRCAT`, `v-TP3-STRCMP`). Also still open: - Harden the program-header parameter loop against non-advancing input (`program p(1;)`) with a `BOOLEAN` flag — **not** `EXIT`, which ICEs gm2. - FreeDOS (`freedos.qcow2`, FD14-LiveCD) is still untried; it is the only third opinion on the `INT 21h` shim. - `SAVED_C2`/`SAVED_R2` (the 8086-opcodes section) and `SAVED_R3` are per-case snapshots taken mid-file. They are **fine as they are**: the files they guard (`Compiler.mod`, `Runtime.mod`) are already trap-covered, so an interruption there is restored by `restore_all` regardless of which per-case copy was live.