Skip to content

Commit 7186e06

Browse files
hyperpolymathclaude
andcommitted
fix(stdlib): wire env_at / arg_at surface — codegen lowers via gen_str_at_via_get (ADR-015 S5, #180)
The codegen-test fixtures tests/codegen/{env_at,arg_at,env_count_and_at}.affine all reference env_at / arg_at, but those surface names were never wired in resolve.ml or typecheck.ml — `dune runtest` hits Resolve.UndefinedVariable before codegen even fires. Latent baseline-rot since commit 58f08b9 (ADR-015 S5) added the wasm IR + helper without completing the surface. Same shape of fix as #362 (string_length), but a full layer-by-layer wiring because env_at/arg_at need TWO WASI imports each. ## Changes * lib/resolve.ml: add `def "env_at"; def "arg_at"`. * lib/typecheck.ml: bind both as `Int -> String / Time`, matching the effect row of their count siblings (env_count / arg_count). * lib/codegen.ml: new ExprApp dispatch right after the env_count/arg_count handler — allocates 8 locals (n, scratch, count, bufsize, ptrvec, src, dst, result), looks up the matching sizes_get + get WASI func indices, and calls Wasi_runtime.gen_str_at_via_get (already present, was just unreferenced). * lib/codegen.ml: extend the optional_wasi table with four new rows — ("env_at", "environ_sizes_get"), ("env_at", "environ_get"), ("arg_at", "args_sizes_get"), ("arg_at", "args_get"). Each row's factory is the existing create_*_import in wasi_runtime.ml. * lib/codegen.ml: add a dedup pass over optional_wasi keyed by WASI import name (keep first occurrence). Necessary because env_count + env_at both want environ_sizes_get; without dedup the wasm would carry two imports under the same name and instantiation would fail. The env_count_and_at.affine fixture is the regression test for this case — its docstring already documented the requirement. ## What this does NOT cover * Interp.eval_program does not gain env_at / arg_at. Same posture as env_count / arg_count (also absent from interp). These are WASI-only surfaces; interp would need a synthetic environ table to simulate them. Out of scope. Closes-Refs ADR-015 S5 surface completion; Refs #180 (parent epic), Refs #339 (the IR-only commit), Refs #361 (originating queue-clearance incident — this is the 4th baseline-rot iteration after #361 / #362 / the .hypatia-ignore-format fix). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f1d47e6 commit 7186e06

3 files changed

Lines changed: 75 additions & 3 deletions

File tree

lib/codegen.ml

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -888,8 +888,7 @@ let rec gen_expr (ctx : context) (expr : expr) : (context * instr list) result =
888888
(added on-demand at module assembly; idx looked up in
889889
[ctx.wasi_func_indices]). The Unit arg satisfies the
890890
zero-param-fn collapse wart; it is evaluated but its value
891-
is unused. String accessors (env_at/arg_at) need byte-level
892-
wasm IR ops (currently absent) and are a tracked follow-up. *)
891+
is unused. String accessors env_at/arg_at are below. *)
893892
let wasi_name =
894893
if id.name = "env_count" then "environ_sizes_get"
895894
else "args_sizes_get"
@@ -909,6 +908,49 @@ let rec gen_expr (ctx : context) (expr : expr) : (context * instr list) result =
909908
in
910909
Ok (ctx_with_heap, code)
911910

911+
| ExprVar id when (id.name = "env_at" || id.name = "arg_at")
912+
&& List.length args = 1 ->
913+
(* ADR-015 S5 (#180): env_at(i) / arg_at(i) — fetch the i-th
914+
entry from the WASI environ/argv vector and return as a
915+
length-prefixed AS string. Lowers via [gen_str_at_via_get]
916+
which needs 8 fresh locals + two WASI imports (sizes_get
917+
paired with get). All locals are pre-allocated here and
918+
passed by index; the helper does not modify the type/scope
919+
context. The sizes_get import is shared with env_count /
920+
arg_count when both are used in the same module; dedup is
921+
done in the optional_wasi assembly below. *)
922+
let (sizes_name, get_name) =
923+
if id.name = "env_at"
924+
then ("environ_sizes_get", "environ_get")
925+
else ("args_sizes_get", "args_get")
926+
in
927+
let sizes_func_idx =
928+
try List.assoc sizes_name ctx.wasi_func_indices
929+
with Not_found -> 1
930+
in
931+
let get_func_idx =
932+
try List.assoc get_name ctx.wasi_func_indices
933+
with Not_found -> 1
934+
in
935+
let* (ctx_with_arg, arg_code) = gen_expr ctx (List.hd args) in
936+
let (ctx1, scratch_local) = alloc_local ctx_with_arg ("__" ^ id.name ^ "_scratch") in
937+
let (ctx2, count_local) = alloc_local ctx1 ("__" ^ id.name ^ "_count") in
938+
let (ctx3, bufsize_local) = alloc_local ctx2 ("__" ^ id.name ^ "_bufsize") in
939+
let (ctx4, ptrvec_local) = alloc_local ctx3 ("__" ^ id.name ^ "_ptrvec") in
940+
let (ctx5, src_local) = alloc_local ctx4 ("__" ^ id.name ^ "_src") in
941+
let (ctx6, dst_local) = alloc_local ctx5 ("__" ^ id.name ^ "_dst") in
942+
let (ctx7, result_local) = alloc_local ctx6 ("__" ^ id.name ^ "_result") in
943+
let (ctx8, n_local) = alloc_local ctx7 ("__" ^ id.name ^ "_n") in
944+
let (ctx_with_heap, heap_idx) = ensure_heap_ptr ctx8 in
945+
let code =
946+
arg_code @
947+
Wasi_runtime.gen_str_at_via_get
948+
heap_idx n_local scratch_local count_local
949+
bufsize_local ptrvec_local src_local dst_local result_local
950+
sizes_func_idx get_func_idx
951+
in
952+
Ok (ctx_with_heap, code)
953+
912954
| ExprVar id when List.mem_assoc id.name ctx.variant_tags ->
913955
(* Enum constructor called as a function: Circle(5), Rect({x:1,y:2}), etc.
914956
Layout: [tag: i32][field1: i32][field2: i32]...
@@ -2564,14 +2606,34 @@ let generate_module ?loader (prog : program) : wasm_module result =
25642606
false prog
25652607
in
25662608
let optional_wasi =
2567-
(* (guest_builtin_name, wasi_import_name, factory) — canonical order. *)
2609+
(* (guest_builtin_name, wasi_import_name, factory) — canonical order.
2610+
ADR-015 S5 (#180): env_at/arg_at each require TWO WASI imports
2611+
(sizes_get + get), so they appear as two rows. When env_count
2612+
and env_at are both used the sizes_get row is requested by
2613+
both — the dedup pass below collapses duplicates by wasi
2614+
import name (keep first occurrence) so the resulting module
2615+
has at most one import per WASI function. *)
25682616
[ ("clock_now_ms", "clock_time_get", Wasi_runtime.create_clock_time_get_import);
25692617
("env_count", "environ_sizes_get", Wasi_runtime.create_environ_sizes_get_import);
25702618
("arg_count", "args_sizes_get", Wasi_runtime.create_args_sizes_get_import);
2619+
("env_at", "environ_sizes_get", Wasi_runtime.create_environ_sizes_get_import);
2620+
("env_at", "environ_get", Wasi_runtime.create_environ_get_import);
2621+
("arg_at", "args_sizes_get", Wasi_runtime.create_args_sizes_get_import);
2622+
("arg_at", "args_get", Wasi_runtime.create_args_get_import);
25712623
("net_shutdown", "sock_shutdown", Wasi_runtime.create_sock_shutdown_import);
25722624
]
25732625
|> List.filter_map
25742626
(fun (b, w, f) -> if uses b then Some (w, f ()) else None)
2627+
(* Dedup by wasi import name, keep first occurrence. Needed because
2628+
env_at + env_count both request environ_sizes_get; without dedup
2629+
the wasm would have two imports under the same name and
2630+
instantiation would fail. *)
2631+
|> List.fold_left (fun (seen, acc) (w, imp_ty) ->
2632+
if List.mem w seen then (seen, acc)
2633+
else (w :: seen, (w, imp_ty) :: acc)
2634+
) ([], [])
2635+
|> snd
2636+
|> List.rev
25752637
|> List.mapi (fun i (w, (imp, ty)) -> (i + 1, w, imp, ty))
25762638
in
25772639
let opt_types = List.map (fun (_, _, _, ty) -> ty) optional_wasi in

lib/resolve.ml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ let seed_builtins (symbols : Symbol.t) : unit =
5757
def "clock_now_ms";
5858
(* WASI env / argv counts (ADR-015 S4b, #180) *)
5959
def "env_count"; def "arg_count";
60+
(* WASI env / argv string accessors (ADR-015 S5, #180). Each lowers to
61+
a `*_sizes_get` + `*_get` pair plus a byte-scan; codegen emits the
62+
full helper inline via [Wasi_runtime.gen_str_at_via_get]. *)
63+
def "env_at"; def "arg_at";
6064
(* WASI socket primitive (ADR-015 S6b, #180) *)
6165
def "net_shutdown";
6266
(* String / char builtins *)

lib/typecheck.ml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1329,6 +1329,12 @@ let register_builtins (ctx : context) : unit =
13291329
follow-up. Effect row `Time` (reserved). *)
13301330
bind_var ctx "env_count" (TArrow (ty_unit, QOmega, ty_int, ESingleton "Time"));
13311331
bind_var ctx "arg_count" (TArrow (ty_unit, QOmega, ty_int, ESingleton "Time"));
1332+
(* ADR-015 S5 (#180): env_at(i) / arg_at(i) -> String / Time.
1333+
Lowers via [Wasi_runtime.gen_str_at_via_get] (8-local scratch +
1334+
byte-scan + heap-allocated AS string). Effect row matches the
1335+
count siblings since they share the WASI environ/args surface. *)
1336+
bind_var ctx "env_at" (TArrow (ty_int, QOmega, ty_string, ESingleton "Time"));
1337+
bind_var ctx "arg_at" (TArrow (ty_int, QOmega, ty_string, ESingleton "Time"));
13321338
(* ADR-015 S6b (#180): WASI socket primitive. `net_shutdown(fd, how)`
13331339
-> Int errno (0 on success, ENOTSOCK on a non-socket fd, etc.).
13341340
`how`: 1 = RD, 2 = WR, 3 = RDWR. Lowers to a

0 commit comments

Comments
 (0)