You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ESC-01: AffineScript has no raw/FFI escape — %%raw port target (Refs 229) #245
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:
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.
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.
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 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.
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
%%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); }
`);
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:
Per-consumer ports (this issue) — close %%raw sites by emitting extern declarations. Mechanical once the host-side runtime wrapper exists.
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.
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.
Activity
docs(estate): #229 foundation — post-#228 re-audit, RS inventory, can…
docs(estate): #229 foundation — post-#228 re-audit, RS inventory, can…
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 — 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
%%rawis 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
%%rawis used for in the estate:%%rawuse%%raw('Math.imul(a,b)')etc. — JS primitive that should have been a bindingextern 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 timerstdlib/effects.affine+ add Deno-builtin wiring (the canonical pattern post-#522 / #504)%%raw('JSON.stringify(x)')— should have beenstdlib/jsonstdlib/jsonalready has the binding (#421 / #463) — port%%rawaway%%raw('window.foo.bar')— DOM accessaffinescript-dom(#56 / #183) — the typed surface this gap was waiting on%%raw('require("...")') / dynamic importextern fn+ a single host-side resolver shimThe 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:
After (two files):
SomeFile.affine(the consumer):SomeFile.runtime.js(or whichever host wrapper the package ships):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.mjsis the test scaffold.Estate impact
14
%%rawoccurrences 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:%%rawsites by emittingexterndeclarations. Mechanical once the host-side runtime wrapper exists.Action
If accepted as doctrine, codify in
docs/RESCRIPT-ELIMINATION.adoc(Tier-3 ESC-01 line) + a one-line entry inSETTLED-DECISIONS.adocreferencing this issue. The migration tool (tools/res-to-affine/ #488 partial-port mode) adds a--raw-to-externflag 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.
hyperpolymath commentedon Sep 19, 2026
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
.affinefiles". Current counts in the consumer repos that carry ReScript surface:%%raw.affineburblegit-scriptsgame-server-admininvariant-pathSampled 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.
hyperpolymath commentedon Sep 30, 2026
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.
feat(stdlib/bun): bun_run_capture — captured child-process run (#781)