Skip to content

Commit a8cec8b

Browse files
committed
fix(print): D-433 — str/pr split for exception values (Throwable.toString parity)
Smell-audited: 1: verified clj println/print of a Throwable ALSO emit the #error data literal (print-method Throwable), so only writeStrValue gained the .ex_info arm — print/println deliberately untouched (no new divergence). `(str ex)` / `(.toString ex)` now render the readable Throwable.toString one-liner `<class>: <message>` (AD-003 simple name; clj prints the FQCN); a real ex-info appends the data map matching clj ExceptionInfo.toString ("ExceptionInfo: boom {:a 1}"). pr/pr-str/prn + print/println keep the #error{...} data literal. Stack trace stays out of scope per AD-007. e2e phase14_exception_ctor (6 new cases). D-433 discharged (easiest-first drain; the 2026-06-25 no-perpetual-defer decision).
1 parent b804e6c commit a8cec8b

3 files changed

Lines changed: 35 additions & 5 deletions

File tree

‎.dev/debt.yaml‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -699,11 +699,6 @@ active:
699699
verified lib needs it; then hash UTF-8 (AD-009) or add a UTF-16 path. Cross-ref
700700
ADR-0108 am1, AD-009.
701701
last_reviewed: "2026-06-10"
702-
- id: "D-433"
703-
status: "open (2026-06-14, surfaced building the D-431 Throwable class_corpus). cljw conflates an exception's str/toString with its pr/pr-str: `(str ex)` / `(.toString ex)` / `(pr-str ex)` ALL return cljw's compact `#error{:message \"boom\" :data nil}` form. clj distinguishes: `(.toString ex)` = `(str ex)` → \"java.lang.Exception: boom\" (Throwable.toString = class + ': ' + message, a readable one-liner); `(pr-str ex)` → the `#error {:cause :via :trace [JVM frames]}` data literal. cljw matches NEITHER. The JVM-FQCN + JVM-stack-trace parts are correctly out of scope (ADR-0059 / AD-003 / AD-007 — no JVM class, no JVM trace). The IMPROVEMENT toward F-011: give `(str ex)` / `(.toString ex)` the readable `<simple-name>: <message>` one-liner (AD-003 simple name), and keep `#error{...}` for the pr/pr-str (data-literal) context — i.e. introduce the str-vs-pr distinction cljw lacks for error values. Excluded from the Throwable class_corpus (a divergence, not a clj match). SCOPE (user-confirmed 2026-06-14): this is ONLY the str/pr one-liner — the STACK TRACE is intentionally NOT chased (cljw-native trace, no JVM frames; codified in AD-007). A SEPARATE, possibly-higher-value layer is matching the error DATA-SHAPE keys a lib reads programmatically (Throwable->map → :cause/:via/:data/:message; ex-data/ex-message already match) while keeping trace VALUES cljw-native — verify which libs read these before investing. D-433 itself (the readable `(str ex)` one-liner) is LOW priority; may not be worth doing at all per the user's integration that clj's error UX is not the bar."
704-
category: "clj-parity"
705-
barrier: "none — repro in hand: `(str (Exception. \"boom\"))` → cljw `#error{:message \"boom\" :data nil}` vs clj `\"java.lang.Exception: boom\"`. Touches the shared error-value print path (writeStrValue vs writeReprValue) which has AD-007 rendering pins — needs care to not regress error-rendering. LOW priority (trace explicitly out of scope per AD-007 + user). Take up only with a real consumer; the data-shape-key layer (above) is the better target if error compat matters. Until then a documented, tracked residual — NOT silently floating."
706-
last_reviewed: "2026-06-14"
707702
- id: "D-033"
708703
status: "DUE — trigger fired (corrected 2026-06-17 audit). primitive/ files long past the ~500 trigger: core.zig 1837 / math.zig 1444 / collection.zig 1190; core + math also breach the ROADMAP A6 ≤1000-line cap. Split never done. Next: confirm A6 still applies, then split (primitive/core|sequence|…) or amend the cap with an ADR."
709704
category: "structure"
@@ -6571,6 +6566,12 @@ discharged:
65716566
barrier: "DISCHARGED (was: instaparse GLL frontier)."
65726567
last_reviewed: "2026-07-06"
65736568

6569+
- id: "D-433"
6570+
status: "DISCHARGED 2026-07-06 (easiest-first drain; the 2026-06-25 no-perpetual-defer decision overrides the old take-up-on-consumer barrier). The str-vs-pr split for exceptions landed: writeStrValue gained an .ex_info arm — `(str ex)` / `(.toString ex)` render the readable Throwable.toString one-liner `<class>: <message>` (AD-003 simple name; clj prints the FQCN), and a REAL ex-info appends the data map matching clj ExceptionInfo.toString (`ExceptionInfo: boom {:a 1}`); empty message omits the colon. pr/pr-str/prn AND print/println keep the `#error{…}` data literal — clj-verified that println/print of a Throwable emit the data form too (print-method Throwable), so print/println were deliberately NOT rerouted (writeArgsSpaced's printValue path untouched; no new divergence). e2e phase14_exception_ctor str_exc/tostr_exc/str_iae/str_exinfo/prstr_exinfo/prstr_exc. Corpus infeasible (the one-liner's class prefix is the AD-003 simple-name divergence). The data-shape-key layer (Throwable->map :cause/:via/:trace) remains OUT of this row's scope — file a new row if a lib consumer surfaces."
6571+
category: "clj-parity"
6572+
barrier: "DISCHARGED (was: str/pr conflation for error values)."
6573+
last_reviewed: "2026-07-06"
6574+
65746575
conventions: |-
65756576
- Structure: two top-level lists — `active:` and `discharged:`. Active entries carry id / status / category / barrier (+ optional quality_floor / last_reviewed); discharged entries carry id / discharged_at / resolution. Edit as YAML (no Markdown table to align).
65766577
- New debt: grep `.dev/debt.yaml` for class-overlap before adding (per `.claude/rules/debt_dedup.md`).

‎src/runtime/print.zig‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,24 @@ pub fn writeStrValue(rt: *Runtime, env: *env_mod.Env, w: *Writer, v: Value) anye
136136
// `(str *ns*)` → bare "user" (clj Namespace.toString = the name), not the
137137
// readable `#object[Namespace …]` form pr/prn use.
138138
.ns => try w.writeAll(v.decodePtr(*const env_mod.Namespace).name),
139+
// `str`/`.toString` of an exception = the readable Throwable.toString
140+
// one-liner `<class>: <message>` (AD-003 simple name; clj prints the
141+
// FQCN); a real ex-info appends the data map, matching clj
142+
// ExceptionInfo.toString's trailing ` data`. The `#error{…}` data
143+
// literal stays the pr/prn form (D-433 str↔pr split).
144+
.ex_info => {
145+
const cls = ex_info_collection.className(v);
146+
try w.writeAll(cls orelse "ExceptionInfo");
147+
const msg = ex_info_collection.message(v);
148+
if (msg.len > 0) {
149+
try w.writeAll(": ");
150+
try w.writeAll(msg);
151+
}
152+
if (cls == null) {
153+
try w.writeByte(' ');
154+
try printValue(w, ex_info_collection.data(v));
155+
}
156+
},
139157
// `str`/`.toString` of a BigInt/BigDecimal drops the `N`/`M` reader
140158
// suffix that `pr`/`prn` keep — JVM `BigInteger`/`BigDecimal.toString`
141159
// emit plain digits (D-212). Ratio's `1/2` form is already suffix-free.

‎test/e2e/phase14_exception_ctor.sh‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,15 @@ assert_eq 'cls_iae' "$("$BIN" -e '(str (class (IllegalArgumentException. "x")))
6767
# instance? rides the hierarchy: an IllegalArgumentException IS a RuntimeException.
6868
assert_eq 'inst_iae_rte' "$("$BIN" -e '(instance? RuntimeException (IllegalArgumentException. "x"))')" 'true'
6969

70+
# str vs pr-str of an exception (D-433): `(str ex)` / `(.toString ex)` = the
71+
# readable Throwable.toString one-liner `<class>: <message>` (AD-003 simple
72+
# name — clj prints the FQCN); a real ex-info appends the data map (clj
73+
# ExceptionInfo.toString). `pr-str` keeps the `#error{…}` data literal.
74+
assert_eq 'str_exc' "$("$BIN" -e '(str (Exception. "boom"))')" '"Exception: boom"'
75+
assert_eq 'tostr_exc' "$("$BIN" -e '(.toString (Exception. "boom"))')" '"Exception: boom"'
76+
assert_eq 'str_iae' "$("$BIN" -e '(str (IllegalArgumentException. "x"))')" '"IllegalArgumentException: x"'
77+
assert_eq 'str_exinfo' "$("$BIN" -e '(str (ex-info "boom" {:a 1}))')" '"ExceptionInfo: boom {:a 1}"'
78+
assert_eq 'prstr_exinfo' "$("$BIN" -e '(pr-str (ex-info "boom" {:a 1}))')" '"#error{:message \"boom\" :data {:a 1}}"'
79+
assert_eq 'prstr_exc' "$("$BIN" -e '(pr-str (Exception. "boom"))')" '"#error{:message \"boom\" :data nil}"'
80+
7081
echo "ALL phase14_exception_ctor PASS"

0 commit comments

Comments
 (0)