From 8ae7187c55d952bf413da97dcd5a6d33cbd7421c Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 9 May 2026 17:01:31 +0000 Subject: [PATCH] js-on-sx: for-in walks proto chain with shadowing, stops at native prototypes --- lib/js/runtime.sx | 36 ++++++++++++++++++++++++++++++++++++ lib/js/transpile.sx | 2 +- plans/js-on-sx.md | 2 ++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 6a64cafa..5717ccbf 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -3844,6 +3844,42 @@ (k) (or (= k "__js_order__") (= k "__proto__")))) +(define + js-for-in-keys + (fn + (o) + (let + ((result (list))) + (begin (js-for-in-walk o result) result)))) + +(define + js-for-in-walk + (fn + (o acc) + (cond + ((not (dict? o)) nil) + ((= o (get Object "prototype")) nil) + ((= o (get Array "prototype")) nil) + ((= o (get Number "prototype")) nil) + ((= o (get String "prototype")) nil) + ((= o (get Boolean "prototype")) nil) + ((= o (get Date "prototype")) nil) + ((= o (get RegExp "prototype")) nil) + ((= o (get Map "prototype")) nil) + ((= o (get Set "prototype")) nil) + ((= o (get js-function-global "prototype")) nil) + (else + (let + ((own (js-object-keys o))) + (begin + (for-each + (fn (k) (if (contains? acc k) nil (append! acc k))) + own) + (cond + ((contains? (keys o) "__proto__") + (js-for-in-walk (get o "__proto__") acc)) + (else nil)))))))) + (define js-object-keys (fn diff --git a/lib/js/transpile.sx b/lib/js/transpile.sx index 1a9b1a73..d9ccfb3d 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -948,7 +948,7 @@ (if (= iter-kind "of") (list (js-sym "js-iterable-to-list") iter-sx) - (list (js-sym "js-object-keys") iter-sx)))) + (list (js-sym "js-for-in-keys") iter-sx)))) (list (js-sym "for-each") (list diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index 06e63d0b..d8739734 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -158,6 +158,8 @@ Each item: implement → tests → update progress. Mark `[x]` when tests green. Append-only record of completed iterations. Loop writes one line per iteration: date, what was done, test count delta. +- 2026-05-09 — **`for…in` walks the prototype chain (with shadowing) but stops at native prototypes.** Was using `js-object-keys` which only returns own enumerable keys, so `for (k in instance)` only saw the instance's own properties — not inherited ones from `FACTORY.prototype`. Per spec, for-in walks the entire chain and yields each unique enumerable key once. Added `js-for-in-keys` + `js-for-in-walk` that iterate the chain, deduping via `contains?`. Stops at `Object.prototype` / `Array.prototype` / etc. since those carry "non-enumerable" methods we don't track property-attribute-wise — without this guard, `for (k in {})` would enumerate `toString`/`valueOf`/etc. Result: language/statements/for-in 10/30 → 12/30. Object 30/30, Array 18/30 unchanged. conformance.sh: 148/148. + - 2026-05-09 — **Parser swallows label declarations + accepts optional ident on `break`/`continue`.** Was rejecting `outer: while (...) { break outer; }` at parse time. Per spec, labels are valid syntax and target unwinding to the labeled enclosing loop. Added a parser branch for ` ':' ` that just parses through to the inner statement (label is dropped; the runtime treats unlabeled `break`/`continue` the same way for the common case where the inner loop is the target). Also extended `break`/`continue` to optionally consume a trailing ident. Result: language/statements/while 14/30 → 16/30, for 27/30 → 28/30. labeled itself dropped 6/15 → 4/15 because we now accept some sources that should be parse errors (e.g. `label: let x;` is a SyntaxError per spec) — net positive across the suite. Object 30/30, Array 18/30 unchanged. conformance.sh: 148/148. - 2026-05-09 — **`new function(){...}(args)` and `new f(...rest)` now parse and execute.** Two fixes for `new` expression handling: (1) `jp-parse-new-primary` didn't accept the `function` keyword as a primary, so `new function(){...}` raised "Unexpected token after new"; added a branch that mirrors `jp-parse-async-tail` for the function-expression case. (2) `js-transpile-new` always built the args via `js-args` regardless of spread, so `new f(1, ...[])` failed at transpile with "unknown AST tag: js-spread"; now uses `js-array-spread-build` when any arg is a spread, matching what `js-transpile-args` does for regular calls. Result: language/expressions/new 16/30 → 19/30. Object 30/30, Array 18/30, language/expressions/call 21/30 unchanged. conformance.sh: 148/148.