Skip to content
Open
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
45 changes: 45 additions & 0 deletions baml_language/crates/baml_compiler2_ast/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,25 @@ pub enum Expr {
base: ExprId,
field: Name,
},
/// Optional field access: `obj?.field` — short-circuits to null if base is null (BEP-020).
OptionalFieldAccess {
base: ExprId,
field: Name,
},
Index {
base: ExprId,
index: ExprId,
},
/// Optional index: `obj?.[expr]` — short-circuits to null if base is null (BEP-020).
OptionalIndex {
base: ExprId,
index: ExprId,
},
/// Optional call: `func?.(args)` — short-circuits to null if callee is null (BEP-020).
OptionalCall {
callee: ExprId,
args: Vec<ExprId>,
},
Missing,
}

Expand Down Expand Up @@ -391,6 +406,36 @@ pub enum BinaryOp {
Shl,
Shr,
Instanceof,
/// Null coalescing: `a ?? b` — returns `a` if non-null, else `b` (BEP-020).
NullCoalesce,
}

impl std::fmt::Display for BinaryOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
BinaryOp::Add => "+",
BinaryOp::Sub => "-",
BinaryOp::Mul => "*",
BinaryOp::Div => "/",
BinaryOp::Mod => "%",
BinaryOp::Eq => "==",
BinaryOp::Ne => "!=",
BinaryOp::Lt => "<",
BinaryOp::Le => "<=",
BinaryOp::Gt => ">",
BinaryOp::Ge => ">=",
BinaryOp::And => "&&",
BinaryOp::Or => "||",
BinaryOp::BitAnd => "&",
BinaryOp::BitOr => "|",
BinaryOp::BitXor => "^",
BinaryOp::Shl => "<<",
BinaryOp::Shr => ">>",
BinaryOp::Instanceof => "instanceof",
BinaryOp::NullCoalesce => "??",
};
write!(f, "{s}")
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down
154 changes: 154 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 @@ -269,8 +269,11 @@ impl LoweringContext {
}
SyntaxKind::PATH_EXPR => self.lower_path_expr(node),
SyntaxKind::FIELD_ACCESS_EXPR => self.lower_field_access_expr(node),
SyntaxKind::OPTIONAL_FIELD_ACCESS_EXPR => self.lower_optional_field_access_expr(node),
SyntaxKind::ENV_ACCESS_EXPR => self.lower_env_access_expr(node),
SyntaxKind::INDEX_EXPR => self.lower_index_expr(node),
SyntaxKind::OPTIONAL_INDEX_EXPR => self.lower_optional_index_expr(node),
SyntaxKind::OPTIONAL_CALL_EXPR => self.lower_optional_call_expr(node),
SyntaxKind::PAREN_EXPR => {
if let Some(inner) = node.children().next() {
self.lower_expr(&inner)
Expand Down Expand Up @@ -332,6 +335,13 @@ impl LoweringContext {
SyntaxKind::LESS_LESS => op = Some(BinaryOp::Shl),
SyntaxKind::GREATER_GREATER => op = Some(BinaryOp::Shr),
SyntaxKind::KW_INSTANCEOF => op = Some(BinaryOp::Instanceof),
SyntaxKind::QUESTION_QUESTION => op = Some(BinaryOp::NullCoalesce),
SyntaxKind::QUESTION if op.is_none() => {
// Two consecutive QUESTION tokens = null coalescing (??)
// The parser emits them as two separate tokens in BINARY_EXPR.
// First QUESTION sets a provisional op; second one confirms.
op = Some(BinaryOp::NullCoalesce);
}
SyntaxKind::INTEGER_LITERAL => {
let value = token.text().parse::<i64>().unwrap_or(0);
let expr_id = self.alloc_expr(Expr::Literal(Literal::Int(value)), span);
Expand Down Expand Up @@ -1439,6 +1449,147 @@ impl LoweringContext {
self.alloc_expr(Expr::Index { base, index }, node.text_range())
}

fn lower_optional_field_access_expr(&mut self, node: &SyntaxNode) -> ExprId {
// OPTIONAL_FIELD_ACCESS_EXPR: <base_expr> QUESTION_DOT WORD
let mut base = None;
let mut field = None;

for elem in node.children_with_tokens() {
match elem {
rowan::NodeOrToken::Node(child) => {
if base.is_none() {
base = Some(self.lower_expr(&child));
}
}
rowan::NodeOrToken::Token(token) => {
if token.kind() == SyntaxKind::WORD && base.is_some() {
field = Some(Name::new(token.text()));
}
}
}
}

let base = base.unwrap_or_else(|| self.alloc_expr(Expr::Missing, node.text_range()));
let field = field.unwrap_or_else(|| Name::new("_"));

self.alloc_expr(Expr::OptionalFieldAccess { base, field }, node.text_range())
Comment on lines +1452 to +1475
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

foo?.bar currently loses its base during lowering.

For simple bases, the parser hands this node to lowering as WORD QUESTION_DOT WORD with no child expression node. This implementation only fills base from child nodes, so foo?.bar ends up as Expr::OptionalFieldAccess { base: Missing, field: "_" }.

🛠️ Proposed fix
 fn lower_optional_field_access_expr(&mut self, node: &SyntaxNode) -> ExprId {
     // OPTIONAL_FIELD_ACCESS_EXPR: <base_expr> QUESTION_DOT WORD
     let mut base = None;
     let mut field = None;
+    let mut seen_question_dot = false;
 
     for elem in node.children_with_tokens() {
         match elem {
             rowan::NodeOrToken::Node(child) => {
-                if base.is_none() {
+                if !seen_question_dot && base.is_none() {
                     base = Some(self.lower_expr(&child));
                 }
             }
             rowan::NodeOrToken::Token(token) => {
-                if token.kind() == SyntaxKind::WORD && base.is_some() {
+                if token.kind() == SyntaxKind::QUESTION_DOT {
+                    seen_question_dot = true;
+                } else if !seen_question_dot && base.is_none() {
+                    base = self.try_lower_bare_token(&token);
+                } else if seen_question_dot
+                    && token.kind() == SyntaxKind::WORD
+                    && field.is_none()
+                {
                     field = Some(Name::new(token.text()));
                 }
             }
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn lower_optional_field_access_expr(&mut self, node: &SyntaxNode) -> ExprId {
// OPTIONAL_FIELD_ACCESS_EXPR: <base_expr> QUESTION_DOT WORD
let mut base = None;
let mut field = None;
for elem in node.children_with_tokens() {
match elem {
rowan::NodeOrToken::Node(child) => {
if base.is_none() {
base = Some(self.lower_expr(&child));
}
}
rowan::NodeOrToken::Token(token) => {
if token.kind() == SyntaxKind::WORD && base.is_some() {
field = Some(Name::new(token.text()));
}
}
}
}
let base = base.unwrap_or_else(|| self.alloc_expr(Expr::Missing, node.text_range()));
let field = field.unwrap_or_else(|| Name::new("_"));
self.alloc_expr(Expr::OptionalFieldAccess { base, field }, node.text_range())
fn lower_optional_field_access_expr(&mut self, node: &SyntaxNode) -> ExprId {
// OPTIONAL_FIELD_ACCESS_EXPR: <base_expr> QUESTION_DOT WORD
let mut base = None;
let mut field = None;
let mut seen_question_dot = false;
for elem in node.children_with_tokens() {
match elem {
rowan::NodeOrToken::Node(child) => {
if !seen_question_dot && base.is_none() {
base = Some(self.lower_expr(&child));
}
}
rowan::NodeOrToken::Token(token) => {
if token.kind() == SyntaxKind::QUESTION_DOT {
seen_question_dot = true;
} else if !seen_question_dot && base.is_none() {
base = self.try_lower_bare_token(&token);
} else if seen_question_dot
&& token.kind() == SyntaxKind::WORD
&& field.is_none()
{
field = Some(Name::new(token.text()));
}
}
}
}
let base = base.unwrap_or_else(|| self.alloc_expr(Expr::Missing, node.text_range()));
let field = field.unwrap_or_else(|| Name::new("_"));
self.alloc_expr(Expr::OptionalFieldAccess { base, field }, node.text_range())

}

fn lower_optional_index_expr(&mut self, node: &SyntaxNode) -> ExprId {
// OPTIONAL_INDEX_EXPR: <base_expr> QUESTION_DOT L_BRACKET <index_expr> R_BRACKET
let mut base = None;
let mut index = None;
let mut seen_lbracket = false;

for elem in node.children_with_tokens() {
match elem {
rowan::NodeOrToken::Node(child) => {
if !seen_lbracket {
if base.is_none() {
base = Some(self.lower_expr(&child));
}
} else if index.is_none() {
index = Some(self.lower_expr(&child));
}
}
rowan::NodeOrToken::Token(token) => {
if token.kind() == SyntaxKind::L_BRACKET {
seen_lbracket = true;
} else if !seen_lbracket && base.is_none() {
base = self.try_lower_bare_token(&token);
} else if seen_lbracket && index.is_none() {
index = self.try_lower_bare_token(&token);
}
}
}
}

let base = base.unwrap_or_else(|| self.alloc_expr(Expr::Missing, node.text_range()));
let index = index.unwrap_or_else(|| self.alloc_expr(Expr::Missing, node.text_range()));

self.alloc_expr(Expr::OptionalIndex { base, index }, node.text_range())
}

fn lower_optional_call_expr(&mut self, node: &SyntaxNode) -> ExprId {
// OPTIONAL_CALL_EXPR: <callee_expr> QUESTION_DOT CALL_ARGS
let callee_node = node.children().find(|n| n.kind() != SyntaxKind::CALL_ARGS);

let callee = if let Some(n) = callee_node {
self.lower_expr(&n)
} else {
let word_token = node
.children_with_tokens()
.filter_map(rowan::NodeOrToken::into_token)
.find(|t| t.kind() == SyntaxKind::WORD);

if let Some(token) = word_token {
self.alloc_expr(
Expr::Path(vec![Name::new(token.text())]),
token.text_range(),
)
} else {
self.alloc_expr(Expr::Missing, node.text_range())
}
};

let args = node
.children()
.find(|n| n.kind() == SyntaxKind::CALL_ARGS)
.map(|args_node| {
let mut args = Vec::new();
for element in args_node.children_with_tokens() {
match element {
rowan::NodeOrToken::Node(child_node) => {
if is_expr_node_kind(child_node.kind()) {
args.push(self.lower_expr(&child_node));
}
}
rowan::NodeOrToken::Token(token) => {
let span = token.text_range();
match token.kind() {
SyntaxKind::INTEGER_LITERAL => {
let value = token.text().parse::<i64>().unwrap_or(0);
args.push(
self.alloc_expr(Expr::Literal(Literal::Int(value)), span),
);
}
SyntaxKind::FLOAT_LITERAL => {
let text = token.text().to_string();
args.push(
self.alloc_expr(Expr::Literal(Literal::Float(text)), span),
);
}
SyntaxKind::STRING_LITERAL | SyntaxKind::RAW_STRING_LITERAL => {
let content = strip_string_delimiters(token.text());
args.push(
self.alloc_expr(
Expr::Literal(Literal::String(content)),
span,
),
);
}
SyntaxKind::WORD => {
let text = token.text();
let e = match text {
"true" => Expr::Literal(Literal::Bool(true)),
"false" => Expr::Literal(Literal::Bool(false)),
"null" => Expr::Null,
_ => Expr::Path(vec![Name::new(text)]),
};
args.push(self.alloc_expr(e, span));
}
_ => {}
}
}
}
}
args
})
.unwrap_or_default();

self.alloc_expr(Expr::OptionalCall { callee, args }, node.text_range())
}

fn lower_string_literal(&mut self, node: &SyntaxNode) -> ExprId {
let text = node.text().to_string();
let content = strip_string_delimiters(&text);
Expand Down Expand Up @@ -2010,8 +2161,11 @@ fn is_expr_node_kind(kind: SyntaxKind) -> bool {
| SyntaxKind::CALL_EXPR
| SyntaxKind::PATH_EXPR
| SyntaxKind::FIELD_ACCESS_EXPR
| SyntaxKind::OPTIONAL_FIELD_ACCESS_EXPR
| SyntaxKind::ENV_ACCESS_EXPR
| SyntaxKind::INDEX_EXPR
| SyntaxKind::OPTIONAL_INDEX_EXPR
| SyntaxKind::OPTIONAL_CALL_EXPR
| SyntaxKind::IF_EXPR
| SyntaxKind::MATCH_EXPR
| SyntaxKind::CATCH_EXPR
Expand Down
Loading
Loading