Skip to content

Keep the location of templates without interpolations through the v0 AST - #9165

Merged
cknitt merged 1 commit into
template-segment-locsfrom
template-loc-v0
Nov 1, 2026
Merged

cknitt merged 1 commit into
template-segment-locsfrom
template-loc-v0

Conversation

@cknitt

@cknitt cknitt commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Fixes #9164. Stacked on #9166.

Why

PPXs receive the parsetree in the frozen v0 format: Ast_mapper_to0 converts it before they run, and Ast_mapper_from0 converts their output back. A template literal without interpolations came back from that round trip with a location one character short, missing its closing backquote:

let z = `template`

                        expression    segment "template"
parsed                  8..18         9..17
after the round trip    8..17         8..17      ← both wrong

In projects with PPXs, everything that used the template's location, such as error highlights and editor ranges, ended one character early.

Cause

In the current parsetree, a template has two locations: the expression's, which covers the whole literal, and its segment's, which covers just the text (since #9166). The v0 AST has no template node. A template without interpolations is a single string constant marked res.template, and a constant has one location only.

Ast_mapper_to0 built that constant at the segment's location. Ast_mapper_from0 then used it for both the expression and the segment, so the expression lost its backquotes.

Templates with interpolations were never affected. In v0 they are a chain of ^ applications, whose outer node carries the expression's location, and each segment constant keeps its own:

`a${b}c`   →   ("a" ^ b) ^ "c"     (with res.template marks)
               └ outer apply: expression location; "a", "c": segment locations

What changes

  • Ast_mapper_to0: the single constant of a template without interpolations takes the template's location, backquotes included.
  • Ast_mapper_from0: the segment's location is derived from it as the text between the backquotes, one character in from each end. Ghost locations, and ones too short to hold two backquotes, are kept as they are, so templates that PPXs build with Location.none stay as they were.
let z = `template`

                        expression    segment "template"
parsed                  8..18         9..17
v0 constant             8..18
after the round trip    8..18         9..17      ✓

Interpolated templates, tagged templates and %raw templates have their own v0 encodings and don't change.

Tests

template_locations_roundtrip_through_ast0 in ounit_ast_mapper0_tests.ml round-trips a whole file through v0 and checks the tree is identical, locations included:

let a = `template`
let b = %foo(`payload`)
let c = `multi
line`
let d = `a${b}c${d}`
let e = sql`select ${x}`
let f = `a\`b`
let g = %raw(@foo `"x"`)
let h = ``

Without the fix it fails on the first binding.

Once this and #9161 are in, todo_roundtrips_through_ast0 can include a template message.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Kbf3gJFAva9685fjvkajub

@codecov

codecov Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.75%. Comparing base (7ef4188) to head (506cb25).
⚠️ Report is 3 commits behind head on template-segment-locs.

Files with missing lines Patch % Lines
compiler/ml/ast_mapper_from0.ml 75.00% 1 Missing ⚠️
Additional details and impacted files
@@                  Coverage Diff                   @@
##           template-segment-locs    #9165   +/-   ##
======================================================
  Coverage                  81.74%   81.75%           
======================================================
  Files                        460      460           
  Lines                      64953    64957    +4     
======================================================
+ Hits                       53099    53105    +6     
+ Misses                     11854    11852    -2     
Files with missing lines Coverage Δ
compiler/ml/ast_mapper_to0.ml 80.00% <100.00%> (ø)
tests/ounit_tests/ounit_ast_mapper0_tests.ml 90.63% <100.00%> (ø)
compiler/ml/ast_mapper_from0.ml 79.37% <75.00%> (ø)

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pkg-pr-new

pkg-pr-new Bot commented Nov 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@9165

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@9165

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@9165

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@9165

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@9165

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@9165

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@9165

commit: 0347768

@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Changelog

🐛 Bug fix

  • Fix the location of template literals without interpolations ending one character early in projects that use PPXs.

Semantic Diff

1 commit · Full page

Keep the location of templates without interpolations through ast0

506cb25 · cknitt + Claude · Nov 01, 2026, 08:57 UTC

Why

On the v0 wire, a template without interpolations was one string constant at its segment's location, which ends before the closing backquote, so the template lost one character of its location after a PPX round trip. The constant now takes the template's location, and the segment's location is derived from it on the way back (#9164).

AST modified (template after the v0 round trip)

let z = `template`

text

 parsed:              expression [1,0+8]..[1,0+18]
-after the round trip: expression [1,0+8]..[1,0+17]
+after the round trip: expression [1,0+8]..[1,0+18]

Why:
    On the v0 wire, a template without interpolations was one string
    constant at its segment's location, which ends before the closing
    backquote, so the template lost one character of its location after a
    PPX round trip. The constant now takes the template's location, and the
    segment's location is derived from it on the way back (#9164).

AST modified (template after the v0 round trip):
Example:
    let z = `template`
Before (text):
    parsed:              expression [1,0+8]..[1,0+18]
    after the round trip: expression [1,0+8]..[1,0+17]
After (text):
    parsed:              expression [1,0+8]..[1,0+18]
    after the round trip: expression [1,0+8]..[1,0+18]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kbf3gJFAva9685fjvkajub
Signed-off-by: Christoph Knittel <ck@cca.io>
@cknitt
cknitt merged commit 506cb25 into master Nov 1, 2026
12 of 14 checks passed
@cknitt
cknitt deleted the template-loc-v0 branch November 1, 2026 11:25
@cknitt

cknitt commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Folded into #9166, which now contains this commit (506cb25). The two changes depend on each other: #9166 alone would have made the v0 round trip lose both backquotes of a template (see the Codex review there). GitHub marks this PR as merged because its base branch was fast-forwarded to include it; it lands on master with #9166.

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.

v0 AST round trip shortens the location of templates without interpolations

1 participant