11(* SPDX-License-Identifier: PMPL-1.0-or-later *)
22(* SPDX-FileCopyrightText: 2024-2026 hyperpolymath *)
33
4- (* * typed-wasm ownership verifier — Stage 7.
4+ (* * typed-wasm ownership verifier — Stage 7 (per-path analysis added Stage 9) .
55
66 Statically verifies typed-wasm Level 7 (aliasing safety) and Level 10
77 (linearity) constraints on AffineScript's Wasm IR.
1313 re-checks them on the emitted Wasm IR to catch any codegen bugs.
1414
1515 Level 10 — Linearity: a parameter annotated [Linear] (TyOwn) must be
16- loaded exactly once in the function body. Loading zero times means the
17- owned value is dropped; loading more than once means it may be duplicated.
16+ loaded exactly once on every execution path. Zero uses = dropped;
17+ more than one use = possible duplication; non-zero use on some paths
18+ but zero on others = partial drop (dropped on the zero-count path).
1819
1920 Level 7 — Aliasing safety: a parameter annotated [ExclBorrow] (TyMut)
20- must be loaded at most once. Multiple loads create aliased references to
21- the same mutable memory, violating exclusive-borrow invariants.
21+ must be loaded at most once on any execution path.
2222
23- [SharedBorrow] (TyRef) and [Unrestricted] params are not checked by this
24- pass (no constraints violated by multiple reads of a shared reference).
23+ [SharedBorrow] (TyRef) and [Unrestricted] params are not checked.
2524
26- Branch semantics: for [If] instructions, we take the {b max} of the
27- use-counts across the two branches (since only one branch executes at
28- runtime). This is conservative: a linear param used exactly once in
29- each branch of an if/else is counted as 1 (correct), not 2. Stage 8
30- will refine this with a per-path analysis if needed.
25+ Branch semantics (per-path min/max): for [If] instructions we compute
26+ [(min_then, max_then)] and [(min_else, max_else)] independently and
27+ combine as [(min_then, max_then, min_else, max_else) →
28+ (min min_then min_else, max max_then max_else)]. A Linear param used
29+ exactly once in BOTH branches gives (1, 1) — OK. A param used in the
30+ then-branch only gives (min=0, max=1) — [LinearDroppedOnSomePath].
3131
3232 @see typed-wasm LEVEL-STATUS.md — Level 7 and Level 10 sections.
3333*)
@@ -41,37 +41,57 @@ open Codegen
4141(* * An ownership violation found in a Wasm function body. *)
4242type ownership_error =
4343 | LinearNotUsed of { func_idx : int ; param_idx : int }
44- (* * Level 10: Linear parameter was never loaded — dropped without consumption.
45- The owned resource leaks. *)
44+ (* * Level 10: Linear parameter was never loaded on any path — dropped without
45+ consumption. The owned resource leaks unconditionally. *)
46+ | LinearDroppedOnSomePath of { func_idx : int ; param_idx : int }
47+ (* * Level 10: Linear parameter is consumed on some execution paths but
48+ silently dropped on others (per-path min_uses = 0, max_uses >= 1).
49+ The caller's ownership guarantee is satisfied only conditionally —
50+ the drop path leaks the resource. *)
4651 | LinearUsedMultiple of { func_idx : int ; param_idx : int ; count : int }
47- (* * Level 10: Linear parameter was loaded [count] times — potential duplication.
48- Only one consumer is permitted. *)
52+ (* * Level 10: Linear parameter was loaded [count] times on some path —
53+ potential duplication. Only one consumer is permitted. *)
4954 | ExclBorrowAliased of { func_idx : int ; param_idx : int ; count : int }
5055 (* * Level 7: ExclBorrow parameter was loaded [count] times — aliasing violation.
5156 An exclusive borrow must not have multiple simultaneous references. *)
5257
5358(* ============================================================================
54- Use-count analysis
59+ Per-path use-range analysis
5560 ============================================================================ *)
5661
57- (* * Count how many times [local_idx] is loaded inside [instrs].
62+ (* * Compute [(min_uses, max_uses)] — the minimum and maximum number of times
63+ [local_idx] is loaded across all execution paths within [instrs].
5864
59- For [If] nodes the branch counts are combined with [max] rather than
60- summed, reflecting that only one branch executes. All other nodes are
61- summed. Nested [Block] and [Loop] bodies are descended into. *)
62- let rec count_uses (local_idx : int ) (instrs : Wasm.instr list ) : int =
63- List. fold_left (fun acc instr -> acc + uses_in_instr local_idx instr) 0 instrs
65+ Sequential instructions sum their ranges: if instr A uses the param
66+ (1, 1) times and instr B uses it (0, 1) times, the sequence is (1, 2).
6467
65- and uses_in_instr (local_idx : int ) (instr : Wasm.instr ) : int =
68+ For [If] nodes only one branch executes, so we take the component-wise
69+ min/max of the two branch ranges:
70+ - [min = min(then_min, else_min)]: may be zero if one branch doesn't use it
71+ - [max = max(then_max, else_max)]: the worst-case duplication count
72+
73+ Example: [If { LocalGet 0 } { LocalGet 0 }] → [(1, 1)] OK for Linear.
74+ Example: [If { LocalGet 0 } { [] }] → [(0, 1)] LinearDroppedOnSomePath.
75+ Example: [If { LocalGet 0; LocalGet 0 } { LocalGet 0 }] → [(1, 2)]
76+ → LinearUsedMultiple (max = 2).
77+
78+ Nested [Block] and [Loop] bodies are descended into (no new paths). *)
79+ let rec count_uses_range (local_idx : int ) (instrs : Wasm.instr list ) : int * int =
80+ List. fold_left (fun (a_min , a_max ) instr ->
81+ let (i_min, i_max) = uses_range_in_instr local_idx instr in
82+ (a_min + i_min, a_max + i_max)
83+ ) (0 , 0 ) instrs
84+
85+ and uses_range_in_instr (local_idx : int ) (instr : Wasm.instr ) : int * int =
6686 match instr with
67- | Wasm. LocalGet n -> if n = local_idx then 1 else 0
87+ | Wasm. LocalGet n -> if n = local_idx then ( 1 , 1 ) else ( 0 , 0 )
6888 | Wasm. Block (_ , body ) | Wasm. Loop (_ , body ) ->
69- count_uses local_idx body
89+ count_uses_range local_idx body
7090 | Wasm. If (_ , then_ , else_ ) ->
71- (* Only one branch executes: take max so that
72- If { then: LocalGet 0 } { else: LocalGet 0 } counts as 1, not 2. *)
73- max (count_uses local_idx then_) (count_uses local_idx else_ )
74- | _ -> 0
91+ let (t_min, t_max) = count_uses_range local_idx then_ in
92+ let (e_min, e_max) = count_uses_range local_idx else_ in
93+ (min t_min e_min, max t_max e_max )
94+ | _ -> ( 0 , 0 )
7595
7696(* ============================================================================
7797 Per-function verification
@@ -82,27 +102,39 @@ and uses_in_instr (local_idx : int) (instr : Wasm.instr) : int =
82102 [func] is the Wasm function body.
83103 [param_kinds] are the ownership annotations for each parameter (in order).
84104 [func_idx] is the global function index (for error reporting).
85- Returns all violations found (empty list = clean). *)
105+ Returns all violations found (empty list = clean).
106+
107+ Uses per-path min/max analysis:
108+ - [LinearNotUsed]: max_uses = 0 (param dropped on every path)
109+ - [LinearDroppedOnSomePath]: min_uses = 0, max_uses >= 1 (dropped conditionally)
110+ - [LinearUsedMultiple]: max_uses > 1 (duplicated on some path)
111+ Multiple violations can be reported for a single param (e.g. both
112+ [LinearDroppedOnSomePath] and [LinearUsedMultiple] if min=0, max>1). *)
86113let verify_function
87114 (func : Wasm.func )
88115 (param_kinds : ownership_kind list )
89116 (func_idx : int )
90117 : ownership_error list =
91118 List. concat_map (fun (param_idx , kind ) ->
92- let uses = count_uses param_idx func.Wasm. f_body in
119+ let (min_uses, max_uses) = count_uses_range param_idx func.Wasm. f_body in
93120 match kind with
94121 | Linear ->
95- (* Exactly once: zero = dropped, >1 = may be duplicated. *)
96- if uses = 0 then
97- [LinearNotUsed { func_idx; param_idx }]
98- else if uses > 1 then
99- [LinearUsedMultiple { func_idx; param_idx; count = uses }]
100- else
101- []
122+ (* Exactly once on every path: zero everywhere = dropped; zero on some
123+ path = partial drop; more than one on some path = may duplicate. *)
124+ let drop_errors =
125+ if max_uses = 0 then [LinearNotUsed { func_idx; param_idx }]
126+ else if min_uses = 0 then [LinearDroppedOnSomePath { func_idx; param_idx }]
127+ else []
128+ in
129+ let dup_errors =
130+ if max_uses > 1 then [LinearUsedMultiple { func_idx; param_idx; count = max_uses }]
131+ else []
132+ in
133+ drop_errors @ dup_errors
102134 | ExclBorrow ->
103- (* At most once: > 1 creates simultaneous aliases. *)
104- if uses > 1 then
105- [ExclBorrowAliased { func_idx; param_idx; count = uses }]
135+ (* At most once on any path: max > 1 creates simultaneous aliases. *)
136+ if max_uses > 1 then
137+ [ExclBorrowAliased { func_idx; param_idx; count = max_uses }]
106138 else
107139 []
108140 | Unrestricted | SharedBorrow ->
@@ -217,12 +249,17 @@ let pp_error (fmt : Format.formatter) (err : ownership_error) : unit =
217249 | LinearNotUsed { func_idx; param_idx } ->
218250 Format. fprintf fmt
219251 " Level 10 violation: function %d, param %d — Linear (own) param dropped \
220- (must be consumed exactly once)"
252+ on all paths (must be consumed exactly once)"
253+ func_idx param_idx
254+ | LinearDroppedOnSomePath { func_idx; param_idx } ->
255+ Format. fprintf fmt
256+ " Level 10 violation: function %d, param %d — Linear (own) param dropped \
257+ on some paths (per-path min uses = 0; must be consumed on every path)"
221258 func_idx param_idx
222259 | LinearUsedMultiple { func_idx; param_idx; count } ->
223260 Format. fprintf fmt
224261 " Level 10 violation: function %d, param %d — Linear (own) param loaded \
225- %d times (exactly 1 required; possible duplication)"
262+ %d times on some path (exactly 1 required; possible duplication)"
226263 func_idx param_idx count
227264 | ExclBorrowAliased { func_idx; param_idx; count } ->
228265 Format. fprintf fmt
0 commit comments