Skip to content

Commit 1a97b19

Browse files
committed
isolate runtime error model and wrapping helpers
1 parent 014e90c commit 1a97b19

2 files changed

Lines changed: 187 additions & 182 deletions

File tree

vibes/execution.go

Lines changed: 0 additions & 182 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ package vibes
33
import (
44
"context"
55
"errors"
6-
"fmt"
7-
"regexp"
8-
"strings"
96
)
107

118
type ScriptFunction struct {
@@ -78,185 +75,6 @@ type callFrame struct {
7875
Pos Position
7976
}
8077

81-
type StackFrame struct {
82-
Function string
83-
Pos Position
84-
}
85-
86-
type RuntimeError struct {
87-
Type string
88-
Message string
89-
CodeFrame string
90-
Frames []StackFrame
91-
}
92-
93-
type assertionFailureError struct {
94-
message string
95-
}
96-
97-
func (e *assertionFailureError) Error() string {
98-
return e.message
99-
}
100-
101-
const (
102-
runtimeErrorTypeBase = "RuntimeError"
103-
runtimeErrorTypeAssertion = "AssertionError"
104-
runtimeErrorFrameHead = 8
105-
runtimeErrorFrameTail = 8
106-
)
107-
108-
var (
109-
errLoopBreak = errors.New("loop break")
110-
errLoopNext = errors.New("loop next")
111-
errStepQuotaExceeded = errors.New("step quota exceeded")
112-
errMemoryQuotaExceeded = errors.New("memory quota exceeded")
113-
stringTemplatePattern = regexp.MustCompile(`\{\{\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*\}\}`)
114-
)
115-
116-
func (re *RuntimeError) Error() string {
117-
var b strings.Builder
118-
b.WriteString(re.Message)
119-
if re.CodeFrame != "" {
120-
b.WriteString("\n")
121-
b.WriteString(re.CodeFrame)
122-
}
123-
renderFrame := func(frame StackFrame) {
124-
if frame.Pos.Line > 0 && frame.Pos.Column > 0 {
125-
fmt.Fprintf(&b, "\n at %s (%d:%d)", frame.Function, frame.Pos.Line, frame.Pos.Column)
126-
} else if frame.Pos.Line > 0 {
127-
fmt.Fprintf(&b, "\n at %s (line %d)", frame.Function, frame.Pos.Line)
128-
} else {
129-
fmt.Fprintf(&b, "\n at %s", frame.Function)
130-
}
131-
}
132-
133-
if len(re.Frames) <= runtimeErrorFrameHead+runtimeErrorFrameTail {
134-
for _, frame := range re.Frames {
135-
renderFrame(frame)
136-
}
137-
return b.String()
138-
}
139-
140-
for _, frame := range re.Frames[:runtimeErrorFrameHead] {
141-
renderFrame(frame)
142-
}
143-
omitted := len(re.Frames) - (runtimeErrorFrameHead + runtimeErrorFrameTail)
144-
fmt.Fprintf(&b, "\n ... %d frames omitted ...", omitted)
145-
for _, frame := range re.Frames[len(re.Frames)-runtimeErrorFrameTail:] {
146-
renderFrame(frame)
147-
}
148-
149-
return b.String()
150-
}
151-
152-
// Unwrap returns nil to satisfy the error unwrapping interface.
153-
// RuntimeError is a terminal error that wraps the original error message but not the error itself.
154-
func (re *RuntimeError) Unwrap() error {
155-
return nil
156-
}
157-
158-
func canonicalRuntimeErrorType(name string) (string, bool) {
159-
switch {
160-
case strings.EqualFold(name, runtimeErrorTypeBase), strings.EqualFold(name, "Error"):
161-
return runtimeErrorTypeBase, true
162-
case strings.EqualFold(name, runtimeErrorTypeAssertion):
163-
return runtimeErrorTypeAssertion, true
164-
default:
165-
return "", false
166-
}
167-
}
168-
169-
func classifyRuntimeErrorType(err error) string {
170-
if err == nil {
171-
return runtimeErrorTypeBase
172-
}
173-
var assertionErr *assertionFailureError
174-
if errors.As(err, &assertionErr) {
175-
return runtimeErrorTypeAssertion
176-
}
177-
if runtimeErr, ok := err.(*RuntimeError); ok {
178-
if kind, known := canonicalRuntimeErrorType(runtimeErr.Type); known {
179-
return kind
180-
}
181-
}
182-
return runtimeErrorTypeBase
183-
}
184-
185-
func newAssertionFailureError(message string) error {
186-
return &assertionFailureError{message: message}
187-
}
188-
189-
func (exec *Execution) step() error {
190-
exec.steps++
191-
if exec.quota > 0 && exec.steps > exec.quota {
192-
return fmt.Errorf("%w (%d)", errStepQuotaExceeded, exec.quota)
193-
}
194-
if exec.memoryQuota > 0 && (exec.steps&15) == 0 {
195-
if err := exec.checkMemory(); err != nil {
196-
return err
197-
}
198-
}
199-
if exec.ctx != nil {
200-
select {
201-
case <-exec.ctx.Done():
202-
return exec.ctx.Err()
203-
default:
204-
}
205-
}
206-
return nil
207-
}
208-
209-
func (exec *Execution) errorAt(pos Position, format string, args ...any) error {
210-
return exec.newRuntimeError(fmt.Sprintf(format, args...), pos)
211-
}
212-
213-
func (exec *Execution) newRuntimeError(message string, pos Position) error {
214-
return exec.newRuntimeErrorWithType(runtimeErrorTypeBase, message, pos)
215-
}
216-
217-
func (exec *Execution) newRuntimeErrorWithType(kind string, message string, pos Position) error {
218-
if canonical, ok := canonicalRuntimeErrorType(kind); ok {
219-
kind = canonical
220-
} else {
221-
kind = runtimeErrorTypeBase
222-
}
223-
224-
frames := make([]StackFrame, 0, len(exec.callStack)+1)
225-
226-
if len(exec.callStack) > 0 {
227-
// First frame: where the error occurred (within the current function)
228-
current := exec.callStack[len(exec.callStack)-1]
229-
frames = append(frames, StackFrame{Function: current.Function, Pos: pos})
230-
231-
// Remaining frames: the call stack (where each function was called from)
232-
for i := len(exec.callStack) - 1; i >= 0; i-- {
233-
cf := exec.callStack[i]
234-
frames = append(frames, StackFrame(cf))
235-
}
236-
} else {
237-
// No call stack means error at script top level
238-
frames = append(frames, StackFrame{Function: "<script>", Pos: pos})
239-
}
240-
codeFrame := ""
241-
if exec.script != nil {
242-
codeFrame = formatCodeFrame(exec.script.source, pos)
243-
}
244-
return &RuntimeError{Type: kind, Message: message, CodeFrame: codeFrame, Frames: frames}
245-
}
246-
247-
func (exec *Execution) wrapError(err error, pos Position) error {
248-
if err == nil {
249-
return nil
250-
}
251-
if isHostControlSignal(err) {
252-
return err
253-
}
254-
if _, ok := err.(*RuntimeError); ok {
255-
return err
256-
}
257-
return exec.newRuntimeErrorWithType(classifyRuntimeErrorType(err), err.Error(), pos)
258-
}
259-
26078
func (exec *Execution) evalStatements(stmts []Statement, env *Env) (Value, bool, error) {
26179
exec.pushEnv(env)
26280
defer exec.popEnv()

vibes/execution_errors.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package vibes
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"regexp"
7+
"strings"
8+
)
9+
10+
type StackFrame struct {
11+
Function string
12+
Pos Position
13+
}
14+
15+
type RuntimeError struct {
16+
Type string
17+
Message string
18+
CodeFrame string
19+
Frames []StackFrame
20+
}
21+
22+
type assertionFailureError struct {
23+
message string
24+
}
25+
26+
func (e *assertionFailureError) Error() string {
27+
return e.message
28+
}
29+
30+
const (
31+
runtimeErrorTypeBase = "RuntimeError"
32+
runtimeErrorTypeAssertion = "AssertionError"
33+
runtimeErrorFrameHead = 8
34+
runtimeErrorFrameTail = 8
35+
)
36+
37+
var (
38+
errLoopBreak = errors.New("loop break")
39+
errLoopNext = errors.New("loop next")
40+
errStepQuotaExceeded = errors.New("step quota exceeded")
41+
errMemoryQuotaExceeded = errors.New("memory quota exceeded")
42+
stringTemplatePattern = regexp.MustCompile(`\{\{\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*\}\}`)
43+
)
44+
45+
func (re *RuntimeError) Error() string {
46+
var b strings.Builder
47+
b.WriteString(re.Message)
48+
if re.CodeFrame != "" {
49+
b.WriteString("\n")
50+
b.WriteString(re.CodeFrame)
51+
}
52+
renderFrame := func(frame StackFrame) {
53+
if frame.Pos.Line > 0 && frame.Pos.Column > 0 {
54+
fmt.Fprintf(&b, "\n at %s (%d:%d)", frame.Function, frame.Pos.Line, frame.Pos.Column)
55+
} else if frame.Pos.Line > 0 {
56+
fmt.Fprintf(&b, "\n at %s (line %d)", frame.Function, frame.Pos.Line)
57+
} else {
58+
fmt.Fprintf(&b, "\n at %s", frame.Function)
59+
}
60+
}
61+
62+
if len(re.Frames) <= runtimeErrorFrameHead+runtimeErrorFrameTail {
63+
for _, frame := range re.Frames {
64+
renderFrame(frame)
65+
}
66+
return b.String()
67+
}
68+
69+
for _, frame := range re.Frames[:runtimeErrorFrameHead] {
70+
renderFrame(frame)
71+
}
72+
omitted := len(re.Frames) - (runtimeErrorFrameHead + runtimeErrorFrameTail)
73+
fmt.Fprintf(&b, "\n ... %d frames omitted ...", omitted)
74+
for _, frame := range re.Frames[len(re.Frames)-runtimeErrorFrameTail:] {
75+
renderFrame(frame)
76+
}
77+
78+
return b.String()
79+
}
80+
81+
// Unwrap returns nil to satisfy the error unwrapping interface.
82+
// RuntimeError is a terminal error that wraps the original error message but not the error itself.
83+
func (re *RuntimeError) Unwrap() error {
84+
return nil
85+
}
86+
87+
func canonicalRuntimeErrorType(name string) (string, bool) {
88+
switch {
89+
case strings.EqualFold(name, runtimeErrorTypeBase), strings.EqualFold(name, "Error"):
90+
return runtimeErrorTypeBase, true
91+
case strings.EqualFold(name, runtimeErrorTypeAssertion):
92+
return runtimeErrorTypeAssertion, true
93+
default:
94+
return "", false
95+
}
96+
}
97+
98+
func classifyRuntimeErrorType(err error) string {
99+
if err == nil {
100+
return runtimeErrorTypeBase
101+
}
102+
var assertionErr *assertionFailureError
103+
if errors.As(err, &assertionErr) {
104+
return runtimeErrorTypeAssertion
105+
}
106+
if runtimeErr, ok := err.(*RuntimeError); ok {
107+
if kind, known := canonicalRuntimeErrorType(runtimeErr.Type); known {
108+
return kind
109+
}
110+
}
111+
return runtimeErrorTypeBase
112+
}
113+
114+
func newAssertionFailureError(message string) error {
115+
return &assertionFailureError{message: message}
116+
}
117+
118+
func (exec *Execution) step() error {
119+
exec.steps++
120+
if exec.quota > 0 && exec.steps > exec.quota {
121+
return fmt.Errorf("%w (%d)", errStepQuotaExceeded, exec.quota)
122+
}
123+
if exec.memoryQuota > 0 && (exec.steps&15) == 0 {
124+
if err := exec.checkMemory(); err != nil {
125+
return err
126+
}
127+
}
128+
if exec.ctx != nil {
129+
select {
130+
case <-exec.ctx.Done():
131+
return exec.ctx.Err()
132+
default:
133+
}
134+
}
135+
return nil
136+
}
137+
138+
func (exec *Execution) errorAt(pos Position, format string, args ...any) error {
139+
return exec.newRuntimeError(fmt.Sprintf(format, args...), pos)
140+
}
141+
142+
func (exec *Execution) newRuntimeError(message string, pos Position) error {
143+
return exec.newRuntimeErrorWithType(runtimeErrorTypeBase, message, pos)
144+
}
145+
146+
func (exec *Execution) newRuntimeErrorWithType(kind string, message string, pos Position) error {
147+
if canonical, ok := canonicalRuntimeErrorType(kind); ok {
148+
kind = canonical
149+
} else {
150+
kind = runtimeErrorTypeBase
151+
}
152+
153+
frames := make([]StackFrame, 0, len(exec.callStack)+1)
154+
155+
if len(exec.callStack) > 0 {
156+
// First frame: where the error occurred (within the current function)
157+
current := exec.callStack[len(exec.callStack)-1]
158+
frames = append(frames, StackFrame{Function: current.Function, Pos: pos})
159+
160+
// Remaining frames: the call stack (where each function was called from)
161+
for i := len(exec.callStack) - 1; i >= 0; i-- {
162+
cf := exec.callStack[i]
163+
frames = append(frames, StackFrame(cf))
164+
}
165+
} else {
166+
// No call stack means error at script top level
167+
frames = append(frames, StackFrame{Function: "<script>", Pos: pos})
168+
}
169+
codeFrame := ""
170+
if exec.script != nil {
171+
codeFrame = formatCodeFrame(exec.script.source, pos)
172+
}
173+
return &RuntimeError{Type: kind, Message: message, CodeFrame: codeFrame, Frames: frames}
174+
}
175+
176+
func (exec *Execution) wrapError(err error, pos Position) error {
177+
if err == nil {
178+
return nil
179+
}
180+
if isHostControlSignal(err) {
181+
return err
182+
}
183+
if _, ok := err.(*RuntimeError); ok {
184+
return err
185+
}
186+
return exec.newRuntimeErrorWithType(classifyRuntimeErrorType(err), err.Error(), pos)
187+
}

0 commit comments

Comments
 (0)