From ad5539b0de6532832d78520eeda0f37fe401ea9b Mon Sep 17 00:00:00 2001 From: Yogthos Date: Fri, 5 Jun 2026 00:35:48 -0400 Subject: [PATCH] =?UTF-8?q?test(spec):=20macros,=20reader,=20host-interop,?= =?UTF-8?q?=20namespaces=20=E2=80=94=20fix=20def-doc,=20resolve,=20%&?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final spec areas. Bugs caught and fixed: - (def name docstring value) used the docstring as the value; now the 3-arg docstring form binds the value and records :doc meta - resolve was a nil stub; now a special form resolving a symbol to its var (nil if unresolved). Added find-ns (non-creating lookup) and ns-name. - in-ns didn't evaluate its arg, so (in-ns 'foo) failed; now evaluates it per Clojure (the integration test's unquoted form updated to the quoted idiom) - #(... %& ...) built %& as a positional param instead of a & rest param; now emits (fn* [... & gen] ...) so %& captures the rest Full public-API spec layer now in place. conformance 218/218, jpm test green. --- src/jolt/core.janet | 7 ++++- src/jolt/evaluator.janet | 19 +++++++++++--- src/jolt/reader.janet | 13 ++++----- test/integration/namespace-test.janet | 4 +-- test/spec/host-interop-spec.janet | 17 ++++++++++++ test/spec/macros-spec.janet | 27 +++++++++++++++++++ test/spec/namespaces-spec.janet | 27 +++++++++++++++++++ test/spec/reader-syntax-spec.janet | 38 +++++++++++++++++++++++++++ 8 files changed, 140 insertions(+), 12 deletions(-) create mode 100644 test/spec/host-interop-spec.janet create mode 100644 test/spec/macros-spec.janet create mode 100644 test/spec/namespaces-spec.janet create mode 100644 test/spec/reader-syntax-spec.janet diff --git a/src/jolt/core.janet b/src/jolt/core.janet index 6453f4d..b5a9fcb 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -2432,7 +2432,11 @@ @[{:jolt/type :symbol :ns nil :name "ex-info"} msg-form {}]]]) # resolve stub — returns nil (symbols not found in Jolt's clojure.core) -(defn core-resolve [sym] nil) +(defn core-resolve [sym] nil) # shadowed by the resolve special form (needs ctx) +(defn core-ns-name [ns] + # ns object -> its name as a symbol (works whether ns is a table/struct/phm) + (let [nm (core-get ns :name)] + (if nm {:jolt/type :symbol :ns nil :name (string nm)} nil))) # update — works on both structs and tables (defn core-update [m k f & args] @@ -3532,6 +3536,7 @@ "defrecord" core-defrecord "comment" core-comment "resolve" core-resolve + "ns-name" core-ns-name "update" core-update "update-in" core-update-in "assoc-in" core-assoc-in diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index a9ae4c7..392c247 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -580,8 +580,12 @@ ns (ctx-find-ns ctx ns-name) # Create var first (unbound) so self-referencing defs resolve v (ns-intern ns (name-sym :name)) - val (eval-form ctx bindings (in form 2))] + # (def name docstring value): docstring is form 2, value form 3 + has-doc (and (> (length form) 3) (string? (in form 2))) + val (eval-form ctx bindings (in form (if has-doc 3 2)))] (bind-root v val) + (when has-doc + (put v :meta (merge (or (get v :meta) {}) {:doc (in form 2)}))) (when dynamic? (put v :dynamic true)) # def returns the var (Clojure semantics); REPL prints #'ns/name @@ -695,10 +699,19 @@ "ns-aliases" (let [ns (ctx-find-ns ctx (ctx-current-ns ctx))] (ns :aliases)) "ns-imports" (let [ns (ctx-find-ns ctx (ctx-current-ns ctx))] (ns :imports)) "ns-resolve" (ns-resolve (ctx-find-ns ctx (ctx-current-ns ctx)) (in form 1)) - "in-ns" (let [ns-name (sym-name-str (in form 1))] - (ctx-set-current-ns ctx ns-name) + "in-ns" (let [sym (eval-form ctx bindings (in form 1)) + ns-name (if (and (struct? sym) (= :symbol (sym :jolt/type))) (sym :name) (string sym))] (ctx-find-ns ctx ns-name) + (ctx-set-current-ns ctx ns-name) nil) + "resolve" (let [sym (eval-form ctx bindings (in form 1))] + (if (and (struct? sym) (= :symbol (sym :jolt/type))) + (let [r (protect (resolve-var ctx bindings sym))] + (if (= (r 0) true) (r 1) nil)) + nil)) + "find-ns" (let [sym (eval-form ctx bindings (in form 1)) + nm (if (and (struct? sym) (= :symbol (sym :jolt/type))) (sym :name) (string sym))] + (get (get (ctx :env) :namespaces) nm)) "fn*" (let [# optional name: (fn* name [args] ...) / (fn* name ([args] ...)...) named? (and (struct? (in form 1)) (= :symbol ((in form 1) :jolt/type))) fn-name (if named? ((in form 1) :name) nil) diff --git a/src/jolt/reader.janet b/src/jolt/reader.janet index de0e8ff..89803b5 100644 --- a/src/jolt/reader.janet +++ b/src/jolt/reader.janet @@ -303,14 +303,15 @@ f)) (def replaced (replace-pct form)) (def arg-names @[]) - # Sort %1 %2 %3 ..., then %, then %& - (def sorted-keys (sort (keys arg-map))) - (each k sorted-keys + # Positional params %, %1, %2, ... in order; %& becomes a `& rest` param. + (def pos-keys (sort (filter |(not= $ "%&") (keys arg-map)))) + (each k pos-keys (array/push arg-names {:jolt/type :symbol :ns nil :name (get arg-map k)})) + (when (get arg-map "%&") + (array/push arg-names (sym "&")) + (array/push arg-names {:jolt/type :symbol :ns nil :name (get arg-map "%&")})) (def result @[(sym "fn*")]) - (if (> (length arg-names) 0) - (array/push result (tuple ;arg-names)) - (array/push result (tuple))) # no args + (array/push result (tuple ;arg-names)) (array/push result replaced) [result new-pos])) diff --git a/test/integration/namespace-test.janet b/test/integration/namespace-test.janet index 67d4b5d..1c582f2 100644 --- a/test/integration/namespace-test.janet +++ b/test/integration/namespace-test.janet @@ -10,14 +10,14 @@ (print "1: in-ns...") (let [ctx (make-ctx)] - (def form (parse-string "(in-ns my.app)")) + (def form (parse-string "(in-ns 'my.app)")) (eval-form ctx @{} form) (assert (= "my.app" (ctx-current-ns ctx)) "in-ns switches namespace")) (print " passed") (print "2: def in different namespace...") (let [ctx (make-ctx)] - (eval-form ctx @{} (parse-string "(in-ns my.app)")) + (eval-form ctx @{} (parse-string "(in-ns 'my.app)")) (eval-form ctx @{} (parse-string "(def x 42)")) (let [ns (ctx-find-ns ctx "my.app") v (ns-find ns "x")] diff --git a/test/spec/host-interop-spec.janet b/test/spec/host-interop-spec.janet new file mode 100644 index 0000000..e1ea7aa --- /dev/null +++ b/test/spec/host-interop-spec.janet @@ -0,0 +1,17 @@ +# Specification: host (Janet) interop — the `.` forms and jolt.interop. +(use ../support/harness) + +(defspec "interop / dot forms" + ["method call" "\"v=41\"" + "(. {:value 41 :describe (fn [self] (str \"v=\" (:value self)))} describe)"] + ["method with args" "\"Hello Alice\"" + "(. {:greet (fn [self n] (str \"Hello \" n))} greet \"Alice\")"] + ["field access .-" "41" "(.-value {:value 41})"] + ["dot field keyword" "41" "(. {:value 41} :value)"]) + +(defspec "interop / jolt.interop" + ["janet-type quoted list" ":array" "(do (require (quote [jolt.interop :as j])) (j/janet-type (quote (1 2))))"] + ["janet-type list" ":array" "(do (require (quote [jolt.interop :as j])) (j/janet-type (list 1 2)))"] + ["janet-type string" ":string" "(do (require (quote [jolt.interop :as j])) (j/janet-type \"x\"))"] + ["janet-type number" ":number" "(do (require (quote [jolt.interop :as j])) (j/janet-type 1))"] + ["janet-type keyword" ":keyword" "(do (require (quote [jolt.interop :as j])) (j/janet-type :a))"]) diff --git a/test/spec/macros-spec.janet b/test/spec/macros-spec.janet new file mode 100644 index 0000000..cd409ad --- /dev/null +++ b/test/spec/macros-spec.janet @@ -0,0 +1,27 @@ +# Specification: macros, quoting and syntax-quote. +(use ../support/harness) + +(defspec "macros / quoting" + ["quote symbol" "(quote a)" "(quote a)"] + ["quote list" "[1 2 3]" "(quote (1 2 3))"] + ["quote nested" "[1 [2 3]]" "(quote (1 (2 3)))"] + ["quote sugar" "'a" "'a"] + ["syntax-quote literal" "[1 2]" "`[1 2]"] + ["unquote" "[1 2 3]" "(let [x 2] `[1 ~x 3])"] + ["unquote-splicing" "[1 2 3 4]" "(let [xs [2 3]] `[1 ~@xs 4])"] + ["unquote in list" "[3]" "(let [x 3] `(~x))"] + ["syntax-quote symbol qualifies" "true" "(symbol? `foo)"]) + +(defspec "macros / defmacro" + ["simple macro" "3" + "(do (defmacro m [a b] `(+ ~a ~b)) (m 1 2))"] + ["macro unless" "1" + "(do (defmacro unless [c body] `(if ~c nil ~body)) (unless false 1))"] + ["macro with body splice" "6" + "(do (defmacro msum [& xs] `(+ ~@xs)) (msum 1 2 3))"] + ["macroexpand-1" "true" + "(do (defmacro m [x] `(inc ~x)) (list? (macroexpand-1 '(m 5))))"] + ["gensym unique" "false" + "(= (gensym) (gensym))"] + ["gensym# in template" "true" + "(do (defmacro m [] `(let [x# 1] x#)) (= 1 (m)))"]) diff --git a/test/spec/namespaces-spec.janet b/test/spec/namespaces-spec.janet new file mode 100644 index 0000000..71412a3 --- /dev/null +++ b/test/spec/namespaces-spec.janet @@ -0,0 +1,27 @@ +# Specification: namespaces, vars and require. +(use ../support/harness) + +(defspec "namespaces / def & vars" + ["def + deref" "5" "(do (def x 5) x)"] + ["def returns var" "true" "(var? (def y 1))"] + ["declare then def" "2" "(do (declare z) (def z 2) z)"] + ["var special form" "true" "(var? (var +))"] + ["var sugar #'" "true" "(var? #'+)"] + ["var-get" "5" "(do (def w 5) (var-get #'w))"] + ["defn defines fn" "3" "(do (defn f [x] (inc x)) (f 2))"] + ["def with docstring" "7" "(do (def d \"a doc\" 7) d)"]) + +(defspec "namespaces / ns operations" + ["in-ns switches" "true" "(do (in-ns 'my.ns) (symbol? 'x))"] + ["ns-name" "true" "(do (require (quote [clojure.string])) (= 'clojure.string (ns-name (find-ns 'clojure.string))))"] + ["find-ns existing" "true" "(some? (find-ns 'clojure.core))"] + ["find-ns missing" "nil" "(find-ns 'does.not.exist)"] + ["resolve var" "true" "(var? (resolve '+))"] + ["resolve missing" "nil" "(resolve 'totally-undefined-xyz)"]) + +(defspec "namespaces / require & refer" + ["require :as" "\"AB\"" "(do (require '[clojure.string :as s]) (s/upper-case \"ab\"))"] + ["require :refer" "true" "(do (require '[clojure.string :refer [blank?]]) (blank? \"\"))"] + ["require :as + :refer" "true" "(do (require '[clojure.string :as s :refer [blank?]]) (and (blank? \"\") (= \"X\" (s/upper-case \"x\"))))"] + ["require clojure.set" "#{1 2 3}" "(do (require '[clojure.set :as set]) (set/union #{1 2} #{3}))"] + ["require clojure.walk" "{:a 2}" "(do (require '[clojure.walk :as w]) (w/postwalk (fn [x] (if (number? x) (inc x) x)) {:a 1}))"]) diff --git a/test/spec/reader-syntax-spec.janet b/test/spec/reader-syntax-spec.janet new file mode 100644 index 0000000..52edac6 --- /dev/null +++ b/test/spec/reader-syntax-spec.janet @@ -0,0 +1,38 @@ +# Specification: reader syntax & literals. +(use ../support/harness) + +(defspec "reader / scalar literals" + ["integer" "42" "42"] + ["negative" "-7" "-7"] + ["float" "1.5" "1.5"] + ["string" "\"hi\"" "\"hi\""] + ["boolean true" "true" "true"] + ["nil" "nil" "nil"] + ["keyword" ":a" ":a"] + ["namespaced keyword" "true" "(= :a/b :a/b)"] + ["char" "\\a" "\\a"] + ["char newline" "true" "(= \\newline (first \"\\n\"))"] + ["ratio not supported but reads ints" "3" "3"] + ["hex literal" "255" "0xff"] + ["symbol via quote" "'foo" "'foo"]) + +(defspec "reader / collection literals" + ["vector" "[1 2 3]" "[1 2 3]"] + ["list quoted" "[1 2 3]" "'(1 2 3)"] + ["map" "{:a 1}" "{:a 1}"] + ["set" "#{1 2 3}" "#{1 2 3}"] + ["nested" "{:a [1 {:b 2}]}" "{:a [1 {:b 2}]}"] + ["empty vector" "[]" "[]"] + ["empty map" "{}" "{}"] + ["empty set" "#{}" "#{}"]) + +(defspec "reader / dispatch & sugar" + ["anon fn #()" "3" "(#(+ %1 %2) 1 2)"] + ["anon fn single %" "2" "(#(inc %) 1)"] + ["anon fn %&" "[2 3]" "(#(vec %&) 2 3)"] + ["discard #_" "[1 3]" "[1 #_2 3]"] + ["regex literal" "true" "(= \"abc\" (re-find #\"abc\" \"xabcx\"))"] + ["reader conditional" "1" "#?(:clj 1 :cljs 2 :default 3)"] + ["tagged literal var" "true" "(var? #'+)"] + ["deref sugar" "5" "(let [a (atom 5)] @a)"] + ["meta sugar" "{:t 1}" "(meta ^{:t 1} [])"])