@@ -26,6 +26,7 @@ const Runtime = @import("../../runtime/runtime.zig").Runtime;
2626const Env = @import ("../../runtime/env.zig" ).Env ;
2727const env_mod = @import ("../../runtime/env.zig" );
2828const print = @import ("../../runtime/print.zig" );
29+ const ex_info_mod = @import ("../../runtime/collection/ex_info.zig" );
2930
3031/// Everything a handler needs for one request.
3132pub const Ctx = struct {
@@ -75,6 +76,8 @@ const op_table = [_]OpEntry{
7576 .{ .name = "lookup" , .handler = opLookup },
7677 .{ .name = "info" , .handler = opLookup },
7778 .{ .name = "eldoc" , .handler = opLookup },
79+ .{ .name = "cider/analyze-last-stacktrace" , .handler = opAnalyzeLastStacktrace },
80+ .{ .name = "analyze-last-stacktrace" , .handler = opAnalyzeLastStacktrace },
7881};
7982
8083const op_map = blk : {
@@ -197,10 +200,18 @@ const BencodeSink = struct {
197200const Value = @import ("../../runtime/value/value.zig" ).Value ;
198201
199202fn evalImpl (ctx : * Ctx , code_key : []const u8 , emit_each : bool ) anyerror ! void {
200- const code = ctx .str (code_key ) orelse {
203+ const code_scratch = ctx .str (code_key ) orelse {
201204 try ctx .respondStatus (&.{ "error" , "no-code" , "done" });
202205 return ;
203206 };
207+ // The decoded request lives on the per-message SCRATCH arena, but
208+ // eval products (defn'd Functions, forms, analysis nodes) borrow
209+ // slices of the source text and outlive the message — dupe the
210+ // source onto the persistent arena (exactly like the CLI REPL's
211+ // per-line dupe). Without this, the next message's scratch reset
212+ // leaves every borrowed slice dangling (caught as garbage frame
213+ // strings in stamped traces under GC/scratch-reuse pressure).
214+ const code = try ctx .persist .dupe (u8 , code_scratch );
204215 const sess = try ctx .registry .getOrDefault (ctx .str ("session" ));
205216
206217 // ns honoring: an explicit request `ns` must exist (spec:
@@ -359,6 +370,124 @@ fn opLookup(ctx: *Ctx) anyerror!void {
359370 }
360371}
361372
373+ // --- cider/analyze-last-stacktrace (ADR-0170 am1: the *cider-error* buffer) ---
374+
375+ /// Analyze the session's `*e` on demand (the cider-nrepl model: the op
376+ /// reads the session's last exception, it does not keep a parallel
377+ /// error channel). One response dict per cause in the `ex-cause`
378+ /// chain, then `done`; no `*e` → `no-error` + `done`.
379+ fn opAnalyzeLastStacktrace (ctx : * Ctx ) anyerror ! void {
380+ const sess = try ctx .registry .getOrDefault (ctx .str ("session" ));
381+ const e = sess .stars .values [eval_session .StarState .idx_e ];
382+ if (e .isNil ()) {
383+ try ctx .respondStatus (&.{"no-error" });
384+ try ctx .respondStatus (&.{"done" });
385+ return ;
386+ }
387+ var cur = e ;
388+ while (! cur .isNil ()) {
389+ try emitCause (ctx , cur );
390+ cur = if (cur .tag () == .ex_info ) ex_info_mod .cause (cur ) else Value .nil_val ;
391+ }
392+ try ctx .respondStatus (&.{"done" });
393+ }
394+
395+ /// clojure.error phase vocabulary for a stamped cljw phase (drives
396+ /// CIDER's buffer-vs-overlay routing: compile phases render as an
397+ /// inline overlay, JVM style).
398+ fn phaseName (p : @import ("../../runtime/error/info.zig" ).Phase ) []const u8 {
399+ return switch (p ) {
400+ .parse = > "read-source" ,
401+ .analysis = > "compile-syntax-check" ,
402+ .macroexpand = > "macroexpansion" ,
403+ .eval = > "execution" ,
404+ };
405+ }
406+
407+ fn emitCause (ctx : * Ctx , v : Value ) ! void {
408+ var entries : std .ArrayList (bencode .Decoded .Entry ) = .empty ;
409+ if (v .tag () == .ex_info ) {
410+ const class = ex_info_mod .className (v ) orelse "ExceptionInfo" ;
411+ try entries .append (ctx .scratch , .{ .key = "class" , .value = .{ .str = class } });
412+ try entries .append (ctx .scratch , .{ .key = "message" , .value = .{ .str = ex_info_mod .message (v ) } });
413+ if (ex_info_mod .phaseOf (v )) | p | {
414+ try entries .append (ctx .scratch , .{ .key = "phase" , .value = .{ .str = phaseName (p ) } });
415+ }
416+ const d = ex_info_mod .data (v );
417+ if (! d .isNil ()) {
418+ var aw : Writer.Allocating = .init (ctx .scratch );
419+ if (print .printResult (ctx .rt , ctx .env , & aw .writer , d )) {
420+ try entries .append (ctx .scratch , .{ .key = "data" , .value = .{ .str = aw .written () } });
421+ } else | _ | {
422+ // Unprintable ex-data: omit the key; the cause still renders.
423+ }
424+ }
425+ try entries .append (ctx .scratch , .{ .key = "stacktrace" , .value = try emitFrames (ctx , ex_info_mod .originTrace (v )) });
426+ } else {
427+ // A raw thrown Value (`(throw 42)`): render it as the message.
428+ var aw : Writer.Allocating = .init (ctx .scratch );
429+ print .printResult (ctx .rt , ctx .env , & aw .writer , v ) catch {};
430+ try entries .append (ctx .scratch , .{ .key = "class" , .value = .{ .str = "Throwable" } });
431+ try entries .append (ctx .scratch , .{ .key = "message" , .value = .{ .str = aw .written () } });
432+ try entries .append (ctx .scratch , .{ .key = "stacktrace" , .value = .{ .list = &.{} } });
433+ }
434+ try ctx .respond (entries .items );
435+ }
436+
437+ /// Emit the trace as CIDER frame dicts, innermost-FIRST (JVM
438+ /// getStackTrace order; cljw's trace is push-order = outermost-first,
439+ /// so iterate reversed). Flags, cljw's take: every frame `clj` (there
440+ /// is no Java); `repl` for REPL-ish sources (`<repl:N>` / `<nrepl>` /
441+ /// `<stdin>` / `<-e>`); `project` for on-disk paths; `dup` when a
442+ /// frame repeats its list predecessor's fn or file+line (orchard's
443+ /// rule). `file-url` (absolute path) makes the frame navigable.
444+ fn emitFrames (ctx : * Ctx , trace : ? []const @import ("../../runtime/error/info.zig" ).StackFrame ) ! bencode.Decoded {
445+ const t = trace orelse return .{ .list = &.{} };
446+ var items : std .ArrayList (bencode .Decoded ) = .empty ;
447+ var prev_fn : ? []const u8 = null ;
448+ var prev_file : ? []const u8 = null ;
449+ var prev_line : u32 = 0 ;
450+ var i : usize = t .len ;
451+ while (i > 0 ) {
452+ i -= 1 ;
453+ const fr = t [i ];
454+ const fn_name = fr .fn_name orelse "unknown" ;
455+ const file = fr .file orelse "unknown" ;
456+ const name = if (fr .ns ) | ns |
457+ try std .fmt .allocPrint (ctx .scratch , "{s}/{s}" , .{ ns , fn_name })
458+ else
459+ fn_name ;
460+
461+ var flags : std .ArrayList (bencode .Decoded ) = .empty ;
462+ try flags .append (ctx .scratch , .{ .str = "clj" });
463+ const replish = file .len > 0 and file [0 ] == '<' ;
464+ const on_disk = file .len > 0 and file [0 ] == '/' ;
465+ if (replish ) try flags .append (ctx .scratch , .{ .str = "repl" });
466+ if (on_disk ) try flags .append (ctx .scratch , .{ .str = "project" });
467+ // file+line dup only for REAL locations — "unknown"-file frames
468+ // at the same line are distinct calls, not duplicates.
469+ const dup = (prev_fn != null and std .mem .eql (u8 , prev_fn .? , fn_name )) or
470+ (on_disk and prev_file != null and std .mem .eql (u8 , prev_file .? , file ) and prev_line == fr .line and fr .line != 0 );
471+ if (dup ) try flags .append (ctx .scratch , .{ .str = "dup" });
472+ prev_fn = fn_name ;
473+ prev_file = file ;
474+ prev_line = fr .line ;
475+
476+ var fe : std .ArrayList (bencode .Decoded .Entry ) = .empty ;
477+ try fe .append (ctx .scratch , .{ .key = "name" , .value = .{ .str = name } });
478+ try fe .append (ctx .scratch , .{ .key = "fn" , .value = .{ .str = fn_name } });
479+ if (fr .ns ) | ns | try fe .append (ctx .scratch , .{ .key = "ns" , .value = .{ .str = ns } });
480+ try fe .append (ctx .scratch , .{ .key = "var" , .value = .{ .str = name } });
481+ try fe .append (ctx .scratch , .{ .key = "file" , .value = .{ .str = file } });
482+ if (on_disk ) try fe .append (ctx .scratch , .{ .key = "file-url" , .value = .{ .str = file } });
483+ try fe .append (ctx .scratch , .{ .key = "line" , .value = .{ .int = fr .line } });
484+ try fe .append (ctx .scratch , .{ .key = "flags" , .value = .{ .list = flags .items } });
485+ try fe .append (ctx .scratch , .{ .key = "type" , .value = .{ .str = "clj" } });
486+ try items .append (ctx .scratch , .{ .dict = fe .items });
487+ }
488+ return .{ .list = items .items };
489+ }
490+
362491fn lookupMiss (ctx : * Ctx , is_eldoc : bool ) ! void {
363492 if (is_eldoc ) {
364493 try ctx .respondStatus (&.{ "done" , "no-eldoc" });
0 commit comments