Skip to content

Commit ebe819f

Browse files
authored
Merge pull request #11 from BuddhiLW/staging
v1.13.2 — clj-parity: munge/demunge $, slurp on *in*, stack-element-str
2 parents 623974f + e5a495b commit ebe819f

9 files changed

Lines changed: 163 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,33 @@ first stable `1.0.0` tag; pre-1.0 `alpha` / `rc` tags may still change surfaces.
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- **`clojure.core/munge`.** Escapes each character illegal in a Java identifier
13+
as a `_TOKEN_` (the inverse of `clojure.repl/demunge`), preserving the
14+
argument's type: a string munges to a string, a symbol to a symbol.
15+
`(munge "foo-bar?")` is `"foo_bar_QMARK_"`. cljw carried `namespace-munge`
16+
and `demunge` but not the full `munge`; found while writing dev tooling.
17+
18+
- **`clojure.repl/stack-element-str`.** Renders one stack frame as
19+
`<ns>/<fn> (<file>:<line>)`. cljw has no JVM `StackTraceElement` and only
20+
Clojure frames, so it takes the cljw-native frame map `{:ns :fn :file :line}`
21+
(as returned by `stack-trace`) and produces exactly clj's Clojure-frame form,
22+
e.g. `"clojure.core/map (core.clj:2743)"`.
23+
24+
### Fixed
25+
26+
- **`clojure.repl/demunge` maps the `$` nested-class separator to `/`.**
27+
`(demunge "clojure.core$map")` now returns `"clojure.core/map"` as in clj;
28+
cljw reversed the munge tokens but left `$` in place.
29+
30+
- **`slurp` reads from `*in*` and any text reader.** `(slurp *in*)` and
31+
`(with-in-str s (slurp *in*))` raised "expected string, got host_instance"
32+
because the `*in*` reader is a cljw-native text reader, not a host stream, so
33+
it fell through slurp's reader arm. slurp now drains a text reader's unread
34+
remainder (blocking to stdin EOF for `*in*`), and it respects a prior
35+
`read-line`, matching clj.
36+
1037
## [1.13.1] - 2026-08-31
1138

1239
### Added

‎data/placement.yaml‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@
2424
schema_version: "2.0"
2525
summary:
2626
namespaces_required: 34
27-
total: 1418
27+
total: 1421
2828
zig: 405
29-
clj: 793
30-
value: 214
29+
clj: 795
30+
value: 215
3131
unbound: 6
3232
vars:
3333
"cljw.error/*error-context*": { impl: value, flags: "dynamic" }
@@ -626,6 +626,8 @@ vars:
626626
"clojure.core/mod": { impl: zig, flags: "-" }
627627
"clojure.core/monitor-enter": { impl: zig, flags: "-" }
628628
"clojure.core/monitor-exit": { impl: zig, flags: "-" }
629+
"clojure.core/munge": { impl: clj, flags: "-" }
630+
"clojure.core/munge-char-map": { impl: value, flags: "private" }
629631
"clojure.core/name": { impl: zig, flags: "-" }
630632
"clojure.core/namespace": { impl: zig, flags: "-" }
631633
"clojure.core/namespace-munge": { impl: clj, flags: "-" }
@@ -1037,6 +1039,7 @@ vars:
10371039
"clojure.repl/root-cause": { impl: clj, flags: "-" }
10381040
"clojure.repl/source": { impl: clj, flags: "macro" }
10391041
"clojure.repl/source-fn": { impl: clj, flags: "-" }
1042+
"clojure.repl/stack-element-str": { impl: clj, flags: "-" }
10401043
"clojure.set/difference": { impl: clj, flags: "-" }
10411044
"clojure.set/index": { impl: clj, flags: "-" }
10421045
"clojure.set/intersect2": { impl: clj, flags: "private" }

‎src/lang/clj/clojure/core.clj‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,6 +1812,24 @@
18121812
"Convert a Clojure namespace name to a legal Java package name."
18131813
[ns]
18141814
(.replace (str ns) \- \_))
1815+
1816+
;; `munge` escapes every character illegal in a Java identifier as a `_TOKEN_`
1817+
;; (the inverse of clojure.repl/demunge). clj routes through Compiler.munge;
1818+
;; cljw carries the same char-map here. The map is a cljw implementation
1819+
;; detail (clj has no `clojure.core/char-map`), so it stays ^:private.
1820+
(def ^:private munge-char-map
1821+
{\- "_", \: "_COLON_", \+ "_PLUS_", \> "_GT_", \< "_LT_", \= "_EQ_",
1822+
\~ "_TILDE_", \! "_BANG_", \@ "_CIRCA_", \# "_SHARP_", \' "_SINGLEQUOTE_",
1823+
\" "_DOUBLEQUOTE_", \% "_PERCENT_", \^ "_CARET_", \& "_AMPERSAND_",
1824+
\* "_STAR_", \| "_BAR_", \{ "_LBRACE_", \} "_RBRACE_", \[ "_LBRACK_",
1825+
\] "_RBRACK_", \/ "_SLASH_", \\ "_BSLASH_", \? "_QMARK_"})
1826+
1827+
(defn munge
1828+
"Munges a name string or symbol into a legal Java identifier, escaping each
1829+
special character as a `_TOKEN_`. Returns the same type as its argument."
1830+
[s]
1831+
((if (symbol? s) symbol str)
1832+
(apply str (map (fn [c] (get munge-char-map c c)) (str s)))))
18151833
;; `(take-nth n)` / `(take-nth n coll)` — every nth item. The 1-arg form is a
18161834
;; stateful transducer (emit when the running index is a multiple of n); the
18171835
;; 2-arg form is lazy.

‎src/lang/clj/clojure/repl.clj‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,22 @@
2525
(clojure.string/replace "_EQ_" "=")
2626
(clojure.string/replace "_PLUS_" "+")
2727
(clojure.string/replace "_SLASH_" "/")
28+
(clojure.string/replace "$" "/")
2829
(clojure.string/replace "_" "-")))
2930

31+
;; `stack-element-str` renders one cljw stack frame map {:ns :fn :file :line}
32+
;; as `<ns>/<fn> (<file>:<line>)`. clj takes a JVM StackTraceElement and yields
33+
;; `class.method (file:line)` for a Java frame or a demunged `ns/fn` for a
34+
;; Clojure frame; cljw has no StackTraceElement and only Clojure frames
35+
;; (ADR-0059 / ADR-0120), so it takes the cljw-native frame map and produces
36+
;; exactly clj's Clojure-frame form. Same render string as
37+
;; clojure.stacktrace/print-trace-element (both fixed by ADR-0140).
38+
(defn stack-element-str
39+
"Returns a string representation of one cljw stack frame map
40+
{:ns :fn :file :line}, as `<ns>/<fn> (<file>:<line>)`."
41+
[{:keys [ns fn file line]}]
42+
(str (when ns (str ns "/")) fn " (" (or file "") ":" line ")"))
43+
3044
(defn dir-fn
3145
"Returns a sorted seq of symbols naming public vars in
3246
a namespace or namespace alias."

‎src/lang/primitive/file_io.zig‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const keyword_mod = @import("../../runtime/keyword.zig");
2121
const print_mod = @import("../../runtime/print.zig");
2222
const host_instance = @import("../../runtime/host_instance.zig");
2323
const host_stream = @import("../../runtime/io/host_stream.zig");
24+
const text_io = @import("../../runtime/io/text_io.zig");
2425

2526
/// Coerce the file arg of `slurp`/`spit` to a path string: a `.string` directly,
2627
/// or a `java.io.File` host instance (its path lives in `state[0]`, ADR-0126) —
@@ -56,6 +57,10 @@ fn raiseFileIoError(op: []const u8, path: []const u8, e: anyerror, loc: SourceLo
5657
pub fn slurp(rt: *Runtime, env: *Env, args: []const Value, loc: SourceLocation) anyerror!Value {
5758
_ = env;
5859
try error_catalog.checkArity("slurp", args, 1, loc);
60+
// The `*in*` / with-in-str reader is a text_io Reader (not a host_stream
61+
// stream): drain its unread remainder (blocking to stdin EOF for `*in*`).
62+
if (try text_io.drainRemaining(rt, args[0])) |rest_bytes|
63+
return try string_collection.alloc(rt, rest_bytes);
5964
// D-471 IOFactory arm: an open Reader/InputStream drains its remainder
6065
// (clj's slurp routes any IOFactory-coercible arg through io/reader).
6166
if (host_stream.drainRemaining(args[0])) |rest_bytes|

‎src/runtime/io/text_io.zig‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,30 @@ pub fn isTextReader(v: Value) bool {
218218
host_instance.asHostInstance(v).descriptor == &reader_descriptor;
219219
}
220220

221+
/// The unread remainder of a text_io Reader as bytes, for `slurp` / `(slurp
222+
/// *in*)`. Null unless `v` is a text_io Reader. A stdin reader blocks pulling
223+
/// process stdin to EOF first; a pending 1-slot pushback is re-inserted ahead of
224+
/// the cursor so it is included. Advances the cursor to the end; the returned
225+
/// slice points into the reader's own buffer, so the caller copies it.
226+
pub fn drainRemaining(rt: *Runtime, v: Value) !?[]const u8 {
227+
if (!isTextReader(v)) return null;
228+
const st = readerStateOf(v);
229+
if (st.stdin) {
230+
while (fillFromStdin(rt, st)) {
231+
// drain every stdin chunk into st.data until EOF
232+
}
233+
}
234+
if (st.pushback) |cp| {
235+
var buf: [4]u8 = undefined;
236+
const n = std.unicode.utf8Encode(cp, &buf) catch 0;
237+
if (n > 0) try st.data.insertSlice(rt.gc.infra, st.pos, buf[0..n]);
238+
st.pushback = null;
239+
}
240+
const rest = st.data.items[st.pos..];
241+
st.pos = st.data.items.len;
242+
return rest;
243+
}
244+
221245
/// Decode the codepoint at `st.pos` + its UTF-8 byte length, or null at EOF.
222246
/// Invalid UTF-8 degrades to the raw byte as a 1-byte codepoint (no crash).
223247
const Decoded = struct { cp: u21, len: usize };

‎test/clj/suites/io_test.clj‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,16 @@
3838
(is (= :loaded @(resolve 'load-file-fixture-marker))))
3939
(testing "*file* is bound during the load (not the default)"
4040
(is (some? @(resolve 'load-file-fixture-file))))))
41+
42+
;; --- slurp on a Reader (*in* / with-in-str) ---
43+
;; slurp drains a text_io Reader's unread remainder. The `*in*` / with-in-str
44+
;; reader is a text_io Reader, not a host_stream stream, so it fell through the
45+
;; IOFactory arm and slurp raised "expected string". Found writing dev tooling
46+
;; that did (slurp *in*).
47+
(deftest slurp-drains-a-reader
48+
(testing "with-in-str: slurp returns the whole string"
49+
(is (= "hello world" (with-in-str "hello world" (slurp *in*)))))
50+
(testing "slurp drains only the UNREAD remainder after read-line"
51+
(is (= "ab|cd\nef" (with-in-str "ab\ncd\nef" (str (read-line) "|" (slurp *in*))))))
52+
(testing "an empty reader slurps to an empty string"
53+
(is (= "" (with-in-str "" (slurp *in*))))))

‎test/clj/suites/repl_test.clj‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
;; clojure.repl parity (F-011).
2+
(ns suites.repl-test
3+
(:require [clojure.test :refer [deftest is testing]]
4+
[clojure.repl :as repl]))
5+
6+
;; --- demunge ---
7+
;; demunge maps every `$` (the JVM nested-class separator) to `/`, on top of
8+
;; reversing the munge char tokens. cljw reversed the tokens but left `$`
9+
;; in place, found while writing the dev tooling in cljw.
10+
(deftest demunge-maps-dollar-to-slash
11+
(testing "the class$fn separator becomes ns/fn"
12+
(is (= "clojure.core/map" (repl/demunge "clojure.core$map"))))
13+
(testing "every $ maps to / (nested classes)"
14+
(is (= "a/b/c" (repl/demunge "a$b$c"))))
15+
(testing "$ maps alongside the munge tokens"
16+
(is (= "foo/fn?" (repl/demunge "foo$fn_QMARK_")))
17+
(is (= "//x" (repl/demunge "_SLASH_$x"))))
18+
(testing "a name with no $ still reverses the munge tokens"
19+
(is (= "no-dollar" (repl/demunge "no_dollar")))))
20+
21+
;; --- munge (clojure.core, the inverse of demunge) ---
22+
;; Escapes every character illegal in a Java identifier as a `_TOKEN_`,
23+
;; preserving the argument's type (string or symbol).
24+
(deftest munge-escapes-special-chars
25+
(testing "string in, string out"
26+
(is (= "foo_bar_QMARK_" (munge "foo-bar?")))
27+
(is (= "a_SLASH_b.c_d" (munge "a/b.c-d")))
28+
(is (= "a_AMPERSAND_b_BAR_c" (munge "a&b|c"))))
29+
(testing "symbol in, symbol out"
30+
(is (= 'foo_bar_QMARK_ (munge 'foo-bar?)))
31+
(is (symbol? (munge 'a))))
32+
(testing "a name needing no escaping is unchanged"
33+
(is (= "ok" (munge "ok"))))
34+
(testing "munge then demunge round-trips a hyphenated name"
35+
(is (= "foo-bar" (repl/demunge (munge "foo-bar"))))))
36+
37+
;; --- stack-element-str (cljw-native frame map input) ---
38+
;; clj takes a JVM StackTraceElement; cljw has only Clojure frames, so it
39+
;; takes a frame map and produces clj's Clojure-frame render string.
40+
(deftest stack-element-str-renders-a-frame
41+
(testing "a frame map renders as ns/fn (file:line)"
42+
(is (= "clojure.core/map (core.clj:2743)"
43+
(repl/stack-element-str {:ns "clojure.core" :fn "map" :file "core.clj" :line 2743}))))
44+
(testing "a frame with no ns omits the ns/ prefix"
45+
(is (= "eval1 (NO_SOURCE_FILE:1)"
46+
(repl/stack-element-str {:fn "eval1" :file "NO_SOURCE_FILE" :line 1})))))

‎test/e2e/phase16_stdin_read_line.sh‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,14 @@ assert_eq 'read_codepoint' \
4949
"$(printf 'あZ' | "$BIN" -e '(pr-str [(.read *in*) (.read *in*) (.read *in*)])' | tail -1)" \
5050
'"[12354 90 -1]"'
5151

52+
# slurp on process stdin drains the whole remainder to EOF (-e pr-prints it).
53+
assert_eq 'slurp_stdin' \
54+
"$(printf 'piped-stdin-content' | "$BIN" -e '(slurp *in*)' | tail -1)" \
55+
'"piped-stdin-content"'
56+
57+
# slurp drains only the UNREAD remainder after a read-line consumed a line.
58+
assert_eq 'slurp_stdin_after_read_line' \
59+
"$(printf 'first\nrest-of-stream' | "$BIN" -e '(do (read-line) (slurp *in*))' | tail -1)" \
60+
'"rest-of-stream"'
61+
5262
echo "ALL phase16_stdin_read_line PASS"

0 commit comments

Comments
 (0)