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 @@ -4220,6 +4220,7 @@ fn resolve_js(op: String) -> Option(String) {
"set_prop" -> Some("set_prop")
"gen_make" -> Some("gen_make")
"gen_next" -> Some("gen_next")
"gen_return" -> Some("gen_return")
"iter_array" -> Some("iter_array")
"iter_to_array" -> Some("iter_to_array")
"iter_take" -> Some("iter_take")
Expand Down
21 changes: 20 additions & 1 deletion src/twocore/frontend/js/lower.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@
//// future direction could hand a dynamic string off to an embedded arc VM, but that
//// is explicitly out of scope — the focus is making the AOT path excellent.)
////
//// Not yet (a clean `Unsupported` error / panic): generators/async, and a
//// Generators (`function* g(){ … }`) lower to a resumable state machine driven by
//// `.next()` — both the DECLARATION form and the function-EXPRESSION form (e.g. an
//// immediately-invoked `(function*(){ … })()`), which the pre-pass rewrites the same
//// way; async is still unsupported.
//// Not yet (a clean `Unsupported` error / panic): async, and a
//// `return`/`break`/`continue` inside a `try`/`finally` body (it would bypass the
//// finally, so it is rejected). Static
//// field initializers run when the module's `main/0` runs (like any top-level
Expand Down Expand Up @@ -5792,6 +5796,9 @@ fn lower_instance_method_generic(
// generator `.next(v)` — advances a generator, or delegates to a user
// `next` method on an ordinary iterator object.
"next", _ -> coll("gen_next")
// generator `.return(v)` — closes a generator (§27.5.1.4), yielding
// `{value: v, done: true}`; delegates to a user `return` method otherwise.
"return", _ -> coll("gen_return")
// Object.prototype.hasOwnProperty(key) — own-property test (no prototype
// chain in this model), returning a JS boolean.
"hasOwnProperty", [k, ..] -> host("object_has_own", [k])
Expand Down Expand Up @@ -7213,6 +7220,18 @@ fn lambda_nodes_expr(e: ast.Expression) -> List(RawLambda) {
let stmts = arrow_body_stmts(body)
[RawLambda(span, params, stmts), ..lambda_nodes_stmts(stmts)]
}
ast.FunctionExpression(span:, params:, body:, is_generator: True, ..) -> {
// A generator function EXPRESSION (e.g. `(function*(){ … })()`): rewrite its
// body to the resumable state machine, exactly as a generator DECLARATION is
// rewritten in `as_function_decl`. On an unsupported generator shape, fall
// back to the original body so the surviving `yield` reports a clean
// Unsupported error during lowering (rather than silently mis-collecting).
let stmts = case transform_generator(params, body) {
Ok(new_body) -> block_stmts(new_body)
Error(_) -> block_stmts(body)
}
[RawLambda(span, params, stmts), ..lambda_nodes_stmts(stmts)]
}
ast.FunctionExpression(span:, params:, body:, ..) -> {
let stmts = block_stmts(body)
[RawLambda(span, params, stmts), ..lambda_nodes_stmts(stmts)]
Expand Down
5 changes: 5 additions & 0 deletions src/twocore/runtime/rt_js.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,11 @@ pub fn gen_make(step: Dynamic) -> Dynamic
@external(erlang, "twocore_rt_js_ffi", "gen_next")
pub fn gen_next(gen: Dynamic, args: Dynamic) -> Dynamic

/// `gen.return(v)` — close a generator and yield `{value: v, done: true}` (or
/// delegate to a user `return` method on a plain iterator object).
@external(erlang, "twocore_rt_js_ffi", "gen_return")
pub fn gen_return(gen: Dynamic, args: Dynamic) -> Dynamic

/// Materialize the source of a `for-of` as an array: arrays and strings pass
/// through, a generator is drained to an array, anything else becomes empty.
@external(erlang, "twocore_rt_js_ffi", "iter_array")
Expand Down
32 changes: 31 additions & 1 deletion src/twocore_rt_js_ffi.erl
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
bit_and/2, bit_or/2, bit_xor/2, bit_not/1, shl/2, shr/2, ushr/2, pow/2,
math_unary/2, math_binary/3, math_reduce/2, math_random/0,
cell_new/1, cell_get/1, cell_set/2,
new_object/0, globalthis_new/0, wrapper_new/2, error_make/2, error_ctor/1, error_is_error/1, js_error_to_value/1, gen_make/1, gen_next/2, iter_array/1, iter_to_array/1, iter_take/2, iter_drop/2, get_prop/2, set_prop/3, define_data/3, define_accessor/4,
new_object/0, globalthis_new/0, wrapper_new/2, error_make/2, error_ctor/1, error_is_error/1, js_error_to_value/1, gen_make/1, gen_next/2, gen_return/2, iter_array/1, iter_to_array/1, iter_take/2, iter_drop/2, get_prop/2, set_prop/3, define_data/3, define_accessor/4,
builtin_ctor/1, builtin_prototype/1, to_object/1,
static_get/2, static_get_chain/2, static_set/3, has_prop/2, delete_prop/2,
new_array/1, array_construct/1, array_push/2, array_pop/1, is_array/1, array_spread_into/2,
Expand Down Expand Up @@ -1304,6 +1304,36 @@ gen_next(Recv, Args) when is_reference(Recv) ->
gen_next(Recv, Args) ->
delegate(Recv, <<"next">>, Args).

%% `gen.return(v)` — Generator.prototype.return (§27.5.1.4). Closes the generator
%% (as if a `return v` ran at the current suspend point) and returns the result
%% object `{value: v, done: true}`; `v` defaults to `undefined`. A subsequent
%% `.next()`/iterator-helper on the closed generator then sees it exhausted. The
%% generators modelled here have no observable `try`/`finally`, so closing is a
%% simple state transition to DONE. A non-generator receiver delegates to a user
%% `return` method (a plain-object iterator implementing the optional return step);
%% a receiver without one is a non-reference/absent-method — left to `delegate`.
gen_return(Recv, Args) when is_reference(Recv) ->
case erlang:get(?CELL_KEY(Recv)) of
Tag when
Tag =:= js_gen_done orelse
(is_tuple(Tag) andalso
(element(1, Tag) =:= js_gen orelse
element(1, Tag) =:= js_gen_running))
->
%% Replace the step with a permanently-DONE step and keep the `{js_gen,_}`
%% tag, so both `.next()` and the iterator helpers (which dispatch on that
%% tag) treat the closed generator as an empty iterator.
erlang:put(
?CELL_KEY(Recv),
{js_gen, fun(_Sent) -> iter_result(undefined, true) end}
),
iter_result(arg(Args, 0), true);
_ ->
delegate(Recv, <<"return">>, Args)
end;
gen_return(Recv, Args) ->
delegate(Recv, <<"return">>, Args).

%% IteratorClose for a generator receiver (§7.4.11 as used by the Iterator
%% helpers): permanently mark the generator DONE so any later `.next()` returns
%% `{value: undefined, done: true}`. Called when a helper short-circuits (e.g.
Expand Down
96 changes: 96 additions & 0 deletions test/js_compiler_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -6851,3 +6851,99 @@ 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"))
}

// ==== Iterator (wave 13) ====

pub fn generator_expression_iife_yields_test() {
// A generator FUNCTION EXPRESSION invoked immediately (`(function*(){…})()`)
// must produce a real generator object (not `undefined`, as it did before the
// expression form was transformed). Driving it with `.next()` yields each value
// in order, then `{done:true}` (§27.5). Regression for `(function*(){…})()`.
let m =
compile(
"function f() { let it = (function*(){ yield 10; yield 20; })(); "
<> "let a = it.next(); let b = it.next(); let c = it.next(); "
<> "return a.value + b.value * 10 + (c.done === true ? 1000 : 0); }",
)
to_float(call(m, "f", [])) |> should.equal(1210.0)
}

pub fn generator_expression_empty_exhausted_some_test() {
// Mirrors test/built-ins/Iterator/prototype/some/iterator-already-exhausted.js:
// an already-exhausted iterator returns `false` from `.some` for any predicate,
// because there are no remaining values to test (§27.1.4.18).
let m =
compile(
"function f() { let it = (function*(){})(); it.next(); "
<> "let a = it.some(function(x){ return true; }); "
<> "let b = it.some(function(x){ return false; }); "
<> "return (a === false && b === false) ? 1 : 0; }",
)
to_float(call(m, "f", [])) |> should.equal(1.0)
}

pub fn generator_expression_empty_exhausted_toarray_test() {
// Mirrors Iterator/prototype/toArray/iterator-already-exhausted.js: `.toArray`
// on an exhausted iterator yields an empty array (§27.1.4.19).
let m =
compile(
"function f() { let it = (function*(){})(); it.next(); "
<> "return it.toArray().length; }",
)
to_float(call(m, "f", [])) |> should.equal(0.0)
}

pub fn generator_expression_exhausted_every_find_reduce_test() {
// every → true (vacuously) and find → undefined on an exhausted iterator
// (§27.1.4.6 / .8); reduce with an initial value returns that seed unchanged
// (§27.1.4.16).
let m =
compile(
"function f() { let it = (function*(){})(); it.next(); "
<> "let e = it.every(function(x){ return false; }); "
<> "let g = (function*(){})(); g.next(); "
<> "let fd = g.find(function(x){ return true; }); "
<> "let h = (function*(){})(); h.next(); "
<> "let rd = h.reduce(function(a, x){ return a + x; }, 42); "
<> "return (e === true && fd === undefined && rd === 42) ? 1 : 0; }",
)
to_float(call(m, "f", [])) |> should.equal(1.0)
}

pub fn generator_expression_map_take_toarray_test() {
// The lazy helpers compose over a generator-expression source: map doubles each
// value, take limits to two, toArray collects them (§27.1.4.12 / .17 / .19).
let m =
compile(
"function f() { let it = (function*(){ yield 1; yield 2; yield 3; yield 4; })(); "
<> "let arr = it.map(function(x){ return x * 2; }).take(2).toArray(); "
<> "return arr.length * 100 + arr[0] * 10 + arr[1]; }",
)
to_float(call(m, "f", [])) |> should.equal(224.0)
}

pub fn generator_return_closes_test() {
// Generator.prototype.return (§27.5.1.4): `.return(v)` closes the generator and
// yields `{value: v, done: true}`; a following `.next()` sees it exhausted.
let m =
compile(
"function f() { let it = (function*(){ yield 1; yield 2; })(); "
<> "let r = it.return(99); let n = it.next(); "
<> "return (r.value === 99 && r.done === true && n.done === true) ? 1 : 0; }",
)
to_float(call(m, "f", [])) |> should.equal(1.0)
}

pub fn generator_return_underlying_closed_map_test() {
// Mirrors Iterator/prototype/map/underlying-iterator-closed.js: closing the
// source with `.return()` BEFORE building a `.map` helper means the mapped
// iterator produces nothing — its first `.next()` is `{value:undefined,done:true}`.
let m =
compile(
"function f() { let it = (function*(){ yield 0; yield 1; yield 2; })(); "
<> "it.return(); let mapped = it.map(function(){ return 0; }); "
<> "let r = mapped.next(); "
<> "return (r.value === undefined && r.done === true) ? 1 : 0; }",
)
to_float(call(m, "f", [])) |> should.equal(1.0)
}
Loading