Fix four bugs surfaced by the commonmark-app example

- regex: bounded quantifiers {n,m} no longer expand exponentially. The
  desugaring inlined the continuation per optional level, doubling it each
  time, so {0,61} built a PEG the compiler expanded to ~2^61 nodes and hung.
  Each level is now its own grammar rule referenced by name (jolt-3xur).

- strings are a seqable of chars for vec/set/into, matching seq. They went
  through realize-for-iteration, which had no string case, so into/set got
  raw bytes (code points) and set threw; vec used string/from-bytes (1-char
  strings). realize-for-iteration now maps make-char over the bytes, and
  core-vec matches (jolt-dl4s).

- clojure.string/split takes Java Pattern.split limit semantics: negative
  keeps trailing empties, 0/omitted drops them (a no-match result stays
  [input]), positive caps the part count with the remainder unsplit. It used
  to delegate the limit to take, so a negative limit returned [] and the
  2-arg form never trimmed trailing empties (jolt-ik3a).

- System/exit is registered (maps to os/exit), so (System/exit n) works
  (jolt-w2wf).
This commit is contained in:
Yogthos 2026-06-16 23:41:28 -04:00
parent 8192fc9541
commit 91103fb52e
9 changed files with 70 additions and 13 deletions

View file

@ -51,10 +51,16 @@
(str-reverse-b s))
(defn split
([s re]
(vec (str-split re s)))
([s re] (split s re 0))
([s re limit]
(vec (take limit (str-split re s)))))
;; Java Pattern.split semantics: limit > 0 caps the parts (trailing empties
;; kept); limit < 0 splits fully and keeps trailing empties; limit 0 (the
;; default) splits fully then drops trailing empty strings — but a no-match
;; result ([input], the only 1-element case) is returned as-is.
(let [parts (vec (str-split re s (if (pos? limit) limit nil)))]
(if (and (zero? limit) (> (count parts) 1))
(loop [v parts] (if (and (seq v) (= "" (peek v))) (recur (pop v)) v))
parts))))
(defn split-lines
"Split s on \\n or \\r\\n, returning a vector of lines."

View file

@ -418,7 +418,7 @@
(array? coll) (make-vec coll)
(tuple? coll) (make-vec coll)
(struct? coll) (make-vec (map |(in (kvs coll) (+ (* $ 2) 1)) (range (/ (length (kvs coll)) 2))))
(string? coll) (make-vec (map |(string/from-bytes $) (string/bytes coll)))
(string? coll) (make-vec (map make-char (string/bytes coll)))
(make-vec @[]))))
# Bulk-build a map value from a native array of [k v] pairs, mirroring

View file

@ -247,10 +247,16 @@
# (core/20-coll.clj) — pure over get on the tagged value the constructor builds.
# String split/replace that accept either a literal string or a regex value.
(defn core-str-split [pat s]
(defn core-str-split [pat s &opt limit]
(if (regex? pat)
(re-split pat s)
(string/split pat s)))
(re-split pat s limit)
(let [parts (string/split pat s)]
# literal separator: a positive limit caps the part count; rejoin the tail
# with the (constant) separator to recover the unsplit remainder.
(if (and limit (> limit 0) (> (length parts) limit))
(array/push (array/slice parts 0 (- limit 1))
(string/join (array/slice parts (- limit 1)) pat))
parts))))
(defn core-str-replace-all [pat repl s]
(if (regex? pat)
(re-replace-all pat s repl)

View file

@ -167,6 +167,9 @@
(phm? c) (phm-entries c)
# sorted colls iterate their comparator-ordered entries/elements
(core-sorted? c) (sorted-entries-arr c)
# a string is a seqable of CHARS in Clojure (not bytes/1-char strings) — mirror
# core-seq so vec/set/into over a string agree with seq (jolt-dl4s)
(string? c) (tuple ;(map make-char (string/bytes c)))
# byte array (Janet buffer) -> array of byte values
(buffer? c) (let [a @[]] (each x c (array/push a x)) a)
# struct map literal (no :jolt/type marker — not a symbol/char) -> entries

View file

@ -82,7 +82,9 @@
"file.separator" "/"
"user.dir" (os/cwd)
"user.home" (or (os/getenv "HOME") "")
"java.io.tmpdir" (or (os/getenv "TMPDIR") "/tmp")})})
"java.io.tmpdir" (or (os/getenv "TMPDIR") "/tmp")})
# terminate the process with the given status code
"exit" (fn [&opt status] (os/exit (if (nil? status) 0 status)))})
# sentinels portable code compares against. jolt numbers are doubles, so these
# are the f64 approximations.

View file

@ -292,10 +292,18 @@
~(choice ,(emit item (keyword r)) ,k)
~(choice ,k ,(emit item (keyword r)))))
(keyword r))
(do (var acc k) (var c (- hi lo))
# Each optional level is its OWN grammar rule that
# references the previous level by name. Inlining instead
# (choice (emit item acc) acc) duplicates `acc` per level,
# so {0,n} built a structure the PEG compiler expanded to
# 2^n nodes and hung (jolt-3xur). Naming keeps it O(n).
(do (def kr (fresh)) (put grammar kr k)
(var acc (keyword kr)) (var c (- hi lo))
(while (> c 0)
(set acc (if greedy ~(choice ,(emit item acc) ,acc)
~(choice ,acc ,(emit item acc))))
(let [r (fresh)]
(put grammar r (if greedy ~(choice ,(emit item acc) ,acc)
~(choice ,acc ,(emit item acc))))
(set acc (keyword r)))
(-- c))
acc)))
(var acc tail) (var c lo)
@ -401,10 +409,14 @@
# `(if-let [m (re-seq ...)] ...)` works — an empty seq would be truthy.
(if (= 0 (length out)) nil out))
(defn re-split [re s]
(defn re-split [re s &opt limit]
# limit (Java Pattern.split n>0): at most `limit` parts — stop after limit-1
# splits and keep the rest of the string as the final unsplit part. nil/<=0
# splits at every match.
(def re (re-pattern re))
(def lim (if (and limit (> limit 0)) limit nil))
(def out @[]) (var pos 0) (var last 0)
(while (<= pos (length s))
(while (and (<= pos (length s)) (or (nil? lim) (< (length out) (dec lim))))
(def g (match-at re s pos))
(if (and g (> (length (in g 0)) 0))
(do (array/push out (string/slice s last pos))

View file

@ -159,6 +159,8 @@
"[(Boolean/parseBoolean \"true\") (Boolean/parseBoolean \"false\") (Boolean/parseBoolean \"yes\")]"]
["System/getenv is a map" "true"
"(string? (get (System/getenv) \"HOME\"))"]
# registered, so (System/exit n) resolves; actual exit verified via subprocess
["System/exit resolves" "true" "(fn? System/exit)"]
# NOT every? alone — it held vacuously while seq over a raw host table
# yielded nothing, hiding that read-system-env came back empty
["getenv entries destructure (non-empty)" "true"

View file

@ -62,3 +62,13 @@
[".matches partial -> false" "false" "(.matches \"abcd\" \"a.c\")"]
[".replaceAll regex" "\"a-b-c\"" "(.replaceAll \"a_b_c\" \"_\" \"-\")"]
[".replaceFirst regex" "\"a-b_c\"" "(.replaceFirst \"a_b_c\" \"_\" \"-\")"])
(defspec "regex / bounded quantifiers (jolt-3xur)"
["exact {n}" "\"aaa\"" "(re-matches #\"a{3}\" \"aaa\")"]
["range {n,m} max" "\"aaaa\"" "(re-find #\"a{2,4}\" \"aaaaa\")"]
["zero lower {0,n}" "\"aa\"" "(re-find #\"a{0,3}\" \"aa\")"]
["{n,} unbounded" "\"aaaa\"" "(re-find #\"a{2,}\" \"aaaa\")"]
# nested bounded quantifiers must not blow up the PEG compiler — this used to
# expand to ~2^61 nodes and hang at compile time.
["nested bounds compile" "true"
"(boolean (re-matches #\"[a-z](?:[a-z]{0,61}[a-z])?(?:-[a-z]{0,61}[a-z])*\" \"abc-def\"))"])

View file

@ -20,6 +20,22 @@
["pr-str of a defn" "\"#'user/gg\"" "(pr-str (defn gg [x] x))"]
["seq of string" "[\\a \\b]" "(seq \"ab\")"])
(defspec "string as a seqable of chars (jolt-dl4s)"
["vec of string" "[\\a \\b]" "(vec \"ab\")"]
["into [] of string" "[\\a \\b]" "(into [] \"ab\")"]
["set of string" "true" "(= #{\\a \\b} (set \"ab\"))"]
["into #{} of string" "true" "(= #{\\a \\b} (into #{} \"ab\"))"]
["set dedups chars" "2" "(count (set \"aab\"))"]
["mapv over string" "[\\a \\b]" "(mapv identity \"ab\")"])
(defspec "clojure.string / split limit (jolt-ik3a)"
["neg keeps trailing" "[\"a\" \"\" \"\"]" "(do (require (quote [clojure.string :as s])) (s/split \"a,,\" #\",\" -1))"]
["zero trims trailing" "[\"a\"]" "(do (require (quote [clojure.string :as s])) (s/split \"a,,\" #\",\" 0))"]
["omitted trims" "[\"a\"]" "(do (require (quote [clojure.string :as s])) (s/split \"a,,\" #\",\"))"]
["positive caps parts" "[\"a\" \"b,c\"]" "(do (require (quote [clojure.string :as s])) (s/split \"a,b,c\" #\",\" 2))"]
["empty string" "[\"\"]" "(do (require (quote [clojure.string :as s])) (s/split \"\" #\",\"))"]
["interior empties kept" "[\"a\" \"\" \"b\"]" "(do (require (quote [clojure.string :as s])) (s/split \"a,,b\" #\",\"))"])
(defspec "clojure.string"
["join" "\"a,b,c\"" "(do (require (quote [clojure.string :as s])) (s/join \",\" [\"a\" \"b\" \"c\"]))"]
["join no sep" "\"abc\"" "(do (require (quote [clojure.string :as s])) (s/join [\"a\" \"b\" \"c\"]))"]