Skip to content
Merged
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
6 changes: 5 additions & 1 deletion src/twocore/frontend/js/lower.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4849,7 +4849,11 @@ fn lower_static_call(
// argument of get/set is not modelled here.
"Reflect", "has", [t, k, ..] -> host("reflect_has", [t, k])
"Reflect", "get", [t, k, ..] -> host("reflect_get", [t, k])
"Reflect", "set", [t, k, v, ..] -> host("reflect_set", [t, k, v])
// Reflect.set(target, propertyKey, V[, receiver]): a missing receiver
// defaults to `target` (§28.1.13 step 4.a), so the ordinary 3-arg form still
// writes on the target. A distinct receiver diverts the write there.
"Reflect", "set", [t, k, v, r, ..] -> host("reflect_set", [t, k, v, r])
"Reflect", "set", [t, k, v] -> host("reflect_set", [t, k, v, t])
"Reflect", "deleteProperty", [t, k, ..] ->
host("reflect_delete_property", [t, k])
"Reflect", "ownKeys", [t, ..] -> host("reflect_own_keys", [t])
Expand Down
13 changes: 10 additions & 3 deletions src/twocore/runtime/rt_js.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -1084,10 +1084,17 @@ pub fn reflect_has(target: Dynamic, key: Dynamic) -> Dynamic
@external(erlang, "twocore_rt_js_ffi", "reflect_get")
pub fn reflect_get(target: Dynamic, key: Dynamic) -> Dynamic

/// `Reflect.set(target, key, value)` → JS boolean success; `target` must be an
/// object, else TypeError. A frozen target reports `false`. `receiver` is ignored.
/// `Reflect.set(target, key, value, receiver)` → JS boolean success; `target`
/// must be an object, else TypeError. A frozen target reports `false`. The write
/// lands on `receiver` (defaulting to `target` when the JS call omits it): a
/// primitive receiver or a non-writable/non-extensible receiver reports `false`.
@external(erlang, "twocore_rt_js_ffi", "reflect_set")
pub fn reflect_set(target: Dynamic, key: Dynamic, value: Dynamic) -> Dynamic
pub fn reflect_set(
target: Dynamic,
key: Dynamic,
value: Dynamic,
receiver: Dynamic,
) -> Dynamic

/// `Reflect.deleteProperty(target, key)` → JS boolean success; `target` must be an
/// object, else TypeError. A frozen/sealed non-configurable property reports `false`.
Expand Down
52 changes: 42 additions & 10 deletions src/twocore_rt_js_ffi.erl
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@
object_prevent_extensions/1, object_is_extensible/1,
object_seal/1, object_is_sealed/1,
object_from_entries/1, object_is/2, object_has_own/2,
reflect_has/2, reflect_get/2, reflect_set/3, reflect_delete_property/2,
reflect_has/2, reflect_get/2, reflect_set/4, reflect_delete_property/2,
reflect_own_keys/1, reflect_get_prototype_of/1, reflect_is_extensible/1,
reflect_prevent_extensions/1, reflect_apply/3,
json_stringify/3, json_parse/1,
Expand Down Expand Up @@ -1914,18 +1914,50 @@ reflect_get(Target, Key) when is_reference(Target) ->
reflect_get(Target, _Key) ->
type_error(Target).

%% Reflect.set(target, propertyKey, V) (§28.1.13) -> JS boolean success. TypeError
%% if target is not an Object. A frozen/non-writable target makes the write a no-op
%% and reports `false`; otherwise the write succeeds and reports `true`. The optional
%% `receiver` is not modelled.
reflect_set(Target, Key, V) when is_reference(Target) ->
%% Reflect.set(target, propertyKey, V, Receiver) (§28.1.13) -> JS boolean success.
%% TypeError if `target` is not an Object. Implements the receiver-aware portion of
%% OrdinarySetWithOwnDescriptor (9.1.9.2) that is observable in this v1 model:
%%
%% * A frozen `target` presents a non-writable data descriptor, so the write is
%% refused and `false` is returned (step 5.a).
%% * `Type(Receiver)` must be Object; a primitive receiver (string/number/…) is
%% refused with `false` and nothing is written (step 5.b).
%% * Otherwise the value is written on the RECEIVER, not the target (steps 5.c–f):
%% with no explicit receiver the lowering passes `Receiver = target`, so an
%% ordinary `Reflect.set(o, k, v)` still mutates `o`; with a distinct receiver
%% object the value lands there and the target is left untouched.
%% * A frozen receiver (existing property non-writable) or a non-extensible
%% receiver that lacks the key (CreateDataProperty fails) reports `false`.
%%
%% Individual per-property writable/accessor descriptors and the prototype-chain
%% walk of step 4 are not modelled (no descriptor surface in v1).
reflect_set(Target, Key, V, Receiver) when is_reference(Target) ->
case erlang:get({js_frozen, Target}) =:= true of
true -> false;
true ->
false;
_ ->
set_prop(Target, Key, V),
true
case is_reference(Receiver) of
false ->
false;
true ->
case erlang:get({js_frozen, Receiver}) =:= true of
true ->
false;
_ ->
HasOwn = has_prop(Receiver, Key) =:= 1,
NonExt =
erlang:get({js_nonextensible, Receiver}) =:= true,
case (not HasOwn) andalso NonExt of
true ->
false;
false ->
set_prop(Receiver, Key, V),
true
end
end
end
end;
reflect_set(Target, _Key, _V) ->
reflect_set(Target, _Key, _V, _Receiver) ->
type_error(Target).

%% Reflect.deleteProperty(target, propertyKey) (§28.1.4) -> JS boolean success.
Expand Down
52 changes: 52 additions & 0 deletions test/js_compiler_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -6851,3 +6851,55 @@ pub fn array_map_thisarg_arrow_unaffected_test() {
val("[1, 2, 3].map((x) => x * 2, { k: 9 }).join(',')")
|> should.equal(dyn("2,4,6"))
}

// ==== Reflect (wave 13) ====

// §28.1.13 Reflect.set(target, key, V): with the receiver omitted the receiver
// defaults to the target (step 4.a), so the value is written on the target and a
// successful write reports the boolean `true`.
pub fn reflect_set_default_receiver_test() {
let m =
compile(
"function f() { var o = { p: 43 };"
<> " var r = Reflect.set(o, 'p', 42);"
<> " return (r === true ? 1 : 0) * 1000 + o.p; }",
)
to_float(call(m, "f", [])) |> should.equal(1042.0)
}

// §28.1.13 / 9.1.9.2 OrdinarySetWithOwnDescriptor step 5.c–f: when a distinct
// receiver object is supplied the data write lands on the RECEIVER, the target is
// left unchanged, and the operation still reports `true`.
pub fn reflect_set_distinct_receiver_test() {
let m =
compile(
"function f() { var o = { p: 43 }; var rec = { p: 44 };"
<> " var r = Reflect.set(o, 'p', 42, rec);"
<> " return (r === true ? 1 : 0) * 100000 + o.p * 100 + rec.p; }",
)
to_float(call(m, "f", [])) |> should.equal(104_342.0)
}

// §28.1.13 / 9.1.9.2 step 5.b: a data-property write with a non-Object receiver
// (here a primitive string) returns `false` and mutates nothing on the target.
pub fn reflect_set_primitive_receiver_false_test() {
let m =
compile(
"function f() { var o = { p: 42 };"
<> " var r = Reflect.set(o, 'p', 43, 'not an object');"
<> " return (r === false ? 1 : 0) * 1000 + o.p; }",
)
to_float(call(m, "f", [])) |> should.equal(1042.0)
}

// §28.1.13 / 9.1.9.2 step 5.a: a frozen target presents a non-writable data
// descriptor, so the write is refused with `false` and the value is unchanged.
pub fn reflect_set_frozen_target_false_test() {
let m =
compile(
"function f() { var o = { p: 1 }; Object.freeze(o);"
<> " var r = Reflect.set(o, 'p', 2);"
<> " return (r === false ? 1 : 0) * 1000 + o.p; }",
)
to_float(call(m, "f", [])) |> should.equal(1001.0)
}
Loading