Skip to content

detector: an orphan is a name seen once in the file, so any language that repeats the name in its own syntax reports zero orphans #2727

Closed

Description

@squid-protocol

Found by the #2669 F.4 orphan/api census over the 46-language keyword-rosetta corpus.

detector.py L1383 is the whole definition of orphaned_logic:

elif len(func_name) > 3 and token_counts[func_name] <= 1:
    orphan_count += 1        # "If the function name only exists where it was defined, it's an orphan"

token_counts is over the entire code stream, including the definition's own tokens. So whether a function reads as orphaned depends on how many times the language's syntax writes the name, not on whether anything calls it:

shape languages orphans/file why
name repeated by syntax ada, haskell, livecode 0.00 Ada closes with end Probe_Globals;; Haskell writes probeGlobals :: Int -> Int above the equation; LiveCode ends the handler by name. Count is 2 before anything calls it
slice name synthetic/short yaml, sqlite 0.00 yaml's 13 slices are all named run (3 chars, fails len > 3); sqlite's are Declarative_Block/CREATE_Statement (excluded as synthetic)
name never recurs abap, cobol, jcl, m4, makefile, objective-c, scheme 3.25 COBOL paragraphs, m4 macros, Scheme defines, Make targets: written once, invoked by other means, so every function reads as orphaned
baseline python, go, java, +30 2.50 13 probe functions, 3 called from main

All four rows are the same 12-probe program with identical intent, so the spread is measurement, not code.

This is not cosmetic. orphaned_logic feeds state_slop_orphans (tech debt) and, through the Contextual Baseline Fix (galaxyscope.py L2228), it is converted into api — see the sibling issue on the api column.

Fix shape

Count occurrences outside the function's own [start_idx, end_idx] span instead of over the whole stream. Then:

  • Ada's end Probe_Globals and LiveCode's closing end stop masking a genuinely uncalled procedure (both are inside the span);
  • a recursive self-call is also inside the span, so a recursive-but-uncalled function correctly still reads as unused;
  • a call from anywhere else in the file is outside the span and still clears the orphan flag — the baseline languages keep the behaviour they have today.

Haskell needs a check that the extractor's span starts at the type signature rather than the equation; if it starts at the equation, the signature line is outside the span and haskell keeps its false negative.

Verify on language-crucible before blessing — this moves state_slop_orphans and arch_api on every language, so it is a golden-master change (B.2's lesson: confirm the real-world direction, not just the corpus's).

Part of #2669 (F.4).

Activity

  1. added
    bugUnintended behavior or logic failure in the engine
    metricsHeuristics, risk exposures, and topological math updates
    core-engineModifications to the central physics and parsing engine
    on Sep 4, 2026
  2. squid-protocol commented on Sep 5, 2026

    @squid-protocol
    OwnerAuthor

    Ran the span check this issue asks for, on the real extractor over both corpora. It answers the Haskell question and turns up a defect in the fix shape as written.

    1. The Haskell caveat is moot

    The span does start at the type signature. Measured on keyword-rosetta/data/haskell/a.hs: probeGlobals spans [182,233), whose first line is probeGlobals :: Int -> Int. No extra work needed.

    But haskell still will not read orphans after this fix, for a reason the issue did not anticipate: its module export list names every probe, outside every span. module A (probeGlobals, probeTest, probeSafety) where is one outside-span occurrence per probe, so all three still clear the orphan flag. The only orphan the fix finds in haskell is entry in main.hs — not exported, not called — which is correct. Corpus goes 0.00 → 0.25 orphans/file, not the 2.50 the issue's table implies. That residual is the already-ledgered api-export-names-suppress-orphan-credit shape (corpus-side), not an engine gap.

    2. The fix as written loses real orphans

    Count occurrences outside the function's own [start_idx, end_idx] span

    That is unsafe, because the span does not reliably contain the function's own declaration. Where it does not, the declaration itself counts as an outside reference and clears the flag. Measured over the crucible (40 files/language):

    language today fix as written
    ruby 51 44
    html 5 2
    shell 8 3

    Three languages move down — real orphans lost. Two distinct causes, both traced to concrete files:

    • ruby: 66 of 132 sampled functions have their declaration outside the span (spans are offset — initialize's span head lands mid-require line in base.rb).
    • shell: Mode-B spans start at the {. In curl/initscript.sh the K&R form puts the name on the previous line, so make_module(), db2_name(), setenv() etc. all sit outside their own span.

    3. Corrected fix shape

    Discount the declaration when the span does not contain the name at all — if inside == 0, the definition site is outside by construction, so exactly one outside occurrence is the declaration:

    inside  = count(name, code_stream[start_idx:end_idx])
    outside = count(name, everything_else)
    references = outside - (1 if inside == 0 else 0)
    orphan = references <= 0

    I also tried span ∪ start_line; it fixes ruby and html but not shell, because start_line points at the { line rather than the line carrying the name (a separate metadata observation, worth its own issue — it is what a reader would cite as the declaration's location).

    Result: no language decreases. Crucible orphans 7397 → 8385 (+13%), the direction this issue predicts. The movers are exactly the languages named here (livecode 3 → 127, jcl 34 → 84, typescript 1728 → 2061, ada 0 → 2.50/file), plus the ones the correction recovers (ruby 51 → 59, assembly 26 → 43, matlab 3 → 6, html/shell flat instead of down).

    4. What this fix does not address

    The issue's third table row — abap, cobol, jcl, m4, makefile, objective-c, scheme at 3.25 orphans/file, where every function reads orphaned — is unchanged by span-scoping, in the corpus and in the crucible. Those names genuinely do not recur: COBOL PERFORM, m4 macro invocation, Make target prerequisites are invocation forms the token test cannot see at all. That is a separate no-call-graph family, not something this fix shape reaches, and the issue should not be read as closing it.

    Proceeding with the corrected shape, paired with #2728 (they edit the same loop, and #2728 must not land after this — css's keyframes buckets currently sit inside the token test, so this fix would bless an artifact that #2728 then removes).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugUnintended behavior or logic failure in the enginecore-engineModifications to the central physics and parsing enginemetricsHeuristics, risk exposures, and topological math updates

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions