Skip to content

Commit f46ecfe

Browse files
[PART 1] Refactor stack traces to use call-stack model with proper frame names (#633)
Replace the old per-expression withStackFrame error model with a proactive call-stack approach. The evaluator now maintains an explicit call stack (pushFrame/popFrame) that captures function names and positions. Key changes: - EvalScope tracks call stack with pushFrame/popFrame/pushRootFrame - Error.captureTrace snapshots the stack into StackTrace frames - Function calls show real names (std.map, std.filter) instead of AST node types (Apply1, Lookup) - Lazy values capture creator context so builtin frames (std.map etc.) appear in traces even after the builtin returns - Root frame uses expr.pos for accurate line numbers in files with headers - Use Util.wrapInLessThanGreaterThan for <root> frame name - refresh_golden_outputs.sh supports per-file regeneration via args - Update all golden files and test expectations to match new format
1 parent b78cff1 commit f46ecfe

361 files changed

Lines changed: 5210 additions & 4779 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

sjsonnet/src/sjsonnet/Error.scala

Lines changed: 73 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,102 @@
11
package sjsonnet
22

33
import java.io.{PrintWriter, StringWriter}
4-
import scala.util.control.NonFatal
54

65
/**
7-
* An exception that can keep track of the Sjsonnet call-stack while it is propagating upwards. This
8-
* helps provide good error messages with line numbers pointing towards user code.
6+
* Resolved stack frame captured at error-throw time. Positions are pre-resolved to file:line:col so
7+
* that rendering does not need access to the evaluator.
98
*/
10-
class Error(msg: String, stack: List[Error.Frame] = Nil, underlying: Option[Throwable] = None)
11-
extends Exception(msg, underlying.orNull) {
9+
final class StackTrace(val name: String, val file: String, val line: Int, val col: Int)
1210

13-
setStackTrace(stack.reverseIterator.map(_.ste).toArray)
11+
/**
12+
* An exception carrying a Jsonnet-level stack trace. The trace is captured from the evaluator's
13+
* live call stack when the error is thrown (via [[Error.fail]]).
14+
*/
15+
class Error(
16+
msg: String,
17+
private[sjsonnet] val trace: Array[StackTrace] = Array.empty,
18+
underlying: Option[Throwable] = None)
19+
extends Exception(msg, underlying.orNull) {
1420

1521
override def fillInStackTrace: Throwable = this
16-
17-
def addFrame(pos: Position, expr: Expr = null)(implicit ev: EvalErrorScope): Error = {
18-
if (stack.isEmpty || alwaysAddPos(expr)) {
19-
val exprErrorString = if (expr == null) null else expr.exprErrorString
20-
addFrameString(pos, exprErrorString)
21-
} else this
22-
}
23-
24-
def addFrameString(pos: Position, exprErrorString: String)(implicit ev: EvalErrorScope): Error = {
25-
val newFrame = new Error.Frame(pos, exprErrorString)
26-
stack match {
27-
case s :: ss if s.pos == pos =>
28-
if (s.exprErrorString == null && exprErrorString != null) copy(stack = newFrame :: ss)
29-
else this
30-
case _ => copy(stack = newFrame :: stack)
31-
}
32-
}
33-
34-
def asSeenFrom(ev: EvalErrorScope): Error =
35-
copy(stack = stack.map(_.asSeenFrom(ev)))
36-
37-
protected def copy(
38-
msg: String = msg,
39-
stack: List[Error.Frame] = stack,
40-
underlying: Option[Throwable] = underlying) =
41-
new Error(msg, stack, underlying)
42-
43-
private def alwaysAddPos(expr: Expr): Boolean = expr match {
44-
case _: Expr.LocalExpr | _: Expr.Arr | _: Expr.ObjExtend | _: Expr.ObjBody | _: Expr.IfElse =>
45-
false
46-
case _ => true
47-
}
4822
}
4923

5024
object Error {
51-
final class Frame(val pos: Position, val exprErrorString: String)(implicit ev: EvalErrorScope) {
52-
val ste: StackTraceElement = {
53-
val cl = if (exprErrorString == null) "" else s"[$exprErrorString]"
54-
val (frameFile, frameLine) = ev.prettyIndex(pos) match {
55-
case None => (pos.currentFile.relativeToString(ev.wd) + " offset", pos.offset)
56-
case Some((line, col)) => (pos.currentFile.relativeToString(ev.wd) + ":" + line, col)
57-
}
58-
new StackTraceElement(cl, "", frameFile, frameLine)
59-
}
60-
61-
def asSeenFrom(ev: EvalErrorScope): Frame =
62-
if (ev eq this.ev) this else new Frame(pos, exprErrorString)(ev)
63-
}
64-
65-
def withStackFrame[T](expr: Expr)(implicit
66-
evaluator: EvalErrorScope): PartialFunction[Throwable, Nothing] = {
67-
case e: Error => throw e.addFrame(expr.pos, expr)
68-
case NonFatal(e) =>
69-
throw new Error("Internal Error", Nil, Some(e)).addFrame(expr.pos, expr)
70-
}
25+
def fail(msg: String, pos: Position)(implicit ev: EvalErrorScope): Nothing =
26+
throw new Error(msg, ev.captureTrace(pos))
7127

7228
def fail(msg: String, expr: Expr)(implicit ev: EvalErrorScope): Nothing =
73-
fail(msg, expr.pos, expr.exprErrorString)
74-
75-
def fail(msg: String, pos: Position, cl: String = null)(implicit ev: EvalErrorScope): Nothing =
76-
throw new Error(msg, new Frame(pos, cl) :: Nil, None)
29+
fail(msg, expr.pos)
7730

7831
def fail(msg: String): Nothing =
7932
throw new Error(msg)
8033

81-
def formatError(e: Throwable): String = {
82-
val s = new StringWriter()
83-
val p = new PrintWriter(s)
84-
try {
85-
e.printStackTrace(p)
86-
s.toString.replace("\t", " ")
87-
} finally {
88-
p.close()
89-
}
34+
private def errorPrefix(err: Error): String = err match {
35+
case _: ParseError => "sjsonnet.ParseError: "
36+
case _: StaticError => "sjsonnet.StaticError: "
37+
case _ => "sjsonnet.Error: "
38+
}
39+
40+
def formatError(e: Throwable): String = e match {
41+
case err: Error if err.trace.nonEmpty =>
42+
val sb = new StringBuilder
43+
sb.append(errorPrefix(err)).append(err.getMessage)
44+
for (frame <- err.trace) {
45+
sb.append("\n at [").append(frame.name).append(']')
46+
if (frame.file != null) {
47+
sb.append(".(")
48+
.append(frame.file)
49+
.append(':')
50+
.append(frame.line)
51+
.append(':')
52+
.append(frame.col)
53+
.append(')')
54+
}
55+
}
56+
sb.append('\n')
57+
sb.toString
58+
case err: Error =>
59+
errorPrefix(err) + err.getMessage + '\n'
60+
case _ =>
61+
val s = new StringWriter()
62+
val p = new PrintWriter(s)
63+
try { e.printStackTrace(p); s.toString.replace("\t", " ") }
64+
finally p.close()
9065
}
9166
}
9267

9368
class ParseError(
9469
msg: String,
95-
stack: List[Error.Frame] = Nil,
70+
_trace: Array[StackTrace] = Array.empty,
9671
underlying: Option[Throwable] = None,
9772
val offset: Int = -1)
98-
extends Error(msg, stack, underlying) {
99-
100-
override protected def copy(
101-
msg: String = msg,
102-
stack: List[Error.Frame] = stack,
103-
underlying: Option[Throwable] = underlying): sjsonnet.ParseError =
104-
new ParseError(msg, stack, underlying, offset)
105-
}
73+
extends Error(msg, _trace, underlying)
10674

107-
class StaticError(msg: String, stack: List[Error.Frame] = Nil, underlying: Option[Throwable] = None)
108-
extends Error(msg, stack, underlying) {
109-
110-
override protected def copy(
111-
msg: String = msg,
112-
stack: List[Error.Frame] = stack,
113-
underlying: Option[Throwable] = underlying): sjsonnet.StaticError =
114-
new StaticError(msg, stack, underlying)
115-
}
75+
class StaticError(
76+
msg: String,
77+
_trace: Array[StackTrace] = Array.empty,
78+
underlying: Option[Throwable] = None)
79+
extends Error(msg, _trace, underlying)
11680

11781
object StaticError {
118-
def fail(msg: String, expr: Expr)(implicit ev: EvalErrorScope): Nothing =
119-
throw new StaticError(msg, new Error.Frame(expr.pos, expr.exprErrorString) :: Nil, None)
82+
def fail(msg: String, expr: Expr)(implicit ev: EvalErrorScope): Nothing = {
83+
var trace = ev.captureTrace(expr.pos)
84+
if (trace.isEmpty && expr.pos != null) {
85+
ev.prettyIndex(expr.pos) match {
86+
case Some((line, col)) =>
87+
trace = Array(
88+
new StackTrace(
89+
Util.wrapInLessThanGreaterThan("root"),
90+
expr.pos.currentFile.relativeToString(ev.wd),
91+
line,
92+
col
93+
)
94+
)
95+
case None =>
96+
}
97+
}
98+
throw new StaticError(msg, trace)
99+
}
120100
}
121101

122102
trait EvalErrorScope {
@@ -134,4 +114,6 @@ trait EvalErrorScope {
134114
(splitted(0).toInt, splitted(1).toInt)
135115
}
136116
}
117+
118+
def captureTrace(throwPos: Position): Array[StackTrace] = Array.empty
137119
}

0 commit comments

Comments
 (0)