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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ jobs:
run: opam exec -- dune runtest
- name: Run codegen WASM tests
run: opam exec -- ./tools/run_codegen_wasm_tests.sh
- name: Run codegen Deno-ESM tests (issue #122)
# Compiles tests/codegen-deno/*.affine with the --deno-esm backend
# and runs the *.harness.mjs under Node (CI has Node 20, not Deno;
# the Phase 1 fixtures are pure logic so Node ESM exercises them).
- name: Run codegen Bun-ESM tests (historical codegen-deno corpus)
# Compiles tests/codegen-deno/*.affine with the --bun-esm backend
# and runs the *.harness.mjs under Node (CI has Node 20; these
# fixtures mock host objects and do not need the Bun binary).
run: opam exec -- ./tools/run_codegen_deno_tests.sh
- name: Run native Bun-ESM tests (issue #734)
run: opam exec -- ./tools/run_codegen_bun_tests.sh
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ bisect*.coverage
# issue #122: generated Deno-ESM regression outputs (compiled from the
# committed *.affine fixtures by tools/run_codegen_deno_tests.sh).
/tests/codegen-deno/*.deno.js
/tests/codegen-deno/*.bun.js
# Issue #734: generated native Bun-ESM acceptance outputs.
/tests/codegen-bun/*.bun.js
/tests/codegen-bun/backend-conflict.json
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,17 @@ https://semver.org/spec/v2.0.0.html[Semantic Versioning].

=== [Unreleased]

==== Removed

* `--deno-esm` / `-o FILE.deno.js` is a hard error (E0826). Bun-ESM
(`--bun-esm` / `.bun.js`) is the only JS-host ESM target. The flag is
kept so existing scripts fail loudly (Refs #56)

==== Added

* Aspirational idaptik StartupError fixture compiles with real syntax
(`use Console::{log}`, `use Dom::{…}`, tuple-list attrs and
`#{ bg: … }` records) on `--bun-esm` (Refs #56)
* feat(stdlib): Aggregate.affine — SQL group-by + aggregation primitives
— db-theory #3 (7 externs) (PR #527)
* feat(stdlib): Transaction.affine — affine-bounded write-set isolation
Expand Down
54 changes: 11 additions & 43 deletions bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -492,10 +492,8 @@ let repl_cmd_fn () =
let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
deno_esm bun_esm target path output =
let face = resolve_face ~quiet:json face path in
let is_deno = deno_esm || Filename.check_suffix output ".deno.js" in
let is_bun = bun_esm || Filename.check_suffix output ".bun.js" in
if is_deno && is_bun then
let message = "--deno-esm and --bun-esm are mutually exclusive" in
if deno_esm || Filename.check_suffix output ".deno.js" then
let message = "Deno-ESM was removed; use --bun-esm (and a .bun.js output path)" in
if json then
json_finish [{ Affinescript.Json_output.severity = Error;
code = "E0826"; message;
Expand Down Expand Up @@ -536,10 +534,9 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
use the original [prog] because they handle imports natively
via Codegen.gen_imports / the import section. *)
let flat_prog = Affinescript.Module_loader.flatten_imports loader prog in
let is_deno = deno_esm || Filename.check_suffix output ".deno.js" in
let is_bun = bun_esm || Filename.check_suffix output ".bun.js" in
let is_julia = Filename.check_suffix output ".jl" in
let is_js = (not is_deno) && (not is_bun) && Filename.check_suffix output ".js" in
let is_js = (not is_bun) && Filename.check_suffix output ".js" in
let is_c = Filename.check_suffix output ".c" in
let is_wgsl = Filename.check_suffix output ".wgsl" in
let is_faust = Filename.check_suffix output ".dsp" in
Expand Down Expand Up @@ -570,16 +567,6 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
let oc = open_out_bin output in
output_string oc esm_code;
close_out oc
end else if is_deno then begin
match Affinescript.Codegen_deno.codegen_deno flat_prog resolve_ctx.symbols with
| Error msg ->
add { severity = Error; code = "E0824";
message = Printf.sprintf "Deno-ESM codegen error: %s" msg;
span = Affinescript.Span.dummy; help = None; labels = [] }
| Ok esm_code ->
let oc = open_out_bin output in
output_string oc esm_code;
close_out oc
end else if is_julia then begin
match Affinescript.Julia_codegen.codegen_julia flat_prog resolve_ctx.symbols with
| Error msg ->
Expand Down Expand Up @@ -779,10 +766,9 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
cross-module imports for backends that don't have native
module-system support. Wasm/Wasm-GC keep the original [prog]. *)
let flat_prog = Affinescript.Module_loader.flatten_imports loader prog in
let is_deno = deno_esm || Filename.check_suffix output ".deno.js" in
let is_bun = bun_esm || Filename.check_suffix output ".bun.js" in
let is_julia = Filename.check_suffix output ".jl" in
let is_js = (not is_deno) && (not is_bun) && Filename.check_suffix output ".js" in
let is_js = (not is_bun) && Filename.check_suffix output ".js" in
let is_c = Filename.check_suffix output ".c" in
let is_wgsl = Filename.check_suffix output ".wgsl" in
let is_faust = Filename.check_suffix output ".dsp" in
Expand Down Expand Up @@ -814,17 +800,6 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
close_out oc;
Format.printf "Compiled %s -> %s (Bun-ESM)@." path output;
`Ok ())
else if is_deno then
(match Affinescript.Codegen_deno.codegen_deno flat_prog resolve_ctx.symbols with
| Error e ->
Format.eprintf "@[<v>Deno-ESM codegen error: %s@]@." e;
`Error (false, "Deno-ESM codegen error")
| Ok esm_code ->
let oc = open_out output in
output_string oc esm_code;
close_out oc;
Format.printf "Compiled %s -> %s (Deno-ESM)@." path output;
`Ok ())
else if is_julia then
(match Affinescript.Julia_codegen.codegen_julia flat_prog resolve_ctx.symbols with
| Error e ->
Expand Down Expand Up @@ -1280,28 +1255,21 @@ let vscode_no_lc_arg =
dependency for extensions that ship no language client; the \
wiring passes null in its place.")

(* Issue #122: --deno-esm. Selects the direct AST -> ES-module backend
({!Affinescript.Codegen_deno}) regardless of output extension, so a
drop-in `.js` ES module can be produced (e.g. `-o src/storage.js
--deno-esm`). A `.deno.js` output extension also routes here without
the flag, as a convenience for the test corpus / ad-hoc use. *)
(* --deno-esm was issue #122. Retired: the ESM host is Bun (--bun-esm).
The flag is kept so existing scripts get a clear error instead of
Cmdliner unknown-option. *)
let deno_esm_arg =
Arg.(value & flag & info ["deno-esm"]
~doc:"Emit a standalone Deno/Node ES module directly from the AST \
(issue #122): `export class` for struct+impl, `export` for \
public fns/consts, and `extern fn` lowered to direct host \
calls (Deno.*Sync / JSON / WebAssembly). No wasm, no require, \
no handle table — the output is a drop-in importable ESM. A \
`.deno.js` output extension selects this backend implicitly.")
~doc:"REMOVED. Deno-ESM is gone; use $(b,--bun-esm) / a `.bun.js` \
output path. Passing this flag (or `-o *.deno.js`) is an error.")

let bun_esm_arg =
Arg.(value & flag & info ["bun-esm"]
~doc:"Emit a standalone Bun-native ES module directly from the AST: \
public declarations are exported and host operations use Bun's \
Node-compatible synchronous APIs. The emitted module contains no \
legacy-runtime shim. A `.bun.js` output extension selects this \
backend implicitly. This option is mutually exclusive with \
`--deno-esm`.")
Deno runtime shim. A `.bun.js` output extension selects this \
backend implicitly.")

(** Shared --face flag: select the parser surface-syntax face. *)
let face_arg =
Expand Down
23 changes: 13 additions & 10 deletions docs/CAPABILITY-MATRIX.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,19 @@ Formal spec: link:specs/TYPED-WASM-INTERFACE.adoc[specs/TYPED-WASM-INTERFACE.ado
Widening roadmap (L1–6, L14–16, multi-producer ABI): link:specs/TYPED-WASM-ROADMAP.adoc[specs/TYPED-WASM-ROADMAP.adoc].
Producer-side; target spec is the separate `hyperpolymath/typed-wasm` repo.

|Deno-ESM |works |Direct AST→ES-module transpiler (`lib/codegen_deno.ml`),
`--deno-esm` / `.deno.js`. Shipped + consumer-verified (ubicity).

|Bun-ESM |works |Host-profiled direct AST→ES-module transpiler,
`--bun-esm` / `.bun.js`. Public declarations remain importable ESM; filesystem,
argument, and process operations lower through Bun's synchronous
Node-compatibility surface. Host operations are resolved lazily so a generated
module that only uses browser-provided externs remains browser-importable. CI
compiles, checks for legacy-runtime leakage, parses, and executes filesystem,
byte, argument, export, and missing-path controls under Bun.
|Deno-ESM |retired |`--deno-esm` / `-o FILE.deno.js` is a hard error (E0826).
The JS-host ESM target is Bun-ESM. The flag is kept so existing scripts fail
loudly instead of becoming an unknown option.

|Bun-ESM |works |Direct AST→ES-module transpiler (`lib/codegen_deno.ml`,
`codegen_bun`), `--bun-esm` / `.bun.js`. Public declarations remain importable
ESM; filesystem, argument, and process operations lower through Bun's
synchronous Node-compatibility surface. Host operations are resolved lazily so
a generated module that only uses browser-provided externs remains
browser-importable. CI compiles, checks for legacy-runtime leakage, parses,
and executes filesystem, byte, argument, export, and missing-path controls
under Bun. `tools/run_codegen_deno_tests.sh` (historical name) compiles
`tests/codegen-deno/` with `--bun-esm`.
Migration and host-contract details:
link:guides/bun-esm-migration.adoc[guides/bun-esm-migration.adoc].

Expand Down
4 changes: 2 additions & 2 deletions docs/ECOSYSTEM.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ surface.

|`affinescript-vite` |scaffold |Build-tool integration shell.

|`affinescript-deno-test` |works |Smoke-test harness used by the Deno-ESM
target.
|`affinescript-deno-test` |historical |Smoke-test harness from the retired
Deno-ESM target. JS-host ESM is Bun-ESM (`--bun-esm`).

|`affinescript-tea` |runtime |INT-07 (#182): real host-side TEA runtime
+ run loop (`TeaApp`: load/init/dispatch/model/setScreen/run,
Expand Down
12 changes: 12 additions & 0 deletions docs/EFFECTS-IMPLEMENTATION.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,18 @@ type eval_error =
Handler evaluation catches `PerformEffect`, selects a matching arm, and
evaluates the arm body in the handler context.

== JS-host ESM (Bun) — Canvas-style erasure

Host FFI on `--bun-esm` does **not** require `/ IO` (or `{IO}`) effect
rows. `stdlib/Canvas.affine`, `stdlib/Dom.affine`, and
`stdlib/Console.affine` declare ordinary `extern fn` / wrappers; the
direct-ESM emitter lowers them to host calls and erases effect
declarations (`// effect declaration (erased)`). Declared `/ Async` rows
are the exception: they compile to `async function` so `await fetch` is
legal JS. Algebraic `handle { ... }` is a compile error on this backend
(Refs #555). This is the AOT-safe contract for the #56 remainder — not
a second effect system.

== Not Yet Complete

[arabic]
Expand Down
4 changes: 2 additions & 2 deletions docs/STDLIB-EXTERN-AUDIT.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ producer-side type signature here and a verifier-side contract in
runtime adapter all green. `Http.fetch` is the canonical example
(landed via the #225 CPS line, PR1..PR3d).

|*Adapter-side (Deno-ESM)*
|Compiles via the `--deno-esm` backend; the `extern` is realised
|*Adapter-side (JS-host ESM)*
|Compiles via the `--bun-esm` backend (Deno-ESM retired); the `extern` is realised
by the emitted ESM importing a JS module. The "real impl" is the
adapter shim in `packages/<adapter>/mod.js` (or equivalent).
|Adapter package exists, the symbol is exported, and a
Expand Down
2 changes: 1 addition & 1 deletion docs/bindings-roadmap.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ no further significant → AffineScript work is tractable.
|*WASM-exports calling pattern* — invoke individual `exports.fn_name(args)` from a `WasmExports` value
|`●` usable (Option A landed)
|`stdlib/Deno.affine`
|`wasmCall(exports: WasmExports, name: String, args: [Float]) -> Float` lowers to `Number(exports[name](...args))` on `--deno-esm`. AS-side surface + docstring example landed in `stdlib/Deno.affine`; round-trip exercised by `tests/codegen-deno/wasm_call.{affine,harness.mjs}` against a hand-built 41-byte wasm module exporting `add(i32, i32) -> i32`. *Option A (generic) — typed per-Zig-fn shims can layer on top per-consumer if needed.* Closes #414 via host-side #422 + AS-side this PR. *Generic export-call shipped #455* (`wasm_export_call(exports, name, args: [WasmValue]) -> WasmValue` + `WasmValue` opaque + `wv_i32`/`wv_i64`/`wv_f32`/`wv_f64` + `wv_as_int`/`wv_as_float`/`wv_kind`); end-to-end demo at `examples/wasm-exports-demo.affine` (#500) with smoke-test `tests/codegen-deno/wasm_exports_demo.{affine,harness.mjs}` exercising all four scalar kinds.
|`wasmCall(exports: WasmExports, name: String, args: [Float]) -> Float` lowers to `Number(exports[name](...args))` on `--bun-esm`. AS-side surface + docstring example landed in `stdlib/Deno.affine`; round-trip exercised by `tests/codegen-deno/wasm_call.{affine,harness.mjs}` against a hand-built 41-byte wasm module exporting `add(i32, i32) -> i32`. *Option A (generic) — typed per-Zig-fn shims can layer on top per-consumer if needed.* Closes #414 via host-side #422 + AS-side this PR. *Generic export-call shipped #455* (`wasm_export_call(exports, name, args: [WasmValue]) -> WasmValue` + `WasmValue` opaque + `wv_i32`/`wv_i64`/`wv_f32`/`wv_f64` + `wv_as_int`/`wv_as_float`/`wv_kind`); end-to-end demo at `examples/wasm-exports-demo.affine` (#500) with smoke-test `tests/codegen-deno/wasm_exports_demo.{affine,harness.mjs}` exercising all four scalar kinds.

|6
|*Phoenix Channels / WebSocket* (Socket connect/disconnect, Channel join/leave/push, presence)
Expand Down
18 changes: 12 additions & 6 deletions docs/guides/bun-esm-migration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,18 @@ The source filename is retained to avoid a high-risk mechanical move in the
same change as the semantic migration. It does not imply that Bun output passes
through or embeds the compatibility runtime.

The compatibility flag remains available for existing external consumers. An
estate migration is complete only when its active workflows, runtime files,
lockfiles, build commands, generated artefact names, examples, and current
documentation have moved to Bun. Historical records may retain their original
runtime names when clearly marked as history; renaming history would make it
less accurate.
`--deno-esm` and `-o *.deno.js` are retired: the compiler errors with E0826
and points at `--bun-esm` / `.bun.js`. The flag is kept so existing scripts
fail loudly instead of becoming an unknown option. An estate migration is
complete only when its active workflows, runtime files, lockfiles, build
commands, generated artefact names, examples, and current documentation have
moved to Bun. Historical records may retain their original runtime names when
clearly marked as history; renaming history would make it less accurate.

`affinescript-vite` remains a scaffold (not in this tree). Effect rows such
as `/ IO` are not required on the JS-host ESM path: host FFI is erased at
emit the same way Canvas/Dom externs are (see
link:../EFFECTS-IMPLEMENTATION.adoc[EFFECTS-IMPLEMENTATION.adoc]).

== Verification contract

Expand Down
13 changes: 8 additions & 5 deletions lib/codegen_deno.ml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
(* SPDX-License-Identifier: MPL-2.0 *)
(* SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell *)

(** Deno-ESM Emit Mode (issue #122, Refs #35 #103).
(** Direct ESM emit (issue #122 origin; Bun-ESM is the only JS-host
profile — `--deno-esm` retired, Refs #56).

A *direct* AffineScript-AST → ES-module transpiler. Unlike
{!Codegen_node} (which wraps a compiled [Wasm.wasm_module] in a CJS
shim), this backend emits standalone ES2020 module source — no wasm,
no [require], no handle table — so the output is a drop-in [.js] /
[.mjs] ES module a Deno (or Node ESM) consumer can [import] directly.
[.mjs] ES module a Bun (or Node ESM) consumer can [import] directly.

Why a direct transpiler and not a wasm-wrapping ESM shim: the
motivating consumer ([hyperpolymath/ubicity]'s [storage.ts] /
Expand Down Expand Up @@ -2109,12 +2110,14 @@ let generate (host : host_profile) (program : program) (symbols : Symbol.t) : st
if has_main then emit_line ctx "await main();";
Buffer.contents ctx.output

(* Deno-ESM host profile is retired; the ESM emitter is Bun-only. The
name is kept so existing library callers compile. *)
let codegen_deno (program : program) (symbols : Symbol.t)
: (string, string) result =
try Ok (generate Deno program symbols)
try Ok (generate Bun program symbols)
with
| Failure msg -> Error ("Deno-ESM codegen error: " ^ msg)
| e -> Error ("Deno-ESM codegen error: " ^ Printexc.to_string e)
| Failure msg -> Error ("Bun-ESM codegen error: " ^ msg)
| e -> Error ("Bun-ESM codegen error: " ^ Printexc.to_string e)

let codegen_bun (program : program) (symbols : Symbol.t)
: (string, string) result =
Expand Down
5 changes: 3 additions & 2 deletions stdlib/Console.affine
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
// Console.affine — #56-A typed console surface.
//
// `println` / `eprintln` already lower to `console.log` / `console.error`
// on the JS/Deno backends. These wrappers give the pilot's
// on the JS-host ESM (Bun) backend. These wrappers give the pilot's
// `Console.log(...)` name a home so migration does not accumulate
// `%raw` for the four console methods.
// `%raw` for the four console methods. No `/ IO` row — Canvas-style
// erasure at emit (see docs/EFFECTS-IMPLEMENTATION.adoc).

module Console;

Expand Down
10 changes: 6 additions & 4 deletions stdlib/Deno.affine
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//
// Deno.affine — issue #122 host bindings for the Deno-ESM backend.
// Deno.affine — issue #122 host bindings (JSON / bytes / path helpers).
//
// Unlike stdlib/Vscode.affine (issue #35), these externs are NOT a
// wasm-FFI surface with an Int-handle/readString contract. The
// `--deno-esm` backend (lib/codegen_deno.ml) is a *direct* AST → ES
// module transpiler with no wasm boundary, so each `extern fn` below is
// lowered, at compile time, straight to its host expression:
// `--bun-esm` backend (lib/codegen_deno.ml, `codegen_bun`) is a *direct*
// AST → ES module transpiler with no wasm boundary, so each `extern fn`
// below is lowered, at compile time, straight to its host expression
// (Bun Node-compat for FS; JSON/Date stay platform-neutral). `--deno-esm`
// is retired (E0826).
//
// writeTextFile(p, c) -> Deno.writeTextFileSync(p, c)
// jsonParse(s) -> JSON.parse(s)
Expand Down
6 changes: 4 additions & 2 deletions stdlib/Dom.affine
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
//
// Canonical `module Dom` so `use Dom::{div, h1, p, text, replaceBody}`
// resolves from stdlib/. Constructors are pure VNode builders (no host
// FFI). Runtime mount lives in affinescript-dom (the reconciler); this
// module is the compile-time target for idaptik's pilot syntax.
// FFI, no `/ IO` row — Canvas-style erasure). Runtime mount lives in
// affinescript-dom (the reconciler); this module is the compile-time
// target for idaptik's pilot syntax. `--bun-esm` is the JS-host ESM
// emit; Deno-ESM is retired.
//
// Attrs are `[(String, String)]` — the same tuple-list shape as
// `Http.affine` headers. Record-literal attrs (`#{style: ...}`) are a
Expand Down
2 changes: 1 addition & 1 deletion tests/codegen-deno/aggregate_smoke.harness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ globalThis.__as_sqlite = {
txDb() { return 0; }, txIsLive() { return false; },
};

const mod = await import("./aggregate_smoke.deno.js");
const mod = await import("./aggregate_smoke.bun.js");

// ── COUNT ───────────────────────────────────────────────────────────
assert.equal(mod.smoke_count(":memory:"), 5, "smoke_count: 5 rows inserted");
Expand Down
Loading
Loading