Skip to content

Commit 3bfcc5b

Browse files
committed
Distinguish structural and inferred object rows
Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
1 parent 25bcd3e commit 3bfcc5b

13 files changed

Lines changed: 104 additions & 17 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
- Remove the deprecated `Js` namespace and its runtime modules. https://github.com/rescript-lang/rescript/pull/8531
1919
- Move Belt into the separately installed `@rescript/belt` package. Projects using Belt must install the package and list it in their `rescript.json` dependencies. https://github.com/rescript-lang/rescript/pull/8554
2020
- Correct the structured function details produced by `rescript-tools doc` and exposed by `RescriptTools.Docgen`: parameters now retain labels and optionality, nested functions, tuples, variables, and generic arguments retain their type structure, return types are identified correctly, and non-function values no longer receive fake function details. This changes the published docgen detail schema. https://github.com/rescript-lang/rescript/pull/8576
21-
- Make object-field mutability part of the type. A property has one type for reading and writing, `obj["x"] = v` requires the field to be settable (`@set`, or an inferred open row, which the write makes settable), and a coercion never grants or widens write capability. Previously the getter type and a hidden mangled `"x#="` setter member were tracked independently, so a property could be written at a different type than it was read, and a value coerced to a type without `@set` could still be written through. https://github.com/rescript-lang/rescript/pull/8597
21+
- Make object-field mutability part of the type. A property has one type for reading and writing. Assignment requires `@set`, except on an inferred open row, where assignment makes the field settable. Private rows are not inferred open rows, so a field in `type t = private {.."x": int}` is writable only when annotated with `@set`. Coercions never grant or widen write capability. Previously, getter and setter types were tracked independently, allowing a property to be written at a different type than it was read and allowing writes through a value coerced to a type without `@set`. https://github.com/rescript-lang/rescript/pull/8597
2222
- Remove the undocumented object-field attribute forms `@get` (bare or with a `null`/`undefined`/`nullable` payload) and `@set({no_get: ...})` on object types. Only bare `@set` marks a field settable; nullable getter types are written directly (`null<t>`, `undefined<t>`, `nullable<t>`). https://github.com/rescript-lang/rescript/pull/8597
2323

2424
#### :eyeglasses: Spec Compliance

compiler/ml/ctype.ml

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ let rec object_row ty =
316316
| Tfield {rest = t} -> object_row t
317317
| _ -> ty
318318

319-
let opened_object ty =
319+
let object_row_is_structurally_open ty =
320320
match (object_row ty).desc with
321321
| Tvar _ | Tunivar _ | Tconstr _ -> true
322322
| _ -> false
@@ -2648,13 +2648,17 @@ type object_field_write_error = Owrite_missing | Owrite_not_mutable
26482648

26492649
(* Look up [name] for assignment in the object type [ty].
26502650
- A [Mutable] field yields its type.
2651-
- An [Immutable] field is promoted iff the object row is open; on a
2652-
closed row the write is rejected.
2653-
- An absent field is added as [Mutable] through an open row; on a closed
2654-
row the write is rejected as missing. *)
2651+
- An [Immutable] field is promoted iff the row ends in a [Tvar], the
2652+
same predicate [unify_mutability] uses.
2653+
[object_row_is_structurally_open] also holds for rigid [Tunivar] and
2654+
private-row [Tconstr] terminators. It cannot therefore gate promotion;
2655+
in particular, ordinary copies of a private row share the declaration's
2656+
mutability cell, so a write would later be saved as [@set] in the .cmi.
2657+
- An absent field is added as [Mutable] through a [Tvar] rest; on a
2658+
closed or private row the write is rejected as missing. *)
26552659
let filter_object_field_for_write env name ty :
26562660
(type_expr, object_field_write_error) Result.t =
2657-
let rec write_field ~opened ty =
2661+
let rec write_field ~can_promote ty =
26582662
let ty = expand_head_trace env ty in
26592663
match ty.desc with
26602664
| Tvar _ ->
@@ -2677,13 +2681,13 @@ let filter_object_field_for_write env name ty :
26772681
match mutability_repr mutability with
26782682
| Asttypes.Mutable -> Ok typ
26792683
| Immutable ->
2680-
if opened then (
2684+
if can_promote then (
26812685
set_mutability
26822686
(mutability_ref_repr mutability)
26832687
(Mutability_value Asttypes.Mutable);
26842688
Ok typ)
26852689
else Error Owrite_not_mutable
2686-
else write_field ~opened f.rest
2690+
else write_field ~can_promote f.rest
26872691
| _ -> Error Owrite_missing
26882692
in
26892693
let ty = expand_head_trace env ty in
@@ -2693,8 +2697,8 @@ let filter_object_field_for_write env name ty :
26932697
let ty' = newobj ty1 in
26942698
update_level env ty.level ty';
26952699
link_type ty ty';
2696-
write_field ~opened:true ty1
2697-
| Tobject f -> write_field ~opened:(opened_object ty) f
2700+
write_field ~can_promote:true ty1
2701+
| Tobject f -> write_field ~can_promote:(is_Tvar (object_row ty)) f
26982702
| _ -> Error Owrite_missing
26992703

27002704
(* Unify [ty] and [{.. name: 'a}]. Return ['a]. *)
@@ -3347,7 +3351,8 @@ let rec build_subtype env visited loops posi level t =
33473351
in
33483352
(newty (Tvariant row), Changed)
33493353
| Tobject t1 ->
3350-
if memq_warn t visited || opened_object t1 then (t, Unchanged)
3354+
if memq_warn t visited || object_row_is_structurally_open t1 then
3355+
(t, Unchanged)
33513356
else
33523357
let level' = pred_enlarge level in
33533358
let visited =

compiler/ml/ctype.mli

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,13 @@ val flatten_fields : type_expr -> fields * type_expr
117117
(* Transform a field type into a sorted list of field infos *)
118118
val associate_fields :
119119
fields -> fields -> (field_info * field_info) list * fields * fields
120-
val opened_object : type_expr -> bool
120+
121+
val object_row_is_structurally_open : type_expr -> bool
122+
(** Whether an object row is structurally open: its terminator is a [Tvar],
123+
[Tunivar], or [Tconstr], rather than [Tnil]. This does not imply that the
124+
row can be strengthened. Row-strengthening operations that add a field or
125+
promote field mutability require a [Tvar] terminator. *)
126+
121127
val lid_of_path : ?hash:string -> Path.t -> Longident.t
122128

123129
val sort_row_fields : (label * row_field) list -> (label * row_field) list

compiler/ml/printtyp.ml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -501,7 +501,8 @@ let rec mark_loops_rec visited ty =
501501
| Tobject fi ->
502502
if List.memq px !visited_objects then add_alias px
503503
else (
504-
if opened_object ty then visited_objects := px :: !visited_objects;
504+
if object_row_is_structurally_open ty then
505+
visited_objects := px :: !visited_objects;
505506
let fields, _ = flatten_fields fi in
506507
List.iter (fun {Ctype.f_typ} -> mark_loops_rec visited f_typ) fields)
507508
| Tfield {typ = ty1; rest = ty2} ->

compiler/ml/typetexp.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ and transl_fields env policy o fields =
610610
let t = expand_head env cty.ctyp_type in
611611
match (t, nm) with
612612
| {desc = Tobject {desc = (Tfield _ | Tnil) as tf}}, _ ->
613-
if opened_object t then
613+
if object_row_is_structurally_open t then
614614
raise (Error (sty.ptyp_loc, env, Opened_object nm));
615615
let rec iter_add = function
616616
| Tfield {name = s; mutability; typ = ty1; rest = ty2} ->

tests/ERROR_VARIANTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ Source: [typecore.ml:27](../compiler/ml/typecore.ml).
216216
| `Wrong_name` || `wrong_name_record_field.res`, `Cross_record_extra_field` (multi) | |
217217
| `Name_type_mismatch` || `super_errors_multi/Cross_qualified_constructor_mismatch` | Cross-module constructor disambiguation. |
218218
| `Undefined_method` || `super_errors_multi/Cross_module_alias_dot_access`, `undefined_method` | |
219-
| `Object_field_not_mutable` || `object_write_closed_row`, `object_write_alias`, `object_write_after_forgetting` | Assignment to a field without `@set`; the latter two pin that promotion is per equivalence class (an alias write strengthens the shared constraint) and that a coercion never grants write capability. |
219+
| `Object_field_not_mutable` || `object_write_closed_row`, `object_write_alias`, `object_write_after_forgetting`, `object_private_row_write`, `object_private_row_write_through_signature` | Assignment to a field without `@set`. `object_write_alias` pins that promotion is per equivalence class, and `object_write_after_forgetting` pins that a coercion never grants write capability. The private-row fixtures pin that a `Tconstr` row terminator is structurally open but cannot be strengthened: writing through `type t = private {.."x": int}` (directly or via a signature) is rejected, matching `unify_mutability`. |
220220
| `Private_type` || `private_type_construction.res` | |
221221
| `Private_label` || `private_label.res` | |
222222
| `Not_subtype` || `subtype_*.res`, `coercion_arity_mismatch.res`, `dict_show_no_coercion.res`, etc. | |
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/object_private_row_write.res:7:23-32
4+
5+
5 │ pinned in tests/tests/src/object_mutability_pin.res. */
6+
6 │ type t = private {.."x": int}
7+
7 │ let write = (o: t) => o["x"] = 1
8+
8 │
9+
10+
This expression has type t
11+
The field x is not settable. Only fields annotated with @set, e.g. {@set "x": int}, can be assigned.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
2+
We've found a bug for you!
3+
/.../fixtures/object_private_row_write_through_signature.res:8:26-35
4+
5+
6 │ type t = private {.."x": int}
6+
7 │ }
7+
8 │ let mutate = (o: M.t) => o["x"] = 1
8+
9 │
9+
10+
This expression has type M.t
11+
The field x is not settable. Only fields annotated with @set, e.g. {@set "x": int}, can be assigned.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* A private row is not an inferred open row: writing a field that is
2+
not @set must be rejected. The Tconstr terminator shares the
3+
declaration's mutability cell, so a successful write would persist
4+
@set into the .cmi. The compiling counterpart (private {..@set}) is
5+
pinned in tests/tests/src/object_mutability_pin.res. */
6+
type t = private {.."x": int}
7+
let write = (o: t) => o["x"] = 1
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/* Dual of object_private_row_grants_set.res: a signature that does not
2+
grant @set cannot be written through from outside the module. */
3+
module M: {
4+
type t = private {.."x": int}
5+
} = {
6+
type t = private {.."x": int}
7+
}
8+
let mutate = (o: M.t) => o["x"] = 1

0 commit comments

Comments
 (0)