Skip to content

Commit 383772e

Browse files
feat(codegen): add native Bun ESM host profile (#735)
Implements and validates the Bun-native direct ESM backend required by issue #734, including checked host capabilities, deterministic output, migration documentation, negative controls, and cross-platform host primitives. All review threads are resolved and the final CI matrix is green.
1 parent 295ab45 commit 383772e

17 files changed

Lines changed: 601 additions & 123 deletions

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ jobs:
5656
uses: actions/setup-node@v7.0.0
5757
with:
5858
node-version: "20"
59+
- name: Set up Bun
60+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
5961
- name: Install dependencies
6062
run: opam install . --deps-only --with-test --with-doc --yes
6163
- name: Install tree-sitter CLI (for res-to-affine walker tests)
@@ -79,6 +81,8 @@ jobs:
7981
# and runs the *.harness.mjs under Node (CI has Node 20, not Deno;
8082
# the Phase 1 fixtures are pure logic so Node ESM exercises them).
8183
run: opam exec -- ./tools/run_codegen_deno_tests.sh
84+
- name: Run native Bun-ESM tests (issue #734)
85+
run: opam exec -- ./tools/run_codegen_bun_tests.sh
8286
- name: Run face-transformer regression tests
8387
run: opam exec -- ./tools/run_face_transformer_tests.sh
8488
- name: Issue #35 Phase 3 — block extension.ts regression

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ bisect*.coverage
100100
# issue #122: generated Deno-ESM regression outputs (compiled from the
101101
# committed *.affine fixtures by tools/run_codegen_deno_tests.sh).
102102
/tests/codegen-deno/*.deno.js
103+
# Issue #734: generated native Bun-ESM acceptance outputs.
104+
/tests/codegen-bun/*.bun.js
105+
/tests/codegen-bun/backend-conflict.json
103106
# Local-only build workaround (see file header); never committed.
104107
/dune-workspace
105108
packages/affinescript-cli/deno.lock

bin/main.ml

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -490,9 +490,21 @@ let repl_cmd_fn () =
490490
compilation errors. With [--wasm-gc], targets the WebAssembly GC
491491
proposal instead of WASM 1.0 linear memory. *)
492492
let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
493-
deno_esm target path output =
493+
deno_esm bun_esm target path output =
494494
let face = resolve_face ~quiet:json face path in
495-
if json then begin
495+
let is_deno = deno_esm || Filename.check_suffix output ".deno.js" in
496+
let is_bun = bun_esm || Filename.check_suffix output ".bun.js" in
497+
if is_deno && is_bun then
498+
let message = "--deno-esm and --bun-esm are mutually exclusive" in
499+
if json then
500+
json_finish [{ Affinescript.Json_output.severity = Error;
501+
code = "E0826"; message;
502+
span = Affinescript.Span.dummy; help = None; labels = [] }]
503+
else begin
504+
Format.eprintf "@[<v>Backend selection error: %s@]@." message;
505+
`Error (false, "Backend selection error")
506+
end
507+
else if json then begin
496508
let diags = ref [] in
497509
let add d = diags := d :: !diags in
498510
begin try
@@ -525,8 +537,9 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
525537
via Codegen.gen_imports / the import section. *)
526538
let flat_prog = Affinescript.Module_loader.flatten_imports loader prog in
527539
let is_deno = deno_esm || Filename.check_suffix output ".deno.js" in
540+
let is_bun = bun_esm || Filename.check_suffix output ".bun.js" in
528541
let is_julia = Filename.check_suffix output ".jl" in
529-
let is_js = (not is_deno) && Filename.check_suffix output ".js" in
542+
let is_js = (not is_deno) && (not is_bun) && Filename.check_suffix output ".js" in
530543
let is_c = Filename.check_suffix output ".c" in
531544
let is_wgsl = Filename.check_suffix output ".wgsl" in
532545
let is_faust = Filename.check_suffix output ".dsp" in
@@ -547,14 +560,24 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
547560
let is_why3 = Filename.check_suffix output ".mlw" in
548561
let is_lean = Filename.check_suffix output ".lean" in
549562
let is_spirv = Filename.check_suffix output ".spv" in
550-
if is_deno then begin
563+
if is_bun then begin
564+
match Affinescript.Codegen_deno.codegen_bun flat_prog resolve_ctx.symbols with
565+
| Error msg ->
566+
add { severity = Error; code = "E0825";
567+
message = msg;
568+
span = Affinescript.Span.dummy; help = None; labels = [] }
569+
| Ok esm_code ->
570+
let oc = open_out_bin output in
571+
output_string oc esm_code;
572+
close_out oc
573+
end else if is_deno then begin
551574
match Affinescript.Codegen_deno.codegen_deno flat_prog resolve_ctx.symbols with
552575
| Error msg ->
553576
add { severity = Error; code = "E0824";
554577
message = Printf.sprintf "Deno-ESM codegen error: %s" msg;
555578
span = Affinescript.Span.dummy; help = None; labels = [] }
556579
| Ok esm_code ->
557-
let oc = open_out output in
580+
let oc = open_out_bin output in
558581
output_string oc esm_code;
559582
close_out oc
560583
end else if is_julia then begin
@@ -757,8 +780,9 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
757780
module-system support. Wasm/Wasm-GC keep the original [prog]. *)
758781
let flat_prog = Affinescript.Module_loader.flatten_imports loader prog in
759782
let is_deno = deno_esm || Filename.check_suffix output ".deno.js" in
783+
let is_bun = bun_esm || Filename.check_suffix output ".bun.js" in
760784
let is_julia = Filename.check_suffix output ".jl" in
761-
let is_js = (not is_deno) && Filename.check_suffix output ".js" in
785+
let is_js = (not is_deno) && (not is_bun) && Filename.check_suffix output ".js" in
762786
let is_c = Filename.check_suffix output ".c" in
763787
let is_wgsl = Filename.check_suffix output ".wgsl" in
764788
let is_faust = Filename.check_suffix output ".dsp" in
@@ -779,7 +803,18 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
779803
let is_why3 = Filename.check_suffix output ".mlw" in
780804
let is_lean = Filename.check_suffix output ".lean" in
781805
let is_spirv = Filename.check_suffix output ".spv" in
782-
if is_deno then
806+
if is_bun then
807+
(match Affinescript.Codegen_deno.codegen_bun flat_prog resolve_ctx.symbols with
808+
| Error e ->
809+
Format.eprintf "@[<v>%s@]@." e;
810+
`Error (false, "Bun-ESM codegen error")
811+
| Ok esm_code ->
812+
let oc = open_out_bin output in
813+
output_string oc esm_code;
814+
close_out oc;
815+
Format.printf "Compiled %s -> %s (Bun-ESM)@." path output;
816+
`Ok ())
817+
else if is_deno then
783818
(match Affinescript.Codegen_deno.codegen_deno flat_prog resolve_ctx.symbols with
784819
| Error e ->
785820
Format.eprintf "@[<v>Deno-ESM codegen error: %s@]@." e;
@@ -1259,6 +1294,15 @@ let deno_esm_arg =
12591294
no handle table — the output is a drop-in importable ESM. A \
12601295
`.deno.js` output extension selects this backend implicitly.")
12611296

1297+
let bun_esm_arg =
1298+
Arg.(value & flag & info ["bun-esm"]
1299+
~doc:"Emit a standalone Bun-native ES module directly from the AST: \
1300+
public declarations are exported and host operations use Bun's \
1301+
Node-compatible synchronous APIs. The emitted module contains no \
1302+
legacy-runtime shim. A `.bun.js` output extension selects this \
1303+
backend implicitly. This option is mutually exclusive with \
1304+
`--deno-esm`.")
1305+
12621306
(** Shared --face flag: select the parser surface-syntax face. *)
12631307
let face_arg =
12641308
let faces = Arg.enum [
@@ -1602,11 +1646,11 @@ let repl_cmd =
16021646
Cmd.v info Term.(ret (const repl_cmd_fn $ const ()))
16031647

16041648
let compile_cmd =
1605-
let doc = "Compile a file to WebAssembly (1.0 or GC proposal), Julia (.jl), JavaScript (.js), C (.c), a WGSL compute kernel (.wgsl), a Faust DSP program (.dsp), or an ONNX model (.onnx)" in
1649+
let doc = "Compile a file to WebAssembly (1.0 or GC proposal), native Bun ESM (.bun.js/--bun-esm), Julia (.jl), JavaScript (.js), C (.c), a WGSL compute kernel (.wgsl), a Faust DSP program (.dsp), or an ONNX model (.onnx)" in
16061650
let info = Cmd.info "compile" ~doc in
16071651
Cmd.v info Term.(ret (const compile_file $ face_arg $ json_arg $ wasm_gc_arg
16081652
$ vscode_ext_arg $ vscode_adapter_arg $ vscode_no_lc_arg $ deno_esm_arg
1609-
$ target_arg $ path_arg $ output_arg))
1653+
$ bun_esm_arg $ target_arg $ path_arg $ output_arg))
16101654

16111655
let fmt_cmd =
16121656
let doc = "Format a file" in

docs/CAPABILITY-MATRIX.adoc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,16 @@ Producer-side; target spec is the separate `hyperpolymath/typed-wasm` repo.
195195
|Deno-ESM |works |Direct AST→ES-module transpiler (`lib/codegen_deno.ml`),
196196
`--deno-esm` / `.deno.js`. Shipped + consumer-verified (ubicity).
197197

198+
|Bun-ESM |works |Host-profiled direct AST→ES-module transpiler,
199+
`--bun-esm` / `.bun.js`. Public declarations remain importable ESM; filesystem,
200+
argument, and process operations lower through Bun's synchronous
201+
Node-compatibility surface. Host operations are resolved lazily so a generated
202+
module that only uses browser-provided externs remains browser-importable. CI
203+
compiles, checks for legacy-runtime leakage, parses, and executes filesystem,
204+
byte, argument, export, and missing-path controls under Bun.
205+
Migration and host-contract details:
206+
link:guides/bun-esm-migration.adoc[guides/bun-esm-migration.adoc].
207+
198208
|Node-CJS |works |`lib/codegen_node.ml`; CJS shim, handle table, the
199209
`.affine` VS Code extension compiles through it. Live-host smoke
200210
harness for the compiled extension landed via PR #317 / issue #139:
@@ -250,6 +260,7 @@ test is deleted or renamed without this section (and CI) noticing.
250260
|typed-wasm isolation |`test/test_tw_isolation.ml`
251261
|Codegen (golden snapshots) |`test/test_golden.ml`
252262
|Deno / JS host backends |`test/test_deno_builtins_consistency.ml`, `test/test_int_div_js.ml`
263+
|Bun-ESM host backend |`tools/run_codegen_bun_tests.sh`
253264
|Solo core (executable metatheory) |`test/test_solo_cesk.ml`
254265
|===
255266

docs/guides/bun-esm-migration.adoc

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
= Migrating direct ESM output to Bun
2+
:toc: macro
3+
4+
toc::[]
5+
6+
== Select the Bun host profile
7+
8+
Replace `--deno-esm` with `--bun-esm`, and replace generated `.deno.js` names
9+
with `.bun.js`. A `.bun.js` output suffix selects the profile implicitly, but
10+
the explicit flag is preferable in build definitions because it records the
11+
runtime contract directly.
12+
13+
[source,console]
14+
----
15+
affinescript compile --bun-esm -o build/library.bun.js src/library.affine
16+
bun --check build/library.bun.js
17+
----
18+
19+
The compiler emits public functions, constants, variants, and generated
20+
classes as standard ESM exports. Bun output is checked in CI to contain no
21+
case-insensitive reference to the former runtime.
22+
23+
== Host operations
24+
25+
The shared ESM core contains runtime-neutral lowering for JSON, URL,
26+
WebAssembly, web APIs, and pure language operations. The Bun profile supplies
27+
its own filesystem, arguments, process, environment, subprocess, standard-I/O,
28+
and exit lowerings. Synchronous filesystem and subprocess operations use Bun's
29+
Node-compatibility surface. They are resolved lazily, so a generated module
30+
that only calls browser-provided externs can still load in a webview with no
31+
`process` global.
32+
33+
Import explicit Bun process capabilities from `stdlib/Bun.affine`:
34+
35+
[source,affinescript]
36+
----
37+
use Bun::{ bun_env_get, bun_run, bun_stdin_text,
38+
bun_stdout_write, bun_stderr_write, bun_exit };
39+
----
40+
41+
Names beginning with `bun_` form a checked capability namespace. If such an
42+
extern has no compiler lowering, Bun code generation fails non-zero. This is a
43+
deliberate negative guarantee: a misspelt or unimplemented host operation must
44+
not silently turn into an optimistic JavaScript call. Other `extern fn` names
45+
retain the existing direct-ESM contract and lower to same-named host symbols;
46+
that facility is required for explicit webview bridges such as Gossamer.
47+
48+
== Runtime-neutral core and compatibility
49+
50+
`lib/codegen_deno.ml` currently owns the mature direct-ESM lowering machinery,
51+
but its generated runtime is split into three parts: a compatibility host
52+
profile, a Bun host profile, and a shared runtime-neutral prelude/builtin core.
53+
The source filename is retained to avoid a high-risk mechanical move in the
54+
same change as the semantic migration. It does not imply that Bun output passes
55+
through or embeds the compatibility runtime.
56+
57+
The compatibility flag remains available for existing external consumers. An
58+
estate migration is complete only when its active workflows, runtime files,
59+
lockfiles, build commands, generated artefact names, examples, and current
60+
documentation have moved to Bun. Historical records may retain their original
61+
runtime names when clearly marked as history; renaming history would make it
62+
less accurate.
63+
64+
== Verification contract
65+
66+
`tools/run_codegen_bun_tests.sh` provides positive and planted-negative
67+
controls. It compiles and imports public exports, exercises filesystem and byte
68+
I/O, arguments, environment values, subprocess status, stdin, and explicit
69+
exit status under Bun; checks JavaScript syntax and absence of legacy-runtime
70+
references; compiles twice and compares output byte-for-byte; and proves that
71+
an unsupported `bun_` capability fails compilation.

0 commit comments

Comments
 (0)