Skip to content

Commit 173ab5b

Browse files
committed
fix(flow): preserve self-assignment antecedent types
Resolve self-dependent assignment reads through the flow scheduler instead of injecting unknown. Hoist only unavoidable self reads so repeated replay stays linear without bypassing skippable index dependencies. Add regressions for the minimal assignment and original while-loop cases. Fixes #1180 Assisted-by: Codex
1 parent 98c85ac commit 173ab5b

2 files changed

Lines changed: 137 additions & 32 deletions

File tree

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3599,6 +3599,48 @@ _2 = a[1]
35993599
assert_eq!(ws.humanize_type(after_assign), "Counter");
36003600
}
36013601

3602+
#[test]
3603+
fn test_issue_1180_self_assignment_replays_antecedent_type() {
3604+
let mut ws = VirtualWorkspace::new();
3605+
ws.def(
3606+
r#"
3607+
local index = 1
3608+
index = index - 1
3609+
after_assign = index
3610+
"#,
3611+
);
3612+
3613+
assert_eq!(ws.expr_ty("after_assign"), ws.ty("integer"));
3614+
}
3615+
3616+
#[test]
3617+
fn test_issue_1180_while_loop_preserves_integer_self_assignments() {
3618+
let mut ws = VirtualWorkspace::new_with_init_std_lib();
3619+
let file_id = ws.def(
3620+
r#"
3621+
function test()
3622+
local arr = {1, 2, 3, 4, "hello", 6, false, 7}
3623+
local index = 1
3624+
3625+
while index <= #arr do
3626+
local v = arr[index]
3627+
3628+
if type(v) == "boolean" then
3629+
table.remove(arr, index)
3630+
index = index - 1
3631+
print(index)
3632+
end
3633+
3634+
index = index + 1
3635+
end
3636+
end
3637+
"#,
3638+
);
3639+
let index_type = last_name_expr_type(&ws, file_id, "index");
3640+
3641+
assert_eq!(ws.humanize_type(index_type), "integer");
3642+
}
3643+
36023644
#[test]
36033645
#[timeout(5000)]
36043646
fn test_issue_1114_repeated_self_dependent_assignments_build_semantic_model() {

crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs

Lines changed: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use emmylua_parser::{
2-
BinaryOperator, LuaAssignStat, LuaAstNode, LuaCallExprStat, LuaChunk, LuaDocOpType, LuaExpr,
3-
LuaForStat, LuaIndexKey, LuaIndexMemberExpr, LuaSyntaxId, LuaTableExpr, LuaUnaryExpr,
4-
LuaVarExpr, UnaryOperator,
2+
LuaAssignStat, LuaAstNode, LuaCallExprStat, LuaChunk, LuaDocOpType, LuaExpr, LuaForStat,
3+
LuaIndexKey, LuaIndexMemberExpr, LuaSyntaxId, LuaTableExpr, LuaUnaryExpr, LuaVarExpr,
4+
UnaryOperator,
55
};
66
use hashbrown::HashSet;
77
use std::{rc::Rc, sync::Arc};
@@ -100,6 +100,14 @@ enum Continuation {
100100
expr_type: LuaType,
101101
reuse_antecedent_narrowing: bool,
102102
},
103+
// Resume RHS replay once the assigned ref's pre-assignment type is known.
104+
// This resolves self-reads before earlier RHS dependencies can repeatedly
105+
// walk the same assignment chain.
106+
AssignmentSelfAntecedent {
107+
walk: QueryWalk,
108+
replay: FlowExprReplay,
109+
replay_query: FlowReplayQuery,
110+
},
103111
// Resume an assignment result after proving the branch can reach the
104112
// assignment. An impossible branch must not contribute this result to a merge.
105113
AssignmentReachability {
@@ -218,9 +226,39 @@ impl FlowReplayQuery {
218226
Ok(())
219227
}
220228

221-
fn resolve_dependencies(&mut self, var_ref_id: &VarRefId, typ: LuaType) {
229+
fn has_unskippable_dependency(&self, var_ref_id: &VarRefId, flow_id: FlowId) -> bool {
230+
// A resolved index can skip its prefix and key dependencies. Only hoist
231+
// a self-read when every earlier success path still reaches it.
232+
let mut furthest_success_jump = self.next_dependency_idx;
233+
for (idx, query) in self
234+
.dependency_queries
235+
.iter()
236+
.enumerate()
237+
.skip(self.next_dependency_idx)
238+
{
239+
if idx >= furthest_success_jump
240+
&& query.resolved_type.is_none()
241+
&& query.var_ref_id == *var_ref_id
242+
&& query.flow_id == flow_id
243+
{
244+
return true;
245+
}
246+
if let Some(next_idx) = query.next_dependency_idx_on_success {
247+
furthest_success_jump = furthest_success_jump.max(next_idx);
248+
}
249+
}
250+
251+
false
252+
}
253+
254+
fn resolve_dependencies_at_flow(
255+
&mut self,
256+
var_ref_id: &VarRefId,
257+
flow_id: FlowId,
258+
typ: LuaType,
259+
) {
222260
for query in &mut self.dependency_queries {
223-
if query.var_ref_id == *var_ref_id {
261+
if query.var_ref_id == *var_ref_id && query.flow_id == flow_id {
224262
query.resolved_type = Some(typ.clone());
225263
}
226264
}
@@ -496,6 +534,16 @@ impl<'a> FlowTypeEngine<'a> {
496534
reuse_antecedent_narrowing,
497535
query_result,
498536
),
537+
Some(Continuation::AssignmentSelfAntecedent {
538+
walk,
539+
replay,
540+
replay_query,
541+
}) => self.resume_assignment_self_antecedent(
542+
walk,
543+
replay,
544+
replay_query,
545+
query_result,
546+
),
499547
Some(Continuation::AssignmentReachability { walk, result_type }) => {
500548
match query_result {
501549
Ok(FlowQueryResult::Unreachable) => {
@@ -692,6 +740,30 @@ impl<'a> FlowTypeEngine<'a> {
692740
Ok(self.finish_walk(walk, result_type))
693741
}
694742

743+
fn resume_assignment_self_antecedent(
744+
&mut self,
745+
walk: QueryWalk,
746+
replay: FlowExprReplay,
747+
mut replay_query: FlowReplayQuery,
748+
antecedent_result: FlowResult,
749+
) -> Result<SchedulerStep, InferFailReason> {
750+
let antecedent_type = match antecedent_result {
751+
Ok(FlowQueryResult::Type(antecedent_type)) => antecedent_type,
752+
Ok(FlowQueryResult::Unreachable) => {
753+
return Ok(self.finish_unreachable_branch(walk));
754+
}
755+
Err(err) => return self.fail_query(&walk.query, err),
756+
};
757+
758+
let antecedent_flow_id = replay_query.flow_id;
759+
replay_query.resolve_dependencies_at_flow(
760+
&walk.query.var_ref_id,
761+
antecedent_flow_id,
762+
antecedent_type,
763+
);
764+
self.start_expr_replay(walk, replay, replay_query)
765+
}
766+
695767
fn start_expr_replay(
696768
&mut self,
697769
walk: QueryWalk,
@@ -1106,29 +1178,31 @@ impl<'a> FlowTypeEngine<'a> {
11061178
let expr_idx = i.min(last_expr_idx);
11071179
let result_slot = i.saturating_sub(last_expr_idx);
11081180
let expr = assignment_info.exprs[expr_idx].clone();
1109-
let mut replay_query = FlowReplayQuery::new(
1181+
let replay_query = FlowReplayQuery::new(
11101182
self.db,
11111183
Some(self.tree),
11121184
self.cache,
11131185
antecedent_flow_id,
1114-
expr.clone(),
1186+
expr,
11151187
true,
11161188
);
1117-
// A plain self-dependent RHS would replay this assignment while
1118-
// trying to type itself. Treat that self read as unknown; `and`/`or`
1119-
// assignments still need the antecedent value for their semantics.
1120-
if explicit_var_type.is_none() && !contains_short_circuit_binary_expr(&expr) {
1121-
replay_query.resolve_dependencies(&var_ref_id, LuaType::Unknown);
1189+
let replay = FlowExprReplay::Assignment {
1190+
antecedent_flow_id,
1191+
explicit_var_type,
1192+
result_slot,
1193+
};
1194+
if replay_query.has_unskippable_dependency(&var_ref_id, antecedent_flow_id) {
1195+
let query = FlowQuery::new(self.cache, &var_ref_id, antecedent_flow_id);
1196+
return Ok(SchedulerStep::StartQuery {
1197+
query,
1198+
continuation: Some(Continuation::AssignmentSelfAntecedent {
1199+
walk,
1200+
replay,
1201+
replay_query,
1202+
}),
1203+
});
11221204
}
1123-
return self.start_expr_replay(
1124-
walk,
1125-
FlowExprReplay::Assignment {
1126-
antecedent_flow_id,
1127-
explicit_var_type,
1128-
result_slot,
1129-
},
1130-
replay_query,
1131-
);
1205+
return self.start_expr_replay(walk, replay, replay_query);
11321206
}
11331207

11341208
self.finish_assignment_expr_type(walk, antecedent_flow_id, explicit_var_type, LuaType::Nil)
@@ -1971,17 +2045,6 @@ fn integer_enum_assignment_declared_type(
19712045
.then_some(declared_type)
19722046
}
19732047

1974-
fn contains_short_circuit_binary_expr(expr: &LuaExpr) -> bool {
1975-
expr.descendants::<LuaExpr>().any(|expr| {
1976-
let LuaExpr::BinaryExpr(binary_expr) = expr else {
1977-
return false;
1978-
};
1979-
binary_expr.get_op_token().is_some_and(|token| {
1980-
matches!(token.get_op(), BinaryOperator::OpAnd | BinaryOperator::OpOr)
1981-
})
1982-
})
1983-
}
1984-
19852048
fn is_partial_assignment_expr_compatible(
19862049
db: &DbIndex,
19872050
source_type: &LuaType,

0 commit comments

Comments
 (0)