feat: proper char type ({:jolt/type :jolt/char :ch N}) — reader literals, char?/char/int/str, =, seq/first/rest/nth of strings yield chars, char printing; fix REPL multi-line input accumulation

This commit is contained in:
Yogthos 2026-06-04 15:40:59 -04:00
parent e1556fd102
commit 0d841ef83d
7 changed files with 104 additions and 24 deletions

View file

@ -9,6 +9,10 @@
# Predicates
# ============================================================
(defn core-char? [x] (and (struct? x) (= :jolt/char (x :jolt/type))))
(defn char-code [c] (c :ch))
(defn char->string [c] (string/from-bytes (c :ch)))
(defn core-nil? [x] (nil? x))
(defn core-not [x] (if x false true))
(defn core-some? [x] (not (nil? x)))
@ -302,15 +306,16 @@
(core-sorted-set? coll) (let [i (coll :items)] (if (empty? i) nil (in i 0)))
(lazy-seq? coll) (ls-first coll)
(or (nil? coll) (= 0 (length coll))) nil
(string? coll) (make-char (in coll 0))
(in coll 0)))
(defn core-rest [coll]
(if (lazy-seq? coll) (ls-rest coll)
(if (or (nil? coll) (= 0 (length coll)))
@[]
(if (tuple? coll)
(tuple/slice coll 1)
(array/slice coll 1)))))
(cond
(lazy-seq? coll) (ls-rest coll)
(or (nil? coll) (= 0 (length coll))) @[]
(string? coll) (tuple ;(map make-char (string/bytes (string/slice coll 1))))
(tuple? coll) (tuple/slice coll 1)
(array/slice coll 1)))
(defn core-next [coll]
(let [r (core-rest coll)]
@ -329,7 +334,7 @@
(set? coll) (phs-seq coll)
(phm? coll) (tuple ;(phm-entries coll))
(tuple? coll) (tuple/slice coll)
(string? coll) (map |(string/from-bytes $) (string/bytes coll))
(string? coll) (if (= 0 (length coll)) nil (tuple ;(map make-char (string/bytes coll))))
(struct? coll) (tuple ;(keys coll))
coll))
@ -662,7 +667,7 @@
(do
(var c (if (lazy-seq? coll) (realize-ls coll) coll))
(if (and (>= idx 0) (< idx (length c)))
(in c idx)
(if (string? c) (make-char (in c idx)) (in c idx))
(if (nil? default)
(error (string "Index " idx " out of bounds, length: " (length c)))
default)))))
@ -1022,6 +1027,12 @@
(string? v) (do (buffer/push-string buf "\"") (buffer/push-string buf v) (buffer/push-string buf "\""))
(buffer? v) (do (buffer/push-string buf "\"") (buffer/push-string buf (string v)) (buffer/push-string buf "\""))
(keyword? v) (do (buffer/push-string buf ":") (buffer/push-string buf (string v)))
(core-char? v) (do (buffer/push-string buf "\\")
(buffer/push-string buf
(case (v :ch)
10 "newline" 32 "space" 9 "tab" 13 "return"
12 "formfeed" 8 "backspace" 0 "nul"
(char->string v))))
(regex? v) (do (buffer/push-string buf "#\"") (buffer/push-string buf (v :source)) (buffer/push-string buf "\""))
(number? v) (buffer/push-string buf (string v))
(and (struct? v) (= :symbol (v :jolt/type)))
@ -1046,6 +1057,7 @@
(nil? v) ""
(string? v) v
(buffer? v) (string v)
(core-char? v) (char->string v)
(keyword? v) (string ":" (string v))
(and (struct? v) (= :symbol (v :jolt/type)))
(if (v :ns) (string (v :ns) "/" (v :name)) (v :name))
@ -1156,7 +1168,14 @@
# Integer coercion
# ============================================================
(def core-int (fn [x] (math/trunc x)))
(def core-int (fn [x] (if (core-char? x) (x :ch) (math/trunc x))))
(defn core-char [x]
"(char code-or-char) -> a character value."
(cond
(core-char? x) x
(number? x) (make-char (math/trunc x))
(string? x) (make-char (in x 0))
(error "char expects a number or character")))
(def core-unchecked-inc (fn [x] (+ x 1)))
(def core-unchecked-dec (fn [x] (- x 1)))
(def core-unchecked-add (fn [& xs] (+ ;xs)))
@ -2487,6 +2506,8 @@
"unsigned-bit-shift-right" core-unsigned-bit-shift-right
# Integer coercion / unchecked math
"int" core-int
"char" core-char
"char?" core-char?
"unchecked-inc" core-unchecked-inc
"unchecked-dec" core-unchecked-dec
"unchecked-add" core-unchecked-add

View file

@ -1139,6 +1139,8 @@
(struct? form)
(if (= :symbol (form :jolt/type))
(resolve-sym ctx bindings form)
(if (= :jolt/char (form :jolt/type))
form
(if (= :jolt/set (form :jolt/type))
(apply make-phs (form :value))
(if (= :jolt/tagged (form :jolt/type))
@ -1152,7 +1154,7 @@
(error (string "No reader function for tag " tag))))
(if (get form :jolt/type)
(error (string "Unexpected tagged form: " (form :jolt/type)))
form))))
form)))))
(array? form)
(if (= 0 (length form))
@[]

View file

@ -4,6 +4,7 @@
(use ./api)
(use ./types)
(use ./phm)
(use ./reader)
(def ctx (init))
(ctx-set-current-ns ctx "user")
@ -140,6 +141,12 @@
(number? v) (push-str buf (string v))
(string? v) (push-str buf v)
(keyword? v) (do (push-str buf ":") (push-str buf (string v)))
(and (struct? v) (= :jolt/char (get v :jolt/type)))
(do (push-str buf "\\")
(push-str buf (case (v :ch)
10 "newline" 32 "space" 9 "tab" 13 "return"
12 "formfeed" 8 "backspace" 0 "nul"
(string/from-bytes (v :ch)))))
(and (struct? v) (= :symbol (get v :jolt/type)))
(let [ns (get v :ns) name (get v :name)]
(if ns
@ -161,12 +168,25 @@
(print "Type (exit) to quit.\n")
(var running true)
(var pending "") # accumulates a form split across multiple input lines
(while running
(let [line (read-line (string (ctx-current-ns ctx) "=> "))]
(if (nil? line) (set running false)
(if (= line "(exit)") (set running false)
(if (not (= "" line))
(try
(print-value (eval-string ctx line))
([err]
(eprint "Error: " err)))))))))
(let [prompt (if (= pending "") (string (ctx-current-ns ctx) "=> ") " #_=> ")
line (read-line prompt)]
(cond
(nil? line) (set running false)
(let [input (if (= pending "") line (string pending "\n" line))
trimmed (string/trim input)]
(cond
(= trimmed "(exit)") (set running false)
(= trimmed "") (set pending "")
# Try to parse the accumulated input; if it's an incomplete form
# (unterminated list/vector/map/string), keep reading more lines.
(let [parsed (protect (parse-string input))]
(if (and (= (parsed 0) false)
(string/find "nterminated" (string (parsed 1))))
(set pending input)
(do
(set pending "")
(try
(print-value (eval-string ctx input))
([err] (eprint "Error: " err))))))))))))

View file

@ -8,6 +8,8 @@
# Maps {:a 1} → Janet struct {:a 1}
# Sets #{1 2} → tagged struct {:jolt/type :jolt/set :value [1 2]}
(use ./types)
# Forward declaration for mutual recursion
(var read-form nil)
@ -274,10 +276,10 @@
pos))
(defn read-char [s pos]
# pos is at backslash
# pos is at backslash; produce a char value directly (self-evaluating)
(let [end (read-char-name-end s (+ pos 1))
char-name (string/slice s (+ pos 1) end)]
[{:jolt/type :char :name char-name} end]))
[(char-from-name char-name) end]))
(defn read-anon-fn [s pos]
# pos is at #, next char is (

View file

@ -9,6 +9,24 @@
# Symbols are represented as {:jolt/type :symbol :ns <string-or-nil> :name <string>}
# as produced by the reader.
# Characters are {:jolt/type :jolt/char :ch <codepoint>}, distinct from strings.
(defn make-char [code] {:jolt/type :jolt/char :ch code})
(def- char-named @{"newline" 10 "space" 32 "tab" 9 "return" 13
"formfeed" 12 "backspace" 8 "newpage" 12 "nul" 0})
(defn char-from-name
"Resolve a reader char-literal name (\\a, \\newline, \\uNNNN, \\oNNN) to a char value."
[name]
(cond
(= 1 (length name)) (make-char (in name 0))
(get char-named name) (make-char (get char-named name))
(and (> (length name) 1) (= (in name 0) (get "u" 0)))
(make-char (scan-number (string "16r" (string/slice name 1))))
(and (> (length name) 1) (= (in name 0) (get "o" 0)))
(make-char (scan-number (string "8r" (string/slice name 1))))
(error (string "Unsupported character: \\" name))))
# ============================================================
# Symbol helpers
# ============================================================