Repository navigation
stdlib has never been AOT-compiled as a coherent unit (angle-applied types + flat-namespace collisions; string.affine is the first symptom) #128
Description
Summary
The AffineScript stdlib has never been compiled through the static
resolve → typecheck → codegen pipeline as a coherent unit. It is
interpreter-era code (dynamically typed, flat global namespace) that the
AOT path has never exercised end-to-end. stdlib/string.affine is the
first file that surfaced this (originally reported here), but the two
root causes are stdlib-wide, not string-specific.
This issue is rescoped from "stdlib/string.affine does not compile" to
the actual underlying defect.
Reproduce (original symptom)
affinescript compile stdlib/string.affine -o /tmp/x.deno.js --deno-esm
# => affinescript: Resolution error (fails at resolve, before typecheck/codegen)
Root causes (both stdlib-wide)
1. Angle-bracket applied types are the stdlib's surface, not a string.affine quirk
The static front-end accepts <...> for type parameters but
rejects it for applied types in value/return position
(fn char_at(...) -> Option<Char>). That asymmetry is the actual
defect. It is pervasive, not local:
| file | <…> usages |
|---|---|
option.affine |
36 |
result.affine |
24 |
collections.affine |
22 |
testing.affine |
18 |
prelude.affine |
15 |
io.affine |
10 |
string.affine |
5 |
Rewriting only string.affine to bracket form yields a file that still
cannot use option.affine / prelude.affine because those don't
resolve either — a single-file smoke check would pass while the stdlib
remains incoherent.
2. No module/import story; flat-namespace collisions
No stdlib file uses use / import / open. prelude.affine and
option.affine both define is_some / is_none / unwrap /
unwrap_or / map / filter / contains, with conflicting
signatures (prelude.map(arr, f) vs option.map(f, opt)). Commit
b895374 ("seed Some/None/Ok/Err builtins so it resolves standalone")
is a band-aid that entrenches the interpreter-era global namespace
rather than resolving it; the original "add prelude/option imports or
qualify Option::Some" suggestion assumes import machinery the stdlib
has never been run through under AOT.
Decision required (resolve at source)
Pick the applied-type surface once and apply it uniformly. Per-file
rewrites are the worst of both options.
- (Preferred) Accept
Option<Char>in applied position in the
resolver. Cheap, consistent with the<...>parameter syntax
already accepted, makes the entire existing stdlib compile
unchanged. - Bracket-only. Migrate all of
stdlib/*.affine+ docs + examples
in one sweep and remove the angle alias so it cannot rot back.
Scope of work
- Decide + implement the applied-type surface uniformly (above).
- Resolve the duplicate-definition / namespace question: does the
stdlib have modules, or stay flat-and-deduplicated? Remove the
conflictingpreludevsoptionoverlaps either way. - Add a stdlib-wide AOT compile-smoke gate: every
stdlib/*.affinethroughresolve → typecheck → codegen, plus at
least one test thatuses several stdlib modules together, so the
AOT path can't silently rot again. (Not a single-file check.)
Impact / scope
Non-blocking and separate from #122. The Deno-ESM consumer migration
(ubicity#30 → ubicity#31/#33) deliberately does not depend on the stdlib
(self-contained extern fn primitives; ubicity green 44/44). This is
the separately-tracked structural loose end noted during the #122 work,
now scoped to its true root cause.
Refs #122, #123, #125, #127. Supersedes the original
string.affine-only framing of this issue.
Tracking (sub-issues)
This issue is now the tracking epic. Work items, with dependencies:
- resolver: accept angle-bracket applied types (Option<Char>) in value/return position #131 — resolver: accept angle-bracket applied types in value/return position (core fix; unblocks stdlib: make every stdlib/*.affine compile through resolve→typecheck→codegen #135, cleanup: remove b895374 seeded Some/None/Ok/Err builtins band-aid #138)
- stdlib: decide & document the namespace/module model (modules vs flat-deduplicated) #132 — decide & document stdlib namespace/module model (decision; blocks stdlib: remove prelude/option/result duplicate & conflicting definitions #133, test: multi-module integration test using several stdlib modules together #137)
- stdlib: remove prelude/option/result duplicate & conflicting definitions #133 — remove prelude/option/result duplicate & conflicting defs (needs stdlib: decide & document the namespace/module model (modules vs flat-deduplicated) #132; blocks stdlib: make every stdlib/*.affine compile through resolve→typecheck→codegen #135)
- prelude: unwrap/unwrap_result fall through (return nothing) on None/Err #134 — fix prelude
unwrap/unwrap_resultfall-through (independent) - stdlib: make every stdlib/*.affine compile through resolve→typecheck→codegen #135 — every
stdlib/*.affinecompiles via resolve→typecheck→codegen (needs resolver: accept angle-bracket applied types (Option<Char>) in value/return position #131, stdlib: remove prelude/option/result duplicate & conflicting definitions #133; blocks ci: add stdlib-wide AOT compile-smoke gate #136) - ci: add stdlib-wide AOT compile-smoke gate #136 — stdlib-wide AOT compile-smoke gate in CI (needs stdlib: make every stdlib/*.affine compile through resolve→typecheck→codegen #135)
- test: multi-module integration test using several stdlib modules together #137 — multi-module integration test (needs stdlib: decide & document the namespace/module model (modules vs flat-deduplicated) #132, stdlib: remove prelude/option/result duplicate & conflicting definitions #133, ci: add stdlib-wide AOT compile-smoke gate #136)
- cleanup: remove b895374 seeded Some/None/Ok/Err builtins band-aid #138 — remove
b895374seeded-builtins band-aid (needs resolver: accept angle-bracket applied types (Option<Char>) in value/return position #131, stdlib: decide & document the namespace/module model (modules vs flat-deduplicated) #132, stdlib: remove prelude/option/result duplicate & conflicting definitions #133)
Suggested order: #131 → #132 → #133 → #135 → #136 → #137 → #138, with #134 doable anytime in parallel.
Downstream requirements (consumer-driven, from the ECHIDNA migration)
Folded in from the ReScript→AffineScript migration backlog. These are
requirements this epic must satisfy, not separate work streams — the
Client.res port (echidna [migration-roadmap.rescript-to-affinescript])
is blocked until the stdlib exposes these primitives. They extend the
stdlib surface the AOT pipeline (#135) must compile, beyond the existing
angle-bracket/namespace work:
| echidna issue | primitive needed | maps to stdlib |
|---|---|---|
| hyperpolymath/echidna#61 | Http/Fetch (GET/POST, RequestInit, Response ok/status/json) |
new stdlib/http.affine (no module today; effects.affine has no HTTP) |
| hyperpolymath/echidna#62 | Async[T]/Promise[T] (pure/bind/catch or await) |
concrete Async[T] over effects.affine (currently no documented type) |
| hyperpolymath/echidna#63 | Json (opaque type + Decode/Encode combinators, stringify) |
new stdlib/json.affine |
| hyperpolymath/echidna#64 | Dict/Map (empty/get/insert/set) |
collections.affine (listed TODO; depends on #133/#135) |
Implication for scope: #135's "every stdlib/*.affine compiles" must
include the new http.affine and json.affine plus a real
collections.affine, and #137's multi-module integration test should
exercise the Client.res-shaped path (Http + Async + Json + Dict together)
as the migration's validation target. The four echidna issues stay open as
the consumer-side trackers and close when the migration lands, not when this
epic does.
Refs hyperpolymath/echidna#61, hyperpolymath/echidna#62, hyperpolymath/echidna#63, hyperpolymath/echidna#64.
Activity
[-]stdlib/string.affine does not compile through the static pipeline (Option<Char> angle-generics + unimported Some/None)[/-][+]stdlib has never been AOT-compiled as a coherent unit (angle-applied types + flat-namespace collisions; string.affine is the first symptom)[/+]fix(parser): split fused >> when closing nested applied generics (#131)…
docs(stdlib): settle namespace model — real modules + qualified paths (…
fix(stdlib): prelude unwrap/unwrap_result diverge via panic (#134) (#150
fix(stdlib): single-ownership dedup of prelude/option/result (#133)
27 remaining items
fix(typecheck): instantiate constructor schemes per use-site in patte…
fix(typecheck): generalize parametric enum ctor schemes via shared pa…
fix(stdlib): declare reassigned locals in io.affine as let mut (#128) (…
fix(typecheck): lenient deref so *ref-mode-param type-checks (#128) (#…
fix(stdlib): collections.affine prelude arg-order + let mut (#128) (#191
fix(stdlib): option.affine — last STAGE-A file; 19/19 (#128) (#192)
fix(resolve): remove b895374 Some/None/Ok/Err seed; resolve imports r…
test(stdlib): AOT smoke gate (#136) + multi-module integration (#137) (…
hyperpolymath commentedon May 18, 2026
STAGE A complete — stdlib AOT coherence achieved.
All 19
stdlib/*.affinecompile resolve→typecheck→borrow→Deno-ESM codegen, standalone and as a coherent multi-module set. Landed this session via PRs #186, #187, #188, #189, #190, #191, #192, #193, #194 (each 233→253/253, zero regression).Closers done at root cause (not patched):
Some/None/Ok/Errflat-seed band-aid; root fix:resolve_and_typecheck_modulenow recursively resolves an imported module's ownuseimports through the loader, and type-checks viacheck_program(forward-declaration) so imported modules behave identically to standalone.RuntimeErrorkept (genuine interpreter exception, no module home).test_stdlib_aot.ml: stdlib-wide AOT smoke gate, one case per file, wired intodune runtest(CI).Coherence gaps found & fixed along the way: parametric enum ctor generalization on the import path; lenient deref for
ref-mode params; prelude arg-order / name-conflict drift in collections & option;&mut→mutparam syntax; assignment-arm vs Unit-arm typing;option/collectionsexposed zeropubAPI.Leaving #128 open for joint close at your discretion. STAGE A unblocks STAGE B (effect system, #59→#103).
hyperpolymath commentedon May 18, 2026
Post-merge verification (independent re-check, 2026-05-18)
Confirming the STAGE-A completion comment above (04:42) now that the dust has settled:
MERGEDtomain: fix(typecheck): zero-arg thunk calls, tuple-destructuring let, RuntimeError catch (#128) #186 fix(typecheck): instantiate constructor schemes per use-site in patterns (#128) #187 fix(typecheck): generalize parametric enum ctor schemes (import path) (#128) #188 fix(stdlib): io.affine reassigned locals need let mut (#128) #189 fix(typecheck): lenient deref for ref-mode params (#128) #190 fix(stdlib): collections.affine prelude arg-order + let mut (#128) #191 fix(stdlib): option.affine — final STAGE-A file, stdlib 19/19 (#128) #192 fix(resolve): remove b895374 seed band-aid; recursive cross-module resolution (#138) #193 test(stdlib): AOT smoke gate (#136) + multi-module integration (#137) #194.test/test_stdlib_aot.ml, resolve→typecheck→borrow→Deno-ESM codegen, one case/file) runs indune runteston every push — STAGE-A coherence is now regression-protected onmain, not just a point-in-time pass.lib/resolve.ml), not patched.No remaining STAGE-A work. Refs #128 — leaving this open for your joint-close per the requirements-target protocol; it's ready whenever you want to close it. STAGE-B (effect system, #59→#103) is now unblocked and proceeding.
hyperpolymath commentedon May 18, 2026
Joint-close approved by @hyperpolymath (2026-05-18).
STAGE-A complete and regression-protected on
main(stdlib 19/19 AOT-coherent; 253/253; CI smoke-gate via #194). Closers #138/#136/#137 landed at root cause. Closing per the requirements-target joint-close protocol. STAGE-B (effect system, #59→#103) is underway.docs(history): mark STDLIB-AOT-TRIAGE closed — #128/#131-138 delivered (
hyperpolymath commentedon Jun 1, 2026
Status update — AS stdlib codegen pattern now established (2026-06-01) 📌
For ticket coordinators / anyone tracking this gap:
lib/codegen_deno.ml. Tests live intests/codegen-deno/*.What this means for the gap tracked in this ticket:
stdlib/*.affine→ wire indeno_builtinstable + prelude → smoke-test intests/codegen-deno/*.🤖 Cross-estate AS-status sweep on 2026-06-01.
refactor(descriptiles): migrate .machine_readable/6a2 → descriptiles …