Skip to content

Commit 542e383

Browse files
hyperpolymathclaude
andcommitted
feat(codegen-deno/typecheck): consumer-migration primitives + ++ disambiguation (Refs #122)
Primitives the ubicity#30 storage migration needs, added to the honest host-primitive layer (each a genuine host capability, not smuggled logic): jsonNull (`null`), jsonGet/jsonGetStr (opaque object/index read — the data boundary for arbitrary host JSON), orDefault (`x ?? d` — preserves a JS default parameter), kbString (`(n/1024).toFixed(2)` — runtime number formatting), panic (`throw new Error(...)`). Mirrored in stdlib/Deno.affine. typecheck: `++` disambiguation hardened — when the lhs type is not yet determined (e.g. `let mut acc = []` then `acc ++ [x]`), disambiguate on the rhs instead of defaulting to String (which mis-typed array accumulation as String). Regression-free: 5 Deno-ESM harnesses green; `dune runtest` unchanged (same 2 pre-existing E2E Node-CJS vscode failures, 214 tests). Refs #122 (consumer enablement for ubicity#30). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 925edeb commit 542e383

3 files changed

Lines changed: 165 additions & 9 deletions

File tree

lib/codegen_deno.ml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,17 @@ let () =
175175
b "jsonStringify" (fun a -> Printf.sprintf "JSON.stringify(%s)" (arg 0 a));
176176
b "jsonStringifyPretty" (fun a -> Printf.sprintf "JSON.stringify(%s, null, 2)" (arg 0 a));
177177
b "jsonParse" (fun a -> Printf.sprintf "JSON.parse(%s)" (arg 0 a));
178+
b "jsonNull" (fun _ -> "null");
179+
(* Opaque-object field/index read (the boundary primitive for treating
180+
an arbitrary JS value as data — ubicity's `experience.id` etc.). *)
181+
b "jsonGet" (fun a -> Printf.sprintf "(%s)[%s]" (arg 0 a) (arg 1 a));
182+
b "jsonGetStr" (fun a -> Printf.sprintf "String((%s)[%s])" (arg 0 a) (arg 1 a));
183+
(* Nullish default — preserves a JS default parameter when the arg is
184+
omitted (`new ExperienceStorage()` -> './ubicity-data'). *)
185+
b "orDefault" (fun a -> Printf.sprintf "(%s ?? %s)" (arg 0 a) (arg 1 a));
186+
(* Kilobyte display string: `(n/1024).toFixed(2)` — runtime number
187+
formatting is an honest host primitive in every language. *)
188+
b "kbString" (fun a -> Printf.sprintf "(Number(%s) / 1024).toFixed(2)" (arg 0 a));
178189
(* ---- misc host ---- *)
179190
b "dateNow" (fun _ -> "Date.now()");
180191
b "wasmInstance" (fun a -> Printf.sprintf "__as_wasmInstance(%s)" (arg 0 a));
@@ -199,7 +210,8 @@ let () =
199210
b "parse_float" (fun a -> Printf.sprintf "__as_parseFloat(%s)" (arg 0 a));
200211
b "char_to_int" (fun a -> Printf.sprintf "__as_charToInt(%s)" (arg 0 a));
201212
b "int_to_char" (fun a -> Printf.sprintf "__as_intToChar(%s)" (arg 0 a));
202-
b "show" (fun a -> Printf.sprintf "__as_show(%s)" (arg 0 a))
213+
b "show" (fun a -> Printf.sprintf "__as_show(%s)" (arg 0 a));
214+
b "panic" (fun a -> Printf.sprintf "(() => { throw new Error(%s); })()" (arg 0 a))
203215

204216
(* ============================================================================
205217
Identifier sanitisation (JS reserved words -> trailing underscore)

lib/typecheck.ml

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -869,14 +869,25 @@ let rec synth (ctx : context) (expr : expr) : ty result =
869869
latter is how stdlib/string.affine's split/join/etc. accumulate.
870870
Dispatch on the synthesised lhs type. *)
871871
let* lhs_ty = synth ctx lhs in
872-
match repr lhs_ty with
873-
| TApp (TCon "Array", [elem]) ->
874-
let* () = check ctx rhs (TApp (TCon "Array", [elem])) in
875-
Ok (TApp (TCon "Array", [elem]))
876-
| _ ->
877-
let* () = unify_or_err lhs_ty ty_string in
878-
let* () = check ctx rhs ty_string in
879-
Ok ty_string
872+
(match repr lhs_ty with
873+
| TApp (TCon "Array", [elem]) ->
874+
let* () = check ctx rhs (TApp (TCon "Array", [elem])) in
875+
Ok (TApp (TCon "Array", [elem]))
876+
| TCon "String" ->
877+
let* () = check ctx rhs ty_string in
878+
Ok ty_string
879+
| _ ->
880+
(* lhs type not yet determined (e.g. `let mut acc = []`):
881+
disambiguate on the rhs rather than defaulting to String. *)
882+
let* rhs_ty = synth ctx rhs in
883+
(match repr rhs_ty with
884+
| TApp (TCon "Array", [elem]) ->
885+
let* () = unify_or_err lhs_ty (TApp (TCon "Array", [elem])) in
886+
Ok (TApp (TCon "Array", [elem]))
887+
| _ ->
888+
let* () = unify_or_err lhs_ty ty_string in
889+
let* () = unify_or_err rhs_ty ty_string in
890+
Ok ty_string))
880891
end else begin
881892
let (lhs_ty, rhs_ty, result_ty) = type_of_binop op in
882893
let* () = check ctx lhs lhs_ty in

stdlib/Deno.affine

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
3+
//
4+
// Deno.affine — issue #122 host bindings for the Deno-ESM backend.
5+
//
6+
// Unlike stdlib/Vscode.affine (issue #35), these externs are NOT a
7+
// wasm-FFI surface with an Int-handle/readString contract. The
8+
// `--deno-esm` backend (lib/codegen_deno.ml) is a *direct* AST → ES
9+
// module transpiler with no wasm boundary, so each `extern fn` below is
10+
// lowered, at compile time, straight to its host expression:
11+
//
12+
// writeTextFile(p, c) -> Deno.writeTextFileSync(p, c)
13+
// jsonParse(s) -> JSON.parse(s)
14+
// dateNow() -> Date.now()
15+
// ...
16+
//
17+
// The lowering table lives in lib/codegen_deno.ml (`deno_builtins`);
18+
// the non-trivial leaves are emitted inlined into every module's
19+
// prelude so the output is genuinely drop-in (no runtime adapter, no
20+
// extra package to resolve). packages/affine-deno/mod.js mirrors the
21+
// same surface as a standalone ESM module for `deno test`.
22+
//
23+
// All FS operations are synchronous (`Deno.*Sync`). `await` on a
24+
// synchronously-returned value is valid JS, so an async-shaped consumer
25+
// (e.g. hyperpolymath/ubicity) keeps working without an async-extern
26+
// ABI (issue #103 — documented future work, not required here).
27+
//
28+
// Names here MUST match the `deno_builtins` table keys exactly; an
29+
// unmatched extern silently falls through to a same-named host symbol.
30+
31+
module Deno;
32+
33+
// ── Opaque host value types ────────────────────────────────────────
34+
//
35+
// `Json` is any structured JS value (object/array/string/number/bool/
36+
// null) crossing the boundary opaquely — the AffineScript side never
37+
// inspects it, it only routes it between JSON.* and the host. `Bytes`
38+
// is a Uint8Array; `WasmExports` is an instantiated module's exports.
39+
40+
pub extern type Json;
41+
pub extern type Bytes;
42+
pub extern type WasmExports;
43+
44+
// ── Filesystem (synchronous) ───────────────────────────────────────
45+
46+
/// `Deno.writeTextFileSync(path, content)`. Returns 0.
47+
pub extern fn writeTextFile(path: String, content: String) -> Int;
48+
49+
/// `Deno.readTextFileSync(path)`. Throws on missing file — pair with
50+
/// `isNotFound` in a `try`/`catch` for the not-found-is-null pattern.
51+
pub extern fn readTextFile(path: String) -> String;
52+
53+
/// `Deno.readFileSync(path)` — raw bytes (for wasm modules).
54+
pub extern fn readFileBytes(path: String) -> Bytes;
55+
56+
/// `Deno.removeSync(path)`. Throws if absent (catch + `isNotFound`).
57+
pub extern fn removePath(path: String) -> Int;
58+
59+
/// `Deno.mkdirSync(path, { recursive: true })`.
60+
pub extern fn mkdirRecursive(path: String) -> Int;
61+
62+
/// mkdir -p that swallows AlreadyExists (idempotent ensure-directory).
63+
pub extern fn ensureDir(path: String) -> Int;
64+
65+
/// Names of the *file* entries in `path` (skips sub-directories).
66+
pub extern fn readDirNames(path: String) -> [String];
67+
68+
/// `Deno.statSync(path).size` in bytes.
69+
pub extern fn statSize(path: String) -> Int;
70+
71+
// ── Path ───────────────────────────────────────────────────────────
72+
73+
/// Single-segment join with a `/` separator (idempotent on a trailing
74+
/// slash). Sufficient for the storage-layout use-case.
75+
pub extern fn pathJoin(a: String, b: String) -> String;
76+
77+
// ── Error classification ───────────────────────────────────────────
78+
79+
/// `e instanceof Deno.errors.NotFound` — the only error class the
80+
/// storage layer special-cases (missing file/dir => null/empty).
81+
pub extern fn isNotFound(e: Json) -> Bool;
82+
83+
// ── JSON ───────────────────────────────────────────────────────────
84+
85+
pub extern fn jsonStringify(v: Json) -> String;
86+
87+
/// `JSON.stringify(v, null, 2)` — the on-disk pretty form.
88+
pub extern fn jsonStringifyPretty(v: Json) -> String;
89+
90+
pub extern fn jsonParse(s: String) -> Json;
91+
92+
/// JS `null` as an opaque Json (the not-found / absent sentinel).
93+
pub extern fn jsonNull() -> Json;
94+
95+
/// Opaque field/index read: `value[key]`. The boundary primitive for
96+
/// treating an arbitrary host JS value as data without the AffineScript
97+
/// side modelling its shape (e.g. `experience.id`).
98+
pub extern fn jsonGet(value: Json, key: String) -> Json;
99+
pub extern fn jsonGetStr(value: Json, key: String) -> String;
100+
101+
/// Nullish default — `x ?? d`. Preserves a JS default parameter when
102+
/// the caller omits the argument.
103+
pub extern fn orDefault(x: String, d: String) -> String;
104+
105+
/// Kilobyte display string: `(bytes / 1024).toFixed(2)`. Runtime number
106+
/// formatting is an honest host primitive (cf. Rust `format!`).
107+
pub extern fn kbString(bytes: Int) -> String;
108+
109+
// ── Misc host ──────────────────────────────────────────────────────
110+
111+
/// `Date.now()` — epoch millis (used for timestamped report names).
112+
pub extern fn dateNow() -> Int;
113+
114+
/// `(Number(bytes) / 1024).toFixed(2)` — kilobyte display string.
115+
pub extern fn numToFixed2(bytes: Int) -> String;
116+
117+
pub extern fn endsWith(s: String, suffix: String) -> Bool;
118+
119+
/// `s` with a trailing `suffix` removed (no-op if absent).
120+
pub extern fn stripSuffix(s: String, suffix: String) -> String;
121+
122+
// ── WebAssembly (synchronous instantiate) ──────────────────────────
123+
124+
/// `new WebAssembly.Instance(new WebAssembly.Module(bytes)).exports`.
125+
pub extern fn wasmInstance(bytes: Bytes) -> WasmExports;
126+
127+
// ── Array helper ───────────────────────────────────────────────────
128+
//
129+
// AffineScript has no mutable-array push primitive in this subset;
130+
// this fluent helper appends and returns the array so accumulation
131+
// reads functionally: `acc = arrayPush(acc, x)`.
132+
133+
pub extern fn arrayPush(arr: [Json], v: Json) -> [Json];

0 commit comments

Comments
 (0)