From 1ed8c04f40d1bd5846ee1ab8fe1f2f8d7a345f95 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 17 May 2026 07:49:36 +0100 Subject: [PATCH] feat(parser): bare effect decl + ADR-008 -> T / E effect row (#135 slice 3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #135 slice 3 — two coupled grammar gaps, both pervasively used by the stdlib effect layer and both mandated by settled ADRs: 1. Bare `effect ;` 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` (`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) --- lib/parser.mly | 28 ++++++++++++++++++++++++++++ test/test_e2e.ml | 12 ++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/parser.mly b/lib/parser.mly index 98142a8b..cef36cc2 100644 --- a/lib/parser.mly +++ b/lib/parser.mly @@ -191,6 +191,24 @@ fn_decl: return_type: | ARROW ty = type_expr { (Some ty, None) } | MINUS LBRACE eff = effect_expr RBRACE ARROW ty = type_expr { (Some ty, Some eff) } + /* `-> T / E` — the ADR-008 canonical effect-row return annotation + (issue #135 slice 3). This was the *settled* surface + (SETTLED-DECISIONS ADR-008) yet entirely absent from the grammar; the + whole stdlib effect layer uses it (`extern fn print(s: String) -> + Unit / io;` etc.) and even a normal `fn ... -> T / io` could not be + written. Single `effect_term` covers 100% of stdlib usage; multi- + effect rows remain expressible via the existing `-{ E1 + E2 }->` + form. Grammar-cost note: adding any `ARROW type_expr SLASH _` + production to `return_type` adds exactly one arbitrarily-resolved + reduce/reduce item to an already-r/r state (35 -> 36; the 5 r/r + states are unchanged). This is irreducible without a grammar-wide + restructure of return/effect parsing, is in this grammar's existing + permissive-ambiguity class, and per ADR-009 ("conformance suite is + authoritative; the parser conforms to the spec, validated by tests") + is accepted: behaviour is verified — `-> T / io` parses, division + `a / b`, `-{ E }->`, and braced `effect E {}` are all unaffected, + full suite green. ADR-008 mandates this syntax; it is not sugar. */ + | ARROW ty = type_expr SLASH eff = effect_term { (Some ty, Some eff) } fn_body: | blk = block { FnBlock blk } @@ -489,6 +507,16 @@ effect_decl: ed_name = name; ed_type_params = Option.value type_params ~default:[]; ed_ops = ops } } + /* Bare forward-declaration form `effect ;` (issue #135 slice 3). + stdlib/effects.affine declares `effect io;` / `effect state;` etc. and + supplies the operations separately as `extern fn ... / io;`. An empty + op list is the right model: the effect is a named row label whose ops + live in externs. */ + | vis = visibility? EFFECT name = ident type_params = type_params? SEMICOLON + { { ed_vis = Option.value vis ~default:Private; + ed_name = name; + ed_type_params = Option.value type_params ~default:[]; + ed_ops = [] } } effect_op_decl: (* Type parameters on effect operations are allowed: `fn await[T](promise: Promise[T]) -> T;` *) diff --git a/test/test_e2e.ml b/test/test_e2e.ml index 4255fb26..117e4141 100644 --- a/test/test_e2e.ml +++ b/test/test_e2e.ml @@ -3331,6 +3331,17 @@ let test_slice_index_not_regressed () = (parse_check_passes {|fn idx(xs: [Int]) -> Int { return xs[0]; }|}) +(* Issue #135 slice 3: bare `effect E;` declaration + the ADR-008 canonical + `-> T / E1, E2` effect-row return annotation (was settled but entirely + absent from the grammar; the whole stdlib effects/io layer uses it). *) +let test_bare_effect_and_effect_row () = + Alcotest.(check bool) "effect E; + extern -> T / E + fn -> T / E" true + (parse_check_passes + {|effect io; + extern fn write(s: String) -> Unit / io; + fn q() -> Int / io { return 0; } + fn plain() -> Int { return 1; }|}) + let test_multi_arg_arrow () = Alcotest.(check bool) "(A, B) -> C parses + typechecks" true (parse_check_passes @@ -3383,6 +3394,7 @@ let type_syntax_sugar_tests = [ Alcotest.test_case "fn(x:Int) -> Int { } (#135 fn-lambda)" `Quick test_fn_lambda_typed_block; Alcotest.test_case "xs[a:b]/[a:]/[:b]/[:] (#135 slice 2)" `Quick test_slice_full_range; Alcotest.test_case "xs[0] index non-regressed (#135 sl.2)" `Quick test_slice_index_not_regressed; + Alcotest.test_case "effect E; + -> T / E (#135 slice 3)" `Quick test_bare_effect_and_effect_row; Alcotest.test_case "(A, B) -> C (multi-arg arrow)" `Quick test_multi_arg_arrow; Alcotest.test_case "(A, B) without arrow remains tuple" `Quick test_tuple_type_still_works; ]