diff --git a/lib/codegen_deno.ml b/lib/codegen_deno.ml index 22c0cb0..51c2809 100644 --- a/lib/codegen_deno.ml +++ b/lib/codegen_deno.ml @@ -513,6 +513,14 @@ let () = b "ensureDir" (fun a -> Printf.sprintf "__as_ensureDir(%s)" (arg 0 a)); b "readDirNames" (fun a -> Printf.sprintf "__as_readDirNames(%s)" (arg 0 a)); b "statSize" (fun a -> Printf.sprintf "Deno.statSync(%s).size" (arg 0 a)); + b "statIsFile" (fun a -> Printf.sprintf "Deno.statSync(%s).isFile" (arg 0 a)); + b "statIsDirectory" (fun a -> Printf.sprintf "Deno.statSync(%s).isDirectory" (arg 0 a)); + b "bytesLength" (fun a -> Printf.sprintf "(%s).length" (arg 0 a)); + b "bytesByteAt" (fun a -> Printf.sprintf "(%s)[%s]" (arg 0 a) (arg 1 a)); + b "bytesAsciiSlice" (fun a -> Printf.sprintf "String.fromCharCode(...(%s).slice(%s, %s))" (arg 0 a) (arg 1 a) (arg 2 a)); + (* `import.meta.url` — only legal at module top level, which the + Deno-ESM backend's output already is. *) + b "importMetaUrl" (fun _ -> "import.meta.url"); b "pathJoin" (fun a -> Printf.sprintf "__as_pathJoin(%s, %s)" (arg 0 a) (arg 1 a)); b "isNotFound" (fun a -> Printf.sprintf "__as_isNotFound(%s)" (arg 0 a)); (* ---- JSON ---- *) @@ -1238,6 +1246,13 @@ and gen_try_stmt ctx body catch finally = and gen_stmt ctx (stmt : stmt) : string = match stmt with + | StmtLet { sl_pat = PatWildcard _; sl_value; _ } -> + (* `let _ = X` evaluates X for side effects and drops the value. + Emitting `const _ = X;` produces a JS SyntaxError on the second + occurrence in the same scope ("Identifier '_' has already been + declared"). Drop the binding entirely; the bare expression + statement keeps the semantics. *) + gen_expr ctx sl_value ^ ";" | StmtLet { sl_pat; sl_value; sl_mut; sl_quantity = _; sl_ty = _ } -> let kw = if sl_mut then "let" else "const" in let js = kw ^ " " ^ gen_pattern ctx sl_pat ^ " = " diff --git a/stdlib/Deno.affine b/stdlib/Deno.affine index a69c0b8..80951da 100644 --- a/stdlib/Deno.affine +++ b/stdlib/Deno.affine @@ -68,11 +68,39 @@ pub extern fn readDirNames(path: String) -> [String]; /// `Deno.statSync(path).size` in bytes. pub extern fn statSize(path: String) -> Int; +/// `Deno.statSync(path).isFile` — true if `path` is a regular file. +/// Throws on a missing path (pair with `isNotFound` for the absent case). +pub extern fn statIsFile(path: String) -> Bool; + +/// `Deno.statSync(path).isDirectory` — true if `path` is a directory. +/// Throws on a missing path (pair with `isNotFound` for the absent case). +pub extern fn statIsDirectory(path: String) -> Bool; + /// Recursive walk under `root` — every file path beneath it, depth-first. /// Mirrors `std/fs/walk` for the common case (no glob filter; callers /// filter by extension). Throws on a missing root via `Deno.readDirSync`. pub extern fn walkRecursive(root: String) -> [String]; +// ── Bytes accessors ──────────────────────────────────────────────── +// +// The opaque `Bytes` type (a Uint8Array under the hood) gets minimal +// inspectors so AffineScript can peek at file magic, parse fixed-width +// binary headers, and recover ASCII slices without crossing back through +// `readTextFile`. Bounds-check via `bytesLength` — `bytesByteAt` with +// an out-of-range index returns `undefined → NaN` coerced to 0. + +/// `b.length` — number of bytes. +pub extern fn bytesLength(b: Bytes) -> Int; + +/// `b[i]` — byte value at index `i` (0..255). Bounds-check via `bytesLength`. +pub extern fn bytesByteAt(b: Bytes, i: Int) -> Int; + +/// `String.fromCharCode(...b.slice(start, end))` — decode `[start, end)` +/// as a Latin-1 / ASCII-safe string. Each byte becomes one code point +/// (0..255). Use this for ASCII-only headers (PDF magic, MZ, ELF, etc.); +/// for full UTF-8, prefer `readTextFile` directly. +pub extern fn bytesAsciiSlice(b: Bytes, start: Int, end: Int) -> String; + // ── Path ─────────────────────────────────────────────────────────── /// Single-segment join with a `/` separator (idempotent on a trailing @@ -121,6 +149,15 @@ pub extern fn dateNow() -> Int; /// returns epoch millis as `Int`. pub extern fn dateNowIso() -> String; +// ── Module identity ──────────────────────────────────────────────── + +/// `import.meta.url` — the absolute URL of the importing module. The JS +/// idiom for "find my own location" (cf. `__dirname` / `__filename`). At +/// Deno-ESM top level, lowers to the bare `import.meta.url` expression; +/// callers parse it (`new URL(...)`/`fileURLToPath`/string split) for +/// directory-relative behaviour. +pub extern fn importMetaUrl() -> String; + // ── CLI ──────────────────────────────────────────────────────────── /// `Deno.args` — command-line arguments (excludes argv[0]). diff --git a/stdlib/string.affine b/stdlib/string.affine index 8e6996d..f09a072 100644 --- a/stdlib/string.affine +++ b/stdlib/string.affine @@ -68,7 +68,7 @@ fn starts_with(s: String, prefix: String) -> Bool { } /// Check if string ends with the given suffix -fn ends_with(s: String, suffix: String) -> Bool { +pub fn ends_with(s: String, suffix: String) -> Bool { let slen = len(s); let sfxlen = len(suffix); if sfxlen > slen { diff --git a/tests/codegen-deno/deno_scripting_part2.affine b/tests/codegen-deno/deno_scripting_part2.affine new file mode 100644 index 0000000..d2d8714 --- /dev/null +++ b/tests/codegen-deno/deno_scripting_part2.affine @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MPL-2.0 +// campaign #239 STEP 3 — second batch of Deno-scripting stdlib gaps +// surfaced during STEP 2 ports (panic-attack#82, session-sentinel#25, +// tropical-resource-typing#15, nafa-app#23). +// +// New externs / fixes exercised: +// - statIsFile / statIsDirectory (gap 3) +// - bytesLength / bytesByteAt / +// bytesAsciiSlice (gap 4) +// - importMetaUrl (gap 5) +// - let _ = X wildcard binding emits +// a bare expression statement (gap 7) +// +// Signatures stay in primitives so the harness can stub the host. + +use Deno::{ statIsFile, statIsDirectory, bytesLength, bytesByteAt, bytesAsciiSlice, importMetaUrl, readFileBytes }; + +pub fn classify_path(p: String) -> Int { + if statIsFile(p) { + 1 + } else { + if statIsDirectory(p) { + 2 + } else { + 0 + } + } +} + +pub fn first_byte(p: String) -> Int { + let b = readFileBytes(p); + if bytesLength(b) == 0 { + 0 + } else { + bytesByteAt(b, 0) + } +} + +pub fn header_string(p: String, n: Int) -> String { + let b = readFileBytes(p); + let lim = bytesLength(b); + let end = if n > lim { lim } else { n }; + bytesAsciiSlice(b, 0, end) +} + +pub fn module_url_has_scheme() -> Bool { + let u = importMetaUrl(); + // Any well-formed module URL starts with `file:` or `http`. + len(u) > 4 +} + +fn side(n: Int) -> Int { n + 1 } + +pub fn discard_chain() -> Int { + // Three back-to-back `let _ = side(N)` discards. Before the fix this + // tripped JS `SyntaxError: Identifier '_' has already been declared` + // because the AS pattern lowered to `const _ = ...` thrice. Now they + // lower to bare expression statements. + let _ = side(1); + let _ = side(2); + let _ = side(3); + 42 +} diff --git a/tests/codegen-deno/deno_scripting_part2.deno.js b/tests/codegen-deno/deno_scripting_part2.deno.js new file mode 100644 index 0000000..b87d5c8 --- /dev/null +++ b/tests/codegen-deno/deno_scripting_part2.deno.js @@ -0,0 +1,318 @@ +// Generated by AffineScript compiler (Deno-ESM target, issue #122) +// SPDX-License-Identifier: MPL-2.0 +// ---- AffineScript Deno-ESM runtime ---- +const Some = (value) => ({ tag: "Some", value }); +const None = { tag: "None" }; +const Ok = (value) => ({ tag: "Ok", value }); +const Err = (error) => ({ tag: "Err", error }); +const Unit = null; +const print = (s) => { Deno.stdout.writeSync(new TextEncoder().encode(String(s))); }; +const println = (s) => { console.log(String(s)); }; +// ---- Deno host shims (extern fn lowering targets, issue #122) ---- +// Kept tiny + inlined so emitted modules are genuinely drop-in (no extra +// package to publish or resolve). The same surface is mirrored, for +// standalone `deno test`, by packages/affine-deno/mod.js. +const __as_ensureDir = (p) => { + try { Deno.mkdirSync(p, { recursive: true }); } + catch (e) { if (!(e instanceof Deno.errors.AlreadyExists)) throw e; } +}; +const __as_pathJoin = (a, b) => { + if (a.length === 0) return b; + const sep = a.endsWith("/") || a.endsWith("\\") ? "" : "/"; + return a + sep + b; +}; +const __as_readDirNames = (p) => { + const names = []; + for (const entry of Deno.readDirSync(p)) { + if (entry.isFile) names.push(entry.name); + } + return names; +}; +const __as_isNotFound = (e) => (e instanceof Deno.errors.NotFound); +const __as_walkRecursive = (root) => { + const out = []; + const rec = (dir) => { + for (const entry of Deno.readDirSync(dir)) { + const full = (dir.endsWith("/") ? dir : dir + "/") + entry.name; + if (entry.isFile) out.push(full); + else if (entry.isDirectory) rec(full); + } + }; + rec(root); + return out; +}; +const __as_regexMatch = (s, pat) => new RegExp(pat).test(String(s)); +const __as_wasmInstance = (bytes) => + new WebAssembly.Instance(new WebAssembly.Module(bytes)).exports; +const __as_wasmCall = (exports, name, args) => Number(exports[name](...(args || []))); +// ---- WasmValue (Deno.affine #455 — Tier 1 #5, Option B) ---- +// Opaque tagged value crossing the AS/JS boundary as `{ kind, v }`. +// `kind` is one of "i32" | "i64" | "f32" | "f64". The `v` payload is +// `BigInt` for i64 (preserves precision beyond 2^53), `Number` otherwise. +const __as_wv_i32 = (n) => ({ kind: "i32", v: (Number(n) | 0) }); +const __as_wv_i64 = (n) => ({ kind: "i64", v: BigInt(n) }); +const __as_wv_f32 = (f) => ({ kind: "f32", v: Math.fround(Number(f)) }); +const __as_wv_f64 = (f) => ({ kind: "f64", v: Number(f) }); +const __as_wv_as_int = (v) => { + if (v == null) return 0; + if (typeof v.v === "bigint") { + // i64: truncate to safe-integer Number; caller's responsibility for + // precision-sensitive paths (use wv_kind to detect). + return Number(v.v); + } + // i32 / f32 / f64: truncate toward zero per AS Int semantics. + return (Number(v.v) | 0); +}; +const __as_wv_as_float = (v) => { + if (v == null) return 0; + return typeof v.v === "bigint" ? Number(v.v) : Number(v.v); +}; +const __as_wv_kind = (v) => (v && typeof v.kind === "string") ? v.kind : ""; +const __as_wasm_export_call = (exports, name, args) => { + // Unmarshal AS-side [WasmValue] to raw JS scalars for the wasm call. + const rawArgs = (args || []).map((wv) => { + if (wv == null) return 0; + // i64 payload is BigInt; wasm i64 imports accept BigInt directly. + return wv.v; + }); + const result = exports[name](...rawArgs); + // Wrap return as f64 (lossless for any numeric; callers expecting i32/i64 + // can rebuild via wv_i32(wv_as_int(result)) or inspect wv_kind). + if (typeof result === "bigint") { + return { kind: "i64", v: result }; + } + return { kind: "f64", v: Number(result) }; +}; +// ---- motion (bindings #4): consumer-provided import ---- +// Host JS environment must expose globalThis.__as_motion (the motion +// library or a compatible mock). Tests set it in the harness before +// importing the generated module; production consumers typically do +// `import * as m from "motion"; globalThis.__as_motion = m;` once at +// module-init time. The AffineScript-side externs (stdlib/Motion.affine) +// don't see this indirection — they call __as_motion* helpers directly. +const __as_motionAnimate = (target, keyframes, options) => + globalThis.__as_motion.animate(target, keyframes, options); +const __as_motionAwait = (controls) => + Promise.resolve(controls).then(() => 0); +const __as_motionCancel = (controls) => { + if (controls && typeof controls.cancel === "function") controls.cancel(); + return 0; +}; +// `animateMini` / `tween` / `spring` / `ease` — bindings #4 follow-up +// surface. Each helper resolves the host method on globalThis.__as_motion +// at call time so a mock that only stubs a subset still works for the +// rest (the smoke harness exercises every variant). +const __as_motionAnimateMini = (target, keyframes, options) => + globalThis.__as_motion.animateMini(target, keyframes, options); +const __as_motionTween = (target, from, to, options) => + globalThis.__as_motion.tween(target, from, to, options); +const __as_motionSpring = (target, keyframes, springConfig) => + globalThis.__as_motion.spring(target, keyframes, springConfig); +const __as_motionEase = (name) => + globalThis.__as_motion.ease(name); +// ---- pixi.js (bindings #1): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi (the PIXI namespace +// from `import * as PIXI from "pixi.js"`). Tests set it in the harness +// before importing the generated module. +const __as_pixiAppInit = async (options) => { + const app = new globalThis.__as_pixi.Application(); + await app.init(options); + return app; +}; +const __as_pixiAppCanvas = (app) => app.canvas; +const __as_pixiAppStage = (app) => app.stage; +const __as_pixiAppTicker = (app) => app.ticker; +const __as_pixiAppDestroy = (app) => { app.destroy(); return 0; }; +const __as_pixiContainerNew = () => new globalThis.__as_pixi.Container(); +const __as_pixiContainerAddChild = (p, c) => { p.addChild(c); return 0; }; +const __as_pixiContainerRemoveChild = (p, c) => { p.removeChild(c); return 0; }; +const __as_pixiContainerSetPosition = (c, x, y) => { c.x = x; c.y = y; return 0; }; +const __as_pixiContainerSetVisible = (c, v) => { c.visible = v; return 0; }; +const __as_pixiContainerDestroy = (c) => { c.destroy(); return 0; }; +const __as_pixiSpriteFrom = (t) => new globalThis.__as_pixi.Sprite(t); +// Upcasts are identity — PIXI's class hierarchy makes Sprite/Graphics/ +// Text actual Container subclasses, so the JS object is the same. +const __as_pixiSpriteAsContainer = (s) => s; +const __as_pixiTextureFromUrl = (url) => globalThis.__as_pixi.Texture.from(url); +const __as_pixiGraphicsNew = () => new globalThis.__as_pixi.Graphics(); +const __as_pixiGraphicsRect = (g, x, y, w, h) => { g.rect(x, y, w, h); return 0; }; +const __as_pixiGraphicsFill = (g, color) => { g.fill({ color }); return 0; }; +const __as_pixiGraphicsClear = (g) => { g.clear(); return 0; }; +const __as_pixiGraphicsAsContainer = (g) => g; +const __as_pixiTextNew = (options) => new globalThis.__as_pixi.Text(options); +const __as_pixiTextSetText = (t, content) => { t.text = content; return 0; }; +const __as_pixiTextAsContainer = (t) => t; +const __as_pixiTickerAdd = (t, cb) => { t.add(cb); return 0; }; +const __as_pixiTickerStart = (t) => { t.start(); return 0; }; +const __as_pixiTickerStop = (t) => { t.stop(); return 0; }; +// ---- @pixi/ui (bindings #3): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi_ui (the namespace +// from `import * as PixiUI from "@pixi/ui"`). Tests set it in the +// harness before importing the generated module; production +// consumers typically do once at module-init time. The +// AffineScript-side externs (stdlib/PixiUI.affine) don't see this +// indirection — they call __as_pixiUi* helpers directly. +// +// Upcasts to Container are identity — @pixi/ui's Button / +// FancyButton / Slider / Switch are all real PIXI.Container +// subclasses, so the JS object is the same. +const __as_pixiUiButtonNew = (options) => new globalThis.__as_pixi_ui.Button(options); +const __as_pixiUiButtonOnPress = (b, cb) => { b.onPress.connect(cb); return 0; }; +const __as_pixiUiButtonAsContainer = (b) => b; +const __as_pixiUiFancyButtonNew = (options) => new globalThis.__as_pixi_ui.FancyButton(options); +const __as_pixiUiFancyButtonAsContainer = (b) => b; +const __as_pixiUiSliderNew = (options) => new globalThis.__as_pixi_ui.Slider(options); +const __as_pixiUiSliderOnUpdate = (s, cb) => { s.onUpdate.connect(cb); return 0; }; +const __as_pixiUiSliderAsContainer = (s) => s; +const __as_pixiUiSwitchNew = (options) => new globalThis.__as_pixi_ui.Switch(options); +const __as_pixiUiSwitchOnChange = (sw, cb) => { sw.onChange.connect(cb); return 0; }; +const __as_pixiUiSwitchAsContainer = (sw) => sw; +// ---- @pixi/sound (bindings #2): consumer-provided import ---- +// Host JS environment exposes globalThis.__as_pixi_sound (the `Sound` +// named export from `@pixi/sound`). Tests set it in the harness before +// importing the generated module; production consumers typically do +// `import { Sound } from "@pixi/sound"; globalThis.__as_pixi_sound = Sound;` +// once at module-init time. The AffineScript-side externs +// (stdlib/PixiSound.affine) don't see this indirection — they call +// __as_pixiSound* helpers directly. +const __as_pixiSoundFrom = (url) => globalThis.__as_pixi_sound.from(url); +const __as_pixiSoundPlay = (s) => { s.play(); return 0; }; +const __as_pixiSoundStop = (s) => { s.stop(); return 0; }; +const __as_pixiSoundPause = (s) => { s.pause(); return 0; }; +const __as_pixiSoundResume = (s) => { s.resume(); return 0; }; +const __as_pixiSoundSetVolume = (s, vol) => { s.volume = vol; return 0; }; +const __as_pixiSoundSetLoop = (s, loop) => { s.loop = loop; return 0; }; +// `++` is overloaded (string concat / array concat); `a + b` would +// stringify arrays. Dispatch on shape so stdlib/string.affine's +// `result ++ [x]` and `a ++ b` are both correct. +const __as_concat = (a, b) => Array.isArray(a) ? a.concat(b) : (a + b); +// Honest host/runtime primitives underpinning the AffineScript-level +// stdlib/string.affine (its is_empty/starts_with/ends_with/split/join/ +// replace/... are real AffineScript on top of these). +const __as_strSub = (s, start, n) => String(s).slice(start, start + n); +const __as_strGet = (s, i) => String(s)[i]; +const __as_strFind = (s, n) => String(s).indexOf(n); +const __as_charToInt = (c) => String(c).codePointAt(0); +const __as_intToChar = (n) => String.fromCodePoint(n); +const __as_parseInt = (s) => { + const n = parseInt(String(s), 10); + return Number.isNaN(n) ? None : Some(n); +}; +const __as_parseFloat = (s) => { + const n = parseFloat(String(s)); + return Number.isNaN(n) ? None : Some(n); +}; +const __as_show = (v) => (typeof v === "string" ? v : JSON.stringify(v)); +// ---- Http (issue #160): portable fetch round-trip ---- +// `headers` crosses the boundary as an AffineScript [(String, String)] +// assoc list == JS array of [name, value] pairs. `body` is an +// AffineScript Option == { tag: "Some", value } | { tag: "None" }. +// The result is the `Response` record shape { status, headers, body }. +const __as_httpHeadersToObject = (pairs) => { + const o = {}; + for (const kv of (pairs || [])) o[kv[0]] = kv[1]; + return o; +}; +const __as_httpHeadersFromResponse = (res) => { + const out = []; + res.headers.forEach((value, key) => out.push([key, value])); + return out; +}; +// ---- hpm-json-rsr Zig FFI shims (stdlib/json.affine v0.3) ---- +// `HpmJsonValue` is opaque to AffineScript; on Deno-ESM it's just the +// underlying JS value from JSON.parse. The shims mirror the sentinel +// conventions of the Zig exports so the AffineScript-side wrappers +// (`to_json`, `parse`) behave identically across backends. +const __as_hpmJsonParse = (s) => { + try { return Some(JSON.parse(String(s))); } catch (_e) { return None; } +}; +const __as_hpmJsonFree = (_v) => 0; +const __as_hpmJsonType = (v) => { + if (v === null || v === undefined) return 0; + if (typeof v === "boolean") return 1; + if (typeof v === "number") return Number.isInteger(v) ? 2 : 3; + if (typeof v === "string") return 4; + if (Array.isArray(v)) return 5; + if (typeof v === "object") return 6; + return -1; +}; +const __as_hpmJsonBool = (v) => (typeof v === "boolean" ? (v ? 1 : 0) : -1); +const __as_hpmJsonInt = (v) => + (typeof v === "number" ? Math.trunc(v) : Number.MIN_SAFE_INTEGER); +const __as_hpmJsonFloat = (v) => (typeof v === "number" ? v : NaN); +const __as_hpmJsonString = (v) => (typeof v === "string" ? v : ""); +const __as_hpmJsonObjectGet = (v, k) => { + if (v === null || typeof v !== "object" || Array.isArray(v)) return None; + return Object.prototype.hasOwnProperty.call(v, String(k)) + ? Some(v[String(k)]) : None; +}; +const __as_hpmJsonArrayLen = (v) => (Array.isArray(v) ? v.length : 0); +const __as_hpmJsonArrayGet = (v, i) => { + if (!Array.isArray(v)) return None; + const idx = Number(i); + return (idx >= 0 && idx < v.length) ? Some(v[idx]) : None; +}; +const __as_hpmJsonEscapeString = (s) => { + let out = ""; + const src = String(s); + for (let i = 0; i < src.length; i++) { + const c = src.charCodeAt(i); + if (c === 0x22) out += "\\\""; + else if (c === 0x5c) out += "\\\\"; + else if (c === 0x0a) out += "\\n"; + else if (c === 0x0d) out += "\\r"; + else if (c === 0x09) out += "\\t"; + else if (c === 0x08) out += "\\b"; + else if (c === 0x0c) out += "\\f"; + else if (c < 0x20) out += "\\u00" + c.toString(16).padStart(2, "0"); + else out += src[i]; + } + return out; +}; +const __as_httpFetch = async (url, method, headers, bodyOpt) => { + const init = { method, headers: __as_httpHeadersToObject(headers) }; + if (bodyOpt && bodyOpt.tag === "Some") init.body = bodyOpt.value; + // `globalThis.fetch` explicitly: the stdlib `Http.fetch` compiles to a + // module-level `function fetch`, which would otherwise shadow the host. + const res = await globalThis.fetch(url, init); + const text = await res.text(); + return { + status: res.status, + headers: __as_httpHeadersFromResponse(res), + body: text, + }; +}; +// ---- end runtime ---- + +export function classify_path(p) { + return (Deno.statSync(p).isFile ? (() => { return 1; })() : (() => { return (Deno.statSync(p).isDirectory ? (() => { return 2; })() : (() => { return 0; })()); })()); +} + +export function first_byte(p) { + const b = Deno.readFileSync(p); + return (((b).length === 0) ? (() => { return 0; })() : (() => { return (b)[0]; })()); +} + +export function header_string(p, n) { + const b = Deno.readFileSync(p); + const lim = (b).length; + const end = ((n > lim) ? (() => { return lim; })() : (() => { return n; })()); + return String.fromCharCode(...(b).slice(0, end)); +} + +export function module_url_has_scheme() { + const u = import.meta.url; + return (((u).length) > 4); +} + +function side(n) { + return (n + 1); +} + +export function discard_chain() { + side(1); + side(2); + side(3); + return 42; +} + diff --git a/tests/codegen-deno/deno_scripting_part2.harness.mjs b/tests/codegen-deno/deno_scripting_part2.harness.mjs new file mode 100644 index 0000000..3b635f6 --- /dev/null +++ b/tests/codegen-deno/deno_scripting_part2.harness.mjs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MPL-2.0 +// campaign #239 STEP 3 part 2 — Node-ESM harness for the second wave +// of Deno-scripting externs (stat predicates, byte accessors, module URL) +// plus the `let _ = X` wildcard-binding codegen fix. + +import assert from "node:assert/strict"; + +// ── In-memory FS stub for statSync + readFileSync ─────────────────── +const files = { + "/etc/hosts": { kind: "file", bytes: new Uint8Array([37, 80, 68, 70, 45]) }, // "%PDF-" + "/var/run": { kind: "dir" }, + "/empty.bin": { kind: "file", bytes: new Uint8Array() }, +}; + +globalThis.Deno = globalThis.Deno || {}; +globalThis.Deno.statSync = (path) => { + const e = files[path]; + if (!e) { + const err = new Error(`stub: no such path ${path}`); + err.code = "ENOENT"; + throw err; + } + return { + size: e.kind === "file" ? e.bytes.length : 0, + isFile: e.kind === "file", + isDirectory: e.kind === "dir", + }; +}; +globalThis.Deno.readFileSync = (path) => { + const e = files[path]; + if (!e || e.kind !== "file") throw new Error(`stub: not a file ${path}`); + return e.bytes; +}; + +const { + classify_path, + first_byte, + header_string, + module_url_has_scheme, + discard_chain, +} = await import("./deno_scripting_part2.deno.js"); + +// statIsFile / statIsDirectory +assert.equal(classify_path("/etc/hosts"), 1, "statIsFile true for a regular file"); +assert.equal(classify_path("/var/run"), 2, "statIsDirectory true for a directory"); + +// bytes accessors +assert.equal(first_byte("/etc/hosts"), 37, "bytesByteAt(0) returns first byte (%)"); +assert.equal(first_byte("/empty.bin"), 0, "bytesByteAt fallback on empty stays 0"); + +// bytesAsciiSlice on the PDF magic +assert.equal(header_string("/etc/hosts", 5), "%PDF-", "bytesAsciiSlice decodes Latin-1"); +assert.equal(header_string("/etc/hosts", 99), "%PDF-", "bytesAsciiSlice clamps to length"); +assert.equal(header_string("/empty.bin", 5), "", "bytesAsciiSlice empty when no bytes"); + +// importMetaUrl — at module top level this is the URL of this very harness's +// imported module; should be a non-empty file:/// or http(s):// string. +assert.equal(module_url_has_scheme(), true, "importMetaUrl returns a non-trivial URL"); + +// let _ = X chain — three back-to-back discards must not redeclare `_`. +// The harness reaching this assertion at all is the test (a syntax error +// would have thrown during the dynamic import above). +assert.equal(discard_chain(), 42, "let _ = X chain executes without const-redeclaration"); + +console.log("deno_scripting_part2.harness.mjs OK");