Skip to content

ESC-04: ReScript block-module — multi-module-per-file has no clean target (Refs 229) #262

Description

@hyperpolymath

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: hoist module P; header to the top, drop
    the wrapping braces, dedent the body, keep use after 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.Effect unrepresentable (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:

  1. Doctrine: split per file. Each module X { } becomes its own
    X.affine with a module …; header (mechanical once the split
    convention + loader path mapping is fixed — see the cross-module
    resolution note below).
  2. 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

Activity

  1. added 5 commits that reference this issue on May 19, 2026
  2. hyperpolymath commented on Jun 1, 2026

    @hyperpolymath
    OwnerAuthor

    Status update — AS stdlib codegen pattern now established (2026-06-01) 📌

    For ticket coordinators / anyone tracking this gap:

    What this means for the gap tracked in this ticket:

    • The codegen pattern is now a known repeatable shape: declare in stdlib/*.affine → wire in deno_builtins table + prelude → smoke-test in tests/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.

  3. hyperpolymath commented on Jun 11, 2026

    @hyperpolymath
    OwnerAuthor

    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 to Domain::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.thing resolution that doesn't go through the module loader), or
    (b) sugar to flat names (Outer_Inner_thing) which clashes with the existing Resolve.lower_qualified_value_paths that rewrites Mod.fn to fn when Mod is ImportSimple.

    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-split flag is a small extension over the existing --translate / --partial modes.

    Recommendation: close #262 with the doctrine decision + a one-line note in SETTLED-DECISIONS.adoc referencing ADR-011's "file-per-module" stance.

    🤖 Cross-estate AS-status sweep on 2026-06-11.

  4. hyperpolymath commented on Sep 19, 2026

    @hyperpolymath
    OwnerAuthor

    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.affine with module Config { } module Concepts { } module Types { } ….

    That case no longer exists. lol/src/cyc/OpenCyc.affine is not on standards@main — the lol/ tree is gone from that repository.

    What remains in standards: exactly one .affine file with a multiple-block-module shape:

    2-protocols/axel/axel_sts_demo.affine
    

    And in the affinescript repo 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, keep use after 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.

  5. added
    scope:estateAffects many or all repos across the estate
    feeds:valence-shellFeeds valence-shell's proofs/engineering (D127): judge progress by what it contributes there
    on Sep 30, 2026
  6. hyperpolymath commented on Sep 30, 2026

    @hyperpolymath
    OwnerAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew capability or improvement to existing behaviourfeeds:valence-shellFeeds valence-shell's proofs/engineering (D127): judge progress by what it contributes therepriority:p1High - schedule nextscope:estateAffects many or all repos across the estate

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions