|
| 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