From e4cbbb89121520bbea48bfe89bc5e03f80a9e4d9 Mon Sep 17 00:00:00 2001 From: Yogthos Date: Tue, 30 Jun 2026 23:44:22 -0400 Subject: [PATCH] fix REPL treating a regex literal as an unbalanced form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit repl-form-complete? entered the :regex state on '#' but only consumed the '#', so the opening '"' was then read by the :regex handler as the CLOSING quote. The regex body got scanned in :code state, and any delimiter or quote inside it (a group like #"(a)", a char class #"[0-9]+") threw off the paren/string count — so a one-line regex form was judged incomplete and the REPL hung waiting for continuation lines. Consume the '#"' together. Adds a self-checking predicate test (test/chez/repl-reader-test.clj, run via joltc so jolt.main resolves) and an end-to-end regex REPL case in smoke.sh. --- host/chez/smoke.sh | 23 +++++++++++++++++++++++ jolt-core/jolt/main.clj | 5 +++-- test/chez/repl-reader-test.clj | 27 +++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 test/chez/repl-reader-test.clj diff --git a/host/chez/smoke.sh b/host/chez/smoke.sh index af95858..0ca1882 100755 --- a/host/chez/smoke.sh +++ b/host/chez/smoke.sh @@ -75,6 +75,18 @@ else fails=$((fails + 1)) fi +# Unit-checks the REPL read-until-complete predicate over balanced/unbalanced, +# string, comment and regex-literal inputs. A multi-form `joltc run` so jolt.main +# is loaded and its private var resolves; the file self-checks and prints a sentinel. +rr_out="$(bin/joltc run test/chez/repl-reader-test.clj 2>/dev/null)" +if printf '%s' "$rr_out" | grep -q 'REPL-READER OK'; then + pass=$((pass + 1)) +else + echo " FAIL: repl-form-complete? predicate" + echo " $(printf '%s' "$rr_out" | grep REPL-READER | tail -1)" + fails=$((fails + 1)) +fi + # REPL must exit on :repl/quit / :exit — a reliable exit that works in any # terminal, unlike ^D (which some terminals/editors don't deliver as EOF). # Pipe: an evaluable form, the quit keyword, then a sentinel that must NOT run. @@ -107,5 +119,16 @@ else fails=$((fails + 1)) fi +# A single-line regex literal is complete on its own — the #" opens a regex whose +# body (delimiters, quotes and all) must not be miscounted as unbalanced parens. +repl_out="$(printf '(re-find #"(a)(b)" "ab")\n:exit\n' | bin/joltc repl 2>/dev/null)" +if printf '%s' "$repl_out" | grep -q 'ab' && ! printf '%s' "$repl_out" | grep -q 'error'; then + pass=$((pass + 1)) +else + echo " FAIL: repl should evaluate a one-line regex literal, not wait for more input" + printf '%s\n' "$repl_out" | sed 's/^/ | /' + fails=$((fails + 1)) +fi + echo "cli smoke: $pass passed, $fails failed" [ "$fails" -eq 0 ] diff --git a/jolt-core/jolt/main.clj b/jolt-core/jolt/main.clj index a0c1b72..6aa3542 100644 --- a/jolt-core/jolt/main.clj +++ b/jolt-core/jolt/main.clj @@ -103,8 +103,9 @@ (= c \;) (recur (inc i) depth :comment) (= c \\) (recur (+ i 2) depth :code) ; char literal: \( (= c \") (recur (inc i) depth :string) - (= c \#) (recur (inc i) depth - (if (= (get s (inc i)) \") :regex :code)) + (= c \#) (if (= (get s (inc i)) \") + (recur (+ i 2) depth :regex) ; consume the #" together + (recur (inc i) depth :code)) (#{\( \[ \{} c) (recur (inc i) (inc depth) :code) (#{\) \] \}} c) (recur (inc i) (dec depth) :code) :else (recur (inc i) depth :code)) diff --git a/test/chez/repl-reader-test.clj b/test/chez/repl-reader-test.clj new file mode 100644 index 0000000..16a41e3 --- /dev/null +++ b/test/chez/repl-reader-test.clj @@ -0,0 +1,27 @@ +;; Self-checking regression for the REPL's read-until-complete predicate +;; (jolt.main/repl-form-complete?), which decides whether a line buffer is a whole +;; form or the REPL should keep reading continuation lines. Runs via `bin/joltc run` +;; (jolt.main is loaded, so the private var resolves); prints a sentinel the smoke +;; gate greps. The regex cases are the ones that regressed: a #"..." literal opens a +;; regex whose body — parens, quotes and all — must not be miscounted as delimiters. +(require 'jolt.main) + +(def complete? jolt.main/repl-form-complete?) + +;; [input expected-complete?] +(def cases + [["(+ 1 2)" true] ; balanced + ["(+ 1" false] ; open paren -> keep reading + ["(defn g [] (foo (bar" false] ; deeply unbalanced + ["[1 2 {:a 3}]" true] ; mixed bracket types balance + ["(str \")\")" true] ; a close-paren inside a string doesn't count + ["\\(" true] ; a paren char literal doesn't count + ["(+ 1 2) ; ) ) trailing" true] ; parens in a line comment don't count + ["(re-find #\"(a)(b)\" \"ab\")" true] ; groups inside a regex must not count as depth + ["#\"[0-9]+\"" true] ; a bare regex literal is a complete form + ["#\"a(b" false]]) ; an unterminated regex is incomplete + +(let [bad (remove (fn [[in exp]] (= exp (boolean (complete? in)))) cases)] + (println (if (empty? bad) + "REPL-READER OK" + (str "REPL-READER FAIL " (pr-str (map first bad))))))