feat(verify): per-path use-range analysis + intra-fn verifier (C3)#21
Merged
Merged
Conversation
079ff80 to
88fcf34
Compare
2283161 to
63b1880
Compare
Ports `Tw_verify.count_uses_range` + `verify_function` + `verify_from_module`
from hyperpolymath/affinescript/lib/tw_verify.ml. After this commit the
C1 `verify_from_module` stub is gone — calling it on a wasm module that
carries an `affinescript.ownership` custom section actually enforces the
L7 (aliasing) and L10 (linearity) constraints.
The algorithm
-------------
OCaml walks an in-memory instruction tree recursively. wasmparser hands
us a flat operator stream with structured-control delimiters, so we run
the same per-path `(min, max)` algorithm with an explicit frame stack:
Frame::Plain — Block, Loop, or the implicit body scope
Frame::IfThen — `If` before any `Else` is seen
Frame::IfElse — `If` after `Else`; then-side totals are frozen
Frame transitions on the structured-control operators:
Block / Loop → push Plain
If → push IfThen
Else → top must be IfThen; transition to IfElse,
freezing the then-side totals
End → pop, collapse to (m, x), add into parent
Plain → (min, max)
IfThen, no else seen → (0, then_max)
IfElse → (min(t,e), max(t,e))
LocalGet n → if n matches the counter, add (1, 1) to the
top frame's currently-active side
Anything else → no-op
The function body's terminating `End` pops the bottom frame and the
collapse becomes the final result.
The frame state is split out as a private `OpCounter` trait so C4 can
reuse the exact same machinery with a `Call`-based counter for
cross-module verification.
Per-function rules (mirroring OCaml `verify_function`)
------------------------------------------------------
For each param at index `i`, compute `(min_uses, max_uses)` then apply:
Linear:
max == 0 → LinearNotUsed
min == 0, max ≥ 1 → LinearDroppedOnSomePath
max > 1 → LinearUsedMultiple { count: max }
(both "drop" and "dup" can fire for the same param if min=0, max>1)
ExclBorrow:
max > 1 → ExclBorrowAliased { count: max }
Unrestricted | SharedBorrow: no constraints
Module-level entry
------------------
`verify_from_module(wasm_bytes)`:
1. Single wasmparser pass over the module:
- Tally import_count for the function index space
- Capture the `affinescript.ownership` custom section (if any)
- Collect every `Payload::CodeSectionEntry` body in order
2. If no ownership section: trivially `Ok(())`.
3. Parse the section (C2's `parse_ownership_section_payload`).
4. For each entry, translate `func_idx` (global) to a body index by
subtracting `import_count`. Skip imports (no body) and out-of-
range entries — matches OCaml's short-circuit behaviour.
5. Aggregate all per-function violations into one `VerifyError::Ownership`
vector, or return `Ok(())` if clean.
Tests
-----
29/29 unit tests pass:
count_uses_range layer (range_in helper synthesises a 1-fn module
via wasm-encoder and pulls the body out for direct analysis):
no_uses → (0, 0)
one_use → (1, 1)
two_uses_same_path → (2, 2)
use_in_both_if_branches → (1, 1)
use_in_then_only → (0, 1)
use_twice_in_then_once_in_else → (1, 2)
use_inside_block_passthrough → (1, 1)
use_inside_loop_passthrough → (1, 1)
nested_if_use_in_inner_then_only → (0, 1)
verify_from_module end-to-end (synthetic modules with ownership
custom sections, hitting each error variant + the clean paths):
linear_used_exactly_once_is_clean
linear_not_used_at_all_errors → LinearNotUsed
linear_dropped_on_some_path_errors → LinearDroppedOnSomePath
linear_used_twice_errors → LinearUsedMultiple
excl_borrow_used_twice_errors → ExclBorrowAliased
excl_borrow_used_once_is_clean
unrestricted_used_arbitrarily_is_clean
module_without_ownership_section_is_trivially_clean
empty_module_is_trivially_clean
$ cargo test -p typed-wasm-verify
running 29 tests
... all pass ...
test result: ok. 29 passed; 0 failed; 0 ignored
Stacked on top of #20 (C2 section codec). Next: C4 — port the
cross-module boundary verifier (the same `(min, max)` frame stack
applied to `Call` operators against a callee's exported interface).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
63b1880 to
103db89
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.
Summary
Tw_verify.count_uses_range+verify_function+verify_from_modulefromhyperpolymath/affinescript/lib/tw_verify.ml.todo!()stub:verify_from_module(wasm_bytes)now actually enforces L7 (aliasing) and L10 (linearity).The algorithm
OCaml walks an in-memory instruction tree recursively. wasmparser hands us a flat operator stream with structured-control delimiters, so we run the same per-path
(min, max)algorithm with an explicit frame stack:Plain { min, max }IfThen { then_min, then_max }Ifbefore anyElseis seenIfElse { then_min, then_max, else_min, else_max }IfafterElse(then-side frozen)Transitions:
The per-counter logic sits behind a private
OpCountertrait so C4 can reuse the same machinery with aCall-based counter.Per-function rules (verbatim port of OCaml
verify_function)Linearmax=0→LinearNotUsed;min=0, max≥1→LinearDroppedOnSomePath;max>1→LinearUsedMultiple. Both drop+dup can fire formin=0, max>1.ExclBorrowmax>1→ExclBorrowAliasedUnrestricted/SharedBorrowModule-level entry
verify_from_module(wasm_bytes):import_countfor function-index translationaffinescript.ownershipcustom section (if any)Payload::CodeSectionEntrybody in orderOk(())parse_ownership_section_payload)func_idxto body index by subtractingimport_count; skip imports + out-of-range entries (matches OCaml short-circuits)VerifyError::Ownership(Vec<OwnershipError>)or returnOk(())Tests
Coverage:
Stacking
Stacked on top of #20 (C2 section codec) → on top of #19 (C1 scaffold). Merge order: #19 → #20 → this. Each downstream PR's base retargets to
mainautomatically as the upstream merges.Follow-up
(min,max)frame stack applied toCalloperators)hyperpolymath/ephapaxconsumer wiringCloses #38 once merged.