Skip to content

Commit 7beb381

Browse files
committed
fix(semantic): make deep flow narrowing iterative and fast
Issue 1028 exposes a pathological narrowing path. Deep guard chains drive semantic token analysis through recursive `get_type_at_flow` branch and condition evaluation, which can overflow the thread stack and repeatedly rebuild the same flow state for repeated index lookups. Rework the flow narrowing hot path so it stays stack safe and fast on those inputs. What changed: - replace recursive branch-merge evaluation in `get_type_at_flow` with an explicit frame stack so deep `BranchLabel` chains no longer recurse on the thread stack - assign each queried `VarRefId` a dense cache id and store hot flow results in direct `[var_ref][flow][condition]` slots instead of hashing `(VarRefId, FlowId, bool)` keys on every step - flatten `BranchLabel` antecedents once per merge and cache the flattened branch lists for reuse across queries - cache parsed flow assignment and flow condition metadata per `FlowId` so assignment var refs and index condition refs are resolved once and reused - share the flow condition helper between `infer_index` and narrowing so the direct index precheck and the main evaluator use the same decoded condition data - add a direct index flow-effect precheck in `infer_index` so repeated index accesses skip full narrowing when backward flow cannot affect the queried index ref - keep assignment source reuse conservative for partial table and object literals so branch-local narrowing behavior stays intact Fixes EmmyLuaLs#1028
1 parent ee55df9 commit 7beb381

7 files changed

Lines changed: 1031 additions & 222 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: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
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::{rc::Rc, sync::Arc};
97

108
use crate::{
119
FileId, FlowId, LuaFunctionType,
@@ -19,16 +17,35 @@ 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+
2234
#[derive(Debug)]
2335
pub struct LuaInferCache {
2436
file_id: FileId,
2537
config: CacheOptions,
2638
pub expr_cache: HashMap<LuaSyntaxId, CacheEntry<LuaType>>,
2739
pub call_cache:
2840
HashMap<(LuaSyntaxId, Option<usize>, LuaType), CacheEntry<Arc<LuaFunctionType>>>,
29-
pub(crate) flow_node_cache: HashMap<(VarRefId, FlowId, bool), CacheEntry<LuaType>>,
41+
pub(in crate::semantic) flow_cache_var_ref_ids: HashMap<VarRefId, u32>,
42+
pub(in crate::semantic) next_flow_cache_var_ref_id: u32,
43+
pub(crate) flow_node_cache: Vec<HashMap<u32, [Option<CacheEntry<LuaType>>; 2]>>,
44+
pub(in crate::semantic) flow_branch_antecedent_cache: Vec<Option<Rc<Vec<FlowId>>>>,
45+
pub(in crate::semantic) flow_condition_info_cache: Vec<Option<Rc<FlowConditionInfo>>>,
46+
pub(in crate::semantic) flow_assignment_info_cache: Vec<Option<Rc<FlowAssignmentInfo>>>,
3047
pub(in crate::semantic) condition_flow_cache:
31-
HashMap<(VarRefId, FlowId, bool), CacheEntry<ConditionFlowAction>>,
48+
Vec<HashMap<u32, [Option<CacheEntry<ConditionFlowAction>>; 2]>>,
3249
pub index_ref_origin_type_cache: HashMap<VarRefId, CacheEntry<LuaType>>,
3350
pub expr_var_ref_id_cache: HashMap<LuaSyntaxId, VarRefId>,
3451
pub narrow_by_literal_stop_position_cache: HashSet<LuaSyntaxId>,
@@ -41,8 +58,13 @@ impl LuaInferCache {
4158
config,
4259
expr_cache: HashMap::new(),
4360
call_cache: HashMap::new(),
44-
flow_node_cache: HashMap::new(),
45-
condition_flow_cache: HashMap::new(),
61+
flow_cache_var_ref_ids: HashMap::new(),
62+
next_flow_cache_var_ref_id: 0,
63+
flow_node_cache: Vec::new(),
64+
flow_branch_antecedent_cache: Vec::new(),
65+
flow_condition_info_cache: Vec::new(),
66+
flow_assignment_info_cache: Vec::new(),
67+
condition_flow_cache: Vec::new(),
4668
index_ref_origin_type_cache: HashMap::new(),
4769
expr_var_ref_id_cache: HashMap::new(),
4870
narrow_by_literal_stop_position_cache: HashSet::new(),
@@ -64,7 +86,12 @@ impl LuaInferCache {
6486
pub fn clear(&mut self) {
6587
self.expr_cache.clear();
6688
self.call_cache.clear();
89+
self.flow_cache_var_ref_ids.clear();
90+
self.next_flow_cache_var_ref_id = 0;
6791
self.flow_node_cache.clear();
92+
self.flow_branch_antecedent_cache.clear();
93+
self.flow_condition_info_cache.clear();
94+
self.flow_assignment_info_cache.clear();
6895
self.condition_flow_cache.clear();
6996
self.index_ref_origin_type_cache.clear();
7097
self.expr_var_ref_id_cache.clear();

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

Lines changed: 131 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
mod infer_array;
22

3-
use std::collections::HashSet;
4-
53
use emmylua_parser::{
6-
LuaExpr, LuaIndexExpr, LuaIndexKey, LuaIndexMemberExpr, NumberResult, PathTrait,
4+
LuaAstNode, LuaChunk, LuaExpr, LuaIndexExpr, LuaIndexKey, LuaIndexMemberExpr, NumberResult,
5+
PathTrait,
76
};
7+
use hashbrown::HashSet;
88
use internment::ArcIntern;
99
use rowan::TextRange;
1010
use smol_str::SmolStr;
1111

1212
use crate::{
13-
CacheEntry, GenericTpl, InFiled, InferGuardRef, LuaAliasCallKind, LuaDeclOrMemberId,
14-
LuaInferCache, LuaInstanceType, LuaMemberOwner, LuaOperatorOwner, TypeOps,
13+
CacheEntry, FlowAntecedent, FlowId, FlowNode, FlowNodeKind, FlowTree, GenericTpl, InFiled,
14+
InferGuardRef, LuaAliasCallKind, LuaDeclOrMemberId, LuaInferCache, LuaInstanceType,
15+
LuaMemberOwner, LuaOperatorOwner, TypeOps,
1516
db_index::{
1617
DbIndex, LuaGenericType, LuaIntersectionType, LuaMemberKey, LuaObjectType,
1718
LuaOperatorMetaMethod, LuaTupleType, LuaType, LuaTypeDeclId, LuaUnionType,
@@ -24,7 +25,11 @@ use crate::{
2425
VarRefId,
2526
infer_index::infer_array::{check_iter_var_range, infer_array_member},
2627
infer_name::get_name_expr_var_ref_id,
27-
narrow::infer_expr_narrow_type,
28+
narrow::{
29+
ConditionFlowAction, InferConditionFlow, get_condition_flow_action,
30+
get_flow_assignment_info, get_flow_cache_var_ref_id, get_flow_condition_info,
31+
get_var_expr_var_ref_id, infer_expr_narrow_type,
32+
},
2833
},
2934
member::get_buildin_type_map_type_id,
3035
member::intersect_member_types,
@@ -107,13 +112,133 @@ fn infer_member_type_pass_flow(
107112
cache
108113
.index_ref_origin_type_cache
109114
.insert(var_ref_id.clone(), CacheEntry::Cache(member_type.clone()));
115+
let file_id = cache.get_file_id();
116+
if let Some(flow_tree) = db.get_flow_index().get_flow_tree(&file_id)
117+
&& let Some(flow_id) = flow_tree.get_flow_id(index_expr.get_syntax_id())
118+
&& let Some(root) = LuaChunk::cast(index_expr.get_root())
119+
&& matches!(
120+
has_direct_index_flow_effect(db, cache, flow_tree, &root, &var_ref_id, flow_id),
121+
Ok(false)
122+
)
123+
{
124+
return Ok(member_type);
125+
}
126+
110127
let result = infer_expr_narrow_type(db, cache, LuaExpr::IndexExpr(index_expr), var_ref_id);
111128
match &result {
112129
Err(InferFailReason::None) => Ok(member_type.clone()),
113130
_ => result,
114131
}
115132
}
116133

134+
fn has_direct_index_flow_effect(
135+
db: &DbIndex,
136+
cache: &mut LuaInferCache,
137+
tree: &FlowTree,
138+
root: &LuaChunk,
139+
var_ref_id: &VarRefId,
140+
start_flow_id: FlowId,
141+
) -> Result<bool, InferFailReason> {
142+
let mut pending = vec![start_flow_id];
143+
let mut visited_labels = HashSet::new();
144+
let var_ref_cache_id = get_flow_cache_var_ref_id(cache, var_ref_id);
145+
146+
while let Some(flow_id) = pending.pop() {
147+
let flow_node = tree.get_flow_node(flow_id).ok_or(InferFailReason::None)?;
148+
match &flow_node.kind {
149+
FlowNodeKind::Start | FlowNodeKind::Unreachable => {}
150+
FlowNodeKind::BranchLabel | FlowNodeKind::NamedLabel(_) => {
151+
if visited_labels.insert(flow_id) {
152+
extend_flow_antecedents(tree, flow_node, &mut pending)?;
153+
}
154+
}
155+
FlowNodeKind::Assignment(assign_ptr) => {
156+
let assignment_info =
157+
get_flow_assignment_info(db, cache, root, flow_node.id, assign_ptr)?;
158+
if assignment_info
159+
.var_ref_ids
160+
.iter()
161+
.flatten()
162+
.any(|assignment_var_ref_id| assignment_var_ref_id == var_ref_id)
163+
{
164+
return Ok(true);
165+
}
166+
extend_flow_antecedents(tree, flow_node, &mut pending)?;
167+
}
168+
FlowNodeKind::TrueCondition(condition_ptr)
169+
| FlowNodeKind::FalseCondition(condition_ptr) => {
170+
let condition_info =
171+
get_flow_condition_info(db, cache, root, flow_node.id, condition_ptr)?;
172+
let condition_flow = if matches!(&flow_node.kind, FlowNodeKind::TrueCondition(_)) {
173+
InferConditionFlow::TrueCondition
174+
} else {
175+
InferConditionFlow::FalseCondition
176+
};
177+
let condition_action = get_condition_flow_action(
178+
db,
179+
tree,
180+
cache,
181+
root,
182+
var_ref_id,
183+
var_ref_cache_id,
184+
flow_node,
185+
&condition_info,
186+
condition_flow,
187+
)?;
188+
if !matches!(condition_action, ConditionFlowAction::Continue) {
189+
return Ok(true);
190+
}
191+
extend_flow_antecedents(tree, flow_node, &mut pending)?;
192+
}
193+
FlowNodeKind::ImplFunc(func_ptr) => {
194+
let func_stat = func_ptr.to_node(root).ok_or(InferFailReason::None)?;
195+
if let Some(func_name) = func_stat.get_func_name()
196+
&& get_var_expr_var_ref_id(db, cache, func_name.to_expr()).as_ref()
197+
== Some(var_ref_id)
198+
{
199+
return Ok(true);
200+
}
201+
extend_flow_antecedents(tree, flow_node, &mut pending)?;
202+
}
203+
FlowNodeKind::TagCast(tag_cast_ptr) => {
204+
let tag_cast = tag_cast_ptr.to_node(root).ok_or(InferFailReason::None)?;
205+
if let Some(key_expr) = tag_cast.get_key_expr()
206+
&& get_var_expr_var_ref_id(db, cache, key_expr).as_ref() == Some(var_ref_id)
207+
{
208+
return Ok(true);
209+
}
210+
extend_flow_antecedents(tree, flow_node, &mut pending)?;
211+
}
212+
FlowNodeKind::LoopLabel
213+
| FlowNodeKind::DeclPosition(_)
214+
| FlowNodeKind::ForIStat(_)
215+
| FlowNodeKind::Break
216+
| FlowNodeKind::Return => {
217+
extend_flow_antecedents(tree, flow_node, &mut pending)?;
218+
}
219+
}
220+
}
221+
222+
Ok(false)
223+
}
224+
225+
fn extend_flow_antecedents(
226+
tree: &FlowTree,
227+
flow_node: &FlowNode,
228+
pending: &mut Vec<FlowId>,
229+
) -> Result<(), InferFailReason> {
230+
match flow_node.antecedent.as_ref() {
231+
Some(FlowAntecedent::Single(flow_id)) => pending.push(*flow_id),
232+
Some(FlowAntecedent::Multiple(multi_id)) => pending.extend(
233+
tree.get_multi_antecedents(*multi_id)
234+
.ok_or(InferFailReason::None)?,
235+
),
236+
None => {}
237+
}
238+
239+
Ok(())
240+
}
241+
117242
pub fn get_index_expr_var_ref_id(
118243
db: &DbIndex,
119244
cache: &mut LuaInferCache,

0 commit comments

Comments
 (0)