Skip to content

Add source positional information to the AST - #63

Merged
kyleconroy merged 72 commits into
mainfrom
claude/ast-positional-info-ub1t0e
Jul 31, 2026
Merged

Add source positional information to the AST#63
kyleconroy merged 72 commits into
mainfrom
claude/ast-positional-info-ub1t0e

Conversation

@kyleconroy

Copy link
Copy Markdown
Collaborator

Every AST node now carries source position information — StartOffset, FragmentLength, StartLine, and StartColumn — matching what Microsoft's ScriptDom emits for the same input.

Approach

ScriptDom already computes positions for every TSqlFragment; TsqlAstParser was explicitly discarding them via a SkipProperties set. Removing those four properties from that set gave us authoritative expected values, so no golden file was hand-authored. All 957 regenerable ast.json files were regenerated with the official parser using the loop documented in CLAUDE.md.

Regeneration is reproducible: re-running it over the committed goldens produces zero diffs, and git diff of parser/testdata between the regeneration commit and the branch tip is empty — every subsequent commit touched only Go code.

Changes

TsqlAstParser/Program.cs — emits the four position properties for fragments with real tokens (StartOffset >= 0); still skips ScriptTokenStream and token indices, which would bloat the JSON with ScriptDom-internal numbering.

ast/fragment.go (new) — a Fragment struct embedded in all 860 node structs, giving every node uniform position fields and accessors. A Pin() mechanism marks spans that reproduce ScriptDom quirks narrower than a node's full token extent, so enclosing parse functions don't widen them.

parser/source_map.go (new) — converts the lexer's byte offsets into ScriptDom's coordinate system: UTF-16 code-unit offsets and 1-based line/column. This is load-bearing rather than theoretical — 149 test inputs contain non-ASCII text where Go byte offsets and .NET string indices diverge. Lone \r starts a new line, matching ScriptDom.

parser/parser.go — span-recording helpers (spanned, spanFrom, tokSpan, spanTokens, spanFromChild, unspan, trimTrailingSemicolon, and others) plus token-end tracking.

parser/parse_*.go, parser/marshal.go — parse functions record spans; the marshaler emits them and derives spans for wrapper nodes synthesized during marshaling.

parser/parser_test.go — the 75 handwritten PhaseOne_* goldens can't be regenerated (the official parser rejects their intentionally partial SQL), so tests whose golden lacks positions are compared with position fields stripped from both sides.

Positional quirks

Most of the work was matching ScriptDom's spans, which are not simply "first token through last token":

  • Statements include their terminating semicolon, but a CREATE VIEW body's SELECT does not
  • EXISTS predicates exclude the EXISTS keyword; ORDER bulk-insert options exclude UNIQUE
  • BUCKET_COUNT index hints span only their value; COMPRESSION_DELAY excludes its time unit
  • Index definitions exclude trailing CLUSTERED/COLUMNSTORE keywords
  • Some synthesized nodes (defaulted options, normalized keyword identifiers) carry no position at all

Beyond positions

Reaching exact golden parity required a few output corrections worth reviewing separately, since they change AST output beyond position fields:

  • SQLCMD $(var) database names now emit SqlCommandIdentifier
  • ALTER ENDPOINT and CREATE MESSAGE TYPE always emit their defaulted enum fields
  • CONNECTION is treated as a user data type rather than a SQL data type

These also fixed 5 pre-existing PhaseOne test failures.

Testing

  • go test ./parser/ passes (fresh run, 1023 tests)
  • All 882 tests with positional goldens match exactly, including position fields
  • All 75 legacy PhaseOne tests pass — 5 were failing before this branch
  • Spot-checked outputs against goldens as parsed JSON: identical, including the Unicode test that exercises UTF-16 offset conversion

🤖 Generated with Claude Code


Generated by Claude Code

claude added 30 commits July 30, 2026 21:34
- TsqlAstParser now emits StartOffset/FragmentLength/StartLine/StartColumn
  for every fragment with real tokens; golden ast.json files regenerated
- New ast.Fragment struct embedded in every AST node
- Lexer/parser track UTF-16 code-unit offsets and line/column via a source map
- Parse functions record node spans; marshaler emits position fields and
  derives spans for wrapper nodes synthesized during marshaling
- Legacy handwritten goldens (PhaseOne_*) compared without positions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…and literals

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
… filters

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…ns, collation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…o columns

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
… options

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
claude added 29 commits July 31, 2026 00:46
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…cope options

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
… options

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…, openjson

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…njson items

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…ool options

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…as string

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…oint defaults

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
… in derived tables

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…..) selects

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…ubqueries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
…comparisons

All 882 comparable positional tests now pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011oXvuZCsJPD7Mbk6yNdT3y
@kyleconroy
kyleconroy merged commit 5cd4b20 into main Jul 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants