Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions baml_language/crates/baml_compiler2_ast/src/lower_expr_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,8 @@ impl LoweringContext {
SyntaxKind::CATCH_EXPR => self.lower_catch_expr(node),
SyntaxKind::THROW_EXPR => self.lower_throw_expr(node),
SyntaxKind::RETURN_EXPR => self.lower_return_expr(node),
SyntaxKind::BREAK_EXPR => self.lower_jump_expr(node, Stmt::Break),
SyntaxKind::CONTINUE_EXPR => self.lower_jump_expr(node, Stmt::Continue),
SyntaxKind::BLOCK_EXPR => {
if let Some(block) = baml_compiler_syntax::ast::BlockExpr::cast(node.clone()) {
self.lower_block_expr(&block)
Expand Down Expand Up @@ -4512,6 +4514,28 @@ impl LoweringContext {
self.alloc_expr(Expr::Return { value }, node.span_range())
}

/// Lower a `break`/`continue` used in expression position (`BREAK_EXPR` /
/// `CONTINUE_EXPR`, e.g. a bare match arm `0 => break`) into a block that
/// holds the corresponding jump statement: `{ break; }` / `{ continue; }`.
///
/// `break`/`continue` carry no value, so — unlike `return` — they need no
/// dedicated `Expr` variant. Desugaring to a single-statement block reuses
/// the fully-tested `Stmt::Break`/`Stmt::Continue` machinery (divergence
/// typing to `never`, defer replay/unwatch, defer-escape diagnostics) and
/// makes the braceless form behave identically to the already-accepted
/// braced arm.
fn lower_jump_expr(&mut self, node: &SyntaxNode, jump: Stmt) -> ExprId {
let span = node.span_range();
let stmt = self.alloc_stmt(jump, span);
self.alloc_expr(
Expr::Block {
stmts: vec![stmt],
tail_expr: None,
},
span,
)
}

/// Lower `defer { BODY }` (BEP-042). The CST shape is
/// `DEFER_STMT [ KW_DEFER BLOCK_EXPR ]`. Unlike `spawn`, the body is NOT a
/// lambda — it is lowered inline as an [`Expr::Block`] in the enclosing
Expand Down
77 changes: 77 additions & 0 deletions baml_language/crates/baml_compiler_parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4678,6 +4678,28 @@ impl<'a> Parser<'a> {
});
}

/// Parse `break` in expression position as a `BREAK_EXPR` — a diverging
/// expression of type `never`, mirroring `parse_return_expr`. This is what
/// lets a braceless `break` be a `catch`/`match` arm value (`0 => break`).
/// Statement position is still handled by `parse_break_stmt` (see
/// `parse_stmt`), so this only fires when `break` is reached through
/// expression parsing. Unlike the statement form, we don't eat a trailing
/// `;` here — that belongs to the enclosing statement, not the expression.
fn parse_break_expr(&mut self) {
self.with_node(SyntaxKind::BREAK_EXPR, |p| {
p.expect(TokenKind::Break);
});
}

/// Parse `continue` in expression position as a `CONTINUE_EXPR` — the
/// `continue` counterpart of `parse_break_expr`. Statement-position
/// `continue` is still handled by `parse_continue_stmt`.
fn parse_continue_expr(&mut self) {
self.with_node(SyntaxKind::CONTINUE_EXPR, |p| {
p.expect(TokenKind::Continue);
});
}

fn parse_if_expr(&mut self) {
// `if let PATTERN = SCRUTINEE { ... }` is a distinct refutable form.
// Decided here by peeking past `if` for a `let` token — patterns
Expand Down Expand Up @@ -6324,6 +6346,15 @@ impl<'a> Parser<'a> {
// `catch`/`match` arm value. Statement-position `return` is taken by
// `parse_stmt` before reaching here.
self.parse_return_expr();
} else if self.at(TokenKind::Break) {
// Break expression (diverging, type `never`) — lets `break` be a
// `catch`/`match` arm value, symmetric with `return`.
// Statement-position `break` is taken by `parse_stmt` first.
self.parse_break_expr();
} else if self.at(TokenKind::Continue) {
// Continue expression (diverging, type `never`) — the `continue`
// counterpart of the `break` case above.
self.parse_continue_expr();
} else if self.at(TokenKind::Word) {
// Collect text as owned String so the borrow is released before any &mut calls.
let text: String = self.current().map(|t| t.text.clone()).unwrap_or_default();
Expand Down Expand Up @@ -9341,6 +9372,52 @@ function Demo() -> int {
assert_eq!(throw_expr_count, 2);
}

#[test]
fn parses_break_and_continue_as_match_arm_expressions() {
// B-619: bare `break`/`continue` are valid in match-arm expression
// position (symmetric with `return`), producing BREAK_EXPR/CONTINUE_EXPR
// nodes — no `E0010 Expected expression` error.
let source = r#"
function Demo(n: int) -> int {
let x = n;
while (true) {
match (x) {
0 => break,
1 => continue,
_ => { x = x - 1; }
}
}
x
}
"#;

let (root, errors) = parse_source(source);
assert_no_errors(&errors);

let break_expr_count = root
.descendants()
.filter(|n| n.kind() == SyntaxKind::BREAK_EXPR)
.count();
assert_eq!(break_expr_count, 1, "expected one BREAK_EXPR node");

let continue_expr_count = root
.descendants()
.filter(|n| n.kind() == SyntaxKind::CONTINUE_EXPR)
.count();
assert_eq!(continue_expr_count, 1, "expected one CONTINUE_EXPR node");

// Bare `break`/`continue` in statement position still parse as the
// statement forms — the expression forms only fire through expression
// parsing (the match arms above).
assert_eq!(
root.descendants()
.filter(|n| n.kind() == SyntaxKind::BREAK_STMT)
.count(),
0,
"arm-position break should not be a BREAK_STMT"
);
}

#[test]
fn parses_catch_on_throw_expression_with_expected_binding() {
let source = r#"
Expand Down
11 changes: 11 additions & 0 deletions baml_language/crates/baml_compiler_syntax/src/syntax_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,17 @@ pub enum SyntaxKind {
/// arm value (e.g. `_ => return 0`) without the statement-only restriction.
/// Statement-position `return` still parses as `RETURN_STMT`.
RETURN_EXPR,
/// `break` in expression position — a diverging expression of type `never`
/// (mirrors `RETURN_EXPR`). Lets `break` appear as a `catch`/`match` arm
/// value (e.g. `0 => break`) without the statement-only restriction.
/// Statement-position `break` still parses as `BREAK_STMT`.
BREAK_EXPR,
/// `continue` in expression position — a diverging expression of type
/// `never` (mirrors `RETURN_EXPR`). Lets `continue` appear as a
/// `catch`/`match` arm value (e.g. `0 => continue`) without the
/// statement-only restriction. Statement-position `continue` still parses
/// as `CONTINUE_STMT`.
CONTINUE_EXPR,
/// `spawn name_expr? block` — BEP-034 spawn expression.
/// Structure: `KW_SPAWN [expr] BLOCK_EXPR`.
SPAWN_EXPR,
Expand Down
56 changes: 38 additions & 18 deletions baml_language/crates/baml_fmt/src/ast/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,22 @@ pub enum Expression {
/// `return` requires, so the output round-trips through `RETURN_STMT` (i.e. is
/// idempotent).
Return(VerbatimSpan),
/// A braceless `break` in expression position (a `BREAK_EXPR`, e.g. a
/// `catch`/`match` arm value like `0 => break`). Handled exactly like
/// [`Expression::Return`] and backed by the same [`VerbatimSpan`]: when an arm
/// printer wraps it into a block it appends the `;` that a block-position
/// `break` requires, so the output round-trips through `BREAK_STMT`.
Break(VerbatimSpan),
/// A braceless `continue` in expression position (a `CONTINUE_EXPR`). The
/// `continue` counterpart of [`Expression::Break`].
Continue(VerbatimSpan),
Unknown(VerbatimSpan),
}

/// A node the strong AST does not model and prints verbatim: an unmodeled
/// expression (e.g. `defer { … }`, `throw e`, `await f`, `spawn { … }`,
/// `x.as<T>`) held as [`Expression::Unknown`], or a braceless `return …` held as
/// [`Expression::Return`].
/// `x.as<T>`) held as [`Expression::Unknown`], or a braceless jump held as
/// [`Expression::Return`], [`Expression::Break`], or [`Expression::Continue`].
///
/// Rather than a single whole-node span, this carries the node's true first and
/// last *token* ranges. The trivia classifier keys leading/trailing comments to
Expand Down Expand Up @@ -204,6 +213,8 @@ impl FromCST for Expression {
}
SyntaxKind::LAMBDA_EXPR => Expression::Lambda(Box::new(LambdaExpr::from_cst(elem)?)),
SyntaxKind::RETURN_EXPR => Expression::Return(VerbatimSpan::from_element(&elem)),
SyntaxKind::BREAK_EXPR => Expression::Break(VerbatimSpan::from_element(&elem)),
SyntaxKind::CONTINUE_EXPR => Expression::Continue(VerbatimSpan::from_element(&elem)),
_ => Expression::Unknown(VerbatimSpan::from_element(&elem)),
};
Ok(expr)
Expand Down Expand Up @@ -253,7 +264,7 @@ impl Expression {
}
Expression::ByteString(bs) => Some(usize::from(bs.span().len())),
Expression::Lambda(_) => None,
Expression::Return(_) => None,
Expression::Return(_) | Expression::Break(_) | Expression::Continue(_) => None,
Expression::Unknown(unknown) => {
// Unmodeled nodes (e.g. `await f`, `x.as<T>`, `spawn { … }`,
// `throw e`) print their source verbatim (see `print`). When that
Expand Down Expand Up @@ -305,12 +316,13 @@ impl Printable for Expression {
Expression::BacktickString(bt) => bt.print(shape, printer),
Expression::ByteString(bs) => bs.print(shape, printer),
Expression::Lambda(lambda) => lambda.print(shape, printer),
// Print the raw `return …` text. The arm printers add the `;` when
// they wrap this into a block (see `CatchArm`/`MatchArm`). A braceless
// `return` only appears as a whole arm value, never nested inside
// another expression, so it always reports multi-lined.
Expression::Return(ret) => {
printer.print_input_range_trimmed_start(ret.content_range());
// Print the raw `return …` / `break` / `continue` text. The arm
// printers add the `;` when they wrap this into a block (see
// `CatchArm`/`MatchArm`). A braceless jump only appears as a whole
// arm value, never nested inside another expression, so it always
// reports multi-lined.
Expression::Return(jump) | Expression::Break(jump) | Expression::Continue(jump) => {
printer.print_input_range_trimmed_start(jump.content_range());
PrintInfo::default_multi_lined()
}
// Unmodeled nodes print their source verbatim. Report `multi_lined`
Expand Down Expand Up @@ -355,8 +367,10 @@ impl Printable for Expression {
Expression::BacktickString(bt) => bt.leftmost_token(),
Expression::ByteString(bs) => bs.leftmost_token(),
Expression::Lambda(lambda) => lambda.leftmost_token(),
Expression::Return(ret) => ret.first_token,
Expression::Unknown(unknown) => unknown.first_token,
Expression::Return(span)
| Expression::Break(span)
| Expression::Continue(span)
| Expression::Unknown(span) => span.first_token,
}
}
fn rightmost_token(&self) -> TextRange {
Expand Down Expand Up @@ -387,8 +401,10 @@ impl Printable for Expression {
Expression::BacktickString(bt) => bt.rightmost_token(),
Expression::ByteString(bs) => bs.rightmost_token(),
Expression::Lambda(lambda) => lambda.rightmost_token(),
Expression::Return(ret) => ret.last_token,
Expression::Unknown(unknown) => unknown.last_token,
Expression::Return(span)
| Expression::Break(span)
| Expression::Continue(span)
| Expression::Unknown(span) => span.last_token,
}
}
}
Expand Down Expand Up @@ -1830,13 +1846,17 @@ impl MatchArm {
/// newline are already emitted; the caller emits the closing `}`).
///
/// `arm_indent` is the arm's own indent; the body is printed one level deeper.
/// A braceless `return` body additionally gets its statement `;` — and its
/// trailing trivia is deliberately left for the arm level so a same-line comment
/// stays attached to the arm (emitted after the wrapped `},`) instead of being
/// split from the `;` or dropped/duplicated when the arm has no comma (B-629).
/// A braceless jump body (`return`/`break`/`continue`) additionally gets its
/// statement `;` — and its trailing trivia is deliberately left for the arm
/// level so a same-line comment stays attached to the arm (emitted after the
/// wrapped `},`) instead of being split from the `;` or dropped/duplicated when
/// the arm has no comma (B-629).
fn print_wrapped_arm_body(printer: &mut Printer, body: &Expression, arm_indent: usize) {
let inner_indent = arm_indent + printer.config.indent_width;
if matches!(body, Expression::Return(_)) {
if matches!(
body,
Expression::Return(_) | Expression::Break(_) | Expression::Continue(_)
) {
printer.print_standalone_leading_and_body(body, inner_indent);
printer.print_str(";");
} else {
Expand Down
52 changes: 52 additions & 0 deletions baml_language/crates/baml_fmt/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,58 @@ mod catch_format_tests {
}
}

#[cfg(test)]
mod match_arm_jump_format_tests {
//! B-619: a braceless `break`/`continue` match arm is wrapped into a block
//! with a trailing `;` — the same treatment `return` gets — so the output
//! round-trips through `BREAK_STMT`/`CONTINUE_STMT` and is idempotent.

use super::*;

#[test]
fn braceless_break_and_continue_arms_wrap_into_blocks() {
let source = r#"function f(n: int) -> int {
let x = n;
while (true) {
match (x) {
0 => break,
1 => continue,
_ => { x = x - 1; }
}
}
x
}
"#;
let expected = r#"function f(n: int) -> int {
let x = n;
while (true) {
match (x) {
0 => {
break;
},
1 => {
continue;
},
_ => {
x = x - 1;
},
}
}
x
}
"#;
let options = FormatOptions::default();
let formatted =
format(source, &options).expect("formatter should succeed on break/continue arms");
assert_eq!(
formatted, expected,
"formatter output didn't match expected\n--- got ---\n{formatted}\n--- want ---\n{expected}"
);
let second = format(&formatted, &options).expect("formatter should be idempotent");
assert_eq!(formatted, second, "formatter should be idempotent");
}
}

#[cfg(test)]
mod is_format_tests {
//! Formatter tests for `<expr> is <pattern>`. Each case rounds the
Expand Down
Loading
Loading