js-on-sx: NaN / Infinity resolve at transpile; strict-eq returns false for NaN
Previously NaN / Infinity were SX symbols that couldn't be (define)'d because the SX tokenizer parses 'NaN' and 'Infinity' as numeric literals. js-transpile-ident now rewrites NaN -> (js-nan-value), Infinity -> (js-infinity-value), each a zero-arg function returning the appropriate IEEE value ((/ 0.0 0.0) and (/ 1.0 0.0)). Also fixes js-number-is-nan: in this SX, (= nan nan) returns true, so the classic 'v !== v' trick doesn't work. Now checks (inspect v) against 'nan'/'-nan' strings. Extends js-strict-eq: NaN === NaN returns false per ES spec. 8 new unit tests, 497/499 total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,16 +13,25 @@
|
|||||||
;; JS `undefined` — we represent it as a distinct keyword so it
|
;; JS `undefined` — we represent it as a distinct keyword so it
|
||||||
;; survives round-trips through the evaluator without colliding with
|
;; survives round-trips through the evaluator without colliding with
|
||||||
;; SX `nil` (which maps to JS `null`).
|
;; SX `nil` (which maps to JS `null`).
|
||||||
(define js-undefined :js-undefined)
|
(define js-nan-value (fn () (/ 0 0)))
|
||||||
|
|
||||||
(define js-undefined? (fn (v) (= v :js-undefined)))
|
(define js-infinity-value (fn () (/ 1 0)))
|
||||||
|
|
||||||
;; ── Type predicates ───────────────────────────────────────────────
|
;; ── Type predicates ───────────────────────────────────────────────
|
||||||
|
|
||||||
(define __js_this_cell__ (dict))
|
(define js-undefined :js-undefined)
|
||||||
|
|
||||||
;; ── Boolean coercion (ToBoolean) ──────────────────────────────────
|
;; ── Boolean coercion (ToBoolean) ──────────────────────────────────
|
||||||
|
|
||||||
|
(define js-undefined? (fn (v) (= v :js-undefined)))
|
||||||
|
|
||||||
|
;; ── Numeric coercion (ToNumber) ───────────────────────────────────
|
||||||
|
|
||||||
|
(define __js_this_cell__ (dict))
|
||||||
|
|
||||||
|
;; Parse a JS-style string to a number. For the slice we just delegate
|
||||||
|
;; to SX's number parser via `str->num`/`parse-number`. Empty string → 0
|
||||||
|
;; per JS (technically ToNumber("") === 0).
|
||||||
(define
|
(define
|
||||||
js-this
|
js-this
|
||||||
(fn
|
(fn
|
||||||
@@ -32,13 +41,13 @@
|
|||||||
(get __js_this_cell__ "this")
|
(get __js_this_cell__ "this")
|
||||||
:js-undefined)))
|
:js-undefined)))
|
||||||
|
|
||||||
;; ── Numeric coercion (ToNumber) ───────────────────────────────────
|
;; Safe number-parser. Tries to call an SX primitive that can parse
|
||||||
|
;; strings to numbers; on failure returns 0 (stand-in for NaN so the
|
||||||
|
;; slice doesn't crash).
|
||||||
(define js-this-set! (fn (v) (dict-set! __js_this_cell__ "this" v)))
|
(define js-this-set! (fn (v) (dict-set! __js_this_cell__ "this" v)))
|
||||||
|
|
||||||
;; Parse a JS-style string to a number. For the slice we just delegate
|
;; Minimal string->number for the slice. Handles integers, negatives,
|
||||||
;; to SX's number parser via `str->num`/`parse-number`. Empty string → 0
|
;; and simple decimals. Returns 0 on malformed input.
|
||||||
;; per JS (technically ToNumber("") === 0).
|
|
||||||
(define
|
(define
|
||||||
js-call-with-this
|
js-call-with-this
|
||||||
(fn
|
(fn
|
||||||
@@ -51,9 +60,6 @@
|
|||||||
((result (js-apply-fn fn-val args)))
|
((result (js-apply-fn fn-val args)))
|
||||||
(begin (js-this-set! saved) result))))))
|
(begin (js-this-set! saved) result))))))
|
||||||
|
|
||||||
;; Safe number-parser. Tries to call an SX primitive that can parse
|
|
||||||
;; strings to numbers; on failure returns 0 (stand-in for NaN so the
|
|
||||||
;; slice doesn't crash).
|
|
||||||
(define
|
(define
|
||||||
js-function-method?
|
js-function-method?
|
||||||
(fn
|
(fn
|
||||||
@@ -66,8 +72,6 @@
|
|||||||
(= name "name")
|
(= name "name")
|
||||||
(= name "length"))))
|
(= name "length"))))
|
||||||
|
|
||||||
;; Minimal string->number for the slice. Handles integers, negatives,
|
|
||||||
;; and simple decimals. Returns 0 on malformed input.
|
|
||||||
(define
|
(define
|
||||||
js-invoke-function-method
|
js-invoke-function-method
|
||||||
(fn
|
(fn
|
||||||
@@ -177,6 +181,13 @@
|
|||||||
((d (mod n radix)) (rest (js-math-trunc (/ n radix))))
|
((d (mod n radix)) (rest (js-math-trunc (/ n radix))))
|
||||||
(js-num-to-str-radix-rec rest radix (str (js-digit-char d) acc))))))
|
(js-num-to-str-radix-rec rest radix (str (js-digit-char d) acc))))))
|
||||||
|
|
||||||
|
;; parse a decimal number from a trimmed non-empty string.
|
||||||
|
;; s — source
|
||||||
|
;; i — cursor
|
||||||
|
;; acc — integer part so far (or total for decimals)
|
||||||
|
;; sign — 1 or -1
|
||||||
|
;; frac? — are we past the decimal point
|
||||||
|
;; fdiv — divisor used to scale fraction digits (only if frac?)
|
||||||
(define
|
(define
|
||||||
js-digit-char
|
js-digit-char
|
||||||
(fn
|
(fn
|
||||||
@@ -209,17 +220,12 @@
|
|||||||
"."
|
"."
|
||||||
(js-pad-int-str (js-to-string (js-math-trunc frac-abs)) d))))))))))
|
(js-pad-int-str (js-to-string (js-math-trunc frac-abs)) d))))))))))
|
||||||
|
|
||||||
;; parse a decimal number from a trimmed non-empty string.
|
|
||||||
;; s — source
|
|
||||||
;; i — cursor
|
|
||||||
;; acc — integer part so far (or total for decimals)
|
|
||||||
;; sign — 1 or -1
|
|
||||||
;; frac? — are we past the decimal point
|
|
||||||
;; fdiv — divisor used to scale fraction digits (only if frac?)
|
|
||||||
(define
|
(define
|
||||||
js-pow-int
|
js-pow-int
|
||||||
(fn (b e) (if (<= e 0) 1 (* b (js-pow-int b (- e 1))))))
|
(fn (b e) (if (<= e 0) 1 (* b (js-pow-int b (- e 1))))))
|
||||||
|
|
||||||
|
;; ── String coercion (ToString) ────────────────────────────────────
|
||||||
|
|
||||||
(define
|
(define
|
||||||
js-pad-int-str
|
js-pad-int-str
|
||||||
(fn (s n) (if (>= (len s) n) s (js-pad-int-str (str "0" s) n))))
|
(fn (s n) (if (>= (len s) n) s (js-pad-int-str (str "0" s) n))))
|
||||||
@@ -255,8 +261,9 @@
|
|||||||
(nth args 5)))
|
(nth args 5)))
|
||||||
(else (apply callable args))))))
|
(else (apply callable args))))))
|
||||||
|
|
||||||
;; ── String coercion (ToString) ────────────────────────────────────
|
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
|
||||||
|
|
||||||
|
;; JS `+`: if either operand is a string → string concat, else numeric.
|
||||||
(define
|
(define
|
||||||
js-invoke-method
|
js-invoke-method
|
||||||
(fn
|
(fn
|
||||||
@@ -292,9 +299,6 @@
|
|||||||
(= name "valueOf")
|
(= name "valueOf")
|
||||||
(= name "toLocaleString"))))
|
(= name "toLocaleString"))))
|
||||||
|
|
||||||
;; ── Arithmetic (JS rules) ─────────────────────────────────────────
|
|
||||||
|
|
||||||
;; JS `+`: if either operand is a string → string concat, else numeric.
|
|
||||||
(define
|
(define
|
||||||
js-invoke-object-method
|
js-invoke-object-method
|
||||||
(fn
|
(fn
|
||||||
@@ -398,6 +402,7 @@
|
|||||||
js-invoke-method-dyn
|
js-invoke-method-dyn
|
||||||
(fn (recv key args) (js-invoke-method recv key args)))
|
(fn (recv key args) (js-invoke-method recv key args)))
|
||||||
|
|
||||||
|
;; Bitwise + logical-not
|
||||||
(define
|
(define
|
||||||
js-call-plain
|
js-call-plain
|
||||||
(fn
|
(fn
|
||||||
@@ -424,7 +429,9 @@
|
|||||||
ret
|
ret
|
||||||
obj))))))
|
obj))))))
|
||||||
|
|
||||||
;; Bitwise + logical-not
|
;; ── Equality ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
;; Strict equality (===): no coercion; js-undefined matches js-undefined.
|
||||||
(define
|
(define
|
||||||
js-instanceof
|
js-instanceof
|
||||||
(fn
|
(fn
|
||||||
@@ -453,9 +460,9 @@
|
|||||||
((not (= (type-of p) "dict")) false)
|
((not (= (type-of p) "dict")) false)
|
||||||
(else (js-instanceof-walk p proto))))))))
|
(else (js-instanceof-walk p proto))))))))
|
||||||
|
|
||||||
;; ── Equality ──────────────────────────────────────────────────────
|
;; Abstract equality (==): type coercion rules.
|
||||||
|
;; Simplified: number↔string coerce both to number; null == undefined;
|
||||||
;; Strict equality (===): no coercion; js-undefined matches js-undefined.
|
;; everything else falls back to strict equality.
|
||||||
(define
|
(define
|
||||||
js-in
|
js-in
|
||||||
(fn
|
(fn
|
||||||
@@ -474,9 +481,11 @@
|
|||||||
((dict-has? obj "__proto__") (js-in-walk (get obj "__proto__") skey))
|
((dict-has? obj "__proto__") (js-in-walk (get obj "__proto__") skey))
|
||||||
(else false))))
|
(else false))))
|
||||||
|
|
||||||
;; Abstract equality (==): type coercion rules.
|
;; ── Relational comparisons ────────────────────────────────────────
|
||||||
;; Simplified: number↔string coerce both to number; null == undefined;
|
|
||||||
;; everything else falls back to strict equality.
|
;; Abstract relational comparison from ES5.
|
||||||
|
;; Numbers compare numerically; two strings compare lexicographically;
|
||||||
|
;; mixed types coerce both to numbers.
|
||||||
(define
|
(define
|
||||||
Error
|
Error
|
||||||
(fn
|
(fn
|
||||||
@@ -513,11 +522,6 @@
|
|||||||
nil)
|
nil)
|
||||||
this))))
|
this))))
|
||||||
|
|
||||||
;; ── Relational comparisons ────────────────────────────────────────
|
|
||||||
|
|
||||||
;; Abstract relational comparison from ES5.
|
|
||||||
;; Numbers compare numerically; two strings compare lexicographically;
|
|
||||||
;; mixed types coerce both to numbers.
|
|
||||||
(define
|
(define
|
||||||
RangeError
|
RangeError
|
||||||
(fn
|
(fn
|
||||||
@@ -586,10 +590,6 @@
|
|||||||
|
|
||||||
(define __js_proto_table__ (dict))
|
(define __js_proto_table__ (dict))
|
||||||
|
|
||||||
(define __js_next_id__ (dict))
|
|
||||||
|
|
||||||
(dict-set! __js_next_id__ "n" 0)
|
|
||||||
|
|
||||||
;; ── Property access ───────────────────────────────────────────────
|
;; ── Property access ───────────────────────────────────────────────
|
||||||
|
|
||||||
;; obj[key] or obj.key in JS. Handles:
|
;; obj[key] or obj.key in JS. Handles:
|
||||||
@@ -597,6 +597,10 @@
|
|||||||
;; • lists indexed by number (incl. .length)
|
;; • lists indexed by number (incl. .length)
|
||||||
;; • strings indexed by number (incl. .length)
|
;; • strings indexed by number (incl. .length)
|
||||||
;; Returns js-undefined if the key is absent.
|
;; Returns js-undefined if the key is absent.
|
||||||
|
(define __js_next_id__ (dict))
|
||||||
|
|
||||||
|
(dict-set! __js_next_id__ "n" 0)
|
||||||
|
|
||||||
(define
|
(define
|
||||||
js-get-ctor-proto
|
js-get-ctor-proto
|
||||||
(fn
|
(fn
|
||||||
@@ -608,6 +612,7 @@
|
|||||||
(else
|
(else
|
||||||
(let ((p (dict))) (begin (dict-set! __js_proto_table__ id p) p)))))))
|
(let ((p (dict))) (begin (dict-set! __js_proto_table__ id p) p)))))))
|
||||||
|
|
||||||
|
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
|
||||||
(define
|
(define
|
||||||
js-reset-ctor-proto!
|
js-reset-ctor-proto!
|
||||||
(fn
|
(fn
|
||||||
@@ -616,13 +621,16 @@
|
|||||||
((id (js-ctor-id ctor)) (p (dict)))
|
((id (js-ctor-id ctor)) (p (dict)))
|
||||||
(begin (dict-set! __js_proto_table__ id p) p))))
|
(begin (dict-set! __js_proto_table__ id p) p))))
|
||||||
|
|
||||||
|
;; ── Short-circuit logical ops ─────────────────────────────────────
|
||||||
|
|
||||||
|
;; `a && b` in JS: if a is truthy return b else return a. The thunk
|
||||||
|
;; form defers evaluation of b — the transpiler passes (fn () b).
|
||||||
(define
|
(define
|
||||||
js-set-ctor-proto!
|
js-set-ctor-proto!
|
||||||
(fn
|
(fn
|
||||||
(ctor proto)
|
(ctor proto)
|
||||||
(let ((id (js-ctor-id ctor))) (dict-set! __js_proto_table__ id proto))))
|
(let ((id (js-ctor-id ctor))) (dict-set! __js_proto_table__ id proto))))
|
||||||
|
|
||||||
;; Setter — mutates the dict. Returns the new value (JS assignment yields rhs).
|
|
||||||
(define
|
(define
|
||||||
js-ctor-id
|
js-ctor-id
|
||||||
(fn
|
(fn
|
||||||
@@ -632,10 +640,9 @@
|
|||||||
(get ctor "__ctor_id__"))
|
(get ctor "__ctor_id__"))
|
||||||
(else (inspect ctor)))))
|
(else (inspect ctor)))))
|
||||||
|
|
||||||
;; ── Short-circuit logical ops ─────────────────────────────────────
|
;; ── console.log ───────────────────────────────────────────────────
|
||||||
|
|
||||||
;; `a && b` in JS: if a is truthy return b else return a. The thunk
|
;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
|
||||||
;; form defers evaluation of b — the transpiler passes (fn () b).
|
|
||||||
(define
|
(define
|
||||||
js-typeof
|
js-typeof
|
||||||
(fn
|
(fn
|
||||||
@@ -665,9 +672,8 @@
|
|||||||
((= v "") false)
|
((= v "") false)
|
||||||
(else true))))
|
(else true))))
|
||||||
|
|
||||||
;; ── console.log ───────────────────────────────────────────────────
|
;; ── Math object ───────────────────────────────────────────────────
|
||||||
|
|
||||||
;; Trivial bridge. `log-info` is available on OCaml; fall back to print.
|
|
||||||
(define
|
(define
|
||||||
js-to-number
|
js-to-number
|
||||||
(fn
|
(fn
|
||||||
@@ -680,13 +686,9 @@
|
|||||||
((= (type-of v) "number") v)
|
((= (type-of v) "number") v)
|
||||||
((= (type-of v) "string") (js-string-to-number v))
|
((= (type-of v) "string") (js-string-to-number v))
|
||||||
(else 0))))
|
(else 0))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
js-string-to-number
|
js-string-to-number
|
||||||
(fn (s) (cond ((= s "") 0) (else (js-parse-num-safe s)))))
|
(fn (s) (cond ((= s "") 0) (else (js-parse-num-safe s)))))
|
||||||
|
|
||||||
;; ── Math object ───────────────────────────────────────────────────
|
|
||||||
|
|
||||||
(define js-parse-num-safe (fn (s) (cond (else (js-num-from-string s)))))
|
(define js-parse-num-safe (fn (s) (cond (else (js-num-from-string s)))))
|
||||||
(define
|
(define
|
||||||
js-num-from-string
|
js-num-from-string
|
||||||
@@ -719,10 +721,16 @@
|
|||||||
(cond
|
(cond
|
||||||
((<= n 0) "")
|
((<= n 0) "")
|
||||||
((js-is-space? (char-at s (- n 1))) (js-trim-right-at s (- n 1)))
|
((js-is-space? (char-at s (- n 1))) (js-trim-right-at s (- n 1)))
|
||||||
(else (substr s 0 n)))))
|
(else (substr s 0 n))))) ; deterministic placeholder for tests
|
||||||
|
|
||||||
(define
|
(define
|
||||||
js-is-space?
|
js-is-space?
|
||||||
(fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r"))))
|
(fn (c) (or (= c " ") (= c "\t") (= c "\n") (= c "\r"))))
|
||||||
|
|
||||||
|
;; The global object — lookup table for JS names that aren't in the
|
||||||
|
;; SX env. Transpiled idents look up locally first; globals here are a
|
||||||
|
;; fallback, but most slice programs reference `console`, `Math`,
|
||||||
|
;; `undefined` as plain symbols, which we bind as defines above.
|
||||||
(define
|
(define
|
||||||
js-parse-decimal
|
js-parse-decimal
|
||||||
(fn
|
(fn
|
||||||
@@ -754,7 +762,7 @@
|
|||||||
sign
|
sign
|
||||||
false
|
false
|
||||||
0)))
|
0)))
|
||||||
(else (* sign (if frac? (/ acc fdiv) acc))))))) ; deterministic placeholder for tests
|
(else (* sign (if frac? (/ acc fdiv) acc)))))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
js-is-digit?
|
js-is-digit?
|
||||||
@@ -773,10 +781,6 @@
|
|||||||
(= c "8")
|
(= c "8")
|
||||||
(= c "9")))))
|
(= c "9")))))
|
||||||
|
|
||||||
;; The global object — lookup table for JS names that aren't in the
|
|
||||||
;; SX env. Transpiled idents look up locally first; globals here are a
|
|
||||||
;; fallback, but most slice programs reference `console`, `Math`,
|
|
||||||
;; `undefined` as plain symbols, which we bind as defines above.
|
|
||||||
(define
|
(define
|
||||||
js-digit-val
|
js-digit-val
|
||||||
(fn
|
(fn
|
||||||
@@ -860,7 +864,8 @@
|
|||||||
((and (js-undefined? a) (js-undefined? b)) true)
|
((and (js-undefined? a) (js-undefined? b)) true)
|
||||||
((or (js-undefined? a) (js-undefined? b)) false)
|
((or (js-undefined? a) (js-undefined? b)) false)
|
||||||
((not (= (type-of a) (type-of b))) false)
|
((not (= (type-of a) (type-of b))) false)
|
||||||
(else (= a b)))))
|
(else
|
||||||
|
(if (or (js-number-is-nan a) (js-number-is-nan b)) false (= a b))))))
|
||||||
|
|
||||||
(define js-strict-neq (fn (a b) (not (js-strict-eq a b))))
|
(define js-strict-neq (fn (a b) (not (js-strict-eq a b))))
|
||||||
|
|
||||||
@@ -2065,7 +2070,11 @@
|
|||||||
(not (= v (/ 1 0)))
|
(not (= v (/ 1 0)))
|
||||||
(not (= v (/ -1 0))))))
|
(not (= v (/ -1 0))))))
|
||||||
|
|
||||||
(define js-number-is-nan (fn (v) (and (number? v) (not (= v v)))))
|
(define
|
||||||
|
js-number-is-nan
|
||||||
|
(fn
|
||||||
|
(v)
|
||||||
|
(and (number? v) (or (= (inspect v) "nan") (= (inspect v) "-nan")))))
|
||||||
|
|
||||||
(define
|
(define
|
||||||
js-number-is-integer
|
js-number-is-integer
|
||||||
|
|||||||
@@ -1213,6 +1213,24 @@ cat > "$TMPFILE" << 'EPOCHS'
|
|||||||
(epoch 3709)
|
(epoch 3709)
|
||||||
(eval "(js-eval \"var a=[1,2,3]; a.keys().join(',')\")")
|
(eval "(js-eval \"var a=[1,2,3]; a.keys().join(',')\")")
|
||||||
|
|
||||||
|
;; ── Phase 11.globals: NaN / Infinity / strict-eq ─────────────
|
||||||
|
(epoch 3750)
|
||||||
|
(eval "(js-eval \"typeof NaN\")")
|
||||||
|
(epoch 3751)
|
||||||
|
(eval "(js-eval \"isNaN(NaN)\")")
|
||||||
|
(epoch 3752)
|
||||||
|
(eval "(js-eval \"isNaN(5)\")")
|
||||||
|
(epoch 3753)
|
||||||
|
(eval "(js-eval \"NaN === NaN\")")
|
||||||
|
(epoch 3754)
|
||||||
|
(eval "(js-eval \"Infinity > 100\")")
|
||||||
|
(epoch 3755)
|
||||||
|
(eval "(js-eval \"-Infinity < 0\")")
|
||||||
|
(epoch 3756)
|
||||||
|
(eval "(js-eval \"isFinite(1)\")")
|
||||||
|
(epoch 3757)
|
||||||
|
(eval "(js-eval \"isFinite(Infinity)\")")
|
||||||
|
|
||||||
;; ── Phase 11.strmore: more String.prototype methods ─────────
|
;; ── Phase 11.strmore: more String.prototype methods ─────────
|
||||||
(epoch 3800)
|
(epoch 3800)
|
||||||
(eval "(js-eval \"'hello'.at(0)\")")
|
(eval "(js-eval \"'hello'.at(0)\")")
|
||||||
@@ -1909,6 +1927,16 @@ check 3707 "arr.toReversed" '"2,1,3"'
|
|||||||
check 3708 "arr.toSorted" '"1,1,3,4,5"'
|
check 3708 "arr.toSorted" '"1,1,3,4,5"'
|
||||||
check 3709 "arr.keys" '"0,1,2"'
|
check 3709 "arr.keys" '"0,1,2"'
|
||||||
|
|
||||||
|
# ── Phase 11.globals: NaN / Infinity / strict-eq ─────────────
|
||||||
|
check 3750 "typeof NaN" '"number"'
|
||||||
|
check 3751 "isNaN(NaN)" 'true'
|
||||||
|
check 3752 "isNaN(5)" 'false'
|
||||||
|
check 3753 "NaN === NaN" 'false'
|
||||||
|
check 3754 "Infinity > 100" 'true'
|
||||||
|
check 3755 "-Infinity < 0" 'true'
|
||||||
|
check 3756 "isFinite(1)" 'true'
|
||||||
|
check 3757 "isFinite(Infinity)" 'false'
|
||||||
|
|
||||||
# ── Phase 11.strmore: more String.prototype methods ───────────
|
# ── Phase 11.strmore: more String.prototype methods ───────────
|
||||||
check 3800 "'hello'.at(0)" '"h"'
|
check 3800 "'hello'.at(0)" '"h"'
|
||||||
check 3801 "'hello'.at(-1)" '"o"'
|
check 3801 "'hello'.at(-1)" '"o"'
|
||||||
|
|||||||
@@ -157,6 +157,8 @@
|
|||||||
(name)
|
(name)
|
||||||
(cond
|
(cond
|
||||||
((= name "undefined") (list (js-sym "quote") :js-undefined))
|
((= name "undefined") (list (js-sym "quote") :js-undefined))
|
||||||
|
((= name "NaN") (list (js-sym "js-nan-value")))
|
||||||
|
((= name "Infinity") (list (js-sym "js-infinity-value")))
|
||||||
(else (js-sym name)))))
|
(else (js-sym name)))))
|
||||||
|
|
||||||
;; ── Unary ops ─────────────────────────────────────────────────────
|
;; ── Unary ops ─────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user