Skip to content

String interpolation: syntax highlighting + inlay-hint suppression - #3797

Merged
codeshaunted merged 8 commits into
canaryfrom
avery/string-interpolation-highlighting
Jun 19, 2026
Merged

String interpolation: syntax highlighting + inlay-hint suppression#3797
codeshaunted merged 8 commits into
canaryfrom
avery/string-interpolation-highlighting

Conversation

@codeshaunted

@codeshaunted codeshaunted commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What

Fixes two editor problems with BEP-049 backtick strings / ${...} interpolation.

1. Syntax highlighting (the big one)

The BAML TextMate grammar had zero support for backtick strings, so everything inside `...` was tokenized as arbitrary BAML code — hence the "very bad" highlighting. Added:

  • backtick-string`...`, ... , ```...``` (an N-tick opener is closed by N consecutive ticks via a \1 backreference, so inner single backticks like inline code stay literal)
  • backtick-escape\`, \$, \\, \n, \r, \t, \0, \uXXXX (\${ stays literal text, not an interpolation)
  • interpolation${...} rendered as embedded code; ${for}/${if}/${else}/${endfor}/${endif} as control keywords; nested braces balance so object literals (${ {a: 1} }) and ${if (p) {..} else {..}} don't close the interp early
  • tagged-template-tag — the tag in prompt`...`

Verified by tokenizing all BEP-049 cases (interpolation, escapes, multi-tick ladders, multi-line ${for}/${endfor}, ${if}/${else}/${endif}, tagged templates, nested backtick-in-interpolation) with vscode-textmate. Regression-checked that normal "...", #"..."#, `a` == `b`, and the declarative LLM-function sample (client<llm>, prompt #"..."# + jinja) are unaffected.

2. Inlay hints inside ${...}

${expr} lowers to a synthetic string.from(expr) call and ${ let … } builds a compiler-generated concat accumulator — both carried spans inside the template, so the inlay-hint provider emitted spurious value: / : string hints the user never wrote. Suppressed (commit 5661b7c0): a call whose span equals its callee's span is a synthesized wrapper, and a let with LetOrigin::Compiler is compiler-generated. User-written ${ let n = … } keeps its hint. Covered by a unit test + golden file; full LSP suite (500 tests) green.

Single source of truth for the grammar

The grammar previously existed as duplicated copies across apps. Consolidated into a new workspace package @b/pkg-grammar (typescript2/pkg-grammar/):

  • canonical baml.tmLanguage.json + jinja.tmLanguage.json live there
  • app-promptfiddle imports them directly (local copies removed)
  • app-vscode-ext keeps a committed mirror under syntaxes/ because VS Code loads the grammar from the physical path in contributes.grammars[] and the .vsix ships bundled (no node_modules). The mirror is regenerated by scripts/sync.mjs via predev/prebuild.
  • a grammar-single-source pre-commit hook fails if the mirror drifts from the package

To edit the grammar going forward: change the files in typescript2/pkg-grammar/, then node typescript2/pkg-grammar/scripts/sync.mjs (or build vscode-ext) and commit the regenerated mirror.

Verification

  • promptfiddle typecheck: grammar import resolves with no new errors (pre-existing failures are un-generated pkg-proto protobufs + un-built @b/bridge_wasm, unrelated)
  • vscode-ext typecheck: clean
  • pre-commit grammar-single-source hook: passes; drift test confirms it catches a tampered mirror

Summary by CodeRabbit

  • New Features
    • Improved BAML TextMate syntax highlighting for backtick templates, including tagged backtick templates and ${ ... } interpolation.
  • Bug Fixes
    • Reduced noisy inlay type/parameter hints by suppressing hints originating from compiler-synthesized interpolation/wrapper constructs (including tagged-template desugaring).
  • Tests
    • Added inlay-hints coverage for backtick interpolation to ensure only user-defined bindings produce hints, plus assertions for tagged-template synthetic nodes.
  • Chores
    • Centralized TextMate grammars into a shared package; updated editor consumers to load packaged grammars and added grammar sync/check tooling with drift coverage.

…odes

`${expr}` lowers to `string.from(expr)` and `${ let … }` builds a concat-scope
accumulator (`let " __m3_concat" = ""`); both carry spans inside the backtick
template, so the inlay-hint provider emitted a `value:` parameter hint on every
interpolation plus a `: string` hint on the accumulator — noise the user never
wrote.

Skip them in `annotations`:
- a call whose span equals its callee's span is a synthesized wrapper (a real
  call's span covers `callee(args…)`), so emit no parameter hints for it.
- a `let` with `LetOrigin::Compiler` is compiler-generated, so emit no type hint.

Real bindings and calls — including a user's explicit `${ let n = … }` — keep
their hints. Covered by a unit test and an `inlay_hints/backtick_interpolation`
golden file.
…mmar

Add TextMate highlighting for BEP-049 backtick strings to the BAML grammar:
- backtick-string: `...` / ``...`` / ```...``` (N-tick opener closed by N ticks
  via a \1 backreference, so inner single backticks stay literal)
- backtick-escape: \` \$ \\ \n \r \t \0 \uXXXX (\${ stays literal text)
- interpolation: ${...} rendered as embedded code, with ${for}/${if}/${else}/
  ${endfor}/${endif} as control keywords; nested braces balance so object
  literals and if/else blocks don't close the interp early
- tagged-template-tag: the tag in prompt`...`

Consolidate the grammar into a single source of truth, @b/pkg-grammar:
- canonical baml.tmLanguage.json + jinja.tmLanguage.json live there
- app-promptfiddle imports them directly (local copies removed)
- app-vscode-ext keeps a committed mirror under syntaxes/ (VS Code loads the
  grammar from the physical path in contributes.grammars; the .vsix ships
  bundled with no node_modules), regenerated by sync.mjs via predev/prebuild
- a grammar-single-source pre-commit hook fails if the mirror drifts

Verified by tokenizing all BEP-049 cases with vscode-textmate; declarative
LLM-function highlighting (client/prompt #"..."# + jinja) is unaffected.
@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
beps Ready Ready Preview, Comment Jun 19, 2026 7:25pm
promptfiddle Ready Ready Preview, Comment Jun 19, 2026 7:25pm
promptfiddle2 Ready Ready Preview, Comment Jun 19, 2026 7:25pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces pkg-grammar as the canonical single source for BAML TextMate grammars, with a sync.mjs script that mirrors grammars into the VS Code extension's syntaxes/ directory. Adds full backtick-string and tagged-template highlighting rules. Migrates app-promptfiddle from local grammar copies to the package. Tracks compiler-synthesized AST nodes separately from user-written ones, removes LetOrigin::Compiler variant, and suppresses corresponding inlay hints in the BAML LSP for interpolation-lowered nodes.

Changes

Grammar Single-Source Package

Layer / File(s) Summary
pkg-grammar package scaffold, sync script, and README
typescript2/pkg-grammar/package.json, typescript2/pkg-grammar/scripts/sync.mjs, typescript2/pkg-grammar/README.md
Defines @b/pkg-grammar with ESM exports for both grammar JSONs and sync/check scripts. sync.mjs reads canonical grammars and either overwrites or drift-checks the VS Code extension mirror; README documents the single-source workflow.
Canonical BAML TextMate grammar
typescript2/pkg-grammar/baml.tmLanguage.json
New complete grammar covering top-level declarations (class, enum, function, client, test, generator, retry_policy), type expressions, config/field bodies, attributes, embedded Jinja, object/member patterns, namespace paths, backtick/raw/quoted strings with ${...} interpolation and brace balancing, operators, and identifiers.
VSCode extension grammar: backtick strings and tagged templates
typescript2/app-vscode-ext/syntaxes/baml.tmLanguage.json
Adds #tagged-template-tag into #expression-body, introduces the tagged-template-tag repository rule, and defines backtick-string, backtick-escape, interpolation, and interpolation-braces rules including endfor/endif tag handling.
Consumer migration and pre-commit enforcement
typescript2/app-promptfiddle/package.json, typescript2/app-promptfiddle/src/playground/MonacoEditor.tsx, typescript2/app-vscode-ext/package.json, .pre-commit-config.yaml
app-promptfiddle adds @b/pkg-grammar dependency and updates MonacoEditor.tsx imports from local relative paths to package paths. app-vscode-ext adds sync:grammar script wired into predev/prebuild. Pre-commit hook grammar-single-source runs sync.mjs --check on *.tmLanguage.json changes.

Compiler Synthetic Node Tracking

Layer / File(s) Summary
AstSourceMap synthetic tracking contract and LetOrigin refactor
baml_language/crates/baml_compiler2_ast/src/ast.rs
Adds HashSet import and three new public fields on AstSourceMap (synthetic_exprs, synthetic_stmts, synthetic_patterns). Exposes three public predicate methods to test whether a given node ID is compiler-synthesized. Removes the LetOrigin::Compiler variant.
LoweringContext synthetic marking and template desugaring
baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
Adds a synthesizing: bool flag to LoweringContext that marks nodes allocated during desugaring. Allocation helpers conditionally insert IDs into synthetic sets when enabled. Wraps backtick and tagged-template elaboration with flag save/restore to record only elaboration-generated nodes as synthetic. Updates template-desugaring let bindings to use LetOrigin::Source instead of LetOrigin::Compiler in accumulators, ${for} loops, tagged template bodies, and prompt-tag closure generation.
Default-initialized source maps at synthesis callsites
baml_language/crates/baml_compiler2_ast/src/auto_derive_json.rs, baml_language/crates/baml_compiler2_ast/src/lower_cst.rs
Simplifies AstSourceMap construction in four auto-derive-json helpers and six lower-cst synthesis helpers by replacing explicit field enumeration with ..Default::default().
LSP module pattern updates for LetOrigin::Source
baml_language/crates/baml_lsp2_actions/src/completions.rs, baml_language/crates/baml_lsp2_actions/src/outline.rs, baml_language/crates/baml_lsp2_actions/src/type_info.rs
Updates completions, outline, and type_info to explicitly match on LetOrigin::Source instead of wildcard patterns, accommodating the removal of LetOrigin::Compiler.
Synthetic node marking unit test
baml_language/crates/baml_compiler2_ast/src/lib.rs
Verifies that lowering a tagged template marks the elaborated closure body and its statements as synthetic in the source_map, while user-written tag and interpolation expressions remain non-synthetic.

LSP Inlay Hint Suppression

Layer / File(s) Summary
Annotation suppression and interpolation hint test coverage
baml_language/crates/baml_lsp2_actions/src/annotations.rs, baml_language/crates/baml_lsp2_actions_tests/test_files/inlay_hints/backtick_interpolation.baml
In process_body, skips type-hint emission for synthetic let statements and parameter-name hints for synthetic call expressions (preventing hints from string.from(...) wrapper calls). New .baml fixture and tests assert that ${...} interpolation-lowering synthetic nodes produce no value: hints while user-defined let bindings retain type hints.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • BoundaryML/baml#3577: Introduces the core backtick/interpolation/control-flow/tagged-template parsing and lowering (Expr::Template, lower_expr_body, tagged-template closure/body shaping); this PR extends that implementation by marking desugared nodes synthetic and suppressing their LSP inlay hints.

Poem

🐇 Hop, hop, one truth for the grammar we keep,
A sync script ensures there's no drift while you sleep.
Backtick strings sparkle with colorful flair,
Compiler ghosts vanish—no hints left to snare.
From single source flows, all the patterns align,
The rabbit declares: this grammar is fine! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely describes the main changes: adding syntax highlighting support for backtick string interpolation and suppressing inlay hints for compiler-synthesized nodes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch avery/string-interpolation-highlighting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

⏭️ Performance benchmarks were skipped

Perf benchmarks (CodSpeed) are opt-in on pull requests — they no longer run on every push. They always run automatically after merge to canary/main.

To run them on this PR, do any of the following, then push a commit (or re-run CI):

  • Add RUN_CODSPEED=1 to the PR description, or
  • Include run-perf or /perf in the PR title or any commit message.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

No description provided.

Replace the span-equality heuristic the inlay-hint provider used to detect
synthesized `string.from(${…})` interpolation wrappers ("a real call's span is
strictly larger than its callee's") with an explicit, universal marker.

`AstSourceMap` now carries `synthetic_exprs` / `synthetic_stmts` /
`synthetic_patterns` sets. Lowering populates them via a scoped `synthesizing`
flag on the `alloc_expr`/`alloc_stmt`/`alloc_pattern` chokepoints: backtick
elaboration flips it on, so every generated node (the `string.from` wrapper, the
concat accumulator, the `+` folds, …) is recorded, while the user-written
segments lowered beforehand keep their non-synthetic ids. Nested backtick
strings save/restore the flag.

The annotations walker now asks `source_map.is_synthetic_expr/stmt(...)` instead
of comparing spans (call hint) and instead of matching `LetOrigin::Compiler`
(let type hint) — one uniform concept for "the compiler generated this," no
reliance on an unenforced span invariant.

Pure side metadata: IR output (HIR/TIR/MIR/codegen) is unchanged. The 10
`AstSourceMap { … }` literals in lower_cst/auto_derive_json are converted to
`..Default::default()` so future fields don't have to touch every site.

Tests: 500 lsp2_actions (incl. the interpolation golden + unit test), 70 ast,
19 backtick fixtures, 98 json/derive — all green; clippy --all-targets clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@baml_language/crates/baml_compiler2_ast/src/ast.rs`:
- Around line 594-600: Update the doc comments for the methods
`is_synthetic_stmt` and `is_synthetic_pattern` to correctly reference the fields
they actually check. In the `is_synthetic_stmt` method, change the doc comment
reference from `synthetic_exprs` to `synthetic_stmts` to match the field being
checked. In the `is_synthetic_pattern` method, change the doc comment reference
from `synthetic_exprs` to `synthetic_patterns` to match the field being checked.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bef9cc15-3ea5-4600-802c-810e86bed8a8

📥 Commits

Reviewing files that changed from the base of the PR and between 9a7b7bd and 6bf6389.

📒 Files selected for processing (5)
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
  • baml_language/crates/baml_compiler2_ast/src/auto_derive_json.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_cst.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
  • baml_language/crates/baml_lsp2_actions/src/annotations.rs

Comment thread baml_language/crates/baml_compiler2_ast/src/ast.rs Outdated
With the inlay-hint provider switched to the synthetic-node marker, nothing
distinguished `LetOrigin::Compiler` from `Source` anymore — every remaining
reader (type_info / completions / outline) lumped it into the `_` arm. Remove
the variant; the compiler-generated concat/loop accumulators now lower with
`LetOrigin::Source`, and "this binding is synthesized" is carried solely by
`AstSourceMap::synthetic_stmts`.

`LetOrigin` is now `{ Source, Client, RetryPolicy }`. The three readers match
all three explicitly (no wildcard), so adding a future variant is a compile
error rather than a silent fall-through.

No IR change (no snapshot prints the origin); workspace build clean, clippy
--all-targets clean, 570 lsp2_actions + ast tests and the backtick/json/derive
fixtures all green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs (1)

3348-3351: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mark tagged-template body allocations synthetic too.

elaborate_tagged_body creates compiler-only __tt_parts / __tt_values / __tt_cur lets and helper calls, but it is called with self.synthesizing == false, so the allocation helpers at Lines 490-509 never record these nodes in AstSourceMap.synthetic_*. After the new LetOrigin::Source assignments, LSP consumers that skip synthetic nodes can still see these tagged-template internals as source-authored.

Suggested fix
 fn elaborate_tagged_body(&mut self, segments: &[TemplateSegment], span: TextRange) -> ExprId {
+    let prev_synth = std::mem::replace(&mut self.synthesizing, true);
     let parts = Name::new(" __tt_parts");
     let values = Name::new(" __tt_values");
     let cur = Name::new(" __tt_cur");
@@
-    self.alloc_expr(
+    let body = self.alloc_expr(
         Expr::Block {
             stmts,
             tail_expr: Some(tail),
         },
         span,
-    )
+    );
+    self.synthesizing = prev_synth;
+    body
 }

Also applies to: 3381-3382, 3406-3443, 3621-3653

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs` around lines
3348 - 3351, The calls to elaborate_tagged_body in the lower_expr_body.rs file
(at the locations specified in the comment and additional lines mentioned) are
creating compiler-internal variables like __tt_parts, __tt_values, and __tt_cur,
but since self.synthesizing is false during these calls, the allocation helpers
at lines 490-509 do not record these nodes as synthetic in AstSourceMap. To fix
this, before each call to elaborate_tagged_body, save the current value of
self.synthesizing, set self.synthesizing to true, call elaborate_tagged_body,
and then restore the original self.synthesizing value. This ensures that all
compiler-generated nodes created within elaborate_tagged_body are properly
marked as synthetic so LSP consumers can correctly skip them.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs`:
- Around line 3348-3351: The calls to elaborate_tagged_body in the
lower_expr_body.rs file (at the locations specified in the comment and
additional lines mentioned) are creating compiler-internal variables like
__tt_parts, __tt_values, and __tt_cur, but since self.synthesizing is false
during these calls, the allocation helpers at lines 490-509 do not record these
nodes as synthetic in AstSourceMap. To fix this, before each call to
elaborate_tagged_body, save the current value of self.synthesizing, set
self.synthesizing to true, call elaborate_tagged_body, and then restore the
original self.synthesizing value. This ensures that all compiler-generated nodes
created within elaborate_tagged_body are properly marked as synthetic so LSP
consumers can correctly skip them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f198b221-78e8-4a84-bc38-3b8daff977af

📥 Commits

Reviewing files that changed from the base of the PR and between 6bf6389 and 8fbda17.

📒 Files selected for processing (6)
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
  • baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
  • baml_language/crates/baml_lsp2_actions/src/annotations.rs
  • baml_language/crates/baml_lsp2_actions/src/completions.rs
  • baml_language/crates/baml_lsp2_actions/src/outline.rs
  • baml_language/crates/baml_lsp2_actions/src/type_info.rs
💤 Files with no reviewable changes (1)
  • baml_language/crates/baml_compiler2_ast/src/ast.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • baml_language/crates/baml_lsp2_actions/src/annotations.rs

is_synthetic_stmt/is_synthetic_pattern referenced synthetic_exprs; point them
at the fields they actually check (per PR review).
The synthesizing flag was only wrapped around the untagged backtick path, so
`tag`...`` desugaring (both `lower_tagged_template_expr` and
`build_prompt_tag_closure`) allocated its `__tt_parts`/`__tt_values`/`__tt_cur`
accumulators and `.push(...)` calls with `synthesizing = false` — leaving them
unmarked in the source map. Wrap both `elaborate_tagged_body` call sites,
mirroring the untagged path; the user-written segments and `${…}` interps are
lowered beforehand and keep their non-synthetic ids.

No observable change today (those nodes happen to be skipped by the inlay
walker's empty-span / type-annotation guards), but it makes "synthetic =
compiler-generated" uniform across both template paths and robust to future
span changes — per PR review.

Tests:
- ast: `ast_tagged_template_body_marked_synthetic` asserts the elaborated body
  block + its stmts are synthetic while the tag expr and user `${…}` interp are
  not (verified it fails when the wrap is removed).
- lsp2: `annotations_skip_tagged_template_synthetic_hints` checks a custom tag in
  an expression function yields only the user's `let q: string` hint.
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.

1 participant