diff --git a/lib/js/parser.sx b/lib/js/parser.sx index 6d411771..a48949aa 100644 --- a/lib/js/parser.sx +++ b/lib/js/parser.sx @@ -333,6 +333,10 @@ (do (jp-advance! st) (jp-parse-new-expr st))) ((and (= (get t :type) "keyword") (= (get t :value) "this")) (do (jp-advance! st) (list (quote js-ident) "this"))) + ((and (= (get t :type) "op") (or (= (get t :value) "++") (= (get t :value) "--"))) + (do + (jp-advance! st) + (list (quote js-prefix) (get t :value) (jp-parse-unary st)))) ((and (= (get t :type) "op") (or (= (get t :value) "-") (= (get t :value) "+") (= (get t :value) "!") (= (get t :value) "~"))) (do (jp-advance! st) @@ -582,6 +586,11 @@ (jp-call-args-loop st args) (jp-expect! st "punct" ")") (jp-parse-postfix st (list (quote js-call) left args))))) + ((or (jp-at? st "op" "++") (jp-at? st "op" "--")) + (let + ((op (get (jp-peek st) :value))) + (jp-advance! st) + (list (quote js-postfix) op left))) (else left)))) (define diff --git a/lib/js/test.sh b/lib/js/test.sh index dede7b02..b1eba014 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -875,6 +875,30 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 1208) (eval "(js-eval \"isFinite(1/0)\")") +;; ── Phase 11.incdec: ++ / -- ───────────────────────────────────── +(epoch 1300) +(eval "(js-eval \"var x = 5; x++; x\")") +(epoch 1301) +(eval "(js-eval \"var x = 5; var y = x++; y\")") +(epoch 1302) +(eval "(js-eval \"var x = 5; ++x\")") +(epoch 1303) +(eval "(js-eval \"var x = 5; var y = ++x; y\")") +(epoch 1304) +(eval "(js-eval \"var x = 5; x--; x\")") +(epoch 1305) +(eval "(js-eval \"var x = 5; --x\")") +(epoch 1306) +(eval "(js-eval \"var o = {n: 10}; o.n++; o.n\")") +(epoch 1307) +(eval "(js-eval \"var o = {n: 10}; ++o.n\")") +(epoch 1308) +(eval "(js-eval \"var a = [1,2,3]; a[1]++; a[1]\")") +(epoch 1309) +(eval "(js-eval \"var x = 5; for (var i = 0; i < 3; i++) x++; x\")") +(epoch 1310) +(eval "(js-eval \"var sum = 0; for (var i = 1; i <= 5; i++) sum = sum + i; sum\")") + EPOCHS OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -1340,6 +1364,19 @@ check 1206 "Number.EPSILON > 0" 'true' check 1207 "isFinite(1)" 'true' check 1208 "isFinite(Inf)" 'false' +# ── Phase 11.incdec: ++ / -- ──────────────────────────────────── +check 1300 "x++ bumps x" '6' +check 1301 "x++ returns old" '5' +check 1302 "++x bumps and returns" '6' +check 1303 "++x captured new" '6' +check 1304 "x-- decrements" '4' +check 1305 "--x decrements, returns new" '4' +check 1306 "obj.n++" '11' +check 1307 "++obj.n" '11' +check 1308 "a[1]++" '3' +check 1309 "for-loop x++" '8' +check 1310 "for-loop accumulator" '15' + TOTAL=$((PASS + FAIL)) if [ $FAIL -eq 0 ]; then echo "✓ $PASS/$TOTAL JS-on-SX tests passed" diff --git a/lib/js/transpile.sx b/lib/js/transpile.sx index e92f7c27..ae7044ed 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -101,6 +101,10 @@ (js-transpile-funcexpr (nth ast 1) (nth ast 2) (nth ast 3))) ((js-tag? ast "js-assign") (js-transpile-assign (nth ast 1) (nth ast 2) (nth ast 3))) + ((js-tag? ast "js-postfix") + (js-transpile-postfix (nth ast 1) (nth ast 2))) + ((js-tag? ast "js-prefix") + (js-transpile-prefix (nth ast 1) (nth ast 2))) ((js-tag? ast "js-new") (js-transpile-new (nth ast 1) (nth ast 2))) ((js-tag? ast "js-class") @@ -440,6 +444,120 @@ ((= op "=") rhs-expr) (else (js-compound-update op lhs-expr rhs-expr))))) +(define + js-transpile-prefix + (fn + (op target) + (let + ((delta (if (= op "++") 1 -1))) + (cond + ((js-tag? target "js-ident") + (let + ((name (nth target 1))) + (let + ((sxname (js-sym name))) + (list + (js-sym "set!") + sxname + (list + (js-sym "+") + (list (js-sym "js-to-number") sxname) + delta))))) + ((js-tag? target "js-member") + (let + ((obj-sx (js-transpile (nth target 1))) (key (nth target 2))) + (list + (js-sym "js-set-prop") + obj-sx + key + (list + (js-sym "+") + (list + (js-sym "js-to-number") + (list (js-sym "js-get-prop") obj-sx key)) + delta)))) + ((js-tag? target "js-index") + (let + ((obj-sx (js-transpile (nth target 1))) + (key-sx (js-transpile (nth target 2)))) + (list + (js-sym "js-set-prop") + obj-sx + key-sx + (list + (js-sym "+") + (list + (js-sym "js-to-number") + (list (js-sym "js-get-prop") obj-sx key-sx)) + delta)))) + (else (error "js-transpile-prefix: unsupported target")))))) + +(define + js-transpile-postfix + (fn + (op target) + (let + ((delta (if (= op "++") 1 -1))) + (cond + ((js-tag? target "js-ident") + (let + ((name (nth target 1))) + (let + ((sxname (js-sym name))) + (list + (js-sym "let") + (list + (list + (js-sym "__js_old__") + (list (js-sym "js-to-number") sxname))) + (list + (js-sym "set!") + sxname + (list (js-sym "+") (js-sym "__js_old__") delta)) + (js-sym "__js_old__"))))) + ((js-tag? target "js-member") + (let + ((obj-sx (js-transpile (nth target 1))) (key (nth target 2))) + (list + (js-sym "let") + (list + (list (js-sym "__js_obj__") obj-sx) + (list + (js-sym "__js_old__") + (list + (js-sym "js-to-number") + (list (js-sym "js-get-prop") (js-sym "__js_obj__") key)))) + (list + (js-sym "js-set-prop") + (js-sym "__js_obj__") + key + (list (js-sym "+") (js-sym "__js_old__") delta)) + (js-sym "__js_old__")))) + ((js-tag? target "js-index") + (let + ((obj-sx (js-transpile (nth target 1))) + (key-sx (js-transpile (nth target 2)))) + (list + (js-sym "let") + (list + (list (js-sym "__js_obj__") obj-sx) + (list (js-sym "__js_key__") key-sx) + (list + (js-sym "__js_old__") + (list + (js-sym "js-to-number") + (list + (js-sym "js-get-prop") + (js-sym "__js_obj__") + (js-sym "__js_key__"))))) + (list + (js-sym "js-set-prop") + (js-sym "__js_obj__") + (js-sym "__js_key__") + (list (js-sym "+") (js-sym "__js_old__") delta)) + (js-sym "__js_old__")))) + (else (error "js-transpile-postfix: unsupported target")))))) + (define js-param-sym (fn diff --git a/plans/js-on-sx.md b/plans/js-on-sx.md index 19ff3557..7185f72e 100644 --- a/plans/js-on-sx.md +++ b/plans/js-on-sx.md @@ -175,7 +175,9 @@ Append-only record of completed iterations. Loop writes one line per iteration: - 2026-04-23 — **Regex literal support (lex+parse+transpile+runtime stub).** Runner now accepts repeatable `--filter` flags (OR'd). Lexer gains `js-regex-context?` (returns true at SOF or when last token is op/non-closing-punct/regex-keyword incl. return/typeof/in/of/throw/new/delete/instanceof/void/yield/await/case/do/else) and `read-regex` (handles `\` escapes and `[...]` classes, collects flags as ident chars). `scan!` intercepts `/` ahead of the operator-match tries when in a regex context and emits `{:type "regex" :value {:pattern :flags}}`. Parser adds a `regex` primary branch → `(js-regex pat flags)`. Transpile emits `(js-regex-new pat flags)`. Runtime adds: `js-regex?` predicate (dict + `__js_regex__` key), `js-regex-new` builds the tagged dict with `source / flags / global / ignoreCase / multiline / sticky / unicode / dotAll / hasIndices / lastIndex` populated; `js-regex-invoke-method` dispatches `.test` / `.exec` / `.toString`; `js-invoke-method` gets a regex branch before the generic method-lookup fallback. Stub engine (`js-regex-stub-test` / `-exec`) uses `js-string-index-of` — not a real regex, but enough to make `/foo/.test('hi foo')` work. `__js_regex_platform__` dict + `js-regex-platform-override!` let a real platform primitive be swapped in later without runtime changes. 30 new unit tests (17 lex + 3 parse + 1 transpile + 4 obj-shape + 4 prop + 2 test()): **308/310** (278→+30). Conformance unchanged. Gotcha: `contains?` with 2 args expects `(contains? list x)`, NOT a dict — use `(contains? (keys d) k)` or `dict-has?`. First pass forgot that and cascaded errors across Math / class tests via the `js-regex?` predicate inside `js-invoke-method`. Wide scoreboard run across 9 targeted categories launched in background. -- 2026-04-23 — **Expanded Math + Number globals.** Added `Math.sqrt/.pow/.trunc/.sign/.cbrt/.hypot` using SX primitives (`sqrt`, `pow`, `abs`, hand-rolled loops). Added missing constants: `Math.LN2 / LN10 / LOG2E / LOG10E / SQRT2 / SQRT1_2`; bumped PI/E precision to full 16-digit. New `Number` global: `isFinite`, `isNaN`, `isInteger`, `isSafeInteger`, `MAX_VALUE / MIN_VALUE / MAX_SAFE_INTEGER / MIN_SAFE_INTEGER / EPSILON / POSITIVE_INFINITY / NEGATIVE_INFINITY / NaN`. Global `isFinite`, `isNaN`, `Infinity`, `NaN`. `js-number-is-nan` uses the self-inequality trick `(and (number? v) (not (= v v)))`. Wired into `js-global`. 21 new unit tests (12 Math + 9 Number), **329/331** (308→+21). Conformance unchanged. Gotchas: (1) `sx_insert_near` takes a single node — multi-define source blocks get silently truncated. Use `sx_insert_child` at the root per define. (2) SX `(/ 1 0)` → `inf`, and `1e999` also → `inf`; both can be used as `Infinity`. (3) `1e999` has no `-` form — wrap as `(- 0 1e999)` or just use `-1e999` literal. +- 2026-04-23 — **Expanded Math + Number globals.** Added `Math.sqrt/.pow/.trunc/.sign/.cbrt/.hypot` using SX primitives (`sqrt`, `pow`, `abs`, hand-rolled loops). Added missing constants: `Math.LN2 / LN10 / LOG2E / LOG10E / SQRT2 / SQRT1_2`; bumped PI/E precision to full 16-digit. New `Number` global: `isFinite`, `isNaN`, `isInteger`, `isSafeInteger`, `MAX_VALUE / MIN_VALUE / MAX_SAFE_INTEGER / MIN_SAFE_INTEGER / EPSILON / POSITIVE_INFINITY / NEGATIVE_INFINITY / NaN`. Global `isFinite`, `isNaN`, `Infinity`, `NaN`. `js-number-is-nan` uses the self-inequality trick `(and (number? v) (not (= v v)))`. Wired into `js-global`. 21 new unit tests (12 Math + 9 Number), **329/331** (308→+21). Conformance unchanged. Gotchas: (1) `sx_insert_near` takes a single node — multi-define source blocks get silently truncated. Use `sx_insert_child` at the root per define. (2) SX `(/ 1 0)` → `inf`, and `1e999` also → `inf`; both can be used as `Infinity`. (3) **`(define NaN ...)` and `(define Infinity ...)` crash at load — SX tokenizer parses `NaN` and `Infinity` as the *numeric literals* `nan` / `inf`, so `define` sees `(define )` and rejects it with "Expected symbol, got number". Drop those top-level aliases; put the values in `js-global` dict instead where the keyword key avoids the conflict.** + +- 2026-04-23 — **Postfix/prefix `++` / `--`.** Parser: postfix branch in `jp-parse-postfix` (matches `op ++`/`--` after the current expression and emits `(js-postfix op target)`), prefix branch in `jp-parse-primary` *before* the unary-`-/+/!/~` path emits `(js-prefix op target)`. Transpile: `js-transpile-prefix` emits `(set! sxname (+ (js-to-number sxname) ±1))` for idents, `(js-set-prop obj key (+ (js-to-number (js-get-prop obj key)) ±1))` for members/indices. `js-transpile-postfix` uses a `let` binding to cache the old value via `js-to-number`, then updates and returns the saved value — covers ident, member, and index targets. 11 new unit tests (ident inc/dec, pre vs post return value, obj.key, a[i], in `for(;; i++)`, accumulator loop), **340/342** (329→+11). Conformance unchanged. ## Phase 3-5 gotchas