feat: add control flow analysis and error reporting for unreachable code
This commit is contained in:
+65
-3
@@ -79,6 +79,7 @@ pub struct Sema {
|
|||||||
deferred_unary_neg: Vec<(Span, Ty, Ty, Option<u64>)>,
|
deferred_unary_neg: Vec<(Span, Ty, Ty, Option<u64>)>,
|
||||||
deferred_binary: Vec<(Span, Ty)>,
|
deferred_binary: Vec<(Span, Ty)>,
|
||||||
deferred_literals: Vec<(Span, Ty)>,
|
deferred_literals: Vec<(Span, Ty)>,
|
||||||
|
is_reachable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sema {
|
impl Sema {
|
||||||
@@ -92,6 +93,7 @@ impl Sema {
|
|||||||
deferred_unary_neg: Vec::new(),
|
deferred_unary_neg: Vec::new(),
|
||||||
deferred_binary: Vec::new(),
|
deferred_binary: Vec::new(),
|
||||||
deferred_literals: Vec::new(),
|
deferred_literals: Vec::new(),
|
||||||
|
is_reachable: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,18 +102,22 @@ impl Sema {
|
|||||||
(!self.errors.is_empty()).then_some(self.errors)
|
(!self.errors.is_empty()).then_some(self.errors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pushes a new, empty scope onto the environment stack.
|
||||||
fn enter_scope(&mut self) {
|
fn enter_scope(&mut self) {
|
||||||
self.scopes.push(HashMap::new());
|
self.scopes.push(HashMap::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Pops the current scope from the environment stack.
|
||||||
fn leave_scope(&mut self) {
|
fn leave_scope(&mut self) {
|
||||||
self.scopes.pop();
|
self.scopes.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Binds a name to a type in the current innermost scope.
|
||||||
fn bind(&mut self, name: &str, ty: Ty) {
|
fn bind(&mut self, name: &str, ty: Ty) {
|
||||||
self.scopes.last_mut().unwrap().insert(name.to_string(), 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> {
|
fn lookup(&self, name: &str) -> Option<&Ty> {
|
||||||
self.scopes.iter().rev().find_map(|scope| scope.get(name))
|
self.scopes.iter().rev().find_map(|scope| scope.get(name))
|
||||||
}
|
}
|
||||||
@@ -259,8 +265,17 @@ impl Sema {
|
|||||||
.map(|t| Ty::from(&t.kind))
|
.map(|t| Ty::from(&t.kind))
|
||||||
.unwrap_or(Ty::Unit);
|
.unwrap_or(Ty::Unit);
|
||||||
|
|
||||||
|
self.is_reachable = true;
|
||||||
|
|
||||||
let typed_body = self.analyze_stmt(body, &expected_ret_ty);
|
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();
|
self.leave_scope();
|
||||||
|
|
||||||
TypedDecl::Function {
|
TypedDecl::Function {
|
||||||
@@ -279,10 +294,16 @@ impl Sema {
|
|||||||
match &stmt.kind {
|
match &stmt.kind {
|
||||||
StmtKind::Compound { inner } => {
|
StmtKind::Compound { inner } => {
|
||||||
let mut typed_inner = Vec::new();
|
let mut typed_inner = Vec::new();
|
||||||
|
let mut reported_unreachable = false;
|
||||||
|
|
||||||
self.enter_scope();
|
self.enter_scope();
|
||||||
|
|
||||||
for s in inner {
|
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));
|
typed_inner.push(self.analyze_stmt(s, expected_ret_ty));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,10 +322,24 @@ impl Sema {
|
|||||||
self.errors.push(SemanticError::new(err, condition.span));
|
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_then = self.analyze_stmt(then, expected_ret_ty);
|
||||||
let typed_elze = elze
|
let reachable_after_then = self.is_reachable;
|
||||||
.as_ref()
|
|
||||||
.map(|stmt| self.analyze_stmt(stmt, expected_ret_ty));
|
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 {
|
TypedStmt::If {
|
||||||
condition: typed_condition,
|
condition: typed_condition,
|
||||||
@@ -320,6 +355,8 @@ impl Sema {
|
|||||||
self.errors.push(SemanticError::new(err, expr.span));
|
self.errors.push(SemanticError::new(err, expr.span));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.is_reachable = false;
|
||||||
|
|
||||||
TypedStmt::Return {
|
TypedStmt::Return {
|
||||||
value: Some(typed_expr),
|
value: Some(typed_expr),
|
||||||
}
|
}
|
||||||
@@ -328,6 +365,8 @@ impl Sema {
|
|||||||
self.errors.push(SemanticError::new(err, stmt.span));
|
self.errors.push(SemanticError::new(err, stmt.span));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.is_reachable = false;
|
||||||
|
|
||||||
TypedStmt::Return { value: None }
|
TypedStmt::Return { value: None }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -765,4 +804,27 @@ mod test {
|
|||||||
let src = "fn test() { if 12 {} }";
|
let src = "fn test() { if 12 {} }";
|
||||||
assert!(analyze(src).is_err());
|
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"))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user