Skip to content

fix(commonlisp): keep specializes edge when the specializer is in another file - #3543

Open
fade wants to merge 1 commit into
Graphify-Labs:v8from
fade:commonlisp-crossfile-specializes
Open

fade wants to merge 1 commit into
Graphify-Labs:v8from
fade:commonlisp-crossfile-specializes

Conversation

@fade

@fade fade commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

A defmethod dispatching on a class defined in another file loses its specializes edge.

The specializer is resolved with _cl_id(stem, name), a file-scoped id. When the class lives in a
different file no node carries that id, so the dangling-edge filter removes the edge. Because most
methods in a Common Lisp system are defined apart from the class they dispatch on, this removes
nearly every specializes edge a real codebase would have.

This resolves the specializer through ensure_class_ref, the same sourceless-stub path the
cross-file superclass fix already uses, so the corpus rewire collapses the stub onto the real
defclass when the corpus holds exactly one. A same-file specializer still binds to its local node.

It also adds specializes to _SUPERTYPE_RELATIONS. Functions and types occupy separate namespaces
in Common Lisp, so one symbol is routinely both: list, stream, condition, pathname. Without
that guard a specializer stub can collapse onto a same-named function and assert a dispatch
relationship that does not exist. specializes appears nowhere outside the Common Lisp extractor,
so the guard reaches no other language.

Reproduction

(defclass circle () ())
(defgeneric area (obj))
(defmethod area ((obj circle)) 1)   ; same file: edge present
(defmethod area ((obj square)) 2)   ; square defined elsewhere: edge dropped

Before: one specializes edge. After: two, the second resolving through a sourceless stub.

Two tests. test_cl_crossfile_specializer_specializes_edge_survives asserts the same-file case still
binds locally, so the fix cannot pass by making everything a stub, and
test_specializer_does_not_bind_to_same_named_function covers the namespace guard.

…ther file

A defmethod dispatching on a class defined in another file lost its specializes
edge. The specializer resolved to a file-scoped id with no backing node, so the
dangling-edge filter pruned the edge. Resolve through the same sourceless stub
the cross-file superclass path uses, so the corpus rewire collapses it onto the
real defclass; a same-file specializer still binds locally.

Most methods in a Common Lisp system live apart from the class they dispatch
on, so this removed nearly every specializes edge in a real codebase.

Add specializes to the supertype relations at the same time. Functions and
types occupy separate namespaces in Common Lisp, so one symbol is routinely
both, and without the guard a specializer stub can collapse onto a same-named
function and assert a dispatch relationship that does not exist.

@graphify-labs graphify-labs 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.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

Fixes Common Lisp method specializers dropping their specializes edges: extract_commonlisp now resolves the specializer type through ensure_class_ref (the same sourceless-stub path used for cross-file superclasses) instead of minting a file-scoped id that pointed at a non-existent node and got pruned by the dangling-edge filter. Adds specializes to _SUPERTYPE_RELATIONS in _rewire_unique_stub_nodes so a specializer stub is never rebound to a same-named function, since functions and types share names but occupy separate namespaces in CL. Adds tests covering that same-file specializers still bind locally, cross-file ones survive as sourceless stubs, and a specializer never collapses onto a function.

Worth a look

  • Forward-declared Common Lisp specializers can bind to an external stub instead of the later local classgraphify/extractors/commonlisp.py:322 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2281 functions depend on the 818 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 591 callers, 44 callees
  • new: _rebuild_code() — 116 callers, 51 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: dispatch_command() — 2 callers, 124 callees
  • new: extract_commonlisp() — 29 callers, 6 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • new: run_pipeline() — 8 callers, 13 callees
  • …and 33 more — each is listed as a finding

Verification — 2281 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 2106 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

109 of 276 test file(s) selected (39%) via static blast radius.

  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_commonlisp_specializer_binding.py — impact, changed-test
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_file_node_id_spec.py — impact
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_go_builtin_call_targets.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_import_self_loops.py — impact
  • tests/test_imported_export_forwarding.py — impact
  • tests/test_incremental.py — impact
  • tests/test_indirect_call_arrow_single_param_shadow.py — impact
  • tests/test_indirect_call_catch_binding_shadow.py — impact
  • tests/test_indirect_call_external_import_shadow.py — impact
  • tests/test_indirect_call_for_of_binding_shadow.py — impact
  • tests/test_indirect_call_function_expression_shadow.py — impact
  • tests/test_indirect_call_nested_closure_shadow.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_inferred_confidence_rubric.py — impact
  • tests/test_inherited_field_receivers.py — impact
  • tests/test_issue_3405_python_resolution.py — impact
  • tests/test_java_member_calls.py — impact
  • … and 59 more

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify \_rewire\_unique\_stub\_nodes.

The verifier did not have enough to check \_rewire\_unique\_stub\_nodes, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: the input domain has 81 values but only 9 distinct were tested — a small finite domain must be EXHAUSTED, not sampled (an untested input could invert the result)

Could not verify: Could not verify extract\_commonlisp.

The verifier did not have enough to check extract\_commonlisp, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 41 more finding(s) on lines outside this diff (see the check run).

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