diff --git a/src/twocore/backend/emit_core.gleam b/src/twocore/backend/emit_core.gleam index 9fd2405..c215004 100644 --- a/src/twocore/backend/emit_core.gleam +++ b/src/twocore/backend/emit_core.gleam @@ -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") diff --git a/src/twocore/frontend/js/lower.gleam b/src/twocore/frontend/js/lower.gleam index 6b71797..69bddf5 100644 --- a/src/twocore/frontend/js/lower.gleam +++ b/src/twocore/frontend/js/lower.gleam @@ -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 @@ -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]) @@ -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)] diff --git a/src/twocore/runtime/rt_js.gleam b/src/twocore/runtime/rt_js.gleam index bffd603..50632ce 100644 --- a/src/twocore/runtime/rt_js.gleam +++ b/src/twocore/runtime/rt_js.gleam @@ -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") diff --git a/src/twocore_rt_js_ffi.erl b/src/twocore_rt_js_ffi.erl index b102e66..dfda0fc 100644 --- a/src/twocore_rt_js_ffi.erl +++ b/src/twocore_rt_js_ffi.erl @@ -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, @@ -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. diff --git a/test/js_compiler_test.gleam b/test/js_compiler_test.gleam index 7011cc1..1e95633 100644 --- a/test/js_compiler_test.gleam +++ b/test/js_compiler_test.gleam @@ -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) +}