Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

We've found a bug for you!
/.../fixtures/object_coercion_mutable_unequal.res:8:35-57

6 │ type wide = {"a": int, "b": int}
7 │ type narrow = {"a": int}
8 │ let p = (v: {@set "x": wide}) => (v :> {@set "x": narrow})
9 │

Type {"x": wide, "x#=": wide => unit} is not a subtype of
{"x": narrow, "x#=": narrow => unit}
Type narrow = {"a": int} is not a subtype of wide = {"a": int, "b": int}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

We've found a bug for you!
/.../fixtures/object_coercion_open_open.res:9:32-51

7 │ type wide = {"a": int, "b": int}
8 │ type narrow = {"a": int}
9 │ let p = (o: {.."x": wide}) => (o :> {.."x": narrow})
10 │

Type {.."x": wide} is not a subtype of {.."x": narrow}
Type wide = {"a": int, "b": int} is not compatible with type
narrow = {"a": int}

The second object is expected to have a field "b" of type int, but it does not.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

We've found a bug for you!
/.../fixtures/object_coercion_promote_readonly_caller.res:11:11-18

9 │ let f = (o: {.."x": wide}) => (o :> {@set "x": wide})
10 │ @val external readonly: {"x": wide} = "readonly"
11 │ let _ = f(readonly)
12 │

This has type: {"x": wide}
But this function argument is expecting: {.."x": wide, "x#=": wide => unit}

The first object is expected to have a field "x#=" of type wide => unit, but it does not.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

We've found a bug for you!
/.../fixtures/object_coercion_readonly_to_mutable.res:6:20-39

4 │ See docs/object_representation_cleanup.md. */
5 │ type t = {"x": int}
6 │ let p = (v: t) => (v :> {@set "x": int})
7 │

Type t = {"x": int} is not a subtype of {"x": int, "x#=": int => unit}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

We've found a bug for you!
/.../fixtures/object_open_write_readonly_caller.res:8:11-18

6 │ let f = (o: {.."x": int}) => o["x"] = 1
7 │ @val external readonly: {"x": int} = "readonly"
8 │ let _ = f(readonly)
9 │

This has type: {"x": int}
But this function argument is expecting: {.."x": int, "x#=": int => unit}

The first object is expected to have a field "x#=" of type int => unit, but it does not.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

We've found a bug for you!
/.../fixtures/object_write_after_open_target_coercion.res:12:3

10 │ let p = (v: {"x": wide}) => {
11 │ let r = (v :> {.."x": narrow})
12 │ r["x"] = {"a": 1}
13 │ }
14 │

This expression has type {"x": narrow}
It has no field x#=
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

We've found a bug for you!
/.../fixtures/object_write_closed_row.res:6:28

4 │ See docs/object_representation_cleanup.md; compiling counterparts in
5 │ tests/tests/src/object_mutability_pin.res. */
6 │ let g = (o: {"x": int}) => o["x"] = 1
7 │

This expression has type {"x": int}
It has no field x#=
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* Pin (object mutability cleanup): mutable-to-mutable coercion is invariant
in the field type — with unequal types it is rejected (today the setter
member demands contravariance while the getter demands covariance). Must
stay an error under the new model (Mutable A <: Mutable B iff A = B).
See docs/object_representation_cleanup.md. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace the missing object-cleanup document reference

These pinning fixtures repeatedly direct maintainers to docs/object_representation_cleanup.md, but a repo-wide file search finds no such document or generated artifact. This leaves the rationale for the deliberately temporary expectations behind a dead reference; include the design document or replace the reference with a durable link to the documented #8584 design.

Useful? React with 👍 / 👎.

type wide = {"a": int, "b": int}
type narrow = {"a": int}
let p = (v: {@set "x": wide}) => (v :> {@set "x": narrow})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Register the new fixtures in the error catalog

The new object fixtures exercise the Not_subtype and Undefined_method variants, but tests/ERROR_VARIANTS.md is unchanged, so the repository’s primary coverage catalog no longer reflects all fixtures that cover those variants. Add these fixtures to the corresponding catalog rows as required by the repository guidance.

AGENTS.md reference: AGENTS.md:L197-L200

Useful? React with 👍 / 👎.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* Pin (object mutability cleanup): when BOTH rows are open, object fields
are invariant — this covariant coercion is rejected. Principled, not an
artifact: an open result is a promotable result, and a covariantly
weakened field must never remain promotable (a later write at the narrow
type would reach readers at the wide type). Must stay an error under the
new model. See docs/object_representation_cleanup.md. */
type wide = {"a": int, "b": int}
type narrow = {"a": int}
let p = (o: {.."x": wide}) => (o :> {.."x": narrow})
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* Pin (object mutability cleanup): COERCION-driven strengthening (as
opposed to the assignment-driven case in
object_open_write_readonly_caller.res): coercing an open-row parameter to
a same-type mutable target constrains the row, so a read-only caller is
rejected. Both halves must survive the new model (the coercion promotes
the open source's field; the demand becomes a Mutable field).
See docs/object_representation_cleanup.md. */
type wide = {"a": int, "b": int}
let f = (o: {.."x": wide}) => (o :> {@set "x": wide})
@val external readonly: {"x": wide} = "readonly"
let _ = f(readonly)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* Pin (object mutability cleanup): a closed read-only field cannot be
coerced to a settable one — write capability cannot be conjured. Must
stay an error under the new model (closed row: no promotion).
See docs/object_representation_cleanup.md. */
type t = {"x": int}
let p = (v: t) => (v :> {@set "x": int})
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* Pin (object mutability cleanup): writing a bare field of an OPEN row is
accepted but strengthens the function's demand — callers must supply a
writable field, so a read-only argument is rejected. Both halves must
survive the new model (write = promotion on the open row; the demand
becomes a Mutable field). See docs/object_representation_cleanup.md. */
let f = (o: {.."x": int}) => o["x"] = 1
@val external readonly: {"x": int} = "readonly"
let _ = f(readonly)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/* Pin (object mutability cleanup): coercing a CLOSED source to an open
target yields a result whose row tail is instantiated from the source,
i.e. closed — so a subsequent write is rejected (the error even prints
the result type as the closed {"x": narrow}). This is what makes the
covariant closed-source/open-target coercion sound: the result is not
promotable. Must stay an error under the new model.
See docs/object_representation_cleanup.md. */
type wide = {"a": int, "b": int}
type narrow = {"a": int}
let p = (v: {"x": wide}) => {
let r = (v :> {.."x": narrow})
r["x"] = {"a": 1}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/* Pin (object mutability cleanup): writing a bare field of a CLOSED object
row is an error — the row cannot acquire a setter. Must stay an error
under the new model (Immutable field in a closed row cannot be promoted).
See docs/object_representation_cleanup.md; compiling counterparts in
tests/tests/src/object_mutability_pin.res. */
let g = (o: {"x": int}) => o["x"] = 1
62 changes: 62 additions & 0 deletions tests/tests/src/object_mutability_pin.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Generated by ReScript, PLEASE EDIT WITH CARE


function forget_write_covariant(v) {
return v;
}

function open_source_covariant(o) {
return o;
}

function closed_source_open_target(v) {
return v;
}

function open_source_promote_same_type(o) {
return o;
}

function open_row_write(o) {
o.x = 1;
}

function read_x(obj) {
return obj.x;
}

function read_from_settable() {
return settableObj.x;
}

function read_from_readonly() {
return readonlyObj.x;
}

function open_source_setter_narrower(o) {
return o;
}

function unrelated_setter_type(o) {
o.x = "hello";
return o.x;
}

function run_unrelated_setter() {
return unrelated_setter_type(plainIntObj);
}

export {
forget_write_covariant,
open_source_covariant,
closed_source_open_target,
open_source_promote_same_type,
open_row_write,
read_x,
read_from_settable,
read_from_readonly,
open_source_setter_narrower,
unrelated_setter_type,
run_unrelated_setter,
}
/* No side effect */
79 changes: 79 additions & 0 deletions tests/tests/src/object_mutability_pin.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/* Pins the current typing behavior of object-field mutability (encoded today
as phantom `"x#="` setter members) ahead of the representation cleanup
described in docs/object_representation_cleanup.md.

Every case in this file compiles today. The ones marked EXPECTED TO FLIP
are intentionally rejected by the new model (single storage location: a
field has one type; promotion only adds write capability, it never
changes the type). The others must keep compiling unchanged.

The rejecting counterparts are pinned in
tests/build_tests/super_errors/fixtures/object_*.res. */

type wide = {"a": int, "b": int}
type narrow = {"a": int} /* wide <: narrow (width subtyping) */

/* Closed rows: coercion may forget write capability, covariantly.
(Mutable A :> Immutable B with A <: B.) */
let forget_write_covariant = (v: {@set "x": wide}): {"x": narrow} => (v :> {"x": narrow})

/* Open source, closed immutable target: ordinary covariance. Sound forever:
the coerced alias is read-only, and a later promotion of the source
writes at the source's own field type. */
let open_source_covariant = (o: {.."x": wide}): {"x": narrow} => (o :> {"x": narrow})

/* Closed source, open target: covariant; the target's tail is instantiated
from the (closed) source, so the result is not promotable. */
let closed_source_open_target = (v: {"x": wide}) => (v :> {.."x": narrow})

/* Open source, mutable target at the SAME type: accepted, and constrains
callers to writable objects (today: absorbs the "x#=" member; new model:
promotion Immutable -> Mutable at the same type). */
let open_source_promote_same_type = (o: {.."x": wide}): {@set "x": wide} => (o :> {@set "x": wide})

/* Writing a bare field of an open row is accepted and strengthens the
demand on callers (today: adds "x#=" through the tail; new model:
promotion). The rejection of a read-only caller is pinned in
object_open_write_readonly_caller.res. */
let open_row_write = (o: {.."x": int}) => o["x"] = 1

/* A generalized getter accepts both read-only and settable objects. */
let read_x = obj => obj["x"]

@val external settable_obj: {@set "x": wide} = "settableObj"
@val external readonly_obj: {"x": wide} = "readonlyObj"

let read_from_settable = (): wide => read_x(settable_obj)
let read_from_readonly = (): wide => read_x(readonly_obj)

/* EXPECTED TO FLIP: today an open row can acquire a setter at a DIFFERENT
type than its getter, because writability is a separate member — this
coercion leaves getter type `wide` and setter type `narrow` on one
property. The new model is capability-only (Immutable A -> Mutable A,
then A = B required), so this becomes a compile error when the cleanup's
Stage D lands. */
let open_source_setter_narrower = (o: {.."x": wide}): {@set "x": narrow} =>
(o :> {@set "x": narrow})

/* EXPECTED TO FLIP (unsoundness, the strongest case).

Today this whole block compiles, and `run_unrelated_setter()` returns
"hello" at declared type `int` when `plain_int_obj` is the plain JS
object {x: 1}.

Mechanism: writability is a separate row member, so the assignment mints
"x#=": string => unit in o's open row from the right-hand side's type,
never relating it to the getter "x": int. The inferred demand is
{.."x": int, "x#=": string => unit} — but both members compile to the
same storage `o.x`, so the write invalidates the getter's type.

New model: the assignment promotes the field to `Mutable int`, and
assigning a `string` is a unification error — the flip enforces the
getter/setter consistency invariant that is missing today. */
let unrelated_setter_type = (o: {.."x": int}): int => {
o["x"] = "hello"
o["x"]
}

@val external plain_int_obj: {.."x": int} = "plainIntObj"
let run_unrelated_setter = (): int => unrelated_setter_type(plain_int_obj)