diff --git a/docs/spec/coverage.md b/docs/spec/coverage.md index c045dbe..7d93ce5 100644 --- a/docs/spec/coverage.md +++ b/docs/spec/coverage.md @@ -1,21 +1,21 @@ # Appendix A — Coverage Dashboard (generated) -Generated 2026-06-22 by `tools/spec_coverage.py` — do not edit by hand. +Generated 2026-06-26 by `tools/spec_coverage.py` — do not edit by hand. Surface: **694** clojure.core vars (ClojureDocs export; 648 with -community examples). jolt interns 574 of them. +community examples). jolt interns 591 of them. | Status | Count | Meaning | |---|---|---| -| implemented+tested | 568 | in jolt and exercised by spec/conformance | -| implemented-untested | 6 | in jolt, no direct test — spec entries will add them | +| implemented+tested | 587 | in jolt and exercised by spec/conformance | +| implemented-untested | 4 | in jolt, no direct test — spec entries will add them | | resolvable-not-interned | 0 | works in code but invisible to ns introspection (conformance finding) | -| missing-portable | 6 | portable semantics, jolt lacks it — implementation gap | +| missing-portable | 0 | portable semantics, jolt lacks it — implementation gap | | special-form | 16 | specified in §3, not a library var | -| dynamic-var | 24 | classification needed: portable default vs host-dependent | +| dynamic-var | 14 | classification needed: portable default vs host-dependent | | agents-taps | 16 | out of scope pending concurrency design note | | stm-refs | 11 | out of scope pending concurrency design note | -| jvm-specific | 47 | catalogued, not specified | +| jvm-specific | 46 | 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 @@ -27,35 +27,35 @@ UNVERIFIED field; that column will be added as entries land. |---|---|---| | `*` | implemented+tested | ✓ | | `*'` | implemented+tested | ✓ | -| `*1` | missing-portable | ✓ | -| `*2` | missing-portable | ✓ | -| `*3` | missing-portable | ✓ | +| `*1` | implemented+tested | ✓ | +| `*2` | implemented+tested | ✓ | +| `*3` | implemented+tested | ✓ | | `*agent*` | dynamic-var | ✓ | | `*allow-unresolved-vars*` | dynamic-var | ✓ | | `*assert*` | implemented+tested | ✓ | | `*clojure-version*` | implemented+tested | ✓ | -| `*command-line-args*` | dynamic-var | ✓ | -| `*compile-files*` | dynamic-var | ✓ | +| `*command-line-args*` | implemented-untested | ✓ | +| `*compile-files*` | implemented+tested | ✓ | | `*compile-path*` | dynamic-var | ✓ | | `*compiler-options*` | dynamic-var | ✓ | -| `*data-readers*` | dynamic-var | ✓ | +| `*data-readers*` | implemented+tested | ✓ | | `*default-data-reader-fn*` | dynamic-var | ✓ | -| `*e` | missing-portable | ✓ | -| `*err*` | implemented-untested | ✓ | -| `*file*` | dynamic-var | ✓ | -| `*flush-on-newline*` | dynamic-var | | +| `*e` | implemented+tested | ✓ | +| `*err*` | implemented+tested | ✓ | +| `*file*` | implemented-untested | ✓ | +| `*flush-on-newline*` | implemented+tested | | | `*fn-loader*` | dynamic-var | | | `*in*` | implemented+tested | | -| `*math-context*` | dynamic-var | | +| `*math-context*` | implemented+tested | | | `*ns*` | implemented+tested | ✓ | -| `*out*` | implemented-untested | ✓ | -| `*print-dup*` | dynamic-var | ✓ | +| `*out*` | implemented+tested | ✓ | +| `*print-dup*` | implemented+tested | ✓ | | `*print-length*` | dynamic-var | ✓ | | `*print-level*` | dynamic-var | ✓ | -| `*print-meta*` | dynamic-var | ✓ | -| `*print-namespace-maps*` | dynamic-var | ✓ | +| `*print-meta*` | implemented+tested | ✓ | +| `*print-namespace-maps*` | implemented-untested | ✓ | | `*print-readably*` | implemented+tested | ✓ | -| `*read-eval*` | dynamic-var | ✓ | +| `*read-eval*` | implemented+tested | ✓ | | `*reader-resolver*` | dynamic-var | | | `*repl*` | dynamic-var | | | `*source-path*` | dynamic-var | ✓ | @@ -63,7 +63,7 @@ UNVERIFIED field; that column will be added as entries land. | `*unchecked-math*` | implemented+tested | ✓ | | `*use-context-classloader*` | dynamic-var | ✓ | | `*verbose-defrecords*` | dynamic-var | | -| `*warn-on-reflection*` | implemented-untested | ✓ | +| `*warn-on-reflection*` | implemented+tested | ✓ | | `+` | implemented+tested | ✓ | | `+'` | implemented+tested | ✓ | | `-` | implemented+tested | ✓ | @@ -131,7 +131,7 @@ UNVERIFIED field; that column will be added as entries land. | `assoc-in` | implemented+tested | ✓ | | `associative?` | implemented+tested | ✓ | | `atom` | implemented+tested | ✓ | -| `await` | implemented-untested | ✓ | +| `await` | implemented+tested | ✓ | | `await-for` | agents-taps | ✓ | | `await1` | agents-taps | | | `bases` | jvm-specific | ✓ | @@ -218,7 +218,7 @@ UNVERIFIED field; that column will be added as entries land. | `declare` | implemented+tested | ✓ | | `dedupe` | implemented+tested | ✓ | | `def` | special-form | ✓ | -| `default-data-readers` | jvm-specific | ✓ | +| `default-data-readers` | implemented+tested | ✓ | | `definline` | jvm-specific | | | `definterface` | implemented+tested | ✓ | | `defmacro` | special-form | ✓ | @@ -375,7 +375,7 @@ UNVERIFIED field; that column will be added as entries land. | `lazy-cat` | implemented+tested | ✓ | | `lazy-seq` | implemented+tested | ✓ | | `let` | implemented+tested | ✓ | -| `letfn` | missing-portable | ✓ | +| `letfn` | implemented+tested | ✓ | | `line-seq` | implemented+tested | ✓ | | `list` | implemented+tested | ✓ | | `list*` | implemented+tested | ✓ | @@ -512,7 +512,7 @@ UNVERIFIED field; that column will be added as entries land. | `rational?` | implemented+tested | ✓ | | `rationalize` | implemented+tested | ✓ | | `re-find` | implemented+tested | ✓ | -| `re-groups` | missing-portable | ✓ | +| `re-groups` | implemented+tested | ✓ | | `re-matcher` | implemented+tested | ✓ | | `re-matches` | implemented+tested | ✓ | | `re-pattern` | implemented+tested | ✓ | @@ -558,7 +558,7 @@ UNVERIFIED field; that column will be added as entries land. | `reset-vals!` | implemented+tested | ✓ | | `resolve` | implemented+tested | ✓ | | `rest` | implemented+tested | ✓ | -| `restart-agent` | implemented-untested | ✓ | +| `restart-agent` | implemented+tested | ✓ | | `resultset-seq` | jvm-specific | ✓ | | `reverse` | implemented+tested | ✓ | | `reversible?` | implemented+tested | ✓ | diff --git a/host/chez/dynamic-var-defaults.ss b/host/chez/dynamic-var-defaults.ss index 387e048..68e0655 100644 --- a/host/chez/dynamic-var-defaults.ss +++ b/host/chez/dynamic-var-defaults.ss @@ -28,3 +28,35 @@ ;; *print-meta* — when true, pr prints metadata with a ^ prefix; default false. (def-var! "clojure.core" "*print-meta*" #f) + +;; Portable clojure.core dynamic vars whose DEFAULT already matches jolt's +;; behaviour, so exposing them is sound (resolve/binding work, reads return the +;; right value) — not a silent divergence. The ones that change observable output +;; if set non-default (*print-length* / *print-level* / *default-data-reader-fn*) +;; are deliberately NOT interned until the printer/reader honour them. +;; +;; *read-eval* — gates #=() read-eval. jolt's reader has no #=, so it reads true +;; (no eval-on-read happens regardless); a lib can (binding [*read-eval* false] …). +(def-var! "clojure.core" "*read-eval*" #t) +;; *print-dup* — gates print-dup (a multimethod that exists); default false. +(def-var! "clojure.core" "*print-dup*" #f) +;; *print-namespace-maps* — jolt never prints the #:ns{…} map shorthand, so the +;; var reads false (accurate); settable for code that toggles it. +(def-var! "clojure.core" "*print-namespace-maps*" #f) +;; *flush-on-newline* — jolt flushes line output; default true. +(def-var! "clojure.core" "*flush-on-newline*" #t) +;; *compile-files* — jolt has no separate compile phase that emits .class files. +(def-var! "clojure.core" "*compile-files*" #f) +;; *math-context* — BigDecimal rounding context; nil = unlimited, jolt's default. +(def-var! "clojure.core" "*math-context*" jolt-nil) +;; *command-line-args* — the args after the script/-main; nil outside a -m run. +(def-var! "clojure.core" "*command-line-args*" jolt-nil) +;; *file* — the source file being loaded; "NO_SOURCE_PATH" when none, like the JVM. +(def-var! "clojure.core" "*file*" "NO_SOURCE_PATH") + +;; REPL result/exception history. Bound by the REPL after each evaluation; nil +;; outside a REPL, which is what reading them returns here. +(def-var! "clojure.core" "*1" jolt-nil) +(def-var! "clojure.core" "*2" jolt-nil) +(def-var! "clojure.core" "*3" jolt-nil) +(def-var! "clojure.core" "*e" jolt-nil) diff --git a/host/chez/natives-reader.ss b/host/chez/natives-reader.ss index 458ad6a..a2ae9bb 100644 --- a/host/chez/natives-reader.ss +++ b/host/chez/natives-reader.ss @@ -16,20 +16,15 @@ (seq->list (jolt-seq names)))) jolt-nil) -;; --- reader-conditional / re-matcher: tagged maps (reader-conditional? + the -;; matcher consumers are overlay tagged-value predicates that read :jolt/type). +;; --- reader-conditional: a tagged map (reader-conditional? is an overlay +;; tagged-value predicate that reads :jolt/type). re-matcher / re-find / re-groups +;; are the stateful matcher API in regex.ss. (define nr-kw-type (keyword "jolt" "type")) (define nr-kw-rc (keyword "jolt" "reader-conditional")) (define nr-kw-form (keyword #f "form")) (define nr-kw-spl (keyword #f "splicing?")) -(define nr-kw-mat (keyword "jolt" "matcher")) -(define nr-kw-re (keyword #f "re")) -(define nr-kw-s (keyword #f "s")) -(define nr-kw-pos (keyword #f "pos")) (define (nr-reader-conditional form splicing?) (jolt-hash-map nr-kw-type nr-kw-rc nr-kw-form form nr-kw-spl splicing?)) -(define (nr-re-matcher re s) - (jolt-hash-map nr-kw-type nr-kw-mat nr-kw-re re nr-kw-s s nr-kw-pos 0.0)) ;; --- macroexpand-1 / macroexpand: expand a (quoted) call form via the runtime ;; macro table (host-contract hc-macro?/hc-expand-1; forward-referenced, resolved @@ -47,6 +42,13 @@ (def-var! "clojure.core" "__reader-features" nr-reader-features-get) (def-var! "clojure.core" "__reader-features-set!" nr-reader-features-set!) (def-var! "clojure.core" "reader-conditional" nr-reader-conditional) -(def-var! "clojure.core" "re-matcher" nr-re-matcher) (def-var! "clojure.core" "macroexpand-1" nr-macroexpand-1) + +;; letfn is a special form (the analyzer lowers it to letrec*, checked before any +;; macro), but on the JVM it is also a clojure.core macro that (resolve 'letfn) +;; finds — like let / loop / fn here. Intern a var so resolution matches; the value +;; is never invoked (the analyzer handles every (letfn …) form), and it is NOT +;; marked a macro, so macroexpand leaves a letfn form alone (it is special). +(def-var! "clojure.core" "letfn" + (lambda args (jolt-throw (jolt-ex-info "letfn is a special form" (jolt-hash-map))))) (def-var! "clojure.core" "macroexpand" nr-macroexpand) diff --git a/host/chez/regex.ss b/host/chez/regex.ss index f38d0b7..3d071a0 100644 --- a/host/chez/regex.ss +++ b/host/chez/regex.ss @@ -182,9 +182,41 @@ (let ((m (irregex-match (regex-t-irx (jolt-re-pattern re)) s))) (if m (irx-result m) jolt-nil))) -(define (jolt-re-find re s) - (let ((m (irregex-search (regex-t-irx (jolt-re-pattern re)) s))) - (if m (irx-result m) jolt-nil))) +;; A stateful matcher (java.util.regex.Matcher): the compiled pattern, the target +;; string, the next search position, and the last successful irregex match. re-find +;; over a matcher steps through non-overlapping matches; re-groups returns the +;; groups of the last one. +(define-record-type matcher-t + (fields irx str (mutable pos) (mutable last)) + (nongenerative jolt-matcher-v1)) +(define (jolt-re-matcher re s) + (make-matcher-t (regex-t-irx (jolt-re-pattern re)) s 0 #f)) +(define (jolt-matcher? x) (matcher-t? x)) + +;; re-find: stateless over (re s), or stateful over a matcher (advance + remember). +(define jolt-re-find + (case-lambda + ((re s) + (let ((m (irregex-search (regex-t-irx (jolt-re-pattern re)) s))) + (if m (irx-result m) jolt-nil))) + ((m) + (let* ((str (matcher-t-str m)) + (len (string-length str)) + (start (matcher-t-pos m)) + (mm (and (<= start len) (irregex-search (matcher-t-irx m) str start)))) + (if mm + (let ((e (irregex-match-end-index mm 0))) + (matcher-t-last-set! m mm) + (matcher-t-pos-set! m (if (> e start) e (+ start 1))) + (irx-result mm)) + (begin (matcher-t-last-set! m #f) jolt-nil)))))) + +;; re-groups: the groups of the matcher's last successful find. Throws when no +;; match has succeeded, like Clojure's IllegalStateException "No match found". +(define (jolt-re-groups m) + (let ((last (matcher-t-last m))) + (if last (irx-result last) + (jolt-throw (jolt-ex-info "No match found" (jolt-hash-map)))))) ;; All non-overlapping matches, left to right. Advance past each match end (or by ;; one on a zero-width match). nil when there are no matches (Clojure: seq-able as @@ -203,4 +235,6 @@ (def-var! "clojure.core" "re-matches" jolt-re-matches) (def-var! "clojure.core" "re-find" jolt-re-find) (def-var! "clojure.core" "re-seq" jolt-re-seq) +(def-var! "clojure.core" "re-matcher" jolt-re-matcher) +(def-var! "clojure.core" "re-groups" jolt-re-groups) (def-var! "clojure.core" "regex?" jolt-regex?) diff --git a/test/chez/corpus.edn b/test/chez/corpus.edn index e9d99f5..36bc392 100644 --- a/test/chez/corpus.edn +++ b/test/chez/corpus.edn @@ -1827,6 +1827,14 @@ {:suite "regex / char-class dash" :label "dash + escaped dot in class" :expected "\"a.b_c-d\"" :actual "(re-matches #\"[\\w-_\\.]+\" \"a.b_c-d\")"} {:suite "regex / char-class dash" :label "trailing dash in class" :expected "[\"a-b\" \"c-d\"]" :actual "(vec (re-seq #\"[\\w-]+\" \"a-b c-d\"))"} {:suite "regex / nested quantifier" :label "(X+)* parses and matches" :expected "true" :actual "(boolean (re-matches #\"([^%]+)*(%(d))?\" \"abc\"))"} + {:suite "regex / matcher" :label "re-find over a matcher" :expected "\"1\"" :actual "(re-find (re-matcher #\"\\d+\" \"a1b22\"))"} + {:suite "regex / matcher" :label "stateful step + re-groups" :expected "[[\"12-34\" \"12\" \"34\"] [\"12-34\" \"12\" \"34\"] [\"56-78\" \"56\" \"78\"] nil]" :actual "(let [m (re-matcher #\"(\\d+)-(\\d+)\" \"12-34 56-78\")] [(re-find m) (re-groups m) (re-find m) (re-find m)])"} + {:suite "regex / matcher" :label "re-groups before a match throws" :expected :throws :actual "(re-groups (re-matcher #\"\\d\" \"abc\"))"} + {:suite "dynamic vars / portable defaults" :label "default reads" :expected "[true false true false]" :actual "[*read-eval* *print-dup* *flush-on-newline* *compile-files*]"} + {:suite "dynamic vars / portable defaults" :label "binding *read-eval*" :expected "false" :actual "(binding [*read-eval* false] *read-eval*)"} + {:suite "special forms / letfn" :label "mutual recursion" :expected "true" :actual "(letfn [(e? [n] (if (zero? n) true (o? (dec n)))) (o? [n] (if (zero? n) false (e? (dec n))))] (e? 10))"} + {:suite "core vars / present and callable" :label "*out* *err* *warn-on-reflection* await restart-agent" :expected "[true true false true true]" :actual "[(boolean *out*) (boolean *err*) *warn-on-reflection* (ifn? await) (ifn? restart-agent)]"} + {:suite "dynamic vars / portable defaults" :label "*data-readers* map, *math-context* nil" :expected "[true true]" :actual "[(map? *data-readers*) (nil? *math-context*)]"} {:suite "regex / re-pattern & string ops" :label "re-pattern build" :expected "\"hi\"" :actual "(re-find (re-pattern \"\\\\w+\") \"hi!\")"} {:suite "regex / re-pattern & string ops" :label "re-pattern is regex?" :expected "true" :actual "(regex? (re-pattern \"a\"))"} {:suite "regex / re-pattern & string ops" :label "split on regex" :expected "[\"a\" \"b\" \"c\"]" :actual "(do (require '[clojure.string :as s]) (s/split \"a1b2c\" #\"\\d\"))"}