Skip to content

Commit f71e0dd

Browse files
committed
feat: add source span tracking to RadValue
Values now track their origin location in source code, enabling "assigned here" context in error messages: - Added Span *Span field to RadValue struct - Modified newRadValue() to create and attach spans from tree-sitter nodes when available - Added spanFromNode() helper that extracts location from nodes - Added HasSpan(), GetSpan(), WithSpan() helper methods Span is nil for intermediate computations (e.g., a + b results) to avoid unnecessary memory overhead. Only values from direct source expressions (literals, assignments, function parameters) track spans. When a RadValue already has a span and passes through newRadValue(), the existing span is preserved to maintain the original definition location.
1 parent e2e06d4 commit f71e0dd

File tree

2 files changed

+82
-14
lines changed

2 files changed

+82
-14
lines changed

core/testing/diagnostic_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,31 @@ func TestNewDiagnosticFromCheckNilCode(t *testing.T) {
224224
coreDiag := core.NewDiagnosticFromCheck(checkDiag, "test.rad")
225225
assert.Equal(t, rl.ErrGenericRuntime, coreDiag.Code)
226226
}
227+
228+
// Tests for RadValue span tracking
229+
230+
func TestRadValueHasSpan(t *testing.T) {
231+
// Value without span
232+
val := core.RadValue{Val: int64(42)}
233+
assert.False(t, val.HasSpan())
234+
assert.Nil(t, val.GetSpan())
235+
236+
// Value with span
237+
span := &core.Span{File: "test.rad", StartRow: 5}
238+
valWithSpan := val.WithSpan(span)
239+
assert.True(t, valWithSpan.HasSpan())
240+
assert.Equal(t, "test.rad", valWithSpan.GetSpan().File)
241+
assert.Equal(t, 5, valWithSpan.GetSpan().StartRow)
242+
}
243+
244+
func TestRadValueWithSpan(t *testing.T) {
245+
span := &core.Span{File: "test.rad", StartRow: 10, StartCol: 5}
246+
val := core.RadValue{Val: "hello"}
247+
248+
// WithSpan returns a copy with the span
249+
result := val.WithSpan(span)
250+
assert.Equal(t, span, result.Span)
251+
252+
// Original is also modified (value semantics - copy)
253+
// This is expected behavior for Go structs
254+
}

core/type_rad_value.go

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ type RadValue struct {
2020
// nulls are RadNull
2121
// errors are *RadError
2222
Val interface{}
23+
24+
// Span tracks where this value originated in source code.
25+
// Used for "assigned here" context in error messages.
26+
// nil for intermediate computations (e.g., a + b).
27+
Span *Span
2328
}
2429

2530
func (v RadValue) Type() rl.RadType {
@@ -51,6 +56,22 @@ func (v RadValue) IsNull() bool {
5156
return v.Val == RAD_NULL
5257
}
5358

59+
// HasSpan returns true if this value has source location information.
60+
func (v RadValue) HasSpan() bool {
61+
return v.Span != nil
62+
}
63+
64+
// GetSpan returns the source location span, or nil if not available.
65+
func (v RadValue) GetSpan() *Span {
66+
return v.Span
67+
}
68+
69+
// WithSpan returns a copy of this value with the given span attached.
70+
func (v RadValue) WithSpan(span *Span) RadValue {
71+
v.Span = span
72+
return v
73+
}
74+
5475
func (v RadValue) Index(i *Interpreter, idxNode *ts.Node) RadValue {
5576
switch coerced := v.Val.(type) {
5677
case RadString:
@@ -453,48 +474,67 @@ func (v RadValue) ToCompatSubject(i *Interpreter) (out rl.TypingCompatVal) {
453474
return
454475
}
455476

477+
// spanFromNode creates a Span from a tree-sitter node for value tracking.
478+
// Returns nil if the node is nil (intermediate computations don't have spans).
479+
func spanFromNode(i *Interpreter, node *ts.Node) *Span {
480+
if node == nil {
481+
return nil
482+
}
483+
file := ""
484+
if i != nil {
485+
file = i.sd.ScriptName
486+
}
487+
span := NewSpanFromNode(node, file)
488+
return &span
489+
}
490+
456491
func newRadValue(i *Interpreter, node *ts.Node, value interface{}) RadValue {
492+
span := spanFromNode(i, node)
457493
switch coerced := value.(type) {
458494
case RadValue:
495+
// Preserve existing span if the value already has one
496+
if coerced.Span == nil {
497+
coerced.Span = span
498+
}
459499
return coerced
460500
case []RadValue: // todo risky to have this? might cover up issues
461501
list := NewRadList()
462502
list.Values = coerced
463503
return newRadValue(i, node, list)
464504
case RadString:
465-
return RadValue{Val: coerced}
505+
return RadValue{Val: coerced, Span: span}
466506
case string:
467-
return RadValue{Val: NewRadString(coerced)}
507+
return RadValue{Val: NewRadString(coerced), Span: span}
468508
case int:
469-
return RadValue{Val: int64(coerced)}
509+
return RadValue{Val: int64(coerced), Span: span}
470510
case int64, float64, bool:
471-
return RadValue{Val: coerced}
511+
return RadValue{Val: coerced, Span: span}
472512
case *RadList:
473-
return RadValue{Val: coerced}
513+
return RadValue{Val: coerced, Span: span}
474514
case RadList:
475-
return RadValue{Val: &coerced}
515+
return RadValue{Val: &coerced, Span: span}
476516
case *RadMap:
477-
return RadValue{Val: coerced}
517+
return RadValue{Val: coerced, Span: span}
478518
case RadMap:
479-
return RadValue{Val: &coerced}
519+
return RadValue{Val: &coerced, Span: span}
480520
case RadFn:
481-
return RadValue{Val: coerced}
521+
return RadValue{Val: coerced, Span: span}
482522
case *RadError:
483-
return RadValue{Val: coerced}
523+
return RadValue{Val: coerced, Span: span}
484524
case map[string]interface{}:
485525
radMap := NewRadMap()
486526
for key, val := range coerced {
487527
radMap.Set(newRadValue(i, node, key), newRadValue(i, node, val))
488528
}
489-
return RadValue{Val: radMap}
529+
return RadValue{Val: radMap, Span: span}
490530
case []interface{}:
491531
list := NewRadListFromGeneric(i, node, coerced)
492-
return RadValue{Val: list}
532+
return RadValue{Val: list, Span: span}
493533
case []string:
494534
list := NewRadListFromGeneric(i, node, coerced)
495-
return RadValue{Val: list}
535+
return RadValue{Val: list, Span: span}
496536
case RadNull, nil:
497-
return RadValue{Val: RAD_NULL}
537+
return RadValue{Val: RAD_NULL, Span: span}
498538
default:
499539
if i != nil && node != nil {
500540
i.errorf(node, "Unsupported value type: %s", TypeAsString(coerced))

0 commit comments

Comments
 (0)