Skip to content

Commit 231495e

Browse files
committed
fix(flow): defer stacked condition narrowing during flow walks
When `get_type_at_flow()` walked backward through a chain of condition nodes, each guard tried to resolve its predecessor immediately. That worked for shallow control flow, but repeated correlated `@return_overload` guards such as: - `if not ok then error(result) end` - `if ok == false then error(result) end` kept re-entering condition inference for the same sibling return values before the walk had reached a stable base type. In large enough chains that caused semantic model construction to recurse until the stack overflowed. Restructure condition narrowing so the flow walker stays iterative across stacked guards. The new shape is: - condition handlers return a `ConditionFlowAction` - a condition can either: - produce an immediate narrowed type - emit a deferred `PendingConditionNarrow` - or continue walking without narrowing - `get_type_at_flow()` collects pending narrows while it traverses antecedents and applies them in reverse order once the underlying source type is known This keeps stacked truthiness, `==` / `~=`, type-guard, and correlated return-overload narrowing on the main iterative flow walk instead of resolving each predecessor recursively. Correlated return-overload narrowing is also split into prepare/apply phases. `prepare_var_from_return_overload_condition()` now computes a `CorrelatedConditionNarrowing` description up front, and the final intersection with the target antecedent type happens later when the pending narrows are applied. That preserves the existing correlated-vs-uncorrelated merge behavior without reopening the same flow question during predecessor traversal. Add an explicit "ignore condition narrowing" mode for assignment source lookup. This matters when a variable is reassigned from a fresh multi-return call after an earlier correlated guard: the assignment source should be inferred from the underlying flow state, not from stale pending narrows gathered before the new assignment. Keep the direct `>` / `>=` narrowing path intact and limit the deferred prepass to the condition forms that need it, so the change stays focused on the stacked guard recursion problem.
1 parent 3e0bb16 commit 231495e

5 files changed

Lines changed: 646 additions & 162 deletions

File tree

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

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
mod test {
33
use crate::{DiagnosticCode, VirtualWorkspace};
44

5+
const STACKED_CORRELATED_GUARDS: usize = 180;
6+
57
#[test]
68
fn test_return_overload_narrow_after_not() {
79
let mut ws = VirtualWorkspace::new();
@@ -541,4 +543,168 @@ mod test {
541543
assert!(after_guard.contains("integer"));
542544
assert!(!after_guard.contains("string"));
543545
}
546+
547+
#[test]
548+
fn test_return_overload_stacked_same_discriminant_guards_build_semantic_model() {
549+
let mut ws = VirtualWorkspace::new();
550+
let repeated_guards =
551+
"if not ok then error(result) end\n".repeat(STACKED_CORRELATED_GUARDS);
552+
let block = format!(
553+
r#"
554+
---@generic T, E
555+
---@param ok boolean
556+
---@param success T
557+
---@param failure E
558+
---@return boolean
559+
---@return T|E
560+
---@return_overload true, T
561+
---@return_overload false, E
562+
local function pick(ok, success, failure)
563+
if ok then
564+
return true, success
565+
end
566+
return false, failure
567+
end
568+
569+
local cond ---@type boolean
570+
local ok, result = pick(cond, 1, "error")
571+
572+
{repeated_guards}
573+
narrowed = result
574+
"#,
575+
);
576+
577+
let file_id = ws.def(&block);
578+
579+
assert!(
580+
ws.analysis
581+
.compilation
582+
.get_semantic_model(file_id)
583+
.is_some(),
584+
"expected semantic model for stacked correlated-guard repro"
585+
);
586+
assert_eq!(ws.expr_ty("narrowed"), ws.ty("integer"));
587+
}
588+
589+
#[test]
590+
fn test_return_overload_stacked_eq_guards_build_semantic_model() {
591+
let mut ws = VirtualWorkspace::new();
592+
let repeated_guards =
593+
"if ok == false then error(result) end\n".repeat(STACKED_CORRELATED_GUARDS);
594+
let block = format!(
595+
r#"
596+
---@generic T, E
597+
---@param ok boolean
598+
---@param success T
599+
---@param failure E
600+
---@return boolean
601+
---@return T|E
602+
---@return_overload true, T
603+
---@return_overload false, E
604+
local function pick(ok, success, failure)
605+
if ok then
606+
return true, success
607+
end
608+
return false, failure
609+
end
610+
611+
local cond ---@type boolean
612+
local ok, result = pick(cond, 1, "error")
613+
614+
{repeated_guards}
615+
narrowed = result
616+
"#,
617+
);
618+
619+
let file_id = ws.def(&block);
620+
621+
assert!(
622+
ws.analysis
623+
.compilation
624+
.get_semantic_model(file_id)
625+
.is_some(),
626+
"expected semantic model for stacked correlated-eq repro"
627+
);
628+
assert_eq!(ws.expr_ty("narrowed"), ws.ty("integer"));
629+
}
630+
631+
#[test]
632+
fn test_return_overload_uncorrelated_later_guard_keeps_prior_narrowing() {
633+
let mut ws = VirtualWorkspace::new();
634+
635+
ws.def(
636+
r#"
637+
---@generic T, E
638+
---@param ok boolean
639+
---@param success T
640+
---@param failure E
641+
---@return boolean
642+
---@return T|E
643+
---@return_overload true, T
644+
---@return_overload false, E
645+
local function pick(ok, success, failure)
646+
if ok then
647+
return true, success
648+
end
649+
return false, failure
650+
end
651+
652+
local cond ---@type boolean
653+
local ok, result = pick(cond, 1, "err")
654+
655+
if not ok then
656+
error(result)
657+
end
658+
659+
ok = cond
660+
661+
if not ok then
662+
error(result)
663+
end
664+
665+
narrowed = result
666+
"#,
667+
);
668+
669+
assert_eq!(ws.expr_ty("narrowed"), ws.ty("integer"));
670+
}
671+
672+
#[test]
673+
fn test_return_overload_reassign_from_fresh_call_ignores_prior_guard() {
674+
let mut ws = VirtualWorkspace::new();
675+
676+
ws.def(
677+
r#"
678+
---@generic T, E
679+
---@param ok boolean
680+
---@param success T
681+
---@param failure E
682+
---@return boolean
683+
---@return T|E
684+
---@return_overload true, T
685+
---@return_overload false, E
686+
local function pick(ok, success, failure)
687+
if ok then
688+
return true, success
689+
end
690+
return false, failure
691+
end
692+
693+
local cond ---@type boolean
694+
local branch ---@type boolean
695+
local ok, result = pick(cond, 1, "err")
696+
697+
if not ok then
698+
error(result)
699+
end
700+
701+
if branch then
702+
ok, result = pick(cond, "x", 2)
703+
narrowed = result
704+
end
705+
"#,
706+
);
707+
708+
assert_eq!(ws.expr_ty("narrowed"), ws.ty("integer|string"));
709+
}
544710
}

0 commit comments

Comments
 (0)