Skip to content

Commit f0bd23b

Browse files
authored
Merge pull request #1182 from lewis6991/codex/issue-1180-self-assignment-flow
fix(flow): preserve self-assignment antecedent types
2 parents 51729d0 + 173ab5b commit f0bd23b

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)