From 22023a87341b2fcc8478a3d0743d3cdc1869a9b1 Mon Sep 17 00:00:00 2001 From: Jooris Hadeler Date: Tue, 21 Apr 2026 18:36:02 +0200 Subject: [PATCH] feat: add control flow analysis and error reporting for unreachable code --- src/frontend/sema.rs | 68 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 65 insertions(+), 3 deletions(-) diff --git a/src/frontend/sema.rs b/src/frontend/sema.rs index 8f0993c..f20eef8 100644 --- a/src/frontend/sema.rs +++ b/src/frontend/sema.rs @@ -79,6 +79,7 @@ pub struct Sema { deferred_unary_neg: Vec<(Span, Ty, Ty, Option)>, deferred_binary: Vec<(Span, Ty)>, deferred_literals: Vec<(Span, Ty)>, + is_reachable: bool, } impl Sema { @@ -92,6 +93,7 @@ impl Sema { deferred_unary_neg: Vec::new(), deferred_binary: Vec::new(), deferred_literals: Vec::new(), + is_reachable: true, } } @@ -100,18 +102,22 @@ impl Sema { (!self.errors.is_empty()).then_some(self.errors) } + /// Pushes a new, empty scope onto the environment stack. fn enter_scope(&mut self) { self.scopes.push(HashMap::new()); } + /// Pops the current scope from the environment stack. fn leave_scope(&mut self) { self.scopes.pop(); } + /// Binds a name to a type in the current innermost scope. fn bind(&mut self, name: &str, ty: Ty) { self.scopes.last_mut().unwrap().insert(name.to_string(), ty); } + /// Looks up a name in the environment, searching from the innermost scope outwards. fn lookup(&self, name: &str) -> Option<&Ty> { self.scopes.iter().rev().find_map(|scope| scope.get(name)) } @@ -259,8 +265,17 @@ impl Sema { .map(|t| Ty::from(&t.kind)) .unwrap_or(Ty::Unit); + self.is_reachable = true; + let typed_body = self.analyze_stmt(body, &expected_ret_ty); + if expected_ret_ty != Ty::Unit && self.is_reachable { + self.errors.push(SemanticError::new( + "not all control paths return a value", + decl.span, + )); + } + self.leave_scope(); TypedDecl::Function { @@ -279,10 +294,16 @@ impl Sema { match &stmt.kind { StmtKind::Compound { inner } => { let mut typed_inner = Vec::new(); + let mut reported_unreachable = false; self.enter_scope(); for s in inner { + if !self.is_reachable && !reported_unreachable { + self.errors + .push(SemanticError::new("unreachable statement", s.span)); + reported_unreachable = true; + } typed_inner.push(self.analyze_stmt(s, expected_ret_ty)); } @@ -301,10 +322,24 @@ impl Sema { self.errors.push(SemanticError::new(err, condition.span)); } + let initial_reachable = self.is_reachable; + + self.is_reachable = initial_reachable; let typed_then = self.analyze_stmt(then, expected_ret_ty); - let typed_elze = elze - .as_ref() - .map(|stmt| self.analyze_stmt(stmt, expected_ret_ty)); + let reachable_after_then = self.is_reachable; + + let typed_elze = elze.as_ref().map(|e| { + self.is_reachable = initial_reachable; + self.analyze_stmt(e, expected_ret_ty) + }); + + let reachable_after_else = if elze.is_some() { + self.is_reachable + } else { + initial_reachable + }; + + self.is_reachable = reachable_after_then || reachable_after_else; TypedStmt::If { condition: typed_condition, @@ -320,6 +355,8 @@ impl Sema { self.errors.push(SemanticError::new(err, expr.span)); } + self.is_reachable = false; + TypedStmt::Return { value: Some(typed_expr), } @@ -328,6 +365,8 @@ impl Sema { self.errors.push(SemanticError::new(err, stmt.span)); } + self.is_reachable = false; + TypedStmt::Return { value: None } } } @@ -765,4 +804,27 @@ mod test { let src = "fn test() { if 12 {} }"; assert!(analyze(src).is_err()); } + + #[test] + fn not_all_paths_return() { + let src = "fn test(a: i32) -> i32 { if a < 5 { return 5; } else { } }"; + assert!(analyze(src).is_err()); + + let src = "fn test() -> i32 { }"; + assert!(analyze(src).is_err()); + + let src = "fn test(a: i32) -> i32 { if a < 5 { return 5; } return 10; }"; + assert!(analyze(src).is_ok()); + } + + #[test] + fn unreachable_code() { + let src = "fn test() -> i32 { return 5; return 10; }"; + let errors = analyze(src).unwrap_err(); + assert!( + errors + .iter() + .any(|e| e.message.contains("unreachable statement")) + ); + } }