From 460651c09fe463d60dca073c05e4d6daa35e7c4a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 2 Jul 2026 12:03:21 +0100 Subject: [PATCH] fix(flow): stop cyclic walks after unreachable continue Track visited flow ids once loop control can feed a walk back into an earlier loop label. If a walk revisits a flow node, fall back to the declared reference type instead of walking forever. Fixes #1146 Assisted-by: Codex --- .../src/compilation/test/flow.rs | 26 ++++++++++++++++++- .../semantic/infer/narrow/get_type_at_flow.rs | 20 ++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/crates/emmylua_code_analysis/src/compilation/test/flow.rs b/crates/emmylua_code_analysis/src/compilation/test/flow.rs index 6dfadbca3..b83ff7c9d 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/flow.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/flow.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod test { - use crate::{DiagnosticCode, LuaType, VirtualWorkspace}; + use crate::{DiagnosticCode, EmmyrcLuaVersion, LuaType, VirtualWorkspace}; use emmylua_parser::{LuaAstToken, LuaLocalName}; use ntest::timeout; @@ -1475,6 +1475,30 @@ end assert_eq!(ws.expr_ty("after_loop"), ws.ty("string?")); } + #[test] + #[timeout(5000)] + fn test_unreachable_continue_loop_after_return_does_not_cycle() { + let mut ws = VirtualWorkspace::new(); + let mut emmyrc = ws.get_emmyrc(); + emmyrc.runtime.version = EmmyrcLuaVersion::LuaJITExt; + ws.update_emmyrc(emmyrc); + + ws.def( + r#" + local side_effect = 0 + return boolean + + local continue_counter = 0 + for i = 1, 5 do + if i % 2 == 0 then + continue + end + continue_counter = continue_counter + 1 + end + "#, + ); + } + #[test] fn test_for_in_loop_post_flow_keeps_incoming_type_after_break() { let mut ws = VirtualWorkspace::new_with_init_std_lib(); diff --git a/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs b/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs index 188c709ae..5fc84839e 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs @@ -1322,7 +1322,18 @@ impl<'a> FlowTypeEngine<'a> { // produces a final type, needs another query first, or reaches a saved // continuation point like a branch merge. fn evaluate_walk(&mut self, mut walk: QueryWalk) -> Result { + // Most walks are acyclic, so only start tracking after loop control can + // feed a later flow node back into an earlier loop label. + let mut visited_flow_ids = None::>; + loop { + if let Some(visited_flow_ids) = &mut visited_flow_ids + && !visited_flow_ids.insert(walk.antecedent_flow_id) + { + let result_type = get_var_ref_type(self.db, self.cache, &walk.query.var_ref_id)?; + return Ok(self.finish_walk(walk, result_type)); + } + // Replays can suspend a query and later revisit an older antecedent // that another query already finished. Use that cached type instead // of walking back through the same replay chain again. @@ -1356,6 +1367,15 @@ impl<'a> FlowTypeEngine<'a> { | FlowNodeKind::Continue | FlowNodeKind::Return | FlowNodeKind::ForIStat(_) => { + if matches!( + &flow_node.kind, + FlowNodeKind::LoopLabel | FlowNodeKind::Continue + ) && visited_flow_ids.is_none() + { + let mut visited = HashSet::new(); + visited.insert(walk.antecedent_flow_id); + visited_flow_ids = Some(visited); + } walk.antecedent_flow_id = get_single_antecedent(flow_node)?; } FlowNodeKind::BranchLabel | FlowNodeKind::NamedLabel(_) => {