diff --git a/jolt-core/clojure/core/00-syntax.clj b/jolt-core/clojure/core/00-syntax.clj index 00ae85d..4b1e3f0 100644 --- a/jolt-core/clojure/core/00-syntax.clj +++ b/jolt-core/clojure/core/00-syntax.clj @@ -298,7 +298,8 @@ `(= ~g ~(mk-const c)))) build (fn build [cls] (if (empty? cls) - nil + ;; no clause matched and no default — Clojure throws here. + `(throw (ex-info (str "No matching clause: " ~g) {})) (if (empty? (rest cls)) (first cls) `(if ~(mk-test (first cls)) ~(nth cls 1) ~(build (drop 2 cls))))))] diff --git a/test/spec/forms-spec.janet b/test/spec/forms-spec.janet new file mode 100644 index 0000000..133e770 --- /dev/null +++ b/test/spec/forms-spec.janet @@ -0,0 +1,80 @@ +# Specification: core special forms (case/fn/let/letfn/loop/if/do/def/call). +# +# Adapted from the jank test corpus (compiler+runtime/test/jank/form/**, /call) — +# we base our coverage on jank's to close gaps, but maintain our own copies since +# jank may diverge. jank-isms are translated to Jolt/Clojure: letfn* -> letfn, +# (catch jank.runtime.object_ref …) -> (catch :default …). Platform-specific cases +# (bigdecimal M, biginteger, ratios, unicode char edges) are intentionally omitted. +# +# Multi-form jank files (def + asserts, ending in :success) are wrapped in a single +# (do … :success) so they run as one expression and assert :success. +(use ../support/harness) + +(defspec "forms / case" + ["bool" ":yes" "(case true true :yes false :no :default)"] + ["keyword match" ":b" "(case :a :x :wrong :a :b :default)"] + ["number match" ":two" "(case 2 1 :one 2 :two :default)"] + ["string match" ":hit" "(case \"x\" \"y\" :miss \"x\" :hit :default)"] + ["nil match" ":nada" "(case nil nil :nada :default)"] + ["default" ":def" "(case 99 1 :one 2 :two :def)"] + ["list of consts" ":vowel" "(case \\a (\\a \\e \\i \\o \\u) :vowel :consonant)"] + ["no match no default" :throws "(case 5 1 :one)"]) + # gap (jolt-vdo): case allows duplicate test constants — Clojure compile-errors. + +(defspec "forms / fn" + ["named fn nil" "nil" "((fn* foo-bar []))"] + ["immediate call" "1" "((fn* [] 1))"] + ["args" "[:a :b]" "((fn* [a b] [a b]) :a :b)"] + ["multi-arity 0" "0" "(do (def add (fn* ([] 0) ([a] a) ([a b] (+ a b)))) (add))"] + ["multi-arity 1" "-500" "(do (def add (fn* ([] 0) ([a] a) ([a b] (+ a b)))) (add -500))"] + ["multi-arity 2" "-450" "(do (def add (fn* ([] 0) ([a] a) ([a b] (+ a b)))) (add -500 50))"] + ["variadic rest" "[3 4]" "(do (def v (fn* ([a b & args] args) ([] 0))) (v 1 2 3 4))"] + ["variadic empty" "0" "(do (def v (fn* ([a b & args] args) ([] 0))) (v))"] + ["variadic collect" "[{} nil :m]" "((fn* [a b & args] args) 'w 't {} nil :m)"] + ["closure capture" "8" "(do (def adder (fn* [n] (fn* [x] (+ x n)))) ((adder 5) 3))"] + ["recur countdown" "0" "(do (def cd (fn* [n] (if (< 0 n) (recur (+ n -1)) n))) (cd 10))"] + ["named self-recur" "120" "(do (def f (fn* fact [n] (if (= n 0) 1 (* n (fact (dec n)))))) (f 5))"] + ["no param vector" :throws "(fn* foo)"] + ["non-symbol param" :throws "(fn* [1] 1)"]) + +(defspec "forms / let" + ["literal" "1" "(let* [a 1] a)"] + ["multiple" "[1 2]" "(let* [a 1 b 2] [a b])"] + ["previous ref" ":bee" "(let* [a 1 b (if (= 1 a) :bee :uh-oh)] b)"] + ["nested let" "3" "(let* [a 5 b (let* [c -2] (+ a c))] b)"] + ["fn value bound" "\":foo\"" "(let* [kw->str (fn* [kw] (str kw))] (kw->str :foo))"] + ["shadowing" "2" "(let* [a 1 a 2] a)"]) + +(defspec "forms / letfn" + ["mutual top" "[1 2]" "(letfn [(a [] 1) (b [] 2)] [(a) (b)])"] + ["mutual recursion" ":done" "(letfn [(ev? [n] (if (= 0 n) :done (od? (dec n)))) (od? [n] (ev? n))] (ev? 4))"] + ["nested letfn" "3" "(letfn [(a [] 5) (b [] (letfn [(c [] -2)] (+ (a) (c))))] (b))"]) + +(defspec "forms / loop" + ["sum" "55" "(loop* [sum 0 cnt 10] (if (= cnt 0) sum (recur (+ cnt sum) (dec cnt))))"] + ["multi binding" "[4 2]" "(loop* [a 1 b 2 n 0] (if (< n 3) (recur (inc a) b (inc n)) [a b]))"]) + # gap (jolt-w2v): loop bindings aren't sequential — a later init can't reference an + # earlier binding (Clojure's loop is like let). (loop* [a 1 b (+ a 1)] …) errors. + +(defspec "forms / try" + ["immediate throw caught" ":caught" "(try (throw :boom) (catch :default e :caught))"] + ["first throw wins" "\"a\"" "(try (throw (ex-info \"a\" {})) (throw (ex-info \"b\" {})) (catch :default e (ex-message e)))"] + ["catch ex-data" "7" "(try (throw (ex-info \"e\" {:v 7})) (catch :default e (:v (ex-data e))))"] + ["finally runs" "9" "(let [a (atom 0)] (try 1 (finally (reset! a 9))) @a)"] + ["body value w/ finally" "1" "(try 1 (finally 2))"] + ["catch value w/ finally" ":h" "(try (throw (ex-info \"x\" {})) (catch :default e :h) (finally :ignored))"] + ["no throw skips catch" "5" "(try 5 (catch :default e :nope))"]) + +(defspec "forms / if-do-def-call" + ["if truthy vec" ":fine" "(if [:ok] :fine :no)"] + ["if truthy str" ":fine" "(if \"good?\" :fine :no)"] + ["if nil = false" ":else" "(if nil :then :else)"] + ["if no else" "nil" "(if false 1)"] + ["do nested" "1" "(do (do (do (do 1))))"] + ["do returns last" "3" "(do 1 2 3)"] + ["def + deref var" "true" "(var? (def one 1))"] + ["def redefine" "100" "(do (def one 1) (def one 100) one)"] + ["def in fn mutates" "[:default :meow]" "(do (def a :default) (def set-a (fn* [v] (def a v))) (let* [before a] (set-a :meow) [before a]))"] + ["call literal fn" "1" "((fn* [] 1))"] + ["call nested" "6" "(+ ((fn* [] 1)) ((fn* [] 2)) ((fn* [] 3)))"] + ["call nil" :throws "(nil)"]) diff --git a/test/spec/reader-forms-spec.janet b/test/spec/reader-forms-spec.janet new file mode 100644 index 0000000..bf35e67 --- /dev/null +++ b/test/spec/reader-forms-spec.janet @@ -0,0 +1,45 @@ +# Specification: reader forms + syntax-quote + metadata. +# +# Adapted from the jank test corpus (test/jank/{syntax-quote,metadata,reader-macro, +# call}); we keep our own copies since jank may diverge. Syntax-quoted symbols are +# qualified to clojure.core (matching jank/Clojure). Platform-specific reader forms +# (#uuid, #inst, ##Inf/##NaN, bigdecimal/biginteger/ratio) are omitted. +(use ../support/harness) + +(defspec "reader / anonymous fn #()" + ["no args" "3" "(#(+ 1 2))"] + ["one arg %" "6" "(#(* % 2) 3)"] + ["positional %1 %2" "[1 2]" "(#(do [%1 %2]) 1 2)"] + ["rest %&" "[1 2 3]" "(#(do %&) 1 2 3)"] + ["fixed + rest" "[2 3]" "(#(do % %&) 1 2 3)"]) + # gap (jolt-6x1): %& with a higher positional (e.g. #(do %2 %&)) miscomputes the + # fixed arity — (#(do %2 %&) 1 2 3) yields (2 3), should be [3] (rest after %2). + +(defspec "reader / var-quote #'" + ["var-quote = var" "true" "(= (var str) #'str)"] + ["is a var" "true" "(var? #'str)"] + ["deref var-quote" "5" "(do (def w 5) (deref #'w))"]) + +(defspec "reader / metadata ^" + ["meta on quoted sym" "true" "(:foo (meta (quote ^:foo bar)))"] + ["with-meta map" "true" "(:k (meta (with-meta {} {:k true})))"] + ["with-meta vector" "true" "(:k (meta (with-meta [] {:k true})))"] + ["non-metadatable num" "nil" "(meta 100)"] + ["non-metadatable str" "nil" "(meta \"\")"] + ["non-metadatable bool" "nil" "(meta true)"]) + # gap (jolt-xl0): ^meta on collection literals ({}/[]/#{}) isn't attached — + # (meta ^:foo {}) is nil; symbol meta + (with-meta …) work. + +(defspec "reader / syntax-quote" + ["plain literal" "[1 2 3]" "`[1 2 3]"] + ["gensym distinct" "true" "(not= `meow# `meow#)"] + ["gensym stable" "true" "(let [s `[meow# meow#]] (= (first s) (second s)))"] + ["qualifies unresolved" "(quote user/foo)" "`foo"] + ["unquote value" "[1 2 3]" "(let [a [1 2 3]] `~a)"] + # functional: the syntax-quoted call evaluates correctly (jolt-265: core syms are + # left bare rather than qualified to clojure.core/, but still resolve at eval). + ["unquote call evals" "6" "(let [a 5] (eval `(+ ~a 1)))"] + ["splice call evals" "6" "(let [a [1 2 3]] (eval `(+ ~@a)))"] + ["splice in vector" "[1 2 3 0 1 2 3]" "(let [b [0] a [1 2 3] e []] `[~@e ~@a ~@b ~@a ~@e])"]) + # gap (jolt-edb): ~/~@ aren't processed inside set literals (`#{~@a} keeps the + # unquote-splicing form literal).