Skip to content

Commit 6ac29b6

Browse files
committed
fix(semantic): make flow narrowing iterative
Replace recursive flow evaluation with an explicit query stack so deep branch and condition chains stay stack-safe. Cache per-var flow lookups, flattened branch inputs, and decoded flow metadata to avoid rebuilding the same narrowing state on repeated queries, and add focused regressions around issue #1028, correlated guards, and index-based narrows. Fixes #1028
1 parent 420edbc commit 6ac29b6

14 files changed

Lines changed: 1811 additions & 939 deletions

File tree

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1866,6 +1866,27 @@ end
18661866
assert_eq!(b, b_expected);
18671867
}
18681868

1869+
#[test]
1870+
fn test_feature_const_local_alias_chain_does_not_inherit_flow() {
1871+
let mut ws = VirtualWorkspace::new_with_init_std_lib();
1872+
1873+
ws.def(
1874+
r#"
1875+
local ret --- @type string | nil
1876+
1877+
local is_string = type(ret) == "string"
1878+
local ok = is_string
1879+
if ok then
1880+
a = ret
1881+
end
1882+
"#,
1883+
);
1884+
1885+
let a = ws.expr_ty("a");
1886+
let a_expected = ws.ty("string?");
1887+
assert_eq!(a, a_expected);
1888+
}
1889+
18691890
#[test]
18701891
fn test_feature_generic_type_guard() {
18711892
let mut ws = VirtualWorkspace::new();

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,99 @@ mod test {
368368

369369
assert_eq!(ws.expr_ty("result"), ws.ty("never"));
370370
}
371+
372+
#[test]
373+
fn test_rawget_guard_narrows_matching_index_expr() {
374+
let mut ws = VirtualWorkspace::new_with_init_std_lib();
375+
376+
ws.def(
377+
r#"
378+
---@class T
379+
---@field x? integer
380+
381+
---@type T
382+
local t = {}
383+
384+
if rawget(t, "x") then
385+
result = t.x
386+
end
387+
"#,
388+
);
389+
390+
assert_eq!(ws.expr_ty("result"), LuaType::Integer);
391+
}
392+
393+
#[test]
394+
fn test_type_guard_call_narrows_matching_index_expr() {
395+
let mut ws = VirtualWorkspace::new();
396+
397+
ws.def(
398+
r#"
399+
---@generic T
400+
---@param inst any
401+
---@param type `T`
402+
---@return TypeGuard<T>
403+
local function instance_of(inst, type)
404+
return true
405+
end
406+
407+
---@class T
408+
---@field x? string|integer
409+
410+
---@type T
411+
local t = {}
412+
413+
if instance_of(t.x, "string") then
414+
result = t.x
415+
end
416+
"#,
417+
);
418+
419+
assert_eq!(ws.expr_ty("result"), LuaType::String);
420+
}
421+
422+
#[test]
423+
fn test_alias_predicate_guard_narrows_matching_index_expr() {
424+
let mut ws = VirtualWorkspace::new();
425+
426+
ws.def(
427+
r#"
428+
---@class T
429+
---@field x? integer
430+
431+
---@type T
432+
local t = {}
433+
434+
local ok = t.x ~= nil
435+
if ok then
436+
result = t.x
437+
end
438+
"#,
439+
);
440+
441+
assert_eq!(ws.expr_ty("result"), LuaType::Integer);
442+
}
443+
444+
#[test]
445+
fn test_alias_chain_predicate_guard_keeps_matching_index_expr_wide() {
446+
let mut ws = VirtualWorkspace::new();
447+
448+
ws.def(
449+
r#"
450+
---@class T
451+
---@field x? integer
452+
453+
---@type T
454+
local t = {}
455+
456+
local has_x = t.x ~= nil
457+
local ok = has_x
458+
if ok then
459+
result = t.x
460+
end
461+
"#,
462+
);
463+
464+
assert_eq!(ws.expr_ty("result"), ws.ty("integer?"));
465+
}
371466
}

crates/emmylua_code_analysis/src/semantic/cache/mod.rs

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
mod cache_options;
22

33
pub use cache_options::{CacheOptions, LuaAnalysisPhase};
4-
use emmylua_parser::LuaSyntaxId;
5-
use std::{
6-
collections::{HashMap, HashSet},
7-
sync::Arc,
8-
};
4+
use emmylua_parser::{LuaExpr, LuaSyntaxId, LuaVarExpr};
5+
use hashbrown::{HashMap, HashSet};
6+
use std::sync::Arc;
97

108
use crate::{
119
FileId, FlowId, LuaFunctionType,
1210
db_index::LuaType,
13-
semantic::infer::{ConditionFlowAction, VarRefId},
11+
semantic::infer::{ConditionFlowAction, InferConditionFlow, VarRefId},
1412
};
1513

1614
#[derive(Debug)]
@@ -19,16 +17,51 @@ pub enum CacheEntry<T> {
1917
Cache(T),
2018
}
2119

20+
#[derive(Debug, Clone)]
21+
pub(in crate::semantic) struct FlowConditionInfo {
22+
pub expr: LuaExpr,
23+
pub index_var_ref_id: Option<VarRefId>,
24+
pub index_prefix_var_ref_id: Option<VarRefId>,
25+
}
26+
27+
#[derive(Debug, Clone)]
28+
pub(in crate::semantic) struct FlowAssignmentInfo {
29+
pub vars: Vec<LuaVarExpr>,
30+
pub exprs: Vec<LuaExpr>,
31+
pub var_ref_ids: Vec<Option<VarRefId>>,
32+
}
33+
34+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35+
pub(in crate::semantic) enum FlowMode {
36+
WithConditions,
37+
WithoutConditions,
38+
}
39+
40+
impl FlowMode {
41+
pub fn uses_conditions(self) -> bool {
42+
matches!(self, Self::WithConditions)
43+
}
44+
}
45+
46+
#[derive(Debug, Default)]
47+
pub(in crate::semantic) struct FlowVarCache {
48+
pub type_cache: HashMap<(FlowId, FlowMode), CacheEntry<LuaType>>,
49+
pub condition_cache: HashMap<(FlowId, InferConditionFlow), CacheEntry<ConditionFlowAction>>,
50+
}
51+
2252
#[derive(Debug)]
2353
pub struct LuaInferCache {
2454
file_id: FileId,
2555
config: CacheOptions,
2656
pub expr_cache: HashMap<LuaSyntaxId, CacheEntry<LuaType>>,
2757
pub call_cache:
2858
HashMap<(LuaSyntaxId, Option<usize>, LuaType), CacheEntry<Arc<LuaFunctionType>>>,
29-
pub(crate) flow_node_cache: HashMap<(VarRefId, FlowId, bool), CacheEntry<LuaType>>,
30-
pub(in crate::semantic) condition_flow_cache:
31-
HashMap<(VarRefId, FlowId, bool), CacheEntry<ConditionFlowAction>>,
59+
pub(in crate::semantic) flow_cache_var_ref_ids: HashMap<VarRefId, u32>,
60+
pub(in crate::semantic) next_flow_cache_var_ref_id: u32,
61+
pub(in crate::semantic) flow_var_caches: Vec<FlowVarCache>,
62+
pub(in crate::semantic) flow_branch_inputs_cache: Vec<Option<Arc<[FlowId]>>>,
63+
pub(in crate::semantic) flow_condition_info_cache: Vec<Option<Arc<FlowConditionInfo>>>,
64+
pub(in crate::semantic) flow_assignment_info_cache: Vec<Option<Arc<FlowAssignmentInfo>>>,
3265
pub index_ref_origin_type_cache: HashMap<VarRefId, CacheEntry<LuaType>>,
3366
pub expr_var_ref_id_cache: HashMap<LuaSyntaxId, VarRefId>,
3467
pub narrow_by_literal_stop_position_cache: HashSet<LuaSyntaxId>,
@@ -41,8 +74,12 @@ impl LuaInferCache {
4174
config,
4275
expr_cache: HashMap::new(),
4376
call_cache: HashMap::new(),
44-
flow_node_cache: HashMap::new(),
45-
condition_flow_cache: HashMap::new(),
77+
flow_cache_var_ref_ids: HashMap::new(),
78+
next_flow_cache_var_ref_id: 0,
79+
flow_var_caches: Vec::new(),
80+
flow_branch_inputs_cache: Vec::new(),
81+
flow_condition_info_cache: Vec::new(),
82+
flow_assignment_info_cache: Vec::new(),
4683
index_ref_origin_type_cache: HashMap::new(),
4784
expr_var_ref_id_cache: HashMap::new(),
4885
narrow_by_literal_stop_position_cache: HashSet::new(),
@@ -64,8 +101,12 @@ impl LuaInferCache {
64101
pub fn clear(&mut self) {
65102
self.expr_cache.clear();
66103
self.call_cache.clear();
67-
self.flow_node_cache.clear();
68-
self.condition_flow_cache.clear();
104+
self.flow_cache_var_ref_ids.clear();
105+
self.next_flow_cache_var_ref_id = 0;
106+
self.flow_var_caches.clear();
107+
self.flow_branch_inputs_cache.clear();
108+
self.flow_condition_info_cache.clear();
109+
self.flow_assignment_info_cache.clear();
69110
self.index_ref_origin_type_cache.clear();
70111
self.expr_var_ref_id_cache.clear();
71112
}

crates/emmylua_code_analysis/src/semantic/infer/infer_index/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
mod infer_array;
22

3-
use std::collections::HashSet;
4-
53
use emmylua_parser::{
64
LuaExpr, LuaIndexExpr, LuaIndexKey, LuaIndexMemberExpr, NumberResult, PathTrait,
75
};
6+
use hashbrown::HashSet;
87
use internment::ArcIntern;
98
use rowan::TextRange;
109
use smol_str::SmolStr;
@@ -107,6 +106,7 @@ fn infer_member_type_pass_flow(
107106
cache
108107
.index_ref_origin_type_cache
109108
.insert(var_ref_id.clone(), CacheEntry::Cache(member_type.clone()));
109+
110110
let result = infer_expr_narrow_type(db, cache, LuaExpr::IndexExpr(index_expr), var_ref_id);
111111
match &result {
112112
Err(InferFailReason::None) => Ok(member_type.clone()),

crates/emmylua_code_analysis/src/semantic/infer/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ pub use infer_name::{find_self_decl_or_member_id, infer_param};
2626
use infer_table::infer_table_expr;
2727
pub use infer_table::{infer_table_field_value_should_be, infer_table_should_be};
2828
use infer_unary::infer_unary_expr;
29-
pub(in crate::semantic) use narrow::ConditionFlowAction;
3029
pub use narrow::VarRefId;
30+
pub(in crate::semantic) use narrow::{ConditionFlowAction, InferConditionFlow};
3131

3232
use rowan::TextRange;
3333
use smol_str::SmolStr;

0 commit comments

Comments
 (0)