Skip to content

feat(verify): per-path use-range analysis + intra-fn verifier (C3)#21

Merged
hyperpolymath merged 1 commit into
mainfrom
feat/typed-wasm-verify-c3-intra-fn
May 15, 2026
Merged

feat(verify): per-path use-range analysis + intra-fn verifier (C3)#21
hyperpolymath merged 1 commit into
mainfrom
feat/typed-wasm-verify-c3-intra-fn

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Summary

  • Ports Tw_verify.count_uses_range + verify_function + verify_from_module from hyperpolymath/affinescript/lib/tw_verify.ml.
  • Replaces C1's todo!() stub: verify_from_module(wasm_bytes) now actually enforces L7 (aliasing) and L10 (linearity).
  • 29/29 tests pass (10 algorithmic + 9 end-to-end + 10 inherited from C1/C2).

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 Meaning
Plain { min, max } Block, Loop, or the implicit body scope
IfThen { then_min, then_max } If before any Else is seen
IfElse { then_min, then_max, else_min, else_max } If after Else (then-side frozen)

Transitions:

Block / Loop      → push Plain
If                → push IfThen
Else              → IfThen → IfElse (freeze then-totals)
End               → pop, collapse, add to parent:
                      Plain                 → (min,  max)
                      IfThen, no else seen  → (0,    then_max)
                      IfElse                → (min(t,e), max(t,e))
LocalGet n        → if n matches, add (1,1) to top frame's active side
Other             → no-op

The per-counter logic sits behind a private OpCounter trait so C4 can reuse the same machinery with a Call-based counter.

Per-function rules (verbatim port of OCaml verify_function)

Kind Rule
Linear max=0LinearNotUsed; min=0, max≥1LinearDroppedOnSomePath; max>1LinearUsedMultiple. Both drop+dup can fire for min=0, max>1.
ExclBorrow max>1ExclBorrowAliased
Unrestricted / SharedBorrow no constraints

Module-level entry

verify_from_module(wasm_bytes):

  1. Single wasmparser pass over the module:
    • Tally import_count for function-index translation
    • Capture affinescript.ownership custom section (if any)
    • Collect every Payload::CodeSectionEntry body in order
  2. No section ⇒ trivially Ok(())
  3. Parse the section (using C2's parse_ownership_section_payload)
  4. For each entry: translate global func_idx to body index by subtracting import_count; skip imports + out-of-range entries (matches OCaml short-circuits)
  5. Aggregate per-fn violations into VerifyError::Ownership(Vec<OwnershipError>) or return Ok(())

Tests

$ cargo test -p typed-wasm-verify
running 29 tests
... (10 section + 1 kind + 18 verify) ...
test result: ok. 29 passed; 0 failed; 0 ignored

Coverage:

  • count_uses_range layer — every transition (no uses, one use, two same-path, both-branches, then-only, asymmetric-counts, Block passthrough, Loop passthrough, nested-If-inner-then-only)
  • verify_from_module end-to-end — every error variant + clean paths (Linear×{used-once,not-used,dropped,used-twice}, ExclBorrow×{once,twice}, Unrestricted unconstrained, no-section, empty-module)

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 main automatically as the upstream merges.

Follow-up

  • C4 — port the cross-module boundary verifier (same (min,max) frame stack applied to Call operators)
  • C5 — cross-compat regression test against affinescript-emitted modules
  • C6/C7 — hyperpolymath/ephapax consumer wiring

Closes #38 once merged.

@hyperpolymath hyperpolymath force-pushed the feat/typed-wasm-verify-c2-section-parser branch from 079ff80 to 88fcf34 Compare May 15, 2026 06:00
@hyperpolymath hyperpolymath force-pushed the feat/typed-wasm-verify-c3-intra-fn branch from 2283161 to 63b1880 Compare May 15, 2026 06:00
@hyperpolymath hyperpolymath changed the base branch from feat/typed-wasm-verify-c2-section-parser to main May 15, 2026 06:03
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>
@hyperpolymath hyperpolymath force-pushed the feat/typed-wasm-verify-c3-intra-fn branch from 63b1880 to 103db89 Compare May 15, 2026 06:04
@hyperpolymath hyperpolymath merged commit e11bb98 into main May 15, 2026
19 of 28 checks passed
@hyperpolymath hyperpolymath deleted the feat/typed-wasm-verify-c3-intra-fn branch May 15, 2026 06:04
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