diff --git a/crates/rts-codegen-new/src/front/run/method_array.rs b/crates/rts-codegen-new/src/front/run/method_array.rs index 85d0e8698..6010f9e0c 100644 --- a/crates/rts-codegen-new/src/front/run/method_array.rs +++ b/crates/rts-codegen-new/src/front/run/method_array.rs @@ -230,6 +230,22 @@ impl<'a, 'b, 'c> Lowerer<'a, 'b, 'c> { return Ok(self.poly_bool_to_bool(res)); } + // The ITERATOR protocol on an array-shaped receiver: `values()`/`keys()`/ + // `entries()`/`Iterator.from` return a materialized array that is ALSO an + // open iterator, and `Array` has no Registry row for `next`/`return`/`throw` + // — so a static bail here would reject `[1,2].values().next()` at COMPILE + // time. Route it to the dynamic dispatch, which walks the cursor only when + // the handle was registered as an iterator; on a plain array the property + // read stays `undefined`, exactly as JS (issue #2042). + if matches!(method, "next" | "return" | "throw") + && resolve_method(RecvClass::Array, method, argc).is_none() + { + let recv = self.lower_expr(module, object)?; + if let Some(v) = self.try_generic_dyn_method_call(module, recv, method, args)? { + return Ok(v); + } + } + let Some(spec) = resolve_method(RecvClass::Array, method, argc) else { return Err(crate::front::error::Unsupported::new(format!( "no Registry entry for `Array.{method}({argc} args)` \ diff --git a/crates/rts-runtime/src/adapters/value/arrayops.rs b/crates/rts-runtime/src/adapters/value/arrayops.rs index 4e5f5c852..c15ed07c1 100644 --- a/crates/rts-runtime/src/adapters/value/arrayops.rs +++ b/crates/rts-runtime/src/adapters/value/arrayops.rs @@ -31,6 +31,20 @@ fn vec_len(vec_handle: u64) -> i64 { rt_vec::__RTS_FN_NS_COLLECTIONS_VEC_LEN(vec_handle).max(0) } +/// Mark a freshly-built Vec as an OPEN ITERATOR — what `values()`/`keys()`/ +/// `entries()` hand back is an iterator object in JS, not an array, so `.next()` +/// must walk it while a plain array's `.next` stays `undefined`. +/// +/// The array stays materialized (`for-of`/spread keep consuming it exactly as +/// before); registering the cursor at CREATION is what makes the two +/// distinguishable at runtime. It has to happen here rather than being inferred +/// later: `generator_next` inserts a cursor with `or_insert(0)` for any handle it +/// is given, so "has a cursor" only means "was created as an iterator" if nothing +/// else can put it there first (issue #2042). +fn open_as_iterator(vec_handle: u64) { + rts_std::collector::generator::open_vec_iterator(vec_handle); +} + /// Read slot `i` of the real Vec as a raw PolyValue word. Caller guarantees /// `0 <= i < len` (we never call it out of range). fn vec_word(vec_handle: u64, i: i64) -> u64 { @@ -477,6 +491,7 @@ pub fn rtsadp_arr_entries(vec_handle: u64) -> u64 { PolyValue::from_object_handle(rt_handles::__rtsn_poly_from_handle(pair)).raw(); rt_vec::__RTS_FN_NS_COLLECTIONS_VEC_PUSH(out, pair_word as i64); } + open_as_iterator(out); PolyValue::from_object_handle(rt_handles::__rtsn_poly_from_handle(out)).raw() } @@ -488,6 +503,7 @@ pub fn rtsadp_arr_keys(vec_handle: u64) -> u64 { for i in 0..len { rt_vec::__RTS_FN_NS_COLLECTIONS_VEC_PUSH(out, PolyValue::from_i32(i as i32).raw() as i64); } + open_as_iterator(out); PolyValue::from_object_handle(rt_handles::__rtsn_poly_from_handle(out)).raw() } @@ -496,6 +512,7 @@ pub fn rtsadp_arr_keys(vec_handle: u64) -> u64 { #[rtse::abi] pub fn rtsadp_arr_values(vec_handle: u64) -> u64 { let copy = copy_vec(vec_handle); + open_as_iterator(copy); PolyValue::from_object_handle(rt_handles::__rtsn_poly_from_handle(copy)).raw() } diff --git a/crates/rts-runtime/src/adapters/value/dyndispatch.rs b/crates/rts-runtime/src/adapters/value/dyndispatch.rs index 2a4b54b34..84c5b1507 100644 --- a/crates/rts-runtime/src/adapters/value/dyndispatch.rs +++ b/crates/rts-runtime/src/adapters/value/dyndispatch.rs @@ -1228,10 +1228,46 @@ fn try_generator_dyn(recv: u64, metodo: &str, a0: u64) -> Option { if pv.is_object() { pv.as_handle() } else { 0 }, recv, ]; - let h = candidatos - .into_iter() - .find(|&c| c != 0 && with_entry(c, |e| matches!(e, Some(Entry::GenState(_)))))?; + // A receiver is an iterator when its handle is either a `GenState` (a lazy + // generator) or a Vec that was OPENED as an iterator — what `arr.values()`/ + // `keys()`/`entries()`/`Iterator.from` return. The Vec case is gated on + // `vec_is_open_iterator`, never on "is a Vec": a plain array must keep reading + // `.next` as `undefined` (`[1,2].next`), so only a handle REGISTERED as an + // iterator at creation qualifies. + let mut is_vec_iter = false; + let h = candidatos.into_iter().find(|&c| { + if c == 0 { + return false; + } + if with_entry(c, |e| matches!(e, Some(Entry::GenState(_)))) { + return true; + } + if with_entry(c, |e| matches!(e, Some(Entry::Vec(_)))) + && rts_std::collector::generator::vec_is_open_iterator(c) + { + is_vec_iter = true; + return true; + } + false + })?; let undef = PolyValue::undefined().raw(); + // A Vec iterator walks its cursor through `generator_next` (the polymorphic + // entry point that handles both GenState and cursored Vec); `gen_sm_next` is + // GenState-only and would read nothing from a Vec. + if is_vec_iter { + let bruto = match metodo { + "next" => rts_std::collector::generator::__rtsn_generator_next(h), + "return" => rts_std::collector::generator::__rtsn_generator_return(h, a0 as i64), + "throw" => rts_std::collector::generator::__rtsn_generator_throw(h, a0 as i64), + _ => return None, + }; + return Some( + PolyValue::from_object_handle( + rts_runtime::namespaces::gc::handles::__rtsn_poly_from_handle(bruto), + ) + .raw(), + ); + } // The `__rtsn_gen*` entry points hand back the RAW handle of the `{value, done}` // result — the front's STATIC path converts it via `build_iter_result`. Here the // word returns DIRECTLY to the caller, so it must be boxed as an OBJECT: a raw diff --git a/crates/rts-shared/src/stdlib/map_set.ts b/crates/rts-shared/src/stdlib/map_set.ts index b557eeb1b..35ecbabe0 100644 --- a/crates/rts-shared/src/stdlib/map_set.ts +++ b/crates/rts-shared/src/stdlib/map_set.ts @@ -93,22 +93,24 @@ class Map { return true; } get size(): number { return this.#keys.length; } - // Iteration helpers — return eager arrays so `for (const k of m.keys())` works - // (the engine iterates a proven array). `entries()` yields `[key, value]` pairs. + // Iteration helpers. The array stays materialized (`for (const k of m.keys())` + // iterates a proven array), but the result is handed back through the Array + // `values()` so it is ALSO an open iterator — `m.keys().next()` walks a cursor + // like JS, instead of reading `undefined` off a plain array (issue #2042). keys(): K[] { const out: K[] = []; for (let i = 0; i < this.#keys.length; i++) { out.push(this.#keys[i]); } - return out; + return out.values(); } values(): V[] { const out: V[] = []; for (let i = 0; i < this.#vals.length; i++) { out.push(this.#vals[i]); } - return out; + return out.values(); } entries(): [K, V][] { const out: [K, V][] = []; for (let i = 0; i < this.#keys.length; i++) { out.push([this.#keys[i], this.#vals[i]]); } - return out; + return out.values(); } forEach(cb: (v: V, k: K, m: Map) => void): void { for (let i = 0; i < this.#keys.length; i++) { cb(this.#vals[i], this.#keys[i], this); } @@ -198,17 +200,18 @@ class Set { } get size(): number { return this.#items.length; } // `values()`/`keys()` both yield the elements (JS Set), `entries()` yields - // `[v, v]` pairs; eager arrays so `for (const v of s.values())` iterates. + // `[v, v]` pairs. Materialized as before, then handed back through the Array + // `values()` so the result is also an OPEN ITERATOR (`s.values().next()`). values(): T[] { const out: T[] = []; for (let i = 0; i < this.#items.length; i++) { out.push(this.#items[i]); } - return out; + return out.values(); } keys(): T[] { return this.values(); } entries(): [T, T][] { const out: [T, T][] = []; for (let i = 0; i < this.#items.length; i++) { out.push([this.#items[i], this.#items[i]]); } - return out; + return out.values(); } forEach(cb: (v: T, v2: T, s: Set) => void): void { for (let i = 0; i < this.#items.length; i++) { cb(this.#items[i], this.#items[i], this); } diff --git a/crates/rts-std/src/collector/generator.rs b/crates/rts-std/src/collector/generator.rs index 27cc2a09d..98cc394d8 100644 --- a/crates/rts-std/src/collector/generator.rs +++ b/crates/rts-std/src/collector/generator.rs @@ -54,6 +54,31 @@ thread_local! { static GEN_RETS: RefCell> = RefCell::new(HashMap::new()); } +/// Whether `handle` is a Vec that was OPENED as an iterator — i.e. registered in +/// `GEN_CURSORS` at CREATION (`Iterator.from`, `arr.values()`), not merely one +/// that `generator_next` cursored on the way past. +/// +/// The distinction matters and is easy to get wrong: `generator_next` uses +/// `or_insert(0)`, so it CREATES a cursor for whatever handle it is handed. Asking +/// "is there a cursor?" after the fact therefore answers yes for a plain array +/// too. This predicate is `contains_key` ONLY — it never inserts — so it stays a +/// property of how the handle was created. That is what lets the dynamic dispatch +/// accept `Entry::Vec` as an iterator without `[1,2].next()` starting to work +/// (issue #2042: a plain array must keep reading `.next` as `undefined`). +pub fn vec_is_open_iterator(handle: u64) -> bool { + GEN_CURSORS.with(|c| c.borrow().contains_key(&handle)) +} + +/// Register `handle` as an OPEN ITERATOR positioned at element 0 — what +/// `arr.values()`/`keys()`/`entries()` and `Iterator.from` hand back. Pairs with +/// [`vec_is_open_iterator`]: this is the ONLY way a Vec becomes an iterator, so a +/// plain array never answers `.next()`. +pub fn open_vec_iterator(handle: u64) { + GEN_CURSORS.with(|c| { + c.borrow_mut().insert(handle, 0); + }); +} + /// `__RTS_GEN_FINISH(buf, ret)` — registra o ret_value do generator (do /// `return X`) e devolve o proprio Vec. Chamado no `return` desugarado. #[rtse::abi(native, value = "generator_set_ret")] diff --git a/tests/claude-iterador-nativo-next.test.ts b/tests/claude-iterador-nativo-next.test.ts new file mode 100644 index 000000000..9c78ede28 --- /dev/null +++ b/tests/claude-iterador-nativo-next.test.ts @@ -0,0 +1,153 @@ +import { describe, test, expect } from "rts:test"; + +// `values()`/`keys()`/`entries()` de Array, Map e Set passam a suportar o +// protocolo `.next()`. Antes falhavam em COMPILAÇÃO — `no Registry entry for +// Array.next(0 args)` —, porque o resultado é um array materializado e `Array` +// não tem linha de Registry para `next`. +// +// Duas peças: +// 1. o handle devolvido é REGISTRADO como iterador aberto na criação +// (`open_vec_iterator`), o que o torna distinguível de um array comum em +// runtime sem side-table nova — `GEN_CURSORS` já existia para o generator +// eager, e `generator_next` já cursoriza `Entry::Vec`; +// 2. `.next()`/`.return()`/`.throw()` sobre receiver-array roteia ao despacho +// dinâmico, que aceita `Entry::Vec` SOMENTE se marcado. +// +// A distinção tem uma armadilha que já derrubou uma tentativa anterior: +// `generator_next` usa `or_insert(0)`, ou seja, CRIA o cursor para qualquer +// handle que receba. Perguntar "tem cursor?" depois responde sim para um array +// comum também. Por isso o predicado é `contains_key` puro, nunca inserindo — +// assim continua sendo uma propriedade de COMO o handle foi criado, e +// `[1,2].next` segue `undefined`. +// +// Valores conferidos contra o Node. Pré-computado no top-level. + +const arrIt = [1, 2].values(); +const primeiro = arrIt.next().value; +const segundo = arrIt.next().value; +const esgotado = JSON.stringify(arrIt.next()); + +const keysPrimeiro = [7, 8].keys().next().value; +const entriesPrimeiro = JSON.stringify([9].entries().next().value); + +const s = new Set([5, 6]); +const setPrimeiro = s.values().next().value; +const setKeys = s.keys().next().value; + +const m = new Map([["k", 9]]); +const mapEntries = JSON.stringify(m.entries().next().value); +const mapKeys = m.keys().next().value; +const mapValues = m.values().next().value; + +// o cursor é do ITERADOR, não do array de origem: dois iteradores sobre o mesmo +// array andam independentes +const base = [10, 20]; +const itA = base.values(); +const itB = base.values(); +itA.next(); +const independentes = itB.next().value; + +// `take` sobre iterador — a forma que os bundles usam +function take(iter, n) { + const out: any[] = []; + for (let i = 0; i < n; i++) { + const r = iter.next(); + if (r.done) break; + out.push(r.value); + } + return out; +} +const viaTake = take([1, 2, 3, 4].values(), 3).join(","); + +// ── não-regressões ───────────────────────────────────────────────────────── +const arrayComumNext = ([1, 2] as any).next; +const forOfValues = [...[1, 2].values()].join(","); +const forOfKeys = [...[7, 8].keys()].join(","); +const joinDeValues = [1, 2, 3].values().join("-"); +const lengthDeValues = [1, 2, 3].values().length; + +let somaForOf = 0; +for (const v of [1, 2, 3].values()) { somaForOf = somaForOf + v; } + +let paresOk = ""; +for (const [i, v] of [9, 8].entries()) { paresOk = paresOk + i + ":" + v + " "; } + +describe("protocolo .next() em iteradores nativos", () => { + test("array values() rende o primeiro valor", () => { + expect(primeiro).toBe(1); + }); + + test("array values() avança o cursor", () => { + expect(segundo).toBe(2); + }); + + test("array values() esgota com done", () => { + expect(esgotado).toBe('{"done":true}'); + }); + + test("array keys()", () => { + expect(keysPrimeiro).toBe(0); + }); + + test("array entries()", () => { + expect(entriesPrimeiro).toBe("[0,9]"); + }); + + test("Set values()", () => { + expect(setPrimeiro).toBe(5); + }); + + test("Set keys()", () => { + expect(setKeys).toBe(5); + }); + + test("Map entries()", () => { + expect(mapEntries).toBe('["k",9]'); + }); + + test("Map keys()", () => { + expect(mapKeys).toBe("k"); + }); + + test("Map values()", () => { + expect(mapValues).toBe(9); + }); + + test("dois iteradores do mesmo array são independentes", () => { + expect(independentes).toBe(10); + }); + + test("take() sobre iterador — a forma dos bundles", () => { + expect(viaTake).toBe("1,2,3"); + }); +}); + +describe("não-regressões: array comum e consumo por iterável", () => { + test("array comum NÃO responde a .next", () => { + expect(arrayComumNext).toBe(undefined); + }); + + test("spread de values() não regrediu", () => { + expect(forOfValues).toBe("1,2"); + }); + + test("spread de keys() não regrediu", () => { + expect(forOfKeys).toBe("0,1"); + }); + + test("for-of de values() não regrediu", () => { + expect(somaForOf).toBe(6); + }); + + test("destructuring de entries() em for-of não regrediu", () => { + expect(paresOk).toBe("0:9 1:8 "); + }); + + test("values() continua respondendo a métodos de array (.join)", () => { + expect(joinDeValues).toBe("1-2-3"); + }); + + test("values() continua respondendo a .length", () => { + expect(lengthDeValues).toBe(3); + }); +}); diff --git a/tests/cross-runtime/generators/claude-native-iterator-next.ts b/tests/cross-runtime/generators/claude-native-iterator-next.ts new file mode 100644 index 000000000..07145b654 --- /dev/null +++ b/tests/cross-runtime/generators/claude-native-iterator-next.ts @@ -0,0 +1,65 @@ +// Cross-runtime: `values()`/`keys()`/`entries()` on Array, Map and Set support +// the `.next()` protocol, not just `for-of`/spread. +// +// These used to fail at COMPILE time — "no Registry entry for `Array.next(0 +// args)`" — because the result is a materialized array and `Array` has no +// Registry row for `next`. The array stays materialized (so `for-of`, spread, +// `.join()` and `.length` keep working exactly as before); what changed is that +// the handle is REGISTERED as an open iterator when it is created, which makes it +// distinguishable from a plain array at runtime. A plain array must therefore +// keep reading `.next` as `undefined`. + +const it = [1, 2].values(); +console.log("first=" + it.next().value); +console.log("second=" + it.next().value); +console.log("exhausted=" + JSON.stringify(it.next())); + +console.log("keys=" + [7, 8].keys().next().value); +console.log("entries=" + JSON.stringify([9].entries().next().value)); + +const s = new Set([5, 6]); +console.log("setValues=" + s.values().next().value); +console.log("setKeys=" + s.keys().next().value); + +const m = new Map([["k", 9]]); +console.log("mapEntries=" + JSON.stringify(m.entries().next().value)); +console.log("mapKeys=" + m.keys().next().value); +console.log("mapValues=" + m.values().next().value); + +// Two iterators over the SAME array advance independently — the cursor belongs +// to the iterator, not to the backing array. +const base = [10, 20]; +const itA = base.values(); +const itB = base.values(); +itA.next(); +console.log("independent=" + itB.next().value); + +// `take(iterator, n)` — the shape bundled code uses. +function take(iter, n) { + const out: any[] = []; + for (let i = 0; i < n; i++) { + const r = iter.next(); + if (r.done) break; + out.push(r.value); + } + return out; +} +console.log("take=" + take([1, 2, 3, 4].values(), 3).join(",")); +console.log("takeBeyondEnd=" + take([1, 2].values(), 5).join(",")); + +// ── non-regressions ───────────────────────────────────────────────────────── +console.log("plainArrayNext=" + ([1, 2] as any).next); +console.log("spreadValues=" + [...[1, 2].values()].join(",")); +console.log("spreadKeys=" + [...[7, 8].keys()].join(",")); + +let sum = 0; +for (const v of [1, 2, 3].values()) { + sum = sum + v; +} +console.log("forOfValues=" + sum); + +let pairs = ""; +for (const [i, v] of [9, 8].entries()) { + pairs = pairs + i + ":" + v + " "; +} +console.log("forOfEntries=" + pairs);