diff --git a/dag/gunbc/guarantee_rung_drop.dag b/dag/gunbc/guarantee_rung_drop.dag index b99f1a7edb1..e3f993eb718 100644 --- a/dag/gunbc/guarantee_rung_drop.dag +++ b/dag/gunbc/guarantee_rung_drop.dag @@ -383,6 +383,26 @@ data gitattributes_committed_emit_drift_stall: GuaranteeStall = GuaranteeStall { next_rung_trigger: "node://adhoc-16f7520a-85f lands: identify the authority edit that introduced the drift, regenerate .gitattributes from that authority, and restore a required generated-artifact drift gate so later authority/artifact disagreement refuses" } +// THE WALL DEADLINE STILL CHARGES A SHARED-ARTIFACT FILL TO WHICHEVER CLAIM PAID IT, and this row +// exists because a comment beside the code would go stale on the day the population changes while +// this row cannot. The 2026-08-27 attribution ruling has now been applied to three homes one at a +// time -- the completion-side CPU split, the completion-side WALL split, and the CPU evaluation +// deadline -- and each application was made only after that home's omission had cost something. +// The wall deadline is the fourth home and it is unrepaired. +// +// WHY IT IS SCOPED OUT RATHER THAN FIXED HERE: measured on run 33185280160, all 44 interruptions +// are on the `Cpu` clock, 44 of 44, so the wall arm is currently unexercised. That is a fact about +// today's population and NOT about the mechanism, which is exactly why the obligation is declared +// as a countable row rather than left as a note. The trigger is deliberately an OBSERVATION rather +// than a promise: the first wall-clock interruption to appear in the floor's ledger is the event +// that makes this reachable, and it fires without anyone remembering this row exists. +data wall_deadline_shared_fill_attribution_stall: GuaranteeStall = GuaranteeStall { + subject: "the wall evaluation deadline charges shared-artifact fill to the claim that paid it, so a wall interruption is a function of discovery order rather than of the row", + current: Mitigatable, + ceiling: StructurallyGuaranteed, + blocker: ClimbableButUnbuilt, + population: BoundedPopulation { members: Cons { head: "v1_interpreter.arm_wall_deadline", tail: Cons { head: "v1_interpreter.wall_deadline_remaining_ms", tail: Cons { head: "v1_interpreter.wall_deadline_exceeded_error", tail: Empty {} } } } }, + next_rung_trigger: "the required floor's ledger reports any INTERRUPTED-BEFORE-VERDICT row whose clock is Wall, at which point the wall deadline must read a fill-netted clock exactly as v1_interpreter.budgeted_cpu_nanos does for the CPU deadline" data heterogeneous_child_list_stall: GuaranteeStall = GuaranteeStall { subject: "a type's child list may hold BOTH a type node and a field node, and v1.04_types child_type_node tells them apart by whether `inferred` is populated -- a stamp v1.02_parse field_to_child_node writes at PARSE TIME as Resolved, before any resolution has run. A child list that mixes the two kinds makes that accessor return the FIELD node where its caller expects the TYPE, with no diagnostic", current: OutsideTheLadder, @@ -432,7 +452,8 @@ data all_guarantee_stalls: List = [ self_host_candidate_generation_add_slice_stall, non_fold_residue_roster_stall, retained_rust_live_tree_migration_stall, - gitattributes_committed_emit_drift_stall + gitattributes_committed_emit_drift_stall, + wall_deadline_shared_fill_attribution_stall ] // A STALL AT ITS CEILING IS NOT A STALL: the row would describe a class that already arrived, and diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index e18064121b8..3bb4dd5e4b9 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -2041,10 +2041,12 @@ pub fn compile_dag_diagnostic_census(source: &str) -> CompileDiagnosticCensus { // `run_claim_measured` as its own `[floor-shared-fill]` column — the claim's marginal and fill // halves still sum to what it actually spent. let fill_started = v1_interpreter::thread_cpu_nanos(); + let fill_wall_started = std::time::Instant::now(); let census = compile_dag_diagnostic_census_uncached(source); record_shared_artifact_fill_cpu( v1_interpreter::thread_cpu_nanos().saturating_sub(fill_started), ); + record_shared_artifact_fill_wall(fill_wall_started.elapsed().as_nanos()); COMPILE_DAG_DIAGNOSTIC_CENSUS_MEMO.with(|m| m.borrow_mut().insert(memo_key, census.clone())); census } @@ -2543,21 +2545,52 @@ thread_local! { /// COUNTED. A cost that vanished here would be the absorbing fallback (§5) wearing an /// accounting label — the deficit's frequency zeroed by construction — so the receipt carries /// marginal and fill as two columns whose sum is the claim's whole measured cost. - static SHARED_ARTIFACT_FILL_CPU_NANOS: std::cell::Cell = + // (the CPU fill cell now lives in `v1_interpreter`; see the delegating fns below) + + /// THE SAME FILL, ON THE OTHER CLOCK. The ruling above is about WHOSE COST A FILL IS, which + /// is a fact about attribution and not about which clock measured it — so it applies once to + /// every ceiling derived from a claim's elapsed time. It was landed on the CPU ceiling alone, + /// and the wall ceiling kept charging the whole fill to the first payer, which is how two + /// rows whose own cost is 0ms and 1ms refused a required floor at ~18000ms against a 10000ms + /// wall requirement (main run 33145062452, `test.claim.transport_script_wall_compile_red`: + /// `[floor-shared-fill]` reported `marginal_cpu_ms=0 fill_cpu_ms=18966` for one of them). + /// A one-clock accounting rule is the §3 failure the CPU comment already names — one concept + /// with two homes, one of which does not apply it — so this cell exists to close the second + /// home rather than to add a policy beside it. + static SHARED_ARTIFACT_FILL_WALL_NANOS: std::cell::Cell = const { std::cell::Cell::new(0) }; } /// Accumulate CPU spent filling a shared memoized artifact. Called ONLY from a memo MISS path, /// and only under the floor guard — outside it there is no memo, so there is no shared artifact /// and nothing to attribute. +/// +/// THE CELL MOVED TO `v1_interpreter` AND THIS DELEGATES. The evaluation deadline must net the +/// same quantity WHILE a claim runs, and the interpreter cannot read a cell owned here, so one +/// counter now serves both readers rather than two counters drifting apart — which is the exact +/// defect (one accounting rule, two homes) this line of work exists to close. fn record_shared_artifact_fill_cpu(nanos: u128) { - SHARED_ARTIFACT_FILL_CPU_NANOS.with(|c| c.set(c.get().saturating_add(nanos))); + v1_interpreter::record_shared_artifact_fill_cpu_nanos(nanos); +} + +/// Accumulate WALL time spent filling a shared memoized artifact, under the same rule and from +/// the same miss paths as `record_shared_artifact_fill_cpu`. The two are recorded together at +/// every call site so a fill can never be counted on one clock and not the other — which is the +/// state that produced the defect this pair exists to close. +fn record_shared_artifact_fill_wall(nanos: u128) { + SHARED_ARTIFACT_FILL_WALL_NANOS.with(|c| c.set(c.get().saturating_add(nanos))); } /// Read the running total for this thread. The claim loop samples it either side of one claim; /// the difference is that claim's fill. pub fn shared_artifact_fill_cpu_nanos() -> u128 { - SHARED_ARTIFACT_FILL_CPU_NANOS.with(|c| c.get()) + v1_interpreter::shared_artifact_fill_cpu_nanos() +} + +/// Read the running wall-clock total for this thread, sampled either side of one claim exactly +/// as the CPU total is. +pub fn shared_artifact_fill_wall_nanos() -> u128 { + SHARED_ARTIFACT_FILL_WALL_NANOS.with(|c| c.get()) } static COMPILE_DAG_RUST_EMIT_CHECK_MEMO_HITS: std::sync::atomic::AtomicU64 = @@ -2645,10 +2678,12 @@ pub fn compile_dag_rust_emit_check( // against, so the two quantities cannot drift apart, and recorded rather than subtracted here // — the claim loop does the split, this only says how much of the cost was a fill. let fill_started = v1_interpreter::thread_cpu_nanos(); + let fill_wall_started = std::time::Instant::now(); let verdict = compile_dag_rust_emit_check_uncached(source, file_path, includes, excludes); record_shared_artifact_fill_cpu( v1_interpreter::thread_cpu_nanos().saturating_sub(fill_started), ); + record_shared_artifact_fill_wall(fill_wall_started.elapsed().as_nanos()); COMPILE_DAG_RUST_EMIT_CHECK_MEMO.with(|m| m.borrow_mut().insert(memo_key, verdict)); verdict } @@ -20481,11 +20516,15 @@ pub fn run_claim_measured( let started = std::time::Instant::now(); let cpu_started_nanos = v1_interpreter::thread_cpu_nanos(); let fill_before_nanos = shared_artifact_fill_cpu_nanos(); + let fill_wall_before_nanos = shared_artifact_fill_wall_nanos(); let outcome = run_claim(ctx, function); // CPU consumed by THIS (witness-eval) thread — the budget metric, so the completion-side // check matches the cooperative stride-poll and neither fires on cold-I/O or contention - // wall time. wall_nanos stays the measurement/receipt basis (unchanged). + // wall time. let measured_cpu_nanos = v1_interpreter::thread_cpu_nanos().saturating_sub(cpu_started_nanos); + // Sampled here rather than after the report so the wall clock can be split by the same rule + // the CPU clock is, and so the reported line and the enforced quantity read one binding. + let measured_wall_nanos = started.elapsed().as_nanos(); // SHARED-ARTIFACT FILL IS NOT THIS CLAIM'S MARGINAL COST (operator-line ruling, 2026-08-27). // Whatever this claim spent filling a memo is consumed by every later claim naming the same // source — one of them measured at literally 0ms in the same run because this one paid — so @@ -20498,7 +20537,18 @@ pub fn run_claim_measured( // the fill is still counted, still attributed and still visible in the receipt. let fill_cpu_nanos = shared_artifact_fill_cpu_nanos().saturating_sub(fill_before_nanos); let cpu_nanos = measured_cpu_nanos.saturating_sub(fill_cpu_nanos); - if fill_cpu_nanos > 0 { + // THE SAME SPLIT ON THE WALL CLOCK. `wall_budget_completion_outcome` below is a + // merge-blocking ceiling, so charging it the fill made it a function of execution order in + // exactly the way the ruling above forbids — and unlike the CPU side it had no exemption + // argument, only an omission. Split, never dropped: both halves are reported and they sum to + // `measured_wall_nanos`. + let fill_wall_nanos = shared_artifact_fill_wall_nanos().saturating_sub(fill_wall_before_nanos); + let wall_nanos = marginal_wall_nanos(measured_wall_nanos, fill_wall_nanos); + // EITHER clock, not the CPU one. A fill that blocked on I/O can spend wall time while + // charging almost no CPU, and under a `fill_cpu_nanos > 0` guard that fill would be + // subtracted from the enforced wall figure and reported nowhere — a cost dropped rather + // than split, which is the one thing the ruling above forbids. + if fill_cpu_nanos > 0 || fill_wall_nanos > 0 { // REPORTED, NOT ABSORBED. Printed on its own line, per claim, whenever a fill happened, // so the quantity the ceiling stops charging is visible at the same grain it was measured // — the difference between attributing a cost and losing one. `triggered_by` is this @@ -20507,13 +20557,16 @@ pub fn run_claim_measured( // this line names who paid. eprintln!( "[floor-shared-fill] claim={function} marginal_cpu_ms={} fill_cpu_ms={} \ - measured_cpu_ms={} provenance=filled-shared-artifact triggered_by={function}", + measured_cpu_ms={} marginal_wall_ms={} fill_wall_ms={} measured_wall_ms={} \ + provenance=filled-shared-artifact triggered_by={function}", cpu_nanos / 1_000_000, fill_cpu_nanos / 1_000_000, measured_cpu_nanos / 1_000_000, + wall_nanos / 1_000_000, + fill_wall_nanos / 1_000_000, + measured_wall_nanos / 1_000_000, ); } - let wall_nanos = started.elapsed().as_nanos(); ctx.clear_eval_deadline(); ctx.clear_wall_deadline(); v1_interpreter::eval_subject_clear(); @@ -20983,6 +21036,19 @@ fn budget_completion_outcome( } } +/// The claim's own wall cost: what it spent, less what it spent filling a shared artifact every +/// later claim naming the same source then reads free. +/// +/// Named rather than written inline at the one call site because it is the quantity +/// `wall_budget_completion_outcome` enforces against, and a merge-blocking ceiling's input +/// deserves a symbol its regression control can drive directly. The subtraction is saturating for +/// the same reason the CPU side's is: the two clocks are sampled at slightly different instants, +/// so a fill measured marginally longer than the enclosing claim is an artifact of sampling, not +/// a negative cost. +fn marginal_wall_nanos(measured_wall_nanos: u128, fill_wall_nanos: u128) -> u128 { + measured_wall_nanos.saturating_sub(fill_wall_nanos) +} + /// Whole-receipt wall budget for Wet self-host receipts: emit+cargo subprocess I/O /// counts against wall time, not CPU. A Pass over the wall budget converts to the same /// typed refusal — silent green would fail open on the nightly falsifier lane budget. @@ -21035,6 +21101,68 @@ mod budget_completion_tests { } } + #[test] + /// THE DISCRIMINATING RED FOR THE WALL HALF OF THE FILL-ATTRIBUTION RULING. A claim whose own + /// wall cost is 1ms, which happened to be the first to reach a shared memo and paid an 18000ms + /// fill for it, must not refuse a 10000ms wall requirement — every later claim naming the same + /// source reads that artifact free, so charging it here makes the ceiling a function of + /// execution order rather than of the tree. + /// + /// The second arm is what makes this a control rather than a restatement: driving the SAME + /// ceiling with the unsplit figure must still refuse. So the test fails if the split is + /// removed AND fails if the ceiling stops firing at all, which is the pair a single assertion + /// cannot carry. The shape is the one main run 33145062452 exhibited on + /// `test.claim.transport_script_wall_compile_red`, whose two rows reported + /// `marginal_cpu_ms=0`/`1` against `fill_cpu_ms` near 19000 and refused the required floor. + fn a_shared_fill_is_not_charged_to_the_claim_that_paid_it() { + let measured_wall_nanos = 18_001_000_000u128; + let fill_wall_nanos = 18_000_000_000u128; + let budget_ms = 10_000u64; + + assert!( + matches!( + wall_budget_completion_outcome( + Some(budget_ms), + ClaimOutcome::Pass, + marginal_wall_nanos(measured_wall_nanos, fill_wall_nanos), + ), + ClaimOutcome::Pass + ), + "a 1ms claim must not refuse a 10s ceiling because it paid an 18s shared fill" + ); + + match wall_budget_completion_outcome( + Some(budget_ms), + ClaimOutcome::Pass, + measured_wall_nanos, + ) { + ClaimOutcome::CompletedOverBudget { kind, .. } => { + assert_eq!(kind, BudgetKind::Wall); + } + other => panic!("the unsplit figure must still refuse, got {other:?}"), + } + } + + #[test] + /// The cost is SPLIT, never DROPPED (§5 — a vanished cost is the absorbing fallback wearing an + /// accounting label). Whatever the ceiling stops charging must still be recoverable, so the + /// two halves are asserted to sum to what the claim actually spent. + fn the_two_wall_halves_sum_to_the_measured_cost() { + let measured_wall_nanos = 18_001_000_000u128; + let fill_wall_nanos = 18_000_000_000u128; + assert_eq!( + marginal_wall_nanos(measured_wall_nanos, fill_wall_nanos) + fill_wall_nanos, + measured_wall_nanos + ); + } + + #[test] + /// A fill sampled marginally longer than the enclosing claim saturates to zero rather than + /// wrapping to an enormous cost that would refuse every ceiling. + fn a_fill_longer_than_the_claim_saturates() { + assert_eq!(marginal_wall_nanos(5, 9), 0); + } + #[test] fn pass_under_budget_stays_pass() { assert!(matches!( diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 538d3e8b2ac..144d8866ac8 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -2839,7 +2839,7 @@ impl InterpContext { pub fn arm_eval_deadline(&self, budget_ms: u64) { self.eval_deadline - .set(Some((thread_cpu_nanos(), budget_ms))); + .set(Some((budgeted_cpu_nanos(), budget_ms))); self.eval_deadline_stride.set(0); } @@ -2851,7 +2851,7 @@ impl InterpContext { /// `Some(0)` means already past. pub fn eval_deadline_remaining_ms(&self) -> Option { let (baseline, budget_ms) = self.eval_deadline.get()?; - let elapsed_ms = (thread_cpu_nanos().saturating_sub(baseline) / 1_000_000) as u64; + let elapsed_ms = (budgeted_cpu_nanos().saturating_sub(baseline) / 1_000_000) as u64; Some(budget_ms.saturating_sub(elapsed_ms)) } @@ -2898,7 +2898,7 @@ impl InterpContext { None => requested, }; self.eval_deadline - .set(Some((thread_cpu_nanos(), effective))); + .set(Some((budgeted_cpu_nanos(), effective))); self.eval_deadline_stride.set(0); } if let Some(requested) = wall_limit_ms { @@ -3657,6 +3657,163 @@ impl Drop for EvaluationBudgetScope<'_> { } } +thread_local! { + /// CPU SPENT FILLING SHARED MEMOIZED ARTIFACTS ON THIS THREAD, and it lives here rather than + /// beside the memos that record it because it now has TWO readers with opposite lifetimes: + /// the claim loop, which nets it at completion, and the evaluation deadline, which must net + /// it WHILE the claim runs. A counter with two homes is the §3 failure this whole line of + /// work exists to close, so there is one cell and `cli_run` delegates to it. + static SHARED_ARTIFACT_FILL_CPU_NANOS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// Accumulate CPU spent filling a shared memoized artifact. Called only from a memo MISS path. +pub fn record_shared_artifact_fill_cpu_nanos(nanos: u128) { + SHARED_ARTIFACT_FILL_CPU_NANOS.with(|c| c.set(c.get().saturating_add(nanos))); +} + +/// The running fill total for this thread. +pub fn shared_artifact_fill_cpu_nanos() -> u128 { + SHARED_ARTIFACT_FILL_CPU_NANOS.with(|c| c.get()) +} + +/// THE CLOCK EVERY CPU BUDGET IS MEASURED ON: thread CPU, less what this thread spent filling +/// shared artifacts that every later claim naming the same source reads free. +/// +/// WHY A CLOCK RATHER THAN A BASELINE THREADED THROUGH EACH DEADLINE. Both the arming instant and +/// the polling instant are read from this one function, so the fill accrued between them cancels +/// in the subtraction that already exists at every site — no deadline has to carry a second +/// baseline, and a site that forgets to net cannot arise, because there is nothing to forget. +/// +/// MONOTONE, which a deadline requires: fill is measured on this same thread clock inside the +/// miss path, so thread CPU rises by at least as much as fill does over any interval and the +/// difference never decreases. The saturating subtraction covers only the sampling skew between +/// the two reads. +/// +/// WHAT THIS DOES NOT REACH, named rather than left for a reader to discover: the WALL deadline +/// (`witness_wall_deadline`) is still armed on a raw `Instant` and still charges a fill to +/// whichever claim paid it. Every interruption in the population this repairs was on the CPU +/// clock — measured, 44 of 44 `Cpu` on run 33185280160 — so the wall half is a real and currently +/// unexercised residue, not a fix silently omitted. +pub fn budgeted_cpu_nanos() -> u128 { + thread_cpu_nanos().saturating_sub(shared_artifact_fill_cpu_nanos()) +} + +#[cfg(test)] +mod budgeted_cpu_clock_tests { + use super::*; + + /// THE DISCRIMINATING RED FOR THE DEADLINE HALF OF THE FILL-ATTRIBUTION RULING. A claim that + /// pays a shared fill must not have that fill counted against its own CPU deadline, because + /// every later claim naming the same source reads the artifact free — so charging it makes an + /// interrupt a function of discovery order rather than of the row. + /// + /// ASSERTED AS A DECREASE, NOT AS A RATIO AGAINST MEASURED WORK. An earlier version of this + /// test burned CPU in a spin loop, declared the measured cost a fill, and compared the clock's + /// advance against `burned / 2`. It failed on the remote runner because `burned` came back + /// ZERO — the thread CPU clock did not advance across the loop — which makes the right-hand + /// side 0 and the assertion unsatisfiable for an unsigned quantity. The test was measuring the + /// runner's clock granularity, not the netting. Recording a KNOWN fill removes the + /// measurement from the test entirely: the fill is an input rather than an observation. + #[test] + fn a_recorded_fill_moves_the_budget_clock_backwards_relative_to_raw_cpu() { + const FILL_NANOS: u128 = 1_000_000; + + // SPEND MORE CPU THAN THE FILL BEFORE RECORDING IT, because a fill larger than the thread's + // own CPU is a state production cannot reach — a fill is always measured FROM CPU actually + // spent inside the miss path, so it is bounded by the raw clock by construction. An earlier + // version recorded 1ms of fill against a fresh test thread that had not yet spent 1ms, + // saturated the clock to zero, and failed on an input the system cannot produce. That was a + // defect in the test's premise, not in the netting. + let mut spin = 0u64; + let mut rounds = 0u32; + while thread_cpu_nanos() < FILL_NANOS * 4 && rounds < 100_000 { + for i in 0..100_000u64 { + spin = spin.wrapping_add(i); + } + rounds += 1; + } + std::hint::black_box(spin); + assert!( + thread_cpu_nanos() >= FILL_NANOS * 4, + "could not accumulate {}ns of thread CPU in {rounds} rounds; the clock is not running \ + and nothing below would be measuring the netting", + FILL_NANOS * 4 + ); + + let budgeted_before = budgeted_cpu_nanos(); + record_shared_artifact_fill_cpu_nanos(FILL_NANOS); + let budgeted_after = budgeted_cpu_nanos(); + + // THE DISCRIMINATING ASSERTION. Raw CPU only ever rises, so a budget clock that moved + // BACKWARDS across a recorded fill can only have subtracted it. Without the netting the + // two clocks move together and this fails. + assert!( + budgeted_after < budgeted_before, + "recording a {FILL_NANOS}ns fill must move the budget clock backwards: \ + {budgeted_before} -> {budgeted_after}" + ); + + // AND THE EXACT RELATION, BRACKETED RATHER THAN DIFFERENCED. An earlier version asserted + // `budget_drop + raw_advance == FILL`, which is false: those two deltas span different + // overlapping intervals, so the CPU elapsed in one does not cancel the CPU elapsed in the + // other. Bracketing needs no tolerance and no assumption about clock speed — `budgeted + + // fill` IS a raw CPU reading, so it must fall between two raw reads taken either side. + let lo = thread_cpu_nanos(); + let budgeted = budgeted_cpu_nanos(); + let hi = thread_cpu_nanos(); + let reconstructed = budgeted + shared_artifact_fill_cpu_nanos(); + assert!( + reconstructed >= lo && reconstructed <= hi, + "budgeted + fill ({reconstructed}) must be a raw CPU reading taken between {lo} and {hi}" + ); + } + + /// THE CONTROL THAT KEEPS THE ONE ABOVE HONEST: a clock that simply froze would satisfy + /// "fills do not advance it" perfectly and disarm every deadline in the process. So ordinary + /// CPU — nothing recorded as a fill — must still move it. + /// + /// The loop runs until the RAW clock is observed to advance rather than for a fixed count, + /// because a fixed count is what made the previous version of this test depend on the + /// runner's clock granularity. The cap makes a never-advancing thread CPU clock a loud + /// failure rather than a hang. + #[test] + fn ordinary_work_still_advances_the_budget_clock() { + let raw_start = thread_cpu_nanos(); + let budgeted_start = budgeted_cpu_nanos(); + + let mut spin = 0u64; + let mut rounds = 0u32; + while thread_cpu_nanos() == raw_start && rounds < 10_000 { + for i in 0..100_000u64 { + spin = spin.wrapping_add(i); + } + rounds += 1; + } + std::hint::black_box(spin); + + assert!( + thread_cpu_nanos() > raw_start, + "the thread CPU clock never advanced across {rounds} rounds of work; the budget \ + clock cannot be tested against a clock that does not run" + ); + assert!( + budgeted_cpu_nanos() > budgeted_start, + "ordinary work must advance the budget clock, or every CPU deadline is disarmed" + ); + } + + /// The clock a deadline is armed on must never run backwards, or an armed deadline could + /// stop being reachable. Fill is measured on the same thread clock inside the miss path, so + /// the difference is monotone; this pins the saturating floor that covers sampling skew. + #[test] + fn the_budget_clock_never_runs_backwards_even_if_fill_overshoots() { + let before = budgeted_cpu_nanos(); + record_shared_artifact_fill_cpu_nanos(u128::MAX / 2); + assert!(budgeted_cpu_nanos() <= before); + assert_eq!(budgeted_cpu_nanos(), 0, "saturates rather than wrapping"); + } +} + pub fn thread_cpu_nanos() -> u128 { #[cfg(unix)] { @@ -3699,7 +3856,7 @@ fn eval_expr(node: &Rc, env: &Rc, ctx: &InterpContext) -> InterpResul ctx.eval_deadline_stride.set(stride); if stride % 4096 == 0 { if let Some((cpu_baseline_nanos, budget_ms)) = cpu_armed { - let elapsed_nanos = thread_cpu_nanos().saturating_sub(cpu_baseline_nanos); + let elapsed_nanos = budgeted_cpu_nanos().saturating_sub(cpu_baseline_nanos); if (elapsed_nanos / 1_000_000) as u64 > budget_ms { return Err(InterpError::EvaluationBudgetExceeded { entry: ctx.budget_entry_or_unnamed(), diff --git a/src/v2/workflow/floor_cost_debt.dag b/src/v2/workflow/floor_cost_debt.dag index e0efaec160d..4d92b1a6cb0 100644 --- a/src/v2/workflow/floor_cost_debt.dag +++ b/src/v2/workflow/floor_cost_debt.dag @@ -4,7 +4,7 @@ import v2.std.algebra { Cons, Empty, list_flat_map, any } import v2.std.collection { List } import v2.std.text { String } -// THE OVER-COST WITHHOLDING MECHANISM, CARRYING 276 IDENTITIES. +// THE OVER-COST WITHHOLDING MECHANISM, CARRYING 274 IDENTITIES. // // The count is stated once, here, and derived nowhere else in this file's prose. An earlier // revision of this header read "CURRENTLY CARRYING NO IDENTITIES" -- true of the empty carrier @@ -176,7 +176,7 @@ import v2.std.text { String } // Nothing here needs to be re-measured to release a row, and nothing may be added to release // one. -// THE ROSTER CARRIES 276 IDENTITIES, IN TWO POPULATIONS THAT ARE ENROLLED FOR DIFFERENT REASONS +// THE ROSTER CARRIES 274 IDENTITIES, IN TWO POPULATIONS THAT ARE ENROLLED FOR DIFFERENT REASONS // AND MUST NOT BE READ AS ONE. // // POPULATION A -- 225 EXPENSIVE PASSES. Claims that REACHED A VERDICT, PASSED, and whose measured @@ -527,7 +527,7 @@ fn floor_cost_debt_proven_chunk_11() -> List { } fn floor_cost_debt_censored_chunk_00() -> List { - Cons { head: "dag.test.claim.lifecycle_survivor_corpus_census.no_raw_lifecycle_string_survivors_remain", tail: Cons { head: "gunbc.test.claim.realization_attempt_keystone_test.witness_real_corpus_entry_produces_typed_standing", tail: Cons { head: "gunbc.test.claim.witness_execution_class_live_census_test.witness_live_roster_census_holds", tail: Cons { head: "test.claim.transport_script_wall_compile_red.wall_red_deleted_free_minter_refuses", tail: Cons { head: "test.claim.transport_script_wall_compile_red.wall_red_string_join_into_retained_record_refuses", tail: Cons { head: "v2.lens.no_dual_representation_test.no_dual_representation_test_clean_holds", tail: Cons { head: "v2.lens.no_dual_representation_test.no_dual_representation_test_coverage_honesty_holds", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate_lean_bash.roster_bash_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate_lean_bash.roster_lean_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_compile_stage_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_glob_discovery_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_lens_cost_model_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_lens_traversal_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_machine_code_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_std_algebra_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate_std.roster_std_change_dag_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate_swift.roster_swift_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_corpus_roster_nonempty", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_dag_tree_receipt_by_execution", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_decl_facts_roster_smoke_totality_holds", tail: Empty {} } } } } } } } } } } } } } } } } } } } } + Cons { head: "dag.test.claim.lifecycle_survivor_corpus_census.no_raw_lifecycle_string_survivors_remain", tail: Cons { head: "gunbc.test.claim.realization_attempt_keystone_test.witness_real_corpus_entry_produces_typed_standing", tail: Cons { head: "gunbc.test.claim.witness_execution_class_live_census_test.witness_live_roster_census_holds", tail: Cons { head: "v2.lens.no_dual_representation_test.no_dual_representation_test_clean_holds", tail: Cons { head: "v2.lens.no_dual_representation_test.no_dual_representation_test_coverage_honesty_holds", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate_lean_bash.roster_bash_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate_lean_bash.roster_lean_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_compile_stage_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_glob_discovery_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_lens_cost_model_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_lens_traversal_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_machine_code_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate.roster_std_algebra_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate_std.roster_std_change_dag_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.complexity.accumulator_copy_roster_gate_swift.roster_swift_zero_suspects_within_ratchet", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_corpus_roster_nonempty", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_dag_tree_receipt_by_execution", tail: Cons { head: "v2.test.claim.enforcement.cost_coverage_witness.cost_coverage_decl_facts_roster_smoke_totality_holds", tail: Empty {} } } } } } } } } } } } } } } } } } } } fn floor_cost_debt_censored_chunk_01() -> List { @@ -569,7 +569,27 @@ fn floor_cost_debt_censored_chunk_02() -> List { // clock as it already is for CPU; when that lands these two become cheap rather than invisible and // their rows should be DELETED, which is the ordinary shrink this contract is built for. // -// THEY ARE NOT DROPPED PRE-EMPTIVELY, because today the floor enforces on the unadjusted wall and +// THAT SHRINK HAS NOW HAPPENED, AND THIS IS IT. gunbc#9609 nets shared-artifact fill out of the +// wall clock exactly as it already is for CPU, so the two rows below are cheap rather than +// invisible and they are DELETED from this roster by the exit path the paragraph above +// prescribes. Their own marginal work was 0ms and 1ms; what withholding was hiding was an +// accounting artifact, not a cost. Removing them returns them to ordinary execution with no +// disposition attached -- verified before the edit: neither identity appears in +// `v2.workflow.floor_expected_red`, `v2.workflow.floor_route_gap` or +// `v2.workflow.floor_non_verdict`, so nothing dormant wakes with them (`suppress_withheld` +// applies to all three rosters, and a deletion here makes any enrolment there observable again). +// +// AND THAT IS WHY THEY HAD TO LEAVE RATHER THAN SIMPLY BEING FIXED. While they sat here the +// floor reported `completed_over_cost_requirement=0` because they DID NOT RUN, which is +// indistinguishable in that counter from the repair having worked. The fix's own discriminating +// evidence -- those two rows reaching a verdict where they previously refused -- is only +// observable once they are back in the plan. +// +// THE PARAGRAPH BELOW IS KEPT IN THE PAST TENSE rather than deleted, because it is the reasoning +// that made the pre-emptive drop wrong at the time and it is what a later reader needs in order +// to tell this shrink from a premature one. +// +// THEY WERE NOT DROPPED PRE-EMPTIVELY, because at the time the floor enforced on the unadjusted wall and // would refuse on them. Dropping a row whose cost is an artifact is right only once the artifact is // gone; dropping it while the gate still reads the artifact just moves the red. What is corrected // here is the CLAIM, not the membership -- an earlier revision described the whole roster as rows