Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3134e5e
match bep update with recent discussions
rossirpaulo Dec 16, 2025
d82cfc2
bep auto generated
rossirpaulo Dec 16, 2025
084b083
fixed minor copies, prefer `T` for `Types`, chose error instead of wa…
rossirpaulo Dec 16, 2025
1cfba25
fixed conceptual mistake regarding matching values as types
rossirpaulo Dec 16, 2025
86df391
simplified comments
rossirpaulo Dec 16, 2025
3c6e100
removed repeated copy on Enum Variants Are Values, Not Types
rossirpaulo Dec 16, 2025
65cf6fe
simplified comment copy
rossirpaulo Dec 16, 2025
e7d4643
maked as accepted
rossirpaulo Dec 16, 2025
5a58334
bep auto-generated
rossirpaulo Dec 16, 2025
f8733b9
Merge remote-tracking branch 'origin/canary' into paulo/map
rossirpaulo Dec 16, 2025
0b8327c
tests snapshots
rossirpaulo Dec 17, 2025
6a856de
Implement match expression desugaring to if-else chain with instanceo…
rossirpaulo Dec 17, 2025
a808166
Add support for typed bindings in pattern matching and enhance match …
rossirpaulo Dec 17, 2025
ab7bca1
Implement match pattern compilation for various pattern types, includ…
rossirpaulo Dec 17, 2025
20a914a
Add new error codes for non-exhaustive match and unreachable arm in t…
rossirpaulo Dec 17, 2025
c9b56fd
Add handling for new TypeError variants: NonExhaustiveMatch and Unrea…
rossirpaulo Dec 17, 2025
24f3446
Add match expression support with comprehensive pattern handling, inc…
rossirpaulo Dec 17, 2025
b2664a1
Add Match and FatArrow token variants to TokenKind enum
rossirpaulo Dec 17, 2025
3f64f45
Add handling for additional TypeError variants: NonExhaustiveMatch an…
rossirpaulo Dec 17, 2025
925e530
Add match expression parsing and pattern handling, including match ar…
rossirpaulo Dec 17, 2025
0eaddb1
Add MatchExpr, MatchArm, MatchPattern, and MatchGuard AST nodes with …
rossirpaulo Dec 17, 2025
e531958
Add KW_MATCH token variant and new syntax kinds for match expressions…
rossirpaulo Dec 17, 2025
5957bdd
Add match exhaustiveness tests for unreachable arms and catch-all pat…
rossirpaulo Dec 17, 2025
03728e1
Implement match expression type inference, exhaustiveness checking, a…
rossirpaulo Dec 17, 2025
d99b26b
Add pattern binding extraction and coverage analysis for match expres…
rossirpaulo Dec 17, 2025
545645a
Add rendering support for match expressions in TreeRenderer. Enhance …
rossirpaulo Dec 17, 2025
692b27b
Added VM tests for match expressions
rossirpaulo Dec 17, 2025
f2f902f
Add new error variants for match expressions in BEP
rossirpaulo Dec 17, 2025
631a563
removed unused code
rossirpaulo Dec 17, 2025
ae1933b
avoid unwrap with no defaults (even if unreachable)
rossirpaulo Dec 17, 2025
94be060
comment needs to support bidirectional type checking, improved exhaus…
rossirpaulo Dec 17, 2025
c2d87df
added is_uninhabited function and documentation around uninhabited and
rossirpaulo Dec 17, 2025
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
282 changes: 282 additions & 0 deletions baml_language/crates/baml_codegen/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,78 @@ impl<'db, 'ctx, 'obj> Compiler<'db, 'ctx, 'obj> {
self.emit(Instruction::LoadArrayElement);
}

Expr::Match { scrutinee, arms } => {
// Desugar match to if-else chain with instanceof checks
// Per BEP-002, the codegen should produce:
// let $scrut = scrutinee
// if ($scrut instanceof Type1) { let binding = $scrut; body1 }
// else if ($scrut instanceof Type2) { ... }
// else { ... }

// Evaluate scrutinee once - the value stays on stack as the temp variable
self.compile_expr(*scrutinee, body);
let scrut_name = self.gensym("match_scrut");
// track_local records the position; the value is already on the stack
let scrut_var = self.track_local(&scrut_name);

// Track jump targets for end of match
let mut end_jumps = Vec::new();

for (i, arm) in arms.iter().enumerate() {
let is_last = i == arms.len() - 1;
let pattern = &body.patterns[arm.pattern];

// Generate pattern matching code based on pattern type
let skip_arm = self.compile_match_pattern(pattern, scrut_var, body, is_last);

// Enter scope for pattern binding (if any)
self.enter_scope();

// Bind pattern variable if it's a binding pattern
self.bind_pattern_variable(pattern, scrut_var);

// Handle guard if present
if let Some(guard_expr) = arm.guard {
self.compile_expr(guard_expr, body);
let skip_due_to_guard = self.emit(Instruction::JumpIfFalse(0));
self.emit(Instruction::Pop(1)); // Pop guard result (true case)
self.compile_expr(arm.body, body);
self.exit_scope(true);
end_jumps.push(self.emit(Instruction::Jump(0)));

// Guard failed - jump here
self.patch_jump(skip_due_to_guard);
self.emit(Instruction::Pop(1)); // Pop guard result (false case)

// If pattern check also needs patching, patch it to continue to next arm
if let Some(skip) = skip_arm {
self.patch_jump(skip);
// Pop the pattern check result (false path)
self.emit(Instruction::Pop(1));
}
} else {
// No guard - compile arm body directly
self.compile_expr(arm.body, body);
self.exit_scope(true);
if !is_last {
end_jumps.push(self.emit(Instruction::Jump(0)));
}

// Patch pattern check to continue to next arm
if let Some(skip) = skip_arm {
self.patch_jump(skip);
// Pop the pattern check result (false path)
self.emit(Instruction::Pop(1));
}
}
}

// Patch all end jumps to point here
for jump in end_jumps {
self.patch_jump(jump);
}
}

Expr::Missing => {
// TODO: cannot compile missing expression - skip
}
Expand Down Expand Up @@ -516,6 +588,12 @@ impl<'db, 'ctx, 'obj> Compiler<'db, 'ctx, 'obj> {
Pattern::Binding(name) => {
self.track_local(name.as_ref());
}
Pattern::TypedBinding { name, ty: _ } => {
self.track_local(name.as_ref());
}
Pattern::Literal(_) | Pattern::EnumVariant { .. } | Pattern::Union(_) => {
// These patterns don't introduce bindings in let statements
}
}
}

Expand Down Expand Up @@ -614,6 +692,14 @@ impl<'db, 'ctx, 'obj> Compiler<'db, 'ctx, 'obj> {
Pattern::Binding(name) => {
ctx.track_local(name.as_ref());
}
Pattern::TypedBinding { name, ty: _ } => {
ctx.track_local(name.as_ref());
}
Pattern::Literal(_)
| Pattern::EnumVariant { .. }
| Pattern::Union(_) => {
// These patterns don't introduce bindings in for-in loops
}
}

// let <pattern> = @array[@loop_i]
Expand Down Expand Up @@ -813,6 +899,202 @@ impl<'db, 'ctx, 'obj> Compiler<'db, 'ctx, 'obj> {
}
}

/// Compile pattern matching code for a match arm.
///
/// Returns `Some(jump_location)` if the pattern needs a jump to skip to the next arm,
/// or `None` if this is a catch-all pattern that always matches.
///
/// # Pattern Types
/// - `TypedBinding { name, ty }` - emit instanceof check against the type
/// - `Binding(_)` - catch-all, always matches (no check needed)
/// - `Literal(lit)` - emit equality check against the literal value
/// - `EnumVariant { .. }` - emit equality check against enum variant (TODO)
/// - `Union(patterns)` - emit OR of sub-pattern checks (TODO)
fn compile_match_pattern(
&mut self,
pattern: &Pattern,
scrut_var: usize,
body: &ExprBody,
is_last: bool,
) -> Option<usize> {
match pattern {
Pattern::TypedBinding { name: _, ty } => {
// Get the type name from the pattern
let type_name = Self::extract_type_name_from_type_ref(ty);

if let Some(class_name) = type_name {
// Look up the Class object index
if let Some(&class_obj_idx) = self.class_object_indices.get(&class_name) {
// Load scrutinee
self.emit(Instruction::LoadVar(scrut_var));

// Load Class object for instanceof comparison
let class_idx =
self.add_constant(Value::Object(ObjectIndex::from_raw(class_obj_idx)));
self.emit(Instruction::LoadConst(class_idx));

// Emit instanceof check
self.emit(Instruction::CmpOp(CmpOp::InstanceOf));

// Jump to next arm if instanceof returns false
if !is_last {
let skip_arm = self.emit(Instruction::JumpIfFalse(0));
self.emit(Instruction::Pop(1)); // Pop instanceof result (true case)
return Some(skip_arm);
} else {
// Last arm - pop the result and continue
self.emit(Instruction::Pop(1));
}
}
}
None
}

Pattern::Binding(_) => {
// Catch-all pattern - always matches, no check needed
None
}

Pattern::Literal(lit) => {
// Load scrutinee
self.emit(Instruction::LoadVar(scrut_var));

// Load literal value for comparison
self.compile_literal(lit);

// Emit equality check
self.emit(Instruction::CmpOp(CmpOp::Eq));

// Jump to next arm if not equal
if !is_last {
let skip_arm = self.emit(Instruction::JumpIfFalse(0));
self.emit(Instruction::Pop(1)); // Pop comparison result (true case)
return Some(skip_arm);
} else {
self.emit(Instruction::Pop(1));
}
None
}

Pattern::EnumVariant {
enum_name: _,
variant: _,
} => {
// TODO: Implement enum variant matching
// For now, treat as catch-all
None
}

Pattern::Union(sub_patterns) => {
// Union pattern: match if ANY sub-pattern matches
// For now, implement simple OR logic for literals/enum variants
if sub_patterns.is_empty() {
return None;
}

let mut success_jumps = Vec::new();

for (j, &sub_pat_id) in sub_patterns.iter().enumerate() {
let is_last_subpat = j == sub_patterns.len() - 1;
let sub_pattern = &body.patterns[sub_pat_id];

// For each sub-pattern, check if it matches
if let Pattern::Literal(lit) = sub_pattern {
self.emit(Instruction::LoadVar(scrut_var));
self.compile_literal(lit);
self.emit(Instruction::CmpOp(CmpOp::Eq));

if !is_last_subpat {
// If matched, jump to success
let success = self.emit(Instruction::JumpIfFalse(0));
// True case - we matched, jump to arm body
self.emit(Instruction::Pop(1));
success_jumps.push(self.emit(Instruction::Jump(0)));
// False case - try next sub-pattern
self.patch_jump(success);
self.emit(Instruction::Pop(1));
} else {
// Last sub-pattern - if this fails, the whole union fails
if !is_last {
let skip_arm = self.emit(Instruction::JumpIfFalse(0));
self.emit(Instruction::Pop(1));
// Patch all success jumps to here
for jump in success_jumps {
self.patch_jump(jump);
}
return Some(skip_arm);
} else {
self.emit(Instruction::Pop(1));
}
}
}
// TODO: Handle enum variant sub-patterns
}

// Patch success jumps to continue here
for jump in success_jumps {
self.patch_jump(jump);
}
None
}
}
}

/// Bind a pattern variable to the scrutinee value.
///
/// For binding patterns (`name` or `name: Type`), creates a local variable
/// that aliases the scrutinee. For other pattern types, this is a no-op.
fn bind_pattern_variable(&mut self, pattern: &Pattern, scrut_var: usize) {
match pattern {
Pattern::TypedBinding { name, ty: _ } | Pattern::Binding(name) => {
// Skip binding for wildcard `_` - it's semantically discarded
// but we still need to track it for scope management
if name.as_str() != "_" {
// Load scrutinee and create a local binding
self.emit(Instruction::LoadVar(scrut_var));
self.track_local(name.as_ref());
}
}
// Other patterns don't introduce bindings
Pattern::Literal(_) | Pattern::EnumVariant { .. } | Pattern::Union(_) => {}
}
}

/// Extract the type name from a TypeRef for instanceof checking.
///
/// Returns `Some(class_name)` for named types (classes), `None` for primitives
/// or types that don't support instanceof.
fn extract_type_name_from_type_ref(ty: &baml_hir::TypeRef) -> Option<String> {
match ty {
baml_hir::TypeRef::Path(path) => {
// For simple paths, return the type name
if path.segments.len() == 1 {
let name = path.segments[0].as_ref();
// Filter out primitive types which don't use instanceof
match name {
"int" | "float" | "string" | "bool" | "null" | "image" | "audio"
| "video" | "pdf" => None,
_ => Some(name.to_string()),
}
} else {
// Qualified path - join segments
Some(
path.segments
.iter()
.map(|s| s.as_ref())
.collect::<Vec<&str>>()
.join("."),
)
}
}
// Union types - would need to handle each variant separately
// For now, return None
baml_hir::TypeRef::Union(_) => None,
// Other types don't use instanceof
_ => None,
}
}

/// Convert HIR binary op to bytecode instruction.
fn binary_op_instruction(op: BinaryOp) -> Instruction {
match op {
Expand Down
2 changes: 2 additions & 0 deletions baml_language/crates/baml_diagnostics/src/compiler_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ const NOT_INDEXABLE: ErrorCode = ErrorCode(8);
const UNEXPECTED_EOF: ErrorCode = ErrorCode(9);
const UNEXPECTED_TOKEN: ErrorCode = ErrorCode(10);
const DUPLICATE_NAME: ErrorCode = ErrorCode(11);
const NON_EXHAUSTIVE_MATCH: ErrorCode = ErrorCode(62);
const UNREACHABLE_ARM: ErrorCode = ErrorCode(63);

/// Render an ariadne Report to a String.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use baml_base::Span;

use super::{
ARGUMENT_COUNT_MISMATCH, CompilerError, DUPLICATE_NAME, ErrorCode, INVALID_OPERATOR,
NO_SUCH_FIELD, NOT_CALLABLE, NOT_INDEXABLE, NameError, ParseError, Report, ReportKind,
TYPE_MISMATCH, TypeError, UNEXPECTED_EOF, UNEXPECTED_TOKEN, UNKNOWN_TYPE, UNKNOWN_VARIABLE,
NO_SUCH_FIELD, NON_EXHAUSTIVE_MATCH, NOT_CALLABLE, NOT_INDEXABLE, NameError, ParseError,
Report, ReportKind, TYPE_MISMATCH, TypeError, UNEXPECTED_EOF, UNEXPECTED_TOKEN, UNKNOWN_TYPE,
UNKNOWN_VARIABLE, UNREACHABLE_ARM,
};

/// The message format and id of each compiler error variant.
Expand Down Expand Up @@ -83,6 +84,25 @@ where
TypeError::NotIndexable { ty, span } => {
simple_error(format!("Type {ty} is not indexable"), span, NOT_INDEXABLE)
}
TypeError::NonExhaustiveMatch {
scrutinee_type,
missing_cases,
span,
} => {
let missing = missing_cases.join(", ");
simple_error(
format!(
"Non-exhaustive match: type {scrutinee_type} not fully covered. Missing: {missing}"
),
span,
NON_EXHAUSTIVE_MATCH,
)
}
TypeError::UnreachableArm { span } => simple_error(
"Unreachable match arm: previous arms already cover all cases".to_string(),
span,
UNREACHABLE_ARM,
),
},
CompilerError::NameError(name_error) => match name_error {
NameError::DuplicateName {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ pub enum TypeError<T> {
NoSuchField { ty: T, field: String, span: Span },
/// Index access on non-indexable type.
NotIndexable { ty: T, span: Span },
/// Match expression is not exhaustive - some cases are not covered.
NonExhaustiveMatch {
scrutinee_type: T,
missing_cases: Vec<String>,
span: Span,
},
/// Match arm is unreachable - it can never match because previous arms cover all cases.
UnreachableArm { span: Span },
}

impl<T> TypeError<T> {
Expand Down Expand Up @@ -90,6 +98,16 @@ impl<T> TypeError<T> {
ty: f(ty),
span: *span,
},
TypeError::NonExhaustiveMatch {
scrutinee_type,
missing_cases,
span,
} => TypeError::NonExhaustiveMatch {
scrutinee_type: f(scrutinee_type),
missing_cases: missing_cases.clone(),
span: *span,
},
TypeError::UnreachableArm { span } => TypeError::UnreachableArm { span: *span },
}
}
}
Loading