Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f47581a
test: reproduce Elixir quote live-hole and accounting gaps
LaimaWu Oct 4, 2026
b5cacea
fix: preserve Elixir unquote calls and count quoted call sites
LaimaWu Oct 4, 2026
d2870fa
test: re-pin extraction scheme tripwire for issue 357
LaimaWu Oct 4, 2026
0cb0a82
test: cover Elixir quote options before enabling live holes
LaimaWu Oct 4, 2026
aa0361d
test: expose inert declines in default map and callees output
LaimaWu Oct 4, 2026
f3f94d7
fix: honor quote options and explain inert declines in compact output
LaimaWu Oct 4, 2026
474d9c0
test: reproduce non-Elixir CDATA legend marker collision
LaimaWu Oct 4, 2026
3dca9ba
fix: select inert decline readings only from emitted XML comments
LaimaWu Oct 4, 2026
2ece366
test: reproduce live holes in quoted declarations and metadata
LaimaWu Oct 4, 2026
3a5c8fb
fix: stop quoted declaration filters at enabled live-hole arguments
LaimaWu Oct 4, 2026
6bc1c90
test: reproduce falsely live unquotes in inert nested quotes
LaimaWu Oct 4, 2026
5c65fed
fix: retain outer inert boundaries after traversing quote live holes
LaimaWu Oct 4, 2026
d9cffb3
docs: describe inert counts after all quote boundaries are considered
LaimaWu Oct 4, 2026
04bdf4d
test(elixir): reproduce inert operator and capture accounting loss
LaimaWu Oct 5, 2026
16284d0
fix(ingest): count inert operator and named-capture references
LaimaWu Oct 5, 2026
fff9fa7
docs: define inert reference counting coverage
LaimaWu Oct 5, 2026
4bfb96d
test(elixir): reproduce quoted module stealing live-hole scope
LaimaWu Oct 5, 2026
75a0670
fix(elixir): skip inert declarations when finding live-hole module scope
LaimaWu Oct 5, 2026
7e83493
test(elixir): pin live-hole callee identity across quoted modules
LaimaWu Oct 5, 2026
0167bdd
chore: account for inert-region ingest section in optimization triage
LaimaWu Oct 5, 2026
bc53df2
docs: regenerate shifted graph seed showcase blocks
LaimaWu Oct 5, 2026
b479b4f
docs: synchronize navigation captures and generated command reference
LaimaWu Oct 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,16 @@ metadata do not become executable calls. Alias/import/require/use and behaviour
module dependencies resolved through declared module identities, regardless of umbrella/file layout.
`@behaviour` and `defimpl` supply contract/implementation relationships for `--uses` and `--lego`.

**Static limits:** quoted AST and macro-generated definitions are not expanded. `use` records the
**Static limits:** quoted AST and macro-generated definitions are not expanded. Ordinary call,
operator and named-function-capture candidates inside a `quote` body mint no edges and are counted
in the map's `declined=` gauge; `--callees` counts those sites on their enclosing function.
Bare variable-shaped AST identifiers are not counted merely for being identifiers. Calls inside `unquote(...)` and
`unquote_splicing(...)` are live when enabled, as are expressions in `bind_quoted:` options.
`bind_quoted` disables unquoting by default; explicit `unquote: true` re-enables it and
`unquote: false` keeps its argument inert. Nonliteral unquote options cannot prove a live hole.
Nested quotes become inert again inside a live hole; an inner quote that is itself inert
cannot execute its own holes in the current stage. Quoted module declarations do not change the lexical module of evaluated holes. Quoted declaration heads and metadata
do not suppress evaluated hole arguments; bound variables in those arguments remain variables. `use` records the
dependency, but does not execute `__using__`; framework DSLs and generated Phoenix/Ecto functions
therefore need an explicit source definition to appear. A call that only an injected import could
answer has no lexical candidate: no edge is minted from a same-named function elsewhere, and the call
Expand Down Expand Up @@ -196,9 +205,7 @@ that function — the underscore rule is for unused variables, and a bare `_seed
(`src/ingest_elixir.h`); and a call that omits a defaulted argument reaches the bodyless head that
evaluates the default beside the clauses, so `--path` and `--impact` see the default expression's calls
from that caller, while a call that supplies the argument reaches the clauses alone
(`src/elixir_resolve.h`). One gap stays open: executable `unquote(...)` and `bind_quoted:` expressions
under `quote` are omitted with the rest of the quoted-AST filter, so a helper called only from inside
an `unquote` has no caller edge from its macro.
(`src/elixir_resolve.h`).

`test/elixircheck.sh`, `test/eliximportcheck.sh` and `test/elixirsemanticcheck.sh` cover extraction,
metrics, exact target selection against decoys, lexical boundaries, contracts, CLI/MCP use-site parity,
Expand Down
6 changes: 3 additions & 3 deletions docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3350,11 +3350,11 @@ $ ./build/ripwire . --slice-depth=3
_Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost._

```
$ ./build/ripwire . --at=src/graph.h:5565
$ ./build/ripwire . --at=src/graph.h:5577 --legend=full
<!-- ripwire at: the ENCLOSING-DEFINITION CHAIN at one FILE:LINE seed. p= the resolved file, l= the 1-based seed line, sym= the innermost enclosing definition's name (what the same seed resolves to in a selector position), chain= the row count. Rows are INDEXED definitions only, outermost first, innermost last: n= the definition's name, t= its kind tag, l= its own start line, el= its end line (1-based, inclusive). A namespace or any construct the index does not carry is NOT a row, so an outer scope can be absent rather than misnamed; a seed line inside no indexed definition is refused, never served as an empty chain. The same seed composes into any SYM selector as @FILE:LINE (callers, callees, impact, around, expand, uses, edit-check, slice, safe-delete, path, connect) and resolves to the innermost row. -->
<!-- root= on this element is the crawl root every p= below is RELATIVE to (single-root runs only; absent => p= is the path ingest itself used, unchanged). -->
<at p="src/graph.h" l="5565" sym="rankGraphTeleport" chain="1" root=".">
<s n="rankGraphTeleport" t="fn" l="5565" el="5593"/>
<at p="src/graph.h" l="5577" sym="rankGraphTeleport" chain="1" root=".">
<s n="rankGraphTeleport" t="fn" l="5575" el="5603"/>
</at>
```

Expand Down
20 changes: 10 additions & 10 deletions docs/captures/COMMANDS_showcase_2026-09-14.md
Original file line number Diff line number Diff line change
Expand Up @@ -3921,37 +3921,37 @@ ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d4

# navigate — seeds, claims, slices, shapes

## `./build/ripwire . --at=src/graph.h:5565`
## `./build/ripwire . --at=src/graph.h:5577 --legend=full`

*Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost.*

`````
<!-- ripwire at: the ENCLOSING-DEFINITION CHAIN at one FILE:LINE seed. p= the resolved file, l= the 1-based seed line, sym= the innermost enclosing definition's name (what the same seed resolves to in a selector position), chain= the row count. Rows are INDEXED definitions only, outermost first, innermost last: n= the definition's name, t= its kind tag, l= its own start line, el= its end line (1-based, inclusive). A namespace or any construct the index does not carry is NOT a row, so an outer scope can be absent rather than misnamed; a seed line inside no indexed definition is refused, never served as an empty chain. The same seed composes into any SYM selector as @FILE:LINE (callers, callees, impact, around, expand, uses, edit-check, slice, safe-delete, path, connect) and resolves to the innermost row. -->
<!-- root= on this element is the crawl root every p= below is RELATIVE to (single-root runs only; absent => p= is the path ingest itself used, unchanged). -->
<at p="src/graph.h" l="5565" sym="rankGraphTeleport" chain="1" root=".">
<s n="rankGraphTeleport" t="fn" l="5565" el="5593"/>
<at p="src/graph.h" l="5577" sym="rankGraphTeleport" chain="1" root=".">
<s n="rankGraphTeleport" t="fn" l="5575" el="5603"/>
</at>
`````

## `./build/ripwire . --callers=@src/graph.h:5565`
## `./build/ripwire . --callers=@src/graph.h:5577 --legend=full`

*The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.*

`````
<!-- ripwire callers/callees: the 1-hop call hierarchy read off the call graph — the callers form lists symbols that CALL of=; the callees form lists symbols of= itself calls. of= is the selector you passed, defs= how many DEFINITIONS it resolved to (rows UNION every def's neighbours), count= the DISTINCT neighbour symbols (a floor, per counts_floor=), windowed by limit= and offset=. A neighbour that is an indexed function-like #define is a macro row (t="macro", role="macro" on the XML row): the edge crosses a macro expansion, not a plain call — rows carry no role= otherwise. Rows are ordered SOURCE first, then test/bench, then docs, by path within a tier. hop_tested=/hop_untested= partition count= by the tested= lens below (1-hop, never transitive). tested="1" on a row means an indexed test transitively reaches it (never 0, omitted when it does not). BLIND SPOT the test-gate legend also names: only a CALL EDGE from an INDEXED test symbol counts here, so a shell or CLI-level test running a built binary as a SUBPROCESS is invisible to it and a repo tested that way reads all-untested. Read untested= as no in-process test reaches it, not as no test covers it. next= is the one pasteable follow-up (the uses verb on this selector: the call sites). counts_floor="1" means every count here is a FLOOR, never a total: edges are extracted from source TEXT by NAME. Missing: dynamic dispatch (virtual/interface/duck-typed), a most-vexing-parse declaration with no call expression, a function-pointer/callback bound to more than one function in scope (reassigned, table-indexed, lambda-bound, or address-taken/reference-bound), and a plain-name binding (fp=handler) whose variable type is not PROVABLY a function pointer (a same-file typedef/declarator; a HEADER typedef is missed; auto/template types are read as unpinned, so KEPT). A macro-generated call site is role="macro" only when its name uniquely names an indexed function-like #define (C-family, t="macro"); a shared name stays a plain call, an unindexed macro is no edge. Read a zero as "none found", never as "none exists". graph_ambiguous=/graph_unresolved= are the whole graph's resolver gauge (calls split over several defs / calls whose in-repo defs were all language-filtered), the map header's ambiguous=/unresolved=. graph_unindexed=N is a third gauge: files no grammar could read (the map header's unindexed=), whose calls raise neither gauge above; absent when zero, and so is this sentence. COUNTING UNIT differs by verb: callers, callees, edit-check, graph-query and pr-context counts are DISTINCT SYMBOLS (repeated calls from one caller, and calls to two overloads, collapse into ONE row; multiplicity survives only in the call graph's edge weight). The reach counts (impact's reaches=, pr-context's dependents=) are the size of a transitive reach SET, each symbol counted once. The uses verb counts call SITES, one row per occurrence — a larger count there for the same symbol is these units agreeing, not disagreeing. The map header's edges= is a unit again different — distinct (caller,callee) PAIRS — and that document carries neither this marker nor this clause. -->
<!-- ripwire callers/callees: the 1-hop call hierarchy read off the call graph — the callers form lists symbols that CALL of=; the callees form lists symbols of= itself calls. of= is the selector you passed, defs= how many DEFINITIONS it resolved to (rows UNION every def's neighbours), count= the DISTINCT neighbour symbols (a floor, per counts_floor=), windowed by limit= and offset=. A neighbour that is an indexed function-like #define is a macro row (t="macro", role="macro" on the XML row): the edge crosses a macro expansion, not a plain call — rows carry no role= otherwise. Rows are ordered SOURCE first, then test/bench, then docs; within a tier callers rank by each row's own caller count (most-called first); callees keep path order. hop_tested=/hop_untested= partition count= by the tested= lens below (1-hop, never transitive). tested="1" on a row means an indexed test transitively reaches it (never 0, omitted when it does not). BLIND SPOT the test-gate legend also names: only a CALL EDGE from an INDEXED test symbol counts here, so a shell or CLI-level test running a built binary as a SUBPROCESS is invisible to it and a repo tested that way reads all-untested. Read untested= as no in-process test reaches it, not as no test covers it. next= is the one pasteable follow-up (the uses verb on this selector: the call sites). counts_floor="1" means every count here is a FLOOR, never a total: edges are extracted from source TEXT by NAME. Missing: dynamic dispatch (virtual/interface/duck-typed), a most-vexing-parse declaration with no call expression, a function-pointer/callback bound to more than one function in scope (reassigned, table-indexed, lambda-bound, or address-taken/reference-bound), and a plain-name binding (fp=handler) whose variable type is not PROVABLY a function pointer (a same-file typedef/declarator; a HEADER typedef is missed; auto/template types are read as unpinned, so KEPT). A macro-generated call site is role="macro" only when its name uniquely names an indexed function-like #define (C-family, t="macro"); a shared name stays a plain call, an unindexed macro is no edge. Read a zero as "none found", never as "none exists". graph_ambiguous=/graph_unresolved= are the whole graph's resolver gauge (calls split over several defs / calls whose in-repo defs were all language-filtered), the map header's ambiguous=/unresolved=. graph_unindexed=N is a third gauge: files no grammar could read (the map header's unindexed=), whose calls raise neither gauge above; absent when zero, and so is this sentence. COUNTING UNIT differs by verb: callers, callees, edit-check, graph-query and pr-context counts are DISTINCT SYMBOLS (repeated calls from one caller, and calls to two overloads, collapse into ONE row; multiplicity survives only in the call graph's edge weight). The reach counts (impact's reaches=, pr-context's dependents=) are the size of a transitive reach SET, each symbol counted once. The uses verb counts call SITES, one row per occurrence — a larger count there for the same symbol is these units agreeing, not disagreeing. The map header's edges= is a unit again different — distinct (caller,callee) PAIRS — and that document carries neither this marker nor this clause. -->
<!-- root= on this element is the crawl root every p= below is RELATIVE to (single-root runs only; absent => p= is the path ingest itself used, unchanged). -->
<callers of="@src/graph.h:5565" defs="1" count="7" root="." hop_tested="0" hop_untested="7" graph_ambiguous="10252" graph_unresolved="12843" graph_unindexed="237" counts_floor="1" next="--uses=@src/graph.h:5565">
<callers of="@src/graph.h:5577" defs="1" count="7" root="." hop_tested="0" hop_untested="7" graph_ambiguous="10254" graph_unresolved="12850" graph_unindexed="237" counts_floor="1" next="--uses=@src/graph.h:5577">
<s t="fn" n="getIndex" p="src/mcpindex.h:1165"/>
<s t="fn" n="rankGraph" p="src/graph.h:5606"/>
<s t="fn" n="anchoredLexicalRank" p="src/graph.h:6252"/>
<s t="fn" n="rankGraph" p="src/graph.h:5616"/>
<s t="fn" n="anchoredLexicalRank" p="src/graph.h:6262"/>
<s t="fn" n="runEval" p="src/eval.h:171"/>
<s t="fn" n="churnDecayRanking" p="src/main.cpp:1380"/>
<s t="fn" n="churnRankedGraph" p="src/main.cpp:1419"/>
<s t="fn" n="runDefaultMap" p="src/main.cpp:1629"/>
</callers>
`````

## `./build/ripwire . --at=src/graph.h:999999`
## `./build/ripwire . --at=src/graph.h:999999 --legend=full`

*A seed past the end of the file — the refusal shape for a faulted location.*

Expand All @@ -3964,7 +3964,7 @@ ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d4
stderr:

`````
ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 6763 lines — the seed asked for line 999999)
ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 9428 lines — the seed asked for line 999999)
`````

## `./build/ripwire . --verify="calls(runDefaultMap, rankGraphTeleport)"`
Expand Down
4 changes: 3 additions & 1 deletion queries/elixir/tags.scm
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
; Elixir definitions are ordinary calls in the grammar. captureTagsFacts applies
; the keyword/head/quote filters in ingest_elixir.h; tags predicates are not evaluated.
; the keyword/head filters in ingest_elixir.h and the shared inert-region
; walk in ingest_inert.h; tags predicates are not evaluated. Capturing every call as
; @inert/@live would still need C++ head checks and enlarge this hot capture stream.
; Anchors restrict definition names to the FIRST argument (never a call's body).
(call target: (identifier)
(arguments . [(alias) (dot) (atom)] @name)) @definition.module
Expand Down
2 changes: 2 additions & 0 deletions scripts/optremarks.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@
"per-node tree-sitter walk — is covered by ingest_binds.h and ingest_relations.h, which run for every grammar." ),
( "src/ingest_elixir.h",
"Elixir-specific capture helpers: per node, but for one grammar with a small corpus share. Same argument as ingest_jsimports.h above." ),
( "src/ingest_inert.h",
"The shared inert-boundary walk currently runs only for Elixir quote captures and context. Like ingest_elixir.h, its remarks cover one grammar, not the general corpus." ),
# ── per-DIRECTIVE, where every hot ingest section above is per-node or per-file ────────────────────
( "src/ingest_importcap.h",
"the shared @import.path capture vocabulary (issue #358), carrying the C family for now. It is called from ONE place in ingest_sidecap.h and runs once per captured "
Expand Down
14 changes: 13 additions & 1 deletion src/compactlegend.h
Original file line number Diff line number Diff line change
Expand Up @@ -1788,7 +1788,19 @@ inline std::string compactLegendText( const CompactLegendSpec& spec, std::string
for( const std::uint16_t i : compactPresentTerms( head, doc, spec.key ) )
{
out += ' ';
out.append( compactReading( i ).reading );
const auto& term = compactReading( i );
if( term.attr == "declined_calls" && compactCommentOpenedBy( doc, "<!-- ripwire callers/callees:" ).find( "ordinary Elixir call candidates in inert quote AST" ) != std::string_view::npos )
{
out += "declined_calls=K: K call sites without edges, including inert Elixir quote sites after unquote/unquote_splicing re-entry; inert sites have no target candidates or uses rows";
}
else if( term.attr == "declined" && !compactCommentOpenedBy( doc, "<!-- hdr:declined=also-counts-ordinary-Elixir-call-sites-inside-inert-quote-AST" ).empty() )
{
out += "declined=K: K call sites without edges, including inert Elixir quote sites after unquote/unquote_splicing re-entry";
}
else
{
out.append( term.reading );
}
out += '.';
}
// #60: the module-scope owner is the one kind that arrives under a dozen different spellings —
Expand Down
12 changes: 11 additions & 1 deletion src/graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ struct Graph
// stores what those calls could equally have meant ONCE PER DISTINCT candidate list (internDeclinedList): list k is declinedListCand[ off[k], off[k+1] ),
// named by declinedListCallCount[k] calls — what the callers and impact answers read to count, once per call, the declines that could have meant THEIR symbols.
std::vector<std::uint32_t> declinedOut;
std::size_t inertDeclinedCalls = 0; // quoted call sites counted in declinedOut, with no candidate list
std::vector<std::uint32_t> declinedListOff{ 0u }; // the leading offset lives here, so a default Graph is already a valid zero-list CSR
std::vector<NodeId> declinedListCand;
std::vector<std::size_t> declinedListCallCount;
Expand Down Expand Up @@ -269,7 +270,7 @@ inline bool namespaceCompatible( RefRole role, SymKind kind ) noexcept
return kind == SymKind::Macro;
}
case RefRole::Call: case RefRole::Read: case RefRole::Write: case RefRole::Import:
case RefRole::Value: case RefRole::Through: // never in the call loop; graph.h valueRefIndex narrows by kind itself
case RefRole::Value: case RefRole::Through: case RefRole::Inert: // Inert exits before the name ladder
{
return true; // un-narrowed — see the doctrine above; a NEW role is a -Werror=switch decision here
}
Expand Down Expand Up @@ -4056,6 +4057,15 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null
disposition = CallDisposition::FileScope; // no caller node for an edge; the use-site index still lists the site
continue;
}
if( r.role == RefRole::Inert )
{
// The syntax is a call candidate, but quoted AST cannot execute it. Count the declined
// site on its caller; do not intern target candidates, since it could reach none of them.
++g.declinedOut[ r.fromSymbol ];
++g.inertDeclinedCalls;
disposition = CallDisposition::Declined;
continue;
}
const auto it = byName.find( r.calleeName );

cand.clear();
Expand Down
Loading
Loading