diff --git a/lib/js/parser.sx b/lib/js/parser.sx index cee89e46..47b11be5 100644 --- a/lib/js/parser.sx +++ b/lib/js/parser.sx @@ -593,6 +593,42 @@ (jp-call-args-loop st args) (jp-expect! st "punct" ")") (jp-parse-postfix st (list (quote js-call) left args))))) + ((jp-at? st "op" "?.") + (do + (jp-advance! st) + (cond + ((jp-at? st "punct" "[") + (begin + (jp-advance! st) + (let + ((k (jp-parse-assignment st))) + (jp-expect! st "punct" "]") + (jp-parse-postfix + st + (list (quote js-optchain-index) left k))))) + ((jp-at? st "punct" "(") + (begin + (jp-advance! st) + (let + ((args (list))) + (jp-call-args-loop st args) + (jp-expect! st "punct" ")") + (jp-parse-postfix + st + (list (quote js-optchain-call) left args))))) + (else + (let + ((t (jp-peek st))) + (if + (or + (= (get t :type) "ident") + (= (get t :type) "keyword")) + (do + (jp-advance! st) + (jp-parse-postfix + st + (list (quote js-optchain-member) left (get t :value)))) + (error "expected ident, [ or ( after ?."))))))) ((or (jp-at? st "op" "++") (jp-at? st "op" "--")) (let ((op (get (jp-peek st) :value))) diff --git a/lib/js/runtime.sx b/lib/js/runtime.sx index 4051001d..8d8d020a 100644 --- a/lib/js/runtime.sx +++ b/lib/js/runtime.sx @@ -1750,6 +1750,24 @@ (define Object {:entries js-object-entries :values js-object-values :freeze js-object-freeze :assign js-object-assign :keys js-object-keys}) +(define + js-optchain-get + (fn + (obj key) + (if + (or (= obj nil) (js-undefined? obj)) + js-undefined + (js-get-prop obj key)))) + +(define + js-optchain-call + (fn + (fn-val args) + (if + (or (= fn-val nil) (js-undefined? fn-val)) + js-undefined + (js-call-plain fn-val args)))) + (define js-array-spread-build (fn diff --git a/lib/js/test.sh b/lib/js/test.sh index 7163b7bd..1a80ae69 100755 --- a/lib/js/test.sh +++ b/lib/js/test.sh @@ -1081,6 +1081,18 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 2603) (eval "(js-eval \"var pt = {px: 100}; var {px} = pt; px + 1\")") +;; ── Phase 11.optchain: ?. optional chaining ───────────────────── +(epoch 2700) +(eval "(js-eval \"var o = {x: 5}; o?.x\")") +(epoch 2701) +(eval "(js-eval \"var o = null; o?.x\")") +(epoch 2702) +(eval "(js-eval \"var o = undefined; o?.x\")") +(epoch 2703) +(eval "(js-eval \"var o = {a:{b:7}}; o?.a?.b\")") +(epoch 2704) +(eval "(js-eval \"var o = {a:null}; var r = o?.a?.b; r === undefined\")") + EPOCHS @@ -1664,6 +1676,13 @@ check 2601 "arr destructure" '6' check 2602 "arr destructure skip" '4' check 2603 "obj partial+add" '101' +# ── Phase 11.optchain ────────────────────────────────────────── +check 2700 "?. obj present" '5' +check 2701 "?. obj null" 'undefined' +check 2702 "?. obj undef" 'undefined' +check 2703 "?. chained" '7' +check 2704 "?. null-chain" 'true' + 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 25e40650..5bcf22c7 100644 --- a/lib/js/transpile.sx +++ b/lib/js/transpile.sx @@ -111,6 +111,12 @@ (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-optchain-member") + (js-transpile-optchain-member (nth ast 1) (nth ast 2))) + ((js-tag? ast "js-optchain-index") + (js-transpile-optchain-index (nth ast 1) (nth ast 2))) + ((js-tag? ast "js-optchain-call") + (js-transpile-optchain-call (nth ast 1) (nth ast 2))) ((js-tag? ast "js-switch") (js-transpile-switch (nth ast 1) (nth ast 2))) ((js-tag? ast "js-new") @@ -819,6 +825,30 @@ (js-collect-funcdecls (rest stmts)))) (else (js-collect-funcdecls (rest stmts)))))) +(define + js-transpile-optchain-member + (fn + (obj-ast name) + (list (js-sym "js-optchain-get") (js-transpile obj-ast) name))) + +(define + js-transpile-optchain-index + (fn + (obj-ast key-ast) + (list + (js-sym "js-optchain-get") + (js-transpile obj-ast) + (js-transpile key-ast)))) + +(define + js-transpile-optchain-call + (fn + (callee-ast args) + (list + (js-sym "js-optchain-call") + (js-transpile callee-ast) + (js-transpile-args args)))) + (define js-transpile-stmt-list (fn