Repository navigation
LANG: slash-effect-row fn(...) -> T / { E } doesn't parse in type-expression position (record fields, type aliases) #547
Description
Is your feature request related to a problem? Please describe.
The slash-form effect row / { E } after -> parses only in function-declaration signatures, not in type-expression positions. So an fn(...) -> T / { E } cannot be written as a record-field type or as the body of a type X = …; alias. This blocks every consumer port that wants to describe an effectful callback as data — paradigmatic case: registries of effectful handlers, e.g. Coprocessor.backend.execute: (cmd, data) => promise<...> in idaptik (~10 sites across src/shared/Coprocessor*.res).
The arrow-form T -{E}-> T already parses in those positions, which both shows the typechecker can handle the effect-row and pins the gap to a parser asymmetry between the two effect-row spellings.
Grounded evidence (oracle probes against HEAD)
| Probe | Result |
|---|---|
pub extern fn do_async(x: Int) -> Int / { Async }; |
✅ parses (top-level decl) |
pub type H = { call: fn(Int) -> Int } (no effect) |
✅ parses |
pub type H = { call: Int -{Async}-> Int } (arrow form) |
✅ parses |
pub type H = { call: fn(Int) -> Int / { Async } } |
❌ parse error at col 33 (the /) |
pub type AsyncCall = fn(Int) -> Int / { Async }; (named type alias) |
❌ parse error at col 33 (the /) |
So the failure is specifically: fn(...) -> T / { E } in any type-expression context — record field, type alias, presumably also function parameter / return position of a higher-order signature. Top-level function decls accept the same syntax fine.
Describe the solution you'd like
Extend the type-expression grammar (type_expr_arrow / type_expr_primary in lib/parser.mly) to accept the slash-effect-row after the -> of a fn-type, mirroring the syntax already accepted in return_type / fn_param-style positions. Two production-shape options:
- Single canonical: pick one of
fn(...) -> T / { E }vsT -{E}-> Tas the canonical type-expression form; accept both at parse time and fold to the canonical AST node (sameTyArrowshape, both spellings legal). Mirrors the./::mixed-separator treatment in feat(parser)!: ADR-014 — module-qualified type/effect paths (Refs #228) #241 / ADR-014. - Slash-form only: add
fn(...) -> T / { effect_term | LBRACE effect_expr RBRACE }as a fresh alternative intype_expr_arrow; leave-{E}->as the arrow form. Conservative.
The owner call on canonical form belongs in a SETTLED-DECISIONS.adoc line per ADR-012; this issue tracks the grammar gap, not the canonical choice.
Describe alternatives considered
-
Use the
-{E}->arrow form in type expressions. Works today and the workaround. Rejected as a long-term answer because:- The two spellings carry the same meaning, and divergence by position (slash in fn decls, arrow in type expressions) is surface friction. Ports paste
(cmd, data) => promise<X>from the JS-source side and hit the parser exactly where the ReScript→AffineScript mechanical transform suggestsfn(cmd, data) -> X / { Async }. - The Frontier Guide and existing Mode-A files (
migration/main/Main.affine) use the slash form for effect rows.
- The two spellings carry the same meaning, and divergence by position (slash in fn decls, arrow in type expressions) is surface friction. Ports paste
-
Estate consumer-port (rewrite every effectful-record-field type to the arrow form). Same objection as the qualified-path / LANG: type/effect grammar has no module-qualified path —
Pkg.Type/Pkg.Effectunrepresentable (estate-wide port blocker; ADR-014) #228 alternative: mechanical-but-divergent, entrenches a positional workaround the language doesn't need.
Additional context
- Mirrors
#228in shape: a settled language facility (effect rows on fn types) that the grammar admits in one position but not another. The fix is a single grammar production, on the same scale as#241. - Real port surfaced this: idaptik
src/shared/Coprocessor.res->migration/shared/Coprocessor.affine(feat(parser): fn(params) => expr lambdas + #135 triage punch list #153) deliberately omitted theexecutecallback field because the slash-form failed in record-field position. The arrow-form workaround works but the file was the natural Mode-A port and should round-trip the slash form. - Oracle / build:
dune build bin/main.exe,AFFINESCRIPT_STDLIB=$PWD/stdlib,main.exe check <file>; classify on stdout (Type checking passedvsparse error).
Refs
Refs #228 (the precedent — language-side grammar gap surfacing during the estate port)
Refs #229 (estate-wide ReScript-surface elimination — this is one of the residual blockers)
Refs hyperpolymath/idaptik#153 (the port that surfaced this — describes the workaround used)
Activity
feat(lang): `const mut <name>: T = init;` module-level mutable binding (
feat(parser): accept slash-effect-row in type-expression position (cl…