From 71aa7d0ee37bf9270004771f6963b8f72f2d3916 Mon Sep 17 00:00:00 2001 From: Adam Hameed Date: Wed, 15 Jul 2026 00:26:37 -0400 Subject: [PATCH] feat: support the ternary conditional operator (?:) - Question/Colon tokens and a ConditionalExpr AST node - Parsed above logical-or with C's right associativity (a ? b : c ? d : e groups as a ? b : (c ? d : e)) - Codegen branches lazily and merges with a phi, mirroring the existing short-circuit lowering; branch types must match - Also removes a stray clippy allow attribute left on LoopContext - Parser unit test plus e2e tests covering chaining, laziness (untaken division by zero), and use with calls; verified against clang Co-Authored-By: Claude Fable 5 --- src/ast/mod.rs | 9 +++++ src/codegen/mod.rs | 78 ++++++++++++++++++++++++++++++++++++++++++- src/lexer/mod.rs | 10 ++++++ src/parser/mod.rs | 64 +++++++++++++++++++++++++++++++---- tests/compiler_e2e.rs | 28 ++++++++++++++++ 5 files changed, 182 insertions(+), 7 deletions(-) diff --git a/src/ast/mod.rs b/src/ast/mod.rs index c1ec8d9..af92bb2 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -84,6 +84,15 @@ pub enum Expr { Unary(UnaryExpr), Binary(BinaryExpr), Call(FunctionCallExpr), + Conditional(ConditionalExpr), +} + +/// Ternary conditional expression: `cond ? then_expr : else_expr`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConditionalExpr { + pub cond: Box, + pub then_expr: Box, + pub else_expr: Box, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/codegen/mod.rs b/src/codegen/mod.rs index 8bf288c..996b0a4 100644 --- a/src/codegen/mod.rs +++ b/src/codegen/mod.rs @@ -215,6 +215,9 @@ fn type_of_expr( .ok_or_else(|| format!("undefined function '{}'", call.name))?; Ok(ret_ty.clone()) } + Expr::Conditional(conditional) => { + type_of_expr(&conditional.then_expr, variables, function_types) + } } } @@ -591,10 +594,83 @@ fn emit_expr<'ctx>( }; Ok(call_val) } + Expr::Conditional(conditional) => { + let cond_val = emit_expr( + context, + builder, + &conditional.cond, + variables, + function_types, + module, + )?; + let cond_int = match cond_val { + BasicValueEnum::IntValue(i) => i, + _ => return Err("ternary condition must be an integer".to_string()), + }; + let cond_is_true = builder + .build_int_compare( + IntPredicate::NE, + cond_int, + context.i32_type().const_zero(), + "ternarycond", + ) + .map_err(|err| err.to_string())?; + + let start_bb = builder.get_insert_block().ok_or("no insert block")?; + let parent_func = start_bb.get_parent().ok_or("no parent function")?; + + let then_bb = context.append_basic_block(parent_func, "ternary.then"); + let else_bb = context.append_basic_block(parent_func, "ternary.else"); + let merge_bb = context.append_basic_block(parent_func, "ternary.merge"); + + builder + .build_conditional_branch(cond_is_true, then_bb, else_bb) + .map_err(|err| err.to_string())?; + + // Then branch (only evaluated when the condition is true) + builder.position_at_end(then_bb); + let then_val = emit_expr( + context, + builder, + &conditional.then_expr, + variables, + function_types, + module, + )?; + builder + .build_unconditional_branch(merge_bb) + .map_err(|err| err.to_string())?; + let actual_then_bb = builder.get_insert_block().ok_or("no insert block")?; + + // Else branch (only evaluated when the condition is false) + builder.position_at_end(else_bb); + let else_val = emit_expr( + context, + builder, + &conditional.else_expr, + variables, + function_types, + module, + )?; + builder + .build_unconditional_branch(merge_bb) + .map_err(|err| err.to_string())?; + let actual_else_bb = builder.get_insert_block().ok_or("no insert block")?; + + if then_val.get_type() != else_val.get_type() { + return Err("ternary branches must have the same type".to_string()); + } + + builder.position_at_end(merge_bb); + let phi = builder + .build_phi(then_val.get_type(), "ternary.result") + .map_err(|err| err.to_string())?; + phi.add_incoming(&[(&then_val, actual_then_bb), (&else_val, actual_else_bb)]); + Ok(phi.as_basic_value()) + } } } -#[allow(clippy::too_many_arguments)] /// Branch targets for `continue` and `break` inside the innermost loop. #[derive(Clone, Copy)] struct LoopContext<'ctx> { diff --git a/src/lexer/mod.rs b/src/lexer/mod.rs index 50769bb..3f5b899 100644 --- a/src/lexer/mod.rs +++ b/src/lexer/mod.rs @@ -39,6 +39,8 @@ pub enum Token { Exclamation, Ampersand, Comma, + Question, + Colon, Integer(i32), } @@ -156,6 +158,14 @@ pub fn tokenize(source: &str) -> Result, String> { chars.next(); tokens.push(Token::Semicolon); } + '?' => { + chars.next(); + tokens.push(Token::Question); + } + ':' => { + chars.next(); + tokens.push(Token::Colon); + } '=' => { chars.next(); if chars.peek() == Some(&'=') { diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 7d0f333..2b2fe89 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1,7 +1,7 @@ use crate::ast::{BinaryExpr, BinaryOp, UnaryExpr, UnaryOp}; use crate::ast::{ - Expr, ForStatement, Function, FunctionCallExpr, IfStatement, IntegerLiteral, Parameter, - Program, ReturnStatement, Statement, Type, VarAssignStatement, VarDeclareStatement, + ConditionalExpr, Expr, ForStatement, Function, FunctionCallExpr, IfStatement, IntegerLiteral, + Parameter, Program, ReturnStatement, Statement, Type, VarAssignStatement, VarDeclareStatement, VariableExpr, WhileStatement, }; use crate::lexer::Token; @@ -290,7 +290,27 @@ impl<'a> Parser<'a> { } fn parse_expression(&mut self) -> Result { - self.parse_logical_or() + self.parse_conditional() + } + + fn parse_conditional(&mut self) -> Result { + let cond = self.parse_logical_or()?; + + if self.peek() != Some(&Token::Question) { + return Ok(cond); + } + self.next(); + + let then_expr = self.parse_expression()?; + self.expect_token(Token::Colon, "`:`")?; + // Right-associative: `a ? b : c ? d : e` is `a ? b : (c ? d : e)`. + let else_expr = self.parse_conditional()?; + + Ok(Expr::Conditional(ConditionalExpr { + cond: Box::new(cond), + then_expr: Box::new(then_expr), + else_expr: Box::new(else_expr), + })) } fn parse_logical_or(&mut self) -> Result { @@ -592,9 +612,9 @@ impl<'a> Parser<'a> { mod tests { use super::{ParseError, Parser, parse}; use crate::ast::{ - BinaryExpr, BinaryOp, Expr, Function, FunctionCallExpr, IntegerLiteral, Parameter, Program, - ReturnStatement, Statement, Type, UnaryExpr, UnaryOp, VarAssignStatement, - VarDeclareStatement, VariableExpr, + BinaryExpr, BinaryOp, ConditionalExpr, Expr, Function, FunctionCallExpr, IntegerLiteral, + Parameter, Program, ReturnStatement, Statement, Type, UnaryExpr, UnaryOp, + VarAssignStatement, VarDeclareStatement, VariableExpr, }; use crate::lexer::Token; @@ -962,6 +982,38 @@ mod tests { assert_eq!(error, ParseError::InvalidIncrementTarget); } + #[test] + fn parses_ternary_conditional() { + let tokens = vec![ + Token::Int, + Token::Identifier("main".to_string()), + Token::LeftParen, + Token::RightParen, + Token::LeftBrace, + Token::Return, + Token::Integer(1), + Token::Question, + Token::Integer(2), + Token::Colon, + Token::Integer(3), + Token::Semicolon, + Token::RightBrace, + ]; + + let program = parse(&tokens).expect("parser should accept a ternary conditional"); + + assert_eq!( + program.functions[0].body[0], + Statement::Return(ReturnStatement { + expr: Expr::Conditional(ConditionalExpr { + cond: Box::new(Expr::IntegerLiteral(IntegerLiteral { value: 1 })), + then_expr: Box::new(Expr::IntegerLiteral(IntegerLiteral { value: 2 })), + else_expr: Box::new(Expr::IntegerLiteral(IntegerLiteral { value: 3 })), + }), + }) + ); + } + #[test] fn parses_comparisons_and_logical_operators() { let tokens = vec![ diff --git a/tests/compiler_e2e.rs b/tests/compiler_e2e.rs index abddf39..e6d0b2f 100644 --- a/tests/compiler_e2e.rs +++ b/tests/compiler_e2e.rs @@ -33,6 +33,34 @@ fn evaluates_division_and_subtraction() { assert_program_exit_code("int main() { return 20 / 5 - 1; }\n", 3); } +#[test] +fn evaluates_ternary_conditional() { + assert_program_exit_code("int main() { return 1 ? 10 : 20; }\n", 10); + assert_program_exit_code("int main() { return 0 ? 10 : 20; }\n", 20); +} + +#[test] +fn evaluates_chained_ternary_right_associativity() { + assert_program_exit_code( + "int main() { int x = 2; return x == 1 ? 10 : x == 2 ? 20 : 30; }\n", + 20, + ); +} + +#[test] +fn ternary_evaluates_only_taken_branch() { + // The untaken branch divides by zero; lazy evaluation means no trap. + assert_program_exit_code("int main() { int zero = 0; return 0 ? 5 / zero : 7; }\n", 7); +} + +#[test] +fn evaluates_ternary_with_calls_and_assignment() { + assert_program_exit_code( + "int max(int a, int b) { return a > b ? a : b; }\nint main() { int m = max(3, 9); m += 1 ? 2 : 100; return m; }\n", + 11, + ); +} + #[test] fn evaluates_break_in_while_loop() { assert_program_exit_code(