|
| 1 | +(* SPDX-License-Identifier: MPL-2.0 *) |
| 2 | +(* SPDX-FileCopyrightText: 2026 hyperpolymath *) |
| 3 | + |
| 4 | +(** Regression: every `b "name"` registered in lib/codegen_deno.ml's |
| 5 | + `deno_builtins` must have a matching `extern fn name` declaration |
| 6 | + somewhere under `stdlib/*.affine`. Two production bugs of exactly |
| 7 | + this shape landed within two PRs on 2026-05-31 (#504 STEP 3 — Deno |
| 8 | + bytesLength / bytesByteAt / bytesAsciiSlice; #509 STEP 4-B — Deno |
| 9 | + math_random / random_u32 / random_in_range / performance_now). In |
| 10 | + both cases the codegen lowering was wired AND a fixture referenced |
| 11 | + the name, but the stdlib `pub extern fn` was missing, leaving the |
| 12 | + resolver to fail with `undefined value: X` at compile time. Both |
| 13 | + landed on main because nothing cross-checked the two registries. |
| 14 | +
|
| 15 | + The codegen builtin space spans multiple stdlib modules (Deno, |
| 16 | + Canvas, Ipc, Pixi*, Motion, plus base prelude names like |
| 17 | + `string_length`, `int_to_string`), so the test reads every |
| 18 | + `stdlib/*.affine` (not just Deno.affine) and asserts subset. *) |
| 19 | + |
| 20 | +let read_file path = |
| 21 | + let ic = open_in path in |
| 22 | + let n = in_channel_length ic in |
| 23 | + let s = really_input_string ic n in |
| 24 | + close_in ic; |
| 25 | + s |
| 26 | + |
| 27 | +(* OCaml's `Str` module does not interpret backslash escapes inside |
| 28 | + `[...]` character classes — `[ \t]+` would match space-or-backslash- |
| 29 | + or-`t`, then consume the `t` of `targetPostMessage` and report |
| 30 | + `argetPostMessage` as the capture. The source files use plain spaces |
| 31 | + (no tabs) so a `[ ]+` class is precise and correct. *) |
| 32 | +let codegen_builtin_re = |
| 33 | + Str.regexp {|b[ ]+"\([A-Za-z_][A-Za-z0-9_]*\)"|} |
| 34 | + |
| 35 | +let stdlib_extern_re = |
| 36 | + Str.regexp {|extern[ ]+fn[ ]+\([A-Za-z_][A-Za-z0-9_]*\)|} |
| 37 | + |
| 38 | +let extract_names re text = |
| 39 | + let rec loop pos acc = |
| 40 | + match (try Some (Str.search_forward re text pos) with Not_found -> None) with |
| 41 | + | None -> List.rev acc |
| 42 | + | Some _ -> |
| 43 | + let name = Str.matched_group 1 text in |
| 44 | + loop (Str.match_end ()) (name :: acc) |
| 45 | + in |
| 46 | + loop 0 [] |
| 47 | + |
| 48 | +let stdlib_dir = |
| 49 | + let candidates = |
| 50 | + (match Sys.getenv_opt "AFFINESCRIPT_STDLIB" with |
| 51 | + | Some d -> [ d ] | None -> []) |
| 52 | + @ [ "../stdlib"; "stdlib"; "../../stdlib" ] |
| 53 | + in |
| 54 | + match |
| 55 | + List.find_opt |
| 56 | + (fun d -> Sys.file_exists (Filename.concat d "prelude.affine")) |
| 57 | + candidates |
| 58 | + with |
| 59 | + | Some d -> d |
| 60 | + | None -> |
| 61 | + failwith "test_deno_builtins_consistency: cannot locate stdlib/ (no prelude.affine)" |
| 62 | + |
| 63 | +let codegen_path = |
| 64 | + let candidates = |
| 65 | + [ "../lib/codegen_deno.ml"; "lib/codegen_deno.ml"; "../../lib/codegen_deno.ml" ] |
| 66 | + in |
| 67 | + match List.find_opt Sys.file_exists candidates with |
| 68 | + | Some p -> p |
| 69 | + | None -> |
| 70 | + failwith "test_deno_builtins_consistency: cannot locate lib/codegen_deno.ml" |
| 71 | + |
| 72 | +let list_stdlib_affine () = |
| 73 | + Sys.readdir stdlib_dir |
| 74 | + |> Array.to_list |
| 75 | + |> List.filter (fun n -> Filename.check_suffix n ".affine") |
| 76 | + |> List.map (Filename.concat stdlib_dir) |
| 77 | + |
| 78 | +(* Compiler-synthesised JS helpers and runtime intrinsics don't have |
| 79 | + per-name `extern fn` decls; they're wired in `lib/resolve.ml` / |
| 80 | + `lib/typecheck.ml` directly. Same for the JSON-FFI bridge surface |
| 81 | + (one umbrella binding header, no per-call extern). *) |
| 82 | +let codegen_only_names = |
| 83 | + [ "len"; "panic"; "get"; "set"; "slice"; "show"; "error"; "make_ref"; |
| 84 | + "int_to_string"; "float_to_string"; "string_to_int"; "parse_int"; |
| 85 | + "parse_float"; "int_to_char"; "char_to_int"; |
| 86 | + "string_length"; "string_sub"; "string_get"; "string_find"; |
| 87 | + "string_char_code_at"; "string_from_char_code"; |
| 88 | + "to_lowercase"; "to_uppercase"; "trim"; |
| 89 | + "http_request"; |
| 90 | + "hpm_json_array_get"; "hpm_json_array_len"; "hpm_json_bool"; |
| 91 | + "hpm_json_escape_string"; "hpm_json_float"; "hpm_json_free"; |
| 92 | + "hpm_json_int"; "hpm_json_object_get"; "hpm_json_parse"; |
| 93 | + "hpm_json_string"; "hpm_json_type"; |
| 94 | + ] |
| 95 | + |
| 96 | +let test_codegen_subset_of_stdlib () = |
| 97 | + let codegen_src = read_file codegen_path in |
| 98 | + let start_marker = "let deno_builtins" in |
| 99 | + let s_idx = |
| 100 | + try Str.search_forward (Str.regexp_string start_marker) codegen_src 0 |
| 101 | + with Not_found -> |
| 102 | + failwith "`let deno_builtins` not found in lib/codegen_deno.ml" |
| 103 | + in |
| 104 | + let after = String.sub codegen_src s_idx (String.length codegen_src - s_idx) in |
| 105 | + let codegen_names = extract_names codegen_builtin_re after in |
| 106 | + |
| 107 | + let stdlib_names = |
| 108 | + list_stdlib_affine () |
| 109 | + |> List.concat_map (fun p -> extract_names stdlib_extern_re (read_file p)) |
| 110 | + in |
| 111 | + let stdlib_set = List.sort_uniq compare stdlib_names in |
| 112 | + let codegen_only_set = List.sort_uniq compare codegen_only_names in |
| 113 | + |
| 114 | + let missing = |
| 115 | + List.filter |
| 116 | + (fun n -> not (List.mem n stdlib_set) && not (List.mem n codegen_only_set)) |
| 117 | + codegen_names |
| 118 | + |> List.sort_uniq compare |
| 119 | + in |
| 120 | + match missing with |
| 121 | + | [] -> () |
| 122 | + | xs -> |
| 123 | + let msg = |
| 124 | + Printf.sprintf |
| 125 | + "codegen registers Deno-ESM builtins with no matching `extern fn` under stdlib/*.affine and no entry in the codegen-only allowlist: %s\n\ |
| 126 | + For each name: either add `pub extern fn NAME(...)` to the relevant `stdlib/*.affine` module, OR add it to `codegen_only_names` in this test if it is a true compiler-internal name." |
| 127 | + (String.concat ", " xs) |
| 128 | + in |
| 129 | + Alcotest.fail msg |
| 130 | + |
| 131 | +let tests = |
| 132 | + [ ("codegen builtins subset of stdlib extern decls", |
| 133 | + `Quick, |
| 134 | + test_codegen_subset_of_stdlib) ] |
0 commit comments