Skip to content

Commit f3c765a

Browse files
feat(effect-sites): #234 S2a — shared call-site numbering (ADR-016) (#275)
ADR-016 mechanism foundation. The AST has no loc/id on `ExprApp` and the ADR rejects annotating it, so the effect side-table is keyed by a *deterministic shared call-site numbering*: `lib/effect_sites.ml` is a single, total, pure pre-order traversal that assigns every `ExprApp` a 0-based ordinal. Typecheck (producer) and the WasmGC CPS detector (consumer) will both obtain ordinals by calling THIS function on the same `prog`, so keys cannot drift — with no AST shape change. This slice is `effect_sites.ml` ONLY (the keying primitive). No typecheck/codegen change ⇒ pure, gate-neutral. S2b adds the typecheck table on top; S3 threads + switches codegen; S4 retires the hardcoded set (per ADR-016 staging). API: `fold_calls` / `count` / `to_list` / `iter`. Ordering contract (documented, stable, do-not-change-without-amending-ADR-016): strict left-to-right pre-order; a call node is numbered before its callee/argument sub-exprs are descended. Exhaustive over every expr/stmt/block/match/handler/unsafe/fn-body/top-level position (TopFn/Const/Impl/Trait-default bodies). Tests: `test/test_effect_sites.ml` (5 cases — count, contiguous pre-order ordinals, determinism+purity across runs/re-parses, zero calls, calls nested in arg/while/for/match/lambda/block positions). Registered in test_main. `dune test --force` 287/287. Zero regression. Refs #234. Not Closes — staged campaign; owner closes per ISSUE-CLOSURE.
1 parent dc6a893 commit f3c765a

4 files changed

Lines changed: 230 additions & 0 deletions

File tree

lib/dune

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
codegen_node
1414
desugar_traits
1515
effect
16+
effect_sites
1617
error
1718
error_collector
1819
error_formatter

lib/effect_sites.ml

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
(* SPDX-License-Identifier: PMPL-1.0-or-later *)
2+
(* SPDX-FileCopyrightText: 2026 hyperpolymath *)
3+
4+
(** Shared call-site numbering for the effect-threaded async-boundary
5+
side-table (ADR-016, issue #234).
6+
7+
The AST carries no location or node id on [ExprApp], and ADR-016
8+
rejects annotating it. Instead this module defines a single,
9+
deterministic pre-order traversal that assigns every [ExprApp] a
10+
0-based ordinal. Both the producer ([Typecheck], which records
11+
[ordinal -> effect_row]) and the consumer (the WasmGC CPS
12+
boundary detector) obtain ordinals by calling *this same function*,
13+
so the keys cannot drift — without changing the AST shape.
14+
15+
Ordering contract (stable; do not change without amending ADR-016):
16+
a strict left-to-right *pre-order* walk of the program. An
17+
[ExprApp] node is numbered *before* its callee and argument
18+
sub-expressions are descended into. Sub-structures are visited in
19+
source order. [TopFn]/[TopConst]/[TopImpl]/[TopTrait] default
20+
bodies are walked in [prog_decls] order.
21+
22+
This module is pure and depends only on [Ast]; it has no notion of
23+
effects itself (S2a). S2b/S3 build the table on top of it. *)
24+
25+
open Ast
26+
27+
(* The visitor threads an accumulator and a mutable next-ordinal
28+
counter (held in a ref captured by the closures, so the ordinal is a
29+
pure function of traversal position). *)
30+
31+
let fold_calls (type a) (f : a -> int -> expr -> a) (init : a)
32+
(prog : program) : a =
33+
let acc = ref init in
34+
let next = ref 0 in
35+
let rec go_expr (e : expr) : unit =
36+
(match e with
37+
| ExprApp (fn, args) ->
38+
(* Number THIS call site before descending (pre-order). *)
39+
let ord = !next in
40+
incr next;
41+
acc := f !acc ord e;
42+
go_expr fn;
43+
List.iter go_expr args
44+
| ExprLit _ | ExprVar _ | ExprVariant _ -> ()
45+
| ExprLet l ->
46+
go_expr l.el_value;
47+
(match l.el_body with Some b -> go_expr b | None -> ())
48+
| ExprIf i ->
49+
go_expr i.ei_cond;
50+
go_expr i.ei_then;
51+
(match i.ei_else with Some e -> go_expr e | None -> ())
52+
| ExprMatch m ->
53+
go_expr m.em_scrutinee;
54+
List.iter go_arm m.em_arms
55+
| ExprLambda l -> go_expr l.elam_body
56+
| ExprField (e, _) | ExprTupleIndex (e, _) | ExprRowRestrict (e, _)
57+
| ExprSpan (e, _) | ExprUnary (_, e) ->
58+
go_expr e
59+
| ExprIndex (a, b) | ExprBinary (a, _, b) ->
60+
go_expr a;
61+
go_expr b
62+
| ExprTuple es | ExprArray es -> List.iter go_expr es
63+
| ExprRecord r ->
64+
List.iter
65+
(fun (_, eo) -> match eo with Some e -> go_expr e | None -> ())
66+
r.er_fields;
67+
(match r.er_spread with Some e -> go_expr e | None -> ())
68+
| ExprBlock b -> go_block b
69+
| ExprReturn eo | ExprResume eo ->
70+
(match eo with Some e -> go_expr e | None -> ())
71+
| ExprTry t ->
72+
go_block t.et_body;
73+
(match t.et_catch with Some arms -> List.iter go_arm arms | None -> ());
74+
(match t.et_finally with Some b -> go_block b | None -> ())
75+
| ExprHandle h ->
76+
go_expr h.eh_body;
77+
List.iter go_handler h.eh_handlers
78+
| ExprUnsafe ops -> List.iter go_unsafe ops)
79+
and go_arm (a : match_arm) : unit =
80+
(match a.ma_guard with Some g -> go_expr g | None -> ());
81+
go_expr a.ma_body
82+
and go_handler = function
83+
| HandlerReturn (_, e) -> go_expr e
84+
| HandlerOp (_, _, e) -> go_expr e
85+
and go_unsafe = function
86+
| UnsafeRead e | UnsafeForget e -> go_expr e
87+
| UnsafeWrite (a, b) | UnsafeOffset (a, b) ->
88+
go_expr a;
89+
go_expr b
90+
| UnsafeTransmute (_, _, e) -> go_expr e
91+
and go_block (b : block) : unit =
92+
List.iter go_stmt b.blk_stmts;
93+
(match b.blk_expr with Some e -> go_expr e | None -> ())
94+
and go_stmt = function
95+
| StmtLet l -> go_expr l.sl_value
96+
| StmtExpr e -> go_expr e
97+
| StmtAssign (a, _, b) ->
98+
go_expr a;
99+
go_expr b
100+
| StmtWhile (c, b) ->
101+
go_expr c;
102+
go_block b
103+
| StmtFor (_, e, b) ->
104+
go_expr e;
105+
go_block b
106+
in
107+
let go_fn_body = function
108+
| FnBlock b -> go_block b
109+
| FnExpr e -> go_expr e
110+
| FnExtern -> ()
111+
in
112+
let go_top = function
113+
| TopFn fd -> go_fn_body fd.fd_body
114+
| TopConst c -> go_expr c.tc_value
115+
| TopImpl ib ->
116+
List.iter
117+
(function ImplFn fd -> go_fn_body fd.fd_body | ImplType _ -> ())
118+
ib.ib_items
119+
| TopTrait trd ->
120+
List.iter
121+
(function
122+
| TraitFnDefault fd -> go_fn_body fd.fd_body
123+
| TraitFn _ | TraitType _ -> ())
124+
trd.trd_items
125+
| TopType _ | TopEffect _ | TopExternType _ | TopExternFn _ -> ()
126+
in
127+
List.iter go_top prog.prog_decls;
128+
!acc
129+
130+
(** Total number of call sites ([ExprApp] nodes) in [prog]. *)
131+
let count (prog : program) : int =
132+
fold_calls (fun n _ _ -> n + 1) 0 prog
133+
134+
(** All call sites as [(ordinal, node)] in traversal (= ordinal) order. *)
135+
let to_list (prog : program) : (int * expr) list =
136+
List.rev (fold_calls (fun acc ord e -> (ord, e) :: acc) [] prog)
137+
138+
(** [iter f prog] runs [f ordinal node] for every call site, in order. *)
139+
let iter (f : int -> expr -> unit) (prog : program) : unit =
140+
fold_calls (fun () ord e -> f ord e) () prog

test/test_effect_sites.ml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
(* SPDX-License-Identifier: PMPL-1.0-or-later *)
2+
(* SPDX-FileCopyrightText: 2026 hyperpolymath *)
3+
4+
(* ADR-016 / #234 S2a: the shared call-site numbering must be total,
5+
deterministic, and pure (the keying contract for the effect
6+
side-table — typecheck and codegen both call it on the same prog and
7+
MUST agree). *)
8+
9+
open Affinescript
10+
11+
let parse src = Parse_driver.parse_string ~file:"<test_effect_sites>" src
12+
13+
(* Calls: helper(1) helper(2) helper(3) helper(4) — across a let value,
14+
an if-condition, and both if branches. `>` and `+` are ExprBinary,
15+
not calls. *)
16+
let prog_src =
17+
{|
18+
fn helper(x: Int) -> Int { x }
19+
20+
fn main() -> Int {
21+
let a = helper(1);
22+
let b = if helper(2) > 0 { helper(3) } else { helper(4) };
23+
a + b
24+
}
25+
|}
26+
27+
let test_count () =
28+
let p = parse prog_src in
29+
Alcotest.(check int) "four call sites" 4 (Effect_sites.count p)
30+
31+
let test_ordinals_contiguous_preorder () =
32+
let p = parse prog_src in
33+
let ords = List.map fst (Effect_sites.to_list p) in
34+
(* 0..3 in order — pre-order, contiguous, no gaps/dupes. *)
35+
Alcotest.(check (list int)) "ordinals 0..3 in order" [ 0; 1; 2; 3 ] ords
36+
37+
let test_deterministic_and_pure () =
38+
let p = parse prog_src in
39+
let run () = Effect_sites.to_list p |> List.map fst in
40+
Alcotest.(check (list int)) "two runs identical" (run ()) (run ());
41+
(* Re-parsing the same source yields the same numbering. *)
42+
let p2 = parse prog_src in
43+
Alcotest.(check int) "stable across parses"
44+
(Effect_sites.count p) (Effect_sites.count p2)
45+
46+
let test_no_calls () =
47+
let p = parse {|
48+
fn main() -> Int {
49+
let a = 1;
50+
let b = a + 2;
51+
b
52+
}
53+
|} in
54+
Alcotest.(check int) "no call sites" 0 (Effect_sites.count p)
55+
56+
let test_calls_in_many_positions () =
57+
(* arg-nested, block stmt, while, for, match arm, lambda body. *)
58+
let p = parse {|
59+
fn f(x: Int) -> Int { x }
60+
61+
fn main() -> Int {
62+
let mut s = 0;
63+
s = f(f(1));
64+
while f(0) > 9 { s = s + f(2); }
65+
for y in [f(3)] { s = s + y; }
66+
let g = |z: Int| -> Int { f(z) };
67+
s = g(f(4));
68+
match f(5) {
69+
_ => f(6)
70+
}
71+
}
72+
|} in
73+
(* f(f(1))=2, f(0)=1, f(2)=1, f(3)=1, g(f(4))=2, f(5)=1, f(6)=1
74+
=> 10 call sites (g(...) is itself a call). *)
75+
Alcotest.(check int) "ten call sites across positions" 10
76+
(Effect_sites.count p)
77+
78+
let tests =
79+
[
80+
Alcotest.test_case "count" `Quick test_count;
81+
Alcotest.test_case "ordinals contiguous pre-order" `Quick
82+
test_ordinals_contiguous_preorder;
83+
Alcotest.test_case "deterministic & pure" `Quick
84+
test_deterministic_and_pure;
85+
Alcotest.test_case "no calls" `Quick test_no_calls;
86+
Alcotest.test_case "calls in many positions" `Quick
87+
test_calls_in_many_positions;
88+
]

test/test_main.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ let () =
1111
("Golden", Test_golden.tests);
1212
("Examples", Test_golden.example_tests);
1313
("Effects (#59)", Test_effects.tests);
14+
("Effect-sites (#234 S2a)", Test_effect_sites.tests);
1415
] @ Test_e2e.tests @ Test_stdlib_aot.tests)

0 commit comments

Comments
 (0)