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
9 changes: 9 additions & 0 deletions src/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Expr>,
pub then_expr: Box<Expr>,
pub else_expr: Box<Expr>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
78 changes: 77 additions & 1 deletion src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

Expand Down Expand Up @@ -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> {
Expand Down
10 changes: 10 additions & 0 deletions src/lexer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ pub enum Token {
Exclamation,
Ampersand,
Comma,
Question,
Colon,
Integer(i32),
}

Expand Down Expand Up @@ -156,6 +158,14 @@ pub fn tokenize(source: &str) -> Result<Vec<Token>, 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(&'=') {
Expand Down
64 changes: 58 additions & 6 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -290,7 +290,27 @@ impl<'a> Parser<'a> {
}

fn parse_expression(&mut self) -> Result<Expr, ParseError> {
self.parse_logical_or()
self.parse_conditional()
}

fn parse_conditional(&mut self) -> Result<Expr, ParseError> {
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<Expr, ParseError> {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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![
Expand Down
28 changes: 28 additions & 0 deletions tests/compiler_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading