-
Notifications
You must be signed in to change notification settings - Fork 484
Pin current object-field mutability behavior ahead of representation cleanup #8585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: ffi-dynamic-import
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
|
|
||
| [1;31mWe've found a bug for you![0m | ||
| [36m/.../fixtures/object_coercion_mutable_unequal.res[0m:[2m8:35-57[0m | ||
|
|
||
| 6 [2m│[0m type wide = {"a": int, "b": int} | ||
| 7 [2m│[0m type narrow = {"a": int} | ||
| [1;31m8[0m [2m│[0m let p = (v: {@set "x": wide}) => ([1;31mv :> {@set "x": narrow}[0m) | ||
| 9 [2m│[0m | ||
|
|
||
| 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 @@ | ||
|
|
||
| [1;31mWe've found a bug for you![0m | ||
| [36m/.../fixtures/object_coercion_open_open.res[0m:[2m9:32-51[0m | ||
|
|
||
| 7 [2m│[0m type wide = {"a": int, "b": int} | ||
| 8 [2m│[0m type narrow = {"a": int} | ||
| [1;31m9[0m [2m│[0m let p = (o: {.."x": wide}) => ([1;31mo :> {.."x": narrow}[0m) | ||
| 10 [2m│[0m | ||
|
|
||
| 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 [1;33m"b"[0m of type [1;33mint[0m, but it does not. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
|
|
||
| [1;31mWe've found a bug for you![0m | ||
| [36m/.../fixtures/object_coercion_promote_readonly_caller.res[0m:[2m11:11-18[0m | ||
|
|
||
| 9 [2m│[0m let f = (o: {.."x": wide}) => (o :> {@set "x": wide}) | ||
| 10 [2m│[0m @val external readonly: {"x": wide} = "readonly" | ||
| [1;31m11[0m [2m│[0m let _ = f([1;31mreadonly[0m) | ||
| 12 [2m│[0m | ||
|
|
||
| This has type: [1;31m{"x": wide}[0m | ||
| But this function argument is expecting: [1;33m{.."x": wide, "x#=": wide => unit}[0m | ||
|
|
||
| The first object is expected to have a field [1;33m"x#="[0m of type [1;33mwide => unit[0m, but it does not. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
|
|
||
| [1;31mWe've found a bug for you![0m | ||
| [36m/.../fixtures/object_coercion_readonly_to_mutable.res[0m:[2m6:20-39[0m | ||
|
|
||
| 4 [2m│[0m See docs/object_representation_cleanup.md. */ | ||
| 5 [2m│[0m type t = {"x": int} | ||
| [1;31m6[0m [2m│[0m let p = (v: t) => ([1;31mv :> {@set "x": int}[0m) | ||
| 7 [2m│[0m | ||
|
|
||
| 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 @@ | ||
|
|
||
| [1;31mWe've found a bug for you![0m | ||
| [36m/.../fixtures/object_open_write_readonly_caller.res[0m:[2m8:11-18[0m | ||
|
|
||
| 6 [2m│[0m let f = (o: {.."x": int}) => o["x"] = 1 | ||
| 7 [2m│[0m @val external readonly: {"x": int} = "readonly" | ||
| [1;31m8[0m [2m│[0m let _ = f([1;31mreadonly[0m) | ||
| 9 [2m│[0m | ||
|
|
||
| This has type: [1;31m{"x": int}[0m | ||
| But this function argument is expecting: [1;33m{.."x": int, "x#=": int => unit}[0m | ||
|
|
||
| The first object is expected to have a field [1;33m"x#="[0m of type [1;33mint => unit[0m, but it does not. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
|
|
||
| [1;31mWe've found a bug for you![0m | ||
| [36m/.../fixtures/object_write_after_open_target_coercion.res[0m:[2m12:3[0m | ||
|
|
||
| 10 [2m│[0m let p = (v: {"x": wide}) => { | ||
| 11 [2m│[0m let r = (v :> {.."x": narrow}) | ||
| [1;31m12[0m [2m│[0m [1;31mr[0m["x"] = {"a": 1} | ||
| 13 [2m│[0m } | ||
| 14 [2m│[0m | ||
|
|
||
| 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 @@ | ||
|
|
||
| [1;31mWe've found a bug for you![0m | ||
| [36m/.../fixtures/object_write_closed_row.res[0m:[2m6:28[0m | ||
|
|
||
| 4 [2m│[0m See docs/object_representation_cleanup.md; compiling counterparts in | ||
| 5 [2m│[0m tests/tests/src/object_mutability_pin.res. */ | ||
| [1;31m6[0m [2m│[0m let g = (o: {"x": int}) => [1;31mo[0m["x"] = 1 | ||
| 7 [2m│[0m | ||
|
|
||
| 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. */ | ||
| type wide = {"a": int, "b": int} | ||
| type narrow = {"a": int} | ||
| let p = (v: {@set "x": wide}) => (v :> {@set "x": narrow}) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new object fixtures exercise the 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 |
| 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 */ |
| 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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.