Skip to content

feat(pkl): extract calls, branches, throws and decorators - #1426

Open
tthayer wants to merge 2 commits into
DeusData:mainfrom
tthayer:feat/pkl-call-extraction
Open

feat(pkl): extract calls, branches, throws and decorators#1426
tthayer wants to merge 2 commits into
DeusData:mainfrom
tthayer:feat/pkl-call-extraction

Conversation

@tthayer

@tthayer tthayer commented Aug 3, 2026

Copy link
Copy Markdown

Problem

Pkl was already wired end-to-end — grammar vendored, CBM_LANG_PKL, extension mapping, import edges — but the spec was shallow. pkl_call_types was empty_types, so on a real-world 172-file Pkl corpus the graph had 0 CALLS edges while imports worked fine.

branching_node_types, throw_node_types and decorator_node_types were empty too, so Pkl contributed nothing to complexity metrics.

Why this needs a custom extractor

Pkl has no dedicated call node. unqualifiedAccessExpr and qualifiedAccessExpr are the same node whether they are a call or a bare property read — the only discriminator is an argumentList child:

unqualifiedAccessExpr 'helper(a)'     unqualifiedAccessExpr 'host'
  identifier 'helper'                   identifier 'host'
  argumentList '(a)'                  <-- no argumentList

extract_pkl_callee gates on that child. It is dispatched from extract_callee_name with an unconditional return, not via extract_callee_lang_specific: returning NULL from the latter falls through to the generic "first identifier child" fallback, which would mint a CALLS edge for every property read in every Pkl file.

Callee resolution:

source callee
helper(a) helper
utils.fallback(a) utils.fallback (module-qualified; cbm.c shortens to the last dotted segment)
s.trim().toLowerCase() toLowerCase (a receiver that is itself a call has parens in its text, so it is not prefixed)
new Server { ... } Server (links to the class def)

Other spec slots filled

All node names confirmed against real parse trees from a probe linking only the Pkl grammar, not guessed:

  • class: + typeAlias
  • import: + importGlobClause, importExpr
  • branch: ifExpr, whenGenerator, forGenerator
  • throw: throwExpr
  • decorator: annotation

forGenerator is also registered in cbm_is_loop_node_type so for (x in xs) counts toward loop-nesting depth. The name is Pkl-unique, so no other grammar collides.

Validation

Repro battery — the Pkl test moves from the structural battery (dims 1-5) to the full callable battery (dims 1-8), plus an inline negative assertion that bare property reads do not become CALLS edges. repro_grammar_config_pkl PASS.

Full suite: 318 passed, 6 failed, 2 skipped — byte-identical to the pre-change baseline (verified by stashing and re-running). The 6 failures are pre-existing and unrelated (repro_issue480/514/546/581, repro_issue409, repro_ts_inherited_method).

Real corpus — 172 Pkl files of production infrastructure config, indexed clean (0 skipped, 0 parse failures):

before after
CALLS edges 0 367

Split: 270 to Function, 97 to Class (constructors). ~2.1 calls/file — proportionate; a broken gate would have produced tens of thousands.

Quality was spot-checked rather than inferred from the count: sampled Function callees are genuine function definitions invoked with arguments, and a bare property read that shares its name with one of those functions correctly produced no edge — the exact case the argumentList gate exists to catch.

Note for reviewers

The repro fixture deliberately keeps every call site inside a method body, because dim 7 treats Module-sourced in-body calls as the enclosing-func gap. Real-world Pkl does call at module level (many of the 97 constructor edges are such sites), so this is a fixture constraint, not a claim about the language.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@tthayer
tthayer force-pushed the feat/pkl-call-extraction branch 3 times, most recently from 54a64c8 to 4e8d01d Compare August 3, 2026 18:29
@tthayer
tthayer marked this pull request as ready for review August 3, 2026 19:47
@tthayer
tthayer requested a review from DeusData as a code owner August 3, 2026 19:47
@tthayer tthayer mentioned this pull request Aug 4, 2026
1 task
@DeusData DeusData added this to the 0.11.0-rc milestone Aug 10, 2026
@DeusData DeusData added enhancement New feature or request language-request Request for new language support parsing/quality Graph extraction bugs, false positives, missing edges priority/normal Standard review queue; useful PR with ordinary maintainer urgency. labels Aug 10, 2026
@DeusData

Copy link
Copy Markdown
Owner

Thank you for expanding the Pkl extraction coverage across calls, branches, throws, and decorators. I have routed this language-support contribution to 0.11.0-rc with normal priority. The community review queue is currently full, so a complete parser review may take a little time, but the contribution is queued.

@DeusData

Copy link
Copy Markdown
Owner

Thank you for the Pkl language work and the coverage across calls, branches, throws, and decorators. The Pkl changes form one coherent language contribution and do not need to be split further.

One commit is outside that scope: a700bff1 changes Windows helper-spawn behavior in scripts/run-test-wave.py and its harness contract. Please remove that commit from this PR, or restack the Pkl work onto a clean branch without it. CI process-control changes need their own focused review because they affect every language and platform. Once that unrelated commit is separated, we can review this as the Pkl contribution it is. Thank you for helping keep that boundary clear while the maintainer queue is full.

@tthayer
tthayer force-pushed the feat/pkl-call-extraction branch from 12bf64c to 597985d Compare August 24, 2026 21:30
@tthayer

tthayer commented Aug 24, 2026

Copy link
Copy Markdown
Author

a700bff1 removed. Pkl work restacked on main (010569fa) as 9c25f163 + 597985d2; diff is 8 Pkl files, scripts/run-test-wave.py gone.

tests/test_parallel_harness_contract.sh left too: that hunk reads WINDOWS_HELPER_TIMEOUT_SECONDS from the scheduler, so it was the contract half of a700bff1 (misfiled into the Pkl commit) and cannot ship without it. Both halves will come back as their own PR.

scripts/repro.sh, darwin arm64:

clean main branch
passed 689 690
failed 14 14
skipped 2 2

Same 14 failures either way (repro_issue56/409/480/514/523/546/581/627, repro_lsp_go_py, repro_ts_inherited_method); the added pass is repro_grammar_config_pkl. Touched suites green: repro_grammar_config 16/16, repro_call_argument_matrix_b 47/47, repro_language_registry 5/5, repro_call_node_manifest 3/3.

@tthayer

tthayer commented Aug 24, 2026

Copy link
Copy Markdown
Author

Split-out CI change is now #1823.

@DeusData

Copy link
Copy Markdown
Owner

@tthayer — your 24 August restack did everything the 18 August ruling asked, and cleanly; the split-out #1823 is exactly right. One more rebase is needed through no fault of yours: ArkTS and PL/SQL landed on main in the four days after your restack, and both moved the same ledger constants your PR moves. So you don't have to re-derive anything, here is the complete map — including the part that will bite silently.

The trap first: tests/repro/repro_call_node_manifest.c will NOT show a merge conflict, and will still be wrong. Both main and your branch moved its constants by identical deltas from the old base, so git auto-resolves them "clean" — short by exactly Pkl's contribution. We have been bitten by this exact shape twice this month. Set these by hand:

constant your branch correct after rebase
EXPECTED_HISTORICAL_CALL_NODE_TOTAL 222 225
EXPECTED_ACTIVE_PRIMARY_TOTAL / EXPECTED_PRIMARY_OWNERS 192 195
EXPECTED_DIRECT_CALLS 157 159
EXPECTED_CONSTRUCTOR_CALLS 21 22

The other two ledger files conflict visibly, so git will make you look; correct values there: repro_language_registry.c → REF_VOCAB 88, WITHOUT_REF_VOCAB 26, NO_CALL 49, MATRIX_A 68, MATRIX_B 48, CALL_CAPABLE 114, NON_CALL 51; repro_call_argument_matrix_b.c → ROUTINE_ARGUMENT_LANGUAGE_COUNT 38, matrix assert 48. Or simply run the three repro suites after resolving and take the recounted values — but the manifest file needs the hand edit either way, because nothing will flag it.

Two smaller items for the same pass:

  1. Reword 597985d2's commit message — it still describes the removed WINDOWS_HELPER_TIMEOUT_SECONDS harness change. The content is verifiably gone from the diff; the message shouldn't describe a change the commit no longer contains.
  2. For transparency: your added header prose in the manifest ("the historical-snapshot column is the audited ledger, not an archaeology record") was checked against how ArkTS and PL/SQL actually moved those rows, found to codify existing practice accurately, and is accepted as-is.

The review verified the parts that matter: every extraction edit is behind a CBM_LANG_PKL gate with an unconditional return (so the generic fallback can't mint a CALLS edge per property read — the argumentList-gated callee design is exactly right for a language where access-exprs double as property reads); forGenerator exists only in the pkl grammar, so the shared loop-list addition is collision-free; and all 18 spec-row slots land on the correct struct fields. Rebase with the table above and this is ready for the final pass.

Tony Thayer-Osborne and others added 2 commits August 28, 2026 08:35
Pkl was wired end-to-end but shallow: on a real-world 172-file Pkl corpus
the graph had 386 IMPORTS edges and 0 CALLS edges, because pkl_call_types
was empty_types.

Pkl has no dedicated call node. `unqualifiedAccessExpr` and
`qualifiedAccessExpr` are the same node whether they are a call
(`helper(a)`) or a bare property read (`host`) — the only discriminator is
an `argumentList` child. extract_pkl_callee gates on that child, and is
dispatched from extract_callee_name with an unconditional return: falling
through to field-based or generic first-identifier resolution would mint a
CALLS edge for every property read in every Pkl file, since a bare access
expr's first child is an identifier.

Callee resolution:
  helper(a)               -> "helper"
  utils.fallback(a)       -> "utils.fallback"  (module-qualified; cbm.c
                             shortens to the last dotted segment)
  s.trim().toLowerCase()  -> "toLowerCase"     (a receiver that is itself a
                             call has parens in its text and is not prefixed)
  new Server { ... }      -> "Server"          (links to the class def)

Also fills the other empty slots in the Pkl spec, all confirmed against
real parse trees rather than guessed: typeAlias (class), importGlobClause /
importExpr (import), ifExpr / whenGenerator / forGenerator (branch),
throwExpr (throw), annotation (decorator). forGenerator is additionally
registered in cbm_is_loop_node_type so `for (x in xs)` counts toward
loop-nesting depth; the name is Pkl-unique so no other grammar collides.

The Pkl repro test moves from the structural battery (dims 1-5) to the
full callable battery (dims 1-8) and adds an inline negative assertion
that bare property reads do NOT become CALLS edges — the regression the
argumentList gate exists to prevent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Tony Thayer-Osborne <tony.thayerosborne@conductorone.com>
Merging main brought in three contracts this branch violated.

repro_call_node_manifest requires a table row for every live
call_node_types entry, so Pkl's unqualifiedAccessExpr/qualifiedAccessExpr
(direct) and newExpr (constructor) each get one, with the partition totals
moved to match. The historical-snapshot column is the audited ledger, not
an archaeology record -- the suite hard-fails on a row that claims it was
absent -- so a newly registered kind joins the snapshot with the change
that registers it.

repro_language_registry then requires every call-capable language to own
exactly one call-argument matrix row, which turned out to be a real
extractor gap rather than a bookkeeping entry. Pkl's grammar labels no
production with a `name` field, so the generic declared-container rule
never recognised a Pkl binding: method names, parameter names, and
property names were each re-emitted as an ordinary read of themselves,
which Go and Nickel do not do. Add a Pkl occurrence policy that binds the
declared identifier of methodHeader, typedIdentifier, classProperty,
objectProperty, clazz, and typeAlias, resolved against the nearest such
container so annotations, defaults, and bodies stay reads. Recording the
binding also supplies the lexical-shadow proof the matrix row asserts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Tony Thayer-Osborne <tony.thayerosborne@conductorone.com>
@tthayer
tthayer force-pushed the feat/pkl-call-extraction branch from 597985d to 7b72652 Compare August 28, 2026 15:41
@tthayer

tthayer commented Aug 28, 2026

Copy link
Copy Markdown
Author

Rebased onto main (31b611a3), pushed as 1d6a140f + 7b72652a.

  • Visible conflicts resolved with your table; silent auto-merges hand-fixed: manifest 225/195/159/22, registry ref-vocab 87→88, matrix B routine count 37→38 (+ static assert 48).
  • Tip commit reworded — harness paragraph removed.

scripts/repro.sh darwin arm64: 693 passed, 14 failed, 2 skipped — same pre-existing 14. Touched suites green: registry 5/5, matrix B 48/48, manifest 3/3, grammar_config 16/16.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request language-request Request for new language support parsing/quality Graph extraction bugs, false positives, missing edges priority/normal Standard review queue; useful PR with ordinary maintainer urgency.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants