Skip to content

fix(verify): #347 bound array-modelled memory operations, and refuse what cannot be verified - #350

Merged
avrabe merged 1 commit into
fix/346-output-validationfrom
fix/347-bound-memory-ops
Aug 22, 2026
Merged

fix(verify): #347 bound array-modelled memory operations, and refuse what cannot be verified#350
avrabe merged 1 commit into
fix/346-output-validationfrom
fix/347-bound-memory-ops

Conversation

@avrabe

@avrabe avrabe commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Stacked on #349#344. Bases retarget as each merges.

Closes the shipping blocker in #347. 463 KB fused module: >455 s (killed) → 45 s, exit 0, output valid.

Root cause — measured, and not what I expected

Bisected to one pass (every other pass ≤1s), then confirmed it was the verification not the transform (LOOM_Z3_MAX_INSTRUCTIONS=1 → 1s). Then profiled:

14172  loom_core::optimize::inline_functions
13725   TranslationValidator::verify -> Z3_solver_check      97%
 5290    smt::theory_array_base::propagate                   40%
 3849     assert_store_axiom2_core

The solver's array theory — the memory model — instantiating store axioms pairwise, so cost is quadratic in memory accesses per body. Only the inliner triggers it, for a structural reason: it's the one pass that concatenates callee bodies into a caller, so the one pass that multiplies memory accesses per function. That's why size, functions, blocks, loops and br_table all failed to discriminate — the 39s control exceeds the hanging module on every one.

I went in with a different hypothesis — the acyclic-CF executor from #219, which runs only for inline_functions and fit the evidence perfectly. The profile disproved it. A fix for that would have shipped and changed nothing.

Two decisions, both reached by measurement

Exceeding the bound REVERTS, unlike the instruction bound. Returning Ok would accept a transform nothing verified — thousands per module at this bound. Refusing costs optimization and costs nothing in safety. The instruction bound still keeps; that inconsistency is left visible rather than quietly harmonised.

Tiny bodies are exempt, and that is not a nicety. The naive bound broke #219:

required bound
#219 seam dissolution ≥ 4
#347 module completes ≤ 2

Irreconcilable. The first implementation silently regressed shipped, silicon-validated seam work — caught only because those tests exist. Exempting bodies under an instruction floor resolves it, since the seam inlines are tiny and the pathological ones are inlined-large. Result: 45s, seams intact, and slightly better coverage than without the exemption (49.8% vs 45.2%).

What this is not

Not a solution. It buys termination by declining the hardest obligations. They're discharged only when the memory model stops being the incumbent's array theory — #313 slice 5.

Not a timeout, and not for want of trying. The solver's timeout is wired (LOOM_Z3_TIMEOUT_MS, default 5000) and measurably does not bound this: at 100 ms the pass still exceeded 120 s, because the time goes into axiom instantiation, not the search a timeout guards. A wall-clock budget is also ruled out on principle — it would make which functions got verified depend on machine speed, so the same input could produce different output (REQ-14).

Measured cost on a normal module

0.47% larger output; 21% of the optimization gain foregone. Stated rather than buried — this is a real regression traded for 97/100 components becoming usable at all.

Tests

Counter correctness (including through block/if bodies, so stores can't hide under the bound), the float/partial-width exclusion, plus the exemption and its control — a large memory-dense body must not be exempt, without which the bound could never fire.

loom-core --lib 539/539 (default and both) · 410/410 --no-default-features · --test verification 47/47 · loom-cli 21/21.

Verifies TEST-347-MEMORY-OP-BOUND

…what cannot be verified

97 of 100 real-world components exceeded a 60 s budget. One 463 KB
meld-fused module ran past 455 s and was killed, while a structurally
DENSER 1.13 MB ordinary module finished in 39 s — so it is not size,
functions, blocks, loops or br_table, all of which the control exceeds.

Root-caused by profiling, after bisecting the hang to a single pass:

    14172  loom_core::optimize::inline_functions
    13725   TranslationValidator::verify -> Z3_solver_check      97%
     5290    smt::theory_array_base::propagate                   40%
     3849     assert_store_axiom2_core

The cost is the solver's ARRAY theory — the memory model — instantiating
store axioms PAIRWISE, so it grows quadratically in memory accesses per
body. Only the inliner triggers it, for a structural reason: it is the
one pass that concatenates callee bodies into a caller, and so the one
pass that multiplies memory accesses per function.

`LOOM_Z3_MAX_INSTRUCTIONS` bounds instruction count, which does not track
that quantity at all. This bounds the one that does.

I had a different hypothesis going in — the acyclic-CF executor from
#219, which runs ONLY for inline_functions and fit the evidence
perfectly. The profile disproved it. Worth recording, because a fix for
that would have shipped and changed nothing.

--- exceeding the bound REVERTS, unlike the instruction bound ---

Returning Ok would accept a transform nothing verified, and at this bound
that ships thousands of unproven transforms per module — the wrong
direction for a charter whose rule is to skip rather than risk. Refusing
costs optimization and costs nothing in safety, which is what makes a
bound this aggressive defensible. The instruction bound still keeps;
that inconsistency is left visible rather than quietly harmonised, since
changing long-shipped behaviour belongs in its own change.

Reverts are attributed (`<pass>/memory-ops-over-threshold`) and recorded
in ONE place — recording at the refusal site as well would count each
revert twice, which is the same class of defect as the mislabelled
counter this branch series just fixed.

--- tiny bodies are exempt, and that is not a nicety ---

The naive bound broke #219. Measured: the seam-dissolution tests need a
bound >= 4; the 463 KB module needs <= 2. Irreconcilable — and the first
implementation silently regressed a shipped, silicon-validated
capability, caught only because those tests exist.

Exempting bodies at or below an instruction floor resolves it, because
the seam inlines are tiny and the pathological ones are inlined-large.
With the exemption: 45 s (from >455 s killed), seams intact, and
slightly BETTER coverage than without it (49.8% vs 45.2%).

--- what this is not ---

Not a solution. It buys termination by declining the hardest
obligations; they are discharged only when the memory model stops being
the incumbent's array theory (#313 slice 5).

Not a timeout, and not for want of trying: the solver's timeout IS wired
(LOOM_Z3_TIMEOUT_MS, default 5000) and measurably does not bound this —
at 100 ms the pass still exceeded 120 s, because the time goes into
axiom instantiation, not the search a timeout guards. A wall-clock
budget is also ruled out on principle: it would make which functions got
verified depend on machine speed, so the same input could produce
different output (REQ-14).

Measured cost on a normal module: 0.47% larger output, 21% of the
optimization gain foregone.

loom-core --lib 539/539 (default AND both-mode), 410/410
--no-default-features, --test verification 47/47, loom-cli 21/21.

Verifies TEST-347-MEMORY-OP-BOUND
Refs #347, #219, #313, #331

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZof1M5HMBSYVZPeoT4MEn
@avrabe
avrabe force-pushed the fix/346-output-validation branch from 3c51744 to 25e2628 Compare August 21, 2026 18:02
@avrabe
avrabe force-pushed the fix/347-bound-memory-ops branch from 2d7ef52 to 551a37d Compare August 21, 2026 18:02
@avrabe
avrabe merged commit c47908f into fix/346-output-validation Aug 22, 2026
@avrabe
avrabe deleted the fix/347-bound-memory-ops branch August 22, 2026 08:47
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