Skip to content

Commit 2b3f63c

Browse files
hyperpolymathclaude
andcommitted
fix(#559): implement + wire trait coherence checking
Trait.check_coherence was a no-op stub ("TODO: Check for overlapping impls") and was never called, so two impls of the same trait for the same (or unifiable) self type were silently accepted and method resolution picked whichever impl came first — an unsoundness. - Implement check_coherence: two impls overlap iff their self types unify after each is instantiated with fresh vars for its own type params (reuses fresh_impl_self_ty + Unify.unify; the existing OverlappingImpl error). - Add check_all_coherence over every registered trait. - Wire it into Typecheck.check_program after the forward pass registers all impls; surface via a new, clearly-named TraitCoherenceError type_error. - Tests (E2E Trait Coherence #559): duplicate concrete impl → rejected; impls for distinct types → accepted; distinct generic args (Pair[Int,Bool] vs Pair[Bool,Int]) → accepted (no over-reject). Known follow-up (documented in-test): generic-subsumption overlap (impl[T] for Box[T] vs impl for Box[Int]) is not yet detected — it rides on generic-impl handling that has separate pre-existing immaturities; the soundness hole #559 named (silent acceptance of overlapping concrete impls) is closed. 525 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bc061ad commit 2b3f63c

3 files changed

Lines changed: 144 additions & 7 deletions

File tree

lib/trait.ml

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -420,22 +420,56 @@ let find_method_for_type (registry : trait_registry) (self_ty : ty) (method_name
420420
in
421421
search_impls impls
422422

423-
(** Check for overlapping implementations *)
423+
(** Check for overlapping implementations of [trait_name] (#559).
424+
425+
Two impls overlap when some type could satisfy BOTH — i.e. their self
426+
types unify after each is instantiated with fresh unification variables
427+
for its own type parameters. Overlap makes method resolution ambiguous
428+
([find_impl] would return whichever impl happens to come first), so it is
429+
a coherence violation and must be rejected.
430+
431+
Each candidate is given a freshly-instantiated self type via
432+
{!fresh_impl_self_ty} so that one impl's generic parameters cannot leak
433+
into another; the unification side-effects land only on those fresh
434+
variables, which are discarded with each pair. Concrete (parameter-free)
435+
self types carry no unification variables, so unifying them never mutates
436+
the registry's stored types. *)
424437
let check_coherence (registry : trait_registry) (trait_name : string) : unit result =
425438
match Hashtbl.find_opt registry.impls trait_name with
426439
| None -> Ok ()
427440
| Some impls ->
428-
(* TODO: Check for overlapping impls *)
429-
(* For now, just ensure no duplicate self types *)
441+
let fresh_var () =
442+
let id = !Types.next_tyvar in
443+
Types.next_tyvar := id + 1;
444+
TVar (ref (Unbound (id, 0)))
445+
in
430446
let rec check_pairs = function
431447
| [] -> Ok ()
432-
| _impl :: rest ->
433-
(* Check if any impl in rest has same self_ty *)
434-
(* TODO: Proper unification check *)
435-
check_pairs rest
448+
| impl :: rest ->
449+
let rec check_against = function
450+
| [] -> Ok ()
451+
| other :: more ->
452+
let s1 = fresh_impl_self_ty impl fresh_var in
453+
let s2 = fresh_impl_self_ty other fresh_var in
454+
(match Unify.unify s1 s2 with
455+
| Ok () -> Error (OverlappingImpl (trait_name, impl.ti_self_ty))
456+
| Error _ -> check_against more)
457+
in
458+
(match check_against rest with
459+
| Ok () -> check_pairs rest
460+
| err -> err)
436461
in
437462
check_pairs impls
438463

464+
(** Run {!check_coherence} for every trait that has registered impls (#559).
465+
Returns the first overlap found, if any. *)
466+
let check_all_coherence (registry : trait_registry) : unit result =
467+
Hashtbl.fold (fun trait_name _impls acc ->
468+
match acc with
469+
| Error _ -> acc
470+
| Ok () -> check_coherence registry trait_name
471+
) registry.impls (Ok ())
472+
439473
(** Standard library traits - automatically registered *)
440474
let register_stdlib_traits (registry : trait_registry) : unit =
441475
(* Eq trait *)

lib/typecheck.ml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ type type_error =
123123
(** `break` / `continue` used outside any enclosing loop body
124124
(issue #459). The string carries the keyword name for the
125125
error message. *)
126+
| TraitCoherenceError of string
127+
(** Two impls of the same trait have overlapping (unifiable) self
128+
types, so method resolution would be ambiguous (#559). The string
129+
is the human-readable overlap description. *)
126130

127131
(* Known exports of stdlib/prelude.affine. Mirrors the same list in
128132
lib/face.ml — when an UnboundVariable fires at type-check time with
@@ -198,6 +202,12 @@ let show_type_error = function
198202
"`%s` used outside a loop body (#459). `break` and `continue` must be \
199203
lexically enclosed by a `while` or `for` loop."
200204
kw
205+
| TraitCoherenceError msg ->
206+
Printf.sprintf
207+
"Trait coherence violation (#559): %s. Two implementations whose self \
208+
types can unify would make method resolution ambiguous; remove or \
209+
narrow one of the overlapping impls."
210+
msg
201211

202212
let format_type_error = show_type_error
203213

@@ -2438,6 +2448,13 @@ let check_program ?(import_types : (string, scheme) Hashtbl.t option)
24382448
Ok ()
24392449
| _ -> Ok ()
24402450
) (Ok ()) prog.prog_decls in
2451+
(* #559: trait coherence — now that every impl is registered, reject
2452+
overlapping impls of the same trait (self types that unify). Done before
2453+
the check pass so an ambiguous instance base is reported up front. *)
2454+
let* () = match Trait.check_all_coherence ctx.trait_registry with
2455+
| Ok () -> Ok ()
2456+
| Error re -> Error (TraitCoherenceError (Trait.show_resolution_error re))
2457+
in
24412458
(* Check pass: verify all declarations *)
24422459
let result = List.fold_left (fun acc decl ->
24432460
let* () = acc in

test/test_e2e.ml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2315,6 +2315,91 @@ let handler_fence_tests = [
23152315
Alcotest.test_case "c: handle → loud failure" `Quick test_handle_c_loud_fail;
23162316
]
23172317

2318+
(* ============================================================================
2319+
Issue #559: trait coherence — overlapping impls must be rejected
2320+
============================================================================
2321+
2322+
`Trait.check_coherence` was a no-op stub (TODO) and unwired, so two impls of
2323+
the same trait for the same (or unifiable) self type were silently accepted
2324+
and method resolution picked whichever impl came first. `check_all_coherence`
2325+
now runs in `check_program` after every impl is registered and rejects any
2326+
pair of impls whose self types unify. *)
2327+
2328+
let tc_source src =
2329+
let prog = Parse_driver.parse_string ~file:"<test>" src in
2330+
match resolve_program prog with
2331+
| Error msg -> Error msg
2332+
| Ok (ctx, _) ->
2333+
(match Typecheck.check_program ctx.symbols prog with
2334+
| Ok _ -> Ok ()
2335+
| Error e -> Error (Typecheck.format_type_error e))
2336+
2337+
let test_coherence_duplicate_rejected () =
2338+
let src = {|
2339+
trait Greet { fn greet() -> Int; }
2340+
struct Person { age: Int }
2341+
impl Greet for Person { fn greet() -> Int = 1; }
2342+
impl Greet for Person { fn greet() -> Int = 2; }
2343+
fn main() -> Int = 0;
2344+
|} in
2345+
match tc_source src with
2346+
| Ok () ->
2347+
Alcotest.fail "#559: two impls of Greet for Person must be rejected as \
2348+
overlapping; the checker accepted them"
2349+
| Error msg ->
2350+
Alcotest.(check bool) "error names trait coherence" true
2351+
(contains_str "coherence" msg)
2352+
2353+
let test_coherence_distinct_types_ok () =
2354+
let src = {|
2355+
trait Greet { fn greet() -> Int; }
2356+
struct Person { age: Int }
2357+
struct Robot { id: Int }
2358+
impl Greet for Person { fn greet() -> Int = 1; }
2359+
impl Greet for Robot { fn greet() -> Int = 2; }
2360+
fn main() -> Int = 0;
2361+
|} in
2362+
match tc_source src with
2363+
| Ok () -> ()
2364+
| Error msg ->
2365+
Alcotest.failf "#559: impls of Greet for two DISTINCT types must be \
2366+
accepted (no overlap), got error: %s" msg
2367+
2368+
let test_coherence_distinct_generic_args_ok () =
2369+
(* Coherence must not over-reject: impls for the SAME generic head but
2370+
distinct concrete args (`Pair[Int,Bool]` vs `Pair[Bool,Int]`) do not
2371+
overlap and must both be accepted. *)
2372+
let src = {|
2373+
trait Greet { fn greet() -> Int; }
2374+
enum Pair[A, B] { Mk(A, B) }
2375+
impl Greet for Pair[Int, Bool] { fn greet() -> Int = 1; }
2376+
impl Greet for Pair[Bool, Int] { fn greet() -> Int = 2; }
2377+
fn main() -> Int = 0;
2378+
|} in
2379+
match tc_source src with
2380+
| Ok () -> ()
2381+
| Error msg ->
2382+
Alcotest.failf "#559: impls for Pair[Int,Bool] vs Pair[Bool,Int] do not \
2383+
overlap and must be accepted, got error: %s" msg
2384+
2385+
(* KNOWN LIMITATION (#559): generic-subsumption overlap — a blanket/generic
2386+
impl `impl[T] Greet for Box[T]` overlapping a specific `impl Greet for
2387+
Box[Int]` — is NOT yet detected. The coherence check itself instantiates
2388+
impl type params and would catch it, but generic *impl* handling has its
2389+
own separate immaturities (e.g. `impl[T] ... for Box[T]` currently trips a
2390+
spurious "Trait not found" before coherence runs), so this case rides on a
2391+
prerequisite that is not yet solid. The soundness-critical hole #559 named
2392+
— silently accepting overlapping *concrete* impls — IS fixed and covered by
2393+
the rejected/accepted tests above. Generic-subsumption coherence is tracked
2394+
as follow-up, not pinned here as an executable accept (it would mis-document
2395+
a moving target). *)
2396+
2397+
let coherence_tests = [
2398+
Alcotest.test_case "duplicate impl (same self type) → rejected" `Quick test_coherence_duplicate_rejected;
2399+
Alcotest.test_case "impls for distinct types → accepted" `Quick test_coherence_distinct_types_ok;
2400+
Alcotest.test_case "distinct generic args → accepted (no over-reject)" `Quick test_coherence_distinct_generic_args_ok;
2401+
]
2402+
23182403
(* ============================================================================
23192404
Issue #556: async CPS table-miss loud-fail fence
23202405
============================================================================
@@ -5652,6 +5737,7 @@ let tests =
56525737
("E2E Handler Fence (#555)", handler_fence_tests);
56535738
("E2E Async Fence (#556)", async_fence_tests);
56545739
("E2E Resume Soundness (#555)", resume_soundness_tests);
5740+
("E2E Trait Coherence (#559)", coherence_tests);
56555741
("E2E Ownership Verify", tw_verify_tests);
56565742
("E2E Cmd Linearity", cmd_linear_tests);
56575743
("E2E Boundary Verify", tw_interface_tests);

0 commit comments

Comments
 (0)