From 21d0be58ecd7a9b1040a4b023da5e39827a656da Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 9 May 2026 08:04:21 +0000 Subject: [PATCH] js-on-sx: typeof returns "undefined" for unresolvable references --- lib/js/transpile.sx | 14 ++++++++++++++ plans/js-on-sx.md | 2 ++ 2 files changed, 16 insertions(+) diff --git a/lib/js/transpile.sx b/lib/js/transpile.sx index 1ba7e153..d6cd58be 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -225,6 +225,20 @@ (js-transpile (nth arg 1)) (js-transpile (nth arg 2)))) (else true))) + ((and (= op "typeof") (js-tag? arg "js-ident")) + (let + ((name (nth arg 1))) + (list + (js-sym "if") + (list + (js-sym "or") + (list + (js-sym "env-has?") + (list (js-sym "current-env")) + name) + (list (js-sym "dict-has?") (js-sym "js-global") name)) + (list (js-sym "js-typeof") (js-transpile arg)) + "undefined"))) (else (let ((a (js-transpile arg))) diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index 1f202d04..3109c1fd 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 — **`typeof ` returns `"undefined"` instead of throwing ReferenceError.** Per JS spec, `typeof` on an unresolvable Reference is special-cased — it must return `"undefined"` without throwing. We were transpiling `typeof X` to `(js-typeof )`, and the symbol lookup itself errored for undeclared globals. New transpiler branch in `js-transpile-unop`: when the operand is a `js-ident`, emit `(if (or (env-has? (current-env) "name") (dict-has? js-global "name")) (js-typeof ) "undefined")` — checks both the lexical env (for local var/let/const/parameters) and the global object, and only references the symbol when the if branch is taken (SX `if` is lazy, so the unbound symbol in the false branch never errors). Result: language/expressions/typeof 9/13 → 10/13, built-ins/Object 29/30 → 30/30 (full pass — the `S15.2.1.1_A2_T11.js` test was using `typeof obj` on an undeclared name). conformance.sh: 148/148. + - 2026-05-09 — **`==` returns false when either side is NaN, even across the numeric/string paths.** `js-loose-eq` was converting both sides to numbers (`Number.NaN == "string"` → `NaN == NaN`) and using SX `(=)`, which apparently returns true when both NaN values are the same reference. Per JS, NaN compares unequal to everything including itself. Wrapped both cross-type numeric/string branches in `(or (js-number-is-nan an) (js-number-is-nan bn))` short-circuits to false. Result: language/expressions/equals 20/30 → 23/30. strict-equals/Number/Object unchanged. conformance.sh: 148/148. - 2026-05-09 — **Lexer: `}` ends the regex context, like `)` and `]`.** Was treating `/` after `}` as the start of a regex literal, so `({}) / function(){return 1}` lexed `} / function(){...})` as `}` + regex `/ function(){return 1}/`. Per JS, after `}` of an object literal we're in expression-end position and `/` is division. The "block vs object" distinction is context-sensitive, but in practice expression-position `}` is the common case and there is no statement/block hazard for our parser since blocks at expression position don't typically have a following `/`. Single-char addition to the no-regex-context check. Result: language/expressions/division 25/30 → 26/30. asi/Map/Object unchanged. conformance.sh: 148/148.