Skip to content

fix(fmt): accept backtick strings in prompt/attribute/template_string slots - #3859

Merged
codeshaunted merged 7 commits into
canaryfrom
avery/string-fmt-fix
Jun 27, 2026
Merged

codeshaunted merged 7 commits into
canaryfrom
avery/string-fmt-fix

Conversation

@codeshaunted

@codeshaunted codeshaunted commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Problem

baml fmt bailed on the whole file when a backtick string (`...`) was used as a prompt: value:

Expected token/node STRING_LITERAL or RAW_STRING_LITERAL,
but found BACKTICK_STRING_LITERAL at concierge.baml:21:13

The parser accepts backtick strings everywhere (so baml check/run/test were all happy), but the formatter's hand-rolled token matches for the declarative DSL slots only knew the quoted and raw string tokens. Investigation found the same gap in three slots:

  • prompt: value (PromptField) -- the reported crash
  • attribute arguments (@description(...), etc.)
  • template_string bodies

Fix

  • Generalize the prompt-value enum to a shared StringLiteralValue (raw | quoted | backtick) with a FromCST impl, reused by both PromptField and TemplateStringDecl (no duplicated match).
  • Add the backtick arm to AttributeArg.

Why backtick strings are printed verbatim (not re-indented like raw strings)

The original ask was to format backticks "just as well as raw strings", i.e. with the same interior re-indentation. I implemented that first, but a snapshot in the backtick_strings corpus caught a real problem, which I then confirmed against the compiler lowering:

  • Backtick strings are auto-dedented at lower time (BEP-049 §12 preprocess_template, with ${...} interpolations placeholdered and §13 whitespace control). Their runtime value depends on the interior's exact indentation.
  • A raw-style re-indent does not preserve that dedent. Proven via the compiler: `first line\n second line` lowers to "first line\n second line", but after a raw-style re-indent it lowers to "first line\nsecond line" -- a silent value change.

So backtick interiors are left byte-for-byte unchanged. A value-preserving re-indent would have to replicate the §12/§13 pipeline in the formatter and is deferred.

Side finding (not addressed here)

Raw strings are not dedented at lower time in expression position, so the existing raw-string re-indent already changes raw string values used as plain expression values (it's only safe in prompt position, where the prompt renderer dedents). Pre-existing; flagging for a separate look.

Tests

  • Regression tests in baml_fmt covering backtick acceptance in all three slots (verbatim + idempotent) and a value-preservation guard (backtick_interior_not_reindented).
  • Full baml_fmt suite (66) and the baml_tests formatter snapshot corpus (191) pass; the backtick_strings fixture snapshot is unchanged.

Summary by CodeRabbit

  • New Features

    • Added support for backtick string literals in prompts, template strings, and attribute arguments, with correct formatting and source span handling.
  • Bug Fixes

    • Fixed formatter rejection for backtick literals in declaration slots.
    • Improved auto-dedenting for multi-line backticks: re-indents only when the runtime value is preserved; otherwise keeps the literal verbatim (including block tags and multi-line ${...} interpolations).
  • Tests

    • Added fixtures and regression tests for formatting, idempotency, and value preservation.
  • Documentation

    • Updated BEP-049 guidance describing backtick auto-dedenting behavior and constraints.

… slots

baml fmt bailed on the whole file when a backtick string (`...`) was used
as a `prompt:` value, an attribute argument, or a `template_string` body:
those declarative slots only matched the quoted and raw string tokens, so
a BACKTICK_STRING_LITERAL produced a hard StrongAstError even though the
parser accepts backticks everywhere (baml check/run/test were all happy).

Generalize the prompt-value enum to a shared StringLiteralValue (raw |
quoted | backtick) used by both PromptField and TemplateStringDecl, and add
the backtick arm to AttributeArg.

Backtick strings are printed verbatim. Unlike raw strings, a backtick
string is auto-dedented at lower time (BEP-049 §12 preprocess_template,
with ${...} interpolations placeholdered and §13 whitespace control), so
its runtime value depends on the interior's exact indentation. A raw-style
interior reindent does not preserve that dedent and silently changes the
string's value (verified against the compiler lowering), so the interior
is left untouched. A value-preserving reindent would have to replicate the
§12/§13 pipeline and is deferred.
@vercel

vercel Bot commented Jun 26, 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 26, 2026 11:41pm
promptfiddle Ready Ready Preview, Comment Jun 26, 2026 11:41pm
promptfiddle2 Ready Ready Preview, Comment Jun 26, 2026 11:41pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6d3cfc0f-1e54-4aad-bf46-aa014ef13b92

📥 Commits

Reviewing files that changed from the base of the PR and between 03a117f and c4d6bfe.

📒 Files selected for processing (1)
  • baml_language/crates/baml_tests/tests/backtick_fmt_value_preservation.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • baml_language/crates/baml_tests/tests/backtick_fmt_value_preservation.rs

📝 Walkthrough

Walkthrough

Backtick string literals are now accepted in attribute arguments, prompt fields, and template string declarations. The formatter conditionally re-indents multi-line backticks when value-preserving, and new tests cover dedenting, verbatim cases, and idempotency.

Changes

Backtick string literal support

Layer / File(s) Summary
Backtick string core
baml_language/crates/baml_fmt/src/ast/tokens.rs
BacktickString now stores dedent safety, detects control tags and multiline interpolations, and conditionally re-indents backtick interiors before printing.
Literal AST updates
baml_language/crates/baml_fmt/src/ast/declarations.rs
PromptField and TemplateStringDecl now use StringLiteralValue, which accepts raw, quoted, and backtick literals and updates parsing, printing, and token-range handling.
Attribute args
baml_language/crates/baml_fmt/src/ast/attributes.rs
AttributeArg gains Backtick, parses BACKTICK_STRING_LITERAL, updates the unexpected-kind message, and delegates print/span handling to the backtick string.
Docs and formatter tests
baml_language/crates/baml_fmt/src/lib.rs, baml_language/crates/baml_tests/projects/compiles/backtick_decl_slots/main.baml, baml_language/crates/baml_tests/tests/backtick_fmt_value_preservation.rs, baml_language/crates/baml_tests/projects/compiles/backtick_dedent/main.baml
The BEP-049 comment is updated, and formatter tests plus compile fixtures cover prompt values, attribute arguments, template strings, dedenting, and backtick value preservation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

A bunny hopped through ticks and strings,
and watched the formatter trim its wings.
With values kept the same,
it danced a neat new frame,
then tucked the spaces in soft springs 🐰

🚥 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 clearly summarizes the main change: adding backtick string support in prompt, attribute, and template_string slots.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch avery/string-fmt-fix

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.

@github-actions

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.

@vercel
vercel Bot temporarily deployed to Preview – beps June 26, 2026 20:30 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 26, 2026 20:36 Inactive
@github-actions

Copy link
Copy Markdown

No description provided.

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 26, 2026 20:47 Inactive
…e slots

A compiles-tier fixture demonstrating backtick string literals in the two
declarative slots that compile cleanly: a `prompt:` value and a
`template_string` body. The generated formatter snapshot shows backtick
interiors printed verbatim, and the file is in canonical formatted form so
`baml fmt` leaves it unchanged.

(A backtick in `@description(...)` parses and formats but is a semantic
error -- "@description argument must be a string literal" -- so it cannot
live in a compiles fixture; that slot stays covered by the baml_fmt unit
tests.)
@vercel
vercel Bot temporarily deployed to Preview – beps June 26, 2026 21:37 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 26, 2026 21:44 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 26, 2026 21:56 Inactive
Re-indent a multi-line backtick interior to sit one level past the
surrounding block (like a raw string), in every position -- prompt,
attribute, template_string, and expression. A backtick string is
auto-dedented at lower time (BEP-049 §12), so the formatter strips the
same common-prefix the runtime would, re-emits at the block indent, and
then re-derives the runtime value of both the original and re-indented
interiors and bails to verbatim unless they are byte-identical.

A literal with a ${for}/${if} block tag (§13 whitespace control) or a
multi-line ${...} interpolation (placeholdered before the §12 min-indent,
so its inner lines must not move) is left verbatim, detected from the CST
in BacktickString::from_cst.

Tests: baml_fmt unit tests assert the exact dedented output per position
plus the verbatim cases (block tag, multi-line interp, relative-indent
preservation); a new baml_tests integration test formats a range of
backtick sources and asserts every literal's decoded + §12-dedented value
is unchanged by formatting.
@vercel
vercel Bot temporarily deployed to Preview – beps June 26, 2026 22:20 Inactive

@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_tests/tests/backtick_fmt_value_preservation.rs`:
- Around line 10-24: The backtick_value helper only models unescape plus
preprocess_template, so it is not validating the unsafe backtick cases that
depend on lowering behavior. Update the test to either compute expected values
through the same lowering path used by the compiler for backtick literals, or
remove the `${for}`/`${if}` and multi-line `${...}` rows from this assertion set
and keep them covered by the verbatim/idempotency checks. Use backtick_value and
the affected table entries to locate the test cases.
🪄 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: a508aa1b-4e22-40dc-a3ef-32f8ae9c8cdb

📥 Commits

Reviewing files that changed from the base of the PR and between 8fb21d3 and aaf6a0a.

⛔ Files ignored due to path filters (2)
  • baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__01_lexer__main.snap is excluded by !**/*.snap
  • baml_language/crates/baml_tests/snapshots/compiles/backtick_decl_slots/baml_tests__compiles__backtick_decl_slots__10_formatter__main.snap is excluded by !**/*.snap
📒 Files selected for processing (4)
  • baml_language/crates/baml_fmt/src/ast/tokens.rs
  • baml_language/crates/baml_fmt/src/lib.rs
  • baml_language/crates/baml_tests/projects/compiles/backtick_decl_slots/main.baml
  • baml_language/crates/baml_tests/tests/backtick_fmt_value_preservation.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • baml_language/crates/baml_tests/projects/compiles/backtick_decl_slots/main.baml

@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 26, 2026 22:26 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 26, 2026 22:46 Inactive
…apshots

A compiles-tier fixture whose backtick bodies are deliberately mis-indented
so the formatter snapshot shows the dedent in action across the interesting
cases: an interior pushed far past the block (pulled back to one level), a
ragged interior (relative indentation preserved), blank lines preserved,
single-line ${...} interpolations moved with the text, a first-line-flush
body whose deeper second line is kept (it is part of the §12 value), and the
two verbatim fallbacks -- a ${for} block tag and an over-indented multi-line
${...} interpolation, neither re-indented.
@vercel
vercel Bot temporarily deployed to Preview – beps June 26, 2026 22:51 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 26, 2026 22:57 Inactive
`[`Self::dedent_safe`]` in the public BacktickString doc links to a private
field, which breaks `cargo doc` for public items (rustdoc::private-intra-doc-links
under -D warnings). Refer to the field in plain code text instead.
@vercel
vercel Bot temporarily deployed to Preview – beps June 26, 2026 23:08 Inactive
A single-line backtick has no newline, so the reindent path bails immediately
and prints it verbatim. Add a regression test for the non-obvious case -- a
single-line backtick longer than the line width -- since wrapping it would
insert a newline and change the string's value.
@vercel
vercel Bot temporarily deployed to Preview – beps June 26, 2026 23:15 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 26, 2026 23:21 Inactive
…ases

Addresses review feedback: backtick_value() (unescape + preprocess_template)
does not model §13 whitespace control or placeholder substitution, so a
value-equality assertion is not meaningful for the ${for}/${if} and multi-line
interpolation rows -- it could miss a real lowered-value change. Tag each case
Reindented or Verbatim: Reindented keeps the §12 value-equality check (sound,
since those have no block tag / multi-line interp), Verbatim asserts the raw
backtick literal is left byte-for-byte unchanged (the stronger property the
formatter actually guarantees for them).
@vercel
vercel Bot temporarily deployed to Preview – beps June 26, 2026 23:24 Inactive
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle2 June 26, 2026 23:30 Inactive
@codeshaunted
codeshaunted enabled auto-merge June 26, 2026 23:30
@codeshaunted
codeshaunted added this pull request to the merge queue Jun 26, 2026
@vercel
vercel Bot temporarily deployed to Preview – promptfiddle June 26, 2026 23:41 Inactive
Merged via the queue into canary with commit d2339a3 Jun 27, 2026
53 checks passed
@codeshaunted
codeshaunted deleted the avery/string-fmt-fix branch June 27, 2026 00:01

This branch was previously deployed

3 inactive deployments
Preview – promptfiddle — c4d6bfe8 Deployed Jun 26, 2026 by vercel[bot]
Preview – promptfiddle2 — c4d6bfe8 Deployed Jun 26, 2026 by vercel[bot]
Preview – beps — c4d6bfe8 Deployed Jun 26, 2026 by vercel[bot]
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