Repository navigation
ESC-04: ReScript block-module — multi-module-per-file has no clean target (Refs 229) #262
Description
Problem
ReScript module Name { … } block modules have only a partial
AffineScript target. AffineScript is strictly one module per file
(grammar parser.mly:130-134: a single optional module Path; header,
before imports; no block-module form — module A { } parse-errors).
- Single block-module per file (
<comments> ; use…; ; module P { body })
→ mechanical clean target: hoistmodule P;header to the top, drop
the wrapping braces, dedent the body, keepuseafter the header.
This is in the canonical map (RESCRIPT-ELIMINATION.adoc, structural
Tier-1) and verified to parse. - Multiple block-modules in one file (e.g.
standards/lol/src/cyc/OpenCyc.affine:module Config { } module Concepts { } module Types { } …) has no clean target: it cannot
collapse to one file-header without a semantic re-design (split into N
files, or a namespacing decision). 14 estate occurrences across
block-module-bearing files (post-LANG: type/effect grammar has no module-qualified path —Pkg.Type/Pkg.Effectunrepresentable (estate-wide port blocker; ADR-014) #228 re-audit;tools/estate-rs-audit/).
Escalated language-side rather than guessed per-repo — the same
bidirectional-evidence discipline as ESC-01..03 / #228.
Decision needed
One of:
- Doctrine: split per file. Each
module X { }becomes its own
X.affinewith amodule …;header (mechanical once the split
convention + loader path mapping is fixed — see the cross-module
resolution note below). - Add a block/nested-module form to the grammar (ADR), if
multi-module-per-file is a wanted AffineScript feature.
Related (not this issue, but adjacent)
Even single-block-module files that port cleanly parse then hit
Resolve.UndefinedModule because the repo's module-path↔file-layout
(e.g. idaptik-dlc-vm/src/State.affine declaring module Vm.State;)
does not match the module loader's resolution. That is cross-module
graph coherence (INT-02 loader-bridge territory), explicitly out of
#229's per-file contract — tracked in RESCRIPT-ELIMINATION.adoc Tier-4,
not here.
Context
- Blocks the block-module slice of ESTATE: total ReScript-surface elimination from all .affine (committed goal; blocked-on #228) #229 (
idaptik-dlc-vm28,
standards/lol, parts ofburble). - Refs ESTATE: total ReScript-surface elimination from all .affine (committed goal; blocked-on #228) #229. Reproducer:
tools/estate-rs-audit/.
Activity
docs(estate): #229 closure status — completable done, false scope rem…
docs(adr): renumber block-module ADR 016->017 (main merged a different …
docs(adr): ESC-01 — ADR-018 no raw/FFI escape, typed extern only (Refs …
docs(adr): ESC-04 — ADR-017 ReScript block-module disposition (Refs #262
docs(adr): ESC-04 — ADR-017 ReScript block-module disposition (Refs #262
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.
hyperpolymath commentedon Jun 11, 2026
Doctrine recommendation 2026-06-11 — split-per-file
After the #228 closure batch (#241 / #447 / #523) and the post-#228 idaptik port work in hyperpolymath/idaptik#153, my read on the two options:
Option 1 (doctrine: split per file) — recommended
The cleaner half of "the right answer is the file" — already the canonical convention codified by ADR-011 ("real modules with qualified paths"), and the AS module loader machinery (
module_loader.ml,module/use/::grammar) all assume it.Mechanical migration template (verified against
src/shared/Coprocessor.res):ReScript source (single file):
AS target — two files, identical resolution, zero language change:
Coprocessor/Domain.affine(the inner namespace):Coprocessor.affine(the outer):Call sites stay byte-identical:
Domain.toString(d)ports toDomain::to_string(d)(parser line 909 handles the::form;.form works too post-#241).Option 2 (block-module as nested syntax) — rejected
The block-module form would either:
(a) introduce a real second module scope inside one file — same loader+resolver machinery would need a parallel inside-file path (
Outer.Inner.thingresolution that doesn't go through the module loader), or(b) sugar to flat names (
Outer_Inner_thing) which clashes with the existingResolve.lower_qualified_value_pathsthat rewritesMod.fntofnwhenModisImportSimple.Both paths add language complexity for a feature that's just "sub-file namespacing" — exactly the thing ADR-011 said the file-per-module convention replaces.
Estate impact (verified per the issue body)
module)Action
If accepted as doctrine, the migration script lives in
tools/res-to-affine(#488 / partial-port mode). Adding a--block-module-splitflag is a small extension over the existing--translate/--partialmodes.Recommendation: close #262 with the doctrine decision + a one-line note in
SETTLED-DECISIONS.adocreferencing ADR-011's "file-per-module" stance.🤖 Cross-estate AS-status sweep on 2026-06-11.
hyperpolymath commentedon Sep 19, 2026
Re-measured today (2026-09-19) — the decision this issue escalates is still open, but its blast radius has collapsed.
The issue cites "14 estate occurrences across block-module-bearing files" and names its worst case explicitly:
standards/lol/src/cyc/OpenCyc.affinewithmodule Config { } module Concepts { } module Types { } ….That case no longer exists.
lol/src/cyc/OpenCyc.affineis not onstandards@main— thelol/tree is gone from that repository.What remains in
standards: exactly one.affinefile with a multiple-block-module shape:And in the
affinescriptrepo itself, 0 files use the block-module form — the grammar still has no such production, as the issue states.So the two halves are now cleanly separable:
module P;, drop braces, dedent, keepuseafter the header). With the estate population down to roughly one file, this is now a trivial cleanup rather than a campaign.Neither half is closed; the scale assumption behind the escalation no longer holds, which changes how it should be prioritised.
hyperpolymath commentedon Sep 30, 2026
RULING D191 (2026-09-30T14:24Z) · scope: repo · basis: applied-unless-struck (D113; owner approved the full table, none struck)
Split multi-block-module files one module per file; no new grammar form.
surface: hyperpolymath/standards#787 (comment)
The open question in this issue is answered by the ruling above; future agents should act on it rather than re-ask.