* test: adapt jank's form/reader tests into spec suites; fix case no-match
Vendoring jank's behavior (not the project): we base our own copies on the
jank test corpus to close coverage gaps, but maintain them ourselves since
jank may diverge. Two new spec batteries (jank-isms translated to Jolt:
letfn* -> letfn, jank catch types -> :default; platform-specific bigdec/
biginteger/ratio/uuid/##Inf/unicode cases omitted):
- test/spec/forms-spec.janet (52): case, fn (arity/variadic/closure/recur/
named), let, letfn, loop, try, if/do/def/call — incl :throws regression
cases (no-match, bad params, nil call).
- test/spec/reader-forms-spec.janet (22): #() (% %N %&), #' var-quote,
^metadata, syntax-quote (gensym/unquote/splice).
Fix surfaced by adaptation: case with no matching clause and no default now
throws 'No matching clause' (Clojure semantics) instead of returning nil
(00-syntax). Gate green incl full jpm build+test.
Other gaps the adaptation surfaced are filed (tests adjusted to jolt's
current behavior + a comment, not silently dropped):
jolt-vdo case duplicate test constants not rejected
jolt-w2v loop bindings not sequential (later init can't see earlier)
jolt-6x1 #() %& miscomputes arity with a higher positional (%2 …)
jolt-xl0 ^meta not attached to collection literals ({}/[]/#{})
jolt-265 syntax-quote doesn't fully-qualify core syms to clojure.core/
jolt-edb syntax-quote ~/~@ not processed inside set literals
* core: fix 5 Clojure-conformance gaps surfaced by jank tests
All from adapting jank's form/reader tests; each fix verified in interpret +
compile modes and the spec tests now assert the correct behavior.
- jolt-vdo: case now rejects duplicate test constants at expansion (Clojure
compile error), via bootstrap-safe duplicate detection (00-syntax; analyzer.clj
uses case during its own build, so seed-only fns).
- jolt-w2v: loop bindings are now sequential like let — a later init can
reference an earlier binding. Fixed the interpreter loop* (accumulating scope)
and the back end emit-loop (bind initial inits in a sequential Janet let before
entering the recur target).
- jolt-6x1: #() reader computes the fixed arity from the MAX positional (%2 ->
[p1 p2 & rest]); % and %1 unify; unused lower slots get placeholder params.
- jolt-xl0: ^meta on collection literals ({}/[]/#{}) now attaches — read-meta
passes the NORMALIZED metadata map to with-meta (was the raw meta-form).
- jolt-edb: syntax-quote processes ~/~@ inside set literals (new __sqset builder
in core + set branches in syntax-quote* and syntax-quote-lower).
Deferred: jolt-265 (fully-qualify core syms to clojure.core/ in syntax-quote) —
it passes conformance but breaks the standalone uberscript (the ns macro emits
unqualified require/in-ns, which then qualify and break bundled require/alias).
Reverted to bare (functionally resolves); re-opened with the finding.
Gate green incl full jpm build + jpm test: conformance 269x3, suite >=4034/67,
fixpoint, self-host, sci 422/0, uberscript, all unit + spec (forms 55, reader 31).
---------
Co-authored-by: Yogthos <yogthos@gmail.com>
80 lines
4.8 KiB
Text
80 lines
4.8 KiB
Text
# 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)"]
|
|
["duplicate keys" :throws "(case 1 1 :one 1 :dup :default)"]
|
|
["duplicate in or-group" :throws "(case 2 (1 2) :a (2 3) :b :default)"])
|
|
|
|
(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]))"]
|
|
["init sees prior" "[1 2 3]" "(loop* [a 1 b (+ a 1) c (+ b 1)] [a b c])"])
|
|
|
|
(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)"])
|