diff --git a/specs/tri/t27b/build_record.t27 b/specs/tri/t27b/build_record.t27 new file mode 100644 index 0000000000..20051993f5 --- /dev/null +++ b/specs/tri/t27b/build_record.t27 @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/tri/t27b/build_record.t27 -- when `t27b build` may skip a build because nothing that feeds the +; object changed. Issue gHashTag/t27#10860, epic #10863 (T27F developer tools in t27b and t27c, sped up +; by t27c seal). +; -- +; Why. Every `t27b build`, and every rebuild of `t27b watch`, parses, lowers and encodes the whole `use` +; closure, even when the save changed nothing the object is made of. A hot-reloading T27 OS window +; (specs/t27os/hot_reload.t27) restarts whenever its object is rewritten, so a rewrite of identical bytes +; is a restart for nothing. +; -- +; Nothing is decided twice. specs/verified/reuse.t27 is the reuse rule: reuse only when every part is +; recorded on both sides and equal and the old verdict passed; MISSING never means SAME; the reason is +; the first part that differs. This spec only says which parts a native object has, in that file's order: +; - spec: the root `.t27` bytes; +; - closure: the bytes of every other spec its `use` lines reach; +; - toolchain: the `t27b` binary itself; +; - config: the flags that change the bytes (`--elf`/`--macho`, `--overflow`); +; - verdict: always PASS, because a record is written only after a build succeeded. +; -- +; The record. `` + RECORD_SUFFIX, next to the object, the same shape as specs/t27f/build.t27: `#` +; lines carry what is not a file, every other line is ` ` -- the root first, the closure, +; the `t27b` binary last -- so `shasum -a 256 -c .build` reads it unchanged. The hashes come from the +; system sha256 tool (sha256sum, else `shasum -a 256`); this spec computes none. +; -- +; Same bytes, no rewrite. When a part moved but the new object is byte-identical to the one on disk (a +; comment edit), the object is not rewritten and only the record is refreshed, so a hot-reloading app does +; not restart. This replaces, for t27b, the gen-hash route of specs/verified/watch.t27 watch_skip: t27b +; cannot learn a spec's four gen hashes without running t27c's four backends, which costs more than its +; own build, and comparing the object it just built answers the same question exactly. +; -- +; `--force` builds regardless of the record. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module T27bBuildRecord; + +use verified::reuse::PART_MISSING; +use verified::reuse::PART_SAME; +use verified::reuse::PART_DIFF; +use verified::reuse::REUSE; +use verified::reuse::REBUILD_SPEC; +use verified::reuse::REBUILD_CLOSURE; +use verified::reuse::REBUILD_TOOLCHAIN; +use verified::reuse::REBUILD_CONFIG; +use verified::reuse::REBUILD_MISSING; +use verified::reuse::reuse_decision; + +/// The record lives at `` + this suffix. +pub const RECORD_SUFFIX : str = ".build"; + +/// First line of the record. +pub const RECORD_HEAD : str = "# t27b build record 1"; + +/// The config part: this prefix, then the byte-changing flags. +pub const KEY_FLAGS : str = "# flags "; + +/// `--force` was given. +pub const REBUILD_FORCED : u8 = 7; + +/// The word printed for each reason, indexed by its value (reuse.t27 0..6, then forced). +pub const REASON_WORDS : str = "reuse spec-changed closure-changed t27b-changed flags-changed verdict no-record forced"; + +/// One part of the record against now: `found` is false when the record has no line for it, `same` when +/// every line for it checked OK. +pub fn record_part(found: bool, same: bool) -> u8 { + if (found == false) { return PART_MISSING; } + if (same) { return PART_SAME; } + return PART_DIFF; +} + +/// REUSE, or the first reason to build: forced, then reuse.t27's order over spec, closure, t27b, flags. +pub fn record_reason(force: bool, spec: u8, closure: u8, toolchain: u8, config: u8) -> u8 { + if (force) { return REBUILD_FORCED; } + return reuse_decision(spec, closure, toolchain, config, true); +} + +/// Write the object: only when its bytes differ from the object already on disk (or there is none). +pub fn record_rewrite(same_bytes_on_disk: bool) -> bool { + return same_bytes_on_disk == false; +} + +/// Write the record: only after a successful build, so its verdict is always PASS. +pub fn record_write(build_ok: bool) -> bool { + return build_ok; +} + +test every_part_the_same_reuses { + assert record_reason(false, PART_SAME, PART_SAME, PART_SAME, PART_SAME) == REUSE; +} + +test force_wins_over_a_fresh_record { + assert record_reason(true, PART_SAME, PART_SAME, PART_SAME, PART_SAME) == REBUILD_FORCED; +} + +test no_record_rebuilds { + const m : u8 = record_part(false, true); + assert m == PART_MISSING; + assert record_reason(false, m, m, m, m) == REBUILD_MISSING; +} + +test a_missing_part_never_counts_as_the_same { + assert record_reason(false, PART_SAME, PART_SAME, PART_MISSING, PART_SAME) == REBUILD_MISSING; +} + +test the_first_changed_part_is_the_reason { + const d : u8 = record_part(true, false); + assert d == PART_DIFF; + assert record_reason(false, d, d, d, d) == REBUILD_SPEC; + assert record_reason(false, PART_SAME, d, d, d) == REBUILD_CLOSURE; + assert record_reason(false, PART_SAME, PART_SAME, d, d) == REBUILD_TOOLCHAIN; + assert record_reason(false, PART_SAME, PART_SAME, PART_SAME, d) == REBUILD_CONFIG; +} + +test identical_bytes_are_not_rewritten { + assert record_rewrite(true) == false; + assert record_rewrite(false); +} + +test a_failed_build_writes_no_record { + assert record_write(false) == false; + assert record_write(true); +} + +test forced_is_the_word_after_the_reuse_reasons { + assert record_reason(true, PART_MISSING, PART_MISSING, PART_MISSING, PART_MISSING) == REBUILD_MISSING + 1; +}