Repository navigation
ESC-04: ReScript block-module — multi-module-per-file has no clean target (Refs 229) #262
Description
Activity
- addedenhancementNew capability or improvement to existing behaviourNew capability or improvement to existing behaviour
on May 19, 2026 - added 5 commits that reference this issue
on May 19, 2026 Status update — AS stdlib codegen pattern now established (2026-06-01) 📌
For ticket coordinators / anyone tracking this gap:
- affinescript#522 + feat(stdlib): close 6 STEP-3 Deno-ESM gaps surfaced by TS→AS ports (Refs #239, #242) #504 merged 2026-06-01 establish the canonical stdlib extern → Deno-ESM lowering pattern via
lib/codegen_deno.ml. Tests live intests/codegen-deno/*. - affinescript#463 unblocked non-ASCII string emit in Deno-ESM (Unicode escapes, not octal).
- affinescript#421 put stdlib/json on the Zig-FFI rails (hpm-json-rsr).
What this means for the gap tracked in this ticket:
- The codegen pattern is now a known repeatable shape: declare in
stdlib/*.affine→ wire indeno_builtinstable + prelude → smoke-test intests/codegen-deno/*. - This ticket remains open as a tracked gap; no new mass-port is being kicked off from this post.
- Surfacing status so the broader picture stays accurate as more sub-tickets / dependent campaigns reference it.
🤖 Cross-estate AS-status sweep on 2026-06-01.
- affinescript#522 + feat(stdlib): close 6 STEP-3 Deno-ESM gaps surfaced by TS→AS ports (Refs #239, #242) #504 merged 2026-06-01 establish the canonical stdlib extern → Deno-ESM lowering pattern via
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):
module Domain = { type t = | IO | Vector | ... let toString = (d: t): string => ... } type backend = { ... } let register = (b: backend): unit => ...
AS target — two files, identical resolution, zero language change:
Coprocessor/Domain.affine(the inner namespace):module Coprocessor::Domain; pub enum T { IO, Vector, ... } pub fn to_string(d: T) -> String { match d { ... } }Coprocessor.affine(the outer):module Coprocessor; use Coprocessor::Domain::{T as Domain}; pub type Backend = { ... } pub fn register(b: Backend) -> Unit { ... }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)
- 14 estate occurrences of multi-block-module files
- Each splits 1→N files mechanically (one new file per inner
module) - The single-block-module case (already documented as the structural Tier-1 mechanical clean target) is the same mechanism — only the file count differs
- Cross-module resolution gap noted in the issue body (loader-bridge work) is INT-02 / RESCRIPT-ELIMINATION.adoc Tier-4 — orthogonal to this doctrine call
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.
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:2-protocols/axel/axel_sts_demo.affineAnd 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:
- Single block-module per file — has the mechanical target the issue describes (hoist
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. - Multiple block-modules in one file — the genuine design question (split into N files, or define a namespacing form). Still needs a decision, and it now affects one known file rather than fourteen.
Neither half is closed; the scale assumption behind the escalation no longer holds, which changes how it should be prioritised.
- Single block-module per file — has the mechanical target the issue describes (hoist
- addedpriority:p1High - schedule nextHigh - schedule nextscope:estateAffects many or all repos across the estateAffects many or all repos across the estatestatus:needs-rulingAwaiting an owner decisionAwaiting an owner decisionfeeds:valence-shellFeeds valence-shell's proofs/engineering (D127): judge progress by what it contributes thereFeeds valence-shell's proofs/engineering (D127): judge progress by what it contributes there
on 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.
- removedstatus:needs-rulingAwaiting an owner decisionAwaiting an owner decision
on Sep 30, 2026
Problem
ReScript
module Name { … }block modules have only a partialAffineScript target. AffineScript is strictly one module per file
(grammar
parser.mly:130-134: a single optionalmodule Path;header,before imports; no block-module form —
module A { }parse-errors).<comments> ; use…; ; module P { body })→ mechanical clean target: hoist
module P;header to the top, dropthe wrapping braces, dedent the body, keep
useafter the header.This is in the canonical map (RESCRIPT-ELIMINATION.adoc, structural
Tier-1) and verified to parse.
standards/lol/src/cyc/OpenCyc.affine:module Config { } module Concepts { } module Types { } …) has no clean target: it cannotcollapse 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:
module X { }becomes its ownX.affinewith amodule …;header (mechanical once the splitconvention + loader path mapping is fixed — see the cross-module
resolution note below).
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.UndefinedModulebecause the repo's module-path↔file-layout(e.g.
idaptik-dlc-vm/src/State.affinedeclaringmodule 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
idaptik-dlc-vm28,standards/lol, parts ofburble).tools/estate-rs-audit/.