Skip to content

Commit 6297472

Browse files
committed
fix(semantic): reuse branch-narrowed types in expression inference
Flow narrowing sometimes has to evaluate another expression that uses a value narrowed earlier in the same branch. For example, after `key = "foo"`, `t[key]` should be checked with `key` as `"foo"`, not with its declared `"foo"|"bar"` type. The old fallback avoided recursive flow analysis by evaluating those expressions without flow. That kept the engine from re-entering itself, but it also lost precision in assignment RHS expressions, equality checks, field guards, dynamic table keys, and guarded call or callee aliases. This keeps the flow engine responsible for the ordering. It records work that depends on another expression, resolves the branch-local values first, then resumes the original expression with those values layered over the normal no-flow inputs. Static caches and declared/member types still apply, but temporary expression and call results do not leak into the normal no-flow caches. Values that cannot be evaluated safely without flow now return no answer instead of caching broad guesses. Assignments are conservative. The RHS can still use branch-local values, but the previous branch-narrowed source for the assignment target is reused only when the new RHS is compatible with it. Otherwise the engine falls back to the type before the current guard so stale branch narrowing is dropped after overwrites. The indexing changes share lookup data between normal index/table-field lookup and `__index` fallback. Exact literal keys stay exact, broader union/keyof/enum key types can expand to matching members, and broad string or number keys still include nil when they may miss. Lookup paths used for narrowing still avoid `__index`/operator fallback, so guards only narrow through members that are present directly. The call changes apply the same conservative rule to guarded calls and callee aliases such as `rawget` and `pcall`. Non-string `require` paths and `setmetatable` calls without real `__index`/custom metatable shapes now decline in no-flow instead of guessing. Non-generic overloads keep candidates alive when an argument cannot be evaluated without flow, but such an argument cannot make an overload win unless the remaining candidates return the same type. Regression tests cover branch-narrowed dynamic keys in assignments, initializers, comparisons, field truthiness, literal equality, stacked guards, call/callee aliases, member lookup, `pcall`, and ambiguous overload arguments. Existing assignment overwrite guardrails continue to cover stale narrowing after overwrite. Assisted-by: Codex
1 parent 4ec26a1 commit 6297472

25 files changed

Lines changed: 2790 additions & 1416 deletions

File tree

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

Lines changed: 455 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,28 @@ mod test {
390390
assert_eq!(ws.expr_ty("result"), LuaType::Integer);
391391
}
392392

393+
#[test]
394+
fn test_rawget_alias_guard_narrows_matching_index_expr() {
395+
let mut ws = VirtualWorkspace::new_with_init_std_lib();
396+
397+
ws.def(
398+
r#"
399+
---@class T
400+
---@field x? integer
401+
402+
---@type T
403+
local t = {}
404+
local get = rawget
405+
406+
if get(t, "x") then
407+
result = t.x
408+
end
409+
"#,
410+
);
411+
412+
assert_eq!(ws.expr_ty("result"), LuaType::Integer);
413+
}
414+
393415
#[test]
394416
fn test_type_guard_call_narrows_matching_index_expr() {
395417
let mut ws = VirtualWorkspace::new();

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,31 @@ mod test {
176176
assert_eq!(ws.expr_ty("narrowed"), ws.ty("integer"));
177177
}
178178

179+
#[test]
180+
fn test_pcall_alias_callee_narrows_return_after_error_guard() {
181+
let mut ws = VirtualWorkspace::new_with_init_std_lib();
182+
183+
ws.def(
184+
r#"
185+
---@return integer
186+
local function foo()
187+
return 1
188+
end
189+
190+
local runner = pcall
191+
local ok, result = runner(foo)
192+
193+
if not ok then
194+
error(result)
195+
end
196+
197+
narrowed = result
198+
"#,
199+
);
200+
201+
assert_eq!(ws.expr_ty("narrowed"), ws.ty("integer"));
202+
}
203+
179204
#[test]
180205
fn test_pcall_any_callable_splits_success_unknown_and_failure_string() {
181206
let mut ws = VirtualWorkspace::new_with_init_std_lib();

crates/emmylua_code_analysis/src/db_index/member/lua_member.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ use serde::{Deserialize, Serialize};
66
use smol_str::SmolStr;
77

88
use super::lua_member_feature::LuaMemberFeature;
9-
use crate::{DbIndex, FileId, GlobalId, InferFailReason, LuaInferCache, LuaType, infer_expr};
9+
use crate::{
10+
DbIndex, FileId, GlobalId, InferFailReason, LuaInferCache, LuaType,
11+
semantic::try_infer_expr_for_index,
12+
};
1013

1114
#[derive(Debug)]
1215
pub struct LuaMember {
@@ -116,7 +119,9 @@ impl LuaMemberKey {
116119
}
117120
LuaIndexKey::Idx(idx) => Ok(LuaMemberKey::Integer(*idx as i64)),
118121
LuaIndexKey::Expr(expr) => {
119-
let expr_type = infer_expr(db, cache, expr.clone())?;
122+
let Some(expr_type) = try_infer_expr_for_index(db, cache, expr.clone())? else {
123+
return Err(InferFailReason::None);
124+
};
120125
match expr_type {
121126
LuaType::StringConst(s) => Ok(LuaMemberKey::Name(s.deref().clone())),
122127
LuaType::DocStringConst(s) => Ok(LuaMemberKey::Name(s.deref().clone())),

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

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ mod cache_options;
33
pub use cache_options::{CacheOptions, LuaAnalysisPhase};
44
use emmylua_parser::{LuaExpr, LuaSyntaxId, LuaVarExpr};
55
use hashbrown::{HashMap, HashSet};
6-
use std::{rc::Rc, sync::Arc};
6+
use std::{mem, rc::Rc, sync::Arc};
77

88
use crate::{
99
FileId, FlowId, LuaFunctionType,
@@ -17,13 +17,6 @@ pub enum CacheEntry<T> {
1717
Cache(T),
1818
}
1919

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-
2720
#[derive(Debug, Clone)]
2821
pub(in crate::semantic) struct FlowAssignmentInfo {
2922
pub vars: Vec<LuaVarExpr>,
@@ -53,15 +46,20 @@ pub(in crate::semantic) struct FlowVarCache {
5346
pub struct LuaInferCache {
5447
file_id: FileId,
5548
config: CacheOptions,
49+
no_flow_mode: bool,
5650
pub expr_cache: HashMap<LuaSyntaxId, CacheEntry<LuaType>>,
51+
pub(in crate::semantic) expr_no_flow_cache: HashMap<LuaSyntaxId, CacheEntry<Option<LuaType>>>,
5752
pub call_cache:
5853
HashMap<(LuaSyntaxId, Option<usize>, LuaType), CacheEntry<Arc<LuaFunctionType>>>,
54+
pub(in crate::semantic) call_no_flow_cache:
55+
HashMap<(LuaSyntaxId, Option<usize>, LuaType), CacheEntry<Option<Arc<LuaFunctionType>>>>,
56+
replay_expr_types: Vec<(LuaSyntaxId, LuaType)>,
5957
pub(in crate::semantic) flow_cache_var_ref_ids: HashMap<VarRefId, u32>,
6058
pub(in crate::semantic) next_flow_cache_var_ref_id: u32,
6159
pub(in crate::semantic) flow_var_caches: Vec<FlowVarCache>,
6260
pub(in crate::semantic) flow_branch_inputs_cache: Vec<Option<Arc<[FlowId]>>>,
63-
pub(in crate::semantic) flow_condition_info_cache: Vec<Option<Rc<FlowConditionInfo>>>,
6461
pub(in crate::semantic) flow_assignment_info_cache: Vec<Option<Rc<FlowAssignmentInfo>>>,
62+
pub(in crate::semantic) no_flow_table_exprs: HashSet<LuaSyntaxId>,
6563
pub index_ref_origin_type_cache: HashMap<VarRefId, CacheEntry<LuaType>>,
6664
pub expr_var_ref_id_cache: HashMap<LuaSyntaxId, VarRefId>,
6765
pub narrow_by_literal_stop_position_cache: HashSet<LuaSyntaxId>,
@@ -72,14 +70,18 @@ impl LuaInferCache {
7270
Self {
7371
file_id,
7472
config,
73+
no_flow_mode: false,
7574
expr_cache: HashMap::new(),
75+
expr_no_flow_cache: HashMap::new(),
7676
call_cache: HashMap::new(),
77+
call_no_flow_cache: HashMap::new(),
78+
replay_expr_types: Vec::new(),
7779
flow_cache_var_ref_ids: HashMap::new(),
7880
next_flow_cache_var_ref_id: 0,
7981
flow_var_caches: Vec::new(),
8082
flow_branch_inputs_cache: Vec::new(),
81-
flow_condition_info_cache: Vec::new(),
8283
flow_assignment_info_cache: Vec::new(),
84+
no_flow_table_exprs: HashSet::new(),
8385
index_ref_origin_type_cache: HashMap::new(),
8486
expr_var_ref_id_cache: HashMap::new(),
8587
narrow_by_literal_stop_position_cache: HashSet::new(),
@@ -94,20 +96,80 @@ impl LuaInferCache {
9496
self.file_id
9597
}
9698

99+
pub(in crate::semantic) fn is_no_flow(&self) -> bool {
100+
self.no_flow_mode
101+
}
102+
103+
pub(in crate::semantic) fn with_no_flow<R>(
104+
&mut self,
105+
f: impl FnOnce(&mut LuaInferCache) -> R,
106+
) -> R {
107+
let previous_mode = mem::replace(&mut self.no_flow_mode, true);
108+
let result = f(self);
109+
self.no_flow_mode = previous_mode;
110+
result
111+
}
112+
113+
pub(in crate::semantic) fn replay_expr_type(&self, syntax_id: LuaSyntaxId) -> Option<&LuaType> {
114+
self.replay_expr_types
115+
.iter()
116+
.rev()
117+
.find_map(|(overlay_id, ty)| (*overlay_id == syntax_id).then_some(ty))
118+
}
119+
120+
pub(in crate::semantic) fn with_replay_overlay<R>(
121+
&mut self,
122+
expr_types: &[(LuaSyntaxId, LuaType)],
123+
table_exprs: &[LuaSyntaxId],
124+
f: impl FnOnce(&mut LuaInferCache) -> R,
125+
) -> R {
126+
if expr_types.is_empty() && table_exprs.is_empty() {
127+
return f(self);
128+
}
129+
130+
// Replay overlays change no-flow answers, so isolate overlay-dependent
131+
// cache writes from the normal no-flow caches.
132+
let overlay_len = self.replay_expr_types.len();
133+
self.replay_expr_types.extend(expr_types.iter().cloned());
134+
let mut inserted_table_exprs = Vec::new();
135+
for syntax_id in table_exprs {
136+
if self.no_flow_table_exprs.insert(*syntax_id) {
137+
inserted_table_exprs.push(*syntax_id);
138+
}
139+
}
140+
let saved_expr_no_flow_cache = mem::take(&mut self.expr_no_flow_cache);
141+
let saved_call_no_flow_cache = mem::take(&mut self.call_no_flow_cache);
142+
143+
let result = f(self);
144+
145+
self.expr_no_flow_cache = saved_expr_no_flow_cache;
146+
self.call_no_flow_cache = saved_call_no_flow_cache;
147+
for syntax_id in inserted_table_exprs {
148+
self.no_flow_table_exprs.remove(&syntax_id);
149+
}
150+
self.replay_expr_types.truncate(overlay_len);
151+
result
152+
}
153+
97154
pub fn set_phase(&mut self, phase: LuaAnalysisPhase) {
98155
self.config.analysis_phase = phase;
99156
}
100157

101158
pub fn clear(&mut self) {
159+
self.no_flow_mode = false;
102160
self.expr_cache.clear();
161+
self.expr_no_flow_cache.clear();
103162
self.call_cache.clear();
163+
self.call_no_flow_cache.clear();
164+
self.replay_expr_types.clear();
104165
self.flow_cache_var_ref_ids.clear();
105166
self.next_flow_cache_var_ref_id = 0;
106167
self.flow_var_caches.clear();
107168
self.flow_branch_inputs_cache.clear();
108-
self.flow_condition_info_cache.clear();
109169
self.flow_assignment_info_cache.clear();
170+
self.no_flow_table_exprs.clear();
110171
self.index_ref_origin_type_cache.clear();
111172
self.expr_var_ref_id_cache.clear();
173+
self.narrow_by_literal_stop_position_cache.clear();
112174
}
113175
}

crates/emmylua_code_analysis/src/semantic/infer/infer_call/infer_require.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::{
55
semantic::infer::InferResult,
66
};
77

8-
pub fn infer_require_call(
8+
pub(super) fn infer_require_call(
99
db: &DbIndex,
1010
cache: &mut LuaInferCache,
1111
call_expr: LuaCallExpr,
@@ -14,7 +14,12 @@ pub fn infer_require_call(
1414
let first_arg = arg_list.get_args().next().ok_or(InferFailReason::None)?;
1515
let require_path_type = infer_expr(db, cache, first_arg)?;
1616
let module_path: String = match &require_path_type {
17-
LuaType::StringConst(module_path) => module_path.as_ref().to_string(),
17+
LuaType::StringConst(module_path) | LuaType::DocStringConst(module_path) => {
18+
module_path.as_ref().to_string()
19+
}
20+
_ if cache.is_no_flow() => {
21+
return Err(InferFailReason::None);
22+
}
1823
_ => {
1924
return Ok(LuaType::Any);
2025
}

crates/emmylua_code_analysis/src/semantic/infer/infer_call/infer_setmetatable.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::{
66
semantic::{infer::InferResult, member::find_members_with_key},
77
};
88

9-
pub fn infer_setmetatable_call(
9+
pub(super) fn infer_setmetatable_call(
1010
db: &DbIndex,
1111
cache: &mut LuaInferCache,
1212
call_expr: LuaCallExpr,
@@ -22,6 +22,12 @@ pub fn infer_setmetatable_call(
2222
let metatable = args[1].clone();
2323

2424
let (meta_type, is_index) = infer_metatable_index_type(db, cache, metatable)?;
25+
if cache.is_no_flow() && !is_index && !meta_type.is_custom_type() {
26+
// No-flow setmetatable inference is only used as a conservative fallback.
27+
// If the metatable does not resolve to an actual metatable shape, decline
28+
// instead of treating arbitrary static expressions as the result type.
29+
return Err(InferFailReason::None);
30+
}
2531
match &basic_table {
2632
LuaExpr::TableExpr(table_expr) => {
2733
if table_expr.is_empty() && is_index {

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

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,31 @@ pub fn infer_call_expr_func(
3838
) -> InferCallFuncResult {
3939
let syntax_id = call_expr.get_syntax_id();
4040
let key = (syntax_id, args_count, call_expr_type.clone());
41-
if let Some(cache) = cache.call_cache.get(&key) {
42-
match cache {
41+
let is_no_flow = cache.is_no_flow();
42+
if is_no_flow {
43+
if let Some(cache_entry) = cache.call_no_flow_cache.get(&key) {
44+
match cache_entry {
45+
CacheEntry::Cache(Some(ty)) => return Ok(ty.clone()),
46+
CacheEntry::Cache(None) => return Err(InferFailReason::None),
47+
CacheEntry::Ready => return Err(InferFailReason::RecursiveInfer),
48+
}
49+
}
50+
} else if let Some(cache_entry) = cache.call_cache.get(&key) {
51+
match cache_entry {
4352
CacheEntry::Cache(ty) => return Ok(ty.clone()),
44-
_ => return Err(InferFailReason::RecursiveInfer),
53+
CacheEntry::Ready => return Err(InferFailReason::RecursiveInfer),
4554
}
4655
}
4756

48-
cache.call_cache.insert(key.clone(), CacheEntry::Ready);
57+
if is_no_flow {
58+
cache
59+
.call_no_flow_cache
60+
.insert(key.clone(), CacheEntry::Ready);
61+
} else {
62+
cache.call_cache.insert(key.clone(), CacheEntry::Ready);
63+
}
4964
let result = match &call_expr_type {
50-
LuaType::DocFunction(func) => {
51-
infer_doc_function(db, cache, func, call_expr.clone(), args_count)
52-
}
65+
LuaType::DocFunction(func) => infer_doc_function(db, cache, func, call_expr.clone()),
5366
LuaType::Signature(signature_id) => {
5467
infer_signature_doc_function(db, cache, *signature_id, call_expr.clone(), args_count)
5568
}
@@ -143,15 +156,34 @@ pub fn infer_call_expr_func(
143156

144157
match &result {
145158
Ok(func_ty) => {
159+
if is_no_flow {
160+
cache
161+
.call_no_flow_cache
162+
.insert(key, CacheEntry::Cache(Some(func_ty.clone())));
163+
} else {
164+
cache
165+
.call_cache
166+
.insert(key, CacheEntry::Cache(func_ty.clone()));
167+
}
168+
}
169+
Err(InferFailReason::None) | Err(InferFailReason::RecursiveInfer) if is_no_flow => {
146170
cache
147-
.call_cache
148-
.insert(key, CacheEntry::Cache(func_ty.clone()));
171+
.call_no_flow_cache
172+
.insert(key, CacheEntry::Cache(None));
149173
}
150174
Err(r) if r.is_need_resolve() => {
151-
cache.call_cache.remove(&key);
175+
if is_no_flow {
176+
cache.call_no_flow_cache.remove(&key);
177+
} else {
178+
cache.call_cache.remove(&key);
179+
}
152180
}
153181
Err(InferFailReason::None) => {
154-
cache.call_cache.remove(&key);
182+
if is_no_flow {
183+
cache.call_no_flow_cache.remove(&key);
184+
} else {
185+
cache.call_cache.remove(&key);
186+
}
155187
}
156188
_ => {}
157189
}
@@ -181,7 +213,6 @@ fn infer_doc_function(
181213
cache: &mut LuaInferCache,
182214
func: &LuaFunctionType,
183215
call_expr: LuaCallExpr,
184-
_: Option<usize>,
185216
) -> InferCallFuncResult {
186217
if func.contain_tpl() {
187218
let result = instantiate_func_generic(db, cache, func, call_expr)?;
@@ -597,7 +628,7 @@ fn infer_intersection(
597628
resolve_signature(db, cache, overloads, call_expr, false, args_count)
598629
}
599630

600-
pub(crate) fn unwrapp_return_type(
631+
fn unwrapp_return_type(
601632
db: &DbIndex,
602633
cache: &mut LuaInferCache,
603634
return_type: LuaType,
@@ -721,7 +752,8 @@ pub fn infer_call_expr(
721752
.get_ret()
722753
.clone();
723754

724-
if let Some(tree) = db.get_flow_index().get_flow_tree(&cache.get_file_id())
755+
if !cache.is_no_flow()
756+
&& let Some(tree) = db.get_flow_index().get_flow_tree(&cache.get_file_id())
725757
&& let Some(flow_id) = tree.get_flow_id(call_expr.get_syntax_id())
726758
&& let Some(flow_ret_type) =
727759
get_type_at_call_expr_inline_cast(db, cache, tree, call_expr, flow_id, ret_type.clone())

0 commit comments

Comments
 (0)