Skip to content

Commit c1d93e8

Browse files
committed
fix: stop repeated assignments from hanging
Statements like `text = text .. value` or `total = total + value` could make type analysis revisit the same variable over and over, which stalled semantic model building. Handle that case directly for simple built-in operations. Other assignments still use the normal flow analysis, including custom operators. Fixes #1114 Assisted-by: Codex
1 parent dca540c commit c1d93e8

2 files changed

Lines changed: 204 additions & 4 deletions

File tree

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

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod test {
88
const LARGE_LINEAR_ASSIGNMENT_STEPS: usize = 2048;
99
const MAXWELLHOME_ARRAY_VALUES: usize = 2048;
1010
const ISSUE_1100_HIGHLIGHT_GROUPS: usize = 2048;
11+
const ISSUE_1114_REPEATED_BINARY_STEPS: usize = 512;
1112

1213
#[test]
1314
fn test_closure_return() {
@@ -2942,6 +2943,147 @@ _2 = a[1]
29422943
assert_eq!(ws.humanize_type(after_assign), "Pattern");
29432944
}
29442945

2946+
#[test]
2947+
fn test_assignment_binary_rhs_replays_non_self_dependency() {
2948+
let mut ws = VirtualWorkspace::new();
2949+
ws.def(
2950+
r#"
2951+
---@class FooValue
2952+
---@field kind "foo"
2953+
---@field value integer
2954+
2955+
---@class BarValue
2956+
---@field kind "bar"
2957+
---@field value string
2958+
2959+
local right ---@type FooValue|BarValue
2960+
2961+
if right.kind == "foo" then
2962+
local value
2963+
value = right.value + 1
2964+
after_assign = value
2965+
end
2966+
"#,
2967+
);
2968+
2969+
let after_assign = ws.expr_ty("after_assign");
2970+
assert_eq!(ws.humanize_type(after_assign), "integer");
2971+
}
2972+
2973+
#[test]
2974+
fn test_assignment_rhs_keeps_flow_dependent_concat_operator() {
2975+
let mut ws = VirtualWorkspace::new();
2976+
ws.def(
2977+
r#"
2978+
---@class Rope
2979+
---@operator concat(Rope): Rope
2980+
2981+
local left ---@type Rope?
2982+
local right ---@type Rope?
2983+
2984+
if not left then return end
2985+
if not right then return end
2986+
left = left .. right
2987+
after_assign = left
2988+
"#,
2989+
);
2990+
2991+
let after_assign = ws.expr_ty("after_assign");
2992+
assert_eq!(ws.humanize_type(after_assign), "Rope");
2993+
}
2994+
2995+
#[test]
2996+
fn test_assignment_rhs_keeps_flow_dependent_add_operator() {
2997+
let mut ws = VirtualWorkspace::new();
2998+
ws.def(
2999+
r#"
3000+
---@class Counter
3001+
---@operator add(Counter): Counter
3002+
3003+
local left ---@type Counter?
3004+
local right ---@type Counter?
3005+
3006+
if not left then return end
3007+
if not right then return end
3008+
left = left + right
3009+
after_assign = left
3010+
"#,
3011+
);
3012+
3013+
let after_assign = ws.expr_ty("after_assign");
3014+
assert_eq!(ws.humanize_type(after_assign), "Counter");
3015+
}
3016+
3017+
#[test]
3018+
#[timeout(5000)]
3019+
fn test_issue_1114_repeated_self_concat_builds_semantic_model() {
3020+
let mut ws = VirtualWorkspace::new();
3021+
let repeated_assignments =
3022+
"wnd = wnd .. config.pic[idx][index]\n".repeat(ISSUE_1114_REPEATED_BINARY_STEPS);
3023+
let block = format!(
3024+
r#"
3025+
function f(idx, index)
3026+
local wnd = ""
3027+
local child = ""
3028+
{repeated_assignments}
3029+
return wnd:format(child:sub(1, -2))
3030+
end
3031+
"#
3032+
);
3033+
3034+
let file_id = ws.def(&block);
3035+
3036+
assert!(
3037+
ws.analysis
3038+
.compilation
3039+
.get_semantic_model(file_id)
3040+
.is_some(),
3041+
"expected semantic model for repeated self-concat repro"
3042+
);
3043+
}
3044+
3045+
#[test]
3046+
#[timeout(5000)]
3047+
fn test_issue_1114_repeated_self_add_builds_semantic_model() {
3048+
let mut ws = VirtualWorkspace::new();
3049+
let repeated_assignments =
3050+
"total = total + config.pic[idx][index]\n".repeat(ISSUE_1114_REPEATED_BINARY_STEPS);
3051+
let block = format!(
3052+
r#"
3053+
function f(idx, index)
3054+
local total = 0
3055+
{repeated_assignments}
3056+
return total
3057+
end
3058+
"#
3059+
);
3060+
3061+
let file_id = ws.def(&block);
3062+
3063+
assert!(
3064+
ws.analysis
3065+
.compilation
3066+
.get_semantic_model(file_id)
3067+
.is_some(),
3068+
"expected semantic model for repeated self-add repro"
3069+
);
3070+
}
3071+
3072+
#[test]
3073+
fn test_binary_assignment_infer_error_keeps_previous_type() {
3074+
let mut ws = VirtualWorkspace::new();
3075+
ws.def(
3076+
r#"
3077+
local value = "prior"
3078+
value = config.pic + 1
3079+
after_assign = value
3080+
"#,
3081+
);
3082+
3083+
let after_assign = ws.expr_ty("after_assign");
3084+
assert_eq!(ws.humanize_type(after_assign), "string");
3085+
}
3086+
29453087
#[test]
29463088
fn test_eq_uses_branch_narrowed_rhs_ref_type() {
29473089
let mut ws = VirtualWorkspace::new();

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

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
use emmylua_parser::{
2-
LuaAssignStat, LuaAstNode, LuaChunk, LuaDocOpType, LuaExpr, LuaIndexKey, LuaIndexMemberExpr,
3-
LuaSyntaxId, LuaTableExpr, LuaVarExpr,
2+
BinaryOperator, LuaAssignStat, LuaAstNode, LuaChunk, LuaDocOpType, LuaExpr, LuaIndexKey,
3+
LuaIndexMemberExpr, LuaSyntaxId, LuaTableExpr, LuaVarExpr,
44
};
55
use hashbrown::HashSet;
66
use std::{rc::Rc, sync::Arc};
77

88
use crate::{
99
CacheEntry, DbIndex, FlowId, FlowNode, FlowNodeKind, FlowTree, InferFailReason, LuaDeclId,
10-
LuaInferCache, LuaMemberId, LuaSignatureId, LuaType, TypeOps, check_type_compact,
10+
LuaInferCache, LuaMemberId, LuaSignatureId, LuaType, LuaTypeNode, TypeOps, check_type_compact,
1111
semantic::{
1212
cache::{FlowAssignmentInfo, FlowMode, FlowVarCache},
1313
infer::{
@@ -1049,9 +1049,48 @@ impl<'a> FlowTypeEngine<'a> {
10491049
Some(self.tree),
10501050
self.cache,
10511051
antecedent_flow_id,
1052-
expr,
1052+
expr.clone(),
10531053
true,
10541054
);
1055+
// Flow replay would recurse on `x = x <op> unresolved`; only that
1056+
// self-dependent shape gets the primitive shortcut.
1057+
if explicit_var_type.is_none()
1058+
&& let LuaExpr::BinaryExpr(binary_expr) = &expr
1059+
&& let Some(op_token) = binary_expr.get_op_token()
1060+
&& let Some(fallback_type) = binary_assignment_operator_type(op_token.get_op())
1061+
&& replay_query
1062+
.dependency_queries
1063+
.iter()
1064+
.any(|query| query.var_ref_id == var_ref_id)
1065+
&& !binary_expr
1066+
.get_exprs()
1067+
.is_some_and(|(left_expr, right_expr)| {
1068+
[left_expr, right_expr].into_iter().any(|operand| {
1069+
try_infer_expr_no_flow(self.db, self.cache, operand)
1070+
.ok()
1071+
.flatten()
1072+
.is_some_and(|typ| typ.any_type(LuaType::is_custom_type))
1073+
})
1074+
})
1075+
{
1076+
let expr_type = match try_infer_expr_no_flow(self.db, self.cache, expr.clone()) {
1077+
Ok(Some(expr_type)) => Some(
1078+
expr_type
1079+
.get_result_slot_type(result_slot)
1080+
.unwrap_or(LuaType::Nil),
1081+
),
1082+
Ok(None) | Err(_) if result_slot == 0 => Some(fallback_type),
1083+
Ok(None) | Err(_) => None,
1084+
};
1085+
if let Some(expr_type) = expr_type
1086+
&& matches!(
1087+
expr_type,
1088+
LuaType::Integer | LuaType::Number | LuaType::String
1089+
)
1090+
{
1091+
return Ok(self.finish_walk(walk, expr_type));
1092+
}
1093+
}
10551094
return self.start_expr_replay(
10561095
walk,
10571096
FlowExprReplay::Assignment {
@@ -1647,6 +1686,25 @@ fn preserves_assignment_expr_type(typ: &LuaType) -> bool {
16471686
matches!(typ, LuaType::TableConst(_) | LuaType::Object(_)) || is_exact_assignment_expr_type(typ)
16481687
}
16491688

1689+
fn binary_assignment_operator_type(op: BinaryOperator) -> Option<LuaType> {
1690+
match op {
1691+
BinaryOperator::OpAdd
1692+
| BinaryOperator::OpSub
1693+
| BinaryOperator::OpMul
1694+
| BinaryOperator::OpDiv
1695+
| BinaryOperator::OpMod
1696+
| BinaryOperator::OpPow => Some(LuaType::Number),
1697+
BinaryOperator::OpIDiv
1698+
| BinaryOperator::OpBAnd
1699+
| BinaryOperator::OpBOr
1700+
| BinaryOperator::OpBXor
1701+
| BinaryOperator::OpShl
1702+
| BinaryOperator::OpShr => Some(LuaType::Integer),
1703+
BinaryOperator::OpConcat => Some(LuaType::String),
1704+
_ => None,
1705+
}
1706+
}
1707+
16501708
fn is_partial_assignment_expr_compatible(
16511709
db: &DbIndex,
16521710
source_type: &LuaType,

0 commit comments

Comments
 (0)