Skip to content

Commit a33ef21

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 a33ef21

2 files changed

Lines changed: 322 additions & 4 deletions

File tree

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

Lines changed: 223 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,228 @@ _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+
#[timeout(5000)]
3074+
fn test_issue_1114_repeated_parenthesized_self_concat_builds_semantic_model() {
3075+
let mut ws = VirtualWorkspace::new();
3076+
let repeated_assignments =
3077+
"wnd = (wnd .. config.pic[idx][index])\n".repeat(ISSUE_1114_REPEATED_BINARY_STEPS);
3078+
let block = format!(
3079+
r#"
3080+
function f(idx, index)
3081+
local wnd = ""
3082+
{repeated_assignments}
3083+
return wnd
3084+
end
3085+
"#
3086+
);
3087+
3088+
let file_id = ws.def(&block);
3089+
3090+
assert!(
3091+
ws.analysis
3092+
.compilation
3093+
.get_semantic_model(file_id)
3094+
.is_some(),
3095+
"expected semantic model for repeated parenthesized self-concat repro"
3096+
);
3097+
}
3098+
3099+
#[test]
3100+
#[timeout(5000)]
3101+
fn test_issue_1114_repeated_unary_self_assignment_builds_semantic_model() {
3102+
let mut ws = VirtualWorkspace::new();
3103+
let repeated_assignments =
3104+
"total = -(total + config.pic[idx][index])\n".repeat(ISSUE_1114_REPEATED_BINARY_STEPS);
3105+
let block = format!(
3106+
r#"
3107+
function f(idx, index)
3108+
local total = 0
3109+
{repeated_assignments}
3110+
return total
3111+
end
3112+
"#
3113+
);
3114+
3115+
let file_id = ws.def(&block);
3116+
3117+
assert!(
3118+
ws.analysis
3119+
.compilation
3120+
.get_semantic_model(file_id)
3121+
.is_some(),
3122+
"expected semantic model for repeated unary self-assignment repro"
3123+
);
3124+
}
3125+
3126+
#[test]
3127+
#[timeout(5000)]
3128+
fn test_issue_1114_repeated_comparison_self_assignment_builds_semantic_model() {
3129+
let mut ws = VirtualWorkspace::new();
3130+
let repeated_assignments = "enabled = enabled == config.pic[idx][index]\n"
3131+
.repeat(ISSUE_1114_REPEATED_BINARY_STEPS);
3132+
let block = format!(
3133+
r#"
3134+
function f(idx, index)
3135+
local enabled = true
3136+
{repeated_assignments}
3137+
return enabled
3138+
end
3139+
"#
3140+
);
3141+
3142+
let file_id = ws.def(&block);
3143+
3144+
assert!(
3145+
ws.analysis
3146+
.compilation
3147+
.get_semantic_model(file_id)
3148+
.is_some(),
3149+
"expected semantic model for repeated comparison self-assignment repro"
3150+
);
3151+
}
3152+
3153+
#[test]
3154+
fn test_binary_assignment_infer_error_keeps_previous_type() {
3155+
let mut ws = VirtualWorkspace::new();
3156+
ws.def(
3157+
r#"
3158+
local value = "prior"
3159+
value = config.pic + 1
3160+
after_assign = value
3161+
"#,
3162+
);
3163+
3164+
let after_assign = ws.expr_ty("after_assign");
3165+
assert_eq!(ws.humanize_type(after_assign), "string");
3166+
}
3167+
29453168
#[test]
29463169
fn test_eq_uses_branch_narrowed_rhs_ref_type() {
29473170
let mut ws = VirtualWorkspace::new();

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

Lines changed: 99 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, UnaryOperator,
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,21 @@ impl<'a> FlowTypeEngine<'a> {
10491049
Some(self.tree),
10501050
self.cache,
10511051
antecedent_flow_id,
1052-
expr,
1052+
expr.clone(),
10531053
true,
10541054
);
1055+
// PERF: Replaying `x = <operator using x>` chains can walk every
1056+
// prior assignment; built-in operators only need their result shape.
1057+
if explicit_var_type.is_none()
1058+
&& replay_query
1059+
.dependency_queries
1060+
.iter()
1061+
.any(|query| query.var_ref_id == var_ref_id)
1062+
&& let Some(expr_type) =
1063+
self_dependent_assignment_operator_type(self.db, self.cache, &expr, result_slot)
1064+
{
1065+
return Ok(self.finish_walk(walk, expr_type));
1066+
}
10551067
return self.start_expr_replay(
10561068
walk,
10571069
FlowExprReplay::Assignment {
@@ -1647,6 +1659,89 @@ fn preserves_assignment_expr_type(typ: &LuaType) -> bool {
16471659
matches!(typ, LuaType::TableConst(_) | LuaType::Object(_)) || is_exact_assignment_expr_type(typ)
16481660
}
16491661

1662+
fn self_dependent_assignment_operator_type(
1663+
db: &DbIndex,
1664+
cache: &mut LuaInferCache,
1665+
expr: &LuaExpr,
1666+
result_slot: usize,
1667+
) -> Option<LuaType> {
1668+
let fallback_type = match expr {
1669+
LuaExpr::ParenExpr(paren_expr) => {
1670+
return self_dependent_assignment_operator_type(
1671+
db,
1672+
cache,
1673+
&paren_expr.get_expr()?,
1674+
result_slot,
1675+
);
1676+
}
1677+
LuaExpr::BinaryExpr(binary_expr) => {
1678+
if binary_expr.get_exprs().is_some_and(|(left, right)| {
1679+
[left, right]
1680+
.into_iter()
1681+
.any(|operand| expr_infers_custom_type(db, cache, operand))
1682+
}) {
1683+
return None;
1684+
}
1685+
1686+
match binary_expr.get_op_token()?.get_op() {
1687+
BinaryOperator::OpAdd
1688+
| BinaryOperator::OpSub
1689+
| BinaryOperator::OpMul
1690+
| BinaryOperator::OpDiv
1691+
| BinaryOperator::OpMod
1692+
| BinaryOperator::OpPow => LuaType::Number,
1693+
BinaryOperator::OpIDiv
1694+
| BinaryOperator::OpBAnd
1695+
| BinaryOperator::OpBOr
1696+
| BinaryOperator::OpBXor
1697+
| BinaryOperator::OpShl
1698+
| BinaryOperator::OpShr => LuaType::Integer,
1699+
BinaryOperator::OpConcat => LuaType::String,
1700+
BinaryOperator::OpLt
1701+
| BinaryOperator::OpLe
1702+
| BinaryOperator::OpGt
1703+
| BinaryOperator::OpGe
1704+
| BinaryOperator::OpEq
1705+
| BinaryOperator::OpNe => LuaType::Boolean,
1706+
_ => return None,
1707+
}
1708+
}
1709+
LuaExpr::UnaryExpr(unary_expr) => {
1710+
if unary_expr
1711+
.get_expr()
1712+
.is_some_and(|operand| expr_infers_custom_type(db, cache, operand))
1713+
{
1714+
return None;
1715+
}
1716+
1717+
match unary_expr.get_op_token()?.get_op() {
1718+
UnaryOperator::OpNot => LuaType::Boolean,
1719+
UnaryOperator::OpLen | UnaryOperator::OpBNot => LuaType::Integer,
1720+
UnaryOperator::OpUnm => LuaType::Number,
1721+
UnaryOperator::OpNop => return None,
1722+
}
1723+
}
1724+
_ => return None,
1725+
};
1726+
1727+
let expr_type = match try_infer_expr_no_flow(db, cache, expr.clone()) {
1728+
Ok(Some(expr_type)) => expr_type
1729+
.get_result_slot_type(result_slot)
1730+
.unwrap_or(LuaType::Nil),
1731+
Ok(None) | Err(_) if result_slot == 0 => fallback_type,
1732+
Ok(None) | Err(_) => return None,
1733+
};
1734+
1735+
(expr_type.is_boolean() || expr_type.is_number() || expr_type.is_string()).then_some(expr_type)
1736+
}
1737+
1738+
fn expr_infers_custom_type(db: &DbIndex, cache: &mut LuaInferCache, expr: LuaExpr) -> bool {
1739+
try_infer_expr_no_flow(db, cache, expr)
1740+
.ok()
1741+
.flatten()
1742+
.is_some_and(|typ| typ.any_type(LuaType::is_custom_type))
1743+
}
1744+
16501745
fn is_partial_assignment_expr_compatible(
16511746
db: &DbIndex,
16521747
source_type: &LuaType,

0 commit comments

Comments
 (0)