Problem
Source positions are 1-based line/column only and carry no byte offset. Every
consumer that needs an exact range — an AST-backed formatter, precise LSP text
edits, and exact diagnostic ranges — has to re-derive byte offsets from
line/column against the original source buffer. Many spans are also line-wide
approximations produced by sourceLineSpan, which covers the whole trimmed line
rather than the offending token.
This is the substrate gap behind most of the deferred parser/formatter/span work
(#250): without a byte offset on the position primitive, precise spans and
parser-backed formatting keep re-implementing offset math per call site.
Relevant code:
internal/source/source.go: SourcePosition (Line, Column only), SourceSpan
internal/parser/route_helpers.go: sourceLineSpan line-wide approximation
internal/lang/diagnostic.go: Position, Range mirror line/column for JSON
internal/compiler/validate_spans.go: spans synthesized via string search
Scope
Add a byte Offset to SourcePosition (0-based, into the source buffer)
alongside the existing Line/Column, populated by the parser scanner where it
already knows byte positions. Provide helpers to convert between byte offset and
line/column against a source buffer so downstream code stops re-deriving it.
This is additive. It does not require rewriting the parser; it makes the position
primitive able to carry an exact offset where one is known.
Acceptance Criteria
SourcePosition carries a byte offset alongside line and column.
- The parser populates the offset for records where it scans byte positions;
unknown offsets remain explicitly zero/unset rather than guessed.
- Helpers convert offset <-> line/column and are covered by round-trip tests.
check --json output stays backward compatible in shape (offset is additive
or internal-only as decided).
- Existing span-based diagnostics and reports keep their current behavior.
Verification
go test ./internal/source ./internal/parser ./internal/lang ./internal/compiler
Problem
Source positions are 1-based line/column only and carry no byte offset. Every
consumer that needs an exact range — an AST-backed formatter, precise LSP text
edits, and exact diagnostic ranges — has to re-derive byte offsets from
line/column against the original source buffer. Many spans are also line-wide
approximations produced by
sourceLineSpan, which covers the whole trimmed linerather than the offending token.
This is the substrate gap behind most of the deferred parser/formatter/span work
(#250): without a byte offset on the position primitive, precise spans and
parser-backed formatting keep re-implementing offset math per call site.
Relevant code:
internal/source/source.go:SourcePosition(Line, Column only),SourceSpaninternal/parser/route_helpers.go:sourceLineSpanline-wide approximationinternal/lang/diagnostic.go:Position,Rangemirror line/column for JSONinternal/compiler/validate_spans.go: spans synthesized via string searchScope
Add a byte
OffsettoSourcePosition(0-based, into the source buffer)alongside the existing
Line/Column, populated by the parser scanner where italready knows byte positions. Provide helpers to convert between byte offset and
line/column against a source buffer so downstream code stops re-deriving it.
This is additive. It does not require rewriting the parser; it makes the position
primitive able to carry an exact offset where one is known.
Acceptance Criteria
SourcePositioncarries a byte offset alongside line and column.unknown offsets remain explicitly zero/unset rather than guessed.
check --jsonoutput stays backward compatible in shape (offset is additiveor internal-only as decided).
Verification
go test ./internal/source ./internal/parser ./internal/lang ./internal/compiler