test(spec): macros, reader, host-interop, namespaces — fix def-doc, resolve, %&

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.
This commit is contained in:
Yogthos 2026-06-05 00:35:48 -04:00
parent e3d2aa8d14
commit ad5539b0de
8 changed files with 140 additions and 12 deletions

View file

@ -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

View file

@ -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)

View file

@ -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]))

View file

@ -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")]

View file

@ -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))"])

View file

@ -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)))"])

View file

@ -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}))"])

View file

@ -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} [])"])