fix(verify): #347 bound array-modelled memory operations, and refuse what cannot be verified - #350
Merged
Conversation
…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
force-pushed
the
fix/346-output-validation
branch
from
August 21, 2026 18:02
3c51744 to
25e2628
Compare
avrabe
force-pushed
the
fix/347-bound-memory-ops
branch
from
August 21, 2026 18:02
2d7ef52 to
551a37d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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: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_tableall 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_functionsand 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
Okwould 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:
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 --lib539/539 (default andboth) · 410/410--no-default-features·--test verification47/47 ·loom-cli21/21.Verifies
TEST-347-MEMORY-OP-BOUND