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
1 change: 1 addition & 0 deletions src/twocore/backend/emit_core.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4382,6 +4382,7 @@ fn resolve_js(op: String) -> Option(String) {
"object_from_entries" -> Some("object_from_entries")
"object_is" -> Some("object_is")
"object_has_own" -> Some("object_has_own")
"object_group_by" -> Some("object_group_by")
// Reflect namespace
"reflect_has" -> Some("reflect_has")
"reflect_get" -> Some("reflect_get")
Expand Down
16 changes: 16 additions & 0 deletions src/twocore/frontend/js/lower.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4876,6 +4876,22 @@ fn lower_static_call(
"Object", "is", [x, y, ..] -> host("object_is", [x, y])
"Object", "is", [x] -> host("object_is", [x, undefined()])
"Object", "is", [] -> host("object_is", [undefined(), undefined()])
// Object.hasOwn(obj, key) (sec-object.hasown) — own-property test that, unlike
// `obj.hasOwnProperty(key)`, works with a null-prototype receiver. Reuses the
// same runtime op as `hasOwnProperty`.
"Object", "hasOwn", [o, k, ..] -> host("object_has_own", [o, k])
"Object", "hasOwn", [o] -> host("object_has_own", [o, undefined()])
// Object.getPrototypeOf(obj) (sec-object.getprototypeof) — this v1 model tracks
// no prototype chain, so an ordinary object reports `null` (shared with
// Reflect.getPrototypeOf); a primitive receiver throws a TypeError.
"Object", "getPrototypeOf", [o, ..] -> host("reflect_get_prototype_of", [o])
// Object.groupBy(items, callbackfn) (sec-object.groupby) — group the iterable's
// elements into a new null-prototype object keyed by ToPropertyKey(callback
// result). A missing callback lowers to `undefined` so the runtime's
// IsCallable check throws a TypeError (GroupBy step 2).
"Object", "groupBy", [items, cb, ..] -> host("object_group_by", [items, cb])
"Object", "groupBy", [items] ->
host("object_group_by", [items, undefined()])
"Object", "fromEntries", [e, ..] -> host("object_from_entries", [e])
"Object", "freeze", [o, ..] -> host("object_freeze", [o])
"Object", "isFrozen", [o, ..] -> host("object_is_frozen", [o])
Expand Down
7 changes: 7 additions & 0 deletions src/twocore/runtime/rt_js.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,13 @@ pub fn object_is(x: Dynamic, y: Dynamic) -> Dynamic
@external(erlang, "twocore_rt_js_ffi", "object_has_own")
pub fn object_has_own(obj: Dynamic, key: Dynamic) -> Dynamic

/// `Object.groupBy(items, callbackfn)` → a fresh null-prototype object whose keys
/// are the ToPropertyKey-coerced callback results and whose values are Arrays of
/// the elements that produced each key (encounter order). A non-callable
/// `callbackfn` throws a TypeError before any element is read.
@external(erlang, "twocore_rt_js_ffi", "object_group_by")
pub fn object_group_by(items: Dynamic, callbackfn: Dynamic) -> Dynamic

/// `Reflect.has(target, key)` → JS boolean; whether `target` (which must be an
/// object, else TypeError) has the property `key` (own-property only in v1).
@external(erlang, "twocore_rt_js_ffi", "reflect_has")
Expand Down
31 changes: 31 additions & 0 deletions src/twocore_rt_js_ffi.erl
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +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,
object_group_by/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,
Expand Down Expand Up @@ -3450,6 +3451,36 @@ group_into_map(M, Fn, [E | Es], I) ->
end,
group_into_map(M, Fn, Es, I + 1).

%% Object.groupBy(items, callbackfn) (sec-object.groupby) — group the iterable's
%% elements into a fresh null-prototype plain object. GroupBy is driven with
%% coercion = PROPERTY, so each callback result is run through ToPropertyKey: for
%% every non-Symbol value that is ToPrimitive(hint string) then ToString, so a
%% number key `4` becomes "4" and an object with a `toString` contributes its
%% string form (mirroring `to_string_dispatch` then `to_string`). Elements sharing
%% a key are collected, in encounter order, into that key's Array. A non-callable
%% `callbackfn` throws a TypeError (GroupBy step 2) before any element is read.
%% NOTE: this v1 object model iterates own keys in the backing map's order, not the
%% spec's first-encounter insertion order, so `Object.keys` on the result follows
%% that documented deviation.
object_group_by(Items, Fn) ->
case is_function(Fn) of
true -> ok;
false -> not_callable(Fn)
end,
O = new_object(),
group_into_object(O, Fn, array_from_elems(Items, undefined), 0),
O.

group_into_object(_O, _Fn, [], _I) ->
ok;
group_into_object(O, Fn, [E | Es], I) ->
K = to_string(to_string_dispatch(call_cb(Fn, [E, I]))),
case has_prop(O, K) =:= 1 of
true -> array_push(get_prop(O, K), [E]);
false -> set_prop(O, K, new_array([E]))
end,
group_into_object(O, Fn, Es, I + 1).

%% A WeakMap is a cell `{js_weakmap, Next, Data}`, mirroring the ordinary Map cell so
%% the shared collection-method dispatch (`js_m_set`/`get`/`has`/`delete`) can reuse
%% `mapkey_norm` and the same `Key => {Seq, Value}` storage. No real weak references
Expand Down
61 changes: 61 additions & 0 deletions test/js_compiler_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -7328,3 +7328,64 @@ pub fn wave14_arrow_callback_ignores_thisarg_test() {
val("[1, 2, 3].map((x) => x * 2, { k: 9 }).join(',')")
|> should.equal(dyn("2,4,6"))
}

// ==== Object (wave 15) ====

// Object.groupBy(items, cb) buckets elements into a new object whose values are
// Arrays of the elements that produced each key, in encounter order
// (sec-object.groupby / GroupBy). Keys 'odd' → [1, 3], 'even' → [2].
pub fn wave15_object_groupby_buckets_test() {
let m =
compile(
"function f() { var o = Object.groupBy([1, 2, 3], function(i){ return i % 2 === 0 ? 'even' : 'odd'; }); return o['odd'].length * 100 + o['odd'][0] * 10 + o['odd'][1] + o['even'][0]; }",
)
// odd.length=2 → 200, odd[0]=1 → 10, odd[1]=3 → 3, even[0]=2 → 2 = 215
to_float(call(m, "f", [])) |> should.equal(215.0)
}

// A numeric callback result is run through ToPropertyKey, so key `1` becomes the
// string property "1" (GroupBy with coercion = property). Lengths 1 → ['a','c'].
pub fn wave15_object_groupby_numeric_key_coerced_test() {
let m =
compile(
"function f() { var o = Object.groupBy(['a', 'bb', 'c'], function(s){ return s.length; }); return o['1'].length * 10 + o['2'].length; }",
)
to_float(call(m, "f", [])) |> should.equal(21.0)
}

// ToPropertyKey uses ToString, so 1, '1' and an object whose toString() returns 1
// all collapse to the single key "1" (sec-object.groupby toPropertyKey case).
pub fn wave15_object_groupby_topropertykey_test() {
let m =
compile(
"function f() { var s = { toString: function(){ return 1; } }; var o = Object.groupBy([1, '1', s], function(v){ return v; }); return o['1'].length; }",
)
to_float(call(m, "f", [])) |> should.equal(3.0)
}

// A non-callable callback throws a TypeError before any element is read
// (GroupBy step 2 / IsCallable).
pub fn wave15_object_groupby_noncallable_throws_test() {
let m =
compile(
"function f() { try { Object.groupBy([], null); return 0; } catch (e) { return e.name === 'TypeError' ? 1 : 0; } }",
)
to_float(call(m, "f", [])) |> should.equal(1.0)
}

// The result is a null-prototype object: it has no inherited hasOwnProperty and
// Object.getPrototypeOf reports null (sec-object.groupby step 2).
pub fn wave15_object_groupby_null_prototype_test() {
let m =
compile(
"function f() { var o = Object.groupBy([1], function(x){ return x; }); return (Object.getPrototypeOf(o) === null && o.hasOwnProperty === undefined) ? 1 : 0; }",
)
to_float(call(m, "f", [])) |> should.equal(1.0)
}

// Object.hasOwn(obj, key) (sec-object.hasown) reports own properties, true for a
// present key and false for an absent one.
pub fn wave15_object_hasown_test() {
val("Object.hasOwn({ a: 1 }, 'a')") |> should.equal(dyn(True))
val("Object.hasOwn({ a: 1 }, 'b')") |> should.equal(dyn(False))
}
Loading