Skip to content

Commit ddfb936

Browse files
Merge branch 'main' into claude/funny-faraday-rp3su1
Signed-off-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com>
2 parents 19084c8 + fe0963c commit ddfb936

9 files changed

Lines changed: 564 additions & 113 deletions

File tree

‎bin/main.ml‎

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,21 @@ let parse_file (face : Affinescript.Face.face) path =
177177
CLI used the process CWD as [current_dir], so compiling `src/Main.affine`
178178
could not find a sibling `src/Helper.affine` even though the module
179179
loader already supported relative sibling paths (#642). *)
180-
let loader_config_for_file path =
180+
let loader_config_for_file ?prog path =
181181
let absolute_path =
182182
if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path
183183
else path
184184
in
185185
let config = Affinescript.Module_loader.default_config () in
186-
{ config with current_dir = Filename.dirname absolute_path }
186+
let mod_path =
187+
Option.bind prog (fun (p : Affinescript.Ast.program) ->
188+
Option.map (List.map (fun (id : Affinescript.Ast.ident) -> id.name))
189+
p.prog_module)
190+
in
191+
{ config with
192+
current_dir =
193+
Affinescript.Module_loader.module_root_dir
194+
~file_dir:(Filename.dirname absolute_path) mod_path }
187195

188196
(** Type-check a file. With [--json], emits a structured diagnostic
189197
report on stderr. *)
@@ -196,7 +204,7 @@ let check_file face json path =
196204
let resolve_refs = ref [] in
197205
begin try
198206
let prog = parse_with_face face path in
199-
let loader_config = loader_config_for_file path in
207+
let loader_config = loader_config_for_file ~prog path in
200208
let loader = Affinescript.Module_loader.create loader_config in
201209
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
202210
| Error (e, span) ->
@@ -244,7 +252,7 @@ let check_file face json path =
244252
end else begin
245253
try
246254
let prog = parse_with_face face path in
247-
let loader_config = loader_config_for_file path in
255+
let loader_config = loader_config_for_file ~prog path in
248256
let loader = Affinescript.Module_loader.create loader_config in
249257
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
250258
| Error (e, _span) ->
@@ -294,7 +302,7 @@ let eval_file face json path =
294302
let add d = diags := d :: !diags in
295303
begin try
296304
let prog = parse_with_face face path in
297-
let loader_config = loader_config_for_file path in
305+
let loader_config = loader_config_for_file ~prog path in
298306
let loader = Affinescript.Module_loader.create loader_config in
299307
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
300308
| Error (e, span) ->
@@ -316,7 +324,8 @@ let eval_file face json path =
316324
| Error (err, span) ->
317325
add (Affinescript.Json_output.of_quantity_error (err, span))
318326
| Ok () ->
319-
(match Affinescript.Interp.eval_program prog with
327+
(match Affinescript.Interp.eval_program
328+
(Affinescript.Module_loader.flatten_imports loader prog) with
320329
| Ok env ->
321330
(* Auto-call main() if defined — entry point for TEA apps and scripts *)
322331
(match Affinescript.Value.lookup_env "main" env with
@@ -337,7 +346,7 @@ let eval_file face json path =
337346
end else begin
338347
try
339348
let prog = parse_with_face face path in
340-
let loader_config = loader_config_for_file path in
349+
let loader_config = loader_config_for_file ~prog path in
341350
let loader = Affinescript.Module_loader.create loader_config in
342351
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
343352
| Error (e, _span) ->
@@ -365,7 +374,8 @@ let eval_file face json path =
365374
(Affinescript.Face.format_quantity_error face err);
366375
`Error (false, "Quantity error")
367376
| Ok () ->
368-
(match Affinescript.Interp.eval_program prog with
377+
(match Affinescript.Interp.eval_program
378+
(Affinescript.Module_loader.flatten_imports loader prog) with
369379
| Ok env ->
370380
(* Auto-call main() if defined — entry point for TEA apps and scripts *)
371381
(match Affinescript.Value.lookup_env "main" env with
@@ -519,7 +529,7 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
519529
let add d = diags := d :: !diags in
520530
begin try
521531
let prog = parse_with_face face path in
522-
let loader_config = loader_config_for_file path in
532+
let loader_config = loader_config_for_file ~prog path in
523533
let loader = Affinescript.Module_loader.create loader_config in
524534
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
525535
| Error (e, span) ->
@@ -744,7 +754,7 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
744754
end else begin
745755
try
746756
let prog = parse_with_face face path in
747-
let loader_config = loader_config_for_file path in
757+
let loader_config = loader_config_for_file ~prog path in
748758
let loader = Affinescript.Module_loader.create loader_config in
749759
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
750760
| Error (e, _span) ->
@@ -1037,7 +1047,7 @@ let lint_file face json path =
10371047
let add d = diags := d :: !diags in
10381048
begin try
10391049
let prog = parse_with_face face path in
1040-
let loader_config = loader_config_for_file path in
1050+
let loader_config = loader_config_for_file ~prog path in
10411051
let loader = Affinescript.Module_loader.create loader_config in
10421052
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
10431053
| Error (e, span) ->
@@ -1057,7 +1067,7 @@ let lint_file face json path =
10571067
end else begin
10581068
try
10591069
let prog = parse_with_face face path in
1060-
let loader_config = loader_config_for_file path in
1070+
let loader_config = loader_config_for_file ~prog path in
10611071
let loader = Affinescript.Module_loader.create loader_config in
10621072
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
10631073
| Error (e, _span) ->
@@ -1096,7 +1106,7 @@ let compile_to_wasm_module face path
10961106
: (Affinescript.Wasm.wasm_module, string) Result.t =
10971107
try
10981108
let prog = parse_with_face face path in
1099-
let loader_config = loader_config_for_file path in
1109+
let loader_config = loader_config_for_file ~prog path in
11001110
let loader = Affinescript.Module_loader.create loader_config in
11011111
match Affinescript.Resolve.resolve_program_with_loader prog loader with
11021112
| Error (e, _span) ->
@@ -1160,7 +1170,7 @@ let verify_file face path =
11601170
let face = resolve_face face path in
11611171
try
11621172
let prog = parse_with_face face path in
1163-
let loader_config = loader_config_for_file path in
1173+
let loader_config = loader_config_for_file ~prog path in
11641174
let loader = Affinescript.Module_loader.create loader_config in
11651175
(match Affinescript.Resolve.resolve_program_with_loader prog loader with
11661176
| Error (e, _span) ->
@@ -1688,7 +1698,7 @@ let preview_cafe_cmd =
16881698
let run_pipeline_for_query face path =
16891699
try
16901700
let prog = parse_with_face face path in
1691-
let loader_config = loader_config_for_file path in
1701+
let loader_config = loader_config_for_file ~prog path in
16921702
let loader = Affinescript.Module_loader.create loader_config in
16931703
match Affinescript.Resolve.resolve_program_with_loader prog loader with
16941704
| Error (e, _span) ->

‎docs/COMPLETION-PLAN.adoc‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ Results were consistent, but they do not prove CI is green.
8787
| Memory reclamation. try/catch.
8888

8989
| Bun-ESM (`lib/codegen_deno.ml`)
90-
| Basic programs are correct. A `return` inside a match arm returns 100 instead of 99 (IIFE at `:1575`). aLib conformance is 100 of 103 vectors.
90+
| Basic programs are correct. A `return` inside a match arm is correct in a `let` initializer, a function or lambda tail, and nested statement position; as an argument or operand it is refused at compile time rather than miscompiled (H2, 2026-11-09). aLib conformance is 100 of 103 vectors.
9191
| —
9292
| —
9393

@@ -202,7 +202,7 @@ Results were consistent, but they do not prove CI is green.
202202
All were observed by RUN unless marked otherwise. The maps confirm that H1–H4 and H9 are absent from `docs/SOUNDNESS.adoc`. H5–H8, H10, H12 and H13 were not checked against the ledger. H11 already has a drafted issue (`issues-drafts/06`, referenced from the AFFIRMATION). H14, H15 and H19 belong to typed-wasm, not the AffineScript ledger.
203203

204204
. H1. Interpreter early `return` has no non-local exit (`lib/interp.ml:262`).
205-
. H2. Bun-ESM `return` in expression position is lowered as an IIFE (`lib/codegen_deno.ml:1575`).
205+
. H2. Bun-ESM `return` in expression position is lowered as an IIFE (`lib/codegen_deno.ml:1575`). *Narrowed 2026-11-09:* `let` initializers and function and lambda tails are now lowered to statements, so the `return` leaves the function; a match used as an argument or operand with a `return` in an arm is refused loudly. Anchor: `test/test_module_root_flatten.ml`.
206206
. H3. The JS-text backend drops block returns (`lib/js_codegen.ml`). The Lua backend emits invalid code.
207207
. H4. Non-exhaustive match compiles to "return 0" on core WASM.
208208
. H5. Effect rows are not checked against performed operations or callee rows.
@@ -220,9 +220,9 @@ All were observed by RUN unless marked otherwise. The maps confirm that H1–H4
220220
. H17. The lexer accepts the bad escape `\q`, and an empty `match` parses (conformance invalid cases 010 and 011).
221221
. H18. The interpreter fails when dereferencing refs returned through calls ("Expected reference type"). The session lead reproduced this on two #554 fixtures.
222222
. H19 (typed-wasm). No L5 index guard: `get_player_hp(0,150)` reads an adjacent region. `where` constraints are discarded. Malformed `session` blocks are silently ignored.
223-
. H20. `Face.render_ty` is not injective, contrary to the F-5 model. On the Js face `List[Option[Int]]` and `Option[List[Int]]` both render as `List[Int] | null`; on Cafe both render as `List[Int]?`. The greedy `Option\[\(.*\)\]` regex in `lib/face.ml` causes it (RUN, by calling the built library).
224-
. H21. The Coq proof gate fails open on assertions outside the printed theorems' dependency cone. A planted `Admitted` false lemma and a planted `Axiom False` both passed a replica of `coq-proof-gate.yml` (RUN). The gate also never re-runs when the `lib/*.ml` files the models describe change.
225-
. H22. The staged Agda under `proposals/` (23 files) passes under the canonical flags, but nothing checks it: `tools/check-proofs.sh --agda` reports SKIP, not failure, when `AFFINESCRIPT_ECHO_TYPES_DIR` or `AGDA_STDLIB` is unset, and no workflow or dune rule calls it. No echo-types commit is pinned.
223+
. H20 (found and fixed 2026-11-09, checking and running metadatastician/kyq). A module file found for `A.B` that declares `module A;` was accepted as `A.B`, so `use A.Nonexistent::*` type-checked and imported `A`'s exports; it is now a `ModuleDeclMismatch` load error. Anchor: `test/test_module_root_flatten.ml`.
224+
. H21 (found and fixed 2026-11-09). `eval` ran the unflattened program, so no imported function was bound at run time; flattening also emitted a selected function before the private helpers it calls and carried no helpers for glob or plain imports. `eval` now flattens, every import kind carries its helpers, and they are emitted in the imported module's declaration order. Anchor: `test/test_module_root_flatten.ml`.
225+
. H22 (found and fixed 2026-11-09). The interpreter had no `++` on arrays and no `==`/`!=` beyond Int, Float, String and Bool, though the checker accepts them; it now uses the structural `Value.value_eq`. H7 (`let mut`) is unchanged. Anchor: `test/test_module_root_flatten.ml`.
226226

227227
== 2. Target architecture
228228

‎lib/ast.ml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,15 @@ let rec expr_contains_return (e : expr) : bool =
523523
| ExprUnary (_, x) -> expr_contains_return x
524524
| ExprApp (f, args) ->
525525
expr_contains_return f || List.exists expr_contains_return args
526+
(* A `return` in a match arm (`Err(e) => return Err(e)`) is the common
527+
early-exit shape; without this arm it was invisible, and the Bun-ESM
528+
backend compiled it into a `return` from the match's IIFE instead of
529+
from the function. *)
530+
| ExprMatch { em_scrutinee; em_arms } ->
531+
expr_contains_return em_scrutinee
532+
|| List.exists (fun arm ->
533+
(match arm.ma_guard with Some g -> expr_contains_return g | None -> false)
534+
|| expr_contains_return arm.ma_body) em_arms
526535
| _ -> false
527536

528537
and block_contains_return (blk : block) : bool =

‎lib/codegen_deno.ml‎

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,6 +1434,15 @@ let enter_fn_scope ctx (params : param list) : codegen_ctx =
14341434
call whose head is a known [extern fn] lowers via {!deno_builtins}.
14351435
============================================================================ *)
14361436

1437+
(* Fresh temporaries for `let` initializers lowered to statements. *)
1438+
let let_tmp_counter = ref 0
1439+
1440+
(* Initializer shapes {!gen_assign_tail} can lower to statements. *)
1441+
let rec lowers_to_statements = function
1442+
| ExprSpan (e, _) -> lowers_to_statements e
1443+
| ExprMatch _ | ExprIf _ | ExprBlock _ -> true
1444+
| _ -> false
1445+
14371446
(** Render an expression as JavaScript using the names and host in [ctx].
14381447
Lambdas are synchronous arrows; known qualified constructors refer to
14391448
their emitted bindings. Nested statements can update integer tracking.
@@ -1587,8 +1596,15 @@ let rec gen_expr ctx (expr : expr) : string =
15871596
the enclosing async context: inside a synthesised (async) method,
15881597
`(x) => (await ...)` is a SyntaxError in browsers — Bun's parser
15891598
happens to accept it, which hid the bug. *)
1590-
"((" ^ String.concat ", " ps ^ ") => "
1591-
^ gen_expr { ctx with in_async = false } elam_body ^ ")"
1599+
let lctx = { ctx with in_async = false } in
1600+
if lowers_to_statements elam_body && expr_contains_return elam_body then
1601+
(* A `return` in the body returns from the lambda: emit a block
1602+
body so it is not swallowed by an IIFE (see {!gen_match}). *)
1603+
"((" ^ String.concat ", " ps ^ ") => { "
1604+
^ gen_return_tail lctx elam_body ^ " })"
1605+
else
1606+
"((" ^ String.concat ", " ps ^ ") => "
1607+
^ gen_expr lctx elam_body ^ ")"
15921608
| ExprTry { et_body; et_catch; et_finally } ->
15931609
gen_try ctx et_body et_catch et_finally
15941610
| ExprVariant (ty, ctor) ->
@@ -1651,6 +1667,21 @@ and gen_pattern ctx (pat : pattern) : string =
16511667
| PatOr (p, _) -> gen_pattern ctx p
16521668

16531669
and gen_match ctx scrutinee arms =
1670+
(* As an expression the match is an IIFE, so a `return` in an arm would
1671+
leave the IIFE, not the enclosing function, and the function would
1672+
carry on with the returned value as the match's result: silently wrong
1673+
output. `let` initializers and tail/statement position are lowered to
1674+
statements and handled correctly; any other position (an argument, an
1675+
operand) is refused here rather than miscompiled. *)
1676+
if List.exists (fun arm ->
1677+
expr_contains_return arm.ma_body
1678+
|| (match arm.ma_guard with
1679+
| Some g -> expr_contains_return g | None -> false)) arms
1680+
then
1681+
failwith
1682+
"a `return` inside a match arm is not supported when the match is \
1683+
used as a sub-expression (an argument or operand); bind the match \
1684+
to a `let` first";
16541685
let scrutinee_str = gen_expr ctx scrutinee in
16551686
let scrut_var = "__scrut" in
16561687
let rec gen_arms = function
@@ -1844,6 +1875,58 @@ and gen_match_stmt ctx scrut arms =
18441875
in
18451876
"{ const " ^ sv ^ " = " ^ gen_expr ctx scrut ^ "; " ^ arms_js arms ^ " }"
18461877

1878+
(* Statements that evaluate [e] and hand its value to [sink], which turns a
1879+
JS expression into a statement (`x = v;` or `return v;`). A `return`
1880+
anywhere along a tail path stays a real function return, which the IIFE
1881+
expression form cannot express. Used for `let` initializers and for
1882+
function and lambda bodies that contain an early return. *)
1883+
and gen_tail_to ctx (sink : string -> string) e =
1884+
match e with
1885+
| ExprSpan (inner, _) -> gen_tail_to ctx sink inner
1886+
| ExprReturn _ -> gen_stmt_expr ctx e
1887+
| ExprBlock blk ->
1888+
let b = Buffer.create 64 in
1889+
List.iter (fun s ->
1890+
Buffer.add_string b (gen_stmt ctx s);
1891+
Buffer.add_char b ' ') blk.blk_stmts;
1892+
(match blk.blk_expr with
1893+
| Some tail -> Buffer.add_string b (gen_tail_to ctx sink tail)
1894+
| None -> Buffer.add_string b (sink "Unit"));
1895+
"{ " ^ Buffer.contents b ^ " }"
1896+
| ExprIf { ei_cond; ei_then; ei_else } ->
1897+
let elseb = match ei_else with
1898+
| Some el -> gen_tail_to ctx sink el
1899+
| None -> sink "Unit"
1900+
in
1901+
"if (" ^ gen_expr ctx ei_cond ^ ") { " ^ gen_tail_to ctx sink ei_then
1902+
^ " } else { " ^ elseb ^ " }"
1903+
| ExprMatch { em_scrutinee; em_arms } ->
1904+
incr let_tmp_counter;
1905+
let sv = Printf.sprintf "__scrut_%d" !let_tmp_counter in
1906+
let rec arms_js = function
1907+
| [] -> "throw new Error(\"non-exhaustive match\");"
1908+
| arm :: rest ->
1909+
let cond = gen_pattern_test sv arm.ma_pat in
1910+
let binds = gen_pattern_bindings sv arm.ma_pat in
1911+
let guard = match arm.ma_guard with
1912+
| Some g -> " && (" ^ gen_expr ctx g ^ ")" | None -> "" in
1913+
"if (" ^ cond ^ guard ^ ") { " ^ binds
1914+
^ gen_tail_to ctx sink arm.ma_body ^ " } else " ^ arms_js rest
1915+
in
1916+
"{ const " ^ sv ^ " = " ^ gen_expr ctx em_scrutinee ^ "; "
1917+
^ arms_js em_arms ^ " }"
1918+
| other -> sink (gen_expr ctx other)
1919+
1920+
and gen_assign_tail ctx target e =
1921+
gen_tail_to ctx (fun v -> target ^ " = " ^ v ^ ";") e
1922+
1923+
(* A function or lambda body's tail: statements when it holds an early
1924+
return, else the plain `return <expr>;`. *)
1925+
and gen_return_tail ctx e =
1926+
if lowers_to_statements e && expr_contains_return e then
1927+
gen_tail_to ctx (fun v -> "return " ^ v ^ ";") e
1928+
else "return " ^ gen_expr ctx e ^ ";"
1929+
18471930
and gen_try_stmt ctx body catch finally =
18481931
let b = gen_stmt_seq ctx body in
18491932
let c = match catch with
@@ -1874,8 +1957,22 @@ and gen_stmt ctx (stmt : stmt) : string =
18741957
gen_expr ctx sl_value ^ ";"
18751958
| StmtLet { sl_pat; sl_value; sl_mut; sl_quantity = _; sl_ty = _ } ->
18761959
let kw = if sl_mut then "let" else "const" in
1877-
let js = kw ^ " " ^ gen_pattern ctx sl_pat ^ " = "
1878-
^ gen_expr ctx sl_value ^ ";" in
1960+
let js =
1961+
if lowers_to_statements sl_value && expr_contains_return sl_value then begin
1962+
(* `let x = match e { A(v) => v, B(err) => return f(err) };`
1963+
As an expression the match is an IIFE, and the arm's `return`
1964+
left the IIFE, not the function: the IIFE's result was then
1965+
bound to `x` and the function carried on. Lower the
1966+
initializer to statements that assign a temporary, so the
1967+
`return` is a real function return. *)
1968+
incr let_tmp_counter;
1969+
let tmp = Printf.sprintf "__let_tmp_%d" !let_tmp_counter in
1970+
"let " ^ tmp ^ "; " ^ gen_assign_tail ctx tmp sl_value ^ " "
1971+
^ kw ^ " " ^ gen_pattern ctx sl_pat ^ " = " ^ tmp ^ ";"
1972+
end else
1973+
kw ^ " " ^ gen_pattern ctx sl_pat ^ " = "
1974+
^ gen_expr ctx sl_value ^ ";"
1975+
in
18791976
(* Track an [Int]-bound name for the rest of this body (#478). A
18801977
block statement's scope is the enclosing function, which
18811978
{!enter_fn_scope} already bounds, so no restore is needed here. *)
@@ -1961,11 +2058,11 @@ let visibility_is_public = function
19612058
let gen_body ctx (fb : fn_body) : unit =
19622059
match fb with
19632060
| FnExpr e ->
1964-
emit_line ctx ("return " ^ gen_expr ctx e ^ ";")
2061+
emit_line ctx (gen_return_tail ctx e)
19652062
| FnBlock block ->
19662063
List.iter (fun s -> emit_line ctx (gen_stmt ctx s)) block.blk_stmts;
19672064
(match block.blk_expr with
1968-
| Some e -> emit_line ctx ("return " ^ gen_expr ctx e ^ ";")
2065+
| Some e -> emit_line ctx (gen_return_tail ctx e)
19692066
| None -> ())
19702067
| FnExtern -> ()
19712068

0 commit comments

Comments
 (0)