BEP-049 M1–M4: backtick strings, interpolation, control flow, tagged templates#3577
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Lands the inert form of BEP-049 backtick strings: lex, parse, lower, and
format `` `...` `` (including multi-tick ladder + auto-dedent), with `${...}`
left as literal text until M2 wires interpolation.
- Lexer: new `Backtick` token + `BACKTICK` syntax kind.
- Parser: anchored-close ladder per BEP §8 (incl. JEP-326 cases). Detects
empty multi-tick (`` `` ``) at the opener and emits a clean diagnostic
instead of silently consuming downstream content.
- AST: `BacktickStringLiteral` with `value()` that decodes escapes and runs
multi-line content through `baml_base::dedent::preprocess_template`.
- HIR lowering: backtick literal → `Expr::Literal::String`.
- Formatter: prints backtick strings verbatim, round-trip idempotent.
- Consolidation: moves `unescape_string_literal` into `baml_base::escape`
(kills two near-identical copies) and adds `unescape_backtick_string_literal`
for `` \` `` and `\${` escapes. Lifts `preprocess_template` out of
`sys_llm::jinja::render` into `baml_base::dedent` so both prompt rendering
and the new backtick lowering share a single dedenter.
Tests:
- Unit: 3 lexer + 8 parser + 9 escape + 6 dedent + 6 HIR lowering + 3 fmt.
- Fixture pipeline: `baml_tests/projects/compiles/backtick_strings/`
covers lex → parser → HIR → TIR → MIR → diagnostics → codegen → formatter.
- Runtime: 4 end-to-end tests in `bex_engine` execute backtick functions
on the VM and assert the returned `BexExternalValue::String` matches.
Tracking doc at `architecture/bep_049_milestones.md` lays out M1–M7.
Lexer snapshot churn is mechanical: existing `.baml` fixtures with literal
backticks in docstrings/error-message strings used to lex as `Error "\``"`
and now lex as `Backtick "\``"`.
159d8e3 to
3ab9197
Compare
|
No description provided. |
📝 WalkthroughWalkthroughImplements BEP-049 backtick interpolated string literals and tagged-template expressions end-to-end: shared ChangesBEP-049 End-to-end Implementation
CI Size-gate Updates
Sequence Diagram(s)sequenceDiagram
rect rgba(135, 206, 250, 0.5)
note over BAML Source,Lexer: Lexing
BAML Source->>Lexer: backtick literal or tag`...`
Lexer->>Parser: Backtick, Dollar, LBrace, Word, RBrace tokens
end
rect rgba(144, 238, 144, 0.5)
note over Parser,SyntaxAST: Parsing & Syntax
Parser->>Parser: parse_backtick_string (delimiter ladder, ${for}/${if}/${expr})
Parser->>SyntaxAST: BACKTICK_STRING_LITERAL or TAGGED_TEMPLATE_EXPR node
SyntaxAST->>SyntaxAST: BacktickStringLiteral API — dedent, escape decode, segments
end
rect rgba(255, 218, 185, 0.5)
note over lower_cst,TIR: Lowering & Typing
lower_cst->>lower_expr_body: detect backtick prompt in llm.prompt_field()
lower_expr_body->>CompilerAST: produce Expr::Template (TemplateTag::Default or Custom)
lower_cst->>lower_expr_body: synthesize_llm_call_with_prompt → stores stream_body
TIR->>TIR: validate_tagged_tag, seed template_body_params, check interp nullability
end
rect rgba(221, 160, 221, 0.5)
note over PPIR,LLM Runtime: Execution
PPIR->>PPIR: prefer pre-lowered stream_body over synthesized fallback
LLM Runtime->>LLM Runtime: call_llm_function with prompt_closure parameter
LLM Runtime->>LLM Runtime: branch on prompt_closure — closure vs legacy Jinja
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~150 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
baml_language/crates/baml_compiler_syntax/src/ast.rs (1)
1063-1073: 💤 Low valueDedent condition checks decoded content instead of source multiline status.
The condition
decoded.contains('\n')determines whether to apply dedent based on the decoded content having newlines. This differs fromis_multiline()which correctly checks whether the source spans multiple lines. Per BEP-049, dedent should apply only to source-level multiline strings, not to single-line sources containing\nescape sequences.Practically,
preprocess_templateon single-line content with embedded newlines produces the same result, so tests pass. However, the semantic intent is clearer withis_multiline().♻️ Suggested fix for clarity
pub fn value(&self) -> String { let Some(inner) = self.raw_inner() else { return String::new(); }; let decoded = unescape_backtick_string_literal(&inner); - if decoded.contains('\n') { + if self.is_multiline() { baml_base::dedent::preprocess_template(&decoded) } else { decoded } }🤖 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_compiler_syntax/src/ast.rs` around lines 1063 - 1073, The value() method currently decides to call baml_base::dedent::preprocess_template based on decoded.contains('\n'); change it to use the source-level multiline check by calling is_multiline() instead (i.e., replace the decoded.contains('\n') condition with self.is_multiline()). Keep the rest of the logic (unescape_backtick_string_literal(&inner) and returning either preprocess_template(&decoded) or decoded) unchanged so dedent runs only for source multiline strings.baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs (1)
2300-2301: 💤 Low valueRemove redundant import.
rowan::ast::AstNodeis already imported at line 10, so the localusestatement on line 2301 is unnecessary.♻️ Remove redundant import
fn lower_backtick_string_literal(&mut self, node: &SyntaxNode) -> ExprId { use baml_compiler_syntax::BacktickStringLiteral; - use rowan::ast::AstNode; let value = BacktickStringLiteral::cast(node.clone())🤖 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 2300 - 2301, Remove the redundant import of rowan::ast::AstNode in lower_expr_body.rs: delete the duplicate `use rowan::ast::AstNode;` line (the file already imports AstNode earlier), leaving only the single existing import to avoid duplicate imports and clippy/warning noise; locate the extra `use` near the block that also imports `baml_compiler_syntax::BacktickStringLiteral` and remove it.
🤖 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_compiler_parser/src/parser.rs`:
- Around line 1840-1849: The helper find_first_backtick_pos (and the similar
helper at the other location) only skips basic trivia via is_basic_trivia, but
parse_backtick_string is entered via self.at(TokenKind::Backtick) which skips
full trivia (including comments), so these helpers can miss an opening backtick
when comments lie between current and the backtick. Fix both helpers to skip the
same set of trivia that self.at uses (i.e., include comments) — replace the
is_basic_trivia check with the broader trivia predicate used by self.at (or
reuse the same utility method used by self.at, or add a small wrapper like
is_full_trivia/is_trivia_including_comments) so the loop advances past comments
as well and then checks for TokenKind::Backtick; update both occurrences
(find_first_backtick_pos and the other helper around the parse_backtick_string
flow) to use this corrected trivia-skipping logic.
---
Nitpick comments:
In `@baml_language/crates/baml_compiler_syntax/src/ast.rs`:
- Around line 1063-1073: The value() method currently decides to call
baml_base::dedent::preprocess_template based on decoded.contains('\n'); change
it to use the source-level multiline check by calling is_multiline() instead
(i.e., replace the decoded.contains('\n') condition with self.is_multiline()).
Keep the rest of the logic (unescape_backtick_string_literal(&inner) and
returning either preprocess_template(&decoded) or decoded) unchanged so dedent
runs only for source multiline strings.
In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs`:
- Around line 2300-2301: Remove the redundant import of rowan::ast::AstNode in
lower_expr_body.rs: delete the duplicate `use rowan::ast::AstNode;` line (the
file already imports AstNode earlier), leaving only the single existing import
to avoid duplicate imports and clippy/warning noise; locate the extra `use` near
the block that also imports `baml_compiler_syntax::BacktickStringLiteral` and
remove it.
🪄 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: e5e898f2-8a4b-4eaf-afda-5b333e80befe
⛔ Files ignored due to path filters (55)
baml_language/Cargo.lockis excluded by!**/*.lockbaml_language/crates/baml_tests/snapshots/broken_syntax/banned_expressions/baml_tests__broken_syntax__banned_expressions__01_lexer__banned_expressions.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/error_cases/baml_tests__broken_syntax__error_cases__01_lexer__syntax_errors.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/mismatched_brackets/baml_tests__broken_syntax__mismatched_brackets__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/namespaces_same_name/baml_tests__broken_syntax__namespaces_same_name__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/parser_error_recovery/baml_tests__broken_syntax__parser_error_recovery__01_lexer__partial_input.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/type_annotation_errors/baml_tests__broken_syntax__type_annotation_errors__01_lexer__type_malformed.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__assert_std__/baml_tests__compiles____assert_std____01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__testing_std__/baml_tests__compiles____testing_std____01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__02_parser__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__10_formatter__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/deep_method_call/baml_tests__compiles__deep_method_call__01_lexer__deep_method_call.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/is_operator/baml_tests__compiles__is_operator__01_lexer__is_operator.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_alias_basic/baml_tests__compiles__json_alias_basic__01_lexer__json_alias_basic.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_cross_namespace_static_call/baml_tests__compiles__json_cross_namespace_static_call__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_cross_namespace_static_call/baml_tests__compiles__json_cross_namespace_static_call__01_lexer__ns_pkg_ns_inner_box.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_llm_return_type/baml_tests__compiles__json_llm_return_type__01_lexer__json_llm_return_type.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_map_literal/baml_tests__compiles__json_map_literal__01_lexer__json_map_literal.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_to_from_string_composite_generic/baml_tests__compiles__json_to_from_string_composite_generic__01_lexer__json_to_from_string_composite_generic.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_to_from_string_concrete/baml_tests__compiles__json_to_from_string_concrete__01_lexer__json_to_from_string_concrete.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_to_from_string_generic_forwarding/baml_tests__compiles__json_to_from_string_generic_forwarding__01_lexer__json_to_from_string_generic_forwarding.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_to_from_string_three_level/baml_tests__compiles__json_to_from_string_three_level__01_lexer__json_to_from_string_three_level.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/lambda_fat_arrow/baml_tests__compiles__lambda_fat_arrow__01_lexer__lambda_fat_arrow.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/lambda_field_access/baml_tests__compiles__lambda_field_access__01_lexer__lambda_field_access.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/lexical_scoping/baml_tests__compiles__lexical_scoping__01_lexer__lexical_scoping.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/patterns_new/baml_tests__compiles__patterns_new__01_lexer__patterns_new.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/reflect_type_of_user_generic/baml_tests__compiles__reflect_type_of_user_generic__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/stream_llm_inferred_typeargs/baml_tests__compiles__stream_llm_inferred_typeargs__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_annotation/baml_tests__compiles__type_annotation__01_lexer__type_alias_interaction.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_annotation/baml_tests__compiles__type_annotation__01_lexer__type_as_identifier.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_annotation/baml_tests__compiles__type_annotation__01_lexer__type_positions.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/captured_field_chain/baml_tests__diagnostic_errors__captured_field_chain__01_lexer__captured_field_chain.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_return_type_mismatch/baml_tests__diagnostic_errors__catch_return_type_mismatch__01_lexer__catch_return_type_mismatch.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_throw_regressions/baml_tests__diagnostic_errors__catch_throw_regressions__01_lexer__regressions.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__01_lexer__match_exprs.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generic_call_ambiguity/baml_tests__diagnostic_errors__generic_call_ambiguity__01_lexer__array_compare.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generic_call_ambiguity/baml_tests__diagnostic_errors__generic_call_ambiguity__01_lexer__generic_call.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generics/baml_tests__diagnostic_errors__generics__01_lexer__classes.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/instanceof_removed/baml_tests__diagnostic_errors__instanceof_removed__01_lexer__instanceof_removed.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/json_static_method_arity/baml_tests__diagnostic_errors__json_static_method_arity__01_lexer__json_static_method_arity.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/match_exhaustiveness/baml_tests__diagnostic_errors__match_exhaustiveness__01_lexer__match_exhaustiveness.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/patterns_new/baml_tests__diagnostic_errors__patterns_new__01_lexer__patterns_new.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/stream_types/baml_tests__diagnostic_errors__stream_types__01_lexer__builtin_refs.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/test_with_runner_ambiguity/baml_tests__diagnostic_errors__test_with_runner_ambiguity__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/type_reflection_strict/baml_tests__diagnostic_errors__type_reflection_strict__01_lexer__reflect_intrinsic_errors.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/type_reflection_strict/baml_tests__diagnostic_errors__type_reflection_strict__01_lexer__strict_normalization.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/unknown_type_error/baml_tests__diagnostic_errors__unknown_type_error__01_lexer__unknown_type_error.snapis excluded by!**/*.snap
📒 Files selected for processing (15)
baml_language/crates/baml_base/src/dedent.rsbaml_language/crates/baml_base/src/escape.rsbaml_language/crates/baml_base/src/lib.rsbaml_language/crates/baml_compiler2_ast/src/lib.rsbaml_language/crates/baml_compiler2_ast/src/lower_expr_body.rsbaml_language/crates/baml_compiler_lexer/src/tokens.rsbaml_language/crates/baml_compiler_parser/src/parser.rsbaml_language/crates/baml_compiler_syntax/Cargo.tomlbaml_language/crates/baml_compiler_syntax/src/ast.rsbaml_language/crates/baml_compiler_syntax/src/syntax_kind.rsbaml_language/crates/baml_fmt/src/ast/expressions.rsbaml_language/crates/baml_fmt/src/ast/tokens.rsbaml_language/crates/baml_fmt/src/lib.rsbaml_language/crates/baml_tests/projects/compiles/backtick_strings/main.bamlbaml_language/crates/bex_engine/tests/backtick_strings.rs
Lands the active form of BEP-049 backtick strings: `${expr}` interpolates,
with full block-expression body semantics (BEP §4 — statements + optional
trailing expression). Plus TypeScript-parity tightening for escape decoding
identified by auditing typescript-go's scanner.
## Parser / segments / lowering
- Parser: detect adjacent `${` (no trivia between `$` and `{`) and dispatch
to `parse_backtick_interpolation`, which reuses `parse_block_expr` for
the body — so the full host block grammar (let, while, if, throw, return,
…) works inside `${...}`.
- `BacktickSegment` AST type: alternating `Text` / `Interp` segments
consumed by the lowerer. M4's tagged-template lowering will share the
same primitive — different transform, same input.
- Multi-line dedent (BEP §12) treats `${...}` as opaque placeholders for
min-indent calculation per §12 rule 8.
- Lowering: text segments + `Interp.to_string()` concatenated into a
left-folded `+` chain. Empty / statement-only blocks (BEP §4 unit case)
emit `""` directly — no `unit.to_string()` failure.
## Implicit `.to_string()` (BEP §11)
- Added `to_string(self) -> string` to `Int`, `Bool`, `Float`, `String`
in `baml_std` (Rust impls in `bex_vm::package_baml::*`). String's
variant is a no-op so the lowering doesn't need to special-case it.
- Untagged `${expr}` lowers to `expr.to_string()`. Non-string types
(incl. `int?`, `null`, user classes without `to_string`) produce a
compile error — BEP §7 explicitly endorses this: "surfaces null-handling
bugs in prompts at type-check time instead of at LLM-call time."
## TS-parity escape tightening (audit vs typescript-go/internal/scanner)
- AA: `\r\n` and bare `\r` in backtick text normalize to `\n` (mirrors
scanner.go:1650-1660).
- BB: `\b`, `\v`, `\f` extended escapes added to the canonical escape
decoder (mirrors scanner.go:1721-1736). Applied to both quote and
backtick flavors.
## Bug fixes surfaced by audit
- Lexer regex previously absorbed a trailing `$` into the preceding Word
(`before-$` → one token), masking `${` adjacency. Tightened to require
any `$` to be a separator between identifier chunks. Caught by
`backtick_nested_in_interpolation`.
- `BacktickStringLiteral::delimiter_count` used `filter_map(into_token)
.take_while(BACKTICK)`, silently skipping past child nodes and
miscounting the closing run as part of the opener for `${a}${b}`
(adjacent interps). Now breaks on first non-BACKTICK element.
## Tests
- 38 runtime tests in `bex_engine` exercising every shape: while-loop in
interp body, recursive function calls, mixed-type implicit to_string,
TS-parity escapes, CR/CRLF normalization, comments inside `${...}`,
trailing `$`, multi-tick + interp, nested backticks.
- 18 parser unit tests (incl. segments adjacency regression, empty
multi-tick, JEP-326 anchored close, escape-doesn't-interpolate).
- 9 escape-decoder unit tests + 7 lowering unit tests.
- Compile-pipeline fixtures:
- `compiles/backtick_strings/main.baml` extended with M2 cases —
8 stages green (lexer → parser → HIR → TIR → MIR → diagnostics →
codegen → formatter).
- `diagnostic_errors/backtick_strict_types/main.baml` pins the
strict-type compile errors for null / class-without-to_string /
optional interpolation cases.
## Fixture / snapshot churn (incidental)
- `__baml_std__` snapshots: updated for 4 new `to_string` methods.
- `bytecode_display_expanded_unoptimized`: builtin offsets shifted.
- `diagnostic_errors/control_flow/main.baml`: previously errored on
`z.to_string()` (Int had no to_string); rewrote to use a guaranteed-
missing method so the project keeps its tier classification.
There was a problem hiding this comment.
Actionable comments posted: 2
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_compiler_parser/src/parser.rs (1)
1919-1923:⚠️ Potential issue | 🟠 Major | ⚡ Quick winConsume only the immediate raw token after
\.The second
bump_raw()skips intervening whitespace/newlines, so inputs like`\ ${name}`or`\ `can swallow the later$/`and suppress interpolation or the closing delimiter. After consuming the backslash, this path needs to emit exactly one raw token, not “next non-trivia token.”Proposed fix
+ /// Consume exactly one raw token at `self.current` without skipping trivia. + fn bump_exact_raw(&mut self) -> bool { + let Some(token) = self.tokens.get(self.current) else { + return false; + }; + self.events.push(Event::Token { + kind: token_kind_to_syntax_kind(token.kind), + text: token.text.clone(), + }); + self.current += 1; + true + } + fn parse_backtick_content(&mut self, opening_ticks: usize) { let mut loop_counter = 0; loop { @@ if self.at_raw(TokenKind::Backslash) { self.bump_raw(); if self.current < self.tokens.len() { - self.bump_raw(); + self.bump_exact_raw(); } continue; }🤖 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_compiler_parser/src/parser.rs` around lines 1919 - 1923, After detecting a raw backslash (self.at_raw(TokenKind::Backslash)) we should consume exactly one immediate token (the token directly following the backslash) instead of calling bump_raw which advances to the next non-trivia token; change the second consume to advance/consume only the next token in the token stream (the token at self.current, after the backslash bump) so interpolation delimiters or closing backticks aren't skipped. Update the logic around self.bump_raw(), self.current and self.tokens so the backslash path consumes exactly one following token (the immediate token) rather than “next non-trivia token.”
🧹 Nitpick comments (1)
baml_language/crates/baml_compiler_lexer/src/tokens.rs (1)
853-873: ⚡ Quick winAdd a regression case for
before-${x}token splitting.Line 856 validates
${...}tokenization, but it doesn’t cover the specific guard documented in Lines 112-116 (preventingWord("before-$")). Please add a case assertingWord("before-"), Dollar, LBrace, Word("x"), RBracefor`before-${x}`.Proposed test addition
#[test] fn test_backtick_with_interpolation_tokens() { // ${...} inside backtick — lexer emits the raw tokens; parser assembles let source = "`Hello ${name}`"; let tokens = lex_no_whitespace(source); assert_eq!( tokens, vec![ TokenKind::Backtick, TokenKind::Word, // Hello TokenKind::Dollar, TokenKind::LBrace, TokenKind::Word, // name TokenKind::RBrace, TokenKind::Backtick, ] ); assert_eq!(reconstruct_source(&lex(source)), source); + + // Regression: `${` must not be absorbed into a trailing-$ word segment. + let source2 = "`before-${x}`"; + let tokens2 = lex_no_whitespace(source2); + assert_eq!( + tokens2, + vec![ + TokenKind::Backtick, + TokenKind::Word, // before- + TokenKind::Dollar, + TokenKind::LBrace, + TokenKind::Word, // x + TokenKind::RBrace, + TokenKind::Backtick, + ] + ); + assert_eq!(reconstruct_source(&lex(source2)), source2); }🤖 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_compiler_lexer/src/tokens.rs` around lines 853 - 873, Update the backtick interpolation test to cover the guard that prevents combining a trailing '$' into a Word token: inside the test_backtick_with_interpolation_tokens (which uses lex_no_whitespace and lex/reconstruct_source), add an assertion for the source "`before-${x}`" that expects tokens [TokenKind::Backtick, TokenKind::Word (with text "before-"), TokenKind::Dollar, TokenKind::LBrace, TokenKind::Word (with text "x"), TokenKind::RBrace, TokenKind::Backtick] and also assert reconstruct_source(&lex(source)) == source to ensure round-trip correctness; locate uses of lex_no_whitespace, lex, and reconstruct_source in the test to add this new case.
🤖 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_compiler_syntax/src/ast.rs`:
- Around line 1168-1196: The dedent remapping in BacktickStringLiteral::segments
misaligns when the first decoded part is RawPart::Interp because the current
loop only advances the split iterator for RawPart::Text; fix by changing the
remapping loop that walks decoded (the for p in &mut decoded after
dedented.split(PLACEHOLDER)) so that it consumes iter.next() for every part
(call iter.next() and ignore its value for RawPart::Interp, assign it to
RawPart::Text for Text parts) thereby preserving split positions, keep the
existing tail-handling logic that appends any leftover rest to the last
RawPart::Text, and add a new test covering a multiline template that starts with
an interpolation to prevent regressions.
In `@baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs`:
- Around line 2300-2302: The doc comment on lower_backtick_string_literal is
stale: update the sentence "Implicit `.to_string()` for non-string types is task
`#13`" to state that lowering now performs implicit `.to_string()` on non-string
interpolations (reflecting the conversion logic in the interpolation-handling
block that forces non-string child expressions to strings), so the comment
matches the implemented behavior in lower_backtick_string_literal and the
adjacent interpolation-lowering code paths.
---
Outside diff comments:
In `@baml_language/crates/baml_compiler_parser/src/parser.rs`:
- Around line 1919-1923: After detecting a raw backslash
(self.at_raw(TokenKind::Backslash)) we should consume exactly one immediate
token (the token directly following the backslash) instead of calling bump_raw
which advances to the next non-trivia token; change the second consume to
advance/consume only the next token in the token stream (the token at
self.current, after the backslash bump) so interpolation delimiters or closing
backticks aren't skipped. Update the logic around self.bump_raw(), self.current
and self.tokens so the backslash path consumes exactly one following token (the
immediate token) rather than “next non-trivia token.”
---
Nitpick comments:
In `@baml_language/crates/baml_compiler_lexer/src/tokens.rs`:
- Around line 853-873: Update the backtick interpolation test to cover the guard
that prevents combining a trailing '$' into a Word token: inside the
test_backtick_with_interpolation_tokens (which uses lex_no_whitespace and
lex/reconstruct_source), add an assertion for the source "`before-${x}`" that
expects tokens [TokenKind::Backtick, TokenKind::Word (with text "before-"),
TokenKind::Dollar, TokenKind::LBrace, TokenKind::Word (with text "x"),
TokenKind::RBrace, TokenKind::Backtick] and also assert
reconstruct_source(&lex(source)) == source to ensure round-trip correctness;
locate uses of lex_no_whitespace, lex, and reconstruct_source in the test to add
this new case.
🪄 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: 47ec80ea-80ac-4bcb-af3d-65b1627839d4
⛔ Files ignored due to path filters (27)
baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_builtin_item_by_definition.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_builtin_string.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_truncation_hint.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__02_parser__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__10_formatter__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__02_parser__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__10_formatter__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__02_parser__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__10_formatter__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/null_handling/baml_tests__diagnostic_errors__null_handling__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/null_handling/baml_tests__diagnostic_errors__null_handling__05_diagnostics.snapis excluded by!**/*.snap
📒 Files selected for processing (18)
baml_language/crates/baml_base/src/escape.rsbaml_language/crates/baml_builtins2/baml_std/baml/bool.bamlbaml_language/crates/baml_builtins2/baml_std/baml/float.bamlbaml_language/crates/baml_builtins2/baml_std/baml/int.bamlbaml_language/crates/baml_builtins2/baml_std/baml/string.bamlbaml_language/crates/baml_compiler2_ast/src/lib.rsbaml_language/crates/baml_compiler2_ast/src/lower_expr_body.rsbaml_language/crates/baml_compiler_lexer/src/tokens.rsbaml_language/crates/baml_compiler_parser/src/parser.rsbaml_language/crates/baml_compiler_syntax/src/ast.rsbaml_language/crates/baml_tests/projects/compiles/backtick_strings/main.bamlbaml_language/crates/baml_tests/projects/diagnostic_errors/backtick_strict_types/main.bamlbaml_language/crates/baml_tests/projects/diagnostic_errors/control_flow/main.bamlbaml_language/crates/bex_engine/tests/backtick_strings.rsbaml_language/crates/bex_vm/src/package_baml/float.rsbaml_language/crates/bex_vm/src/package_baml/int.rsbaml_language/crates/bex_vm/src/package_baml/primitives.rsbaml_language/crates/bex_vm/src/package_baml/string.rs
✅ Files skipped from review due to trivial changes (1)
- baml_language/crates/baml_tests/projects/diagnostic_errors/backtick_strict_types/main.baml
Three follow-ups from CodeRabbit review:
1. ast.rs:1196 — segments() dedent remapping misaligned text pieces when
the first decoded part was Interp (multi-line literal starting with
`${...}`). The old loop walked decoded and advanced the split iterator
only for Text parts; the leading empty piece (for "no Text before
first Interp") was assigned to the first Text part, shifting every
subsequent Text by one. Rewrote to walk the pieces/interps in lockstep
instead — one piece per text position, interps interleaved. Skips
empty Text pieces to preserve the "no empty Text segments" invariant.
Regression test: backtick_segments_multiline_starts_with_interp.
2. parser.rs:1849 — count_consecutive_backticks /
find_first_backtick_pos only skipped basic trivia, but the entry
guard `self.at(TokenKind::Backtick)` skips comments too. A literal
like `let s = /*c*/ <backtick>ok<backtick>` reached the helpers with
comment tokens still in front and returned 0 → spurious parse error.
Both helpers now use the existing comment-aware
`skip_trivia_and_comments_from`. Regression tests:
backtick_after_block_comment_parses, backtick_after_line_comment_parses.
3. lower_expr_body.rs:2302 — doc comment said implicit `.to_string()`
was deferred to task #13, but the lowering already applies it. Updated
to reflect the actual behavior.
Six items from two ultrareview runs: - bug_001 (normal): preprocess_template panicked on multi-byte Unicode whitespace (NBSP U+00A0, LINE SEPARATOR U+2028) mixed with ASCII indent. min_indent computed in bytes, then sliced at a non-char- boundary. Reachable via macOS Option+Space or rich-text paste. Walk leading whitespace by char; strip per-line up to target_bytes while always landing on a char boundary (over-strip by one whitespace char at worst). 3 new tests in baml_base::dedent. - bug_011 (normal): backslash escape inside a backtick string overshot — the second bump_raw() skipped trivia before taking the escape target, so backslash + space + closing-backtick silently consumed the closer as the escape target. Added bump_one_token_raw that emits exactly the token at self.current with no trivia walk; use it for the escape target. Regression test: backtick_backslash_followed_by_whitespace_does_not_eat_closer. - bug_008 + bug_002 (normal, same helper): has_backtick_close_ahead_from walked tokens to EOF without scope or comment-awareness. Any downstream backtick literal OR backtick run inside a // line comment (lexer doesn't tokenize comments — // is two Slash tokens, content goes through raw) satisfied the look-ahead and disabled the empty-multi-tick guard. Bound the scan to the current top-level item (abort on intervening function/class/etc. keyword) and skip comments via skip_comment_at. Tests verify the diagnostic fires at the actual bad opener (byte offset check), not from a downstream remnant. 2 new tests. - bug_006 (nit): segments() hardcoded U+F8FF as the in-band dedent placeholder, but U+F8FF is a legitimate PUA codepoint (Apple logo on macOS) that users can type or paste. Collision silently dropped the user's char and misaligned interpolation positions. Walk the PUA range U+E000..U+F8FF and pick the first codepoint that doesn't appear in content. Practically always U+E000. 1 new test. - bug_010 (nit, doc): escape.rs module doc listed 6 always-on escapes but \b/\v/\f are also unconditionally decoded (TS parity, pinned by test). Updated to list all 9 with a TS-parity note.
`if` and `while` already accept paren-optional conditions; `match`
previously required them. Bring it in line — both `match (x) { ... }`
and `match x { ... }` now parse. The optional `: Type` annotation is
only accepted in the parenthesized form (parens are the syntactic anchor
that disambiguates the colon from a binding ascription).
The no-paren form guards against object-literal postfix capture by
incrementing `suppress_object_literal_depth` around the scrutinee
parse, mirroring how `parse_spawn_expr` handles the same hazard. So
`match Foo { _ => 0 }` correctly parses Foo as the scrutinee and
`{ _ => 0 }` as the match body — not as a struct constructor.
Tests:
- match_scrutinee_parens_optional
- match_scrutinee_with_type_annotation_no_parens
- match_paren_form_still_works
Snapshot churn (all real improvements, not regressions):
- compiler2_mir::match_expr previously captured `baml.sys.panic
("parse error")` because its fixture used the no-paren form;
now compiles correctly to a real jump table.
- backtick_strings 03_hir / 04_tir / 04_5_mir / 06_codegen all
dropped a trailing `+ ""` op — incidental fallout from the
segments() lockstep rewrite in PR #3577's earlier round (stale
snapshots that weren't re-run until this full-workspace pass).
There was a problem hiding this comment.
🧹 Nitpick comments (1)
baml_language/crates/baml_compiler_parser/src/parser.rs (1)
7772-7785: ⚡ Quick winAdd a negative test for unparenthesized type-ascribed scrutinees.
You’ve documented
: Typeas parenthesized-only, but current additions only assert positive forms. Add amatch x: int { ... }test that expects diagnostics to lock that contract.Suggested test
+ #[test] + fn match_scrutinee_type_annotation_requires_parens() { + let source = " +function Demo(x: int) -> int { + match x: int { + _ => 0, + } +} +"; + let (_root, errors) = parse_source(source); + assert!( + !errors.is_empty(), + "expected parse errors for unparenthesized type-ascribed scrutinee" + ); + }🤖 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_compiler_parser/src/parser.rs` around lines 7772 - 7785, Add a negative test that ensures unparenthesized type-ascribed scrutinees (e.g., `match x: int { ... }`) produce diagnostics: create a new test function (e.g., match_scrutinee_with_type_annotation_unparenthesized) that calls parse_source with a source containing `match x: int { _ => 0, }`, then assert that the returned errors are non-empty (e.g., assert!(errors.len() > 0)) or otherwise verify the expected diagnostic appears; use the same helpers seen in the file (parse_source and the errors variable) to locate and validate the failure.
🤖 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.
Nitpick comments:
In `@baml_language/crates/baml_compiler_parser/src/parser.rs`:
- Around line 7772-7785: Add a negative test that ensures unparenthesized
type-ascribed scrutinees (e.g., `match x: int { ... }`) produce diagnostics:
create a new test function (e.g.,
match_scrutinee_with_type_annotation_unparenthesized) that calls parse_source
with a source containing `match x: int { _ => 0, }`, then assert that the
returned errors are non-empty (e.g., assert!(errors.len() > 0)) or otherwise
verify the expected diagnostic appears; use the same helpers seen in the file
(parse_source and the errors variable) to locate and validate the failure.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a7464d8b-0070-4c5e-90a2-cfffc7288af2
⛔ Files ignored due to path filters (5)
baml_language/crates/baml_tests/snapshots/compiler2_mir/baml_tests__compiler2_mir__match_expr.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__06_codegen.snapis excluded by!**/*.snap
📒 Files selected for processing (1)
baml_language/crates/baml_compiler_parser/src/parser.rs
…-interpolation # Conflicts: # baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap # baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap
Formatting-only follow-up to ab05af2 (committed without running prek).
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 12
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_compiler_parser/src/parser.rs (1)
7249-7251:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRoute backtick config values through expression parsing.
looks_like_config_expression()treats quoted/raw strings as expressions but omitsTokenKind::Backtick, so a config value starting with a backtick falls through toexpect(TokenKind::Word)instead ofparse_expr().Proposed fix
// Regular string literals are always expressions - if self.at(TokenKind::Quote) { + if self.at(TokenKind::Quote) || self.at(TokenKind::Backtick) { return true; }Also applies to: 7261-7274
🤖 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_compiler_parser/src/parser.rs` around lines 7249 - 7251, The `looks_like_config_expression()` function is missing a check for `TokenKind::Backtick`, causing backtick-prefixed config values to not be routed through `parse_expr()` as intended. Update the `looks_like_config_expression()` function to include `TokenKind::Backtick` alongside the existing checks for quoted and raw strings, ensuring that backtick expressions are handled consistently with other expression types in config value parsing.
🧹 Nitpick comments (1)
baml_language/crates/baml_tests/tests/backtick_block_diagnostics.rs (1)
60-75: ⚡ Quick winAdd untagged-path assertions for the remaining structural cases.
mismatched_if_close_diagnosesandstray_endif_diagnosescurrently validate only thepromptpath, while this suite otherwise targets both prompt + untagged behavior of the shared segment builder.Suggested test additions
#[test] fn mismatched_if_close_diagnoses() { // ${if} closed by ${endfor} assert_has(&prompt("${if (true)}a${endfor}"), "${endif}"); + assert_has(&untagged("${if (true)}a${endfor}"), "${endif}"); } #[test] fn stray_endif_diagnoses() { assert_has(&prompt("a${endif}b"), "stray ${endif}"); + assert_has(&untagged("a${endif}b"), "stray ${endif}"); }🤖 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_tests/tests/backtick_block_diagnostics.rs` around lines 60 - 75, Add untagged-path assertions to the mismatched_if_close_diagnoses and stray_endif_diagnoses test functions to ensure consistency with the rest of the test suite. Both functions currently only assert on the prompt path using assert_has with prompt(), but should also include corresponding assertions using the untagged() function with the same test inputs and expected diagnostic messages, following the pattern already established in stray_endfor_diagnoses.
🤖 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_compiler_parser/src/parser.rs`:
- Around line 2198-2218: The function `has_backtick_close_ahead_from`
prematurely stops searching for closing backticks when it encounters keyword
tokens like `TokenKind::Function`, even if those keywords appear inside the
backtick literal itself (e.g., ````function keyword````). Instead of using these
token kinds as unconditional item boundaries, refine the boundary detection
logic to only return false when a keyword token is confirmed to be outside the
backtick literal being scanned. Track the nesting depth or structure of the
backtick literal to distinguish between tokens that are actual item boundaries
versus tokens that are legitimate content within the backtick delimiters, so
that keywords appearing inside multi-tick literals do not prematurely terminate
the search for the closing delimiter.
- Around line 2450-2458: The block-tag classification in the parser uses
is_basic_trivia() to skip trivia when lookahead-checking for the If token after
else, but basic trivia does not include block comments. This causes block
comments between else and if (or between if and opening braces) to break the
lookahead logic, resulting in incorrect tag classification. Replace the
is_basic_trivia() call used during the lookahead skip loops with a method that
skips all trivia including block comments (or introduce such a helper if it does
not exist), ensuring consistent trivia handling at all block-tag classification
points including the else-if check around the given location and the similar
checks mentioned to also apply at lines 2479-2493.
- Around line 2577-2593: The condition in the parenthesized template for-loop
parsing only checks for TokenKind::Let but should also accept contextual const
keywords. Update the condition that currently checks if self.at(TokenKind::Let)
to also allow contextual const (look at how the host for parser handles this
contextual keyword check), so that expressions like ${for (const x in xs)} route
to parse_for_in_pattern instead of falling through to the C-style for loop path
and erroring.
- Around line 5680-5697: The `has_newline_ahead()` function skips over block
comments without checking if they contain newline tokens inside them, causing
tagged templates to be incorrectly parsed when there is a line break within an
intervening block comment. Modify the `has_newline_ahead()` function to treat
any newline tokens found within block comments as line terminators for the
purpose of tagged template parsing, ensuring that constructs like `tag /*\n*/
`body`` are not misparsed as tagged templates.
In `@baml_language/crates/baml_compiler_syntax/src/ast.rs`:
- Around line 1709-1717: The validation for FlatPart::Else and FlatPart::ElseIf
only checks if the immediate stack frame is Open::If, but does not validate
branch ordering within an if-chain. This allows invalid sequences like multiple
${else} blocks or ${else if} after ${else} to pass validation. Enhance the
validation by tracking the last branch type seen within the current if-chain
(initial, else-if, or else) in the stack state, then add checks to reject ${else
if} when the last branch was ${else}, reject ${else} when it already appeared,
and create corresponding error kind variants and diagnostic mappings for these
new invalid-order cases.
In `@baml_language/crates/baml_compiler2_hir/src/builder.rs`:
- Around line 689-693: The if branch that handles Expr::Block only calls
walk_block_contents on the block's statements and tail expression, which skips
recording the block's ExprId itself in expr_scopes. This causes MIR/TIR scope
lookups for the synthetic body to fail. Add a call to walk_expr for the
flatten_body ExprId with push_block_scope set to false (to avoid duplicating the
scope already created by walk_block_contents) so that the block expression is
properly recorded in expr_scopes, matching the behavior of the else branch that
already calls walk_expr.
In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 4689-4691: The issue is that `tagged_body_param_names` only
resolves body params when they exist in `self.locals`, but when a nested lambda
inside `${...}` references one of these params, `lower_lambda` clears
`self.locals`, preventing the nested lambda from capturing the param since HIR
has no `BindingId` for it. Fix this by either threading the synthetic body
params through the lambda capture path so nested lambdas can access them even
after `self.locals` is cleared, or by converting these params into real scoped
bindings with proper `BindingId`s before MIR lowering so they are available in
the HIR binding system. This fix should be applied wherever
`tagged_body_param_names` is set and wherever nested lambdas are lowered to
ensure consistent param resolution across all interpolation contexts.
- Around line 6451-6455: When handling ParamBinding::OmittedDefault with a
Some(callee_loc) for a method sys-op call, the param_index is receiver-relative
(self parameter excluded) but sysop_default_operand passes it directly to
defaults.param_default which indexes the full signature including self. Adjust
param_index by adding 1 to account for the self parameter when the callee is a
method, before passing it to self.sysop_default_operand(callee_loc,
param_index). Check if the callee_loc refers to a method and only apply the
adjustment in that case.
In `@baml_language/crates/baml_compiler2_tir/src/builder.rs`:
- Around line 8145-8150: The CStyleFor case in the throws analysis for
ast::TemplateSegment::CStyleFor is missing extraction and processing of throw
facts from the init statement's initializer. Currently it only processes cond
and body, but ignores init. Extract the initializer from the init statement
using the same approach shown in lines 2947-2955 (the forward-reference walker),
and call self.collect_throw_facts_from_expr on that extracted initializer with
the appropriate body parameter before processing cond and body.
- Around line 2939-2967: The condition expression in the C-style for loop is
being processed before the loop variable's pattern is pushed onto the shadowed
list, which causes references to the loop variable in the condition to be
incorrectly treated as forward references. Move the pattern shadowing operations
earlier in the sequence: call Self::push_pattern_bindings(p, body, shadowed)
before processing the condition via
Self::collect_default_expr_forward_references(*cond, ...), so that the loop
variable binding is visible when evaluating the condition expression, while
preserving the correct order where the initializer is processed before shadowing
and the inner content is processed after.
In `@baml_language/crates/baml_compiler2_tir/src/throws_analysis.rs`:
- Around line 422-427: The CStyleFor arm in the throws analysis is missing
traversal of the init and step statements, which can throw exceptions but are
currently ignored. Modify the pattern match for ast::TemplateSegment::CStyleFor
to destructure the init and step fields (currently captured by the .. wildcard).
Add calls to collect_from_expr or collect_from_template_segments (whichever is
appropriate for the statement types) for both the init and step fields, ensuring
they are analyzed before collecting from cond and the body segments.
- Around line 384-389: The custom tag template handling in the Expr::Template
match arm collects throws from the tag expression and segments, but omits the
throws that originate from invoking the tag function itself. In the block
starting at Line 384 where Expr::Template with ast::TemplateTag::Custom is
handled, after calling collect_from_expr for the tag and
collect_from_template_segments for the segments, add a call to collect the
throws that result from calling the tag function (similar to how function call
throws would be collected elsewhere in the codebase). This ensures that escaping
throws from tagged template invocations are fully reported.
---
Outside diff comments:
In `@baml_language/crates/baml_compiler_parser/src/parser.rs`:
- Around line 7249-7251: The `looks_like_config_expression()` function is
missing a check for `TokenKind::Backtick`, causing backtick-prefixed config
values to not be routed through `parse_expr()` as intended. Update the
`looks_like_config_expression()` function to include `TokenKind::Backtick`
alongside the existing checks for quoted and raw strings, ensuring that backtick
expressions are handled consistently with other expression types in config value
parsing.
---
Nitpick comments:
In `@baml_language/crates/baml_tests/tests/backtick_block_diagnostics.rs`:
- Around line 60-75: Add untagged-path assertions to the
mismatched_if_close_diagnoses and stray_endif_diagnoses test functions to ensure
consistency with the rest of the test suite. Both functions currently only
assert on the prompt path using assert_has with prompt(), but should also
include corresponding assertions using the untagged() function with the same
test inputs and expected diagnostic messages, following the pattern already
established in stray_endfor_diagnoses.
🪄 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: cf8929ab-afd2-4b73-a967-7663aa0a95f3
⛔ Files ignored due to path filters (108)
baml_language/Cargo.lockis excluded by!**/*.lockbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_namespace_llm.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_alias_string.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_builtin_item_by_definition.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_builtin_string.snapis excluded by!**/*.snapbaml_language/crates/baml_lsp2_actions/src/snapshots/baml_lsp2_actions__describe_tests__describe_builtin_string_with_compiler2_visible_files.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/baml_src/type_reflection.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/banned_expressions/baml_tests__broken_syntax__banned_expressions__01_lexer__banned_expressions.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/error_cases/baml_tests__broken_syntax__error_cases__01_lexer__syntax_errors.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/is_in_type_position/baml_tests__broken_syntax__is_in_type_position__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/mismatched_brackets/baml_tests__broken_syntax__mismatched_brackets__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/namespaces_same_name/baml_tests__broken_syntax__namespaces_same_name__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/parser_error_recovery/baml_tests__broken_syntax__parser_error_recovery__01_lexer__partial_input.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/broken_syntax/type_annotation_errors/baml_tests__broken_syntax__type_annotation_errors__01_lexer__type_malformed.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__assert_std__/baml_tests__compiles____assert_std____01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__testing_std__/baml_tests__compiles____testing_std____01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__02_parser__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/backtick_strings/baml_tests__compiles__backtick_strings__10_formatter__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/bigint_literal/baml_tests__compiles__bigint_literal__01_lexer__bigint_literal.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/comment_in_type/baml_tests__compiles__comment_in_type__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/comment_in_type/baml_tests__compiles__comment_in_type__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/deep_method_call/baml_tests__compiles__deep_method_call__01_lexer__deep_method_call.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/host_callable_call/baml_tests__compiles__host_callable_call__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/is_operator/baml_tests__compiles__is_operator__01_lexer__is_operator.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_alias_basic/baml_tests__compiles__json_alias_basic__01_lexer__json_alias_basic.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_cross_namespace_static_call/baml_tests__compiles__json_cross_namespace_static_call__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_cross_namespace_static_call/baml_tests__compiles__json_cross_namespace_static_call__01_lexer__ns_pkg_ns_inner_box.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_llm_return_type/baml_tests__compiles__json_llm_return_type__01_lexer__json_llm_return_type.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_llm_return_type/baml_tests__compiles__json_llm_return_type__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_llm_return_type/baml_tests__compiles__json_llm_return_type__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_map_literal/baml_tests__compiles__json_map_literal__01_lexer__json_map_literal.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_to_from_string_composite_generic/baml_tests__compiles__json_to_from_string_composite_generic__01_lexer__json_to_from_string_composite_generic.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_to_from_string_concrete/baml_tests__compiles__json_to_from_string_concrete__01_lexer__json_to_from_string_concrete.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_to_from_string_generic_forwarding/baml_tests__compiles__json_to_from_string_generic_forwarding__01_lexer__json_to_from_string_generic_forwarding.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_to_from_string_three_level/baml_tests__compiles__json_to_from_string_three_level__01_lexer__json_to_from_string_three_level.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/lambda_fat_arrow/baml_tests__compiles__lambda_fat_arrow__01_lexer__lambda_fat_arrow.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/lambda_field_access/baml_tests__compiles__lambda_field_access__01_lexer__lambda_field_access.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/lexical_scoping/baml_tests__compiles__lexical_scoping__01_lexer__lexical_scoping.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/llm_image_outputs/baml_tests__compiles__llm_image_outputs__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/llm_image_outputs/baml_tests__compiles__llm_image_outputs__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/numeric_invariance_ok/baml_tests__compiles__numeric_invariance_ok__01_lexer__numeric_invariance_ok.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/optional_function_parameters/baml_tests__compiles__optional_function_parameters__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/optional_function_parameters/baml_tests__compiles__optional_function_parameters__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/patterns_new/baml_tests__compiles__patterns_new__01_lexer__patterns_new.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/reflect_type_of_user_generic/baml_tests__compiles__reflect_type_of_user_generic__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/stream_llm_inferred_typeargs/baml_tests__compiles__stream_llm_inferred_typeargs__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/stream_llm_inferred_typeargs/baml_tests__compiles__stream_llm_inferred_typeargs__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/stream_llm_inferred_typeargs/baml_tests__compiles__stream_llm_inferred_typeargs__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_annotation/baml_tests__compiles__type_annotation__01_lexer__type_alias_interaction.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_annotation/baml_tests__compiles__type_annotation__01_lexer__type_as_identifier.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_annotation/baml_tests__compiles__type_annotation__01_lexer__type_positions.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_errors/baml_tests__compiles__type_builder_errors__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_errors/baml_tests__compiles__type_builder_errors__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_test/baml_tests__compiles__type_builder_test__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_test/baml_tests__compiles__type_builder_test__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/assignment_expr_position/baml_tests__diagnostic_errors__assignment_expr_position__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__02_parser__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/backtick_strict_types/baml_tests__diagnostic_errors__backtick_strict_types__10_formatter__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/captured_field_chain/baml_tests__diagnostic_errors__captured_field_chain__01_lexer__captured_field_chain.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_return_type_mismatch/baml_tests__diagnostic_errors__catch_return_type_mismatch__01_lexer__catch_return_type_mismatch.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/catch_throw_regressions/baml_tests__diagnostic_errors__catch_throw_regressions__01_lexer__regressions.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__02_parser__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/control_flow/baml_tests__diagnostic_errors__control_flow__10_formatter__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/format_checks/baml_tests__diagnostic_errors__format_checks__01_lexer__match_exprs.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generic_call_ambiguity/baml_tests__diagnostic_errors__generic_call_ambiguity__01_lexer__array_compare.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generic_call_ambiguity/baml_tests__diagnostic_errors__generic_call_ambiguity__01_lexer__generic_call.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/generics/baml_tests__diagnostic_errors__generics__01_lexer__classes.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/instanceof_removed/baml_tests__diagnostic_errors__instanceof_removed__01_lexer__instanceof_removed.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/is_pattern_negative/baml_tests__diagnostic_errors__is_pattern_negative__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/json_static_method_arity/baml_tests__diagnostic_errors__json_static_method_arity__01_lexer__json_static_method_arity.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/match_exhaustiveness/baml_tests__diagnostic_errors__match_exhaustiveness__01_lexer__match_exhaustiveness.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/null_handling/baml_tests__diagnostic_errors__null_handling__04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/null_handling/baml_tests__diagnostic_errors__null_handling__05_diagnostics.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/numeric_invariance/baml_tests__diagnostic_errors__numeric_invariance__01_lexer__numeric_invariance.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/patterns_new/baml_tests__diagnostic_errors__patterns_new__01_lexer__patterns_new.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/runtime_id_misuse/baml_tests__diagnostic_errors__runtime_id_misuse__01_lexer__runtime_id.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/stream_types/baml_tests__diagnostic_errors__stream_types__01_lexer__builtin_refs.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/test_with_runner_ambiguity/baml_tests__diagnostic_errors__test_with_runner_ambiguity__01_lexer__main.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/type_reflection_strict/baml_tests__diagnostic_errors__type_reflection_strict__01_lexer__reflect_intrinsic_errors.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/type_reflection_strict/baml_tests__diagnostic_errors__type_reflection_strict__01_lexer__strict_normalization.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/diagnostic_errors/unknown_type_error/baml_tests__diagnostic_errors__unknown_type_error__01_lexer__unknown_type_error.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase3a__optional_param_default_forward_reference_checks_lambda_bodies.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snapis excluded by!**/*.snapbaml_language/sdks/nodejs/bridge_nodejs/dist/proto/baml_cffi.jsis excluded by!**/dist/**
📒 Files selected for processing (61)
baml_language/crates/baml_base/src/dedent.rsbaml_language/crates/baml_base/src/escape.rsbaml_language/crates/baml_base/src/lib.rsbaml_language/crates/baml_builtins2/baml_std/baml/bool.bamlbaml_language/crates/baml_builtins2/baml_std/baml/core.bamlbaml_language/crates/baml_builtins2/baml_std/baml/float.bamlbaml_language/crates/baml_builtins2/baml_std/baml/int.bamlbaml_language/crates/baml_builtins2/baml_std/baml/ns_llm/llm.bamlbaml_language/crates/baml_builtins2/baml_std/baml/ns_llm/llm_types.bamlbaml_language/crates/baml_builtins2/baml_std/baml/string.bamlbaml_language/crates/baml_compiler2_ast/src/ast.rsbaml_language/crates/baml_compiler2_ast/src/auto_derive_json.rsbaml_language/crates/baml_compiler2_ast/src/companions.rsbaml_language/crates/baml_compiler2_ast/src/docstring.rsbaml_language/crates/baml_compiler2_ast/src/lib.rsbaml_language/crates/baml_compiler2_ast/src/lower_cst.rsbaml_language/crates/baml_compiler2_ast/src/lower_expr_body.rsbaml_language/crates/baml_compiler2_ast/src/lower_type_expr.rsbaml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rsbaml_language/crates/baml_compiler2_emit/src/lib.rsbaml_language/crates/baml_compiler2_hir/src/builder.rsbaml_language/crates/baml_compiler2_hir/src/item_tree.rsbaml_language/crates/baml_compiler2_hir/src/scope.rsbaml_language/crates/baml_compiler2_mir/src/lower.rsbaml_language/crates/baml_compiler2_ppir/src/lib.rsbaml_language/crates/baml_compiler2_tir/src/builder.rsbaml_language/crates/baml_compiler2_tir/src/infer_context.rsbaml_language/crates/baml_compiler2_tir/src/inference.rsbaml_language/crates/baml_compiler2_tir/src/normalize.rsbaml_language/crates/baml_compiler2_tir/src/throws_analysis.rsbaml_language/crates/baml_compiler2_visualization/src/control_flow/from_ast.rsbaml_language/crates/baml_compiler_lexer/src/tokens.rsbaml_language/crates/baml_compiler_parser/src/parser.rsbaml_language/crates/baml_compiler_syntax/Cargo.tomlbaml_language/crates/baml_compiler_syntax/src/ast.rsbaml_language/crates/baml_compiler_syntax/src/syntax_kind.rsbaml_language/crates/baml_fmt/src/ast/expressions.rsbaml_language/crates/baml_fmt/src/ast/tokens.rsbaml_language/crates/baml_fmt/src/lib.rsbaml_language/crates/baml_lsp2_actions/src/check.rsbaml_language/crates/baml_lsp2_actions/src/completions.rsbaml_language/crates/baml_tests/projects/compiles/backtick_strings/main.bamlbaml_language/crates/baml_tests/projects/diagnostic_errors/backtick_strict_types/main.bamlbaml_language/crates/baml_tests/projects/diagnostic_errors/control_flow/main.bamlbaml_language/crates/baml_tests/src/compiler2_tir/mod.rsbaml_language/crates/baml_tests/src/compiler2_tir/phase3a.rsbaml_language/crates/baml_tests/tests/backtick_block_diagnostics.rsbaml_language/crates/baml_tests/tests/prompt_tag_e2e.rsbaml_language/crates/baml_tests/tests/prompt_tag_runtime.rsbaml_language/crates/baml_tests/tests/tagged_template_lowering.rsbaml_language/crates/baml_tests/tests/tagged_template_runtime.rsbaml_language/crates/bex_engine/tests/backtick_strings.rsbaml_language/crates/bex_vm/src/package_baml/float.rsbaml_language/crates/bex_vm/src/package_baml/int.rsbaml_language/crates/bex_vm/src/package_baml/primitives.rsbaml_language/crates/bex_vm/src/package_baml/string.rsbaml_language/crates/sys_llm/src/lib.rsbaml_language/crates/sys_ops/src/lib.rsbaml_language/crates/tools_onionskin/src/app.rsbaml_language/crates/tools_onionskin/src/compiler.rsbaml_language/sdks/nodejs/bridge_nodejs/typescript_src/proto/baml_cffi.js
`to_string` is now a magic method (BEP-049 §11): every user/stdlib class
gets a synthesized `function to_string(self) -> string throws never` whose
body delegates to `baml.unstable.string(self)`, exactly mirroring the
existing `to_json`/`from_json` auto-derive. A class that hand-writes its
own `to_string` keeps it (the synth is skipped when one already exists).
This makes any class instance interpolatable in `${…}` without a
hand-written `to_string`; the §11 "has no to_string method" type error no
longer applies to classes. The nullable-interpolation errors (`int | null`
"may be null") are unchanged — those remain a strict-typing diagnostic.
- auto_derive_json.rs: rename maybe_synthesize_json_methods ->
maybe_synthesize_derived_methods; add has_to_string guard +
synthesize_to_string(span).
- backtick_strict_types fixture: drop the FooNoToString case (B), which is
no longer an error; keep nullable cases (A) + (E).
- backtick_strings.rs: positive runtime test — interpolating a class with
no hand-written to_string renders via the universal formatter.
- Regenerate ~229 snapshots (every class method-list gains to_string;
bytecode registry indices renumber accordingly).
Fix 9 unresolved review findings on the string-interpolation work.
Throws analysis (TIR):
- Collect throws from a tagged template's tag invocation — the tag fn is
called, so its declared `throws` escape like a direct call's.
- Walk C-style `${for}` `init`/`step` statements for throws (both the
throws_analysis segment walker and builder's throw-fact collector).
- Reorder C-style `${for}` forward-ref collection so `cond`/body are walked
after the loop var is shadowed (a later param sharing the name must not
flag them as forward references).
HIR:
- Record the tagged-template flatten block's own ExprId in `expr_scopes`
(walk_expr with push_block_scope=false) so MIR/TIR scope lookups for the
synthetic body resolve; the hand-inlined walk skipped that.
Parser:
- Empty-multi-tick guard: only treat an item keyword as a boundary when it
begins a line, so a keyword in multi-tick content (``function kw``) no
longer false-flags an "empty multi-tick" error.
- Skip comments in `${else}`/`${else if}` classification and in
classify_backtick_if_is_block_tag, so an interposed comment (or a brace
inside one) no longer misclassifies the tag.
- Accept contextual `const` in parenthesized template `for` headers.
- Treat a block comment's interior newline as a line terminator in
has_newline_ahead, so `tag /*\n*/ `x`` is not parsed as a tagged template.
Syntax:
- Detect out-of-order `${if}` branches: a duplicate `${else}` or an
`${else if}` after `${else}` now diagnose (new DuplicateElse /
ElseIfAfterElse kinds + diagnostic mappings).
Regression tests: multi-tick-keyword content, const for-header, and the two
out-of-order-else cases.
A real lambda nested inside a tagged-template interpolation may reference the
tag's body-lambda parameter (e.g. `fmt`Hi ${((u) -> { name })("z")}``). That
param is a MIR-only local with no HIR binding — the tag resolves only at TIR
time — so neither TIR's standalone scope inference nor MIR's HIR-driven
capture analysis could see it through the normal paths. It failed to compile
with `[E0003] unresolved name` (and would have hit an unresolved-local panic
in MIR).
Fix spans three layers:
- TIR builder: when typing a Custom tagged template, record the tag's
body-lambda params keyed by the synthetic template-body Lambda scope
(`template_body_params`, bubbling up to the owning Function/Let scope like
`nested_lambda_types`).
- TIR scope inference: a nested lambda's standalone `ScopeKind::Lambda`
inference seeds those params (`seed_template_body_params`) from any
`is_template_body` ancestor, so they type concretely instead of `Unknown`
(which MIR forbids past its runtime-lowering boundary).
- MIR: `tagged_body_param_names` (a name set) becomes
`tagged_body_param_bindings` (name → synthetic `BindingId::parameter`). A
param referenced from a nested lambda — absent from the closure's own
locals — is captured transitively by that BindingId via the existing
capture-forwarding path (`ensure_transitive_capture`).
Adds a runtime regression test (was `[E0003]` before, renders "Hi World!" now).
There was a problem hiding this comment.
Actionable comments posted: 1
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_mir/src/lower.rs (1)
5638-5682:⚠️ Potential issue | 🟠 Major | ⚡ Quick winResolve tagged-body params before falling back to HIR locals/captures.
The
ResolvedName::Unknownbranch only works when no outer binding has the same name. Because body params are MIR-only,resolve_name_at_in_scopecan still return an enclosingResolvedName::Localfor a same-named outer variable, so a nested interpolation lambda captures the outer local instead of the tag body param. The same gap affects multi-segment roots likectx.name/ctx.method()because the root path logic never consultstagged_body_param_bindings.Centralize a helper that resolves a tagged-body param to
Place::LocalorPlace::Capture(self.ensure_transitive_capture(...)), and use it before HIR-capture fallback for single-segment and multi-segment roots.Suggested shape
+ fn tagged_body_param_place(&mut self, name: &Name) -> Option<Place> { + let binding_id = *self.tagged_body_param_bindings.get(name)?; + if let Some(&local) = self.locals.get(name) { + Some(Place::Local(local)) + } else { + Some(Place::Capture(self.ensure_transitive_capture(binding_id))) + } + }Use this helper before
capture_index_for_name_at(...)in the single-name path branch, and as another root case inlower_multi_segment_path_as_field_chain.Also applies to: 5713-5741
🤖 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_mir/src/lower.rs` around lines 5638 - 5682, The current implementation checks for tagged-body params only in the ResolvedName::Unknown branch, but this misses cases where resolve_name_at_in_scope returns a ResolvedName::Local for an outer binding with the same name, causing nested interpolation lambdas to capture the wrong variable. Create a centralized helper method that checks tagged_body_param_bindings and returns the appropriate Place (either Place::Local or Place::Capture with ensure_transitive_capture), then call this helper in the ResolvedName::Local branch before falling back to capture_index_for_name_at, and also apply the same pattern to multi-segment root path resolution in lower_multi_segment_path_as_field_chain to ensure tagged-body params are prioritized over HIR-based captures for both single-segment and multi-segment roots.
🤖 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_tir/src/inference.rs`:
- Around line 1061-1086: The doc comment describing "Search for a `Lambda`
expression..." currently precedes the `seed_template_body_params` function, but
it was originally intended to document the `find_lambda_by_span` function. In
Rust, doc comments attach to the immediately following item, so this comment is
now incorrectly documenting `seed_template_body_params` and leaving
`find_lambda_by_span` undocumented. Move the doc comment that currently appears
above `seed_template_body_params` to immediately precede the
`find_lambda_by_span` function instead.
---
Outside diff comments:
In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 5638-5682: The current implementation checks for tagged-body
params only in the ResolvedName::Unknown branch, but this misses cases where
resolve_name_at_in_scope returns a ResolvedName::Local for an outer binding with
the same name, causing nested interpolation lambdas to capture the wrong
variable. Create a centralized helper method that checks
tagged_body_param_bindings and returns the appropriate Place (either
Place::Local or Place::Capture with ensure_transitive_capture), then call this
helper in the ResolvedName::Local branch before falling back to
capture_index_for_name_at, and also apply the same pattern to multi-segment root
path resolution in lower_multi_segment_path_as_field_chain to ensure tagged-body
params are prioritized over HIR-based captures for both single-segment and
multi-segment roots.
🪄 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: 6441f242-8139-48c3-81be-b05e156ea55d
📒 Files selected for processing (11)
baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rsbaml_language/crates/baml_compiler2_hir/src/builder.rsbaml_language/crates/baml_compiler2_mir/src/lower.rsbaml_language/crates/baml_compiler2_tir/src/builder.rsbaml_language/crates/baml_compiler2_tir/src/inference.rsbaml_language/crates/baml_compiler2_tir/src/throws_analysis.rsbaml_language/crates/baml_compiler_parser/src/parser.rsbaml_language/crates/baml_compiler_syntax/src/ast.rsbaml_language/crates/baml_fmt/src/ast/expressions.rsbaml_language/crates/baml_fmt/src/lib.rsbaml_language/crates/baml_lsp2_actions/src/check.rs
🚧 Files skipped from review as they are similar to previous changes (8)
- baml_language/crates/baml_fmt/src/lib.rs
- baml_language/crates/baml_lsp2_actions/src/check.rs
- baml_language/crates/baml_compiler2_hir/src/builder.rs
- baml_language/crates/baml_compiler2_ast/src/lowering_diagnostic.rs
- baml_language/crates/baml_compiler2_tir/src/throws_analysis.rs
- baml_language/crates/baml_fmt/src/ast/expressions.rs
- baml_language/crates/baml_compiler_syntax/src/ast.rs
- baml_language/crates/baml_compiler_parser/src/parser.rs
Two remaining CodeRabbit threads on PR #3577. **Off-by-one in method sys-op omitted defaults (lower.rs).** When a `$rust_io_function` sys-op is called as a method (e.g. `ctx.output_format_with(...)`), the call plan's `param_index` is receiver-relative — TIR strips `self` via `skip_self_param` for method-convention calls — but `sysop_default_operand` fed it straight to `function_parameter_defaults.param_default(param_index)`, which is indexed self-inclusive (`self` at 0). Omitting an *earlier* optional arg while providing a *later* one (e.g. `ctx.output_format_with(quote_class_fields = true)`, omitting `prefix`) made the omitted `prefix` read `self`'s absent default → `OmittedArg` → engine panic ("Cannot convert omitted argument sentinel"). Previously masked because the only reachable case (`output_format_with`) has all-`null` defaults and the existing test passed `prefix` first. Fix: shift omitted-default indices by one when the sys-op callee uses method convention (new `callee_uses_method_convention`, mirroring TIR's `callee_uses_method_call_convention`). Free-function sys-ops have no `self`, so no shift. Adds a runtime regression test. **Doc nit (inference.rs).** Moved `find_lambda_by_span`'s doc comment back onto it — inserting `seed_template_body_params` had stranded it.
…olation # Conflicts: # baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_alias_string.snap # baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_builtin_item_by_definition.snap # baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_builtin_string.snap # baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snap # baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snap # baml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snap # baml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snap # baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snap # baml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snap
There was a problem hiding this comment.
Actionable comments posted: 1
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_mir/src/lower.rs (1)
6327-6383:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReuse the null-checked base operand for
?.[].Line 6328 lowers
basefor the null guard, but Line 6383 lowers it again for the actual access. A side-effectful base can pass the guard on the first evaluation and then index a different value on the second. Materialize the checked base once and pass it into the access helper.🐛 Proposed fix
- fn lower_optional_index(&mut self, base: AstExprId, index: AstExprId, dest: Place) { - let base_op = self.lower_to_operand(base); + fn lower_optional_index(&mut self, base: AstExprId, index: AstExprId, dest: Place) { + let base_ty = self.expr_ty(base); + let base_op = self.lower_to_operand(base); + let base_local = self.operand_to_local(base_op, base_ty.clone()); let is_null = Rvalue::BinaryOp { op: BinOp::Eq, - left: base_op, + left: Operand::Copy(Place::Local(base_local)), right: Operand::Constant(Constant::Null), }; @@ self.builder.set_current_block(bb_access); - self.lower_optional_index_access(base, index, dest, bb_null); + self.lower_optional_index_access( + Operand::Copy(Place::Local(base_local)), + base_ty, + index, + dest, + bb_null, + ); } else { @@ self.builder.set_current_block(bb_access); - self.lower_optional_index_access(base, index, dest.clone(), bb_null); + self.lower_optional_index_access( + Operand::Copy(Place::Local(base_local)), + base_ty, + index, + dest.clone(), + bb_null, + ); @@ fn lower_optional_index_access( &mut self, - base: AstExprId, + base_op: Operand, + base_ty: RuntimeTy, index: AstExprId, dest: Place, bb_null: BlockId, ) { - let base_ty = self.expr_ty(base); - let base_op = self.lower_to_operand(base); let index_ty = self.expr_ty(index);🤖 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_mir/src/lower.rs` around lines 6327 - 6383, The `lower_optional_index` function lowers the base expression once to check for null (line 6328), but then passes the original AstExprId to `lower_optional_index_access` which lowers it again (line 6383). This causes side-effectful bases to be evaluated twice, potentially yielding different values. Fix this by modifying the `lower_optional_index_access` method signature to accept the already-lowered Operand instead of the AstExprId, then pass the base_op operand from `lower_optional_index` and remove the duplicate lowering inside `lower_optional_index_access`.
🤖 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_tir/src/builder.rs`:
- Around line 4703-4755: The check_index_key_type method conditionally strips
null from index types when allow_null is true (lines 4722-4726), assuming
indices in optional chains are non-null in the narrowed context, but no
narrowing analysis is actually performed on index expressions. This allows null
indices to pass compile-time checks and fail at runtime. Either remove the
allow_null conditional logic entirely and always validate the index_ty directly
against the expected_key type without stripping null, or implement actual
narrowing analysis for indices in optional indexing chains (in
infer_optional_index_expr or similar) to guarantee non-null indices before
passing allow_null as true to check_index_key_type.
---
Outside diff comments:
In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 6327-6383: The `lower_optional_index` function lowers the base
expression once to check for null (line 6328), but then passes the original
AstExprId to `lower_optional_index_access` which lowers it again (line 6383).
This causes side-effectful bases to be evaluated twice, potentially yielding
different values. Fix this by modifying the `lower_optional_index_access` method
signature to accept the already-lowered Operand instead of the AstExprId, then
pass the base_op operand from `lower_optional_index` and remove the duplicate
lowering inside `lower_optional_index_access`.
🪄 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: a9e79bcf-2a9a-4e37-b1a8-4d0eb50ce794
⛔ Files ignored due to path filters (9)
baml_language/Cargo.lockis excluded by!**/*.lockbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_alias_string.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_builtin_item_by_definition.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_builtin_string.snapis excluded by!**/*.snapbaml_language/crates/baml_lsp2_actions/src/snapshots/baml_lsp2_actions__describe_tests__describe_builtin_string_with_compiler2_visible_files.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snapis excluded by!**/*.snap
📒 Files selected for processing (4)
baml_language/crates/baml_builtins2/baml_std/baml/string.bamlbaml_language/crates/baml_compiler2_mir/src/lower.rsbaml_language/crates/baml_compiler2_tir/src/builder.rsbaml_language/crates/baml_compiler2_tir/src/inference.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- baml_language/crates/baml_builtins2/baml_std/baml/string.baml
- baml_language/crates/baml_compiler2_tir/src/inference.rs
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 1
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_mir/src/lower.rs (1)
6327-6383:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReuse the null-checked base operand for
?.[].Line 6328 lowers
basefor the null guard, but Line 6383 lowers it again for the actual access. A side-effectful base can pass the guard on the first evaluation and then index a different value on the second. Materialize the checked base once and pass it into the access helper.🐛 Proposed fix
- fn lower_optional_index(&mut self, base: AstExprId, index: AstExprId, dest: Place) { - let base_op = self.lower_to_operand(base); + fn lower_optional_index(&mut self, base: AstExprId, index: AstExprId, dest: Place) { + let base_ty = self.expr_ty(base); + let base_op = self.lower_to_operand(base); + let base_local = self.operand_to_local(base_op, base_ty.clone()); let is_null = Rvalue::BinaryOp { op: BinOp::Eq, - left: base_op, + left: Operand::Copy(Place::Local(base_local)), right: Operand::Constant(Constant::Null), }; @@ self.builder.set_current_block(bb_access); - self.lower_optional_index_access(base, index, dest, bb_null); + self.lower_optional_index_access( + Operand::Copy(Place::Local(base_local)), + base_ty, + index, + dest, + bb_null, + ); } else { @@ self.builder.set_current_block(bb_access); - self.lower_optional_index_access(base, index, dest.clone(), bb_null); + self.lower_optional_index_access( + Operand::Copy(Place::Local(base_local)), + base_ty, + index, + dest.clone(), + bb_null, + ); @@ fn lower_optional_index_access( &mut self, - base: AstExprId, + base_op: Operand, + base_ty: RuntimeTy, index: AstExprId, dest: Place, bb_null: BlockId, ) { - let base_ty = self.expr_ty(base); - let base_op = self.lower_to_operand(base); let index_ty = self.expr_ty(index);🤖 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_mir/src/lower.rs` around lines 6327 - 6383, The `lower_optional_index` function lowers the base expression once to check for null (line 6328), but then passes the original AstExprId to `lower_optional_index_access` which lowers it again (line 6383). This causes side-effectful bases to be evaluated twice, potentially yielding different values. Fix this by modifying the `lower_optional_index_access` method signature to accept the already-lowered Operand instead of the AstExprId, then pass the base_op operand from `lower_optional_index` and remove the duplicate lowering inside `lower_optional_index_access`.
🤖 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_tir/src/builder.rs`:
- Around line 4703-4755: The check_index_key_type method conditionally strips
null from index types when allow_null is true (lines 4722-4726), assuming
indices in optional chains are non-null in the narrowed context, but no
narrowing analysis is actually performed on index expressions. This allows null
indices to pass compile-time checks and fail at runtime. Either remove the
allow_null conditional logic entirely and always validate the index_ty directly
against the expected_key type without stripping null, or implement actual
narrowing analysis for indices in optional indexing chains (in
infer_optional_index_expr or similar) to guarantee non-null indices before
passing allow_null as true to check_index_key_type.
---
Outside diff comments:
In `@baml_language/crates/baml_compiler2_mir/src/lower.rs`:
- Around line 6327-6383: The `lower_optional_index` function lowers the base
expression once to check for null (line 6328), but then passes the original
AstExprId to `lower_optional_index_access` which lowers it again (line 6383).
This causes side-effectful bases to be evaluated twice, potentially yielding
different values. Fix this by modifying the `lower_optional_index_access` method
signature to accept the already-lowered Operand instead of the AstExprId, then
pass the base_op operand from `lower_optional_index` and remove the duplicate
lowering inside `lower_optional_index_access`.
🪄 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: a9e79bcf-2a9a-4e37-b1a8-4d0eb50ce794
⛔ Files ignored due to path filters (9)
baml_language/Cargo.lockis excluded by!**/*.lockbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_alias_string.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_builtin_item_by_definition.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_describe_builtin_string.snapis excluded by!**/*.snapbaml_language/crates/baml_lsp2_actions/src/snapshots/baml_lsp2_actions__describe_tests__describe_builtin_string_with_compiler2_visible_files.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snapis excluded by!**/*.snap
📒 Files selected for processing (4)
baml_language/crates/baml_builtins2/baml_std/baml/string.bamlbaml_language/crates/baml_compiler2_mir/src/lower.rsbaml_language/crates/baml_compiler2_tir/src/builder.rsbaml_language/crates/baml_compiler2_tir/src/inference.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- baml_language/crates/baml_builtins2/baml_std/baml/string.baml
- baml_language/crates/baml_compiler2_tir/src/inference.rs
🛑 Comments failed to post (1)
baml_language/crates/baml_compiler2_tir/src/builder.rs (1)
4703-4755:
⚠️ Potential issue | 🟠 Major🧩 Analysis chain
🏁 Script executed:
#!/bin/bash # Description: Search for narrowing/flow-analysis that makes optional-indexing indices non-null. # Check for narrowing logic that might guarantee non-null indices in optional contexts. rg -nP -C5 'allow_null.*true|optional.*index|narrowing.*index' \ --glob 'baml_compiler2_tir/**/*.rs' \ --glob '!baml_compiler2_tir/**/builder.rs'Repository: BoundaryML/baml
Length of output: 208
🏁 Script executed:
#!/bin/bash # Explore the directory structure to locate baml_compiler2_tir fd -t d 'baml_compiler2_tir' | head -5 ls -la baml_language/crates/ | head -20Repository: BoundaryML/baml
Length of output: 1377
🏁 Script executed:
#!/bin/bash # Locate the builder.rs file and check context around lines 4703-4789 find . -name 'builder.rs' -type f | grep -i tirRepository: BoundaryML/baml
Length of output: 116
🏁 Script executed:
#!/bin/bash # Look for narrowing-related modules find . -name '*narrowing*' -type f 2>/dev/null | head -20Repository: BoundaryML/baml
Length of output: 887
🏁 Script executed:
#!/bin/bash # Check the actual call sites of check_index_key_type with allow_null parameter rg 'check_index_key_type' -A2 -B2 2>/dev/null | head -50Repository: BoundaryML/baml
Length of output: 3408
🏁 Script executed:
#!/bin/bash # Get more context around the optional indexing call with allow_null=true # Find the line number for the optional indexing call rg -n 'check_index_key_type.*true' baml_language/crates/baml_compiler2_tir/src/builder.rsRepository: BoundaryML/baml
Length of output: 145
🏁 Script executed:
#!/bin/bash # Read the section around the optional indexing call to see the full context sed -n '4770,4800p' baml_language/crates/baml_compiler2_tir/src/builder.rsRepository: BoundaryML/baml
Length of output: 1337
🏁 Script executed:
#!/bin/bash # Check what base_info contains and how index_ty is derived for optional indexing sed -n '4760,4795p' baml_language/crates/baml_compiler2_tir/src/builder.rsRepository: BoundaryML/baml
Length of output: 1547
🏁 Script executed:
#!/bin/bash # Look at the narrowing module to understand what type narrowing is available head -100 baml_language/crates/baml_compiler2_tir/src/narrowing.rsRepository: BoundaryML/baml
Length of output: 3615
🏁 Script executed:
#!/bin/bash # Check if narrowing is applied to index expressions anywhere rg -n 'extract_narrowings|apply.*narrowing' baml_language/crates/baml_compiler2_tir/src/builder.rs | head -20Repository: BoundaryML/baml
Length of output: 1259
🏁 Script executed:
#!/bin/bash # Look at how optional indexing evaluates the index and whether narrowing is applied sed -n '4760,4810p' baml_language/crates/baml_compiler2_tir/src/builder.rsRepository: BoundaryML/baml
Length of output: 2047
🏁 Script executed:
#!/bin/bash # Check the full method that contains the optional indexing logic rg -n 'fn infer_expr_optional_index' baml_language/crates/baml_compiler2_tir/src/builder.rsRepository: BoundaryML/baml
Length of output: 41
🏁 Script executed:
#!/antml:thinking bash # Find the complete optional indexing method sed -n '4760,4850p' baml_language/crates/baml_compiler2_tir/src/builder.rsRepository: BoundaryML/baml
Length of output: 3552
🏁 Script executed:
#!/bin/bash # Verify the narrowing module really doesn't apply to index expressions # by checking what contexts narrowing is actually applied to sed -n '1,150p' baml_language/crates/baml_compiler2_tir/src/narrowing.rs | grep -A 30 'extract_narrowings'Repository: BoundaryML/baml
Length of output: 829
🏁 Script executed:
#!/bin/bash # Check if there's any narrowing applied for index_ty in the optional indexing flow rg -n 'index_ty|index.*narrowing' baml_language/crates/baml_compiler2_tir/src/builder.rs | grep -A2 -B2 '47[67][0-9]'Repository: BoundaryML/baml
Length of output: 374
🏁 Script executed:
#!/bin/bash # Look at remove_null to understand what it does rg -n 'fn remove_null|pub fn remove_null' baml_language/crates/baml_compiler2_tir/src/Repository: BoundaryML/baml
Length of output: 156
🏁 Script executed:
#!/bin/bash # Check where remove_null is defined and what it does sed -n '1,50p' baml_language/crates/baml_compiler2_tir/src/narrowing.rs | tail -30 rg -A5 'pub fn remove_null' baml_language/crates/baml_compiler2_tir/src/narrowing.rsRepository: BoundaryML/baml
Length of output: 1138
🏁 Script executed:
#!/bin/bash # Check if there's any special narrowing/flow analysis for chained optional expressions like node?.children?.[node?.value] rg -B10 -A10 'optional_index|infer_expr_optional' baml_language/crates/baml_compiler2_tir/src/builder.rs | head -60Repository: BoundaryML/baml
Length of output: 1693
🏁 Script executed:
#!/antml:thinking bash # Look at the full remove_null implementation sed -n '379,410p' baml_language/crates/baml_compiler2_tir/src/narrowing.rsRepository: BoundaryML/baml
Length of output: 1148
🏁 Script executed:
#!/bash # Search for any context where the index in optional indexing receives special treatment rg -n 'a\?\.\[|optional.*index|chained.*narrowing' baml_language/crates/baml_compiler2_tir/src/builder.rsRepository: BoundaryML/baml
Length of output: 348
🏁 Script executed:
#!/bash # Check test files for optional indexing with nullable indices to understand intended behavior find baml_language/crates -name '*.baml' -o -name '*.rs' | xargs rg -l '\?\.\[' 2>/dev/null | head -5Repository: BoundaryML/baml
Length of output: 348
🏁 Script executed:
#!/bin/bash # Look at test files to understand the intended semantics cat baml_language/crates/baml_tests/src/compiler2_tir/phase3a.rs | grep -A20 'optional.*index\|OptionalIndex' | head -50Repository: BoundaryML/baml
Length of output: 656
🏁 Script executed:
#!/bin/bash # Search test files for examples with nullable indices in optional indexing rg -B5 -A5 'children.*value|optional.*chain' baml_language/crates/baml_tests/ --glob '*.baml' | head -40Repository: BoundaryML/baml
Length of output: 4330
🏁 Script executed:
#!/bin/bash # Check if there are any tests that verify the behavior with nullable indices rg -B3 -A3 'a\?\.\[.*\?' baml_language/crates/ --glob '*.rs' | head -50Repository: BoundaryML/baml
Length of output: 41
🏁 Script executed:
#!/bin/bash # Get more context around line 4717 to understand the comment better sed -n '4710,4730p' baml_language/crates/baml_compiler2_tir/src/builder.rsRepository: BoundaryML/baml
Length of output: 928
🏁 Script executed:
#!/bin/bash # Check if there are any error recovery tests or examples showing what should happen with nullable indices rg -B5 -A5 'node\?\..*node\?' baml_language/crates/baml_tests/ --glob '*.baml'Repository: BoundaryML/baml
Length of output: 4410
🏁 Script executed:
#!/bin/bash # Look for the Expr::OptionalIndex definition to understand the semantics rg -B3 -A3 'OptionalIndex' baml_language/crates/baml_compiler2_ast/src/ --glob '*.rs' | head -30Repository: BoundaryML/baml
Length of output: 2128
Remove the null-stripping logic in optional indexing or enforce narrowing to guarantee non-null indices.
The code strips null from index types in optional indexing contexts (line 4723, when
allow_null=true) with the comment claiming this avoids "false positives" because indices are "typically nullable-by-type yet non-null in the narrowed chain context" (e.g.,node?.children?.[node?.value]).However, no narrowing analysis is applied to index expressions. The narrowing module handles control flow (if/else conditions, truthiness, instanceof), not subscript indices. In
infer_optional_index_expr, the index type is inferred directly without any type narrowing, then passed tocheck_index_key_typewithallow_null=true, which unconditionally strips null.Since the runtime aborts when indexing with a null subscript (as noted in the comment), this allows code that will fail at runtime to pass compile-time type checking. The comment's assumption that the index is "typically" non-null in chained contexts is not enforced by the compiler.
Either remove the
allow_nulllogic to catch these errors at compile time, or implement narrowing analysis for indices in optional chains to guarantee non-null at the point of indexing.🤖 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_tir/src/builder.rs` around lines 4703 - 4755, The check_index_key_type method conditionally strips null from index types when allow_null is true (lines 4722-4726), assuming indices in optional chains are non-null in the narrowed context, but no narrowing analysis is actually performed on index expressions. This allows null indices to pass compile-time checks and fail at runtime. Either remove the allow_null conditional logic entirely and always validate the index_ty directly against the expected_key type without stripping null, or implement actual narrowing analysis for indices in optional indexing chains (in infer_optional_index_expr or similar) to guarantee non-null indices before passing allow_null as true to check_index_key_type.
A correctness audit against the BEP turned up four real defects; all are fixed
with regression tests (full suite green, 0 snapshot drift).
(1) Cross-site `${ let … }` ICE (BEP §4 / the `{% set %}` equivalent). An
untagged template lowered its segments as a `+`-fold of independent
`Expr::Block`s, so a `let` in one `${…}` never reached a later `${…}` — the
reference resolved to `Ty::Unknown` and panicked at MIR runtime lowering with NO
user-facing diagnostic. Lower into a single shared concat scope
(`InterpPart::{Value,Stmts}`) so a side-effect-only `${ let … }` splices its
statements in and stays visible to later segments, mirroring the `${for}`
accumulator. The fast path (no such segment) keeps the `+` fold so constant
templates still infer `Ty::Literal` (§9). Plus `retain_user_name_diagnostics`:
a genuine unresolved name in `${…}` now reports cleanly instead of ICE-ing.
(2) Inline if-expression / primitive literal-union `to_string` (BEP §4
README:205). `${if (c) { "p" } else { "q" }}` failed to type-check
(`try_resolve_member_on_ty` had no `Ty::Union` arm); fixing that exposed a
pre-existing MIR bug — a primitive literal-union has no class tag, so
`.to_string()` fell through to a class-instance map read (VM `expected map, got
string`). Add the union probe arm, and in MIR dispatch an all-same-primitive
union as one direct call on the primitive's builtin class.
(3) Void/unit tail renders "" (BEP §4 README:194). Falls out of (1): a
void-typed `${ if (false) { … } }` now renders "" instead of erroring on a
missing `to_string`.
(4) Dedent tab/space (BEP §12 Rule 2). `preprocess_template` used a byte-count
minimum (tab tied with space), so a tab-indented and a space-indented line
wrongly shared an indent. Use the longest common leading-whitespace *prefix*
(char-by-char) — mixed tabs/spaces yield strip column zero.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
baml_language/crates/baml_base/src/dedent.rs (1)
38-55: 💤 Low value
consumedandsplit_atare redundant.Both variables are incremented by exactly the same amount (
c.len_utf8()) in every iteration and will always be equal. A single variable would suffice.♻️ Suggested simplification
fn strip_leading_indent(line: &str, target_bytes: usize) -> &str { if target_bytes == 0 { return line; } - let mut consumed = 0usize; - let mut split_at = 0usize; + let mut stripped = 0usize; for c in line.chars() { if !c.is_whitespace() { break; } - consumed += c.len_utf8(); - split_at += c.len_utf8(); - if consumed >= target_bytes { + stripped += c.len_utf8(); + if stripped >= target_bytes { break; } } - &line[split_at..] + &line[stripped..] }🤖 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_base/src/dedent.rs` around lines 38 - 55, In the strip_leading_indent function, the variables consumed and split_at are redundant as they are incremented identically in each loop iteration and maintain equal values throughout. Consolidate these into a single variable that tracks the byte offset. Remove one of the variable declarations and use only one variable for both the comparison against target_bytes and for slicing the return value in the expression &line[...].
🤖 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.
Nitpick comments:
In `@baml_language/crates/baml_base/src/dedent.rs`:
- Around line 38-55: In the strip_leading_indent function, the variables
consumed and split_at are redundant as they are incremented identically in each
loop iteration and maintain equal values throughout. Consolidate these into a
single variable that tracks the byte offset. Remove one of the variable
declarations and use only one variable for both the comparison against
target_bytes and for slicing the return value in the expression &line[...].
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 545943a1-f0d4-483b-8df3-8fd18c223498
📒 Files selected for processing (5)
baml_language/crates/baml_base/src/dedent.rsbaml_language/crates/baml_compiler2_ast/src/lower_expr_body.rsbaml_language/crates/baml_compiler2_mir/src/lower.rsbaml_language/crates/baml_compiler2_tir/src/builder.rsbaml_language/crates/baml_compiler2_tir/src/infer_context.rs
🚧 Files skipped from review as they are similar to previous changes (2)
- baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
- baml_language/crates/baml_compiler2_tir/src/builder.rs
👮 Files not reviewed due to content moderation or server errors (1)
- baml_language/crates/baml_compiler2_mir/src/lower.rs
…olation
Resolves 4 post-merge test failures (no source conflicts — all in generated
snapshots / test expectations):
- numeric_invariance lexer snapshot: regenerated — backtick now lexes as a
`Backtick` token (this branch's feature) instead of `Error`.
- bytecode_display_{expanded,unoptimized}: regenerated — canary grew the
builtin function table (comparison ops / interface fixes) so call indices
shifted, plus this branch's auto-derived `to_string` methods now appear.
- phase3a {new_mode_failures_have_good_diagnostics, nested_lambda_diagnostic_
has_real_span}: updated the asserted substring from `operator \`Add\`` to
`operator \`+\`` to match canary's improved arithmetic diagnostic (#3788),
which now prints the user-facing operator symbol.
Full suite green: 6263 passed, 0 snapshot drift.
…ation String interpolation (auto-derived `to_string` per stdlib class) plus the merged canary code (comparison ops, interface fixes) grew the macOS arm64 artifacts past their ceilings: baml-cli 15.59 MB (was 15.58 MB ceiling; +12.7 KB) packed-program 12.01 MB (was 11.85 MB ceiling; +159.6 KB, +4.4% vs baseline) `packed-program` carries every stdlib class's new `to_string`, so the +508 KB is expected. Record the new measured sizes as the macOS baseline (the prior baml-cli baseline was stale-high at 18.18 MB, predating the size-down work) and raise both ceilings ~3% above per the file's convention. Other platforms are unaffected (linux/wasm comfortably under; both gates green there).
…olation Canary #3792 replaced magic/implicit `to_string` with the opt-in `baml.ToString` interface + a `string.from<T>(value)` universal renderer, and added an HIR rule forbidding a direct `to_string` method on a class. This branch's string interpolation was built on the now-removed magic to_string, so the merge adopts canary's design: - Untagged `${expr}` now elaborates to `string.from(expr)` (the universal renderer) instead of `expr.to_string()`. Removed the magic to_string auto-derive and the obsolete `to_string`-stringability TIR check (the null guard stays). `string.from` is total, so every value interpolates without the type needing a `to_string` method. - Took canary's primitive companions (Int/Float/Bool/String no longer declare a direct `to_string`) and removed the now-orphaned Rust `fn to_string` impls. - Removed the dead MIR primitive-union `to_string` dispatch (interpolation no longer emits `union.to_string()`); kept the union member-resolution arm, which is load-bearing for BEP-044 union field access. - Updated tests for the new model: a user class customises rendering via `implements baml.ToString`; a class without it renders structurally (`Point { x: 1, y: 2 }`); the tagged-template counter uses `string.from(i)`. - Regenerated snapshots (the per-class auto-derived `to_string` is gone from the rendered IR). Full suite green: 6275 passed, 0 snapshot drift.
…ompt companions The playground preview and "generate cURL" call a function's `$render_prompt` / `$build_request` / `$build_request_stream` companions. For new-mode (backtick) LLM functions these went through the Jinja-only `render_prompt`, which has no template registered for them — so they rendered an empty prompt (blank preview / cURL). Only the execute path (`call_llm_function`) carried the compiled prompt closure; the static render path never did, and no test caught it (every prior render test used a Jinja `#"..."#` prompt). Fix, reusing the machinery the execute path already has: - Stdlib: add `PrimitiveClient.render_specialized_prompt` — the single render source of truth (new-mode closure vs legacy Jinja, then specialize). `render_prompt` / `build_request` / `build_request_stream` take an optional `prompt_closure` and route through it. The orchestrator's `execute_once_oneshot` / `execute_stream` now call the same helper, so execution and the static preview/cURL can no longer diverge. - Compiler: new-mode functions stash closure-carrying companion bodies in `LlmBodyDef::companion_bodies` (built in `lower_cst` while the CST backtick is in hand, like `stream_body`); `make_llm_companion` reads them, so the companions pass the prompt closure. Legacy Jinja functions are unchanged. - Tests: `build_request.rs` now exercises the new-mode `$render_prompt` / `$build_request` companions and asserts they render the backtick prompt. Snapshots regenerated for the new stdlib functions + signatures.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
baml_language/crates/baml_compiler2_ast/src/companions.rs (1)
168-177: ⚡ Quick winAvoid silently downgrading new-mode companions to legacy synthesis.
On Line 168, a missing stashed body falls back to the legacy 3-arg call. For backtick/new-mode parents, that can hide an upstream lowering mismatch and change prompt-render behavior. Consider guarding this with an invariant check (or diagnostic) when
companion_bodiesis non-empty buttargetis missing.Suggested hardening
- let (body, source_map) = llm_companion_body(parent, target).unwrap_or_else(|| { - synthesize_llm_builtin_call( - target, - parent.name.as_str(), - ¶m_names, - client_arg_name, - Vec::new(), - parent.span, - ) - }); + let (body, source_map) = if let Some(pair) = llm_companion_body(parent, target) { + pair + } else { + if let Some(DeclarativeMeta::Llm(llm)) = &parent.declarative_meta { + debug_assert!( + llm.companion_bodies.is_empty(), + "missing stashed companion body for target `{target}` on `{}`", + parent.name + ); + } + synthesize_llm_builtin_call( + target, + parent.name.as_str(), + ¶m_names, + client_arg_name, + Vec::new(), + parent.span, + ) + };🤖 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/companions.rs` around lines 168 - 177, The unwrap_or_else call on llm_companion_body at line 168 silently falls back to synthesize_llm_builtin_call when a target is missing, which can hide upstream lowering mismatches for new-mode companions. Add a guard before this fallback to check if companion_bodies is non-empty but target is missing (indicating an unexpected state), and emit an invariant check or diagnostic in that case to prevent silently downgrading to legacy synthesis behavior rather than proceeding with the fallback.baml_language/crates/baml_builtins2/baml_std/baml/ns_llm/llm.baml (1)
91-94: ⚡ Quick winReuse
prompt_template_forto avoid branch-logic drift.The legacy/new-mode
jinja_stringmatch is duplicated in two places even thoughprompt_template_fornow centralizes this rule. Reusing it keeps execution-path behavior aligned with preview/build helpers.Suggested diff
function call_llm_function<T>(client: Client, function_name: string, args: map<string, unknown>, prompt_closure: ((Context) -> PromptAst)? = null) -> T { - // New-mode (backtick) functions have no Jinja template registered; the - // closure renders the prompt, so only look one up for the legacy path. - let jinja_string = match (prompt_closure) { - null => get_jinja_template(function_name), - _ => "", - }; + let jinja_string = prompt_template_for(function_name, prompt_closure); let context = ExecutionContext { jinja_string: jinja_string, @@ function stream_llm_function<TStream, TFinal>(client: Client, function_name: string, args: map<string, unknown>, prompt_closure: ((Context) -> PromptAst)? = null) -> Stream<TStream, TFinal> { - // New-mode (backtick) functions have no Jinja template registered; the - // closure renders the prompt, so only look one up for the legacy path. - let jinja_string = match (prompt_closure) { - null => get_jinja_template(function_name), - _ => "", - }; + let jinja_string = prompt_template_for(function_name, prompt_closure); let context = ExecutionContext { jinja_string: jinja_string,Also applies to: 115-118
🤖 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_builtins2/baml_std/baml/ns_llm/llm.baml` around lines 91 - 94, The jinja_string variable initialization uses a match statement on prompt_closure that is duplicated in two locations. Instead of maintaining this duplicate match logic in both places, refactor both occurrences (the one at lines 91-94 and the one at lines 115-118) to use the existing prompt_template_for function which already centralizes this branch logic. This will eliminate the duplication and ensure consistent behavior across all execution paths.
🤖 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.
Nitpick comments:
In `@baml_language/crates/baml_builtins2/baml_std/baml/ns_llm/llm.baml`:
- Around line 91-94: The jinja_string variable initialization uses a match
statement on prompt_closure that is duplicated in two locations. Instead of
maintaining this duplicate match logic in both places, refactor both occurrences
(the one at lines 91-94 and the one at lines 115-118) to use the existing
prompt_template_for function which already centralizes this branch logic. This
will eliminate the duplication and ensure consistent behavior across all
execution paths.
In `@baml_language/crates/baml_compiler2_ast/src/companions.rs`:
- Around line 168-177: The unwrap_or_else call on llm_companion_body at line 168
silently falls back to synthesize_llm_builtin_call when a target is missing,
which can hide upstream lowering mismatches for new-mode companions. Add a guard
before this fallback to check if companion_bodies is non-empty but target is
missing (indicating an unexpected state), and emit an invariant check or
diagnostic in that case to prevent silently downgrading to legacy synthesis
behavior rather than proceeding with the fallback.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 709a6bec-cf01-41a5-ae01-10c37f1d16d2
⛔ Files ignored due to path filters (26)
baml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_namespace_llm.snapis excluded by!**/*.snapbaml_language/crates/baml_cli/src/snapshots/baml_cli__describe_command_tests__render_builtin_package_listing.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/baml_src/type_reflection.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____03_hir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____04_tir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/__baml_std__/baml_tests__compiles____baml_std____06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/comment_in_type/baml_tests__compiles__comment_in_type__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/comment_in_type/baml_tests__compiles__comment_in_type__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_llm_return_type/baml_tests__compiles__json_llm_return_type__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/json_llm_return_type/baml_tests__compiles__json_llm_return_type__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/llm_image_outputs/baml_tests__compiles__llm_image_outputs__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/llm_image_outputs/baml_tests__compiles__llm_image_outputs__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/optional_function_parameters/baml_tests__compiles__optional_function_parameters__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/optional_function_parameters/baml_tests__compiles__optional_function_parameters__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/stream_llm_inferred_typeargs/baml_tests__compiles__stream_llm_inferred_typeargs__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/stream_llm_inferred_typeargs/baml_tests__compiles__stream_llm_inferred_typeargs__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/testset_vibes_nested/baml_tests__compiles__testset_vibes_nested__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_errors/baml_tests__compiles__type_builder_errors__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_errors/baml_tests__compiles__type_builder_errors__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_test/baml_tests__compiles__type_builder_test__04_5_mir.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/snapshots/compiles/type_builder_test/baml_tests__compiles__type_builder_test__06_codegen.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/src/compiler2_tir/snapshots/baml_tests__compiler2_tir__phase5__snapshot_baml_package_items.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded.snapis excluded by!**/*.snapbaml_language/crates/baml_tests/tests/bytecode_format/snapshots/bytecode_format__bytecode_display_expanded_unoptimized.snapis excluded by!**/*.snap
📒 Files selected for processing (6)
baml_language/crates/baml_builtins2/baml_std/baml/ns_llm/llm.bamlbaml_language/crates/baml_builtins2/baml_std/baml/ns_llm/llm_types.bamlbaml_language/crates/baml_compiler2_ast/src/ast.rsbaml_language/crates/baml_compiler2_ast/src/companions.rsbaml_language/crates/baml_compiler2_ast/src/lower_cst.rsbaml_language/crates/bex_engine/tests/build_request.rs
🚧 Files skipped from review as they are similar to previous changes (3)
- baml_language/crates/baml_compiler2_ast/src/lower_cst.rs
- baml_language/crates/baml_compiler2_ast/src/ast.rs
- baml_language/crates/baml_builtins2/baml_std/baml/ns_llm/llm_types.baml
Implements BEP-049 (String Interpolation) milestones M1 through M4.
What's in this PR
M1 — backtick string literals
`...`: newBACKTICKtoken, multi-tick delimiter ladders, escapes, multi-line auto-dedent (§12), usable in any expression position.M2 —
${expr}interpolation:${...}as a block expression (§4), implicit.to_string()for non-string values (§11), and strict typing (§7/§11) — interpolating a nullable or non-stringable value is a compile-time error.M3 — block control flow:
${for (let x in xs)}…${endfor},${if (c)}…${else if}…${else}…${endif}using host syntax, with §13 whitespace control.M4 — tagged templates (§10):
tag`...`with the//baml:tagged_stringmarker,TaggedString { parts, values }, tag resolution + signature validation, body-lambda params scoping into${…}, and full VM execution — static layouts (M4e.1a) and${for}/${if}runtime flattening (M4e.1b).Architecture: check-then-lower (clean diagnostics)
Both backtick forms are unified behind a first-class
Expr::Template { tag: Default { elaborated } | Custom { tag, body }, segments }node that survives through TIR. Untagged backticks no longer desugar early — interpolation diagnostics point at the original${…}span instead of leaking a synthetic.to_string()call. This matches how tagged templates (and TypeScript) check the structured node and lower late.Testing
Full workspace suite green. Adds unit + runtime coverage for backtick literals, interpolation, strict-type diagnostics, control flow, and tagged-template execution (static,
${for}/${if}, nested, mixed, captures, body-param injection, nested lambdas capturing loop-locals).Not in this PR
M5 (built-in
prompttag), M6 (legacy#"..."#interop + migrator), M7 (drop minijinja).🤖 Generated with Claude Code
Summary by CodeRabbit
${}interpolation, including${for}/${if}control-flow.TaggedStringtemplate metadata.