core: Stage 3 turn 2a — close the implicit Janet root-env leak

resolve-sym's last resort silently resolved any unknown Clojure symbol
against Janet's root environment — leaking Janet builtins with JANET
semantics into Clojure code: (type 1) was Janet's :number, (gensym) returned
Janet symbols (the long-documented (symbol (str (gensym))) macro landmine
existed BECAUSE of this), compare/slurp/int?/any? likewise. The explicit
janet/ prefix is the deliberate interop channel; the implicit fallback is
gone — an unresolved symbol is an error.

What the leak was masking, now proper interned vars:
- gensym: jolt's own (already existed, never interned) — returns real jolt
  symbols; the macro landmine is dead
- compare: full Clojure total order (nil-first, numbers, strings, keywords,
  symbols by ns/name, booleans, chars, uuid/inst, vectors by length then
  elementwise; cross-type throws)
- type: :type metadata override, deftype/record tag as symbol, else a
  taxonomy keyword (host-classified)
- int?: core-integer? — which had a latent bug the leak hid: (integer?
  ##Inf) was true (floor of inf is inf); NaN/infinities now excluded
- any?: constantly true (Clojure 1.9; SCI's namespaces.cljc needs it)
- jolt.interop/janet-type now uses the explicit (janet/type x) channel
- evaluator-test uses init (a bare make-ctx resolved EVERYTHING via the leak)

Suite 4470 -> 4532+ pass / 86-87 clean (proper compare unlocks the sort
files); baselines raised. Conformance 326x3 (+5 rows), +22 predicate spec
rows, stdlib battery green, all specs+unit. Coverage dashboard now counts
previously-leak-resolvable names honestly (missing-portable 19 -> 27).
This commit is contained in:
Yogthos 2026-06-10 12:43:08 -04:00
parent d0c605ac9d
commit c7b0ad9d84
9 changed files with 150 additions and 37 deletions

View file

@ -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<true; chars by codepoint; vectors by length then elementwise;
# uuids by canonical string; insts by epoch ms. Cross-type comparison throws
# (like Clojure's ClassCastException).
(var core-compare nil)
(set core-compare (fn ccompare [a b]
(defn cmp3 [x y] (cond (< x y) -1 (> 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

View file

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

View file

@ -7,7 +7,7 @@
(defn janet-type
[x]
(type x))
(janet/type x))
(defn janet-describe
[x]