diff --git a/tests/build_tests/super_errors/expected/object_coercion_mutable_unequal.res.expected b/tests/build_tests/super_errors/expected/object_coercion_mutable_unequal.res.expected new file mode 100644 index 0000000000..1f15ef2292 --- /dev/null +++ b/tests/build_tests/super_errors/expected/object_coercion_mutable_unequal.res.expected @@ -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} \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/object_coercion_open_open.res.expected b/tests/build_tests/super_errors/expected/object_coercion_open_open.res.expected new file mode 100644 index 0000000000..73454f2319 --- /dev/null +++ b/tests/build_tests/super_errors/expected/object_coercion_open_open.res.expected @@ -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. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/object_coercion_promote_readonly_caller.res.expected b/tests/build_tests/super_errors/expected/object_coercion_promote_readonly_caller.res.expected new file mode 100644 index 0000000000..7d96ef7066 --- /dev/null +++ b/tests/build_tests/super_errors/expected/object_coercion_promote_readonly_caller.res.expected @@ -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. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/object_coercion_readonly_to_mutable.res.expected b/tests/build_tests/super_errors/expected/object_coercion_readonly_to_mutable.res.expected new file mode 100644 index 0000000000..8a305177c1 --- /dev/null +++ b/tests/build_tests/super_errors/expected/object_coercion_readonly_to_mutable.res.expected @@ -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} \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/object_open_write_readonly_caller.res.expected b/tests/build_tests/super_errors/expected/object_open_write_readonly_caller.res.expected new file mode 100644 index 0000000000..feb96929f8 --- /dev/null +++ b/tests/build_tests/super_errors/expected/object_open_write_readonly_caller.res.expected @@ -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. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/object_write_after_open_target_coercion.res.expected b/tests/build_tests/super_errors/expected/object_write_after_open_target_coercion.res.expected new file mode 100644 index 0000000000..1a830e566b --- /dev/null +++ b/tests/build_tests/super_errors/expected/object_write_after_open_target_coercion.res.expected @@ -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#= \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/object_write_closed_row.res.expected b/tests/build_tests/super_errors/expected/object_write_closed_row.res.expected new file mode 100644 index 0000000000..47f058fd0d --- /dev/null +++ b/tests/build_tests/super_errors/expected/object_write_closed_row.res.expected @@ -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#= \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/object_coercion_mutable_unequal.res b/tests/build_tests/super_errors/fixtures/object_coercion_mutable_unequal.res new file mode 100644 index 0000000000..56eab51928 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/object_coercion_mutable_unequal.res @@ -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. */ +type wide = {"a": int, "b": int} +type narrow = {"a": int} +let p = (v: {@set "x": wide}) => (v :> {@set "x": narrow}) diff --git a/tests/build_tests/super_errors/fixtures/object_coercion_open_open.res b/tests/build_tests/super_errors/fixtures/object_coercion_open_open.res new file mode 100644 index 0000000000..34824e62c6 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/object_coercion_open_open.res @@ -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}) diff --git a/tests/build_tests/super_errors/fixtures/object_coercion_promote_readonly_caller.res b/tests/build_tests/super_errors/fixtures/object_coercion_promote_readonly_caller.res new file mode 100644 index 0000000000..63579a6b8e --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/object_coercion_promote_readonly_caller.res @@ -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) diff --git a/tests/build_tests/super_errors/fixtures/object_coercion_readonly_to_mutable.res b/tests/build_tests/super_errors/fixtures/object_coercion_readonly_to_mutable.res new file mode 100644 index 0000000000..451b43ba01 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/object_coercion_readonly_to_mutable.res @@ -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}) diff --git a/tests/build_tests/super_errors/fixtures/object_open_write_readonly_caller.res b/tests/build_tests/super_errors/fixtures/object_open_write_readonly_caller.res new file mode 100644 index 0000000000..53ceb57788 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/object_open_write_readonly_caller.res @@ -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) diff --git a/tests/build_tests/super_errors/fixtures/object_write_after_open_target_coercion.res b/tests/build_tests/super_errors/fixtures/object_write_after_open_target_coercion.res new file mode 100644 index 0000000000..836ae8bee6 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/object_write_after_open_target_coercion.res @@ -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} +} diff --git a/tests/build_tests/super_errors/fixtures/object_write_closed_row.res b/tests/build_tests/super_errors/fixtures/object_write_closed_row.res new file mode 100644 index 0000000000..e3503b9403 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/object_write_closed_row.res @@ -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 diff --git a/tests/tests/src/object_mutability_pin.mjs b/tests/tests/src/object_mutability_pin.mjs new file mode 100644 index 0000000000..6b19aba79d --- /dev/null +++ b/tests/tests/src/object_mutability_pin.mjs @@ -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 */ diff --git a/tests/tests/src/object_mutability_pin.res b/tests/tests/src/object_mutability_pin.res new file mode 100644 index 0000000000..97a15cf900 --- /dev/null +++ b/tests/tests/src/object_mutability_pin.res @@ -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)