@@ -1788,76 +1788,135 @@ let simple_pat_name (p : pattern) : string option =
17881788 | PatVar id -> Some id.name
17891789 | _ -> None
17901790
1791- (* * PR2 base-case recogniser. Given an `Async` function's parameter
1792- names and normalised body, returns [Some (binder, async_call, cont)]
1793- iff the body is exactly `let binder = <call-expr>; <cont>` and the
1794- only variables [cont] can reference across the async split are
1795- [binder] itself or the function parameters (zero live-local
1796- capture — ADR-013 PR2 scope; capture is PR3). Otherwise [None] ⇒
1797- caller keeps the existing synchronous lowering. Conservative: a
1798- [cont] that references a top-level helper/global is also rejected
1799- here (its name is free relative to []), which is safe — it merely
1800- defers more shapes to PR3 rather than risking an unsound split. *)
1791+ (* * Recognised async-primitive calls (ADR-013 #225 PR3a, owner-chosen
1792+ structural-conservative async-boundary identification — see
1793+ affinescript#234 for the effect-threaded generalisation). The async
1794+ boundary is a `let` whose RHS is a call to one of these names. Extend
1795+ this list as wasm-path async stdlib primitives are added. *)
1796+ let async_primitives = [ " http_request_thenable" ]
1797+
1798+ let is_async_prim_call (e : expr ) : bool =
1799+ match e with
1800+ | ExprApp (ExprVar id , _ ) -> List. mem id.name async_primitives
1801+ | _ -> false
1802+
1803+ (* * Any recognised async-primitive name occurring free in [e]. Reuses
1804+ [find_free_vars] (which traverses call heads), so this catches an
1805+ async primitive anywhere in a sub-expression — used to enforce the
1806+ single-boundary rule for PR3a (Async→Async chaining is PR3c). *)
1807+ let mentions_async_prim (e : expr ) : bool =
1808+ let fv = find_free_vars [] e in
1809+ List. exists (fun p -> List. mem p fv) async_primitives
1810+
1811+ (* * Async-fn body recogniser (ADR-013 #225). Returns
1812+ [Some (pre, binder, async_call, cont)] iff the body is, after
1813+ trivial-block unwrapping, a sequence of zero or more simple
1814+ `let`-bindings ([pre]) followed by `let binder = <async-prim call>`
1815+ then a continuation [cont] (the remaining stmts + tail expr), where:
1816+
1817+ - PR2: [pre] = [] (zero live-local capture).
1818+ - PR3a: [pre] may bind live locals; [cont] may reference them — they
1819+ are captured into the continuation env by the proven #199
1820+ ExprLambda path (which already marshals N captures). [pre] is
1821+ restricted to simple `let`s so the captured set is well-defined.
1822+
1823+ Single boundary only: no recognised async primitive may appear in
1824+ [pre] values or in [cont] (chaining = PR3c). Capture soundness: every
1825+ free name in [cont] must be the binder, a param, a top-level
1826+ func/const/global, or a [pre]-bound local — anything else ⇒ [None]
1827+ (fall back to the unchanged synchronous lowering, zero regression).
1828+ The affine/linear single-use obligation (ADR-013 obl. 1) is
1829+ discharged by composition: borrow-check runs on this straight-line
1830+ AST before the transform, and the once-resumption trap guarantees
1831+ [cont] executes exactly once — so no new static machinery here. *)
18011832let detect_async_base_case ~(globals : string list ) (params : string list )
1802- (body : expr ) : (string * expr * expr) option =
1803- (* Unwrap a trivial single-expression block (`{ e }`) so a block-bodied
1804- `Async` fn whose sole content is `let r = <call>; cont` is still
1805- recognised; any non-trivial block falls through to [None]. *)
1833+ (body : expr )
1834+ : (stmt list * string * expr * expr) option =
18061835 let rec unwrap = function
18071836 | ExprBlock { blk_stmts = [] ; blk_expr = Some e } -> unwrap e
18081837 | e -> e
18091838 in
1810- (* Live-local capture check (ADR-013 PR2 scope). A name free in [cont]
1811- is a live-local capture unless it is the async result binder, a
1812- function parameter (re-supplied, not captured), or a top-level
1813- function/const/global (resolved via func_indices, not the closure
1814- env). The lone binder capture itself is handled by the proven #199
1815- ExprLambda path; any *other* live local ⇒ defer to PR3. *)
1816- let accept binder (call : expr ) (cont : expr ) =
1817- match call with
1818- | ExprApp _ ->
1819- let allowed = binder :: params @ globals in
1820- let escaping =
1821- List. filter (fun v -> not (List. mem v allowed))
1822- (dedup (find_free_vars [] cont))
1823- in
1824- if escaping = [] then Some (binder, call, cont) else None
1839+ let pre_bound_name = function
1840+ | StmtLet sl -> simple_pat_name sl.sl_pat
18251841 | _ -> None
18261842 in
1843+ let accept pre binder (call : expr ) (cont : expr ) =
1844+ if not (is_async_prim_call call) then None
1845+ else begin
1846+ (* [pre] must be only simple `let`s (well-defined capture set). *)
1847+ let pre_names = List. map pre_bound_name pre in
1848+ if List. exists (fun n -> n = None ) pre_names then None
1849+ else
1850+ let pre_bound = List. filter_map (fun x -> x) pre_names in
1851+ (* Single boundary: no async primitive in pre values or cont. *)
1852+ let pre_vals = List. filter_map
1853+ (function StmtLet sl -> Some sl.sl_value | _ -> None ) pre in
1854+ if mentions_async_prim cont
1855+ || List. exists mentions_async_prim pre_vals then None
1856+ else
1857+ let allowed = binder :: params @ globals @ pre_bound in
1858+ let escaping =
1859+ List. filter (fun v -> not (List. mem v allowed))
1860+ (dedup (find_free_vars [] cont))
1861+ in
1862+ if escaping = [] then Some (pre, binder, call, cont) else None
1863+ end
1864+ in
1865+ (* Split a stmt list at the first `let b = <async-prim call>`. *)
1866+ let rec split_at_boundary acc = function
1867+ | StmtLet sl :: rest when is_async_prim_call sl.sl_value ->
1868+ begin match simple_pat_name sl.sl_pat with
1869+ | Some binder -> Some (List. rev acc, binder, sl.sl_value, rest)
1870+ | None -> None
1871+ end
1872+ | s :: rest -> split_at_boundary (s :: acc) rest
1873+ | [] -> None
1874+ in
18271875 match unwrap body with
1828- (* Expression-form let : `let r = <call>; cont`. * )
1876+ (* Expression-form let : `let r = <async-prim call>; cont` (no pre) . * )
18291877 | ExprLet lb ->
18301878 begin match simple_pat_name lb.el_pat, lb.el_body with
1831- | Some binder , Some cont -> accept binder lb.el_value cont
1879+ | Some binder , Some cont -> accept [] binder lb.el_value cont
18321880 | _ -> None
18331881 end
1834- (* Block-statement form: `{ let r = <call>; <rest...> }` — the parser's
1835- normal desugaring of a block-bodied fn. The continuation is the
1836- remainder of the block (trailing stmts + tail expr). *)
1837- | ExprBlock { blk_stmts = StmtLet sl :: rest ; blk_expr } ->
1838- begin match simple_pat_name sl.sl_pat with
1839- | Some binder ->
1840- let cont = ExprBlock { blk_stmts = rest; blk_expr } in
1841- accept binder sl.sl_value cont
1882+ (* Block form: optional simple `let`s, then the async boundary, then
1883+ the continuation (remaining stmts + tail expr). *)
1884+ | ExprBlock { blk_stmts; blk_expr } ->
1885+ begin match split_at_boundary [] blk_stmts with
1886+ | Some (pre , binder , call , post ) ->
1887+ let cont = ExprBlock { blk_stmts = post; blk_expr } in
1888+ accept pre binder call cont
18421889 | None -> None
18431890 end
18441891 | _ -> None
18451892
1846- (* * ADR-013 PR2 transform. Lowers a recognised base-case `Async` fn
1847- body `let binder = <async-call>; <cont>` to:
1848- 1. the async call (yields a `Thenable` handle), bound to [binder];
1849- 2. [cont] reified as a zero-arg continuation via the EXISTING #199
1850- ExprLambda path — [binder] is the sole live local so it is
1851- auto-captured into the `[fnId@0,envPtr@4]` env (no new closure
1893+ (* * ADR-013 #225 transform. Lowers a recognised `Async` fn body
1894+ `<pre simple-lets>; let binder = <async-call>; <cont>` to:
1895+ 1. [pre] generated synchronously (its locals become live locals,
1896+ in scope for capture);
1897+ 2. the async call (yields a `Thenable` handle), bound to [binder];
1898+ 3. [cont] reified as a zero-arg continuation via the EXISTING #199
1899+ ExprLambda path — [binder] and every [pre]-bound local that
1900+ [cont] references are auto-captured into the `[fnId@0,envPtr@4]`
1901+ env (the #199 path already marshals N captures; no new closure
18521902 code); a once-resumption trap is prepended to its body;
1853- 3 . `thenableThen(handle, <closure>)`; the fn returns the result.
1903+ 4 . `thenableThen(handle, <closure>)`; the fn returns the result.
18541904 Pure/non-recognised fns never reach here (caller gates on
18551905 [fn_is_async] + [detect_async_base_case]); behaviour is unchanged
1856- for them, exactly as before this slice . *)
1857- let gen_async_base_case (ctx : context ) (binder : string )
1906+ for them, exactly as before. *)
1907+ let gen_async_base_case (ctx : context ) (pre : stmt list ) ( binder : string )
18581908 (async_call : expr ) (cont : expr ) (thenable_then_idx : int )
18591909 : (context * instr list) result =
1860- let * (ctx1, call_code) = gen_expr ctx async_call in
1910+ (* [pre] runs synchronously before the async call; folding [gen_stmt]
1911+ allocates its locals into the context so the continuation's #199
1912+ capture pass (find_free_vars ∩ ctx.locals) picks them up. Simple
1913+ `let`s are stack-neutral, so [pre_code] leaves the stack empty. *)
1914+ let * (ctx_pre, pre_code) =
1915+ List. fold_left (fun acc st ->
1916+ let * (c, code) = acc in
1917+ let * (c', s) = gen_stmt c st in
1918+ Ok (c', code @ s)) (Ok (ctx, [] )) pre in
1919+ let * (ctx1, call_code) = gen_expr ctx_pre async_call in
18611920 let (ctx2, binder_idx) = alloc_local ctx1 binder in
18621921 (* Once-resumption guard global (ADR-013 obligation 1): a second
18631922 continuation entry traps. Defence-in-depth over the host's
@@ -1881,7 +1940,8 @@ let gen_async_base_case (ctx : context) (binder : string)
18811940 | [] -> ctx4 (* unreachable: ExprLambda always lifts exactly one *)
18821941 in
18831942 let body =
1884- call_code
1943+ pre_code (* synchronous prelude; stack-neutral *)
1944+ @ call_code
18851945 @ [ LocalSet binder_idx ]
18861946 @ [ LocalGet binder_idx ] (* arg 1: the Thenable handle *)
18871947 @ closure_code (* arg 2: [fnId,envPtr] continuation *)
@@ -1930,19 +1990,19 @@ let gen_function (ctx : context) (fd : fn_decl) : (context * func) result =
19301990 ~globals: (List. map fst ctx_with_params.func_indices)
19311991 (List. map (fun p -> p.p_name.name) fd.fd_params)
19321992 body_expr with
1933- | Some (binder , call , cont ) ->
1993+ | Some (pre , binder , call , cont ) ->
19341994 begin match List. assoc_opt " thenableThen"
19351995 ctx_with_params.func_indices with
1936- | Some tt -> Some (binder, call, cont, tt)
1996+ | Some tt -> Some (pre, binder, call, cont, tt)
19371997 | None -> None
19381998 end
19391999 | None -> None
19402000 else None
19412001 in
19422002 let * (ctx_final, body_code) =
19432003 match async_base with
1944- | Some (binder , call , cont , tt_idx ) ->
1945- gen_async_base_case ctx_with_params binder call cont tt_idx
2004+ | Some (pre , binder , call , cont , tt_idx ) ->
2005+ gen_async_base_case ctx_with_params pre binder call cont tt_idx
19462006 | None -> gen_expr ctx_with_params body_expr
19472007 in
19482008
0 commit comments