Skip to content

fix(types): рекурсивная функция получает свой же тип, а не теряет его - #4440

Merged
nixel2007 merged 1 commit into
developfrom
fix/recursive-return-knot
Aug 11, 2026
Merged

fix(types): рекурсивная функция получает свой же тип, а не теряет его#4440
nixel2007 merged 1 commit into
developfrom
fix/recursive-return-knot

Conversation

@nixel2007

@nixel2007 nixel2007 commented Aug 11, 2026

Copy link
Copy Markdown
Member

Проблема

Функция, вызывающая саму себя, теряла весь вклад рекурсивной ветки. Живой пример из БСП — УправлениеДоступомСлужебный.УпрощенноеУсловиеОграничения, обход дерева условий:

Функция УпрощенноеУсловиеОграничения(Знач Условие, Контекст, ...)
    Аргумент = УпрощенноеУсловиеОграничения(Условие.Аргумент, Контекст);  // вызов самой себя
    Условие  = Новый Структура("Узел, Аргумент", Условие.Узел, Аргумент); // результат кладётся полем
    Возврат Условие;

Тип такой функции — решение уравнения T = Структура{Аргумент: T}. Вместо этого поле Аргумент пропадало из типа целиком.

Причина не в защите от циклов, а раньше: computeReturnTypes не помечал считаемый метод как считающийся — не клал его в стек расчёта. Из-за этого вызов функцией самой себя не распознавался как рекурсия: он шёл обычным путём в MethodReturnTypeIndexer за значением метода, которого там ещё нет (текущий расчёт его как раз и считает), и получал пустоту. По логу видно, что вызов при этом резолвится штатно — есть и ссылка, и символ, — а стек расчёта пуст.

Что сделано

  1. Метод кладётся в стек расчёта до разбора собственного тела, поэтому вызов самого себя распознаётся как рекурсия.
  2. Рекурсивное ребро отдаёт ссылку на метод (LazyTypeSet) вместо содержимого. Механизм тот же, что уже работает для объявленных самоссылок Узел: Массив из см. Узел: ссылка равна себе по ключу, поэтому подстановка не углубляет тип, а разыменование выражения под курсором форсит по одному уровню.
  3. Тело пересчитывается со своим же приближением, пока набор имён полей растёт: первый проход знает поля, заполненные не из рекурсии, второй — и те, что пришли из неё. Дальше имена перестают расти и расчёт останавливается сам; предел в три прохода — страховка. Уточняющий проход не кэшируется и кэш окружений не читает, иначе получил бы ответ первого прохода.

Замеры

Пакетный анализ ssl_3_1, репортер SARIF, mode: only с UnknownMember + EventHandlerInvalidSignature.

develop ветка
замечаний 31503 / 31528 31609 / 31530
время 58–59 с 58–62 с

Разница в пределах собственной недетерминированности анализа (#4429): у develop между двумя прогонами расходится 273 строки подписи. Деградации по времени нет.

Отдельно — на сборке с наработками по #4429, где каждая функция считается отдельной единицей расчёта и рекурсивное ребро отдаёт накопленное значение. Там подстановка содержимого вкладывала тип на уровень глубже с каждой волной, и это давало разрастание в гигабайты:

без узла с узлом
слияний наборов с декорациями 17 432 017 1 879 228
время 126–134 с 92–109 с
пиковый RSS 5,2 ГБ 4,0 ГБ
дельта замечаний между прогонами 0 0

Тесты

RecursiveReturnTypeTest на фикстуре types/RecursiveReturn.bsl: функция, кладущая свой же результат в поле структуры, получает тип T = Структура{Имя, Вложенный: T}. До правки поле Вложенный в типе отсутствовало.

Связанные задачи

Найдено при работе над #4429. Заменяет закрытый #4439: тот ограничивал глубину слияния наборов, то есть боролся со следствием; с этой правкой ограничитель не срабатывает ни разу, и прогон без него проходит штатно.

Чего здесь нет

Взаимная рекурсия (А вызывает Б, Б вызывает А) не лечится — поле теряется у той функции, которую обход дерева символов встретил первой. Разбор и замеры — в треде ниже; коротко: дешёвое лечение через существующий проход доразрешения на develop не сходится, потому что упирается в стирку записей при перечитывании документа (#4429), а путь редактора им не лечится вовсе. Возьму отдельно.

`computeReturnTypes` не помечал считаемый метод как считающийся, поэтому вызов
функцией самой себя выглядел обычным вызовом и шёл в индекс за значением,
которого там ещё нет. Возвращалась пустота, и весь вклад рекурсивной ветки
терялся: поле, заполняемое таким вызовом, пропадало из типа целиком.

Теперь метод кладётся в стек расчёта до разбора собственного тела, а
рекурсивное ребро отдаёт ссылку на метод (LazyTypeSet) вместо содержимого:
ссылка равна себе по ключу, поэтому подстановка не углубляет тип. Тело
пересчитывается со своим же приближением, пока набор имён полей растёт — двух
проходов хватает, третий предусмотрен страховкой.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017cu7S3zYn7n6GMsdYf5v1q
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The inferencer now supports recursive return types through lazy self-referential approximations and bounded refinement. A Spring-integrated test verifies recursive nested fields.

Changes

Recursive return-type inference

Layer / File(s) Summary
Recursive type approximation
src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java
Recursive calls return recursiveKnot approximations. Method analysis marks the method as active before body inference.
Bounded recursive refinement
src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java
Recursive methods are recalculated with prior results as approximations. Refinement passes stop when fields stabilize or the pass limit is reached. Flow-analysis caching is disabled during refinement.
Recursive return-type validation
src/test/java/com/github/_1c_syntax/bsl/languageserver/types/RecursiveReturnTypeTest.java
The integration test verifies top-level and nested fields in a recursive function result.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Test
  participant ExpressionTypeInferencer
  participant RecursiveMethod
  participant FlowAnalysis
  Test->>ExpressionTypeInferencer: query recursive function expression type
  ExpressionTypeInferencer->>RecursiveMethod: analyze method body
  RecursiveMethod->>ExpressionTypeInferencer: call recursive method
  ExpressionTypeInferencer->>FlowAnalysis: refine recursive return type
  FlowAnalysis-->>ExpressionTypeInferencer: return stabilized fields
  ExpressionTypeInferencer-->>Test: return nested recursive type
Loading

Possibly related PRs

Suggested reviewers: claude, sfaqer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving the inferred type of recursive functions.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/recursive-return-knot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java`:
- Around line 884-925: Track the currently refined method across
refinedByOwnValue and the MethodReturnTypeIndexer lookup path, so an indirect
cycle such as A → B → A detects the repeated active method before
computeIfAbsent re-entry and returns recursiveKnot. Preserve existing refinement
and dependency propagation behavior, and add an integration test covering the
indirect recursion scenario.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fcc55e82-4416-4703-b14f-bcc07b9a4d46

📥 Commits

Reviewing files that changed from the base of the PR and between 6c319a1 and 7b89536.

⛔ Files ignored due to path filters (1)
  • src/test/resources/types/RecursiveReturn.bsl is excluded by !src/test/resources/**
📒 Files selected for processing (2)
  • src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java
  • src/test/java/com/github/_1c_syntax/bsl/languageserver/types/RecursiveReturnTypeTest.java

Comment on lines +884 to +925
if (ctx.cycleCut && !types.isEmpty() && !ctx.inProgress.containsKey(method)) {
types = refinedByOwnValue(method, types, ctx);
}
return new MethodReturnTypeIndexer.ComputedReturnTypes(types, Set.copyOf(ctx.consulted), ctx.sawMissing);
} catch (StackOverflowError | RuntimeException e) {
return new MethodReturnTypeIndexer.ComputedReturnTypes(TypeSet.EMPTY, Set.of(), false);
}
}

/**
* Значение рекурсивной функции, пересчитанное по телу с её же первым приближением.
* <p>
* В первом проходе рекурсивное ребро отвечать нечем: значения метода ещё нет, и поле,
* заполняемое вызовом самого себя, теряется целиком. Второй проход идёт с приближением
* на руках, поэтому ребро отдаёт узел ({@link #recursiveKnot}) и поле остаётся — со
* ссылкой на метод вместо вложенного значения. Больше двух проходов не нужно: узел
* равен себе по ключу, поэтому третий дал бы то же самое.
*
* @param method рекурсивная функция.
* @param approximation значение, посчитанное первым проходом.
* @param ctx контекст расчёта; в него переносится всё, что увидел второй проход.
* @return уточнённое значение; приближение, если второй проход не дал ничего.
*/
private TypeSet refinedByOwnValue(MethodSymbol method, TypeSet approximation, InferenceContext ctx) {
var current = approximation;
for (var pass = 0; pass < MAX_REFINING_PASSES; pass++) {
var refining = new InferenceContext(ctx.documentContext);
refining.depth = ctx.depth + 1;
refining.refining = true;
refining.visited.add(method);
refining.inProgress.put(method, current);
var refined = returnTypeFromBodyInference.of(method, expression -> inferInternal(expression, refining));
ctx.consulted.addAll(refining.consulted);
ctx.dependencies.addAll(refining.dependencies);
ctx.sawMissing = ctx.sawMissing || refining.sawMissing;
if (refined.isEmpty() || refined.getAllFieldNames().equals(current.getAllFieldNames())) {
return refined.isEmpty() ? current : refined;
}
current = refined;
}
return current;
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

indexer="$(fd -a '^MethodReturnTypeIndexer\.java$' src/main/java | head -n 1)"
test_file="$(fd -a '^RecursiveReturnTypeTest\.java$' src/test/java | head -n 1)"

ast-grep outline "$indexer" --items all
rg -n -C 8 'computeIfAbsent|computeReturnTypes|inferredReturnTypes' "$indexer"

sed -n '1,220p' "$test_file"
rg -n -C 6 'recursiveKnot|cycleCut|inProgress|refinedByOwnValue' \
  src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java

Repository: 1c-syntax/bsl-language-server

Length of output: 17103


🏁 Script executed:

#!/bin/bash
set -euo pipefail

indexer="$(fd -a '^MethodReturnTypeIndexer\.java$' src/main/java | head -n 1)"
inferencer="$(fd -a '^ExpressionTypeInferencer\.java$' src/main/java | head -n 1)"

printf '%s\n' '--- indexer computeIfAbsent and storage ---'
sed -n '118,170p' "$indexer"
sed -n '300,370p' "$indexer"

printf '%s\n' '--- method inference and body inference ---'
sed -n '740,895p' "$inferencer"

printf '%s\n' '--- all indirect-recursion fixtures and tests ---'
rg -n -i -C 5 'indirect|mutual|взаим|цик[лк]|recursive|рекурс' src/test src/main \
  -g '*.java' -g '*.bsl' -g '*.md' || true

printf '%s\n' '--- callers of computeIfAbsent and returnTypesOfBody ---'
rg -n -C 5 'computeIfAbsent\(|returnTypesOfBody\(' src/main/java src/test -g '*.java'

Repository: 1c-syntax/bsl-language-server

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

indexer="$(fd -a '^MethodReturnTypeIndexer\.java$' src/main/java | head -n 1)"
inferencer="$(fd -a '^ExpressionTypeInferencer\.java$' src/main/java | head -n 1)"

printf '%s\n' '--- exact indexer methods ---'
rg -n 'computeIfAbsent|store\(|ComputedReturnTypes|indexed' "$indexer" | head -n 80
sed -n '120,160p' "$indexer"
sed -n '300,350p' "$indexer"

printf '%s\n' '--- exact inferencer call path ---'
rg -n 'methodReturnType|returnTypesOfBody|refinedByOwnValue|visited\.add|visited\.remove|inProgress\.put|inProgress\.remove|cycleCut' "$inferencer"
sed -n '760,895p' "$inferencer"

printf '%s\n' '--- focused indirect-recursion matches ---'
rg -n -i 'indirect|mutual|взаимн|взаим|A.*B|Б.*А|рекурсив' src/test -g '*.java' -g '*.bsl' | head -n 120 || true

Repository: 1c-syntax/bsl-language-server

Length of output: 31805


🏁 Script executed:

#!/bin/bash
set -euo pipefail

inferencer="$(fd -a '^ExpressionTypeInferencer\.java$' src/main/java | head -n 1)"

printf '%s\n' '--- recursion limits and relevant context fields ---'
rg -n -C 3 'MAX_DEPTH|MAX_REFINING_PASSES|class InferenceContext|inProgress|cycleCut|refining' "$inferencer" | head -n 180

printf '%s\n' '--- deterministic call-graph verifier for A -> B -> A ---'
python3 - <<'PY'
indexed = set()
events = []
active = set()

def method_return_type(method, visited):
    if method in visited:
        events.append(f"cycle cut at {method}")
        return
    visited.add(method)
    events.append(f"enter methodReturnType({method})")
    if method == "A":
        compute_if_absent("A", visited)
    elif method == "B":
        compute_if_absent("B", visited)
    visited.remove(method)
    events.append(f"leave methodReturnType({method})")

def compute_if_absent(method, visited):
    if method in indexed:
        events.append(f"skip indexed {method}")
        return
    if method in active:
        events.append(f"REENTER before store {method}")
    events.append(f"computeIfAbsent({method})")
    active.add(method)
    # The method body calls the other method.
    other = "B" if method == "A" else "A"
    method_return_type(other, visited)
    active.remove(method)
    indexed.add(method)
    events.append(f"store {method}")

# First pass computes A. Its body calls B, whose body closes the cycle at A.
# The changed implementation then refines B with only B in inProgress.
method_return_type("A", {"A"})
events.append("refine B: visited={B}, inProgress={B}")
method_return_type("B", {"B"})
events.append("refinement would call A with A absent from visited/inProgress")

for event in events:
    print(event)

assert "REENTER before store A" in events, \
    "The simplified path did not reproduce A re-entry"
print("RESULT: A is re-entered before its first computeIfAbsent call stores a result.")
PY

Repository: 1c-syntax/bsl-language-server

Length of output: 6714


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
indexed = set()
active_computations = set()
events = []

def method_return_type(method, visited):
    if method in visited:
        events.append(f"cycle cut at {method}")
        return
    visited.add(method)
    events.append(f"enter methodReturnType({method})")
    compute_if_absent(method, visited)
    visited.remove(method)
    events.append(f"leave methodReturnType({method})")

def compute_if_absent(method, visited):
    if method in indexed:
        events.append(f"skip indexed {method}")
        return
    if method in active_computations:
        events.append(f"REENTER before store {method}")
    events.append(f"computeIfAbsent({method})")
    active_computations.add(method)
    other = "B" if method == "A" else "A"
    method_return_type(other, visited)
    active_computations.remove(method)
    indexed.add(method)
    events.append(f"store {method}")

# Initial A computation. A calls B, and B calls A, which cuts the cycle.
events.append("initial compute A")
compute_if_absent("A", {"A"})

# returnTypesOfBody(B) sees cycleCut and refines B with only B protected.
events.append("refine B: visited={B}, inProgress={B}")
method_return_type("B", {"B"})

for event in events:
    print(event)

assert "REENTER before store A" in events, \
    "The modeled refinement did not re-enter A before its first store"
print("RESULT: A is re-entered before its first computeIfAbsent call stores a result.")
PY

Repository: 1c-syntax/bsl-language-server

Length of output: 514


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
indexed = set()
active_computations = set()
events = []

def method_return_type(method, visited):
    if method in visited:
        events.append(f"cycle cut at {method}")
        return
    visited.add(method)
    events.append(f"enter methodReturnType({method})")
    compute_if_absent(method, visited)
    visited.remove(method)
    events.append(f"leave methodReturnType({method})")

def compute_body(method, visited):
    other = "B" if method == "A" else "A"
    events.append(f"body({method}) calls {other}")
    method_return_type(other, visited)

def compute_if_absent(method, visited):
    if method in indexed:
        events.append(f"skip indexed {method}")
        return
    if method in active_computations:
        events.append(f"REENTER before store {method}")
    events.append(f"computeIfAbsent({method})")
    active_computations.add(method)
    compute_body(method, visited)
    active_computations.remove(method)
    indexed.add(method)
    events.append(f"store {method}")

# Initial A computation. A calls B, and B calls A, which cuts the cycle.
events.append("initial compute A")
compute_if_absent("A", {"A"})

# returnTypesOfBody(B) sees cycleCut and refines B directly.
# The refinement context protects B only; A is absent from both visited and inProgress.
events.append("refine B: visited={B}, inProgress={B}")
compute_body("B", {"B"})

for event in events:
    print(event)

assert "REENTER before store A" in events, \
    "The modeled refinement did not re-enter A before its first store"
print("RESULT: A is re-entered before its first computeIfAbsent call stores a result.")
PY

Repository: 1c-syntax/bsl-language-server

Length of output: 614


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
indexed = set()
active_computations = set()
events = []

def method_return_type(method, visited):
    if method in visited:
        events.append(f"cycle cut at {method}")
        return
    visited.add(method)
    events.append(f"enter methodReturnType({method})")
    compute_if_absent(method, visited)
    visited.remove(method)
    events.append(f"leave methodReturnType({method})")

def compute_body(method, visited):
    other = "B" if method == "A" else "A"
    events.append(f"body({method}) calls {other}")
    method_return_type(other, visited)

def compute_if_absent(method, visited):
    if method in indexed:
        events.append(f"skip indexed {method}")
        return
    if method in active_computations:
        events.append(f"REENTER before store {method}")
    events.append(f"computeIfAbsent({method})")
    active_computations.add(method)
    compute_body(method, visited)

    # This is the changed returnTypesOfBody condition:
    # B observed the cycle, and its refinement context protects B only.
    if method == "B":
        events.append("refine B: visited={B}, inProgress={B}")
        compute_body("B", {"B"})

    active_computations.remove(method)
    indexed.add(method)
    events.append(f"store {method}")

events.append("initial compute A")
compute_if_absent("A", {"A"})

for event in events:
    print(event)

assert "REENTER before store A" in events, \
    "The modeled refinement did not re-enter A while A was still computing"
print("RESULT: A is re-entered before its first computeIfAbsent call stores a result.")
PY

Repository: 1c-syntax/bsl-language-server

Length of output: 303


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
indexed = set()
active_stack = []
events = []

class ReentryObserved(Exception):
    pass

def method_return_type(method, visited):
    if method in visited:
        events.append(f"cycle cut at {method}")
        return
    visited.add(method)
    events.append(f"enter methodReturnType({method})")
    compute_if_absent(method, visited)
    visited.remove(method)
    events.append(f"leave methodReturnType({method})")

def compute_body(method, visited):
    other = "B" if method == "A" else "A"
    events.append(f"body({method}) calls {other}")
    method_return_type(other, visited)

def compute_if_absent(method, visited):
    if method in indexed:
        events.append(f"skip indexed {method}")
        return
    if method in active_stack:
        events.append(f"REENTER before store {method}")
        raise ReentryObserved(method)
    events.append(f"computeIfAbsent({method})")
    active_stack.append(method)
    try:
        compute_body(method, visited)
        if method == "B":
            events.append("refine B: visited={B}, inProgress={B}")
            compute_body("B", {"B"})
    finally:
        active_stack.pop()
    indexed.add(method)
    events.append(f"store {method}")

try:
    events.append("initial compute A")
    compute_if_absent("A", {"A"})
except ReentryObserved:
    events.append("stopped after first re-entry")

for event in events:
    print(event)

assert "REENTER before store A" in events, \
    "The modeled refinement did not re-enter A while A was still computing"
print("RESULT: A is re-entered before its first computeIfAbsent call stores a result.")
PY

Repository: 1c-syntax/bsl-language-server

Length of output: 507


Track the active cycle method during refinement.

In an A → B → A cycle, refining B protects only B. A is re-entered before computeIfAbsent stores its first result because the indexer tracks indexed methods only. Track the active cycle method and return recursiveKnot for the repeated method. Add an indirect-recursion integration test covering ExpressionTypeInferencer.java:884-915 and MethodReturnTypeIndexer.java:138-142.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java`
around lines 884 - 925, Track the currently refined method across
refinedByOwnValue and the MethodReturnTypeIndexer lookup path, so an indirect
cycle such as A → B → A detects the repeated active method before
computeIfAbsent re-entry and returns recursiveKnot. Preserve existing refinement
and dependency propagation behavior, and add an integration test covering the
indirect recursion scenario.

Copilot AI 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.

Pull request overview

Fixes recursive return-type inference so self-referential fields retain their types.

Changes:

  • Tracks the currently inferred method to detect recursion.
  • Adds lazy recursive type knots and refinement passes.
  • Adds a recursive structure inference test and fixture.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
ExpressionTypeInferencer.java Implements recursive return-type refinement.
RecursiveReturnTypeTest.java Tests nested recursive type preservation.
RecursiveReturn.bsl Provides the recursive BSL fixture.
Suppressed comments (1)

src/main/java/com/github/_1c_syntax/bsl/languageserver/types/inferencer/ExpressionTypeInferencer.java:835

  • During a refining pass this resolver still reads symbolTypeIndex, which does not contain the current approximation until store runs. Thus the recursive node exposes field names but not their types to its own body. For example, with a base branch returning {A: Number}, then R = F(); R.Insert("B", R.A); return R, R.A resolves to empty during refinement and B is never inferred. The element resolver above has the same problem. Resolve lazy contents from the active ctx.inProgress approximation while refining, then fall back to the finalized index after the pass.
          () -> symbolTypeIndex.getReturnTypes(method).getLocalFields(ref)

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

*/
private TypeSet recursiveKnot(MethodSymbol method, InferenceContext ctx) {
var approximation = ctx.inProgress.get(method);
var known = approximation == null ? symbolTypeIndex.getReturnTypes(method) : approximation;
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Test Results

 4 062 files  +6   4 062 suites  +6   55m 1s ⏱️ - 3m 5s
 4 215 tests +1   4 144 ✅ +1   71 💤 ±0  0 ❌ ±0 
25 290 runs  +6  24 860 ✅ +6  430 💤 ±0  0 ❌ ±0 

Results for commit 7b89536. ± Comparison against base commit 6c319a1.

♻️ This comment has been updated with latest results.

@nixel2007

Copy link
Copy Markdown
Member Author

Замечание подтвердилось — воспроизвёл тестом на фикстуре из двух функций, вызывающих друг друга: поле, заполненное вызовом соседа, теряется целиком. Причём теряется у той функции, которую обход дерева символов встретил первой: поменяйте функции местами в модуле — потеряется другое поле.

Попробовал самое дешёвое лечение — считать обрыв цикла незавершённым расчётом, чтобы значение доводил до неподвижной точки уже существующий проход после наполнения области. Замеры на ssl_3_1 (волны прохода, DEBUG индексатора):

вариант волны разборов документов время
как в PR сейчас 1 (62 метода) 32 38 с
обрыв = незавершённость 10, упор в предохранитель 168 54 с
то же + критерий «значение изменилось» 96 → 15 → 13 → 13… не сходится 122 47 с
то же, сравнение с прошлым посчитанным 42 → 11 → 2 → 2… 47 43 с

Последние два метода не сходятся не из-за цикла: они считаются то в значение, то в пустоту, потому что записи их зависимостей стираются при перечитывании документа — это #4429. На ветке, где та стирка вылечена, проход сходится штатно: изменений 8228 → 629 → 48 → 0 за четыре волны, и результат при этом побайтово тот же, что без этой правки.

Вывод: в этот PR не беру. На текущем корпусе выигрыша нет (расхождение подписи 327 строк — внутри собственного разброса анализа 273–363), стоимость +13 % времени, сходимость зависит от чужой правки, а путь редактора она не лечит вовсе — связи внутри документа не хранятся, поэтому в юнит-тесте с одним файлом взаимная рекурсия остаётся красной. Возьму её вместе с работой по #4429 либо отдельной правкой модели (ленивый источник полей у типа), где итерации не нужны вовсе.

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.

2 participants