Cache error offset in ParserError for safer position tracking

Instead of requiring downstream consumers to re-derive the byte offset
from pointer arithmetic on the Highlight slice, compute and cache the
offset inside the parser at error-capture time via setErrOffset().

This is safer because:
- The parser is the one place where the backing-array guarantee is known
  to hold (Highlight is always a subslice of the parse buffer)
- Downstream consumers (wrapDecodeError) can use the cached offset
  directly, avoiding the need for pointer comparison
- Errors created outside the parser (strict.go) set the offset from
  existing Raw ranges, which are already correct by construction

Add ParserError.SetOffset/Offset methods for setting and retrieving the
cached offset. Update wrapDecodeError to prefer the cached offset when
available, falling back to subsliceOffset for backward compatibility.

Co-authored-by: Thomas Pelletier <thomas@pelletier.dev>
This commit is contained in:
Cursor Agent
2026-04-12 13:00:38 +00:00
parent d528d3c6b4
commit 154d80392f
4 changed files with 106 additions and 11 deletions
+38
View File
@@ -2,6 +2,7 @@ package unstable
import (
"bytes"
"errors"
"fmt"
"reflect"
"unicode"
@@ -17,6 +18,9 @@ type ParserError struct {
Highlight []byte
Message string
Key []string // optional
offset int
offsetValid bool
}
// Error is the implementation of the error interface.
@@ -24,6 +28,21 @@ func (e *ParserError) Error() string {
return e.Message
}
// SetOffset records the byte offset of the error highlight within the
// document. Used by the parser to cache position information so
// downstream consumers don't need to re-derive it from pointers.
func (e *ParserError) SetOffset(offset int) {
e.offset = offset
e.offsetValid = true
}
// Offset returns the byte offset of the error highlight within the
// document, if it was previously set by the parser. The boolean
// indicates whether the offset is valid.
func (e *ParserError) Offset() (int, bool) {
return e.offset, e.offsetValid
}
// NewParserError is a convenience function to create a ParserError
//
// Warning: Highlight needs to be a subslice of Parser.data, so only slices
@@ -137,12 +156,14 @@ func (p *Parser) NextExpression() bool {
}
if len(p.left) == 0 || p.err != nil {
p.setErrOffset()
return false
}
p.ref, p.left, p.err = p.parseExpression(p.left)
if p.err != nil {
p.setErrOffset()
return false
}
@@ -165,6 +186,23 @@ func (p *Parser) Error() error {
return p.err
}
// setErrOffset computes and caches the byte offset of the error's
// highlight within p.data, so downstream consumers can use it
// without pointer arithmetic.
func (p *Parser) setErrOffset() {
if p.err == nil {
return
}
var perr *ParserError
if !errors.As(p.err, &perr) {
return
}
if perr.offsetValid || len(perr.Highlight) == 0 {
return
}
perr.SetOffset(p.subsliceOffset(perr.Highlight))
}
// Position describes a position in the input.
type Position struct {
// Number of bytes from the beginning of the input.