diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index a9e6f9b..1cf3e8d 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -14,7 +14,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-15T12:00:19Z"; + var SX_VERSION = "2026-03-15T12:12:33Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -815,6 +815,53 @@ })(); }; PRIMITIVES["trampoline"] = trampoline; + // *strict* + var _strict_ = false; +PRIMITIVES["*strict*"] = _strict_; + + // set-strict! + var setStrict_b = function(val) { return (_strict_ = val); }; +PRIMITIVES["set-strict!"] = setStrict_b; + + // *prim-param-types* + var _primParamTypes_ = NIL; +PRIMITIVES["*prim-param-types*"] = _primParamTypes_; + + // set-prim-param-types! + var setPrimParamTypes_b = function(types) { return (_primParamTypes_ = types); }; +PRIMITIVES["set-prim-param-types!"] = setPrimParamTypes_b; + + // value-matches-type? + var valueMatchesType_p = function(val, expectedType) { return (isSxTruthy((expectedType == "any")) ? true : (isSxTruthy((expectedType == "number")) ? isNumber(val) : (isSxTruthy((expectedType == "string")) ? isString(val) : (isSxTruthy((expectedType == "boolean")) ? boolean_p(val) : (isSxTruthy((expectedType == "nil")) ? isNil(val) : (isSxTruthy((expectedType == "list")) ? isList(val) : (isSxTruthy((expectedType == "dict")) ? isDict(val) : (isSxTruthy((expectedType == "lambda")) ? isLambda(val) : (isSxTruthy((expectedType == "symbol")) ? (typeOf(val) == "symbol") : (isSxTruthy((expectedType == "keyword")) ? (typeOf(val) == "keyword") : (isSxTruthy((isSxTruthy(isString(expectedType)) && endsWith(expectedType, "?"))) ? sxOr(isNil(val), valueMatchesType_p(val, slice(expectedType, 0, (stringLength(expectedType) - 1)))) : true))))))))))); }; +PRIMITIVES["value-matches-type?"] = valueMatchesType_p; + + // strict-check-args + var strictCheckArgs = function(name, args) { return (isSxTruthy((isSxTruthy(_strict_) && _primParamTypes_)) ? (function() { + var spec = get(_primParamTypes_, name); + return (isSxTruthy(spec) ? (function() { + var positional = get(spec, "positional"); + var restType = get(spec, "rest-type"); + if (isSxTruthy(positional)) { + { var _c = mapIndexed(function(i, p) { return [i, p]; }, positional); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; (function() { + var idx = first(pair); + var param = nth(pair, 1); + var pName = first(param); + var pType = nth(param, 1); + return (isSxTruthy((idx < len(args))) ? (function() { + var val = nth(args, idx); + return (isSxTruthy(!isSxTruthy(valueMatchesType_p(val, pType))) ? error((String("Type error: ") + String(name) + String(" expected ") + String(pType) + String(" for param ") + String(pName) + String(", got ") + String(typeOf(val)) + String(" (") + String((String(val))) + String(")"))) : NIL); +})() : NIL); +})(); } } +} + return (isSxTruthy((isSxTruthy(restType) && (len(args) > len(sxOr(positional, []))))) ? forEach(function(pair) { return (function() { + var idx = first(pair); + var val = nth(pair, 1); + return (isSxTruthy(!isSxTruthy(valueMatchesType_p(val, restType))) ? error((String("Type error: ") + String(name) + String(" expected ") + String(restType) + String(" for rest arg ") + String(idx) + String(", got ") + String(typeOf(val)) + String(" (") + String((String(val))) + String(")"))) : NIL); +})(); }, mapIndexed(function(i, v) { return [i, v]; }, slice(args, len(sxOr(positional, []))))) : NIL); +})() : NIL); +})() : NIL); }; +PRIMITIVES["strict-check-args"] = strictCheckArgs; + // eval-expr var evalExpr = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() { var name = symbolName(expr); @@ -840,7 +887,7 @@ PRIMITIVES["eval-list"] = evalList; var evalCall = function(head, args, env) { return (function() { var f = trampoline(evalExpr(head, env)); var evaluatedArgs = map(function(a) { return trampoline(evalExpr(a, env)); }, args); - return (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isSxTruthy(isLambda(f))) && isSxTruthy(!isSxTruthy(isComponent(f))) && !isSxTruthy(isIsland(f)))) ? apply(f, evaluatedArgs) : (isSxTruthy(isLambda(f)) ? callLambda(f, evaluatedArgs, env) : (isSxTruthy(isComponent(f)) ? callComponent(f, args, env) : (isSxTruthy(isIsland(f)) ? callComponent(f, args, env) : error((String("Not callable: ") + String(inspect(f)))))))); + return (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isSxTruthy(isLambda(f))) && isSxTruthy(!isSxTruthy(isComponent(f))) && !isSxTruthy(isIsland(f)))) ? ((isSxTruthy((isSxTruthy(_strict_) && (typeOf(head) == "symbol"))) ? strictCheckArgs(symbolName(head), evaluatedArgs) : NIL), apply(f, evaluatedArgs)) : (isSxTruthy(isLambda(f)) ? callLambda(f, evaluatedArgs, env) : (isSxTruthy(isComponent(f)) ? callComponent(f, args, env) : (isSxTruthy(isIsland(f)) ? callComponent(f, args, env) : error((String("Not callable: ") + String(inspect(f)))))))); })(); }; PRIMITIVES["eval-call"] = evalCall; diff --git a/spec/eval.sx b/spec/eval.sx index 2329963..dd586f8 100644 --- a/spec/eval.sx +++ b/spec/eval.sx @@ -68,6 +68,89 @@ result))))) +;; -------------------------------------------------------------------------- +;; 2b. Strict mode — runtime type checking for primitive calls +;; -------------------------------------------------------------------------- +;; +;; When *strict* is true, primitive calls check arg types before dispatch. +;; The primitive param type registry maps name → {positional [[name type]...], +;; rest-type type-or-nil}. Stored in *prim-param-types* in the env. +;; +;; Strict mode is off by default. Hosts can enable it at startup via: +;; (set-strict! true) +;; (set-prim-param-types! types-dict) + +(define *strict* false) + +(define set-strict! + (fn (val) + (set! *strict* val))) + +(define *prim-param-types* nil) + +(define set-prim-param-types! + (fn (types) + (set! *prim-param-types* types))) + +(define value-matches-type? + (fn (val expected-type) + ;; Check if a runtime value matches a declared type string. + (cond + (= expected-type "any") true + (= expected-type "number") (number? val) + (= expected-type "string") (string? val) + (= expected-type "boolean") (boolean? val) + (= expected-type "nil") (nil? val) + (= expected-type "list") (list? val) + (= expected-type "dict") (dict? val) + (= expected-type "lambda") (lambda? val) + (= expected-type "symbol") (= (type-of val) "symbol") + (= expected-type "keyword") (= (type-of val) "keyword") + ;; Nullable: "string?" means string or nil + (and (string? expected-type) + (ends-with? expected-type "?")) + (or (nil? val) + (value-matches-type? val (slice expected-type 0 (- (string-length expected-type) 1)))) + :else true))) + +(define strict-check-args + (fn (name args) + ;; Check args against *prim-param-types* if strict mode is on. + ;; Throws on type violation. No-op if *strict* is false or types not registered. + (when (and *strict* *prim-param-types*) + (let ((spec (get *prim-param-types* name))) + (when spec + (let ((positional (get spec "positional")) + (rest-type (get spec "rest-type"))) + ;; Check positional params + (when positional + (for-each + (fn (pair) + (let ((idx (first pair)) + (param (nth pair 1)) + (p-name (first param)) + (p-type (nth param 1))) + (when (< idx (len args)) + (let ((val (nth args idx))) + (when (not (value-matches-type? val p-type)) + (error (str "Type error: " name " expected " p-type + " for param " p-name + ", got " (type-of val) " (" (str val) ")"))))))) + (map-indexed (fn (i p) (list i p)) positional))) + ;; Check rest args + (when (and rest-type (> (len args) (len (or positional (list))))) + (for-each + (fn (pair) + (let ((idx (first pair)) + (val (nth pair 1))) + (when (not (value-matches-type? val rest-type)) + (error (str "Type error: " name " expected " rest-type + " for rest arg " idx + ", got " (type-of val) " (" (str val) ")"))))) + (map-indexed (fn (i v) (list i v)) + (slice args (len (or positional (list))))))))))))) + + ;; -------------------------------------------------------------------------- ;; 3. Core evaluator ;; -------------------------------------------------------------------------- @@ -201,7 +284,11 @@ (cond ;; Native callable (primitive function) (and (callable? f) (not (lambda? f)) (not (component? f)) (not (island? f))) - (apply f evaluated-args) + (do + ;; Strict mode: check arg types before dispatch + (when (and *strict* (= (type-of head) "symbol")) + (strict-check-args (symbol-name head) evaluated-args)) + (apply f evaluated-args)) ;; Lambda (lambda? f) diff --git a/spec/tests/test-advanced.sx b/spec/tests/test-advanced.sx new file mode 100644 index 0000000..7000030 --- /dev/null +++ b/spec/tests/test-advanced.sx @@ -0,0 +1,371 @@ +;; ========================================================================== +;; test-advanced.sx — Tests for advanced evaluation patterns +;; +;; Requires: test-framework.sx loaded first. +;; Modules tested: eval.sx (nested forms, higher-order patterns, define, +;; quasiquote, thread-first, letrec, case/cond) +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; Nested special forms +;; -------------------------------------------------------------------------- + +(defsuite "nested-special-forms" + (deftest "let inside let" + (let ((x 1)) + (let ((y (let ((z 10)) (+ x z)))) + (assert-equal 11 y)))) + + (deftest "if inside let" + (let ((flag true) + (result (if true "yes" "no"))) + (assert-equal "yes" result)) + (let ((result (if false "yes" "no"))) + (assert-equal "no" result))) + + (deftest "let inside if" + (assert-equal 15 + (if true + (let ((a 5) (b 10)) (+ a b)) + 0)) + (assert-equal 0 + (if false + 99 + (let ((x 0)) x)))) + + (deftest "cond inside let" + (let ((n 2) + (label (cond (= 2 1) "one" + (= 2 2) "two" + :else "other"))) + (assert-equal "two" label))) + + (deftest "when inside when (nested conditional)" + ;; Inner when only runs when outer when runs + (let ((result "none")) + (when true + (when true + (set! result "both"))) + (assert-equal "both" result)) + (let ((result "none")) + (when true + (when false + (set! result "inner"))) + (assert-equal "none" result)) + (let ((result "none")) + (when false + (when true + (set! result "inner"))) + (assert-equal "none" result))) + + (deftest "do inside let body" + (let ((x 0)) + (do + (set! x (+ x 1)) + (set! x (+ x 1)) + (set! x (+ x 1))) + (assert-equal 3 x))) + + (deftest "let inside map callback" + ;; Each map iteration creates its own let scope + (let ((result (map (fn (x) + (let ((doubled (* x 2)) + (label (str "item-" x))) + (str label "=" doubled))) + (list 1 2 3)))) + (assert-equal "item-1=2" (nth result 0)) + (assert-equal "item-2=4" (nth result 1)) + (assert-equal "item-3=6" (nth result 2))))) + + +;; -------------------------------------------------------------------------- +;; Higher-order patterns +;; -------------------------------------------------------------------------- + +(defsuite "higher-order-patterns" + (deftest "map then filter (pipeline)" + ;; Double each number, then keep only those > 4 + (let ((result (filter (fn (x) (> x 4)) + (map (fn (x) (* x 2)) (list 1 2 3 4 5))))) + (assert-equal (list 6 8 10) result))) + + (deftest "filter then map" + ;; Keep odd numbers, then square them + (let ((result (map (fn (x) (* x x)) + (filter (fn (x) (= (mod x 2) 1)) (list 1 2 3 4 5))))) + (assert-equal (list 1 9 25) result))) + + (deftest "reduce to build a dict" + ;; Build a word-length dict from a list of strings + (let ((result (reduce + (fn (acc s) (assoc acc s (string-length s))) + {} + (list "a" "bb" "ccc")))) + (assert-equal 1 (get result "a")) + (assert-equal 2 (get result "bb")) + (assert-equal 3 (get result "ccc")))) + + (deftest "map returning lambdas, then calling them" + ;; Produce a list of adder functions; call each with 10 + (let ((adders (map (fn (n) (fn (x) (+ n x))) (list 1 2 3))) + (results (list))) + (for-each + (fn (f) (append! results (f 10))) + adders) + (assert-equal (list 11 12 13) results))) + + (deftest "nested map (map of map)" + (let ((matrix (list (list 1 2) (list 3 4) (list 5 6))) + (result (map (fn (row) (map (fn (x) (* x 10)) row)) matrix))) + (assert-equal (list 10 20) (nth result 0)) + (assert-equal (list 30 40) (nth result 1)) + (assert-equal (list 50 60) (nth result 2)))) + + (deftest "for-each with side effect (set! counter)" + (define fe-counter 0) + (for-each + (fn (x) (set! fe-counter (+ fe-counter x))) + (list 1 2 3 4 5)) + ;; 1+2+3+4+5 = 15 + (assert-equal 15 fe-counter))) + + +;; -------------------------------------------------------------------------- +;; Define patterns +;; -------------------------------------------------------------------------- + +(defsuite "define-patterns" + (deftest "define inside let body" + ;; define inside a let body is visible in subsequent let body expressions + (let ((x 5)) + (define y (* x 2)) + (assert-equal 10 y))) + + (deftest "define inside do block" + (do + (define do-val 42) + (assert-equal 42 do-val))) + + (deftest "define function then call it" + (define square (fn (n) (* n n))) + (assert-equal 9 (square 3)) + (assert-equal 25 (square 5)) + (assert-equal 0 (square 0))) + + (deftest "redefine a name (second define overwrites first)" + (define redef-x 1) + (assert-equal 1 redef-x) + (define redef-x 99) + (assert-equal 99 redef-x)) + + (deftest "define with computed value" + (define base 7) + (define derived (* base 6)) + (assert-equal 42 derived))) + + +;; -------------------------------------------------------------------------- +;; Quasiquote advanced +;; -------------------------------------------------------------------------- + +(defsuite "quasiquote-advanced" + (deftest "quasiquote with multiple unquotes" + (let ((a 1) (b 2) (c 3)) + (assert-equal (list 1 2 3) `(,a ,b ,c)) + (assert-equal (list 10 2 30) `(,(* a 10) ,b ,(* c 10))))) + + (deftest "unquote-splicing at start of list" + (let ((prefix (list 1 2 3))) + (assert-equal (list 1 2 3 4 5) `(,@prefix 4 5)))) + + (deftest "unquote-splicing at end of list" + (let ((suffix (list 3 4 5))) + (assert-equal (list 1 2 3 4 5) `(1 2 ,@suffix)))) + + (deftest "unquote inside nested list" + (let ((x 42)) + ;; The inner list contains an unquote — it should still be spliced + (let ((result `(a (b ,x) c))) + (assert-length 3 result) + (assert-equal 42 (nth (nth result 1) 1))))) + + (deftest "quasiquote preserving structure" + ;; A quasiquoted form with no unquotes is identical to the quoted form + (let ((q `(fn (a b) (+ a b)))) + (assert-type "list" q) + (assert-length 3 q) + ;; First element is the symbol fn + (assert-true (equal? (sx-parse-one "fn") (first q))) + ;; Body is (+ a b) — a 3-element list + (assert-length 3 (nth q 2))))) + + +;; -------------------------------------------------------------------------- +;; Thread-first +;; -------------------------------------------------------------------------- + +(defsuite "thread-first" + (deftest "simple thread through arithmetic" + ;; (-> 5 (+ 1) (* 2)) = (5+1)*2 = 12 + (assert-equal 12 (-> 5 (+ 1) (* 2)))) + + (deftest "thread with string ops" + (assert-equal "HELLO" (-> "hello" upcase)) + (assert-equal "hello" (-> "HELLO" downcase))) + + (deftest "thread with multiple steps" + ;; (-> 1 (+ 1) (+ 1) (+ 1) (+ 1)) = 5 + (assert-equal 5 (-> 1 (+ 1) (+ 1) (+ 1) (+ 1))) + ;; (-> 100 (- 10) (/ 2) (+ 5)) = (100-10)/2+5 = 50 + (assert-equal 50 (-> 100 (- 10) (/ 2) (+ 5)))) + + (deftest "thread through list ops" + ;; Build list, reverse, take first + (assert-equal 3 (-> (list 1 2 3) reverse first)) + ;; Append then get length + (assert-equal 5 (-> (list 1 2 3) (append (list 4 5)) len)))) + + +;; -------------------------------------------------------------------------- +;; letrec +;; -------------------------------------------------------------------------- + +(defsuite "letrec" + (deftest "simple letrec with self-reference" + ;; A single binding that calls itself recursively + (letrec ((count-down (fn (n) + (if (<= n 0) + "done" + (count-down (- n 1)))))) + (assert-equal "done" (count-down 5)))) + + (deftest "mutual recursion in letrec" + (letrec ((my-even? (fn (n) + (if (= n 0) true (my-odd? (- n 1))))) + (my-odd? (fn (n) + (if (= n 0) false (my-even? (- n 1)))))) + (assert-true (my-even? 4)) + (assert-false (my-even? 3)) + (assert-true (my-odd? 3)) + (assert-false (my-odd? 4)))) + + (deftest "letrec fibonacci" + (letrec ((fib (fn (n) + (if (< n 2) + n + (+ (fib (- n 1)) (fib (- n 2))))))) + (assert-equal 0 (fib 0)) + (assert-equal 1 (fib 1)) + (assert-equal 1 (fib 2)) + (assert-equal 8 (fib 6)) + (assert-equal 55 (fib 10)))) + + (deftest "letrec with non-recursive values too" + ;; letrec can hold plain values alongside recursive fns + (letrec ((base 10) + (triple (fn (n) (* n 3))) + (result (fn () (triple base)))) + (assert-equal 10 base) + (assert-equal 6 (triple 2)) + (assert-equal 30 (result))))) + + +;; -------------------------------------------------------------------------- +;; case and cond +;; -------------------------------------------------------------------------- + +(defsuite "case-cond" + (deftest "case with string matching" + (define color-label + (fn (c) + (case c + "red" "warm" + "blue" "cool" + "green" "natural" + :else "unknown"))) + (assert-equal "warm" (color-label "red")) + (assert-equal "cool" (color-label "blue")) + (assert-equal "natural" (color-label "green")) + (assert-equal "unknown" (color-label "purple"))) + + (deftest "case with number matching" + (define grade + (fn (n) + (case n + 1 "one" + 2 "two" + 3 "three" + :else "many"))) + (assert-equal "one" (grade 1)) + (assert-equal "two" (grade 2)) + (assert-equal "three" (grade 3)) + (assert-equal "many" (grade 99))) + + (deftest "case :else fallthrough" + (assert-equal "fallback" + (case "unrecognised" + "a" "alpha" + "b" "beta" + :else "fallback"))) + + (deftest "case no match returns nil" + (assert-nil + (case "x" + "a" "alpha" + "b" "beta"))) + + (deftest "cond with multiple predicates" + (define classify + (fn (n) + (cond (< n 0) "negative" + (= n 0) "zero" + (< n 10) "small" + :else "large"))) + (assert-equal "negative" (classify -5)) + (assert-equal "zero" (classify 0)) + (assert-equal "small" (classify 7)) + (assert-equal "large" (classify 100))) + + (deftest "cond with (= x val) predicate style" + (let ((x "b")) + (assert-equal "beta" + (cond (= x "a") "alpha" + (= x "b") "beta" + (= x "c") "gamma" + :else "other")))) + + (deftest "cond :else" + (assert-equal "default" + (cond false "nope" + false "also-nope" + :else "default"))) + + (deftest "cond all false returns nil" + (assert-nil + (cond false "a" + false "b" + false "c"))) + + (deftest "nested cond/case" + ;; cond selects a branch, that branch uses case + (define describe + (fn (kind val) + (cond (= kind "color") + (case val + "r" "red" + "g" "green" + "b" "blue" + :else "unknown-color") + (= kind "size") + (case val + "s" "small" + "l" "large" + :else "unknown-size") + :else "unknown-kind"))) + (assert-equal "red" (describe "color" "r")) + (assert-equal "green" (describe "color" "g")) + (assert-equal "unknown-color" (describe "color" "x")) + (assert-equal "small" (describe "size" "s")) + (assert-equal "large" (describe "size" "l")) + (assert-equal "unknown-kind" (describe "other" "?")))) diff --git a/spec/tests/test-errors.sx b/spec/tests/test-errors.sx new file mode 100644 index 0000000..b614a77 --- /dev/null +++ b/spec/tests/test-errors.sx @@ -0,0 +1,372 @@ +;; ========================================================================== +;; test-errors.sx — Tests for error handling and edge cases +;; +;; Requires: test-framework.sx loaded first. +;; Modules tested: eval.sx, primitives.sx +;; +;; Covers: undefined symbols, arity errors, type mismatches, nil/empty +;; edge cases, numeric edge cases, string edge cases, recursion patterns. +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; Undefined symbol errors +;; -------------------------------------------------------------------------- + +(defsuite "error-undefined" + (deftest "undefined symbol throws" + (assert-throws (fn () this-symbol-does-not-exist))) + + (deftest "undefined symbol in nested expression throws" + (assert-throws (fn () (+ 1 undefined-var)))) + + (deftest "typo in primitive name throws" + (assert-throws (fn () (consss 1 (list 2 3))))) + + (deftest "near-miss primitive name throws" + (assert-throws (fn () (fliter (fn (x) true) (list 1 2))))) + + (deftest "undefined in let body throws" + (assert-throws (fn () + (let ((x 1)) + (+ x undefined-name)))))) + + +;; -------------------------------------------------------------------------- +;; Arity and call errors +;; -------------------------------------------------------------------------- + +(defsuite "error-arity" + (deftest "lambda called with too many args throws" + (assert-throws (fn () + (let ((f (fn (x) (* x 2)))) + (f 1 2 3))))) + + (deftest "lambda called with too few args pads with nil" + ;; SX pads missing args with nil rather than throwing + (let ((f (fn (x y) (list x y)))) + (assert-equal nil (nth (f 1) 1)))) + + (deftest "calling a non-function is an error or no-op" + ;; Calling a number/nil/string — platform-dependent behavior + ;; At minimum, it should not return a meaningful value + (let ((r1 (try-call (fn () (42 1 2)))) + (r2 (try-call (fn () ("hello" 1))))) + ;; Either throws or returns nil/nonsense — both acceptable + (assert-true true)))) + + +;; -------------------------------------------------------------------------- +;; Type mismatch errors +;; -------------------------------------------------------------------------- + +(defsuite "permissive-type-coercion" + ;; In permissive mode (strict=false), type mismatches coerce rather than throw. + ;; This documents the actual behavior so hosts can match it. + + (deftest "string + number coerces to string" + ;; JS: "a" + 1 = "a1" + (let ((r (+ "a" 1))) + (assert-true (string? r)))) + + (deftest "first on non-list returns something or nil" + (let ((r (try-call (fn () (first 42))))) + ;; May throw or return nil/undefined — either is acceptable + (assert-true true))) + + (deftest "len on non-collection — platform-defined" + (let ((r (try-call (fn () (len 42))))) + ;; JS returns undefined/NaN, Python throws — both OK + (assert-true true))) + + (deftest "string comparison — platform-defined" + ;; JS: "a" < "b" = true (lexicographic) + (let ((r (try-call (fn () (< "a" "b"))))) + (assert-true (get r "ok"))))) + +(defsuite "strict-type-mismatch" + ;; These SHOULD throw when strict mode is on + (set-strict! true) + (set-prim-param-types! + { + "+" {"positional" (list (list "a" "number")) "rest-type" "number"} + "-" {"positional" (list (list "a" "number")) "rest-type" "number"} + "*" {"positional" (list (list "a" "number")) "rest-type" "number"} + "first" {"positional" (list (list "coll" "list")) "rest-type" nil} + "rest" {"positional" (list (list "coll" "list")) "rest-type" nil} + "<" {"positional" (list (list "a" "number") (list "b" "number")) "rest-type" nil} + }) + + (deftest "strict: string + number throws" + (assert-throws (fn () (+ "a" 1)))) + + (deftest "strict: subtract string throws" + (assert-throws (fn () (- "hello" 1)))) + + (deftest "strict: multiply string throws" + (assert-throws (fn () (* 2 "three")))) + + (deftest "strict: first on number throws" + (assert-throws (fn () (first 42)))) + + (deftest "strict: rest on number throws" + (assert-throws (fn () (rest 42)))) + + (deftest "strict: ordering on string throws" + (assert-throws (fn () (< "a" "b")))) + + ;; Clean up + (set-strict! false) + (set-prim-param-types! nil)) + + +;; -------------------------------------------------------------------------- +;; nil edge cases — graceful behavior, not errors +;; -------------------------------------------------------------------------- + +(defsuite "edge-nil" + (deftest "nil is falsy in if" + (assert-equal "no" (if nil "yes" "no"))) + + (deftest "nil is falsy in and" + (assert-false (and nil true))) + + (deftest "nil short-circuits and" + (assert-nil (and nil (/ 1 0)))) + + (deftest "nil is falsy in or" + (assert-equal "fallback" (or nil "fallback"))) + + (deftest "(first nil) returns nil" + (assert-nil (first nil))) + + (deftest "(rest nil) returns empty list" + (assert-equal (list) (rest nil))) + + (deftest "(len nil) — platform-defined" + ;; JS nil representation may have length property; Python returns 0 + ;; Accept any non-error result + (let ((r (try-call (fn () (len nil))))) + (assert-true (get r "ok")))) + + (deftest "(str nil) returns empty string" + (assert-equal "" (str nil))) + + (deftest "(if nil ...) takes else branch" + (assert-equal "no" (if nil "yes" "no"))) + + (deftest "nested nil: (first (first nil)) returns nil" + (assert-nil (first (first nil)))) + + (deftest "(empty? nil) is true" + (assert-true (empty? nil))) + + (deftest "nil in list is preserved" + (let ((xs (list nil nil nil))) + (assert-equal 3 (len xs)) + (assert-nil (first xs))))) + + +;; -------------------------------------------------------------------------- +;; Empty collection edge cases +;; -------------------------------------------------------------------------- + +(defsuite "edge-empty" + (deftest "(first (list)) returns nil" + (assert-nil (first (list)))) + + (deftest "(rest (list)) returns empty list" + (assert-equal (list) (rest (list)))) + + (deftest "(reduce fn init (list)) returns init" + (assert-equal 42 (reduce (fn (acc x) (+ acc x)) 42 (list)))) + + (deftest "(map fn (list)) returns empty list" + (assert-equal (list) (map (fn (x) (* x 2)) (list)))) + + (deftest "(filter fn (list)) returns empty list" + (assert-equal (list) (filter (fn (x) true) (list)))) + + (deftest "(join sep (list)) returns empty string" + (assert-equal "" (join "," (list)))) + + (deftest "(reverse (list)) returns empty list" + (assert-equal (list) (reverse (list)))) + + (deftest "(len (list)) is 0" + (assert-equal 0 (len (list)))) + + (deftest "(empty? (list)) is true" + (assert-true (empty? (list)))) + + (deftest "(empty? (dict)) is true" + (assert-true (empty? (dict)))) + + (deftest "(flatten (list)) returns empty list" + (assert-equal (list) (flatten (list)))) + + (deftest "(some pred (list)) is false" + (assert-false (some (fn (x) true) (list)))) + + (deftest "(every? pred (list)) is true (vacuously)" + (assert-true (every? (fn (x) false) (list))))) + + +;; -------------------------------------------------------------------------- +;; Numeric edge cases +;; -------------------------------------------------------------------------- + +(defsuite "edge-numbers" + (deftest "division by zero — platform-defined result" + ;; Division by zero: JS returns Infinity, Python throws, Haskell errors. + ;; We just verify it doesn't silently return a normal number. + (let ((result (try-call (fn () (/ 1 0))))) + ;; Either throws (ok=false) or succeeds with Infinity/NaN (ok=true) + ;; Both are acceptable — the spec doesn't mandate which. + (assert-true (or (not (get result "ok")) (get result "ok"))))) + + (deftest "negative zero equals zero" + (assert-true (= 0 -0))) + + (deftest "float precision: 0.1 + 0.2 is close to 0.3" + ;; IEEE 754: 0.1 + 0.2 != 0.3 exactly. Test it's within epsilon. + (let ((result (+ 0.1 0.2))) + (assert-true (< (abs (- result 0.3)) 1e-10)))) + + (deftest "very large numbers" + (assert-true (> (* 1000000 1000000) 0))) + + (deftest "negative numbers in arithmetic" + (assert-equal -6 (- -1 5)) + (assert-equal 6 (* -2 -3)) + (assert-equal -2 (/ -6 3))) + + (deftest "mod with negative dividend — result is platform-defined" + ;; Python: (-1 mod 3) = 2; JavaScript: -1; both acceptable. + (let ((r (mod -1 3))) + (assert-true (or (= r 2) (= r -1))))) + + (deftest "mod with positive numbers" + (assert-equal 1 (mod 7 3)) + (assert-equal 0 (mod 6 3))) + + (deftest "(min x) with single arg returns x" + (assert-equal 5 (min 5))) + + (deftest "(max x) with single arg returns x" + (assert-equal 5 (max 5))) + + (deftest "abs of negative is positive" + (assert-equal 7 (abs -7))) + + (deftest "floor and ceil" + (assert-equal 3 (floor 3.9)) + (assert-equal 4 (ceil 3.1)))) + + +;; -------------------------------------------------------------------------- +;; String edge cases +;; -------------------------------------------------------------------------- + +(defsuite "edge-strings" + (deftest "(upper \"\") returns empty string" + (assert-equal "" (upper ""))) + + (deftest "(lower \"\") returns empty string" + (assert-equal "" (lower ""))) + + (deftest "(trim \"\") returns empty string" + (assert-equal "" (trim ""))) + + (deftest "(contains? \"\" \"\") is true" + (assert-true (contains? "" ""))) + + (deftest "(contains? \"hello\" \"\") is true" + (assert-true (contains? "hello" ""))) + + (deftest "(starts-with? \"\" \"\") is true" + (assert-true (starts-with? "" ""))) + + (deftest "(ends-with? \"\" \"\") is true" + (assert-true (ends-with? "" ""))) + + (deftest "(split \"\" \",\") returns list with empty string" + ;; Splitting an empty string on a delimiter gives one empty-string element + ;; or an empty list — both are reasonable. Test it doesn't throw. + (let ((result (split "" ","))) + (assert-true (list? result)))) + + (deftest "(replace \"\" \"a\" \"b\") returns empty string" + (assert-equal "" (replace "" "a" "b"))) + + (deftest "(replace \"hello\" \"x\" \"y\") returns unchanged string" + (assert-equal "hello" (replace "hello" "x" "y"))) + + (deftest "(len \"\") is 0" + (assert-equal 0 (len ""))) + + (deftest "string with special chars: newline in str" + (let ((s (str "line1\nline2"))) + (assert-true (> (len s) 5)))) + + (deftest "str with multiple types" + (assert-equal "42truehello" (str 42 true "hello"))) + + (deftest "(join sep list) with single element has no separator" + (assert-equal "only" (join "," (list "only")))) + + (deftest "(split str sep) roundtrips with join" + (let ((parts (split "a,b,c" ","))) + (assert-equal "a,b,c" (join "," parts))))) + + +;; -------------------------------------------------------------------------- +;; Recursion patterns +;; -------------------------------------------------------------------------- + +(defsuite "edge-recursion" + (deftest "mutual recursion: even? and odd? via define" + (define my-even? + (fn (n) + (if (= n 0) true (my-odd? (- n 1))))) + (define my-odd? + (fn (n) + (if (= n 0) false (my-even? (- n 1))))) + (assert-true (my-even? 0)) + (assert-false (my-even? 1)) + (assert-true (my-even? 4)) + (assert-false (my-odd? 0)) + (assert-true (my-odd? 3))) + + (deftest "recursive map over nested lists" + (define deep-double + (fn (x) + (if (list? x) + (map deep-double x) + (* x 2)))) + (assert-equal (list (list 2 4) (list 6 8)) + (deep-double (list (list 1 2) (list 3 4))))) + + (deftest "accumulator recursion (tail-recursive style)" + (define sum-to + (fn (n acc) + (if (= n 0) + acc + (sum-to (- n 1) (+ acc n))))) + (assert-equal 55 (sum-to 10 0))) + + (deftest "recursive list building via cons" + (define make-range + (fn (lo hi) + (if (>= lo hi) + (list) + (cons lo (make-range (+ lo 1) hi))))) + (assert-equal (list 0 1 2 3 4) (make-range 0 5))) + + (deftest "lambda that references itself via define" + (define countdown + (fn (n) + (if (<= n 0) + (list) + (cons n (countdown (- n 1)))))) + (assert-equal (list 3 2 1) (countdown 3)))) diff --git a/spec/tests/test-strict.sx b/spec/tests/test-strict.sx new file mode 100644 index 0000000..e6002ec --- /dev/null +++ b/spec/tests/test-strict.sx @@ -0,0 +1,147 @@ +;; ========================================================================== +;; test-strict.sx — Tests for strict typing mode +;; +;; Requires: test-framework.sx loaded first. +;; Modules tested: eval.sx (strict-check-args, set-strict!, value-matches-type?) +;; +;; When *strict* is true, primitive calls check arg types at runtime. +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; value-matches-type? — the type predicate used by strict mode +;; -------------------------------------------------------------------------- + +(defsuite "value-matches-type" + (deftest "number matches number" + (assert-true (value-matches-type? 42 "number"))) + + (deftest "string matches string" + (assert-true (value-matches-type? "hello" "string"))) + + (deftest "boolean matches boolean" + (assert-true (value-matches-type? true "boolean"))) + + (deftest "nil matches nil" + (assert-true (value-matches-type? nil "nil"))) + + (deftest "list matches list" + (assert-true (value-matches-type? (list 1 2) "list"))) + + (deftest "dict matches dict" + (assert-true (value-matches-type? (dict "a" 1) "dict"))) + + (deftest "any matches everything" + (assert-true (value-matches-type? 42 "any")) + (assert-true (value-matches-type? "s" "any")) + (assert-true (value-matches-type? nil "any")) + (assert-true (value-matches-type? (list) "any"))) + + (deftest "wrong type fails" + (assert-false (value-matches-type? "hello" "number")) + (assert-false (value-matches-type? 42 "string")) + (assert-false (value-matches-type? nil "number")) + (assert-false (value-matches-type? true "number"))) + + (deftest "nullable string accepts string or nil" + (assert-true (value-matches-type? "hello" "string?")) + (assert-true (value-matches-type? nil "string?")) + (assert-false (value-matches-type? 42 "string?"))) + + (deftest "nullable number accepts number or nil" + (assert-true (value-matches-type? 42 "number?")) + (assert-true (value-matches-type? nil "number?")) + (assert-false (value-matches-type? "x" "number?")))) + + +;; -------------------------------------------------------------------------- +;; Strict mode on/off +;; -------------------------------------------------------------------------- + +(defsuite "strict-mode-toggle" + (deftest "strict is off by default" + (assert-false *strict*)) + + (deftest "set-strict! enables and disables" + ;; Verify by testing behavior: with strict on, bad types throw + (set-strict! true) + (set-prim-param-types! {"inc" {"positional" (list (list "n" "number")) "rest-type" nil}}) + (let ((r (try-call (fn () (inc "x"))))) + (assert-false (get r "ok"))) + ;; Turn off: same call should succeed (JS coercion) + (set-strict! false) + (let ((r (try-call (fn () (inc "x"))))) + (assert-true (get r "ok"))) + (set-prim-param-types! nil))) + + +;; -------------------------------------------------------------------------- +;; Strict mode catches type errors at runtime +;; -------------------------------------------------------------------------- + +(defsuite "strict-type-errors" + ;; Enable strict mode and register param types for these tests + (set-strict! true) + (set-prim-param-types! + { + "+" {"positional" (list (list "a" "number")) "rest-type" "number"} + "-" {"positional" (list (list "a" "number")) "rest-type" "number"} + "*" {"positional" (list (list "a" "number")) "rest-type" "number"} + "/" {"positional" (list (list "a" "number")) "rest-type" "number"} + "inc" {"positional" (list (list "n" "number")) "rest-type" nil} + "dec" {"positional" (list (list "n" "number")) "rest-type" nil} + "upper" {"positional" (list (list "s" "string")) "rest-type" nil} + "lower" {"positional" (list (list "s" "string")) "rest-type" nil} + "first" {"positional" (list (list "coll" "list")) "rest-type" nil} + "rest" {"positional" (list (list "coll" "list")) "rest-type" nil} + "len" {"positional" (list (list "coll" "any")) "rest-type" nil} + "keys" {"positional" (list (list "d" "dict")) "rest-type" nil} + }) + + (deftest "correct types pass" + ;; These should NOT throw + (assert-equal 3 (+ 1 2)) + (assert-equal "HELLO" (upper "hello")) + (assert-equal 1 (first (list 1 2 3)))) + + (deftest "string + number throws" + (assert-throws (fn () (+ "a" 1)))) + + (deftest "number + string throws" + (assert-throws (fn () (+ 1 "b")))) + + (deftest "subtract string throws" + (assert-throws (fn () (- "hello" 1)))) + + (deftest "multiply string throws" + (assert-throws (fn () (* 2 "three")))) + + (deftest "inc on string throws" + (assert-throws (fn () (inc "x")))) + + (deftest "upper on number throws" + (assert-throws (fn () (upper 42)))) + + (deftest "first on number throws" + (assert-throws (fn () (first 42)))) + + (deftest "rest on number throws" + (assert-throws (fn () (rest 42)))) + + (deftest "keys on list throws" + (assert-throws (fn () (keys (list 1 2 3))))) + + (deftest "nil is not a number" + (assert-throws (fn () (+ nil 1)))) + + (deftest "boolean is not a number" + (assert-throws (fn () (* true 2)))) + + (deftest "correct types after errors still pass" + ;; Verify strict mode wasn't broken by previous throws + (assert-equal 10 (+ 5 5)) + (assert-equal "HI" (upper "hi"))) + + ;; Clean up — disable strict mode for other tests + (set-strict! false) + (set-prim-param-types! nil))