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
16 changes: 6 additions & 10 deletions affinescript-deno-test/cli.affine
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
// SPDX-License-Identifier: MPL-2.0
// Ported via Harvard Engine mechanical processor
// Ported via Harvard Engine (Semantic pass)

module cli;

// TODO: Complete semantic implementation

/* === ORIGINAL TYPESCRIPT CONTEXT ===
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
//
Expand All @@ -27,7 +24,7 @@ module cli;
import { compileToWasm } from "./lib/compile.ts";
import { discoverTestFiles } from "./lib/discover.ts";

function usage(): never {
fn usage(): never {
console.error(
"Usage: deno run --allow-read --allow-run --allow-env cli.ts <root>\n" +
"\n" +
Expand All @@ -39,10 +36,10 @@ function usage(): never {
}

if (import.meta.main) {
const root = Deno.args[0];
let root = Deno.args[0];
if (!root) usage();

const sources = await discoverTestFiles(root);
let sources = await discoverTestFiles(root);
if (sources.length === 0) {
console.error(`No *_test.affine files found under ${root}`);
Deno.exit(1);
Expand All @@ -51,14 +48,13 @@ if (import.meta.main) {
console.log(`Discovered ${sources.length} test file(s):`);
for (const source of sources) {
try {
const wasm = await compileToWasm(source);
let wasm = await compileToWasm(source);
console.log(` ✓ ${source} → ${wasm}`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
let message = error instanceof Error ? error.message : String(error);
console.error(` ✗ ${source}\n ${message}`);
Deno.exit(1);
}
}
}

==================================== */
6 changes: 1 addition & 5 deletions affinescript-deno-test/example/smoke_driver.affine
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
// SPDX-License-Identifier: MPL-2.0
// Ported via Harvard Engine mechanical processor
// Ported via Harvard Engine (Semantic pass)

module smoke_driver;

// TODO: Complete semantic implementation

/* === ORIGINAL TYPESCRIPT CONTEXT ===
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
//
Expand All @@ -21,4 +18,3 @@ import { runAll } from "../mod.ts";

await runAll(new URL("./", import.meta.url).pathname);

==================================== */
26 changes: 11 additions & 15 deletions affinescript-deno-test/lib/compile.affine
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
// SPDX-License-Identifier: MPL-2.0
// Ported via Harvard Engine mechanical processor
// Ported via Harvard Engine (Semantic pass)

module compile;

// TODO: Complete semantic implementation

/* === ORIGINAL TYPESCRIPT CONTEXT ===
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
//
Expand All @@ -14,16 +11,16 @@ module compile;
// Wraps the `affinescript compile` CLI. Given a `.affine` source file,
// produces a sibling `.wasm` file and returns its absolute path.
//
// The `AFFINESCRIPT_BIN` env var overrides the default path to the compiler.
// The `AFFINESCRIPT_BIN` env let overrides the default path to the compiler.
// Default is the local dev-build at developer-ecosystem/nextgen-languages/
// affinescript/_build/install/default/bin/affinescript (useful while the
// compiler is not on $PATH).

const DEFAULT_BIN =
let DEFAULT_BIN =
"/var/mnt/eclipse/repos/developer-ecosystem/nextgen-languages/affinescript/_build/install/default/bin/affinescript";

/** Absolute path to the `affinescript` compiler binary. */
export function resolveCompilerPath(): string {
fn resolveCompilerPath(): string {
return Deno.env.get("AFFINESCRIPT_BIN") ?? DEFAULT_BIN;
}

Expand All @@ -32,29 +29,29 @@ export function resolveCompilerPath(): string {
* path to the emitted `.wasm`. Throws with compiler stderr if compilation
* fails.
*/
export async function compileToWasm(sourcePath: string): Promise<string> {
const absolute = sourcePath.startsWith("/")
async fn compileToWasm(sourcePath: string): string {
let absolute = sourcePath.startsWith("/")
? sourcePath
: `${Deno.cwd()}/${sourcePath}`;

const wasmPath = absolute.replace(/\.(affine|afs|rattle|pyaff|jsaff)$/, ".wasm");
let wasmPath = absolute.replace(/\.(affine|afs|rattle|pyaff|jsaff)$/, ".wasm");
if (wasmPath === absolute) {
throw new Error(
`compileToWasm: source file must end in .affine / .afs / .rattle / .pyaff / .jsaff — got ${sourcePath}`,
);
}

const bin = resolveCompilerPath();
const cmd = new Deno.Command(bin, {
let bin = resolveCompilerPath();
let cmd = new Deno.Command(bin, {
args: ["compile", absolute, "-o", wasmPath],
stdout: "piped",
stderr: "piped",
});

const { code, stdout, stderr } = await cmd.output();
if (code !== 0) {
const out = new TextDecoder().decode(stdout);
const err = new TextDecoder().decode(stderr);
let out = new TextDecoder().decode(stdout);
let err = new TextDecoder().decode(stderr);
throw new Error(
`affinescript compile failed (exit ${code}) for ${sourcePath}\n` +
`STDOUT:\n${out}\nSTDERR:\n${err}`,
Expand All @@ -64,4 +61,3 @@ export async function compileToWasm(sourcePath: string): Promise<string> {
return wasmPath;
}

==================================== */
16 changes: 6 additions & 10 deletions affinescript-deno-test/lib/discover.affine
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
// SPDX-License-Identifier: MPL-2.0
// Ported via Harvard Engine mechanical processor
// Ported via Harvard Engine (Semantic pass)

module discover;

// TODO: Complete semantic implementation

/* === ORIGINAL TYPESCRIPT CONTEXT ===
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
//
// affinescript-deno-test: discover.ts
//
// Glob-based discovery of AffineScript test files.
// Default convention: any file matching `*_test.affine` or `*.test.affine`.
// Default convention: unknown file matching `*_test.affine` or `*.test.affine`.

import { walk } from "jsr:@std/fs@1/walk";

/** Default regex for matching AffineScript test files by filename. */
export const DEFAULT_TEST_PATTERN = /(?:_test|\.test)\.(?:affine|afs|rattle|pyaff|jsaff)$/;
let DEFAULT_TEST_PATTERN = /(?:_test|\.test)\.(?:affine|afs|rattle|pyaff|jsaff)$/;

/**
* Recursively walk `root` and return absolute paths of files matching
* `pattern` (default: `*_test.affine` or `*.test.affine`).
*/
export async function discoverTestFiles(
async fn discoverTestFiles(
root: string,
pattern: RegExp = DEFAULT_TEST_PATTERN,
): Promise<string[]> {
const absoluteRoot = root.startsWith("/") ? root : `${Deno.cwd()}/${root}`;
): string[] {
let absoluteRoot = root.startsWith("/") ? root : `${Deno.cwd()}/${root}`;
const matches: string[] = [];

for await (const entry of walk(absoluteRoot, { includeDirs: false, match: [pattern] })) {
Expand All @@ -38,4 +35,3 @@ export async function discoverTestFiles(
return matches;
}

==================================== */
67 changes: 31 additions & 36 deletions affinescript-deno-test/lib/runner.affine
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
// SPDX-License-Identifier: MPL-2.0
// Ported via Harvard Engine mechanical processor
// Ported via Harvard Engine (Semantic pass)

module runner;

// TODO: Complete semantic implementation

/* === ORIGINAL TYPESCRIPT CONTEXT ===
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
//
// affinescript-deno-test: runner.ts
//
// Loads a compiled AffineScript WASM module and wraps every exported function
// whose name starts with `test_` as a Deno.test() case. A test passes when
// the function returns `true`, fails when it returns `false`.
// the fn returns `true`, fails when it returns `false`.
//
// Convention (v0.2.0): each `.affine` file may define multiple tests via
// the `pub fn test_<name>() -> Bool` syntax. Every `pub fn test_*` export
// becomes a separate Deno.test() case. Non-`pub` helpers stay internal to
// the `pub fn test_<name>() -> Bool` syntax. Every `pub fn test_*` // becomes a separate Deno.test() case. Non-`pub` helpers stay internal to
// the module. This relies on the AffineScript compiler honouring `fd_vis`
// in its WASM-export decision (commit ce324fa, both codegen.ml and
// in its WASM-decision (commit ce324fa, both codegen.ml and
// codegen_gc.ml).
//
// Uses the existing @hyperpolymath/affine-js bridge for WASM loading and
Expand All @@ -29,23 +25,23 @@ module runner;
import { AffineModule } from "@hyperpolymath/affine-js";

/** Convention: every `pub fn` whose name begins with this prefix is a test. */
export const TEST_PREFIX = "test_";
let TEST_PREFIX = "test_";

/** Result shape returned by AffineScript Bool exports (via affine-js). */
interface BoolValue {
struct BoolValue {
kind: "bool";
value: boolean;
}

/**
* Derive the Deno.test() case name from the file basename + export name.
* For a wasm at `/path/to/math_test.wasm` with export `test_add`, yields
* Derive the Deno.test() case name from the file basename + name.
* For a wasm at `/path/to/math_test.wasm` with `test_add`, yields
* `math / add`.
*/
function caseName(wasmPath: string, exportName: string): string {
const base = wasmPath.split("/").pop() ?? wasmPath;
const fileStem = base.replace(/\.wasm$/, "").replace(/(_test|\.test)$/, "");
const caseStem = exportName.replace(/^test_/, "");
fn caseName(wasmPath: string, exportName: string): string {
let base = wasmPath.split("/").pop() ?? wasmPath;
let fileStem = base.replace(/\.wasm$/, "").replace(/(_test|\.test)$/, "");
let caseStem = exportName.replace(/^test_/, "");
return `${fileStem} / ${caseStem}`;
}

Expand All @@ -55,7 +51,7 @@ function caseName(wasmPath: string, exportName: string): string {
* do not use IO. For test modules that only return Bool, we can satisfy this
* with a no-op that reports `0` bytes written on every call.
*/
function makeWasiStub(): WebAssembly.ModuleImports {
fn makeWasiStub(): WebAssembly.ModuleImports {
return {
fd_write: (
_fd: number,
Expand All @@ -71,22 +67,22 @@ function makeWasiStub(): WebAssembly.ModuleImports {
}

/**
* Register a Deno.test() case for every `test_*` export in the WASM module
* Register a Deno.test() case for every `test_*` in the WASM module
* at `wasmPath`. Path should be absolute; relative paths resolve against CWD.
*
* Returns the number of tests registered. Throws if no `test_*` exports
* are found (indicating a misconfigured file — at least one `pub fn test_*`
* is expected).
*/
export async function registerTestsFromWasm(wasmPath: string): Promise<number> {
const absolute = wasmPath.startsWith("/")
async fn registerTestsFromWasm(wasmPath: string): number {
let absolute = wasmPath.startsWith("/")
? wasmPath
: `${Deno.cwd()}/${wasmPath}`;

const bytes = await Deno.readFile(absolute);
const wasmMod = await WebAssembly.compile(bytes);
const neededImports = WebAssembly.Module.imports(wasmMod);
const needsWasi = neededImports.some((i) => i.module === "wasi_snapshot_preview1");
let bytes = await Deno.readFile(absolute);
let wasmMod = await WebAssembly.compile(bytes);
let neededImports = WebAssembly.Module.imports(wasmMod);
let needsWasi = neededImports.some((i) => i.module === "wasi_snapshot_preview1");

// AffineModule.fromBytes only supplies imports under the "env" module key,
// so when WASI is required we must take the alternative path: raw
Expand All @@ -95,8 +91,8 @@ export async function registerTestsFromWasm(wasmPath: string): Promise<number> {
return await registerTestsWithWasi(bytes, wasmPath);
}

const mod = await AffineModule.fromBytes(bytes);
const testExports = mod.functionExports.filter((name: string) =>
let mod = await AffineModule.fromBytes(bytes);
let testExports = mod.functionExports.filter((name: string) =>
name.startsWith(TEST_PREFIX)
);

Expand All @@ -110,7 +106,7 @@ export async function registerTestsFromWasm(wasmPath: string): Promise<number> {

for (const exportName of testExports) {
Deno.test(caseName(wasmPath, exportName), () => {
const result = mod.call(exportName, { returnType: "bool" }) as BoolValue;
let result = mod.call(exportName, { returnType: "bool" }) as BoolValue;
if (result.kind !== "bool") {
throw new Error(
`test '${exportName}' returned non-bool value: ${JSON.stringify(result)}`,
Expand All @@ -129,39 +125,39 @@ export async function registerTestsFromWasm(wasmPath: string): Promise<number> {
* wasi_snapshot_preview1. Bypasses AffineModule because its constructor
* only accepts imports under the "env" module key.
*/
async function registerTestsWithWasi(
async fn registerTestsWithWasi(
bytes: Uint8Array,
wasmPath: string,
): Promise<number> {
): number {
// Copy into a fresh ArrayBuffer-backed Uint8Array so the TS BufferSource
// overload matches (Deno's Uint8Array default-types to ArrayBufferLike,
// which the WebAssembly.instantiate overload rejects).
const buf = new Uint8Array(bytes.byteLength);
let buf = new Uint8Array(bytes.byteLength);
buf.set(bytes);
const { instance } = await WebAssembly.instantiate(buf.buffer, {
env: {},
wasi_snapshot_preview1: makeWasiStub(),
});

const testExports = Object.keys(instance.exports).filter(
let testExports = Object.keys(instance.exports).filter(
(name) =>
typeof instance.exports[name] === "function" &&
name.startsWith(TEST_PREFIX),
);

if (testExports.length === 0) {
const available = Object.keys(instance.exports).join(", ");
let available = Object.keys(instance.exports).join(", ");
throw new Error(
`affinescript-deno-test: no '${TEST_PREFIX}*' function exports found in ${wasmPath}. ` +
`affinescript-deno-test: no '${TEST_PREFIX}*' fn exports found in ${wasmPath}. ` +
`Available: [${available}]. ` +
`Each test must be declared as 'pub fn test_<name>() -> Bool'.`,
);
}

for (const exportName of testExports) {
const fn = instance.exports[exportName] as () => number;
let fn = instance.exports[exportName] as () => number;
Deno.test(caseName(wasmPath, exportName), () => {
const raw = fn();
let raw = fn();
// AffineScript compiles Bool to i32 (0 = false, 1 = true).
if (raw !== 0 && raw !== 1) {
throw new Error(
Expand All @@ -177,4 +173,3 @@ async function registerTestsWithWasi(
return testExports.length;
}

==================================== */
Loading
Loading