From 66b04349049fc2cd9894ef51b21d4e10560ad29a Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Wed, 3 Jun 2026 16:34:50 +0100 Subject: [PATCH] fix: separate unreachable conditions from never types Truthiness checks for call conditions treated an impossible branch as `Result(Never)`. That made the queried symbol look like `never` while walking the branch, so unrelated callable values inside impossible `if`/`else` bodies could be narrowed to non-callable. Add an explicit unreachable condition action and handle it by query mode. Normal point queries continue past an unreachable condition edge so unrelated symbols keep their declared types. Merge-branch queries finish with `never`, so unreachable branch predecessors still drop out of the merged type. Carry branch reachability as an internal flow-query result instead of inferring it from `LuaType::Never`. Assignment and cast continuations can then distinguish an unreachable predecessor from an ordinary type result that happens to be `never`. Preserve merge-branch mode through assignment, tag-cast, and correlated condition subqueries. That prevents assignments, annotated assignments, and casts inside impossible branches from contributing their statement-local type effects to the final merge. Rename flow modes around their actual use and keep single-predecessor branch labels out of merge mode. Add coverage for plain call conditions, always-false and always-true call-condition branches, and unreachable assignment, annotated-assignment, and cast merge contributions. Assisted-by: Codex --- .../src/compilation/test/flow.rs | 128 ++++++++++ .../src/semantic/cache/mod.rs | 25 +- .../infer/narrow/condition_flow/mod.rs | 7 +- .../semantic/infer/narrow/get_type_at_flow.rs | 224 ++++++++++++++---- 4 files changed, 332 insertions(+), 52 deletions(-) diff --git a/crates/emmylua_code_analysis/src/compilation/test/flow.rs b/crates/emmylua_code_analysis/src/compilation/test/flow.rs index 4bcd66dd8..a5130f852 100644 --- a/crates/emmylua_code_analysis/src/compilation/test/flow.rs +++ b/crates/emmylua_code_analysis/src/compilation/test/flow.rs @@ -304,6 +304,134 @@ mod test { assert_eq!(ws.expr_ty("after_guard"), ws.ty("string")); } + #[test] + fn test_plain_call_condition_keeps_inner_call_prefix_type() { + let mut ws = VirtualWorkspace::new(); + let code = r#" + local function a() end + local function b() end + + b() + if a() then + b() + inner = b + end + "#; + ws.def(code); + + let ty = ws.expr_ty("inner"); + assert!(ty.is_function()); + + let mut diag_ws = VirtualWorkspace::new(); + assert!(diag_ws.has_no_diagnostic(DiagnosticCode::CallNonCallable, code)); + } + + #[test] + fn test_false_call_condition_keeps_inner_unrelated_type() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@return false + local function always_false() + return false + end + + ---@type string + local value = "ok" + if always_false() then + inner = value + end + "#, + ); + + assert_eq!(ws.expr_ty("inner"), ws.ty("string")); + } + + #[test] + fn test_true_call_condition_keeps_else_call_prefix_type() { + let mut ws = VirtualWorkspace::new(); + let code = r#" + ---@return true + local function always_true() + return true + end + + local function b() end + if always_true() then + else + b() + end + "#; + + assert!(ws.has_no_diagnostic(DiagnosticCode::CallNonCallable, code)); + } + + #[test] + fn test_false_call_condition_assignment_does_not_contribute_to_merge() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@return false + local function always_false() + return false + end + + local value = "before" + if always_false() then + value = 1 + end + after = value + "#, + ); + + let after = ws.expr_ty("after"); + assert_eq!(ws.humanize_type(after), "string"); + } + + #[test] + fn test_false_call_condition_tag_cast_does_not_contribute_to_merge() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@return false + local function always_false() + return false + end + + local value = "before" + if always_false() then + ---@cast value integer + end + after = value + "#, + ); + + let after = ws.expr_ty("after"); + assert_eq!(ws.humanize_type(after), r#""before""#); + } + + #[test] + fn test_false_call_condition_doc_assignment_does_not_contribute_to_merge() { + let mut ws = VirtualWorkspace::new(); + ws.def( + r#" + ---@return false + local function always_false() + return false + end + + local value = "before" + if always_false() then + ---@type integer + value = 1 + end + after = value + "#, + ); + + assert_eq!(ws.expr_ty("after"), ws.ty("string")); + } + #[test] fn test_branch_join_keeps_union_when_only_one_side_narrows() { let mut ws = VirtualWorkspace::new(); diff --git a/crates/emmylua_code_analysis/src/semantic/cache/mod.rs b/crates/emmylua_code_analysis/src/semantic/cache/mod.rs index c6dc256fa..353557757 100644 --- a/crates/emmylua_code_analysis/src/semantic/cache/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/cache/mod.rs @@ -26,19 +26,32 @@ pub(in crate::semantic) struct FlowAssignmentInfo { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub(in crate::semantic) enum FlowMode { - WithConditions, - WithoutConditions, + Normal, + // Query one predecessor of a merge label; if that walk reaches an + // unreachable condition edge, it contributes `never` to the merged type. + MergeBranch, + // Re-query assignment antecedents without applying condition narrows. + IgnoreConditions, } -impl FlowMode { - pub fn uses_conditions(self) -> bool { - matches!(self, Self::WithConditions) +#[derive(Debug, Clone)] +pub(in crate::semantic) enum FlowQueryResult { + Type(LuaType), + Unreachable, +} + +impl FlowQueryResult { + pub(in crate::semantic) fn into_type(self) -> LuaType { + match self { + Self::Type(typ) => typ, + Self::Unreachable => LuaType::Never, + } } } #[derive(Debug, Default)] pub(in crate::semantic) struct FlowVarCache { - pub type_cache: HashMap<(FlowId, FlowMode), CacheEntry>, + pub type_cache: HashMap<(FlowId, FlowMode), CacheEntry>, pub condition_cache: HashMap<(FlowId, InferConditionFlow), CacheEntry>, } diff --git a/crates/emmylua_code_analysis/src/semantic/infer/narrow/condition_flow/mod.rs b/crates/emmylua_code_analysis/src/semantic/infer/narrow/condition_flow/mod.rs index c88d17092..dba17e7fd 100644 --- a/crates/emmylua_code_analysis/src/semantic/infer/narrow/condition_flow/mod.rs +++ b/crates/emmylua_code_analysis/src/semantic/infer/narrow/condition_flow/mod.rs @@ -121,6 +121,7 @@ pub(in crate::semantic) enum CorrelatedDiscriminantNarrow { #[derive(Debug, Clone)] pub(in crate::semantic) enum ConditionFlowAction { Continue, + Unreachable, Result(LuaType), Pending(PendingConditionNarrow), NeedExprType { @@ -712,12 +713,12 @@ pub(in crate::semantic::infer::narrow) fn resolve_expr_type_continuation( condition_flow, ), ExprTypeContinuation::Truthiness { condition_flow } => Ok(match condition_flow { - _ if expr_type.is_never() => ConditionFlowAction::Result(LuaType::Never), + _ if expr_type.is_never() => ConditionFlowAction::Unreachable, InferConditionFlow::TrueCondition if expr_type.is_always_falsy() => { - ConditionFlowAction::Result(LuaType::Never) + ConditionFlowAction::Unreachable } InferConditionFlow::FalseCondition if expr_type.is_always_truthy() => { - ConditionFlowAction::Result(LuaType::Never) + ConditionFlowAction::Unreachable } _ => ConditionFlowAction::Continue, }), 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 a29a511b3..de525f733 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 @@ -9,7 +9,7 @@ use crate::{ CacheEntry, DbIndex, FlowId, FlowNode, FlowNodeKind, FlowTree, InferFailReason, LuaDeclId, LuaInferCache, LuaMemberId, LuaSignatureId, LuaType, TypeOps, check_type_compact, semantic::{ - cache::{FlowAssignmentInfo, FlowMode, FlowVarCache}, + cache::{FlowAssignmentInfo, FlowMode, FlowQueryResult, FlowVarCache}, infer::{ InferResult, VarRefId, infer_name::infer_global_type, @@ -36,8 +36,7 @@ use crate::{ }; #[derive(Debug, Clone, PartialEq, Eq, Hash)] -// One cached flow query: one ref at one flow node, optionally without replaying -// pending condition narrows. +// One cached flow query: one ref at one flow node under one flow mode. // Example: "what is `x` at flow 42, with current guards applied?" struct FlowQuery { var_ref_id: VarRefId, @@ -52,7 +51,7 @@ impl FlowQuery { var_ref_id: var_ref_id.clone(), var_cache_idx: get_flow_cache_var_ref_id(cache, var_ref_id), flow_id, - mode: FlowMode::WithConditions, + mode: FlowMode::Normal, } } @@ -97,6 +96,7 @@ enum Continuation { AssignmentAntecedent { walk: QueryWalk, antecedent_flow_id: FlowId, + explicit_var_type: Option, expr_type: LuaType, reuse_antecedent_narrowing: bool, }, @@ -389,7 +389,7 @@ fn collect_expr_dependency_queries( // `StartQuery` begins one query, optionally saving the current query first. // `ContinueWalk` keeps scanning backward through the current query. // `ResumeNext(result)` pops one suspended query from `stack` and resumes it -// with the result of the dependency query that just finished. +// with the internal result of the dependency query that just finished. enum SchedulerStep { // Start or reuse one `(var_ref, flow_id, mode)` query. // If `continuation` is present, save that suspended query first so this @@ -408,9 +408,11 @@ enum SchedulerStep { // query result. // Example: after querying `shape.kind`, continue narrowing // `if shape.kind == "circle" then`. - ResumeNext(InferResult), + ResumeNext(FlowResult), } +type FlowResult = Result; + // Single owner of flow evaluation. Only this engine is allowed to schedule // follow-up queries, which keeps the flow path iterative. struct FlowTypeEngine<'a> { @@ -456,11 +458,13 @@ impl<'a> FlowTypeEngine<'a> { Some(Continuation::AssignmentAntecedent { walk, antecedent_flow_id, + explicit_var_type, expr_type, reuse_antecedent_narrowing, }) => self.resume_assignment_antecedent( walk, antecedent_flow_id, + explicit_var_type, expr_type, reuse_antecedent_narrowing, query_result, @@ -469,7 +473,12 @@ impl<'a> FlowTypeEngine<'a> { walk, replay, replay_query, - }) => self.resume_expr_replay(walk, replay, replay_query, query_result), + }) => self.resume_expr_replay( + walk, + replay, + replay_query, + query_result.map(FlowQueryResult::into_type), + ), Some(Continuation::TagCastAntecedent { walk, cast_op_types, @@ -484,7 +493,7 @@ impl<'a> FlowTypeEngine<'a> { flow_id, condition_flow, subquery, - query_result, + query_result.map(FlowQueryResult::into_type), ), Some(Continuation::FieldLiteralSiblingDependency { walk, @@ -496,7 +505,7 @@ impl<'a> FlowTypeEngine<'a> { flow_id, condition_flow, subquery, - query_result, + query_result.map(FlowQueryResult::into_type), ), Some(Continuation::CorrelatedSearchRoot { walk, @@ -510,12 +519,12 @@ impl<'a> FlowTypeEngine<'a> { advance_pending_correlated_condition( self.db, pending_correlated_condition, - query_result, + query_result.map(FlowQueryResult::into_type), ), ), // No suspended query is waiting on this result, so it is the // final answer for the original `run(...)` request. - None => break query_result, + None => break query_result.map(FlowQueryResult::into_type), }, } .unwrap_or_else(|err| SchedulerStep::ResumeNext(Err(err))); @@ -534,7 +543,7 @@ impl<'a> FlowTypeEngine<'a> { .and_then(|var_cache| var_cache.type_cache.get(&type_cache_key)) { Ok(SchedulerStep::ResumeNext(match cache_entry { - CacheEntry::Cache(narrow_type) => Ok(narrow_type.clone()), + CacheEntry::Cache(query_result) => Ok(query_result.clone()), CacheEntry::Ready => Err(InferFailReason::RecursiveInfer), })) } else { @@ -568,10 +577,10 @@ impl<'a> FlowTypeEngine<'a> { branch_flow_ids: Arc<[FlowId]>, next_pending_idx: usize, merged_type: LuaType, - branch_result: InferResult, + branch_result: FlowResult, ) -> Result { let branch_type = match branch_result { - Ok(branch_type) => branch_type, + Ok(branch_result) => branch_result.into_type(), Err(err) => return self.fail_query(&walk.query, err), }; @@ -583,10 +592,15 @@ impl<'a> FlowTypeEngine<'a> { // Branches are resumed from the end because the initial merge setup // schedules the last incoming branch first. let branch_idx = next_pending_idx - 1; + // The remaining predecessor is still a merge contribution. Preserve + // MergeBranch so an unreachable condition edge contributes `never` + // instead of continuing to the type from before the merge. + let branch_mode = match walk.query.mode { + FlowMode::Normal | FlowMode::MergeBranch => FlowMode::MergeBranch, + FlowMode::IgnoreConditions => FlowMode::IgnoreConditions, + }; Ok(SchedulerStep::StartQuery { - query: walk - .query - .at_flow(branch_flow_ids[branch_idx], walk.query.mode), + query: walk.query.at_flow(branch_flow_ids[branch_idx], branch_mode), continuation: Some(Continuation::Merge { walk, branch_flow_ids, @@ -604,26 +618,44 @@ impl<'a> FlowTypeEngine<'a> { &mut self, walk: QueryWalk, antecedent_flow_id: FlowId, + explicit_var_type: Option, expr_type: LuaType, reuse_antecedent_narrowing: bool, - antecedent_result: InferResult, + antecedent_result: FlowResult, ) -> Result { let antecedent_type = match antecedent_result { - Ok(antecedent_type) => antecedent_type, + Ok(FlowQueryResult::Type(antecedent_type)) => antecedent_type, + Ok(FlowQueryResult::Unreachable) => { + return Ok(self.finish_unreachable_branch(walk)); + } Err(err) => return self.fail_query(&walk.query, err), }; + if let Some(explicit_var_type) = explicit_var_type { + let result_type = finish_assignment_result( + self.db, + self.cache, + &explicit_var_type, + &expr_type, + &walk.query.var_ref_id, + true, + Some(explicit_var_type.clone()), + ); + return Ok(self.finish_walk(walk, result_type)); + } + if reuse_antecedent_narrowing && !can_reuse_narrowed_assignment_source(self.db, &antecedent_type, &expr_type) { let next_query = walk .query - .at_flow(antecedent_flow_id, FlowMode::WithoutConditions); + .at_flow(antecedent_flow_id, FlowMode::IgnoreConditions); return Ok(SchedulerStep::StartQuery { query: next_query, continuation: Some(Continuation::AssignmentAntecedent { walk, antecedent_flow_id, + explicit_var_type: None, expr_type, reuse_antecedent_narrowing: false, }), @@ -818,10 +850,13 @@ impl<'a> FlowTypeEngine<'a> { &mut self, walk: QueryWalk, cast_op_types: Vec, - antecedent_result: InferResult, + antecedent_result: FlowResult, ) -> Result { let mut cast_input_type = match antecedent_result { - Ok(resolved_type) => resolved_type, + Ok(FlowQueryResult::Type(resolved_type)) => resolved_type, + Ok(FlowQueryResult::Unreachable) => { + return Ok(self.finish_unreachable_branch(walk)); + } // `---@cast` is an explicit assertion, so unresolved source types // should still be narrowed by applying the cast from `unknown`. Err(_) => LuaType::Unknown, @@ -1100,6 +1135,24 @@ impl<'a> FlowTypeEngine<'a> { expr_type: LuaType, ) -> Result { if let Some(explicit_var_type) = explicit_var_type { + if matches!(walk.query.mode, FlowMode::MergeBranch) { + // The annotation still supplies the assignment source type, but + // this branch must first prove it can reach the assignment. + let subquery = walk + .query + .at_flow(antecedent_flow_id, FlowMode::MergeBranch); + return Ok(SchedulerStep::StartQuery { + query: subquery, + continuation: Some(Continuation::AssignmentAntecedent { + walk, + antecedent_flow_id, + explicit_var_type: Some(explicit_var_type), + expr_type, + reuse_antecedent_narrowing: true, + }), + }); + } + let var_ref_id = walk.query.var_ref_id.clone(); let result_type = finish_assignment_result( self.db, @@ -1116,14 +1169,17 @@ impl<'a> FlowTypeEngine<'a> { // Broad RHS types replace the previous runtime type. The old path still // queried the antecedent and then discarded it in finish_assignment_result. let reuse_antecedent_narrowing = preserves_assignment_expr_type(&expr_type); - if !expr_type.is_unknown() && !reuse_antecedent_narrowing { + if !expr_type.is_unknown() + && !reuse_antecedent_narrowing + && !matches!(walk.query.mode, FlowMode::MergeBranch) + { return Ok(self.finish_walk(walk, expr_type)); } - let mode = if reuse_antecedent_narrowing { - FlowMode::WithConditions - } else { - FlowMode::WithoutConditions + let mode = match (walk.query.mode, reuse_antecedent_narrowing) { + (FlowMode::MergeBranch, _) => FlowMode::MergeBranch, + (_, true) => FlowMode::Normal, + (_, false) => FlowMode::IgnoreConditions, }; let subquery = walk.query.at_flow(antecedent_flow_id, mode); Ok(SchedulerStep::StartQuery { @@ -1131,6 +1187,7 @@ impl<'a> FlowTypeEngine<'a> { continuation: Some(Continuation::AssignmentAntecedent { walk, antecedent_flow_id, + explicit_var_type: None, expr_type, reuse_antecedent_narrowing, }), @@ -1145,7 +1202,12 @@ impl<'a> FlowTypeEngine<'a> { err: InferFailReason, ) -> Result { if let Some(explicit_var_type) = explicit_var_type { - return Ok(self.finish_walk(walk, explicit_var_type)); + return self.finish_assignment_expr_type( + walk, + antecedent_flow_id, + Some(explicit_var_type), + LuaType::Unknown, + ); } let var_ref_id = walk.query.var_ref_id.clone(); @@ -1179,7 +1241,7 @@ impl<'a> FlowTypeEngine<'a> { condition_flow: InferConditionFlow, ) -> Result { let antecedent_flow_id = get_single_antecedent(flow_node)?; - if !walk.query.mode.uses_conditions() { + if matches!(walk.query.mode, FlowMode::IgnoreConditions) { walk.antecedent_flow_id = antecedent_flow_id; return Ok(SchedulerStep::ContinueWalk(walk)); } @@ -1233,7 +1295,10 @@ impl<'a> FlowTypeEngine<'a> { if cached_action { return match action { ConditionFlowAction::Continue => Ok(SchedulerStep::ContinueWalk(walk)), - ConditionFlowAction::Result(result_type) => Ok(self.finish_walk(walk, result_type)), + ConditionFlowAction::Unreachable => Ok(self.finish_unreachable_condition(walk)), + ConditionFlowAction::Result(result_type) => { + Ok(self.finish_condition_result(walk, result_type)) + } ConditionFlowAction::Pending(pending_condition_narrow) => { let mut walk = walk; walk.pending_condition_narrows @@ -1275,9 +1340,11 @@ impl<'a> FlowTypeEngine<'a> { } let antecedent_flow_id = get_single_antecedent(flow_node)?; - let subquery = walk - .query - .at_flow(antecedent_flow_id, FlowMode::WithConditions); + let mode = match walk.query.mode { + FlowMode::MergeBranch => FlowMode::MergeBranch, + FlowMode::Normal | FlowMode::IgnoreConditions => FlowMode::Normal, + }; + let subquery = walk.query.at_flow(antecedent_flow_id, mode); Ok(SchedulerStep::StartQuery { query: subquery, continuation: Some(Continuation::TagCastAntecedent { @@ -1315,13 +1382,28 @@ impl<'a> FlowTypeEngine<'a> { } else { Arc::<[FlowId]>::from(get_multi_antecedents(self.tree, flow_node)?) }; - let Some(next_pending_idx) = branch_flow_ids.len().checked_sub(1) else { - return Ok(self.finish_walk(walk, LuaType::Never)); - }; + match branch_flow_ids.len() { + 0 => return Ok(self.finish_unreachable_branch(walk)), + // A single predecessor is just a branch-entry label, not a merge. + // Keep the current mode so queries inside an impossible then/else + // body can still resolve unrelated symbols from their declarations. + 1 => { + walk.antecedent_flow_id = branch_flow_ids[0]; + continue; + } + _ => {} + } + let next_pending_idx = branch_flow_ids.len() - 1; let q = &walk.query; - let next_query = q.at_flow(branch_flow_ids[next_pending_idx], q.mode); + // Multiple predecessors make this a merge. Query each + // predecessor as a merge contribution so an unreachable + // condition edge contributes `never` to the final union. + let branch_mode = match q.mode { + FlowMode::Normal | FlowMode::MergeBranch => FlowMode::MergeBranch, + FlowMode::IgnoreConditions => FlowMode::IgnoreConditions, + }; return Ok(SchedulerStep::StartQuery { - query: next_query, + query: q.at_flow(branch_flow_ids[next_pending_idx], branch_mode), continuation: Some(Continuation::Merge { walk, branch_flow_ids, @@ -1453,6 +1535,15 @@ impl<'a> FlowTypeEngine<'a> { ); Ok(SchedulerStep::ContinueWalk(walk)) } + ConditionFlowAction::Unreachable => { + get_flow_var_cache(self.cache, walk.query.var_cache_idx) + .condition_cache + .insert( + (flow_id, condition_flow), + CacheEntry::Cache(ConditionFlowAction::Unreachable), + ); + Ok(self.finish_unreachable_condition(walk)) + } ConditionFlowAction::Result(result_type) => { get_flow_var_cache(self.cache, walk.query.var_cache_idx) .condition_cache @@ -1460,7 +1551,7 @@ impl<'a> FlowTypeEngine<'a> { (flow_id, condition_flow), CacheEntry::Cache(ConditionFlowAction::Result(result_type.clone())), ); - Ok(self.finish_walk(walk, result_type)) + Ok(self.finish_condition_result(walk, result_type)) } ConditionFlowAction::Pending(pending_condition_narrow) => self.start_pending_condition( walk, @@ -1498,9 +1589,13 @@ impl<'a> FlowTypeEngine<'a> { self.start_field_literal_sibling_subquery(walk, flow_id, condition_flow, subquery) ), ConditionFlowAction::NeedCorrelated(pending_correlated_condition) => { + let mode = match walk.query.mode { + FlowMode::MergeBranch => FlowMode::MergeBranch, + FlowMode::Normal | FlowMode::IgnoreConditions => FlowMode::Normal, + }; let subquery = walk.query.at_flow( pending_correlated_condition.current_search_root_flow_id, - FlowMode::WithConditions, + mode, ); Ok(SchedulerStep::StartQuery { query: subquery, @@ -1515,6 +1610,39 @@ impl<'a> FlowTypeEngine<'a> { } } + fn finish_unreachable_condition(&mut self, walk: QueryWalk) -> SchedulerStep { + // Unreachable describes the condition edge, not the queried symbol. A + // merge query asks what this branch contributes, so the answer is + // `never`; point queries continue to the antecedent so unrelated + // symbols inside impossible branches keep their declared types. + match walk.query.mode { + FlowMode::MergeBranch => self.finish_unreachable_branch(walk), + FlowMode::Normal | FlowMode::IgnoreConditions => SchedulerStep::ContinueWalk(walk), + } + } + + fn finish_condition_result(&mut self, walk: QueryWalk, result_type: LuaType) -> SchedulerStep { + // Condition results describe a guard edge. If the guard removes every + // possible type while calculating a merge contribution, the branch is + // unreachable rather than an ordinary `never`-typed value. + if matches!(walk.query.mode, FlowMode::MergeBranch) && result_type.is_never() { + self.finish_unreachable_branch(walk) + } else { + self.finish_walk(walk, result_type) + } + } + + fn finish_unreachable_branch(&mut self, walk: QueryWalk) -> SchedulerStep { + let query = walk.query; + get_flow_var_cache(self.cache, query.var_cache_idx) + .type_cache + .insert( + (query.flow_id, query.mode), + CacheEntry::Cache(FlowQueryResult::Unreachable), + ); + SchedulerStep::ResumeNext(Ok(FlowQueryResult::Unreachable)) + } + fn finish_walk(&mut self, walk: QueryWalk, narrow_type: LuaType) -> SchedulerStep { let QueryWalk { query, @@ -1522,18 +1650,28 @@ impl<'a> FlowTypeEngine<'a> { .. } = walk; let mut final_type = narrow_type; - if query.mode.uses_conditions() { + let mut condition_made_unreachable = false; + if !matches!(query.mode, FlowMode::IgnoreConditions) { for pending_condition_narrow in pending_condition_narrows.into_iter().rev() { + let was_possible = !final_type.is_never(); final_type = pending_condition_narrow.apply(self.db, self.cache, final_type); + condition_made_unreachable |= matches!(query.mode, FlowMode::MergeBranch) + && was_possible + && final_type.is_never(); } } + let query_result = if condition_made_unreachable { + FlowQueryResult::Unreachable + } else { + FlowQueryResult::Type(final_type) + }; get_flow_var_cache(self.cache, query.var_cache_idx) .type_cache .insert( (query.flow_id, query.mode), - CacheEntry::Cache(final_type.clone()), + CacheEntry::Cache(query_result.clone()), ); - SchedulerStep::ResumeNext(Ok(final_type)) + SchedulerStep::ResumeNext(Ok(query_result)) } fn fail_query(