diff --git a/docs/spec/coverage.md b/docs/spec/coverage.md index ebf4b7c..77025c3 100644 --- a/docs/spec/coverage.md +++ b/docs/spec/coverage.md @@ -3,19 +3,19 @@ Generated 2026-06-10 by `tools/spec_coverage.py` — do not edit by hand. Surface: **694** clojure.core vars (ClojureDocs export; 648 with -community examples). jolt interns 550 of them. +community examples). jolt interns 535 of them. | Status | Count | Meaning | |---|---|---| -| implemented+tested | 399 | in jolt and exercised by spec/conformance | -| implemented-untested | 151 | in jolt, no direct test — spec entries will add them | -| resolvable-not-interned | 22 | works in code but invisible to ns introspection (conformance finding) | -| missing-portable | 19 | portable semantics, jolt lacks it — implementation gap | -| special-form | 13 | specified in §3, not a library var | -| dynamic-var | 29 | classification needed: portable default vs host-dependent | +| implemented+tested | 397 | in jolt and exercised by spec/conformance | +| implemented-untested | 138 | in jolt, no direct test — spec entries will add them | +| resolvable-not-interned | 2 | works in code but invisible to ns introspection (conformance finding) | +| missing-portable | 27 | portable semantics, jolt lacks it — implementation gap | +| special-form | 15 | specified in §3, not a library var | +| dynamic-var | 31 | classification needed: portable default vs host-dependent | | agents-taps | 22 | out of scope pending concurrency design note | | stm-refs | 11 | out of scope pending concurrency design note | -| jvm-specific | 50 | catalogued, not specified | +| jvm-specific | 53 | catalogued, not specified | Classifications are initial and mechanical — reclassifying is an ordinary spec change. A var is *Verified* only when its §9 entry exists and carries no @@ -41,14 +41,14 @@ UNVERIFIED field; that column will be added as entries land. | `*data-readers*` | dynamic-var | ✓ | | `*default-data-reader-fn*` | dynamic-var | ✓ | | `*e` | implemented-untested | ✓ | -| `*err*` | implemented-untested | ✓ | +| `*err*` | dynamic-var | ✓ | | `*file*` | dynamic-var | ✓ | | `*flush-on-newline*` | dynamic-var | | | `*fn-loader*` | dynamic-var | | | `*in*` | dynamic-var | | | `*math-context*` | dynamic-var | | | `*ns*` | dynamic-var | ✓ | -| `*out*` | implemented-untested | ✓ | +| `*out*` | dynamic-var | ✓ | | `*print-dup*` | dynamic-var | ✓ | | `*print-length*` | dynamic-var | ✓ | | `*print-level*` | dynamic-var | ✓ | @@ -111,7 +111,7 @@ UNVERIFIED field; that column will be added as entries land. | `amap` | jvm-specific | ✓ | | `ancestors` | implemented+tested | ✓ | | `and` | implemented+tested | ✓ | -| `any?` | implemented-untested | ✓ | +| `any?` | implemented+tested | ✓ | | `apply` | implemented+tested | ✓ | | `areduce` | jvm-specific | ✓ | | `array-map` | implemented-untested | ✓ | @@ -193,7 +193,7 @@ UNVERIFIED field; that column will be added as entries land. | `comparator` | implemented+tested | ✓ | | `compare` | implemented+tested | ✓ | | `compare-and-set!` | implemented+tested | ✓ | -| `compile` | implemented-untested | ✓ | +| `compile` | jvm-specific | ✓ | | `complement` | implemented+tested | ✓ | | `completing` | implemented+tested | ✓ | | `concat` | implemented+tested | ✓ | @@ -221,7 +221,7 @@ UNVERIFIED field; that column will be added as entries land. | `default-data-readers` | jvm-specific | ✓ | | `definline` | jvm-specific | | | `definterface` | implemented-untested | ✓ | -| `defmacro` | implemented+tested | ✓ | +| `defmacro` | special-form | ✓ | | `defmethod` | implemented+tested | ✓ | | `defmulti` | implemented+tested | ✓ | | `defn` | implemented+tested | ✓ | @@ -267,7 +267,7 @@ UNVERIFIED field; that column will be added as entries land. | `enumeration-seq` | implemented-untested | ✓ | | `error-handler` | agents-taps | ✓ | | `error-mode` | agents-taps | ✓ | -| `eval` | implemented+tested | ✓ | +| `eval` | missing-portable | ✓ | | `even?` | implemented+tested | ✓ | | `every-pred` | implemented+tested | ✓ | | `every?` | implemented+tested | ✓ | @@ -290,7 +290,7 @@ UNVERIFIED field; that column will be added as entries land. | `find-keyword` | missing-portable | ✓ | | `find-ns` | implemented+tested | ✓ | | `find-protocol-impl` | jvm-specific | | -| `find-protocol-method` | implemented-untested | | +| `find-protocol-method` | jvm-specific | | | `find-var` | implemented+tested | ✓ | | `first` | implemented+tested | ✓ | | `flatten` | implemented+tested | ✓ | @@ -298,7 +298,7 @@ UNVERIFIED field; that column will be added as entries land. | `float-array` | implemented-untested | ✓ | | `float?` | implemented+tested | ✓ | | `floats` | implemented-untested | ✓ | -| `flush` | implemented-untested | ✓ | +| `flush` | missing-portable | ✓ | | `fn` | implemented+tested | ✓ | | `fn?` | implemented+tested | ✓ | | `fnext` | implemented+tested | ✓ | @@ -383,7 +383,7 @@ UNVERIFIED field; that column will be added as entries land. | `load` | jvm-specific | ✓ | | `load-file` | jvm-specific | ✓ | | `load-reader` | jvm-specific | ✓ | -| `load-string` | implemented-untested | ✓ | +| `load-string` | missing-portable | ✓ | | `loaded-libs` | jvm-specific | ✓ | | `locking` | implemented+tested | ✓ | | `long` | implemented-untested | ✓ | @@ -437,13 +437,13 @@ UNVERIFIED field; that column will be added as entries land. | `ns-aliases` | implemented+tested | ✓ | | `ns-imports` | implemented-untested | ✓ | | `ns-interns` | implemented-untested | ✓ | -| `ns-map` | implemented-untested | ✓ | +| `ns-map` | missing-portable | ✓ | | `ns-name` | implemented+tested | ✓ | | `ns-publics` | implemented+tested | ✓ | | `ns-refers` | missing-portable | ✓ | | `ns-resolve` | implemented+tested | ✓ | | `ns-unalias` | implemented+tested | ✓ | -| `ns-unmap` | implemented-untested | ✓ | +| `ns-unmap` | missing-portable | ✓ | | `nth` | implemented+tested | ✓ | | `nthnext` | implemented+tested | ✓ | | `nthrest` | implemented+tested | ✓ | @@ -484,7 +484,7 @@ UNVERIFIED field; that column will be added as entries land. | `print-method` | implemented-untested | ✓ | | `print-simple` | jvm-specific | ✓ | | `print-str` | implemented+tested | ✓ | -| `printf` | implemented+tested | ✓ | +| `printf` | missing-portable | ✓ | | `println` | implemented+tested | ✓ | | `println-str` | implemented+tested | ✓ | | `prn` | implemented+tested | ✓ | @@ -594,7 +594,7 @@ UNVERIFIED field; that column will be added as entries land. | `simple-ident?` | implemented+tested | ✓ | | `simple-keyword?` | implemented+tested | ✓ | | `simple-symbol?` | implemented+tested | ✓ | -| `slurp` | implemented-untested | ✓ | +| `slurp` | missing-portable | ✓ | | `some` | implemented+tested | ✓ | | `some->` | implemented+tested | ✓ | | `some->>` | implemented+tested | ✓ | @@ -608,7 +608,7 @@ UNVERIFIED field; that column will be added as entries land. | `sorted-set-by` | implemented+tested | ✓ | | `sorted?` | implemented+tested | ✓ | | `special-symbol?` | implemented-untested | ✓ | -| `spit` | implemented-untested | ✓ | +| `spit` | missing-portable | ✓ | | `split-at` | implemented+tested | ✓ | | `split-with` | implemented+tested | ✓ | | `splitv-at` | implemented+tested | | @@ -618,7 +618,7 @@ UNVERIFIED field; that column will be added as entries land. | `stream-seq!` | jvm-specific | | | `stream-transduce!` | jvm-specific | | | `string?` | implemented+tested | ✓ | -| `struct` | implemented-untested | ✓ | +| `struct` | jvm-specific | ✓ | | `struct-map` | jvm-specific | ✓ | | `subs` | implemented+tested | ✓ | | `subseq` | implemented+tested | ✓ | @@ -648,8 +648,8 @@ UNVERIFIED field; that column will be added as entries land. | `transient` | implemented+tested | ✓ | | `tree-seq` | implemented+tested | ✓ | | `true?` | implemented+tested | ✓ | -| `try` | implemented+tested | ✓ | -| `type` | implemented-untested | ✓ | +| `try` | special-form | ✓ | +| `type` | implemented+tested | ✓ | | `unchecked-add` | implemented-untested | ✓ | | `unchecked-add-int` | implemented-untested | ✓ | | `unchecked-byte` | implemented-untested | ✓ | diff --git a/jolt-core/clojure/core/20-coll.clj b/jolt-core/clojure/core/20-coll.clj index 54dd85e..24ef2f3 100644 --- a/jolt-core/clojure/core/20-coll.clj +++ b/jolt-core/clojure/core/20-coll.clj @@ -148,6 +148,9 @@ (defn keyword-identical? [a b] (= a b)) +;; Clojure 1.9: true for ANY argument incl. nil (used as a spec predicate). +(defn any? [x] true) + (defn comparator [pred] (fn [a b] (cond (pred a b) -1 (pred b a) 1 :else 0))) diff --git a/src/jolt/core.janet b/src/jolt/core.janet index dde1708..68ddac3 100644 --- a/src/jolt/core.janet +++ b/src/jolt/core.janet @@ -208,7 +208,12 @@ (defn core-even? [n] (= 0 (% (need-int n "even?") 2))) (defn core-odd? [n] (not= 0 (% (need-int n "odd?") 2))) -(defn core-integer? [x] (and (number? x) (= x (math/floor x)))) +# Finite integral number: NaN and the infinities are NOT integers (floor of +# inf is inf, so the naive floor check wrongly accepted them). +(defn core-integer? [x] + (and (number? x) (= x x) + (< x math/inf) (> x (- math/inf)) + (= x (math/floor x)))) (defn core-boolean? [x] (or (= x true) (= x false))) (defn core-list? [x] (or (plist? x) (and (array? x) (not (get x :jolt/type))))) @@ -1827,6 +1832,74 @@ # Host time source for the `time` macro (monotonic, milliseconds). (defn core-current-time-ms [] (* 1000 (os/clock :monotonic))) +# Clojure compare: a total order over comparable values. nil sorts first; +# numbers numerically; strings/keywords lexically; symbols by ns then name; +# booleans false x y) 1 0)) + (cond + (and (nil? a) (nil? b)) 0 + (nil? a) -1 + (nil? b) 1 + (and (number? a) (number? b)) (cmp3 a b) + (and (or (string? a) (buffer? a)) (or (string? b) (buffer? b))) + (cmp3 (string a) (string b)) + (and (keyword? a) (keyword? b)) (cmp3 (string a) (string b)) + (and (core-symbol? a) (core-symbol? b)) + (let [r (cmp3 (string (or (a :ns) "")) (string (or (b :ns) "")))] + (if (= 0 r) (cmp3 (a :name) (b :name)) r)) + (and (boolean? a) (boolean? b)) + (cond (= a b) 0 (= a false) -1 1) + (and (core-char? a) (core-char? b)) (cmp3 (a :ch) (b :ch)) + (and (struct? a) (= :jolt/uuid (get a :jolt/type)) + (struct? b) (= :jolt/uuid (get b :jolt/type))) + (cmp3 (a :str) (b :str)) + (and (struct? a) (= :jolt/inst (get a :jolt/type)) + (struct? b) (= :jolt/inst (get b :jolt/type))) + (cmp3 (a :ms) (b :ms)) + (and (jvec? a) (jvec? b)) + (let [la (vcount a) lb (vcount b)] + (if (not= la lb) + (cmp3 la lb) + (do + (var r 0) (var i 0) + (while (and (= r 0) (< i la)) + (set r (ccompare (vnth a i) (vnth b i))) + (++ i)) + r))) + (error (string "Cannot compare " (type a) " with " (type b)))))) + +# Clojure type: the :type metadata when present, else the value's type. With no +# class objects on this host, the "class" is a symbol: a deftype/record value +# yields its type tag symbol; everything else a taxonomy keyword +# (host-classified — see spec coverage). +(defn core-type [x] + (def m (core-meta x)) + (def override (and m (core-get m :type))) + (if (not (nil? override)) + override + (cond + (and (table? x) (get x :jolt/deftype)) + {:jolt/type :symbol :ns nil :name (get x :jolt/deftype)} + (nil? x) nil + (boolean? x) :boolean + (number? x) :number + (or (string? x) (buffer? x)) :string + (keyword? x) :keyword + (core-symbol? x) :symbol + (core-char? x) :char + (and (struct? x) (get x :jolt/type)) (get x :jolt/type) + (jvec? x) :vector + (core-map? x) :map + (set? x) :set + (core-seq? x) :seq + (or (function? x) (cfunction? x)) :fn + (table? x) (or (get x :jolt/type) :table) + :else (keyword (type x))))) + # Capture *out*: run thunk with Janet's :out dynamic bound to a buffer, so all # print/println/pr/prn output (which go through `prin` -> (dyn :out)) is collected # and returned as a string. The with-out-str macro (overlay) wraps a body thunk. @@ -2994,6 +3067,10 @@ "hash-unordered-coll" core-hash-unordered-coll "prefers" core-prefers "random-uuid" core-random-uuid + "gensym" gensym + "int?" core-integer? + "compare" core-compare + "type" core-type "parse-long" core-parse-long "parse-double" core-parse-double "parse-boolean" core-parse-boolean diff --git a/src/jolt/evaluator.janet b/src/jolt/evaluator.janet index a860cb0..b4430f8 100644 --- a/src/jolt/evaluator.janet +++ b/src/jolt/evaluator.janet @@ -470,12 +470,10 @@ class-name (string/slice name (+ last-dot 1))] (let [target-ns (ctx-find-ns ctx class-ns) tv (ns-find target-ns class-name)] (if tv (var-get tv) tv))) - # Fall back to Janet's global environment - (let [root-env (fiber/getenv (fiber/current)) - entry (in root-env (symbol name))] - (if (not (nil? entry)) - (if (table? entry) (entry :value) entry) - (error (string "Unable to resolve symbol: " name)))))))))))))))) + # No implicit Janet fallback (Stage 3): an unresolved + # Clojure symbol is an error. Host access is the explicit + # janet/ prefix above. + (error (string "Unable to resolve symbol: " name)))))))))))))) (defn- parse-arg-names "Parse a parameter vector, handling & rest args. diff --git a/src/jolt/jolt/interop.clj b/src/jolt/jolt/interop.clj index 92b9a54..28073c8 100644 --- a/src/jolt/jolt/interop.clj +++ b/src/jolt/jolt/interop.clj @@ -7,7 +7,7 @@ (defn janet-type [x] - (type x)) + (janet/type x)) (defn janet-describe [x] diff --git a/test/integration/clojure-test-suite-test.janet b/test/integration/clojure-test-suite-test.janet index 36a4627..7a2b70f 100644 --- a/test/integration/clojure-test-suite-test.janet +++ b/test/integration/clojure-test-suite-test.janet @@ -43,7 +43,7 @@ # Raised 4004 -> 4034 / clean 66 -> 67 porting partition-all + repeatedly to the # overlay, which required fixing two leniencies (a char is not callable; take # validates its count) — correct beyond those fns, so the suite rose broadly. -(def baseline-pass 4470) +(def baseline-pass 4532) # A file is "clean" when it ran with zero failures AND zero errors. (def baseline-clean-files 86) # Per-file wall-clock budget (seconds). Normal files finish in well under 1s, so diff --git a/test/integration/conformance-test.janet b/test/integration/conformance-test.janet index 70389a4..13d495c 100644 --- a/test/integration/conformance-test.janet +++ b/test/integration/conformance-test.janet @@ -200,6 +200,13 @@ ["inst offset normalized" "true" "(= #inst \"2020-01-01T01:00:00+01:00\" #inst \"2020-01-01T00:00:00Z\")"] ["sq literal collapse" "true" "(= \"meow\" ```\"meow\")"] ["sq number collapse" "42" "``42"] + + ### ---- stage 3: proper vars replace the Janet root-env leak ---- + ["compare total order" "[-1 0 1]" "[(compare nil 1) (compare :a :a) (compare \"b\" \"a\")]"] + ["compare vectors" "-1" "(compare [1 2] [1 3])"] + ["gensym jolt symbol" "true" "(symbol? (gensym))"] + ["any? anything" "true" "(and (any? nil) (any? 1) (any? :k))"] + ["int? excludes Inf" "false" "(int? ##Inf)"] ["macroexpand-1 when" "2" "(count (rest (macroexpand-1 (quote (when true 1)))))"] ### ---- HIGH: aliased namespace calls ---- diff --git a/test/spec/predicates-spec.janet b/test/spec/predicates-spec.janet index dd42db0..c8bcbc1 100644 --- a/test/spec/predicates-spec.janet +++ b/test/spec/predicates-spec.janet @@ -157,3 +157,30 @@ ["not-empty full" "[1]" "(not-empty [1])"] ["not-empty empty" "nil" "(not-empty [])"] ["not-empty string" "nil" "(not-empty \"\")"]) + +# Stage 3 turn 2a: the implicit Janet root-env fallback is GONE — these are now +# proper interned clojure.core vars with Clojure semantics (compare's total +# order, meta-aware type, any?, gensym returning jolt symbols). +(defspec "predicates / compare, type, any? (stage 3)" + ["compare =" "0" "(compare 1 1)"] + ["compare <" "-1" "(compare 1 2)"] + ["compare nil first" "-1" "(compare nil 1)"] + ["compare nil nil" "0" "(compare nil nil)"] + ["compare strings" "-1" "(compare \"a\" \"b\")"] + ["compare keywords" "-1" "(compare :a :b)"] + ["compare symbols" "-1" "(compare (quote a) (quote b))"] + ["compare bools" "-1" "(compare false true)"] + ["compare vec length" "-1" "(compare [1 2] [1 2 3])"] + ["compare vec elems" "-1" "(compare [1 2] [1 3])"] + ["compare cross-type throws" :throws "(compare 1 \"a\")"] + ["sort with compare" "[nil 1 3]" "(sort compare [3 nil 1])"] + ["type meta override" ":custom" "(type (with-meta [1] {:type :custom}))"] + ["type of record" "true" "(do (defrecord TyR [a]) (= (symbol (str (type (->TyR 1)))) (type (->TyR 1))))"] + ["any? value" "true" "(any? 5)"] + ["any? nil" "true" "(any? nil)"] + ["gensym is symbol" "true" "(symbol? (gensym))"] + ["gensym prefix" "true" "(do (require (quote [clojure.string :as s])) (s/starts-with? (str (gensym \"p_\")) \"p_\"))"] + ["gensym distinct" "false" "(= (gensym) (gensym))"] + ["int? Inf false" "false" "(int? ##Inf)"] + ["integer? Inf false" "false" "(integer? ##Inf)"] + ["integer? NaN false" "false" "(integer? ##NaN)"]) diff --git a/test/unit/evaluator-test.janet b/test/unit/evaluator-test.janet index 62b69d3..e407327 100644 --- a/test/unit/evaluator-test.janet +++ b/test/unit/evaluator-test.janet @@ -12,10 +12,11 @@ {:jolt/type :symbol :ns nil :name name}))) # Helper: parse and eval +# init, not bare make-ctx: with the implicit Janet root-env fallback removed +# (Stage 3), a context without clojure.core interned can't resolve inc/+/etc. +(def- shared-ctx (init)) (defn eval-str [s] - (let [ctx (make-ctx) - form (parse-string s)] - (eval-form ctx @{} form))) + (eval-form shared-ctx @{} (parse-string s))) (print "1: literals...") (assert (= 42 (eval-str "42")) "integer")