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
16 changes: 16 additions & 0 deletions crates/rts-codegen-new/src/front/run/method_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)` \
Expand Down
17 changes: 17 additions & 0 deletions crates/rts-runtime/src/adapters/value/arrayops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
}

Expand All @@ -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()
}

Expand All @@ -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()
}

Expand Down
42 changes: 39 additions & 3 deletions crates/rts-runtime/src/adapters/value/dyndispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1228,10 +1228,46 @@ fn try_generator_dyn(recv: u64, metodo: &str, a0: u64) -> Option<u64> {
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
Expand Down
19 changes: 11 additions & 8 deletions crates/rts-shared/src/stdlib/map_set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,22 +93,24 @@ class Map<K, V> {
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<K, V>) => void): void {
for (let i = 0; i < this.#keys.length; i++) { cb(this.#vals[i], this.#keys[i], this); }
Expand Down Expand Up @@ -198,17 +200,18 @@ class Set<T> {
}
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<T>) => void): void {
for (let i = 0; i < this.#items.length; i++) { cb(this.#items[i], this.#items[i], this); }
Expand Down
25 changes: 25 additions & 0 deletions crates/rts-std/src/collector/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@ thread_local! {
static GEN_RETS: RefCell<HashMap<u64, i64>> = 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")]
Expand Down
153 changes: 153 additions & 0 deletions tests/claude-iterador-nativo-next.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading
Loading