Commit f34391c
* feat(codegen): Deno-ESM target — direct AST→ES module backend (Refs #122)
Adds `lib/codegen_deno.ml`, a direct AffineScript-AST → ES-module
transpiler, plus a `--deno-esm` flag (and `.deno.js` extension) wired
into both compile dispatch paths in `bin/main.ml`.
Why a direct transpiler, not a wasm-wrapping ESM shim (the handoff's
original Phase 1): the motivating consumer (ubicity storage.ts /
wasm-bridge.ts) is pure JS-value orchestration — it JSON-stringifies
opaque objects, JSON-parses file contents back into JS objects, and
returns those to JS callers. AffineScript's wasm ABI is i32-only
(Codegen: params/ret all I32); arbitrary JS objects / JSON.stringify
cannot cross it. There is essentially no computation that belongs in
wasm, so source-to-source emission is the correct tool.
Backend behaviour:
- `pub fn`/`pub const`/`pub enum` -> `export` (ESM, no require).
- `extern fn` -> direct host calls via a lowering table
(Deno.*Sync / JSON / WebAssembly / path helpers), inlined so output
is genuinely drop-in (no extra package to resolve).
- struct + receiver-first free functions -> `export class`:
constructor synthesised from the struct-returning fn (record literal
-> `this.f = e`), methods from fns whose first param is the struct
(receiver -> `this`), cross-method calls rewritten to `this.m(...)`.
This is necessary because the current grammar accepts neither
inherent `impl Type {}` nor a `self` expression (SELF_KW has no
expression production — even stdlib/traits.affine fails to parse), so
an "instance method" must be a free function taking the struct first.
- Methods emitted `async`: the consumer surface is entirely async and
callers `await`; `await` on a synchronously-returned value is valid
JS, so the exact API is preserved with no async-extern ABI (#103 not
required for this consumer; remains documented future work).
- js_reserved trimmed to real ECMAScript reserved words (drops the
Java-ism primitives Js_codegen carries: double/int/boolean/... are
valid JS identifiers; mangling them would corrupt a consumer's
required ESM export surface).
Regression: tests/codegen-deno/class_basic.{affine,harness.mjs} +
tools/run_codegen_deno_tests.sh + a CI step. The harness runs under
Node 20 (CI has no Deno; Phase 1 fixtures are pure logic and the
generated module only touches the Deno global lazily in unused
helpers). `dune build` clean; `dune runtest` unchanged (the 2
pre-existing E2E Node-CJS `--vscode-extension` failures, #116/#117
territory, exist identically on pristine HEAD and are out of #122
scope).
Refs #122, #35, #103.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(codegen-deno): statement-position return + control flow (Refs #122)
The Deno-ESM backend inherited js_codegen's expression-only lowering of
`return`/`if`/`match`/`try`: a statement-position `return e;` became
`(() => { return e; })();`, whose value is discarded — so any function
using an explicit `return` (or early-return in an `if`, or `return`
inside a `for`/`while`) actually returned `undefined`. Phase 1 dodged
it only because every fixture used `= expr;` bodies.
Add a distinct statement-position lowering (gen_stmt_expr / gen_branch /
gen_stmt_seq / gen_match_stmt / gen_try_stmt): in statement context
`return` and control flow emit real JS statements; gen_expr keeps the
IIFE form for genuine expression positions. StmtExpr, and loop trailing
exprs, route through it.
Regression: tests/codegen-deno/control_flow.{affine,harness.mjs} —
while+trailing return, early return in if, return inside for+if,
fallthrough. Full Deno-ESM suite green; Phase 1 unaffected.
Refs #122.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(grammar): self expression + inherent impl blocks (Refs #122)
Two pre-existing AffineScript grammar gaps blocked idiomatic OO and
made even stdlib/traits.affine unparseable:
1. SELF_KW had no expression production. The receiver-param productions
already bind a parameter named "self", but the body could never
reference it — `self.field` / `self.method(x)` was always a parse
error. Add `| SELF_KW { ExprVar (mk_ident "self" ...) }` to
expr_primary; it resolves/typechecks as the ordinary "self"
parameter binding (no resolver/typechecker change needed).
2. Inherent `impl Type { ... }` was a parse error at the `{`: the rule
pre-committed to `impl_trait_ref? self_ty` where both alternatives
start with an ident, an unresolved ambiguity. Restructure to parse
one type_expr unconditionally, then an optional `FOR type_expr`;
when `FOR` is present the leading type was the trait
(trait_ref_of_type_expr recovers its name/args). Conflict-free;
trait impls (`impl Trait for Type`) still parse identically.
Verified: `impl Counter { fn bumped(ref self, by: Int) -> Int {
self.start + by } }` compiles; trait-impl + receiver-first probes
still compile; `dune runtest` unchanged (same 2 pre-existing E2E
Node-CJS vscode failures, no new regressions); Deno-ESM suite green.
Residual (separate, tracked): `Self` as a *type annotation* in trait
default methods (stdlib/traits.affine:12 `ref other: Self`) — a narrow
grammar/typecheck item, distinct from `self`-the-expression.
Refs #122.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(typecheck): auto-deref ref/own/mut for struct field access (Refs #122)
`fn(c: ref Point) = c.x` failed with "Field 'x' not found in type
ref {x: Int, ...}": ExprField unified the receiver type directly with
the expected record, so a TRef/TMut/TOwn wrapper made projection fail.
Strip TRef/TMut/TOwn (via repr) before record projection — Rust-like
auto-deref for field access. The borrow checker still governs aliasing
separately; this only affects the *type* of the projected field, which
is identical through a reference. Trait-method fallback path unchanged.
This unblocks receiver-first methods that take `ref/own/mut self|recv`
(the idiomatic, allocation-free form) under the Deno-ESM class synth.
Regression: tests/codegen-deno/ref_fields.{affine,harness.mjs} —
ref/own/mut receivers synthesised as Point methods, field access
verified. `dune runtest` unchanged (same 2 pre-existing E2E Node-CJS
vscode failures, 214 tests, zero new regressions). Deno-ESM suite green.
Refs #122.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(grammar): qualified Type::Variant patterns in match arms (Refs #122)
`pattern_primary` accepted only bare `upper_ident` / `upper_ident(p..)`
constructors, never the qualified `Type::Variant` form. So every
qualified match arm was a parse error — `match c { Color::Red => .. }`,
recursive ADTs (`IntList::Cons(h, t) => ..`), and stdlib/traits.affine's
`Ordering::Less => ..`. This blocked both `= match {..};` expression
bodies and block-body matches.
Add `upper_ident COLONCOLON upper_ident` and the payload form to
pattern_primary. The type qualifier is discarded — PatCon carries only
the variant name, exactly matching ExprVariant and the codegen tag
dispatch (`scrut.tag === "Variant"`, payload at `.values[i]`), so the
qualified and bare forms are interchangeable and consistent with the
enum factory emission.
Regression: tests/codegen-deno/match_enum.{affine,harness.mjs} —
nullary qualified patterns in an expression-body match, plus recursive
`len`/`sum` over a payload ADT (verified len=3, sum=60). `dune runtest`
unchanged (same 2 pre-existing E2E Node-CJS vscode failures, 214 tests,
zero new regressions). Deno-ESM suite green.
Refs #122.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(stdlib/typecheck): C-proper string primitives + polymorphic ++ (Refs #122)
Option-C enabler: make AffineScript-level string logic compile through
the static pipeline + Deno-ESM backend, resting on a thin honest
host-primitive layer instead of bespoke per-op externs.
typecheck:
- `++` (OpConcat) is now polymorphic over String and [T] — dispatched
on the synthesised lhs type. Was String-only, so `acc ++ [x]` (how
stdlib/string.affine's split/join accumulate) failed
`Unify(Array, String)`.
- `len` broadened from `Array[t]->Int` to `'a->Int` (stdlib/string
calls it on String and arrays; matches the interpreter + the
`.length` lowering).
- Bound the honest string/char primitives: string_get/string_sub/
string_find/to_lowercase/to_uppercase/trim/parse_int/parse_float/
char_to_int/int_to_char/show (resolve.ml already knew them).
codegen_deno:
- Lower those primitives + `++` (shape-dispatched `__as_concat`, so
array concat isn't string-coerced) to JS intrinsics. The lowering
table now applies to ANY matching call head (not only declared
externs) so AffineScript-level stdlib resolves...
- ...EXCEPT a same-named user definition shadows the intrinsic
(new ctx.local_fns guard) — fixes a user `fn len(xs: IntList)`
being hijacked into `.length` (NaN).
Net: ends_with/strip_suffix/find/int_to_string/string-&-array `++`
all work as real AffineScript over the primitive layer
(tests/codegen-deno/string_prims.{affine,harness.mjs}). Full Deno-ESM
suite green (5 harnesses); `dune runtest` unchanged (same 2 pre-existing
E2E Node-CJS vscode failures, 214 tests, zero new regressions).
Note: stdlib/string.affine itself still has a separate Resolution error
(angle-generic `Option<Char>` + unimported Some/None) — that file's own
issue, not the primitive layer; addressed in the consumer migration.
Refs #122.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(codegen-deno/typecheck): consumer-migration primitives + ++ disambiguation (Refs #122)
Primitives the ubicity#30 storage migration needs, added to the honest
host-primitive layer (each a genuine host capability, not smuggled
logic): jsonNull (`null`), jsonGet/jsonGetStr (opaque object/index
read — the data boundary for arbitrary host JSON), orDefault (`x ?? d`
— preserves a JS default parameter), kbString (`(n/1024).toFixed(2)` —
runtime number formatting), panic (`throw new Error(...)`). Mirrored in
stdlib/Deno.affine.
typecheck: `++` disambiguation hardened — when the lhs type is not yet
determined (e.g. `let mut acc = []` then `acc ++ [x]`), disambiguate on
the rhs instead of defaulting to String (which mis-typed array
accumulation as String).
Regression-free: 5 Deno-ESM harnesses green; `dune runtest` unchanged
(same 2 pre-existing E2E Node-CJS vscode failures, 214 tests).
Refs #122 (consumer enablement for ubicity#30).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent bab0051 commit f34391c
19 files changed
Lines changed: 1497 additions & 15 deletions
File tree
- .github/workflows
- bin
- lib
- stdlib
- tests/codegen-deno
- tools
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
43 | 49 | | |
44 | 50 | | |
45 | 51 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
478 | 478 | | |
479 | 479 | | |
480 | 480 | | |
481 | | - | |
| 481 | + | |
482 | 482 | | |
483 | 483 | | |
484 | 484 | | |
| |||
512 | 512 | | |
513 | 513 | | |
514 | 514 | | |
| 515 | + | |
515 | 516 | | |
516 | | - | |
| 517 | + | |
517 | 518 | | |
518 | 519 | | |
519 | 520 | | |
| |||
534 | 535 | | |
535 | 536 | | |
536 | 537 | | |
537 | | - | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
538 | 549 | | |
539 | 550 | | |
540 | 551 | | |
| |||
725 | 736 | | |
726 | 737 | | |
727 | 738 | | |
| 739 | + | |
728 | 740 | | |
729 | | - | |
| 741 | + | |
730 | 742 | | |
731 | 743 | | |
732 | 744 | | |
| |||
747 | 759 | | |
748 | 760 | | |
749 | 761 | | |
750 | | - | |
| 762 | + | |
| 763 | + | |
| 764 | + | |
| 765 | + | |
| 766 | + | |
| 767 | + | |
| 768 | + | |
| 769 | + | |
| 770 | + | |
| 771 | + | |
| 772 | + | |
| 773 | + | |
751 | 774 | | |
752 | 775 | | |
753 | 776 | | |
| |||
1178 | 1201 | | |
1179 | 1202 | | |
1180 | 1203 | | |
| 1204 | + | |
| 1205 | + | |
| 1206 | + | |
| 1207 | + | |
| 1208 | + | |
| 1209 | + | |
| 1210 | + | |
| 1211 | + | |
| 1212 | + | |
| 1213 | + | |
| 1214 | + | |
| 1215 | + | |
| 1216 | + | |
| 1217 | + | |
1181 | 1218 | | |
1182 | 1219 | | |
1183 | 1220 | | |
| |||
1524 | 1561 | | |
1525 | 1562 | | |
1526 | 1563 | | |
1527 | | - | |
| 1564 | + | |
1528 | 1565 | | |
1529 | 1566 | | |
1530 | 1567 | | |
| |||
0 commit comments