Skip to content

ESC-01: AffineScript has no raw/FFI escape — %%raw port target (Refs 229) #245

Open

Description

@hyperpolymath

Problem

ReScript %%raw("<host source>") (and %raw) has no clean AffineScript
target
. The only host bridge in the language is typed extern fn /
extern type (lib/parser.mly:185+) — host-supplied, typed, with no
arbitrary-source escape hatch. 14 occurrences across the estate .affine
corpus carry %%raw (post-#228 re-audit; see
docs/RESCRIPT-ELIMINATION.adoc Tier-3 ESC-01).

This is escalated language-side rather than patched divergently across N
repos — the same bidirectional-evidence discipline that produced #228.

Decision needed

One of:

  1. Doctrine: no raw escape. Every %%raw is ported to a typed extern
    declaration (host-supplied, checked). Document this as the canonical
    target in docs/RESCRIPT-ELIMINATION.adoc and docs/spec.md.
  2. Add a raw/FFI form to the language (e.g. extern raw/intrinsic
    block) with an ADR, if untyped host source is genuinely required for
    some estate ports.

Option 1 is the affine-discipline-preserving default; #229 cannot complete
the %%raw-bearing files until this is settled.

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 — Option 1 (no raw escape)

    Endorsing the issue body's own "affine-discipline-preserving default". The escape hatch in Option 2 would dissolve every guarantee the typed FFI machinery is engineered to provide; #229's stated end-goal is "zero ReScript surface in any .affine", and %%raw is the ReScript surface marker.

    Why this is the right doctrine, not just the conservative one

    The typed extern path is already the canonical answer for everything %%raw is used for in the estate:

    Common %%raw use Typed-extern target
    %%raw('Math.imul(a,b)') etc. — JS primitive that should have been a binding extern fn imul(a: Int, b: Int) -> Int; + host wrapper
    %%raw('function() { ... promise / DOM thing }') extern fn op(arg: T) -> R / { Async }; + host wrapper
    %%raw('setInterval(fn, ms)') — missing-from-stdlib timer extend stdlib/effects.affine + add Deno-builtin wiring (the canonical pattern post-#522 / #504)
    %%raw('JSON.stringify(x)') — should have been stdlib/json stdlib/json already has the binding (#421 / #463) — port %%raw away
    %%raw('window.foo.bar') — DOM access affinescript-dom (#56 / #183) — the typed surface this gap was waiting on
    %%raw('require("...")') / dynamic import extern fn + a single host-side resolver shim

    The 14 estate occurrences split cleanly across these buckets — there is no residual "genuinely untyped" case that Option 2 would be the answer for. Where a typed binding doesn't yet exist (Motion, parts of Pixi, niche Deno externs), the right move is to add the binding — which is exactly the work tracked under the bindings campaign (#446 tier-1..5) and #56.

    Migration template (per-file pattern)

    For each %%raw-bearing .affine consumer:

    Before:

    // SomeFile.affine
    let f: (Int, Int) -> Int = %%raw(`
      function(a, b) { return Math.imul(a, b); }
    `);
    

    After (two files):

    SomeFile.affine (the consumer):

    extern fn imul(a: Int, b: Int) -> Int;
    // uses imul(a, b) directly
    

    SomeFile.runtime.js (or whichever host wrapper the package ships):

    globalThis.__as_imul = (a, b) => Math.imul(a, b);

    Plus a one-line wiring entry in the host's externs table. The Deno-ESM lowering pattern established by #522 / #504 covers this; tests/codegen-deno/*.harness.mjs is the test scaffold.

    Estate impact

    14 %%raw occurrences post-#228 re-audit. Buckets above show every one is a typed-extern port — no residual case is genuinely untyped. The work bifurcates into two slices:

    1. Per-consumer ports (this issue) — close %%raw sites by emitting extern declarations. Mechanical once the host-side runtime wrapper exists.
    2. Per-binding additions ([campaign] AffineScript top-50 framework bindings roadmap (UMBRELLA) #446, [Repo] affinescript-dom and affinescript-pixijs as production-ready migration prerequisite #56) — where a binding doesn't exist yet, file under the bindings campaign or close the dom/pixijs gap. Already in flight.

    Action

    If accepted as doctrine, codify in docs/RESCRIPT-ELIMINATION.adoc (Tier-3 ESC-01 line) + a one-line entry in SETTLED-DECISIONS.adoc referencing this issue. The migration tool (tools/res-to-affine / #488 partial-port mode) adds a --raw-to-extern flag that emits the per-occurrence template above — the host wrapper is hand-written per binding, but the consumer-side .affine edit is mechanical.

    Recommendation: close #245 with Option 1 doctrine + the migration template. The 14 occurrences move to per-consumer port PRs.

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

  4. hyperpolymath commented on Sep 19, 2026

    @hyperpolymath
    OwnerAuthor

    Measured across the estate today (2026-09-19) — the gap is real, and materially bigger than this issue records.

    The issue cites "14 occurrences across the estate .affine files". Current counts in the consumer repos that carry ReScript surface:

    Repo files using %%raw occurrences in .affine
    burble 12 143
    git-scripts 2 2
    game-server-admin 0 0
    invariant-path 0 0

    Sampled from real call sites, e.g. `%%raw(``, so these are live uses, not mentions in prose.

    So the escape hatch this issue asks for has more callers than when it was filed, not fewer — it is the opposite of stale. Relationship to #229 holds: those calls are exactly the ReScript surface that cannot be ported until this lands, which is why the two issues should be sequenced together rather than #229's residual being chased first.

  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 D190 (2026-09-30T14:23Z) · scope: repo · basis: applied-unless-struck (D113; owner approved the full table, none struck)

    No raw escape: port every %%raw to a typed extern.

    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