Skip to content

fix: stop repeated assignments from hanging#1115

Open
lewis6991 wants to merge 1 commit into
EmmyLuaLs:mainfrom
lewis6991:issue1114
Open

fix: stop repeated assignments from hanging#1115
lewis6991 wants to merge 1 commit into
EmmyLuaLs:mainfrom
lewis6991:issue1114

Conversation

@lewis6991

Copy link
Copy Markdown
Collaborator

Problem

Repeated assignments like this could make semantic model building hang:

wnd = wnd .. config.pic[idx][index]
wnd = wnd .. config.pic[idx][index]
wnd = wnd .. config.pic[idx][index]

The same issue can happen with other simple binary operators:

total = total + config.pic[idx][index]

Flow replay tries to infer the right-hand side by querying the values it depends on. For x = x <op> unresolved, that can query x at the same assignment path over and over.

Solution

When an assignment RHS is a self-dependent built-in binary expression, return the primitive result type directly instead of starting another flow replay.

The shortcut stays narrow:

  • non-self RHS assignments still use normal flow replay
  • custom operators still use normal flow replay
  • repeated self-concat and self-add both build a semantic model

Tests

  • cargo test -p emmylua_code_analysis test_assignment_rhs_keeps_flow_dependent -- --nocapture
  • cargo test -p emmylua_code_analysis issue_1114 -- --nocapture
  • cargo test -p emmylua_code_analysis test_assignment_binary_rhs_replays_non_self_dependency -- --nocapture
  • cargo test -p emmylua_code_analysis test_binary_assignment_infer_error_keeps_previous_type -- --nocapture
  • cargo test -p emmylua_code_analysis flow
  • cargo fmt --all --check
  • git diff --check

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Issues Found:

  1. Potential Infinite Loop Risk (Critical)

    • The new binary_assignment_operator_type function returns Some(LuaType::Number) for arithmetic operators and Some(LuaType::String) for concatenation. However, the condition at line 1074-1083 only checks for Integer, Number, or String types. If a custom type with @operator add returns a different type (e.g., Counter), the fallback to LuaType::Number will be incorrect.
  2. Inconsistent Type Inference (Medium)

    • In test_binary_assignment_infer_error_keeps_previous_type, the test expects "string" when assigning config.pic + 1 to a string variable. This behavior might be confusing - silently keeping the previous type instead of reporting an error could mask real issues.
  3. Missing Error Handling (Low)

    • The try_infer_expr_no_flow call at line 1074 could return Err, but the match arm at line 1077 treats Err the same as Ok(None) for result_slot != 0. This might hide legitimate inference errors.

Recommendations:

  1. Add type checking for custom operator types:
// After the existing condition, add:
&& !binary_expr.get_exprs().is_some_and(|(left_expr, right_expr)| {
    [left_expr, right_expr].iter().any(|operand| {
        try_infer_expr_no_flow(self.db, self.cache, operand.clone())
            .ok()
            .flatten()
            .is_some_and(|typ| typ.any_type(LuaType::is_custom_type))
    })
})
  1. Consider adding a warning or error for type mismatch in binary assignments rather than silently preserving the previous type.

  2. Improve error handling for try_infer_expr_no_flow:

// Instead of treating Err as None, log or propagate the error
Err(e) => {
    log::warn!("Inference error in binary assignment: {:?}", e);
    None
}
  1. Add more test coverage for edge cases:

    • Custom types with @operator that return non-primitive types
    • Mixed operator types (e.g., string + number)
    • Nested binary expressions in assignments
  2. Consider performance implications - The repeat(512) in tests might be excessive. Consider reducing to a smaller number that still reproduces the issue (e.g., 100-200).

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes flow replay for self-dependent binary assignments (such as repeated additions or string concatenations) by introducing a shortcut that resolves them to fallback primitive types, preventing excessive recursion. It also adds several unit tests to verify these scenarios. The review feedback is highly constructive, pointing out that the type matching is too restrictive and should support constant variants of primitive types, and identifying an unused import.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1085 to +1092
if let Some(expr_type) = expr_type
&& matches!(
expr_type,
LuaType::Integer | LuaType::Number | LuaType::String
)
{
return Ok(self.finish_walk(walk, expr_type));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The pattern match matches!(expr_type, LuaType::Integer | LuaType::Number | LuaType::String) is too restrictive because it only matches the base primitive types. It will not match constant types like LuaType::IntegerConst, LuaType::FloatConst, LuaType::StringConst, LuaType::DocIntegerConst, or LuaType::DocStringConst.

Using the helper methods is_integer(), is_number(), and is_string() on LuaType will correctly match both the base types and their constant variants, ensuring the shortcut is correctly applied when constant types are inferred.

Suggested change
if let Some(expr_type) = expr_type
&& matches!(
expr_type,
LuaType::Integer | LuaType::Number | LuaType::String
)
{
return Ok(self.finish_walk(walk, expr_type));
}
if let Some(expr_type) = expr_type
&& (expr_type.is_integer() || expr_type.is_number() || expr_type.is_string())
{
return Ok(self.finish_walk(walk, expr_type));
}

use crate::{
CacheEntry, DbIndex, FlowId, FlowNode, FlowNodeKind, FlowTree, InferFailReason, LuaDeclId,
LuaInferCache, LuaMemberId, LuaSignatureId, LuaType, TypeOps, check_type_compact,
LuaInferCache, LuaMemberId, LuaSignatureId, LuaType, LuaTypeNode, TypeOps, check_type_compact,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The import LuaTypeNode is not used anywhere in this file and can be safely removed.

Suggested change
LuaInferCache, LuaMemberId, LuaSignatureId, LuaType, LuaTypeNode, TypeOps, check_type_compact,
LuaInferCache, LuaMemberId, LuaSignatureId, LuaType, TypeOps, check_type_compact,

@lewis6991

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a performance optimization to prevent deep recursive flow replays when analyzing self-dependent assignment chains (such as x = x + 1 or wnd = wnd .. value), resolving Issue 1114. It adds helper functions to determine the type of built-in operators directly and includes several test cases. The review feedback points out that the optimization's type matching is too restrictive because it only checks for base primitive types and misses constant variants (e.g., IntegerConst, StringConst), which could cause the optimization to be bypassed for constant expressions and lead to hangs.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +1735 to +1739
matches!(
expr_type,
LuaType::Boolean | LuaType::Integer | LuaType::Number | LuaType::String
)
.then_some(expr_type)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current matches! check only matches the base primitive types (Boolean, Integer, Number, String). However, try_infer_expr_no_flow can return constant variants of these types (such as BooleanConst, IntegerConst, FloatConst, StringConst, or DocBooleanConst) if the operands can be partially or fully evaluated/inferred as constants.

If a constant type is returned, the shortcut will not be taken, and the engine will fall back to a full flow replay. This can cause hangs or timeouts on long assignment chains of constant expressions (e.g., total = -total or x = x + 0).

We should expand the matches! check to also include these constant variants to ensure the performance shortcut is reliably taken.

    matches!(
        expr_type,
        LuaType::Boolean
            | LuaType::BooleanConst(_)
            | LuaType::DocBooleanConst(_)
            | LuaType::Integer
            | LuaType::IntegerConst(_)
            | LuaType::Number
            | LuaType::FloatConst(_)
            | LuaType::String
            | LuaType::StringConst(_)
    )
    .then_some(expr_type)

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 EmmyLuaLs#1114

Assisted-by: Codex
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant