fix(semantic): reuse branch-narrowed types in expression inference#1057
fix(semantic): reuse branch-narrowed types in expression inference#1057lewis6991 wants to merge 3 commits intoEmmyLuaLs:mainfrom
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the type inference and flow analysis engine to support structural expression replay through a new "no-flow" mode and continuation-based dependency resolution. The changes include significant updates to the inference cache and the flow engine's walk logic. Feedback focuses on ensuring correct Lua semantics for assignments with missing values, which should result in nil rather than skipping the assignment. Other suggestions include treating declined expressions as Unknown to avoid stale types, optimizing the handling of cached condition actions, and addressing potential performance regressions caused by the removal of condition filters.
6297472 to
86e4277
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a 'no-flow' mode for expression inference, allowing the semantic model to resolve types without triggering full flow-aware analysis. This is achieved by adding expr_no_flow_cache and call_no_flow_cache to LuaInferCache, along with mechanisms to replay expression types using dependency overlays. The changes also include significant refactoring of the flow-aware narrowing logic, moving from a subquery-based approach to a continuation-based model for resolving dependencies. My feedback highlights that the current dependency collection logic in collect_expr_dependency_queries may miss narrowed member accesses; I have suggested tracking all trackable references, including index and call references, to ensure correct narrowing during replay.
| if matches!(expr, LuaExpr::NameExpr(_)) { | ||
| let flow_id = tree | ||
| .and_then(|tree| tree.get_flow_id(expr.get_syntax_id())) | ||
| .unwrap_or(fallback_flow_id); | ||
| if let Some(var_ref_id) = get_var_expr_var_ref_id(db, cache, expr.clone()) { | ||
| dependency_queries.push(FlowExprTypeQuery { | ||
| var_ref_id, | ||
| flow_id, | ||
| syntax_id: expr.get_syntax_id(), | ||
| }); | ||
| } | ||
| return; |
There was a problem hiding this comment.
The current implementation of collect_expr_dependency_queries only tracks NameExpr as dependencies for expression replay. This means that if a member access like t.x is narrowed by a guard, the replay mechanism will only resolve t with flow and then perform a no-flow lookup for x, losing the narrowing on t.x itself.
By using get_var_expr_var_ref_id for any expression, we can track all trackable references (including IndexRef and CallRef) as dependencies. The flow engine will then be responsible for resolving the entire chain of dependencies, ensuring that narrowed members are correctly handled during replay.
if let Some(var_ref_id) = get_var_expr_var_ref_id(db, cache, expr.clone()) {
let flow_id = tree
.and_then(|tree| tree.get_flow_id(expr.get_syntax_id()))
.unwrap_or(fallback_flow_id);
dependency_queries.push(FlowExprTypeQuery {
var_ref_id,
flow_id,
syntax_id: expr.get_syntax_id(),
});
return;
}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
Use no-flow inference where loading only needs declared call signatures, and avoid inferring semantic-token call prefixes that cannot use the result. Move condition AST resolution behind cache misses so cached flow walks do less repeated work.
86e4277 to
3c6d2ca
Compare
Exact name and integer keys already have a direct member lookup. When that misses, returning FieldNotFound avoids scanning every broad compatible member and unioning unrelated signatures. Assisted-by: Codex
Replaces #1044.
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 withkeyas"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
__indexfallback. 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
rawgetandpcall. Non-stringrequirepaths andsetmetatablecalls 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.