Skip to content

fix(decl-impl-match): resolve symbols in full annex subtree#5549

Merged
marsninja merged 3 commits intojaseci-labs:mainfrom
marsninja:fix/annex-symbol-resolution
Apr 14, 2026
Merged

fix(decl-impl-match): resolve symbols in full annex subtree#5549
marsninja merged 3 commits intojaseci-labs:mainfrom
marsninja:fix/annex-symbol-resolution

Conversation

@marsninja
Copy link
Copy Markdown
Collaborator

Summary

DeclImplMatchPass previously called _resolve_symbols_in_subtree only on (TypeAlias, GlobalVars, ModuleCode) items of each annex module. Standalone top-level constructs in .impl.jac files — def, obj, node, edge, walker, enum, class — fell through the whitelist, so their body reads were left with .sym = None. That broke type inference on every local inside those constructs and produced spurious W2001: Name 'x' may be undefined warnings for every downstream read.

Concrete symptom (from a real jac-client file):

def _get_platform_asset -> str | None {
    system = platform.system();     # `system` typed as Unknown
    machine = platform.machine();
    system_map = _PLATFORM_MAP.get(system, {});   # W2001: 'system' may be undefined
    return system_map.get(machine, None);         # W2001: 'machine' may be undefined
}

Change

Drop the whitelist in DeclImplMatchPass.transform and resolve the whole module subtree for each primary + annex + variant module. _resolve_symbols_in_subtree is already idempotent (skips Names whose .sym is set), so the matched-impl path — which runs first and owns those bodies — keeps its responsibility. The broadening only fills in the gaps.

After the fix, the same file type-checks cleanly and the evaluator returns the expected types:

L47 system     -> str
L48 machine    -> str
L49 system_map -> dict[str, str]

Test plan

  • jac/tests/compiler/passes/main/test_decl_impl_match_pass.jac — new regression test standalone def/obj/node/walker in .impl.jac get symbol resolution with fixtures standalone_def_in_annex.jac / standalone_def_in_annex.impl.jac. Asserts .sym is resolved on local reads inside each construct type defined entirely in an annex.
  • Full jac/tests/compiler + jac/tests/language suite: 2695 passed, 57 skipped, 1 xfailed, zero new failures. (test_checker_pass.jac::checker_import_missing_module fails identically on main — pre-existing, unrelated.)
  • jac check on the original jac-client bun_installer.impl.jac that motivated the report now passes cleanly with full type flow on locals.

DeclImplMatchPass only called _resolve_symbols_in_subtree on
(TypeAlias, GlobalVars, ModuleCode) items of each annex module.
Standalone top-level constructs in .impl.jac files - def, obj, node,
edge, walker, enum, class - had their body reads left with .sym=None,
breaking type inference on locals and emitting spurious W2001
"may be undefined" warnings for every downstream use.

Drop the whitelist and resolve the whole module subtree instead.
_resolve_symbols_in_subtree is already idempotent (skips Names whose
sym is set), so the matched-impl path continues to own those bodies.

Adds a regression test exercising def / obj / node / walker constructs
defined entirely in a .impl.jac annex with no decl in the primary file.
PyastGenPass used to rewrite `import from foo { X }` into
`if typing.TYPE_CHECKING: from foo import X` whenever every recorded
use of X looked like a type annotation. That classification is unsound:
dataclass, Pydantic, attrs, msgspec, SQLAlchemy, and anything else that
calls typing.get_type_hints() resolves string annotations against the
module's runtime globals, so names like ClassVar / Protocol / TypedDict /
NamedTuple / custom model types need to stay importable at runtime.

The classifier only appeared to work because its input (sym.uses) was
under-populated. Once DeclImplMatchPass began resolving annotation-side
reads correctly, ClassVar got demoted from the runtimelib/test.jac
generated Python, dataclass stopped recognizing
JacTestCheck.test_suite_path as a class variable, and every downstream
.test.jac crashed at pytest collection with
`ValueError: mutable default <class 'dict'>`.

Remove _process_type_only_imports, _is_type_annotation_use, and
_has_runtime_annotation along with their call site in exit_module.
Explicit `with entry { if TYPE_CHECKING { ... } }` blocks in Jac source
continue to work unchanged and remain the right way to break import
cycles. The unused `# jac:runtime` opt-out comment (never referenced by
any in-tree .jac) goes with the classifier.

Adds a regression test that compiles a fixture with a ClassVar-typed
class variable, asserts the generated module has no top-level
TYPE_CHECKING guard, and execs the module to verify dataclass can
resolve the annotation at runtime.

Audit: 1852 .jac files recompile with no regressions. Full jac test
suite: 3235 passed, 0 failed. Downstream packages (jac-mcp, jac-byllm,
jac-client) clean; jac-scale has net -5 failures vs baseline (6
pre-existing parallel-run flakes now pass).
The broadened `_resolve_symbols_in_subtree` walks every Name and
AtomTrailer in the module, including bodies that the prior whitelist
never visited. When the type checker pulls in an imported `.py` file
via `pyast_load_pass`, the synthesized AST occasionally contains
nodes whose parent chain doesn't reach a scope (e.g. `self[k.upper()]`
in `redis/client.py:89`), and `UniScopeNode.sym_tab` raises
`ValueError("No scope found")` when the resolver tries to look them
up. That single node was aborting resolution for the entire imported
module and surfacing through `jac check` as
`Error checking '<file>.jac': No scope found`, with the failed-files
summary printing `0 errors, 0 warnings` because the exception path in
`analysis.impl.jac` returns empty diagnostic lists.

Wrap each `use_lookup` / `chain_use_lookup` call in a `try/except
ValueError`. Symbol resolution is best-effort: failing to bind a single
detached Name leaves its `.sym` as None (the same state the rest of
the compiler already tolerates), but no longer poisons resolution for
the surrounding subtree. Restores `jac check` parity with `origin/main`
on the full corpus (323 passed, 0 failed).
@marsninja marsninja merged commit 97fa399 into jaseci-labs:main Apr 14, 2026
13 checks passed
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