Skip to content

Commit f1df0a6

Browse files
committed
initial POC
1 parent ff6e518 commit f1df0a6

File tree

2 files changed

+387
-241
lines changed

2 files changed

+387
-241
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
As a follow up to [#774](https://github.com/babashka/sci/issues/774), it has
2+
come up that users want to request a stack trace at the point of execution, just
3+
for debugging / printing. To facilitate this we should pre-process the stack
4+
instead of constructing during an exception such that you have it readily
5+
available.
6+
7+
You can see the beginnings of it in this case: `(try (/ 1 0) (catch ^:sci/error
8+
Exception e))`where the error has one stack element about `/` but none of the
9+
preceding ones, which we can add during analysis time.
10+
11+
---
12+
13+
# Plan: Stack Trace at Point of Execution
14+
15+
## Summary
16+
17+
Add `sci.core/current-stacktrace` to return the lexical stack (enclosing forms/functions) at any point during execution. Stack info is built during analysis and inlined as a constant, so zero runtime overhead.
18+
19+
## API
20+
21+
```clojure
22+
;; sci.core/current-stacktrace is a special var recognized by the analyzer.
23+
;; When called from SCI code, returns the lexical stack at that point:
24+
;; => [{:ns user :name inner :file "script.clj" :line 1 :column 14}
25+
;; {:ns user :name outer :file "script.clj" :line 2 :column 14}]
26+
27+
;; Host exposes to users as they wish:
28+
(def ctx (sci/init {:namespaces
29+
{'debug {'stacktrace sci.core/current-stacktrace}}}))
30+
31+
;; SCI user calls it:
32+
(defn my-fn []
33+
(debug/stacktrace)) ;; analyzer inlines the lexical stack here
34+
```
35+
36+
## Implementation
37+
38+
### Approach
39+
Treat `current-stacktrace` as a **special form** recognized by the analyzer. When the analyzer encounters a call to the function, it replaces it with a constant containing the current lexical stack. Zero runtime cost.
40+
41+
### Step 1: Track lexical stack during analysis
42+
43+
In `src/sci/impl/analyzer.cljc`:
44+
- Add `:lexical-stack` key to `ctx` (initially `[]`)
45+
- Push stack frame when entering `defn`/`fn`/`let`/`loop` (line ~355, ~540, ~767)
46+
- Use `make-stack` from utils to create frames
47+
48+
```clojure
49+
;; In analyze-fn*, after creating fn-name:
50+
(let [stack-frame (utils/make-stack (meta fn-expr))
51+
stack-frame (assoc stack-frame :name fn-name)
52+
ctx (update ctx :lexical-stack (fnil conj []) stack-frame)]
53+
...)
54+
```
55+
56+
### Step 2: Handle current-stacktrace calls
57+
58+
In `src/sci/impl/analyzer.cljc`, in `analyze-call`:
59+
- Check if resolved symbol is `current-stacktrace`
60+
- If so, return a constant node with `(:lexical-stack ctx)`
61+
62+
```clojure
63+
;; In analyze-call, after resolving f:
64+
(if (= 'sci.core/current-stacktrace (some-> f meta :sci/built-in))
65+
(->constant (:lexical-stack ctx))
66+
;; ... normal call handling
67+
)
68+
```
69+
70+
### Step 3: Register the function
71+
72+
In `src/sci/impl/namespaces.cljc`:
73+
- Add `current-stacktrace` to `sci.core` namespace
74+
- Mark it with metadata so analyzer can recognize it
75+
76+
In `src/sci/core.cljc`:
77+
- Add public `current-stacktrace` function (for documentation/API)
78+
79+
### Files to modify
80+
81+
| File | Changes |
82+
|------|---------|
83+
| `src/sci/impl/analyzer.cljc` | Track `:lexical-stack` in ctx, push on fn/let/loop entry, handle `current-stacktrace` calls |
84+
| `src/sci/impl/namespaces.cljc` | Add `current-stacktrace` to `sci.core` namespace with marker metadata |
85+
| `src/sci/core.cljc` | Add `current-stacktrace` for public API/docs |
86+
87+
## Note on aliasing
88+
89+
When host exposes `current-stacktrace` under a different name:
90+
```clojure
91+
(sci/init {:namespaces {'debug {'st sci.core/current-stacktrace}}})
92+
```
93+
The analyzer resolves `debug/st` to the same var, recognizes it by identity (via metadata marker), and inlines the stack. The var carries the marker, not the symbol.
94+
95+
## Verification
96+
97+
1. Add test in `test/sci/stacktrace_test.cljc`:
98+
```clojure
99+
(deftest current-stacktrace-test
100+
(let [ctx (sci/init {:namespaces
101+
{'debug {'stacktrace sci.core/current-stacktrace}}})
102+
result (sci/eval-string* ctx
103+
"(defn inner [] (debug/stacktrace))
104+
(defn outer [] (inner))
105+
(outer)")]
106+
(is (= 'inner (:name (first result))))
107+
(is (= 'outer (:name (second result))))))
108+
```
109+
110+
2. Run: `script/test/jvm`
111+
112+
---
113+
114+
## POC Results
115+
116+
Minimal POC implemented in `analyzer.cljc` only (2 changes). Results:
117+
118+
### With plain `fn` (line/column preserved):
119+
```clojure
120+
(sci/eval-string "((fn outer [] ((fn inner [] (sci.impl/current-stacktrace)))))")
121+
;; => [{:line 1, :column 2, :ns user, :file nil, :name outer}
122+
;; {:line 1, :column 16, :ns user, :file nil, :name inner}]
123+
```
124+
125+
### With `defn` (line/column lost due to macro expansion):
126+
```clojure
127+
(sci/eval-string "(defn outer [] (defn inner [] (sci.impl/current-stacktrace)) (inner)) (outer)")
128+
;; => [{:ns user, :file nil, :name outer}
129+
;; {:ns user, :file nil, :name inner}]
130+
```
131+
132+
### Known limitation
133+
`defn` macro expansion creates a new `fn*` form without preserving line/column from original expression. This can be fixed by passing metadata through the macro expansion.

0 commit comments

Comments
 (0)