From eede831ab44b9039a7f887a529d1f138af90dfef Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 17 May 2026 11:57:37 +0100 Subject: [PATCH] feat(parser): trait method default bodies (left-factored) (#135 slice 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #135 slice 5. `trait_item` had `fn_sig SEMICOLON` (TraitFn) and a whole `fn_decl` (TraitFnDefault) as separate productions; they share the long prefix `visibility? FN name params return_type?`, the LR conflict resolved toward the signature form, so a trait method with a default body — `pub fn ne(ref self, ref other: Self) -> Bool { ... }` in stdlib/traits.affine — failed at the `{`. Fix by left-factoring: parse `fn_sig` once, then branch on SEMICOLON (→ TraitFn) vs `fn_body` (→ TraitFnDefault built from the sig + body). The two trait-method forms now differ purely on the next token — the shared-prefix ambiguity is removed, not papered over. (`ref self`, sig-only methods, and associated types already worked and are verified non-regressed.) Trait defaults don't use `total`/`where` (none in stdlib); those default to false/[]. Effect: traits.affine advances 12 → 124 (cleared 100+ lines; next blocker is `while let` / `Vec::new()`, a distinct slice-4-class construct). Two regression tests; full suite green (228). Advances #135. Refs #128, #135. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/parser.mly | 20 +++++++++++++++++++- test/test_e2e.ml | 23 +++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/lib/parser.mly b/lib/parser.mly index cef36cc2..95581948 100644 --- a/lib/parser.mly +++ b/lib/parser.mly @@ -552,7 +552,25 @@ supertraits: trait_item: | sig_ = fn_sig SEMICOLON { TraitFn sig_ } - | f = fn_decl { TraitFnDefault f } + /* Trait method *default body* (issue #135 slice 5). Previously the two + forms were `fn_sig SEMICOLON` vs a whole `fn_decl`, which share the + long prefix `visibility? FN name params return_type?`; the LR conflict + resolved toward the signature form, so `pub fn ne(ref self, ...) -> + Bool { ... }` (stdlib/traits.affine) failed at the `{`. Left-factor: + parse `fn_sig` once, then branch on SEMICOLON vs fn_body — no shared- + prefix ambiguity (the two now differ purely on the next token). + Trait defaults don't use `total`/`where` (none in stdlib); fd_total + = false, fd_where = []. */ + | sig_ = fn_sig body = fn_body + { TraitFnDefault { fd_vis = sig_.fs_vis; + fd_total = false; + fd_name = sig_.fs_name; + fd_type_params = sig_.fs_type_params; + fd_params = sig_.fs_params; + fd_ret_ty = sig_.fs_ret_ty; + fd_eff = sig_.fs_eff; + fd_where = []; + fd_body = body } } | TYPE name = ident kind = kind_annotation? default = type_default? SEMICOLON { TraitType { tt_name = name; tt_kind = kind; tt_default = default } } diff --git a/test/test_e2e.ml b/test/test_e2e.ml index 117e4141..dfb4fcbc 100644 --- a/test/test_e2e.ml +++ b/test/test_e2e.ml @@ -3342,6 +3342,27 @@ let test_bare_effect_and_effect_row () = fn q() -> Int / io { return 0; } fn plain() -> Int { return 1; }|}) +(* Issue #135 slice 5: trait method *default body* (left-factored vs the + signature form so the shared prefix no longer mis-resolves toward the + `;` form). `ref self` receiver + sig-only + assoc type unaffected. *) +let test_trait_default_body () = + Alcotest.(check bool) "trait fn with default body + ref self" true + (parse_check_passes + {|trait Eq { + pub fn eq(ref self, ref other: Self) -> Bool; + pub fn ne(ref self, ref other: Self) -> Bool { + return !self.eq(other); + } + }|}) + +let test_trait_sig_and_assoc_not_regressed () = + Alcotest.(check bool) "sig-only trait fn + associated type still parse" true + (parse_check_passes + {|trait Iter { + type Item; + pub fn next(mut self) -> Option; + }|}) + let test_multi_arg_arrow () = Alcotest.(check bool) "(A, B) -> C parses + typechecks" true (parse_check_passes @@ -3395,6 +3416,8 @@ let type_syntax_sugar_tests = [ 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 "trait default body + ref self (#135 sl5)" `Quick test_trait_default_body; + Alcotest.test_case "trait sig + assoc non-regressed (#135 sl5)" `Quick test_trait_sig_and_assoc_not_regressed; 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; ]