Skip to content

Commit 530a951

Browse files
authored
Merge pull request #1039 from lewis6991/fix/missing-return-dynamic-loops
fix(flow): separate dynamic loop divergence from infinite loops
2 parents 3530d81 + fef8322 commit 530a951

7 files changed

Lines changed: 264 additions & 54 deletions

File tree

crates/emmylua_code_analysis/src/compilation/analyzer/lua/func_body.rs

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,32 @@ enum ConditionState {
2424
#[derive(Debug, Default)]
2525
struct ReturnFlow {
2626
return_points: Vec<LuaReturnPoint>,
27-
can_continue: bool,
27+
can_fall_through: bool,
2828
can_break: bool,
29-
// `can_stall` means control can get stuck without returning, e.g. `while true do end`.
30-
can_stall: bool,
29+
// Some reachable path is proven to loop forever instead of returning or
30+
// reaching the following statements, e.g. `while true do end`.
31+
is_infinite: bool,
32+
// Some reachable path can stay inside a runtime-dependent loop without
33+
// proving the body itself is infinite. Keep this separate from
34+
// reachability so stricter loop diagnostics can opt into it later without
35+
// affecting inferred return types.
36+
may_diverge: bool,
3137
}
3238

3339
impl ReturnFlow {
34-
fn continue_flow() -> Self {
40+
fn fallthrough() -> Self {
3541
Self {
36-
can_continue: true,
42+
can_fall_through: true,
3743
..Default::default()
3844
}
3945
}
4046

4147
fn merge_choice(&mut self, mut other: Self) {
4248
self.return_points.append(&mut other.return_points);
43-
self.can_continue |= other.can_continue;
49+
self.can_fall_through |= other.can_fall_through;
4450
self.can_break |= other.can_break;
45-
self.can_stall |= other.can_stall;
51+
self.is_infinite |= other.is_infinite;
52+
self.may_diverge |= other.may_diverge;
4653
}
4754
}
4855

@@ -54,22 +61,22 @@ where
5461
F: FnMut(&LuaExpr) -> Result<LuaType, InferFailReason>,
5562
{
5663
let mut flow = analyze_block_returns(body, infer_expr_type)?;
57-
if flow.can_continue || flow.can_break {
64+
if flow.can_fall_through || flow.can_break {
5865
flow.return_points.push(LuaReturnPoint::Nil);
5966
}
6067

6168
Ok(flow.return_points)
6269
}
6370

64-
pub fn does_func_body_always_return_or_exit<F>(
71+
pub(in crate::compilation::analyzer) fn analyze_func_body_missing_return_flags_with<F>(
6572
body: LuaBlock,
6673
infer_expr_type: &mut F,
67-
) -> Result<bool, InferFailReason>
74+
) -> Result<(bool, bool, bool), InferFailReason>
6875
where
6976
F: FnMut(&LuaExpr) -> Result<LuaType, InferFailReason>,
7077
{
7178
let flow = analyze_block_returns(body, infer_expr_type)?;
72-
Ok(!flow.can_continue && !flow.can_break && !flow.can_stall)
79+
Ok((flow.can_fall_through, flow.can_break, flow.is_infinite))
7380
}
7481

7582
fn analyze_block_returns<F>(
@@ -80,21 +87,22 @@ where
8087
F: FnMut(&LuaExpr) -> Result<LuaType, InferFailReason>,
8188
{
8289
let mut flow = ReturnFlow::default();
83-
let mut can_continue = true;
90+
let mut can_fall_through = true;
8491

8592
for stat in block.get_stats() {
86-
if !can_continue {
93+
if !can_fall_through {
8794
break;
8895
}
8996

9097
let stat_flow = analyze_stat_returns(stat, infer_expr_type)?;
9198
flow.return_points.extend(stat_flow.return_points);
9299
flow.can_break |= stat_flow.can_break;
93-
flow.can_stall |= stat_flow.can_stall;
94-
can_continue = stat_flow.can_continue;
100+
flow.is_infinite |= stat_flow.is_infinite;
101+
flow.may_diverge |= stat_flow.may_diverge;
102+
can_fall_through = stat_flow.can_fall_through;
95103
}
96104

97-
flow.can_continue = can_continue;
105+
flow.can_fall_through = can_fall_through;
98106
Ok(flow)
99107
}
100108

@@ -107,7 +115,7 @@ where
107115
{
108116
match block {
109117
Some(block) => analyze_block_returns(block, infer_expr_type),
110-
None => Ok(ReturnFlow::continue_flow()),
118+
None => Ok(ReturnFlow::fallthrough()),
111119
}
112120
}
113121

@@ -135,7 +143,7 @@ where
135143
..Default::default()
136144
}),
137145
LuaStat::ReturnStat(return_stat) => Ok(analyze_return_stat_returns(return_stat)),
138-
_ => Ok(ReturnFlow::continue_flow()),
146+
_ => Ok(ReturnFlow::fallthrough()),
139147
}
140148
}
141149

@@ -159,15 +167,31 @@ where
159167
let condition_state =
160168
analyze_condition_state(while_stat.get_condition_expr(), infer_expr_type)?;
161169
match condition_state {
162-
ConditionState::Falsy => Ok(ReturnFlow::continue_flow()),
170+
ConditionState::Falsy => Ok(ReturnFlow::fallthrough()),
163171
ConditionState::Never => Ok(ReturnFlow::default()),
164-
ConditionState::Truthy | ConditionState::Dynamic => {
172+
ConditionState::Truthy => {
165173
let body = analyze_optional_block_returns(while_stat.get_block(), infer_expr_type)?;
166174
Ok(ReturnFlow {
167175
return_points: body.return_points,
168-
can_continue: matches!(condition_state, ConditionState::Dynamic) || body.can_break,
176+
can_fall_through: body.can_break,
169177
can_break: false,
170-
can_stall: body.can_continue || body.can_stall,
178+
is_infinite: (body.can_fall_through && !body.can_break) || body.is_infinite,
179+
may_diverge: (body.can_fall_through && body.can_break) || body.may_diverge,
180+
})
181+
}
182+
ConditionState::Dynamic => {
183+
let body = analyze_optional_block_returns(while_stat.get_block(), infer_expr_type)?;
184+
Ok(ReturnFlow {
185+
return_points: body.return_points,
186+
can_fall_through: true,
187+
can_break: false,
188+
// A dynamic condition may skip the loop entirely, but a body
189+
// that is already proven infinite remains infinite once
190+
// entered.
191+
is_infinite: body.is_infinite,
192+
// We keep dynamic loops in `may_diverge` rather than trying to
193+
// prove exit from body progress here.
194+
may_diverge: body.can_fall_through || body.may_diverge,
171195
})
172196
}
173197
}
@@ -183,25 +207,32 @@ where
183207
let body = analyze_optional_block_returns(repeat_stat.get_block(), infer_expr_type)?;
184208
let mut flow = ReturnFlow {
185209
return_points: body.return_points,
186-
can_continue: body.can_break,
210+
can_fall_through: body.can_break,
187211
can_break: false,
188-
can_stall: body.can_stall,
212+
is_infinite: body.is_infinite,
213+
may_diverge: body.may_diverge,
189214
};
190215

191-
if !body.can_continue {
216+
if !body.can_fall_through {
192217
return Ok(flow);
193218
}
194219

195220
match analyze_condition_state(repeat_stat.get_condition_expr(), infer_expr_type)? {
196221
ConditionState::Truthy => {
197-
flow.can_continue = true;
222+
flow.can_fall_through = true;
198223
}
199224
ConditionState::Falsy => {
200-
flow.can_stall = true;
225+
if body.can_break {
226+
flow.may_diverge = true;
227+
} else {
228+
flow.is_infinite = true;
229+
}
201230
}
202231
ConditionState::Dynamic => {
203-
flow.can_continue = true;
204-
flow.can_stall = true;
232+
flow.can_fall_through = true;
233+
// Same rule as dynamic `while`: the loop body is reachable, but
234+
// the exit still depends on runtime state.
235+
flow.may_diverge = true;
205236
}
206237
ConditionState::Never => {}
207238
}
@@ -217,7 +248,7 @@ where
217248
F: FnMut(&LuaExpr) -> Result<LuaType, InferFailReason>,
218249
{
219250
let mut flow = analyze_optional_block_returns(for_stat.get_block(), infer_expr_type)?;
220-
flow.can_continue = true;
251+
flow.can_fall_through = true;
221252
flow.can_break = false;
222253
Ok(flow)
223254
}
@@ -286,7 +317,7 @@ where
286317
}
287318

288319
if can_reach_next_clause {
289-
flow.can_continue = true;
320+
flow.can_fall_through = true;
290321
}
291322

292323
Ok(flow)
@@ -300,7 +331,7 @@ where
300331
F: FnMut(&LuaExpr) -> Result<LuaType, InferFailReason>,
301332
{
302333
let mut flow = analyze_optional_block_returns(for_range_stat.get_block(), infer_expr_type)?;
303-
flow.can_continue = true;
334+
flow.can_fall_through = true;
304335
flow.can_break = false;
305336
Ok(flow)
306337
}
@@ -309,7 +340,7 @@ fn analyze_call_expr_stat_returns(
309340
call_expr_stat: LuaCallExprStat,
310341
) -> Result<ReturnFlow, InferFailReason> {
311342
let Some(call_expr) = call_expr_stat.get_call_expr() else {
312-
return Ok(ReturnFlow::continue_flow());
343+
return Ok(ReturnFlow::fallthrough());
313344
};
314345

315346
if call_expr.is_error() {
@@ -319,7 +350,7 @@ fn analyze_call_expr_stat_returns(
319350
});
320351
}
321352

322-
Ok(ReturnFlow::continue_flow())
353+
Ok(ReturnFlow::fallthrough())
323354
}
324355

325356
fn analyze_return_stat_returns(return_stat: LuaReturnStat) -> ReturnFlow {

crates/emmylua_code_analysis/src/compilation/analyzer/lua/mod.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
mod call;
22
mod closure;
33
mod for_range_stat;
4-
mod func_body;
4+
pub(in crate::compilation::analyzer) mod func_body;
55
mod metatable;
66
mod module;
77
mod stats;
@@ -13,9 +13,7 @@ pub use closure::analyze_return_point;
1313
use emmylua_parser::{LuaAst, LuaAstNode, LuaExpr};
1414
use for_range_stat::analyze_for_range_stat;
1515
pub use for_range_stat::infer_for_range_iter_expr_func;
16-
pub use func_body::{
17-
LuaReturnPoint, analyze_func_body_returns_with, does_func_body_always_return_or_exit,
18-
};
16+
pub use func_body::{LuaReturnPoint, analyze_func_body_returns_with};
1917
use metatable::analyze_setmetatable;
2018
use module::analyze_chunk_return;
2119
use stats::{

crates/emmylua_code_analysis/src/compilation/analyzer/mod.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,27 @@ mod infer_cache_manager;
66
mod lua;
77
mod unresolve;
88

9-
pub(crate) use lua::does_func_body_always_return_or_exit;
10-
119
use crate::{
12-
Emmyrc, FileId, InFiled, InferFailReason,
10+
Emmyrc, FileId, InFiled, InferFailReason, LuaType,
1311
db_index::{DbIndex, WorkspaceId},
1412
profile::Profile,
1513
};
16-
use emmylua_parser::LuaChunk;
14+
use emmylua_parser::{LuaBlock, LuaChunk, LuaExpr};
1715
use hashbrown::{HashMap, HashSet};
1816
use infer_cache_manager::InferCacheManager;
1917
use std::sync::Arc;
2018
use unresolve::UnResolve;
2119

20+
pub(super) fn analyze_func_body_missing_return_flags_with<F>(
21+
body: LuaBlock,
22+
infer_expr_type: &mut F,
23+
) -> Result<(bool, bool, bool), InferFailReason>
24+
where
25+
F: FnMut(&LuaExpr) -> Result<LuaType, InferFailReason>,
26+
{
27+
lua::func_body::analyze_func_body_missing_return_flags_with(body, infer_expr_type)
28+
}
29+
2230
pub fn analyze(db: &mut DbIndex, need_analyzed_files: Vec<InFiled<LuaChunk>>, config: Arc<Emmyrc>) {
2331
if need_analyzed_files.is_empty() {
2432
return;

crates/emmylua_code_analysis/src/compilation/mod.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,21 @@ mod test;
33

44
use std::sync::Arc;
55

6-
pub(crate) use analyzer::does_func_body_always_return_or_exit;
7-
86
use crate::{
9-
Emmyrc, FileId, InFiled, LuaIndex, LuaInferCache, db_index::DbIndex, semantic::SemanticModel,
7+
Emmyrc, FileId, InFiled, InferFailReason, LuaIndex, LuaInferCache, LuaType, db_index::DbIndex,
8+
semantic::SemanticModel,
109
};
10+
use emmylua_parser::{LuaBlock, LuaExpr};
11+
12+
pub(crate) fn analyze_func_body_missing_return_flags_with<F>(
13+
body: LuaBlock,
14+
infer_expr_type: &mut F,
15+
) -> Result<(bool, bool, bool), InferFailReason>
16+
where
17+
F: FnMut(&LuaExpr) -> Result<LuaType, InferFailReason>,
18+
{
19+
analyzer::analyze_func_body_missing_return_flags_with(body, infer_expr_type)
20+
}
1121

1222
#[derive(Debug)]
1323
pub struct LuaCompilation {

crates/emmylua_code_analysis/src/compilation/test/closure_return_test.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,44 @@ mod test {
301301
);
302302
}
303303

304+
#[test]
305+
fn test_inferred_return_from_truthy_while_with_break_before_return() {
306+
assert_inferred_return_without_nil(
307+
r#"
308+
local function f(done)
309+
while true do
310+
if done then
311+
break
312+
end
313+
end
314+
315+
return 1
316+
end
317+
318+
result = f()
319+
"#,
320+
);
321+
}
322+
323+
#[test]
324+
fn test_inferred_return_from_infinite_repeat_with_break_before_return() {
325+
assert_inferred_return_without_nil(
326+
r#"
327+
local function f(done)
328+
repeat
329+
if done then
330+
break
331+
end
332+
until false
333+
334+
return 1
335+
end
336+
337+
result = f()
338+
"#,
339+
);
340+
}
341+
304342
#[test]
305343
fn test_return_flow_keeps_local_while_call_condition_dynamic() {
306344
let mut ws = VirtualWorkspace::new();

crates/emmylua_code_analysis/src/diagnostic/checker/check_return_count.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use emmylua_parser::{
55

66
use crate::{
77
DiagnosticCode, LuaSignatureId, LuaType, SemanticModel, SignatureReturnStatus,
8-
compilation::does_func_body_always_return_or_exit,
8+
compilation::analyze_func_body_missing_return_flags_with,
99
};
1010

1111
use super::{Checker, DiagnosticContext, get_return_stats};
@@ -111,13 +111,20 @@ fn check_missing_return(
111111
// 检测缺少返回语句需要处理 if while
112112
if min_expected_return_count > 0 {
113113
let range = if let Some(block) = closure_expr.get_block() {
114-
if does_func_body_always_return_or_exit(block.clone(), &mut |expr: &LuaExpr| {
115-
Ok(semantic_model
116-
.infer_expr(expr.clone())
117-
.unwrap_or(LuaType::Unknown))
118-
})
119-
.ok()?
120-
{
114+
let (can_fall_through, can_break, is_infinite) =
115+
analyze_func_body_missing_return_flags_with(
116+
block.clone(),
117+
&mut |expr: &LuaExpr| {
118+
Ok(semantic_model
119+
.infer_expr(expr.clone())
120+
.unwrap_or(LuaType::Unknown))
121+
},
122+
)
123+
.ok()?;
124+
125+
// `MissingReturn` currently ignores runtime-dependent divergence if
126+
// a later `return` is still reachable.
127+
if !can_fall_through && !can_break && !is_infinite {
121128
return Some(());
122129
}
123130

0 commit comments

Comments
 (0)