Summary
The foreign block parser tokenises its body line-by-line and rejects two natural multi-line shapes:
:requires (Cap) annotation on a line below foreign — fails with foreign: Expected: (name [:as alias] : type), got: (:requires (NetCap)).
- Type signatures wrapped across lines — fails with
foreign-type-tokens->sexp: Cannot parse foreign type tokens (per the pitfall doc).
Both forms parse fine when collapsed onto a single line. The result is awkward foreign lines that often run past 100 columns when broadcast propagators or capability-protected APIs need to be declared.
Filed from docs/tracking/2026-04-27_GOBLIN_PITFALLS.md § #20 and docs/tracking/2026-04-28_ETPROP_PITFALLS.md § #6.
Reproducer
Variant 1 — :requires on a separate line
ns r20
foreign racket "racket/base"
:requires (NetCap)
[string-length :as slen : String -> Int]
Actual (current main, Racket 9.1):
foreign: Expected: (name [:as alias] : type), got: (:requires (NetCap))
Workaround: collapse onto one line:
foreign racket "racket/base" :requires (NetCap) [string-length :as slen : String -> Int]
Variant 2 — multi-line type signature (per ETP-#6 pitfall doc)
The pitfall doc captures this shape:
foreign racket "racket/base"
[net-add-prop : Posit32 Posit32 Posit32
[List [List Posit32]] [List Posit32] Posit32 -> Posit32]
→ foreign-type-tokens->sexp: Cannot parse foreign type tokens
Workaround: a single (long) line:
[net-add-prop : Posit32 Posit32 Posit32 [List [List Posit32]] [List Posit32] Posit32 -> Posit32]
Why this matters
- Broadcast propagators in the propagator-FFI take several
[List ...] typed arguments; signatures naturally exceed 100 chars.
- Capability protected APIs (
:requires (Cap)) are exactly the right place to use line-continuation for readability — and they're the ones the parser refuses.
Suggested fix paths
The foreign form already lives in a WS-mode block context — the WS reader's existing indent-continuation could be applied to the foreign body. Smallest scope:
- Allow
:requires (...) on the line directly below foreign (indented past the foreign keyword).
- Allow multi-token type signatures inside
[name :as alias : ...] to wrap by treating the entire [...] form as a single token group regardless of internal newlines.
Related
Summary
The
foreignblock parser tokenises its body line-by-line and rejects two natural multi-line shapes::requires (Cap)annotation on a line belowforeign— fails withforeign: Expected: (name [:as alias] : type), got: (:requires (NetCap)).foreign-type-tokens->sexp: Cannot parse foreign type tokens(per the pitfall doc).Both forms parse fine when collapsed onto a single line. The result is awkward
foreignlines that often run past 100 columns when broadcast propagators or capability-protected APIs need to be declared.Filed from
docs/tracking/2026-04-27_GOBLIN_PITFALLS.md§ #20 anddocs/tracking/2026-04-28_ETPROP_PITFALLS.md§ #6.Reproducer
Variant 1 —
:requireson a separate lineActual (current
main, Racket 9.1):Workaround: collapse onto one line:
Variant 2 — multi-line type signature (per ETP-#6 pitfall doc)
The pitfall doc captures this shape:
→
foreign-type-tokens->sexp: Cannot parse foreign type tokensWorkaround: a single (long) line:
Why this matters
[List ...]typed arguments; signatures naturally exceed 100 chars.:requires (Cap)) are exactly the right place to use line-continuation for readability — and they're the ones the parser refuses.Suggested fix paths
The
foreignform already lives in a WS-mode block context — the WS reader's existing indent-continuation could be applied to theforeignbody. Smallest scope::requires (...)on the line directly belowforeign(indented past theforeignkeyword).[name :as alias : ...]to wrap by treating the entire[...]form as a single token group regardless of internal newlines.Related
letbinding variants #21 — multi-line clause body silently produces??__match-fail(sibling layout issue, distinct surface).defnbody on a single line silently drops trailing tokens #47 — bare-juxtapositiondefnbody silently drops trailing tokens (sibling).