core: spec 35-var batch A — 1.11 parsers, map/partition variants, with-redefs, ns fns

Fifteen vars from the spec coverage gap (docs/spec/coverage.md):
parse-long/parse-double/parse-boolean (strict validation; scan-number alone
accepts 0x10), newline, current-time-ms (host clock for time), update-keys/
update-vals (PHM base, collisions last-wins), partitionv/partitionv-all/
splitv-at (lazy seqs of vectors; splitv-at's tail stays a seq, matching the
reference), with-redefs/with-redefs-fn (roots restored on throw), time,
macroexpand (expand-1 to fixpoint), alias/ns-unalias (write BOTH alias stores
— require :as uses string-keyed :imports while ns-aliases reads :aliases;
split filed), ns-publics (symbol-keyed map; publics == interns, no privacy).

Three pre-existing bugs fixed along the way:
- (partition n step pad coll) misparsed pad as the coll and returned ()
- (require 'bare.symbol) rejected — only vector specs were accepted
- analyze-form leaked the interpreted analyzer's ns on a punt: a throw out of
  the analyzer left current-ns=jolt.analyzer and :compile-ns set, so the
  fallback interpretation resolved user vars against the wrong namespace
  (bit (var user-sym) under compile mode)

And one overlay-authoring landmine documented in-code: a 20-coll fn must not
use 30-macros macros (with-redefs-fn's dotimes compiled as a forward ref that
resolved to the macro fn at runtime) — loop/recur instead.

Gate: conformance 316x3 (+14 rows), suite 4324 pass / 78 clean (was 4081/72;
parse_*/update_* files now contribute), baselines raised, all specs+unit,
fixpoint, self-host, sci, staged. Coverage: missing-portable 35 -> 20.
This commit is contained in:
Yogthos 2026-06-10 11:16:54 -04:00
parent 894af34b4c
commit eb7a9f1b20
13 changed files with 277 additions and 44 deletions

View file

@ -351,15 +351,24 @@
# jolt.analyzer), and the interpreter rebinds current-ns to a fn's defining ns
# while it runs — so h/current-ns must read this instead of ctx-current-ns.
(put (ctx :env) :compile-ns (ctx-current-ns ctx))
(def saved-ns (ctx-current-ns ctx))
(def av (ns-find (ctx-find-ns ctx "jolt.analyzer") "analyze"))
# Pre-kernel bootstrap: ensure-analyzer is gated until the kernel tier loads
# (see api/load-core-overlay!), so a compile request from an earlier tier (e.g.
# 00-syntax's destructure defn) finds no analyzer. That fallback is DESIGNED —
# route it through the sanctioned punt channel rather than crashing on a nil var.
(unless av (error "jolt/uncompilable: analyzer not built (pre-kernel bootstrap)"))
(def r ((var-get av) ctx form))
(unless av
(put (ctx :env) :compile-ns nil)
(error "jolt/uncompilable: analyzer not built (pre-kernel bootstrap)"))
# The analyzer runs INTERPRETED; the interpreter rebinds current-ns to a fn's
# defining ns (jolt.analyzer) while it runs and only restores on normal return.
# A punt THROWS out of those frames, leaking jolt.analyzer as current-ns (and
# :compile-ns stayed set) — the fallback interpretation then resolves user vars
# against the wrong ns. Restore both on every exit.
(def r (protect ((var-get av) ctx form)))
(put (ctx :env) :compile-ns nil)
r)
(ctx-set-current-ns ctx saved-ns)
(if (r 0) (r 1) (error (r 1))))
# The analyzer's deliberate punt signal — (uncompilable why) throws the string
# "jolt/uncompilable: <why>". Anything else escaping the compile step is an

View file

@ -1409,12 +1409,15 @@
# (core/20-coll.clj).
(defn core-partition
"(partition n coll) or (partition n step coll). Only complete partitions of
size n are kept (use partition-all to keep the trailing remainder)."
"(partition n coll), (partition n step coll), or (partition n step pad coll).
Only complete partitions of size n are kept; with pad, the final partial
partition is padded from pad (possibly to fewer than n if pad runs out)."
[n & rest]
(let [has-step (> (length rest) 1)
step (if has-step (first rest) n)
coll (if has-step (in rest 1) (first rest))]
(let [argc (length rest)
step (if (>= argc 2) (first rest) n)
pad (if (>= argc 3) (in rest 1) nil)
has-pad (>= argc 3)
coll (case argc 1 (first rest) 2 (in rest 1) 3 (in rest 2))]
# Option A: always lazy.
(defn pstep [c]
(fn []
@ -1425,9 +1428,16 @@
(array/push part (core-first cur))
(set cur (core-rest cur))
(++ i))
(if (= i n)
(cond
(= i n)
(let [next-cur (if (= step n) cur (lazy-from (core-drop (- step n) cur)))]
@[(tuple/slice (tuple ;part)) (pstep next-cur)])
# partial final partition: pad it (last partition, then stop)
(and has-pad (> i 0))
(do
(each x (realize-for-iteration pad)
(when (< (length part) n) (array/push part x)))
@[(tuple/slice (tuple ;part)) (fn [] nil)])
nil)))))
(make-lazy-seq (pstep (lazy-from coll)))))
@ -1775,6 +1785,44 @@
(prin "\n")
nil)
(defn core-newline [] (prin "\n") nil)
# Clojure 1.11 string->scalar parsers: nil on malformed input, throw on a
# non-string. Validation is strict (scan-number alone accepts 0x10 etc.).
(defn- parse-arg-str [s who]
(if (or (string? s) (buffer? s)) (string s)
(error (string who " requires a string, got " (type s)))))
(defn core-parse-long [s]
(def str* (parse-arg-str s "parse-long"))
(def n (length str*))
(def start (if (and (> n 0) (or (= 43 (in str* 0)) (= 45 (in str* 0)))) 1 0))
(if (and (> n start)
(do (var ok true)
(for i start n (when (or (< (in str* i) 48) (> (in str* i) 57)) (set ok false)))
ok))
(scan-number str*)
nil))
(defn core-parse-double [s]
(def str* (parse-arg-str s "parse-double"))
# strict float shape: [+-] digits [. digits] [eE [+-] digits] — at least one
# digit overall; "Infinity"/"-Infinity"/"NaN" accepted like the reference.
(cond
(= str* "Infinity") math/inf
(= str* "-Infinity") (- math/inf)
(= str* "NaN") math/nan
(do
(def pat (peg/compile ~(sequence (opt (set "+-")) (choice (sequence (some :d) (opt (sequence "." (any :d)))) (sequence "." (some :d))) (opt (sequence (set "eE") (opt (set "+-")) (some :d))) -1)))
(if (peg/match pat str*) (scan-number str*) nil))))
(defn core-parse-boolean [s]
(def str* (parse-arg-str s "parse-boolean"))
(case str* "true" true "false" false nil))
# Host time source for the `time` macro (monotonic, milliseconds).
(defn core-current-time-ms [] (* 1000 (os/clock :monotonic)))
# 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.
@ -2942,6 +2990,11 @@
"hash-unordered-coll" core-hash-unordered-coll
"prefers" core-prefers
"random-uuid" core-random-uuid
"parse-long" core-parse-long
"parse-double" core-parse-double
"parse-boolean" core-parse-boolean
"newline" core-newline
"current-time-ms" core-current-time-ms
"parse-uuid" core-parse-uuid
"interpose" core-interpose
"mapcat" core-mapcat

View file

@ -784,13 +784,14 @@
(defn require-impl
"(require '[ns :as a :refer [...]] ...) — load + alias/refer each spec. A fn, so
the args (quoted specs) arrive evaluated. Varargs (Clojure-compatible); each spec
is a vector. eval-require does the namespace load + alias/refer into current-ns."
is a vector [ns & opts] or a bare ns symbol (treated as [ns])."
[ctx & specs]
(each spec specs
(let [s (if (pvec? spec) (pv->array spec) spec)]
(if (and (indexed? s) (> (length s) 0))
(eval-require ctx s)
(error "require expects a vector spec"))))
(cond
(and (indexed? s) (> (length s) 0)) (eval-require ctx s)
(and (struct? s) (= :symbol (s :jolt/type))) (eval-require ctx @[s])
(error "require expects a vector spec or a namespace symbol"))))
nil)
(defn in-ns-impl
@ -1102,15 +1103,47 @@
# Reader / expansion as plain fns: read-string parses one form; macroexpand-1
# expands a (quoted, already-evaluated) call form once via its macro var.
(ns-intern core "read-string" (fn [s] (parse-string s)))
(ns-intern core "macroexpand-1"
(def expand-1 (fn [the-form]
(if (and (array? the-form) (> (length the-form) 0)
(struct? (first the-form)) (= :symbol ((first the-form) :jolt/type)))
(let [v (resolve-var ctx @{} (first the-form))]
(if (and v (var-macro? v))
(apply (var-get v) (tuple/slice the-form 1))
the-form))
the-form)))
(ns-intern core "macroexpand-1" expand-1)
# macroexpand: expand repeatedly until the head is no longer a macro (the
# form's SUBFORMS are not expanded, matching Clojure).
(ns-intern core "macroexpand"
(fn [the-form]
(if (and (array? the-form) (> (length the-form) 0)
(struct? (first the-form)) (= :symbol ((first the-form) :jolt/type)))
(let [v (resolve-var ctx @{} (first the-form))]
(if (and v (var-macro? v))
(apply (var-get v) (tuple/slice the-form 1))
the-form))
the-form)))
(var cur the-form)
(var nxt (expand-1 cur))
(while (not= cur nxt) (set cur nxt) (set nxt (expand-1 cur)))
cur))
# alias/ns-unalias: alias bookkeeping is currently split (require :as writes
# the string-keyed :imports table that resolution reads; :aliases is the
# introspection table ns-aliases reads) — write/remove BOTH until unified.
(ns-intern core "alias"
(fn [alias-sym ns-sym]
(def cur (ctx-find-ns ctx (ctx-current-ns ctx)))
(ns-import cur (alias-sym :name) (ns-sym :name))
(ns-add-alias cur alias-sym (ctx-find-ns ctx (ns-sym :name)))
nil))
(ns-intern core "ns-unalias"
(fn [ns-d alias-sym]
(def ns (ns-or-current ns-d))
(put (ns :imports) (alias-sym :name) nil)
(put (ns :aliases) alias-sym nil)
nil))
# ns-publics: {symbol -> var} (jolt has no private vars, so publics = interns).
# Keys are symbol structs (value-hashed), matching Clojure's symbol keys.
(ns-intern core "ns-publics"
(fn [&opt ns-d]
(def ns (ns-or-current ns-d))
(var m (make-phm))
(loop [[nm v] :pairs (ns :mappings)]
(set m (phm-assoc m {:jolt/type :symbol :ns nil :name nm} v)))
m))
core)
# Dispatch a special form by its string name.