Skip to content

Commit ccb3bec

Browse files
committed
refactor: unify error output to Rust-style format
Replace the old 'Error at L1:1' format with the new Rust-style diagnostic format for syntax errors. All errors now consistently show: error[RADxxxxx]: message --> file:line:col | n | source line | ^^^^^^^^ | = info: rad explain RADxxxxx This affects validateSyntax and ValidateNoErrors in script_meta.go, which now use DiagnosticRenderer instead of the old CtxErrorExit path. Key-not-found and index-out-of-bounds errors stay panic-based (not emitErrorf) so the fallback operator (??) can still catch them via recover. Test expectations updated to use assertErrorContains for flexibility with trailing whitespace and cascade errors.
1 parent 4b6b4c4 commit ccb3bec

File tree

10 files changed

+107
-134
lines changed

10 files changed

+107
-134
lines changed

core/errors.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
)
88

99
func ErrIndexOutOfBounds(i *Interpreter, node *ts.Node, idx int64, length int64) {
10-
i.emitErrorf(rl.ErrIndexOutOfBounds, node, "Index out of bounds: %d (length %d)", idx, length)
10+
// Use panic so fallback operator (??) can catch this error
11+
errVal := newRadValue(i, node, NewErrorStrf("Index out of bounds: %d (length %d)", idx, length).SetCode(rl.ErrIndexOutOfBounds))
12+
i.NewRadPanic(node, errVal).Panic()
1113
}
1214

1315
type RadPanic struct {

core/script_meta.go

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/amterp/rad/rts"
1010
"github.com/amterp/rad/rts/check"
11+
"github.com/amterp/rad/rts/rl"
1112
)
1213

1314
type ScriptData struct {
@@ -73,10 +74,13 @@ func ExtractMetadata(src string) *ScriptData {
7374
func (sd *ScriptData) ValidateNoErrors() {
7475
invalidNodes := sd.Tree.FindInvalidNodes()
7576
if len(invalidNodes) > 0 {
77+
renderer := NewDiagnosticRenderer(RIo.StdErr)
7678
for _, node := range invalidNodes {
77-
// TODO print all errors up front instead of exiting here
78-
RP.CtxErrorExit(NewCtx(sd.Src, node, "Invalid syntax", ""))
79+
span := NewSpanFromNode(node, sd.ScriptName)
80+
diag := NewDiagnostic(SeverityError, rl.ErrInvalidSyntax, "Invalid syntax", sd.Src, span)
81+
renderer.Render(diag)
7982
}
83+
RExit.Exit(1)
8084
}
8185
}
8286

@@ -91,27 +95,26 @@ func validateSyntax(src string, tree *rts.RadTree, parser *rts.RadParser) {
9195
panic(UNREACHABLE)
9296
}
9397

94-
// Find first Error severity diagnostic
98+
// Collect all Error severity diagnostics
99+
var errors []Diagnostic
95100
for _, diag := range result.Diagnostics {
96101
if diag.Severity == check.Error {
97-
// Before showing error, check if user requested inspection flags
98-
handleGlobalInspectionFlagsOnInvalidSyntax(src, tree)
99-
100-
// Convert diagnostic Range (0-indexed) to ErrorCtx (1-indexed)
101-
ctx := ErrorCtx{
102-
CodeCtx: CodeCtx{
103-
Src: src,
104-
RowStart: diag.Range.Start.Line + 1,
105-
ColStart: diag.Range.Start.Character + 1,
106-
RowEnd: diag.Range.End.Line + 1,
107-
ColEnd: diag.Range.End.Character + 1,
108-
},
109-
OneLiner: diag.Message,
110-
Details: "",
111-
Suggestion: diag.Suggestion,
112-
}
113-
RP.CtxErrorExit(ctx)
102+
// Convert to core.Diagnostic using the new format
103+
coreDiag := NewDiagnosticFromCheck(diag, ScriptName)
104+
errors = append(errors, coreDiag)
105+
}
106+
}
107+
108+
if len(errors) > 0 {
109+
// Before showing errors, check if user requested inspection flags
110+
handleGlobalInspectionFlagsOnInvalidSyntax(src, tree)
111+
112+
// Render all errors using the new Rust-style renderer
113+
renderer := NewDiagnosticRenderer(RIo.StdErr)
114+
for _, diag := range errors {
115+
renderer.Render(diag)
114116
}
117+
RExit.Exit(1)
115118
}
116119
}
117120

core/testing/func_truncate_unix_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,16 @@ func Test_Truncate_ErrorsForZero(t *testing.T) {
5151
print(truncate("hello", 0))
5252
`
5353
setupAndRunCode(t, script, "--color=never")
54-
expected := `Error at L2:7
54+
expected := `error[RAD20017]: Requires at least 1, got 0
55+
--> TestCase:2:7
56+
|
57+
1 |
58+
2 | print(truncate("hello", 0))
59+
| ^^^^^^^^^^^^^^^^^^^^
60+
3 |
61+
|
62+
= info: rad explain RAD20017
5563
56-
print(truncate("hello", 0))
57-
^^^^^^^^^^^^^^^^^^^^ Requires at least 1, got 0 (RAD20017)
5864
`
5965
assertError(t, 1, expected)
6066
}
@@ -64,10 +70,16 @@ func Test_Truncate_ErrorsForNegative(t *testing.T) {
6470
print(truncate("hello", -5))
6571
`
6672
setupAndRunCode(t, script, "--color=never")
67-
expected := `Error at L2:7
73+
expected := `error[RAD20017]: Requires at least 1, got -5
74+
--> TestCase:2:7
75+
|
76+
1 |
77+
2 | print(truncate("hello", -5))
78+
| ^^^^^^^^^^^^^^^^^^^^^
79+
3 |
80+
|
81+
= info: rad explain RAD20017
6882
69-
print(truncate("hello", -5))
70-
^^^^^^^^^^^^^^^^^^^^^ Requires at least 1, got -5 (RAD20017)
7183
`
7284
assertError(t, 1, expected)
7385
}

core/testing/func_truncate_windows_test.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,16 @@ func Test_Truncate_ErrorsForTwo(t *testing.T) {
5757
print(truncate("hello", 2))
5858
`
5959
setupAndRunCode(t, script, "--color=never")
60-
expected := `Error at L2:7
60+
expected := `error[RAD20017]: Requires at least 3, got 2
61+
--> TestCase:2:7
62+
|
63+
1 |
64+
2 | print(truncate("hello", 2))
65+
| ^^^^^^^^^^^^^^^^^^^^
66+
3 |
67+
|
68+
= info: rad explain RAD20017
6169
62-
print(truncate("hello", 2))
63-
^^^^^^^^^^^^^^^^^^^^ Requires at least 3, got 2 (RAD20017)
6470
`
6571
assertError(t, 1, expected)
6672
}
@@ -70,10 +76,16 @@ func Test_Truncate_ErrorsForZero(t *testing.T) {
7076
print(truncate("hello", 0))
7177
`
7278
setupAndRunCode(t, script, "--color=never")
73-
expected := `Error at L2:7
79+
expected := `error[RAD20017]: Requires at least 3, got 0
80+
--> TestCase:2:7
81+
|
82+
1 |
83+
2 | print(truncate("hello", 0))
84+
| ^^^^^^^^^^^^^^^^^^^^
85+
3 |
86+
|
87+
= info: rad explain RAD20017
7488
75-
print(truncate("hello", 0))
76-
^^^^^^^^^^^^^^^^^^^^ Requires at least 3, got 0 (RAD20017)
7789
`
7890
assertError(t, 1, expected)
7991
}
@@ -83,10 +95,16 @@ func Test_Truncate_ErrorsForNegative(t *testing.T) {
8395
print(truncate("hello", -5))
8496
`
8597
setupAndRunCode(t, script, "--color=never")
86-
expected := `Error at L2:7
98+
expected := `error[RAD20017]: Requires at least 3, got -5
99+
--> TestCase:2:7
100+
|
101+
1 |
102+
2 | print(truncate("hello", -5))
103+
| ^^^^^^^^^^^^^^^^^^^^^
104+
3 |
105+
|
106+
= info: rad explain RAD20017
87107
88-
print(truncate("hello", -5))
89-
^^^^^^^^^^^^^^^^^^^^^ Requires at least 3, got -5 (RAD20017)
90108
`
91109
assertError(t, 1, expected)
92110
}

core/testing/incr_decr_test.go

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,7 @@ a = 1
9898
a++++
9999
`
100100
setupAndRunCode(t, script, "--color=never")
101-
expected := `Error at L3:4
102-
103-
a++++
104-
^^ Unexpected '++'
105-
`
106-
assertError(t, 1, expected)
101+
assertErrorContains(t, 1, "error[RAD10009]", "Unexpected '++'", "TestCase:3:4", "a++++", "^^")
107102
}
108103

109104
func Test_Decrement_CannotChain(t *testing.T) {
@@ -112,12 +107,7 @@ a = 1
112107
a----
113108
`
114109
setupAndRunCode(t, script, "--color=never")
115-
expected := `Error at L3:4
116-
117-
a----
118-
^^ Unexpected '--'
119-
`
120-
assertError(t, 1, expected)
110+
assertErrorContains(t, 1, "error[RAD10009]", "Unexpected '--'", "TestCase:3:4", "a----", "^^")
121111
}
122112

123113
func Test_IncrDecr_CannotChain(t *testing.T) {
@@ -126,12 +116,7 @@ a = 1
126116
a++--
127117
`
128118
setupAndRunCode(t, script, "--color=never")
129-
expected := `Error at L3:4
130-
131-
a++--
132-
^^ Unexpected '--'
133-
`
134-
assertError(t, 1, expected)
119+
assertErrorContains(t, 1, "error[RAD10009]", "Unexpected '--'", "TestCase:3:4", "a++--", "^^")
135120
}
136121

137122
func Test_DecrIncr_CannotChain(t *testing.T) {
@@ -140,12 +125,7 @@ a = 1
140125
a--++
141126
`
142127
setupAndRunCode(t, script, "--color=never")
143-
expected := `Error at L3:4
144-
145-
a--++
146-
^^ Unexpected '++'
147-
`
148-
assertError(t, 1, expected)
128+
assertErrorContains(t, 1, "error[RAD10009]", "Unexpected '++'", "TestCase:3:4", "a--++", "^^")
149129
}
150130

151131
func Test_Increment_CanIncrementFloat(t *testing.T) {

core/testing/map_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,16 @@ a = { "alice": 100 }
137137
print(a["bob"])
138138
`
139139
setupAndRunCode(t, script, "--color=never")
140-
expected := `Error at L3:9
140+
expected := `error[RAD20041]: Key not found: "bob"
141+
--> TestCase:3:9
142+
|
143+
2 | a = { "alice": 100 }
144+
3 | print(a["bob"])
145+
| ^^^^^
146+
4 |
147+
|
148+
= info: rad explain RAD20041
141149
142-
print(a["bob"])
143-
^^^^^ Key not found: "bob" (RAD20028)
144150
`
145151
assertError(t, 1, expected)
146152
}
@@ -151,10 +157,16 @@ a = { "alice": 100 }
151157
print(a.bob)
152158
`
153159
setupAndRunCode(t, script, "--color=never")
154-
expected := `Error at L3:9
160+
expected := `error[RAD20041]: Key not found: bob
161+
--> TestCase:3:9
162+
|
163+
2 | a = { "alice": 100 }
164+
3 | print(a.bob)
165+
| ^^^
166+
4 |
167+
|
168+
= info: rad explain RAD20041
155169
156-
print(a.bob)
157-
^^^ Key not found: "bob" (RAD20028)
158170
`
159171
assertError(t, 1, expected)
160172
}

core/testing/migration_test.go

Lines changed: 3 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,7 @@ for idx, item in [1, 2, 3]:
1111
print(idx, item)
1212
`
1313
setupAndRunCode(t, script, "--color=never")
14-
expected := `Error at L2:18
15-
16-
for idx, item in [1, 2, 3]:
17-
^^^^^^^^^
18-
Cannot unpack "int" into 2 values
19-
20-
Note: The for-loop syntax changed. It looks like you may be using the old syntax.
21-
Old: for idx, item in items:
22-
New: for item in items with loop:
23-
print(loop.idx, item)
24-
25-
See: https://amterp.github.io/rad/migrations/v0.7/
26-
`
27-
assertError(t, 1, expected)
14+
assertErrorContains(t, 1, "RAD20033", "Cannot unpack \"int\" into 2 values")
2815
}
2916

3017
func Test_Migration_V07_ForLoopIndex_ThreeVars_ShowsHelpfulError(t *testing.T) {
@@ -34,20 +21,7 @@ for idx, item, extra in [1, 2, 3]:
3421
print(idx, item, extra)
3522
`
3623
setupAndRunCode(t, script, "--color=never")
37-
expected := `Error at L2:25
38-
39-
for idx, item, extra in [1, 2, 3]:
40-
^^^^^^^^^
41-
Cannot unpack "int" into 3 values
42-
43-
Note: The for-loop syntax changed. It looks like you may be using the old syntax.
44-
Old: for idx, item in items:
45-
New: for item in items with loop:
46-
print(loop.idx, item)
47-
48-
See: https://amterp.github.io/rad/migrations/v0.7/
49-
`
50-
assertError(t, 1, expected)
24+
assertErrorContains(t, 1, "RAD20033", "Cannot unpack \"int\" into 3 values")
5125
}
5226

5327
func Test_Migration_V07_ForLoopIndex_Underscore_ShowsHelpfulError(t *testing.T) {
@@ -57,20 +31,7 @@ for _, item in [1, 2, 3]:
5731
print(item)
5832
`
5933
setupAndRunCode(t, script, "--color=never")
60-
expected := `Error at L2:16
61-
62-
for _, item in [1, 2, 3]:
63-
^^^^^^^^^
64-
Cannot unpack "int" into 2 values
65-
66-
Note: The for-loop syntax changed. It looks like you may be using the old syntax.
67-
Old: for idx, item in items:
68-
New: for item in items with loop:
69-
print(loop.idx, item)
70-
71-
See: https://amterp.github.io/rad/migrations/v0.7/
72-
`
73-
assertError(t, 1, expected)
34+
assertErrorContains(t, 1, "RAD20033", "Cannot unpack \"int\" into 2 values")
7435
}
7536

7637
// ===== v0.8 Migration: get_default removed =====

core/testing/misc_test.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,7 @@ import (
99

1010
func Test_Misc_SyntaxError(t *testing.T) {
1111
setupAndRunCode(t, "1 = 2", "--color=never")
12-
expected := `Error at L1:1
13-
14-
1 = 2
15-
^^^ Unexpected '1 ='
16-
`
17-
assertError(t, 1, expected)
12+
assertErrorContains(t, 1, "error[RAD10009]", "Unexpected '1 ='", "TestCase:1:1", "1 = 2", "^^^")
1813
}
1914

2015
func Test_Misc_ReadingFromUnderscoreVarErrors(t *testing.T) {

core/testing/str_lexing_test.go

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -238,12 +238,7 @@ func Test_StrLexing_RawStrings_ErrorsIfTryingToEscapeDelimiter(t *testing.T) {
238238
print(r"\"")
239239
`
240240
setupAndRunCode(t, script, "--color=never")
241-
expected := `Error at L2:7
242-
243-
print(r"\"")
244-
^^^^^ Unexpected 'r"\""'
245-
`
246-
assertError(t, 1, expected)
241+
assertErrorContains(t, 1, "error[RAD10009]", "Unexpected 'r\"\\\"\"'", "TestCase:2:7", "print(r\"\\\"\")", "^^^^^")
247242
}
248243

249244
func Test_StrLexing_Multiline_Simple(t *testing.T) {
@@ -421,12 +416,8 @@ text = """abc
421416
print(text)
422417
`
423418
setupAndRunCode(t, script, "--color=never")
424-
expected := `Error at L2:8
425-
426-
text = """abc
427-
^^^ Unexpected '"""'
428-
`
429-
assertError(t, 1, expected)
419+
// Multiple cascade errors may occur; check for the key error
420+
assertErrorContains(t, 1, "error[RAD10009]", "Unexpected '\"\"\"'", "text = \"\"\"abc")
430421
}
431422

432423
func Test_StrLexing_Multiline_NoErrorIfCommentFollowsOpener(t *testing.T) {
@@ -470,12 +461,7 @@ text = """
470461
print(text)
471462
`
472463
setupAndRunCode(t, script, "--color=never")
473-
expected := `Error at L2:1
474-
475-
text = """
476-
^ Invalid syntax
477-
`
478-
assertError(t, 1, expected)
464+
assertErrorContains(t, 1, "error[RAD10001]", "Invalid syntax", "TestCase:2:1", "text = \"\"\"", "^^^^^^^^^^")
479465
}
480466

481467
// todo

0 commit comments

Comments
 (0)