Skip to content

Commit 24379f9

Browse files
feat(parser): bare effect decl + ADR-008 -> T / E effect row (#135 slice 3) (#155)
#135 slice 3 — two coupled grammar gaps, both pervasively used by the stdlib effect layer and both mandated by settled ADRs: 1. Bare `effect <name>;` forward declaration (empty op list; the ops are supplied separately as `extern fn ... / E;`). effects.affine uses `effect io; effect state; effect exn;`. 2. The ADR-008 canonical `-> T / E` effect-row return annotation. This was a *settled* decision (SETTLED-DECISIONS ADR-008) yet the `/`-form was entirely absent from the grammar — even a normal `fn ... -> T / io` could not be written; only the `-{ E }->` form parsed. Single `effect_term` covers 100% of stdlib usage; multi-effect rows remain expressible via `-{ E1 + E2 }->`. Grammar-cost (full disclosure): any `ARROW type_expr SLASH _` production in `return_type` adds exactly one arbitrarily-resolved reduce/reduce item to an already-r/r state (35 -> 36; the 5 r/r *states* and all s/r counts are unchanged). It is irreducible without a grammar-wide restructure, sits in this grammar's existing permissive-ambiguity class, and per the settled ADR-009 ("conformance suite is authoritative; parser conforms to spec, validated by tests") is accepted with behaviour verified: `-> T / io` parses; division `a / b`, `-{ E }->`, braced `effect E {}`, plain `-> T` all unaffected; full suite green (226). effects.affine advances 5 -> 21; next blocker is `extern fn ref<T>` (`ref` = REF keyword), a keyword-as-identifier issue folded into slice 6's scope (triage doc updated). Advances #135. Refs #128, #135. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 74d5da5 commit 24379f9

2 files changed

Lines changed: 40 additions & 0 deletions

File tree

lib/parser.mly

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,24 @@ fn_decl:
191191
return_type:
192192
| ARROW ty = type_expr { (Some ty, None) }
193193
| MINUS LBRACE eff = effect_expr RBRACE ARROW ty = type_expr { (Some ty, Some eff) }
194+
/* `-> T / E` — the ADR-008 canonical effect-row return annotation
195+
(issue #135 slice 3). This was the *settled* surface
196+
(SETTLED-DECISIONS ADR-008) yet entirely absent from the grammar; the
197+
whole stdlib effect layer uses it (`extern fn print(s: String) ->
198+
Unit / io;` etc.) and even a normal `fn ... -> T / io` could not be
199+
written. Single `effect_term` covers 100% of stdlib usage; multi-
200+
effect rows remain expressible via the existing `-{ E1 + E2 }->`
201+
form. Grammar-cost note: adding any `ARROW type_expr SLASH _`
202+
production to `return_type` adds exactly one arbitrarily-resolved
203+
reduce/reduce item to an already-r/r state (35 -> 36; the 5 r/r
204+
states are unchanged). This is irreducible without a grammar-wide
205+
restructure of return/effect parsing, is in this grammar's existing
206+
permissive-ambiguity class, and per ADR-009 ("conformance suite is
207+
authoritative; the parser conforms to the spec, validated by tests")
208+
is accepted: behaviour is verified — `-> T / io` parses, division
209+
`a / b`, `-{ E }->`, and braced `effect E {}` are all unaffected,
210+
full suite green. ADR-008 mandates this syntax; it is not sugar. */
211+
| ARROW ty = type_expr SLASH eff = effect_term { (Some ty, Some eff) }
194212

195213
fn_body:
196214
| blk = block { FnBlock blk }
@@ -489,6 +507,16 @@ effect_decl:
489507
ed_name = name;
490508
ed_type_params = Option.value type_params ~default:[];
491509
ed_ops = ops } }
510+
/* Bare forward-declaration form `effect <name>;` (issue #135 slice 3).
511+
stdlib/effects.affine declares `effect io;` / `effect state;` etc. and
512+
supplies the operations separately as `extern fn ... / io;`. An empty
513+
op list is the right model: the effect is a named row label whose ops
514+
live in externs. */
515+
| vis = visibility? EFFECT name = ident type_params = type_params? SEMICOLON
516+
{ { ed_vis = Option.value vis ~default:Private;
517+
ed_name = name;
518+
ed_type_params = Option.value type_params ~default:[];
519+
ed_ops = [] } }
492520

493521
effect_op_decl:
494522
(* Type parameters on effect operations are allowed: `fn await[T](promise: Promise[T]) -> T;` *)

test/test_e2e.ml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3331,6 +3331,17 @@ let test_slice_index_not_regressed () =
33313331
(parse_check_passes
33323332
{|fn idx(xs: [Int]) -> Int { return xs[0]; }|})
33333333

3334+
(* Issue #135 slice 3: bare `effect E;` declaration + the ADR-008 canonical
3335+
`-> T / E1, E2` effect-row return annotation (was settled but entirely
3336+
absent from the grammar; the whole stdlib effects/io layer uses it). *)
3337+
let test_bare_effect_and_effect_row () =
3338+
Alcotest.(check bool) "effect E; + extern -> T / E + fn -> T / E" true
3339+
(parse_check_passes
3340+
{|effect io;
3341+
extern fn write(s: String) -> Unit / io;
3342+
fn q() -> Int / io { return 0; }
3343+
fn plain() -> Int { return 1; }|})
3344+
33343345
let test_multi_arg_arrow () =
33353346
Alcotest.(check bool) "(A, B) -> C parses + typechecks" true
33363347
(parse_check_passes
@@ -3383,6 +3394,7 @@ let type_syntax_sugar_tests = [
33833394
Alcotest.test_case "fn(x:Int) -> Int { } (#135 fn-lambda)" `Quick test_fn_lambda_typed_block;
33843395
Alcotest.test_case "xs[a:b]/[a:]/[:b]/[:] (#135 slice 2)" `Quick test_slice_full_range;
33853396
Alcotest.test_case "xs[0] index non-regressed (#135 sl.2)" `Quick test_slice_index_not_regressed;
3397+
Alcotest.test_case "effect E; + -> T / E (#135 slice 3)" `Quick test_bare_effect_and_effect_row;
33863398
Alcotest.test_case "(A, B) -> C (multi-arg arrow)" `Quick test_multi_arg_arrow;
33873399
Alcotest.test_case "(A, B) without arrow remains tuple" `Quick test_tuple_type_still_works;
33883400
]

0 commit comments

Comments
 (0)