Skip to content

Commit 052d3cc

Browse files
cknittclaude
andcommitted
Refuse coercing another type into a private record or variant
Why: A private type's values can only be built in the module that defines it, but structural coercion built them anywhere: a plain record, another module's private record with the same fields, a variant, a polymorphic variant or a string coerced into a private record or variant (#9124). `subtype_rec` refuses a coercion whose target's definition is a private record or variant unless the source is that same type. Coercing out of a private type is unchanged. Typing modified (coercion into a private type): Example: module Blob: { type t = private {size: float} } = { type t = {size: float} } type plain = {size: float} let fake = (p: plain) => (p :> Blob.t) Before (rule): a record coerces into a record with compatible fields, private or not After (rule): a record or variant coerces into a private record or variant only from that same type Diagnostic added (coercion into a private type): Example: let fake = (p: plain) => (p :> Blob.t) Before (diagnostic): none After (diagnostic): Type plain is not a subtype of Blob.t Blob.t is private: its values can only be built in the module that defines it, so another type cannot be coerced to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CMkrYaktctdKc4M41WF36t Claude-Session: https://claude.ai/code/session_01Kr4cjoUi2f5VpMvE6wT9bo Signed-off-by: Christoph Knittel <ck@cca.io>
1 parent 799a82f commit 052d3cc

18 files changed

Lines changed: 227 additions & 1 deletion
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Coercing another type into a private record or variant is an error: only the module defining a private type can build its values. Coercing out of a private type still works.

‎compiler/ml/ctype.ml‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ type subtype_context =
8787
right_record_name: Path.t;
8888
issues: Record_coercion.record_field_subtype_violation list;
8989
}
90+
| Coercion_into_private_type of {typename: Path.t}
9091

9192
exception Subtype of type_pairs * type_pairs * subtype_context option
9293

@@ -3500,6 +3501,20 @@ let extract_concrete_typedecl_opt env t =
35003501
| v -> Some v
35013502
| exception Not_found -> None
35023503

3504+
(* Whether [t2] is a private record or variant that [t1], another type, would
3505+
be coerced into *)
3506+
let coerces_into_private_type env t1 t2 =
3507+
match extract_concrete_typedecl_opt env t2 with
3508+
| Some
3509+
( p2,
3510+
_,
3511+
{type_private = Private; type_kind = Type_record _ | Type_variant _} )
3512+
-> (
3513+
match extract_concrete_typedecl_opt env t1 with
3514+
| Some (p1, _, _) -> not (Path.same p1 p2)
3515+
| None -> true)
3516+
| Some _ | None -> false
3517+
35033518
let rec subtype_rec env trace t1 t2 cstrs =
35043519
let t1 = repr t1 in
35053520
let t2 = repr t2 in
@@ -3564,6 +3579,20 @@ let rec subtype_rec env trace t1 t2 cstrs =
35643579
(trace, t1, t2, !(current_univar_pairs ()), None) :: cstrs)
35653580
| Tconstr (p1, _, _), _ when generic_private_abbrev env p1 ->
35663581
subtype_rec env trace (expand_abbrev_opt env t1) t2 cstrs
3582+
| _, Tconstr _ when coerces_into_private_type env t1 t2 ->
3583+
(* Only the module defining a private record or variant builds its
3584+
values; a coercion from another type would build one anywhere *)
3585+
let typename =
3586+
match extract_concrete_typedecl_opt env t2 with
3587+
| Some (p2, _, _) -> p2
3588+
| None -> assert false
3589+
in
3590+
( trace,
3591+
t1,
3592+
t2,
3593+
!(current_univar_pairs ()),
3594+
Some (Coercion_into_private_type {typename}) )
3595+
:: cstrs
35673596
| Tconstr (p1, [], _), Tconstr (p2, [], _)
35683597
when Path.same p1 Predef.path_int && Path.same p2 Predef.path_float ->
35693598
(* Int can always be coerced to float *)

‎compiler/ml/ctype.mli‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ type subtype_context =
5151
right_record_name: Path.t;
5252
issues: Record_coercion.record_field_subtype_violation list;
5353
} (** Records have fields that are not compatible. *)
54+
| Coercion_into_private_type of {typename: Path.t}
55+
(** The target is a private record or variant, whose values only its own
56+
module can build. *)
5457

5558
type type_pairs = (type_expr * type_expr) list
5659
exception Unify of type_pairs

‎compiler/ml/printtyp.ml‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1789,7 +1789,12 @@ let report_subtyping_error ppf env tr1 txt1 tr2 ctx =
17891789
fprintf ppf "@ - ";
17901790
print_record_field_subtype_violation ppf issue ~left_record_name
17911791
~right_record_name)
1792-
issues)
1792+
issues
1793+
| Coercion_into_private_type {typename} ->
1794+
fprintf ppf
1795+
"@ @{<info>%s@} is private: its values can only be built in the \
1796+
module that defines it, so another type cannot be coerced to it."
1797+
(Path.name typename))
17931798
| None -> ())
17941799

17951800
let report_ambiguous_type_error ppf env (tp0, tp0') tpl txt1 txt2 txt3 =
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/coercion_into_private_record.res:12:35-45
4+
5+
10 │
6+
11 │ // A plain record coerced into a private one: constructs a Blob.t from
7+
│ outside
8+
12 │ let fake = (p: plain): Blob.t => (p :> Blob.t)
9+
13 │ let s = Blob.size(fake({size: 1.}))
10+
14 │
11+
12+
Type plain is not a subtype of Blob.t
13+
14+
Blob.t is private: its values can only be built in the module that defines it, so another type cannot be coerced to it.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/coercion_into_private_record_from_private.res:16:15-37
4+
5+
14 │ }
6+
15 │
7+
16 │ let forged = (Fake.make(1.) :> Blob.t)
8+
17 │
9+
10+
Type Fake.t is not a subtype of Blob.t
11+
12+
Blob.t is private: its values can only be built in the module that defines it, so another type cannot be coerced to it.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/coercion_into_private_unboxed_variant.res:6:19-29
4+
5+
4 │ @unboxed type t = Text(string) | Number(int)
6+
5 │ }
7+
6 │ let fromString = ("a" :> Id.t)
8+
7 │
9+
10+
Type string is not a subtype of Id.t
11+
12+
Id.t is private: its values can only be built in the module that defines it, so another type cannot be coerced to it.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/coercion_into_private_variant.res:8:16-37
4+
5+
6 │
6+
7 │ type plain = Red | Green
7+
8 │ let forged = ((Red: plain) :> Color.t)
8+
9 │
9+
10+
Type plain is not a subtype of Color.t
11+
12+
Color.t is private: its values can only be built in the module that defines it, so another type cannot be coerced to it.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/coercion_into_private_variant_from_polyvariant.res:6:18-41
4+
5+
4 │ type t = Red | Green
6+
5 │ }
7+
6 │ let fromPoly = ((#Red: [#Red]) :> Color.t)
8+
7 │
9+
10+
Type [#Red] is not a subtype of Color.t
11+
12+
Color.t is private: its values can only be built in the module that defines it, so another type cannot be coerced to it.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Blob: {
2+
type t = private {size: float}
3+
let size: t => float
4+
} = {
5+
type t = {size: float}
6+
let size = b => b.size
7+
}
8+
9+
type plain = {size: float}
10+
11+
// A plain record coerced into a private one: constructs a Blob.t from outside
12+
let fake = (p: plain): Blob.t => (p :> Blob.t)
13+
let s = Blob.size(fake({size: 1.}))

0 commit comments

Comments
 (0)