Skip to content

Commit c810f7d

Browse files
committed
feat(typecheck): #234 S2b — build the per-call-site effect side-table (ADR-016)
Builds on S2a (lib/effect_sites.ml shared numbering). `Typecheck` gains `context.call_effects : (int, eff) Hashtbl.t`, keyed by the shared `Effect_sites` ordinal, populated by `populate_call_effects` after a successful check + quantity pass (only on the Ok path). A call site's row is the callee's *declared* effect row when the callee is a statically-named function (`f(..)`, `m.f(..)`, through `ExprSpan`), else EPure. Extern fns parse as `TopFn` with `FnExtern`/`fd_eff` (parser.mly:188,206), so the same `name -> eff` map (via the existing `lower_effect_expr`) covers stdlib async primitives (`http_request_thenable` → `/{Net,Async}`) and user-defined `Async` functions uniformly. `lower_effect_expr` is wrapped defensively (→ EPure) though the check pass already validated. GATE-NEUTRAL by construction: the table is built and returned in the context but NOTHING reads it yet. S3 threads it into codegen and switches the WasmGC CPS boundary predicate onto it (structural recogniser stays the sound table-miss fallback); S4 retires the hardcoded `async_primitives` set. Tests (test/test_effect_sites.ml +2): the table has one entry per call site; a call to a `/{Net,Async}` extern carries Async while a pure call does not; every `Effect_sites` ordinal is present (producer/consumer-agreement contract). `dune test --force` 290/290. Zero regression (no codegen/behaviour change). Refs #234. Not Closes — staged campaign; owner closes per ISSUE-CLOSURE.
1 parent f3c765a commit c810f7d

2 files changed

Lines changed: 122 additions & 1 deletion

File tree

lib/typecheck.ml

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,14 @@ type context = {
189189
declared_effects : (string, unit) Hashtbl.t;
190190
(** User-declared effect names (`effect <name>;`). Consulted by
191191
[lower_effect_expr] alongside the v1 registry (issue #59). *)
192+
call_effects : (int, eff) Hashtbl.t;
193+
(** ADR-016 / #234 S2b: per-call-site effect rows, keyed by the
194+
shared [Effect_sites] ordinal. Populated after a successful check
195+
pass; the value is the callee's declared effect row (EPure when
196+
the callee is not a statically-named function). Built here but
197+
NOT yet consulted by codegen — S3 threads & switches the WasmGC
198+
CPS boundary predicate onto it (the structural recogniser remains
199+
the sound table-miss fallback). *)
192200
}
193201

194202
type 'a result = ('a, type_error) Result.t
@@ -219,6 +227,7 @@ let create_context (symbols : Symbol.t) : context =
219227
current_eff = fresh_effvar 0;
220228
trait_registry = Trait.create_registry ();
221229
declared_effects = Hashtbl.create 16;
230+
call_effects = Hashtbl.create 64;
222231
}
223232

224233
(** Enter a deeper let-level. *)
@@ -1754,6 +1763,53 @@ let check_decl (ctx : context) (decl : top_level) : (unit, type_error) Result.t
17541763
[Resolve.resolve_program_with_loader] so that [ExprApp (ExprVar f, ...)]
17551764
can resolve [f] to the imported function's scheme even though [f] does
17561765
not appear in [prog.prog_decls]. *)
1766+
(* ADR-016 / #234 S2b: build the per-call-site effect side-table.
1767+
Keyed by the shared [Effect_sites] ordinal so the (future) codegen
1768+
consumer agrees with this producer. A call's effect row is the
1769+
callee's *declared* row when the callee is a statically-named
1770+
function (`f(..)`, `m.f(..)`, through `ExprSpan`); otherwise EPure
1771+
(sound: S3's predicate is "row ⊇ Async ⇒ boundary", and an
1772+
over-conservative EPure just defers to the structural fallback —
1773+
exactly today's behaviour). Extern fns parse as [TopFn] with
1774+
[FnExtern]/[fd_eff] (parser.mly), so this covers the stdlib async
1775+
primitives and user `Async` fns uniformly. Pure traversal; built,
1776+
not yet consumed. *)
1777+
let populate_call_effects (ctx : context) (prog : Ast.program) : unit =
1778+
let fn_eff : (string, eff) Hashtbl.t = Hashtbl.create 64 in
1779+
List.iter
1780+
(function
1781+
| Ast.TopFn fd ->
1782+
let e =
1783+
match fd.fd_eff with
1784+
| Some ee -> (try lower_effect_expr ctx ee with _ -> EPure)
1785+
| None -> EPure
1786+
in
1787+
Hashtbl.replace fn_eff fd.fd_name.name e
1788+
| _ -> ())
1789+
prog.prog_decls;
1790+
let rec callee_name (e : Ast.expr) : string option =
1791+
match e with
1792+
| Ast.ExprVar id -> Some id.name
1793+
| Ast.ExprField (_, id) -> Some id.name
1794+
| Ast.ExprSpan (e, _) -> callee_name e
1795+
| _ -> None
1796+
in
1797+
Effect_sites.iter
1798+
(fun ord call ->
1799+
let row =
1800+
match call with
1801+
| Ast.ExprApp (head, _) ->
1802+
(match callee_name head with
1803+
| Some n ->
1804+
(match Hashtbl.find_opt fn_eff n with
1805+
| Some e -> e
1806+
| None -> EPure)
1807+
| None -> EPure)
1808+
| _ -> EPure
1809+
in
1810+
Hashtbl.replace ctx.call_effects ord row)
1811+
prog
1812+
17571813
let check_program ?(import_types : (string, scheme) Hashtbl.t option)
17581814
(symbols : Symbol.t) (prog : Ast.program)
17591815
: (context, type_error) Result.t =
@@ -1799,7 +1855,11 @@ let check_program ?(import_types : (string, scheme) Hashtbl.t option)
17991855
This runs after type checking succeeds so that we report type
18001856
errors first (they are more fundamental). *)
18011857
begin match Quantity.check_program_quantities prog with
1802-
| Ok () -> Ok ctx
1858+
| Ok () ->
1859+
(* ADR-016 / #234 S2b: build the per-call-site effect side-table
1860+
on the fully-checked program. Built, not yet consumed. *)
1861+
populate_call_effects ctx prog;
1862+
Ok ctx
18031863
| Error (qerr, span) ->
18041864
Error (QuantityError (qerr, span))
18051865
end

test/test_effect_sites.ml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,63 @@ fn main() -> Int {
7575
Alcotest.(check int) "ten call sites across positions" 10
7676
(Effect_sites.count p)
7777

78+
(* ── #234 S2b: typecheck populates the ordinal→effect side-table ── *)
79+
80+
let rec eff_mentions (name : string) (e : Types.eff) : bool =
81+
match e with
82+
| Types.EPure | Types.EVar _ -> false
83+
| Types.ESingleton s -> s = name
84+
| Types.EUnion es -> List.exists (eff_mentions name) es
85+
86+
let typecheck_ctx (src : string) : Typecheck.context =
87+
let prog = parse src in
88+
let loader = Module_loader.create (Module_loader.default_config ()) in
89+
match Resolve.resolve_program_with_loader prog loader with
90+
| Error (e, _) -> Alcotest.failf "resolve: %s" (Resolve.show_resolve_error e)
91+
| Ok (rc, _) ->
92+
(match Typecheck.check_program rc.Resolve.symbols prog with
93+
| Ok ctx -> ctx
94+
| Error e -> Alcotest.failf "typecheck: %s" (Typecheck.format_type_error e))
95+
96+
(* helper(1)=ord0 (pure, no eff), prim(2)=ord1 (declared /{Net,Async}).
97+
`a + b` is ExprBinary, not a call. *)
98+
let s2b_src =
99+
{|
100+
extern fn prim(x: Int) -> Int / { Net, Async };
101+
102+
fn helper(x: Int) -> Int { x }
103+
104+
fn main() -> Int {
105+
let a = helper(1);
106+
let b = prim(2);
107+
a + b
108+
}
109+
|}
110+
111+
let test_s2b_table_built () =
112+
let ctx = typecheck_ctx s2b_src in
113+
Alcotest.(check int) "two table entries" 2
114+
(Hashtbl.length ctx.Typecheck.call_effects);
115+
let e0 = Hashtbl.find ctx.Typecheck.call_effects 0 in
116+
let e1 = Hashtbl.find ctx.Typecheck.call_effects 1 in
117+
Alcotest.(check bool) "helper(1) carries no Async" false
118+
(eff_mentions "Async" e0);
119+
Alcotest.(check bool) "prim(2) carries Async" true
120+
(eff_mentions "Async" e1)
121+
122+
let test_s2b_keyed_by_effect_sites_ordinals () =
123+
(* Producer/consumer agreement contract: every Effect_sites ordinal
124+
has an entry in the table. *)
125+
let ctx = typecheck_ctx s2b_src in
126+
let p = parse s2b_src in
127+
List.iter
128+
(fun (ord, _) ->
129+
Alcotest.(check bool)
130+
(Printf.sprintf "ordinal %d present" ord)
131+
true
132+
(Hashtbl.mem ctx.Typecheck.call_effects ord))
133+
(Effect_sites.to_list p)
134+
78135
let tests =
79136
[
80137
Alcotest.test_case "count" `Quick test_count;
@@ -85,4 +142,8 @@ let tests =
85142
Alcotest.test_case "no calls" `Quick test_no_calls;
86143
Alcotest.test_case "calls in many positions" `Quick
87144
test_calls_in_many_positions;
145+
Alcotest.test_case "S2b: typecheck builds the effect side-table"
146+
`Quick test_s2b_table_built;
147+
Alcotest.test_case "S2b: keyed by Effect_sites ordinals" `Quick
148+
test_s2b_keyed_by_effect_sites_ordinals;
88149
]

0 commit comments

Comments
 (0)