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

@lewis6991 lewis6991 commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

Repeated self-dependent assignments can make semantic model building replay the same assignment while it is still resolving the RHS:

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

The dependency replay for value asks for value again at the same assignment path, so long chains can stall analysis.

Solution

Resolve plain self-dependencies inside assignment RHS replay as unknown before scheduling the dependent replay. This breaks the recursive dependency cycle at the point where the RHS reads the value being assigned.

The handling stays narrow: non-self RHS dependencies keep normal flow replay, and and/or assignments still use the antecedent value because short-circuiting depends on it.

Fixes #1114

@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));
}

Comment thread crates/emmylua_code_analysis/src/semantic/infer/narrow/get_type_at_flow.rs Outdated
@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)

Self-dependent assignment RHS replay can ask for the same assignment
value while resolving the expression. Resolve those plain self reads as
unknown inside replay, while keeping short-circuit assignments flow-aware.

This covers repeated operator chains and generic call/index replay while
preserving flow-aware dependencies for non-self reads.

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.

Stuck loading

1 participant