Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions lib/codegen_deno.ml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,19 @@ const __as_readDirNames = (p) => {
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 || [])));
Expand Down Expand Up @@ -400,6 +413,22 @@ let () =
b "kbString" (fun a -> Printf.sprintf "(Number(%s) / 1024).toFixed(2)" (arg 0 a));
(* ---- misc host ---- *)
b "dateNow" (fun _ -> "Date.now()");
(* `new Date().toISOString()` — UTC ISO-8601 timestamp string. Distinct
from `dateNow()` which returns epoch millis as Int. *)
b "dateNowIso" (fun _ -> "(new Date().toISOString())");
(* `Deno.args` — CLI argument vector (excludes argv[0]). *)
b "args" (fun _ -> "Deno.args");
(* `Deno.exit(code)` — terminate process. Never returns. *)
b "exit" (fun a -> Printf.sprintf "Deno.exit(%s)" (arg 0 a));
(* `console.error(s)` — stderr write. Returns 0 for chaining; the
comma-expression preserves the AffineScript Int contract. *)
b "consoleError" (fun a -> Printf.sprintf "(console.error(%s), 0)" (arg 0 a));
(* Recursive file walk — depth-first, returns every file path under
`root`. Filtering by extension is the caller's responsibility. *)
b "walkRecursive" (fun a -> Printf.sprintf "__as_walkRecursive(%s)" (arg 0 a));
(* `new RegExp(pat).test(s)` — minimal regex surface. Invalid `pat`
throws at call time (RegExp constructor error). *)
b "regexMatch" (fun a -> Printf.sprintf "__as_regexMatch(%s, %s)" (arg 0 a) (arg 1 a));
b "wasmInstance" (fun a -> Printf.sprintf "__as_wasmInstance(%s)" (arg 0 a));
b "wasmCall" (fun a -> Printf.sprintf "__as_wasmCall(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
(* ---- motion (bindings #4) ---- *)
Expand Down
33 changes: 33 additions & 0 deletions stdlib/Deno.affine
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ pub extern fn readDirNames(path: String) -> [String];
/// `Deno.statSync(path).size` in bytes.
pub extern fn statSize(path: String) -> Int;

/// 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];

// ── Path ───────────────────────────────────────────────────────────

/// Single-segment join with a `/` separator (idempotent on a trailing
Expand Down Expand Up @@ -111,6 +116,34 @@ pub extern fn kbString(bytes: Int) -> String;
/// `Date.now()` — epoch millis (used for timestamped report names).
pub extern fn dateNow() -> Int;

/// `new Date().toISOString()` — UTC ISO-8601 timestamp string
/// (e.g. `"2026-05-30T12:34:56.789Z"`). Distinct from `dateNow()` which
/// returns epoch millis as `Int`.
pub extern fn dateNowIso() -> String;

// ── CLI ────────────────────────────────────────────────────────────

/// `Deno.args` — command-line arguments (excludes argv[0]).
pub extern fn args() -> [String];

/// `Deno.exit(code)` — terminate the process with `code`. Never returns;
/// the `Int` return type is for type-level compatibility with `if/else`
/// arms that flow through `exit` in their non-returning branch.
pub extern fn exit(code: Int) -> Int;

// ── Diagnostics ────────────────────────────────────────────────────

/// `console.error(s)` — write to stderr. (Use `print`/`println` for
/// stdout.) Returns 0 for chaining.
pub extern fn consoleError(s: String) -> Int;

// ── Regex ──────────────────────────────────────────────────────────

/// `new RegExp(pat).test(s)` — true iff `s` matches the JS regex source
/// `pat`. Minimal regex surface; for extraction or replace, add a
/// specialised extern. Invalid `pat` throws at call time.
pub extern fn regexMatch(s: String, pat: String) -> Bool;

/// `(Number(bytes) / 1024).toFixed(2)` — kilobyte display string.
pub extern fn numToFixed2(bytes: Int) -> String;

Expand Down
48 changes: 48 additions & 0 deletions tests/codegen-deno/deno_scripting.affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// SPDX-License-Identifier: MPL-2.0
// issue #122 follow-up — Deno-scripting surface (campaign #239 STEP 3 first-cut).
//
// Exercises the new externs added to stdlib/Deno.affine to unblock
// estate TS-script ports: recursive walk, args, exit, ISO timestamp,
// stderr, and minimal regex. The harness stubs `Deno.readDirSync` (for
// walkRecursive), `Deno.args`, `Deno.exit`, and captures `console.error`
// so nothing touches the real process or filesystem.
//
// Signatures stay in primitives (String/[String]/Int/Bool) so the
// fixture is self-contained and consumes only the new lowerings.

use Deno::{ walkRecursive, args, exit, dateNowIso, consoleError, regexMatch };

pub fn count_walked(root: String) -> Int {
let entries = walkRecursive(root);
len(entries)
}

pub fn first_walked(root: String) -> String {
let entries = walkRecursive(root);
if len(entries) == 0 {
return "";
}
return entries[0];
}

pub fn arg_count() -> Int {
let av = args();
len(av)
}

pub fn iso_starts_with_year() -> Bool {
let s = dateNowIso();
// ISO-8601 timestamps lead with a four-digit year, then `-`.
regexMatch(s, "^[0-9]{4}-")
}

pub fn is_pa_code(cat: String) -> Bool {
regexMatch(cat, "^PA[0-9]{3}")
}

pub fn warn_then_zero(msg: String) -> Int {
let _ = consoleError(msg);
return 0;
}

pub fn exit_with(code: Int) -> Int = exit(code);
74 changes: 74 additions & 0 deletions tests/codegen-deno/deno_scripting.harness.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: MPL-2.0
// issue #122 follow-up — Node-ESM harness for new Deno-scripting externs.
//
// Stubs only what the new surface needs: a tiny in-memory FS for
// `Deno.readDirSync` (so `walkRecursive` traverses it deterministically),
// `Deno.args` / `Deno.exit`, and captures `console.error`.

import assert from "node:assert/strict";

// ── In-memory FS stub for walkRecursive ─────────────────────────────
// Shape: { "/root": ["a", "b/", ".hidden"], "/root/b": ["c"] }
const fs = {
"/root": [{ name: "a.txt", isFile: true, isDirectory: false },
{ name: "sub", isFile: false, isDirectory: true }],
"/root/sub": [{ name: "b.txt", isFile: true, isDirectory: false },
{ name: "deeper", isFile: false, isDirectory: true }],
"/root/sub/deeper": [{ name: "c.txt", isFile: true, isDirectory: false }],
"/empty": [],
};

globalThis.Deno = globalThis.Deno || {};
globalThis.Deno.readDirSync = (path) => {
const entries = fs[path];
if (!entries) throw new Error(`stub: no such dir ${path}`);
return entries;
};

// args / exit stubs — exit captures the code instead of terminating.
let lastExit = null;
globalThis.Deno.args = ["alpha", "beta", "gamma"];
globalThis.Deno.exit = (code) => { lastExit = code; return code; };

// Capture stderr writes from consoleError.
const stderrLog = [];
const origError = console.error;
console.error = (...a) => { stderrLog.push(a.join(" ")); };

const {
count_walked,
first_walked,
arg_count,
iso_starts_with_year,
is_pa_code,
warn_then_zero,
exit_with,
} = await import("./deno_scripting.deno.js");

// walkRecursive — depth-first across nested dirs.
assert.equal(count_walked("/root"), 3, "walkRecursive finds 3 files (a.txt + b.txt + c.txt)");
assert.equal(first_walked("/root"), "/root/a.txt", "walkRecursive depth-first leading entry");
assert.equal(count_walked("/empty"), 0, "walkRecursive on empty dir returns []");

// Deno.args
assert.equal(arg_count(), 3, "Deno.args lowered as [String]");

// ISO timestamp shape — real new Date(), so just check leading year.
assert.equal(iso_starts_with_year(), true, "dateNowIso starts with 4-digit year");

// Regex
assert.equal(is_pa_code("PA001"), true, "regexMatch matches PA001");
assert.equal(is_pa_code("PA42"), false, "regexMatch rejects PA42 (needs 3 digits)");
assert.equal(is_pa_code("UnsafeCode"), false, "regexMatch rejects bare category name");

// consoleError captured to stderr
assert.equal(warn_then_zero("test warning"), 0, "consoleError returns 0");
assert.equal(stderrLog.length, 1, "consoleError went to stderr");
assert.equal(stderrLog[0], "test warning", "consoleError payload preserved");

// exit captured (doesn't actually terminate the harness because we stubbed it)
exit_with(2);
assert.equal(lastExit, 2, "Deno.exit lowered correctly");

console.error = origError;
console.log("deno_scripting.harness.mjs OK");
Loading