Skip to content

Common Lisp support, rebased onto v8 and moved into extractors/ - #2274

Open
fade wants to merge 11 commits into
Graphify-Labs:v8from
fade:common-lisp-extractors
Open

Common Lisp support, rebased onto v8 and moved into extractors/#2274
fade wants to merge 11 commits into
Graphify-Labs:v8from
fade:common-lisp-extractors

Conversation

@fade

@fade fade commented Jul 28, 2026

Copy link
Copy Markdown

The ask

Do you want Common Lisp as a supported language in graphify? A yes or a no is all I need.
If the answer is no, I will close this and stop. Either answer is genuinely useful to us, and
thank you for the attention it takes to give one.

Where it comes from

This is not a fresh implementation. It descends directly from #172 by @jansaasman, opened
on 9 April, and the history still carries his commits as its base:

  • Add Common Lisp support (language #21), 9 April
  • Common Lisp: walk wrapper macros, reader conditionals, more definer forms, 10 April
  • Fix edge direction lost in JSON, Cypher, HTML, and Canvas exports, 10 April

That work established the extraction approach: which forms count as definitions, how to walk
wrapper macros so definitions nested inside them are not lost, and how reader conditionals are
handled. Those decisions are his and they are still the ones in force here. What has been added
on top is consolidation rather than redesign:

  • typing the rationale nodes so they stop leaking into unrelated node types
  • wiring the file extensions into detection
  • declaring the grammar as an optional extra rather than a hard dependency
  • porting and extending the extraction suite, and silencing a deprecation warning the grammar
    emits
  • listing the language in the README tables

If Common Lisp lands, the credit for the design belongs upstream of this PR.

How it has tracked the codebase since April

graphify has moved a long way in the four months since #172 was opened, and this branch has been
rebased forward twice rather than left to rot:

  1. first onto the v4 line, shortly after the original PR
  2. then onto v8
  3. and again onto current v8 for this PR, across roughly 640 commits of upstream movement,
    spanning the 0.8.35 to 0.9.29 releases

The rebases were not mechanical. Each one surfaced real conflicts where the two sides had grown
into the same place: the code extension set had gained .mts, .cts, CUDA, Metal and others
alongside the four Lisp ones; the optional extras table had gained pascal; the README language
row had grown from 29 grammars to 36. In every case the upstream side was taken and the Lisp
addition re-applied on top, so nothing of yours was reverted by the rebase.

Reorganised to match where extract.py is going

The most significant change since #172 is structural, and it is the reason this PR looks
different from that one.

extractors/MIGRATION.md states that graphify/extract.py is being split into the
graphify/extractors/ package, one language per PR, tracking #1212. Twenty seven language
modules already live there. #172 predates that split and added its extractor to extract.py,
which was correct at the time and is now against the grain.

So the extractor has been moved to graphify/extractors/commonlisp.py, following the playbook in
MIGRATION.md rather than inventing a shape:

  • a self contained module importing only from graphify.extractors.base, never from
    graphify.extract, so the dependency direction the package requires is preserved
  • a facade re-export from graphify.extract, kept alphabetical
  • an entry in LANGUAGE_EXTRACTORS in extractors/__init__.py
  • no test relocation, per MIGRATION.md's rule that a migration edits no tests outside
    test_extractors_registry.py. The registry sweep there now covers commonlisp automatically.

The effect on the file being emptied is the point: extract.py grows by five lines rather than
by five hundred.
The extractor is 492 lines and all of it lands in the new package.

If a new language should go somewhere other than extractors/, say so and I will move it. The
migration document is written for relocating existing extractors and does not state where new
ones belong, so this is an inference from direction of travel, not a claim about your intent.

What it is

  • .lisp, .cl, .lsp and .asd, via tree-sitter-commonlisp.
  • Nodes for packages, classes, structs, conditions, functions, generic functions, methods,
    macros and variables; contains, calls, inherits, specializes and imports edges.
  • The grammar is a commonlisp optional extra, so a default uv tool install graphifyy is
    unchanged, matching how dm, terraform and pascal already behave.
  • 23 tests in tests/test_languages.py against tests/fixtures/sample.lisp, each skipped when
    the grammar is absent, in the same style as the existing dm guard.
  • 8 files, +814 / -4, of which 492 lines are the one new module.
  • Run against a clean checkout of current v8 on the same machine: no test fails here that does
    not also fail on v8 unpatched.

⚠ One thing to flag rather than let you discover: uv.lock has no tree-sitter-commonlisp
entry, while the other optional grammars do. CI installs with --frozen, so the grammar will not
be present there and the Lisp tests will skip rather than run. They will not fail, but they will
also not be proving anything on your side until the lock is regenerated. I have not touched the
lock file, since hand editing it seemed worse than naming the gap.

Why a ruling rather than a review

Two Common Lisp PRs are open, #172 since April and this one since June, and neither has drawn a
comment. I do not read that as neglect: your recently merged PRs are outside contributions landing
within a day or two. It reads instead like these two are asking a different kind of question. Not
"is this code correct" but "does graphify want Common Lisp, and another optional grammar
dependency, at all". That is a scope call, it is yours, and one word settles it.

We maintain a fork in the meantime, so a no costs us nothing and is a perfectly good answer. What
does cost something is a third rebase against silence.

fade and others added 11 commits July 28, 2026 10:28
Add an imperative tree-sitter-commonlisp walker that extracts packages,
classes, functions, methods, macros, generic-function dispatch, and calls
from Common Lisp source. Registered alongside the other custom extractors
so homoiconic CL — where every defXXX form is the same list_lit node — is
classified by reading definer symbols rather than tree-sitter node types.


Co-authored-by: jansaasman <ja@franz.com>
Emit docstring-derived rationale nodes with file_type "rationale" instead
of "code" so the cross-file resolution filter excludes them, keeping
docstrings out of import and symbol-resolution edges.


Co-authored-by: jansaasman <ja@franz.com>
Classify .lisp/.cl/.lsp/.asd as code and route them to the Common Lisp
extractor so ASDF systems and source files build into the graph.


Co-authored-by: jansaasman <ja@franz.com>
Declare the Common Lisp grammar under optional-dependencies, matching the
policy for other niche grammars, so the core install stays lean and Lisp
users opt in with the commonlisp extra.


Co-authored-by: jansaasman <ja@franz.com>
Add the sample.lisp fixture and the Common Lisp extraction tests covering
packages, classes, defuns, generics, macros, method specializers,
inheritance, imports, custom definers, reader conditionals, and docstring
rationale typing.


Co-authored-by: jansaasman <ja@franz.com>
The latest tree-sitter-commonlisp release returns an int pointer that the
tree-sitter runtime only accepts via a deprecated path, so extracting Lisp
spewed a DeprecationWarning per file. Suppress that one warning at the
construction site so Lisp extraction runs clean.


Co-authored-by: jansaasman <ja@franz.com>
Move the Common Lisp extraction tests into the language-extractor suite
where the project keeps per-language coverage, so they sit with their peers
rather than in the general multi-language file.


Co-authored-by: jansaasman <ja@franz.com>
Surface Common Lisp in the supported-extensions table and add the
commonlisp optional-extra row so users know the grammar exists and how to
install it.


Co-authored-by: jansaasman <ja@franz.com>
extract.py is being decomposed one language per module (Graphify-Labs#1212), so a
newly added language belongs in the package rather than adding another
500 lines to the file being emptied. The facade re-export and the
LANGUAGE_EXTRACTORS entry keep every existing importer and the registry
identity guard working unchanged.

The extractor body is unchanged; only the import plumbing moved.
The Common Lisp branch carried a second copy of the "Main extract and
collect_files" banner and _check_tree_sitter_version. Python let it pass
because the two definitions are identical, so nothing failed, but the
file should define it once.
The grammar is an optional extra, so a default install does not have it,
and these tests called the extractor unguarded: absent grammar meant a hard
failure rather than a skip. Every other optional-grammar language already
guards this way.

Matters for a fresh checkout and for any lane that installs from the lock
file, where the extra is not present at all.
@fade fade mentioned this pull request Jul 28, 2026

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

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).


Graphify review — findings

This pull request adds a new Common Lisp extractor. It introduces graphify/extractors/commonlisp.py (backed by the tree-sitter-commonlisp grammar) and wires it into the extractor registry, the _DISPATCH table in extract.py, and the CODE_EXTENSIONS set in detect.py for .lisp, .cl, .lsp, and .asd files. Supporting changes update the README to document the new commonlisp optional install and file-type coverage (bumping the grammar count from 36 to 37), and add a corresponding test suite for the Common Lisp language extraction. The surface area spans detection, extraction dispatch, the new extractor module, docs, and tests.

No blocking issues surfaced.

Analysis details — impact, health, verification

Impact & health

graphify review

Impact — 1844 function(s) in the blast radius of 785 changed node(s).

Health — regressions introduced by this change:

  • new offender: extract_commonlisp() (Ca=25 Ce=1) — 25 callers depend on it (afferent coupling)
  • new offender: _process_form() (Ca=1 Ce=10) — fans out to 10 callees (efferent coupling)
  • new offender: _handle_def_form() (Ca=1 Ce=6) — fans out to 6 callees (efferent coupling)
  • new offender: _handle_defpackage() (Ca=1 Ce=6) — fans out to 6 callees (efferent coupling)

Verification — 1844 function(s) need re-proving (callee-first):
graphify_extract_xaml_inferred_viewmodel_names_add → graphify_extract_safe_extract → graphify_extract_safe_extract_with_xaml_root → graphify_extract_node_label_key → graphify_extract_lang_is_case_insensitive → graphify_extract_lang_family → graphify_extract_is_top_level_function_definition → graphify_extract_rewire_unique_stub_nodes → graphify_extract_file_node_id → graphify_extract_repoint_python_package_imports …

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not enforced this run (re-run with --prove to verify the blast radius)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1686 function(s) in blast radius — re-run with --prove to formally verify them

· 4 grounded finding(s) anchored inline below.

})


def extract_commonlisp(path: Path) -> dict:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regressionextract_commonlisp()

25 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

t = _text(node)
return t.lstrip(":").lstrip("#:")

def _handle_defpackage(node) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_handle_defpackage()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

idx += 1
return children[idx:]

def _handle_def_form(node, def_keyword: str) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_handle_def_form()

fans out to 6 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

return False
return sym.startswith("def") or sym.startswith("define-")

def _process_form(top) -> bool:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_process_form()

fans out to 10 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

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