Skip to content

fix(python): separate package symbol scope from file identity - #2528

Open
DeusData wants to merge 10 commits into
mainfrom
fix/python-init-symbol-scope-v2
Open

DeusData wants to merge 10 commits into
mainfrom
fix/python-init-symbol-scope-v2

Conversation

@DeusData

@DeusData DeusData commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Functions and classes declared in pkg/__init__.py have names such as pkg.Field, but resolution used the file identity pkg.__init__ as their symbol scope. This could lose precise call edges and choose a same-named base class from another module.

Keep the raw file identity for Module nodes and top-level callers, and use package scope for Python symbols. Propagate explicit language through resolution, include Python package definitions in import filtering, preserve complete long qualified names, and report scope allocation failures explicitly. Other languages and the public raw/lineage resolver APIs retain their scope semantics.

Regression coverage includes ordinary-module controls, package-local and imported calls, inheritance collisions, sequential and parallel indexing, scope allocation failures, import-filter fallback, and resolver cache/long-name boundaries.

Stacked on #2525 (which includes #2522 and #2498). This PR targets main, so its diff includes those dependencies until they land. Existing indexes need reindexing to acquire corrected edges.

Parallel resolver error slots use matching tracked allocation and cleanup. Import-filter failures share cleanup while preserving the full-registry fallback. The existing raw-allocation allowances are unchanged.

Validation: source review, formatting, and diff checks. Builds, tests, compiler/syntax checks, revert proof, and local CI are UNRUN, as requested. GitHub CI is pending.

@DeusData

DeusData commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Checkpoint and handover (2026-10-03 UTC)

Published head: 71337a2853c1aef198dc0b882565a817d3a1c7f5. The remote head was verified.

Python init handling is published across 14 files with 22 new tests, unrun locally. Prerequisite: #2525. The allocator follow-up preserves shared-filter failure cleanup and checked parallel-array allocation without relaxing the baseline. #2529 is the next dependent PR.

Hosted snapshot at 2026-10-03 21:54:22 UTC: 3 queued. Confirm the required checks on this exact head before treating it as ready.

No additional local build, test, lint, sanitizer, benchmark or CI runs were performed at this checkpoint, as requested. Earlier executed evidence remains historical; prepared tests and the newer source-reviewed changes must still be validated by the hosted gate.

The campaign is paused at the maintainer’s request. Local monitoring has stopped; hosted jobs remain running. No merge was performed. Thanks for reviewing this change.

Memoize C expression evaluation, index repeated scope and registry queries,
and report incomplete walks explicitly on memo or depth failure.

Refs #1527

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Use tracked zeroed storage for the parallel LSP failure array and
release it with the matching memory class after worker joins.
Keep existing raw-allocation allowances unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…ds the cross walk

The cross-stage memo and depth failure cases defined S and called s.m()
in the same file. The per-file LSP already resolves that call, and the
parallel driver skips the cross-file LSP walk for a file whose call sites
are all resolved, so the CBM_TEST_*_FAIL_STAGE=cross seam never fired on
the parallel path and the four parallel cases saw no file error.

Move S into a second file (memo_def.cpp / memo_def.py, imported from
Python), so run() holds a call only the cross walk can resolve. The
assertions are unchanged.

With the corrected fixture all 17 issue-1527 pipeline cases pass in 3 of
3 runs; removing the parallel failure capture in pass_parallel.c turns
exactly the four parallel cross cases RED again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The depth guard of the C/C++ and Python expression evaluators
(C_EVAL_DEPTH_LIMIT, PY_LSP_MAX_EVAL_DEPTH = 256) now reports a sticky
per-file failure that stops the walk and drops every LSP resolution of the
file. Both evaluators recursed once per chain link, so a 300-term sum or a
C++ fluent chain of ~130 calls counted as 256 levels of nesting: one long
expression cost its whole file every LSP call edge. A chain's length is not
nesting, and the guard must not decide graph content for it.

c_eval_expr_type and py_eval_expr_type now first walk down the chain
operands below the node (the operand the evaluator evaluates first and
derives the link's type from), then evaluate them innermost first at the
caller's depth, keeping pending links on a small arena-owned stack in the
context that nested evaluations share. When a link is evaluated, its chain
operand is already memoized, so the evaluator's own recursive call into it
is a memo hit one frame deep. Every node is still evaluated by the
unchanged inner evaluator, evaluation does not mutate scope within one
outermost call, and only operands the recursion itself evaluates are
pre-evaluated, so the types are exactly the recursive ones.

No longer counted as depth (any length):
- C/C++: binary operators other than == != < > <= >= && || (those yield
  bool without evaluating an operand), member access (. and ->), calls on
  a non-name callee, subscripts, conditional consequences, parentheses,
  comma lists.
- Python: binary operators, attribute access, calls on obj.m(...), f()()
  and (expr)(), subscripts, the else branch of a conditional, parentheses.

Still counted, unchanged guard and reporting (genuine nesting):
- C/C++: call arguments used for template argument deduction,
  std::move/std::forward operands, unary, pointer and update operands,
  assignment left sides, lambda return expressions, co_await, fold and
  _Generic operands.
- Python: tuple/list/set elements, dictionary keys and values, the true
  branch of a conditional, await operands, next()/iter()/assert_type()
  arguments, called lambdas.

Proof:
- New c_lsp and py_lsp tests build chains of 300, 2,000 and 20,000 terms
  (a binary + chain and a .a() receiver chain) next to an unrelated s.m().
  On the previous evaluator, and with this change reverted, all four fail
  at 300 terms: the depth error is recorded and the file resolves 0 calls
  (C++ per-file; Python per-file and through the cross-file dispatch).
  With it, every size records no error, resolves run -> S.m, and the
  chain's result type resolves x.done() / the final .done().
- Real input, isolated cache, before and after:
  redis 4f20cb48 (C): 36,876 CALLS, 10,699 LSP-resolved;
  django fb113765 (Python): 60,123 CALLS, 25,153 LSP-resolved.
  Identical CALLS edge sets on both (0 lost, 0 gained, 0 strategy
  changes), 0 files with an LSP error before and after, wall time
  unchanged (redis 5.5 s, django 9.1-10.2 s both ways).

Refs #1527

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Preserve the arena-owned raw Module caller name so top-level resolutions
join the extractor in both pipeline drivers. Add occurrence and ownership
controls, including package modules and named lambdas.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Resolve symbols declared in __init__.py under their package while retaining
the raw module identity for file and top-level caller nodes. Carry explicit
language through registry callers, select Python package imports, preserve
long exact names, and report scope allocation failures without partial output.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Use tracked zeroed storage for the parallel LSP failure array and
release it with the matching memory class after worker joins.
Share import-filter failure cleanup without changing fallback state.
Keep existing raw-allocation allowances unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_scope_lookup_local() returns the frame binding (CBMVarBinding), not
its type, so passing it to pylsp_init_named_type() did not compile
(-Werror=incompatible-pointer-types). Add pylsp_local_type(), which
returns the binding's type field or NULL when the name is unbound, and
assert on that type as the test intended.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…alk call

The cross and register scope-failure cases defined S and called s.m()
inside pkg/__init__.py. The per-file LSP already resolves those calls,
and the parallel driver skips the cross-file walk for a file whose call
sites are all resolved, so CBM_TEST_PY_LSP_SCOPE_FAIL_STAGE=cross and
=register never fired on the parallel path and both parallel cases saw
no file error.

For those two stages, move S into memo_def.py (imported by
pkg/__init__.py) so run() holds calls only the cross walk resolves, and
count that module's File node. The raw stage keeps its single-file
fixture; the error assertions are unchanged.

With the corrected fixture all eight package-scope pipeline cases pass;
removing the CBM_PY_LSP_SCOPE_ERROR capture in pass_parallel.c turns
exactly the two parallel cross/register cases RED again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…vice_edge

The call now carries both the route mount and the caller language;
wrap its arguments the way clang-format lays them out so lint-format
passes. No code change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData
DeusData force-pushed the fix/python-init-symbol-scope-v2 branch from 71337a2 to 6e47cf0 Compare October 4, 2026 22:17

This branch has not been deployed

No deployments
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