Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .ripwire_quality_acks
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,7 @@ ack duplication 5a94cb2859bf1601 71 now=71 was=0 prov=recon a probe TU deliberat
ack duplication 5ae632d0ded8f363 58 now=58 was=0 prov=recon facet=switch-name-table per-enumerator switches name every enumerator and carry no default: on purpose — that is what makes -Werror=switch a compile error at each one when an enumerator is appended; two such switches over one enum share the enumerator spelling by construction
ack duplication 5bfc60bf2eec4379 58 now=58 was=0 prov=recon readability-wave1 (clones.h scanCodeTokens extraction): normalizeTokens and objectKeys are both 'call a scanner, emplace_back each visit into a vector, return it' — the same 58-token visitor-to-vector adapter over two unrelated scanners in two different layers (the clone tokenizer in core vs the JSON top-level-key walker in the MCP layer). Neither can call the other; a shared generic adapter would be a cross-layer template earning six lines. Kept as written.
ack duplication 5c9174022e5a0734 44 DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b)
ack duplication 5c98961b8ec17a64 35 now=35 was=0 p=src/docparse.h:160 #155 (pr438 B2): inSortedTable is the span form of the table lookup isStrictlySortedTable's tables share; isMarkdownGrammarExtension is the same one-line binary_search over a different table, and merging them would need docparse.h to include externalnames.h for no gain
ack duplication 5e4956ffb34ce360 36 now=36 was=0 prov=recon M1: a 36-token shape coincidence, not duplication. isMcpProtocolVersionSupported walks a version list and mcpVerbDeclaresLegend walks a field list; both are the C++ spelling of 'is X in this list', over different tables, different types and different call sites. Folding them into one generic helper would couple the protocol handshake to the legend family for no shared meaning.
ack duplication 5f01b66132b916bd 44 DISCLOSE sinks: a reason-to-field switch is the shape every multi-reason sink shares; -Wswitch exhaustiveness over DisclosureWhy is the point (Diagnostics.h 4b)
ack duplication 5fad9d85c2552890 32 now=32 was=0 p=src/receiverevidence.h:142 lane FE-B (receiver evidence): lexical clone pairs, not shared logic — the per-language predicates (implicitReceiverLang, classNameReceiverLang, memberNeverReachesFreeFunction, isReceiverEvidenceKind, callableKind, classLike, isThisRoot) are one-line OR-lists over different enum sets that share only the shape with quality.h/model.h/slice.h predicates asking other questions; keyOf/typeKey build different keys into a reused buffer; classAt (innermost class span by byte) vs flipimpact innermostAtLine (by line, other table); impactViaColumn vs lintrules dependencyCapableMask share a bitmask idiom; the JS fixture encode|stringify is a deliberate twin (test/receiverevidencefix: same-named near misses); pargates.py is unchanged since cd87e30e (a regrouped pre-existing pair). The one real clone, the enclosing-function walk, was merged into innermostEnclosingFn instead of acked
Expand Down
18 changes: 18 additions & 0 deletions changelog.d/155-kotlin-scope-functions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
### Fixed — Kotlin scope functions no longer bind Java methods

A Kotlin call with a trailing lambda to `let`, `run`, `with`, `apply` or `also` now stays off the Java
method of the same name. Before this change, `s.run { }` bound `Job.run`, and `with( sb ) { }` bound
`Builder.with`. Ripwire now reads the call as the stdlib scope function. It drops the Java candidates and
keeps the Kotlin ones. A call with no Kotlin target reports `external=` with that proof.

Calls without a trailing lambda keep their edges. `j.run()` still binds `Job.run`, and `b.with( "x" )` still
binds `Builder.with`.

Known floors:

- A parenthesised lambda, such as `s.let( { } )`, is not a trailing lambda. Its edge is unchanged.
- A Java method called from Kotlin with a trailing lambda is dropped when its name is a scope-function name.
The spelling alone does not tell the two apart.
- A receiver of a Java type is not checked. A receiver whose type is an in-tree Kotlin class that defines the method
keeps its own binding (`t.run { }` on a `t: Task` binds `Task.run`).
- A qualified `kotlin.run { }` can still bind an in-tree Kotlin `run`, hedged.
12 changes: 12 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,18 @@ triple-quoted string that ends in an escaped `$`). `test/kotlincheck.sh`
covers extraction, both bridge directions in flat and split layouts, the Java-edge invariant, the
bodyless-type collapse, hostile nesting, metrics and determinism.

**Kotlin scope functions (#155).** A call with a trailing lambda to `let`, `run`, `with`, `apply` or `also`
(`s.run { }`, `with( sb ) { }`, `xs.also { }`) is the stdlib inline function, so FE-A (`FalseEdgeRules`, `graph.h`)
drops every Java candidate of that name and keeps the Kotlin ones; an emptied set is `external=` with that proof.
The fact is `Reference::trailingLambda`, set by `kotlinTrailingLambda` (`ingest_metrics.h`); the names are
`kKotlinScopeFunctionNames` (`externalnames.h`, regenerated by one `javap` command recorded there). A receiver rule
that already picked a Kotlin member (`t.run { }` on a `t: Task` whose class defines `run`) keeps its proof; a pick
holding a Java method is undone, so `s.run { }` is decided here. Stated floors: no receiver typing beyond that (a
receiver of a Java type is not checked); a qualified `kotlin.run { }` can still bind an in-tree Kotlin `run` (hedged); no SAM conversion; a parenthesised lambda (`s.let( { } )`) is not trailing and
keeps the ladder; and a Java method of one of these names called from Kotlin with a trailing lambda is dropped,
because the spelling alone cannot tell the two apart. `test/kotlincheck.sh` §15 and `test/falseedgecheck.sh` arm J
pin it.

<a id="clojure-extraction"></a>
**Clojure extraction.** The vendored Yogthos parser is a fork of Sogaiu's grammar. The grammar
rules match at the pinned revisions, while Yogthos' generated ABI-15 parser completed 4,930 of 4,933
Expand Down
23 changes: 23 additions & 0 deletions src/externalnames.h
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,19 @@ inline constexpr std::string_view kRustStdMemberNames[] = {
"to_uppercase", "to_vec", "trim", "unwrap", "unwrap_or", "unwrap_or_default", "unwrap_or_else", "values"
};

// ── THE KOTLIN SCOPE-FUNCTION TABLE (graph.h FalseEdgeRules, the Kotlin arm; gate test/kotlincheck.sh §15 and test/falseedgecheck.sh
// arm J). A Kotlin call with a TRAILING lambda to one of these names is the stdlib scope function, never a Java method of the same
// name: `s.run { }`, `with( sb ) { }`, `s?.let { }`, `xs.also { }`. The fact is the call's trailing lambda (ingest_metrics.h
// kotlinTrailingLambda); the name alone is never a veto — `block.run()` on a Runnable has no lambda and keeps its edge.
// PROVENANCE: the five functions the Kotlin documentation names as scope functions — "There are five of them: let, run, with,
// apply, and also" (https://kotlinlang.org/docs/scope-functions.html, read 2026-10-24) — each an inline function of kotlin-stdlib
// 2.4.0 (https://repo1.maven.org/maven2/org/jetbrains/kotlin/kotlin-stdlib/2.4.0/kotlin-stdlib-2.4.0.jar), class
// kotlin.StandardKt__StandardKt. Regenerated and diffed by ONE command, which prints exactly these five names:
// javap -p -cp kotlin-stdlib-2.4.0.jar kotlin.StandardKt__StandardKt | sed -n 's/.* \([a-z][A-Za-z]*\)(.*/\1/p' | grep -xE 'let|run|with|apply|also' | LC_ALL=C sort -u
// takeIf, takeUnless, repeat and use stay OUT: the documentation does not call them scope functions, and no corpus evidence joined them.
// 5 names; sorted by byte (isStrictlySortedTable below).
inline constexpr std::string_view kKotlinScopeFunctionNames[] = { "also", "apply", "let", "run", "with" };

// The standard templates whose `->` reaches their FIRST template argument, so a `->` call through a declaration written
// `std::unique_ptr<ns::List>` / `std::shared_ptr<…>` / `std::optional<…>` may be a member of that argument (graph.h
// StdMemberGate::stdArrowPointee reads the declaration's recorded text). std::weak_ptr and std::reference_wrapper have no
Expand Down Expand Up @@ -406,6 +419,16 @@ static_assert( isStrictlySortedTable( kRustStdMemberNames ), "kRustStdMemberName
static_assert( isStrictlySortedTable( kCppStdArrowTemplates ), "kCppStdArrowTemplates must be strictly sorted (binary search)" );
static_assert( isStrictlySortedTable( kCppStdElementFirstArg ), "kCppStdElementFirstArg must be strictly sorted (binary search)" );
static_assert( isStrictlySortedTable( kCppStdElementSecondArg ), "kCppStdElementSecondArg must be strictly sorted (binary search)" );
static_assert( isStrictlySortedTable( kKotlinScopeFunctionNames ), "kKotlinScopeFunctionNames must be strictly sorted (binary search)" );
// #155: the lookup of a strictly sorted table (isStrictlySortedTable), taken as a span so a new table reuses it.
inline bool inSortedTable( std::span<const std::string_view> table, std::string_view name ) noexcept
{
return std::binary_search( table.begin(), table.end(), name, rw::sortutil::svLess );
}
inline bool isKotlinScopeFunction( std::string_view name ) noexcept
{
return inSortedTable( kKotlinScopeFunctionNames, name );
}

// Which template argument an element access of the standard container `name` ("map", "vector") yields: 0 or 1, or -1 when `name`
// is no container of kCppStdElementFirstArg / kCppStdElementSecondArg.
Expand Down
65 changes: 62 additions & 3 deletions src/graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -5668,8 +5668,17 @@ struct DispositionTally
// external= with proof (one `C external` census row) or unresolved= without, and every other call keeps the unchanged ladder.
// CLASS-BODY KEEP. A function of the class whose BODY is the caller survives rule (1): Python evaluates a class body as a
// scope, so `DEFAULTS = _default()` there reaches the `_default` defined above it.
// STATED FLOORS (named in test/falseedgecheck.sh's header): implicit-receiver languages (Java, C#, C++, Kotlin, Swift,
// Ruby, ObjC) are untouched — a bare call there reaches the enclosing class's methods; PHP, Lua and Zig have no arm; a
// (4) KOTLIN SCOPE FUNCTIONS (#155; test/kotlincheck.sh §15, test/falseedgecheck.sh arm J). A Kotlin call with a TRAILING lambda to
// let/run/with/apply/also (externalnames.h kKotlinScopeFunctionNames) is the stdlib inline function. Java and other JVM
// candidates of that name drop; Kotlin candidates keep; an emptied set is external=. A receiver rule that already picked a Kotlin
// member (`t.run { }` on a `t: Task` whose class defines run) keeps its proof; a pick holding a Java method is undone, so
// `s.run { }` (which carries a receiver) is decided here. One fact: Reference::trailingLambda (ingest_metrics.h kotlinTrailingLambda).
// STATED FLOORS (named in test/falseedgecheck.sh's header): implicit-receiver languages (Java, C#, C++, Swift, Ruby, ObjC) are
// untouched — a bare call there reaches the enclosing class's methods. Kotlin's scope arm (4) keys on the spelling and the
// trailing lambda ALONE: no receiver typing beyond the Kotlin pick above (a receiver of a Java type is not checked), no SAM conversion, qualified `kotlin.run { }` can still bind an in-tree Kotlin `run` (hedged),
// a parenthesised lambda (`s.let( { } )`, `with( x, { } )`) is NOT trailing and keeps the ladder, and a Java method of
// that name called from Kotlin WITH a trailing lambda is dropped (the same spelling is indistinguishable here).
// PHP, Lua and Zig have no arm; a
// Go package whose name differs from its path's last element is not recognised as an import (its calls keep the
// ladder); a JS `require` inside a function body and browser-only globals are not modelled; a JS script's top-level
// function reached from another script by a bare global-table name would read as external.
Expand Down Expand Up @@ -5999,6 +6008,32 @@ struct FalseEdgeRules
}
return ( !lookup( r.fileId, r.calleeName, bound ) && externalnames::isJsGlobalName( r.calleeName ) ) ? Verdict::External : Verdict::Keep;
}
// #155 KOTLIN SCOPE FUNCTIONS (test/kotlincheck.sh §15, test/falseedgecheck.sh arm J): a Kotlin call with a trailing lambda to
// let/run/with/apply/also IS the stdlib inline function. A Java (or other JVM-compatible) candidate of that name is a method
// the call cannot reach: dropped. Kotlin candidates are kept and narrowed. An emptied set is External with that proof (the name
// is the stdlib scope function, externalnames.h). Nothing dropped → Keep: the ladder decides exactly as before.
Verdict kotlinScopeDecide( const Reference& r, std::span<const NodeId> ids, rw::SmallVec<NodeId, 2>& kept ) const
{
kept.clear();
bool dropped = false;
for( NodeId c : ids )
{
const Symbol& s = ing.symbols[ c ];
if( langCompatible( s.lang, r.lang ) && !langFacts( s.lang ).trailingLambdaScopeFns )
{
dropped = true;
continue;
}
kept.push_back( c );
}
if( !dropped )
{
kept.clear();
return Verdict::Keep;
}
const bool anyKotlin = std::any_of( kept.begin(), kept.end(), [ & ]( NodeId c ) { return langFacts( ing.symbols[ c ].lang ).trailingLambdaScopeFns; } );
return anyKotlin ? Verdict::Narrow : Verdict::External;
}
bool aliasIsForeign( const Alias& a, const Reference& r ) const
{
if( a.fromIdentifier ) // `const { x } = G`: foreign iff G is a global object no binding in the file hides
Expand All @@ -6012,6 +6047,10 @@ struct FalseEdgeRules
Verdict decide( const Reference& r, std::span<const NodeId> ids, rw::SmallVec<NodeId, 2>& kept ) const
{
kept.clear();
if( active && langFacts( r.lang ).trailingLambdaScopeFns && r.role == RefRole::Call && r.trailingLambda && externalnames::isKotlinScopeFunction( r.calleeName ) )
{
return kotlinScopeDecide( r, ids, kept ); // #155: ahead of the receiver gate — `s.run { }` carries a receiver
}
if( !active || r.role != RefRole::Call || !r.qualifier.empty() || r.fromSymbol >= ing.symbols.size() || r.recv != RecvKind::None )
{
return Verdict::Keep;
Expand Down Expand Up @@ -10189,9 +10228,29 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null
// nested namesakes dropped, an inherited body, an interface's dispatch split); otherwise narrowed stays false and the name-based fallback
// runs. Skipped when the call was already pinned canonically or by Rule 1 (the more specific / already-resolved signals).
const bool narrowedBeforeReceiverRules = narrowed;
// #155: a Kotlin trailing-lambda scope call is the stdlib function unless its receiver's type proves a Kotlin member; FE-A decides the rest.
const bool kotlinScopeCall = langFacts( r.lang ).trailingLambdaScopeFns && r.role == RefRole::Call && r.trailingLambda
&& externalnames::isKotlinScopeFunction( r.calleeName );
// #155: a receiver rule still decides such a call when every candidate it picks is a Kotlin definition (`t.run { }` on a
// `t: Task` whose class defines run, beside another class's run); a pick holding a Java (non-Kotlin) method is undone, and
// FE-A below decides the call (kotlincheck §15 tree f).
const auto kotlinScopeUndo = [ & ]() noexcept -> bool
{
if( !kotlinScopeCall
|| std::all_of( cand.begin(), cand.end(), [ & ]( NodeId c ) noexcept { return langFacts( ing.symbols[ c ].lang ).trailingLambdaScopeFns; } ) )
{
return false;
}
cand.clear();
return true;
};
if( !scipPinned && !canonical && !narrowed && writtenRoot != JsWrittenRoot::Pinned )
{
narrowed = narrowTo( narrower.rule2RecvVarType( r, classIds, chaUp ), r, cand ) || narrower.forgetClaim();
if( narrowed && kotlinScopeUndo() )
{
narrowed = narrower.forgetClaim();
}
}
// P2-D Rule 2c (CLASS-NAME receiver, Phase 4b): `Cls.m()` resolves to `Cls::m` (or the shallowest base defining `m`) when Cls is an in-repo class that no
// local and no C++ member of the caller's class or its bases hides (then 2b reads the member). After Rule 2, before 2b. See Narrower::rule2cClassNameRecv.
Expand Down Expand Up @@ -10230,7 +10289,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null
cand.push_back( c );
}
}
narrowed = !cand.empty();
narrowed = !cand.empty() && !kotlinScopeUndo();
}
// lane L6 (ifacedispatch.h): the receiver's recorded type is an interface, a trait or a class declaring the callee with
// no body — C++ through class identity's dispatch split (Rule 2 on a local or parameter, Rule 2b's field), every other
Expand Down
Loading
Loading