Skip to content

Kotlin scope functions (run, let, apply, also, with) bind to unrelated Java methods #155

Closed

Description

@joyful-ii-V-I

What is wrong

Kotlin's scope functions are standard-library calls, but ripwire resolves calls by name. Once Kotlin
and Java share one call graph, x.run { … } binds to a same-named Java run() whenever the Kotlin
side defines no run of its own. apply, let, also and with do the same. Removing these
false edges is what makes --callers, --impact and the ranking trustworthy in mixed Java/Kotlin
codebases — Android above all.

The evidence

On square/retrofit, --callers=run goes from 2 on main to 5 with the Kotlin support PR
(#126). Two of the three new rows are .run { User(name) } lambdas in Kotlin serialization tests —
false edges. The third is a real block.run() on a Runnable, which the fix must keep.

The kit records why the obvious levers cannot fix this on their own: arity, receivers,
own-language-first, and the external-name veto are each examined and ruled out, with three
reproduction trees and their recorded output.

Size

Medium. One extracted per-call-site fact carried through the reference cache record (with parser
and cache version bumps, their mirrors and pins), a small sorted name table in
src/externalnames.h, a candidate filter beside keepOwnJvmLanguageCandidates, a new section in
test/kotlincheck.sh, and measurements on retrofit, nowinandroid and ktor.

Prerequisites

Where to start

prompts/help-wanted/kotlin-scope-functions.md is a self-contained prompt for a coding agent. It
covers the retrofit rows, how Kotlin calls are captured and resolved, the three reproductions, the
design space (the trailing-lambda fact, the name table, the rule, where refused sites go, and what
stays out of scope), the non-negotiables, red-first acceptance arms, the known traps, and what the
fix PR's description must contain. Like every prompt in prompts/, it ends by writing a plan and
stopping
— a maintainer agrees the plan before any code is written.

Comment here to claim it.

Activity

  1. joyful-ii-V-I commented on Sep 16, 2026

    @joyful-ii-V-I
    CollaboratorAuthor

    Related, same failure class in TypeScript: #59 (built-in member calls binding unrelated functions) and its first step #163.

  2. llvm-x86 commented on Oct 24, 2026

    @llvm-x86
    Contributor

    I want to take this issue. I need a go-ahead on the plan first, as the kit asks.

    I reproduced the false edge on current main. --callers=run on the kit's tree gives two callers. build (kt/Use.kt:3, via=name) is false. kick is true.

    Plan:

    1. Add one per-call-site fact: the call has a trailing lambda. Carry it through RawRef, Reference and the cache record.
    2. Bump kCacheVersion, kParserVer and their mirrors. Re-pin test/qschemetrip.hash with a RE-PIN LOG entry.
    3. Add a sorted name table for run, let, apply, also and with in src/externalnames.h.
    4. Add a resolver rule beside keepOwnJvmLanguageCandidates in src/graph.h. A trailing-lambda call to one of these names does not bind to a Java method. Refused sites get a disposition under the fix(resolver): a declined call no longer reads as "no caller exists" — declined=, declined_calls=, and every call accounted for #136 conservation line.
    5. Add red-first arms to test/kotlincheck.sh.
    6. Measure on retrofit, nowinandroid and ktor.

    Questions:

    • Is this plan right?
    • Is the cache-version bump acceptable for this fix?
    • Should the work wait for a free slot in the cache-version sequence?

    I will write no code until a maintainer agrees.

  3. joyful-ii-V-I commented on Oct 24, 2026

    @joyful-ii-V-I
    CollaboratorAuthor

    Thanks @llvm-x86. It's yours, and after #423 we're glad to have you on another one. The plan is right in substance: the trailing-lambda fact, a name table and a site-keyed rule, with name-only vetoes rejected. Go ahead, with the updates below. The kit was written on 09-11, and main has moved since.

    Your three questions

    • Is the plan right? Yes, with the placement change in "Where the rule goes" below.
    • Cache-version bump? Yes, it's needed. The record changes shape, so kCacheVersion + kIngestCacheVersionMirror + kMinRefRecordBytes move together, and the extraction change moves kParserVer + kIngestParserVerMirror.
    • Wait for a free slot? No. Use placeholder values above everything live and say so in a comment. We renumber at landing, as we did for feat(situ,test-gate): start the caller walk from the changed symbols, not the changed files #423. Train 34, now in CI, takes main to kParserVer 226 and kCacheVersion 40, and train 35 has reserved values after that. So start at kParserVer 228 and kCacheVersion 41, and put the qschemetrip re-pin in its own commit so it's cheap to redo.

    What changed on main since the kit

    • kMinRefRecordBytes is now 60 (3×u32 + 12×u8 + 9×str), not 39. Your fact is one more u8, written and read beside memberCall / forwarded in writeRef/readRef, with RawRef → Reference copied in ingest_model.h.
    • externalnames.h now has a builtin-method family: kPythonBuiltinMethodNames, kJsBuiltinMethodNames, kRubyBuiltinMethodNames, kCppStdMemberNames and kRustStdMemberNames. Each table is generated by one command written in its provenance comment, and each has a sortedness static_assert. Give the Kotlin table the same treatment: cite the Kotlin stdlib docs page and version for the five scope functions, so the list can be regenerated and diffed, not hand-tuned.
    • RawRef::memberCall (FE-A) now records "written on a receiver" for Go/JS/TS/Rust. Kotlin isn't in it. You don't need it for this fix, but it's the natural home for the receiver gap you'll disclose (tree C).

    Where the rule goes: fit it into an existing family, not a new standalone rule
    docs/ARCHITECTURE.md (a seeds → hops → walks section lands with train 34) and CONTRIBUTING's feature-family map both push toward one mechanism per family, with languages differing by data. On main, three neighbours already cover "a call by name that cannot reach the in-tree target":

    • FE-A FalseEdgeRules (graph.h, gate test/falseedgecheck.sh). It removes candidates the call cannot reach by the language's own rules. When nothing survives, the site is external= with proof (one C external census row), and unresolved= without proof. It never declines and never retargets. Its STATED FLOORS header lists Kotlin as untouched today.
    • BuiltinMethodGate (Python/JS/TS/Ruby): it declines a builtin-named member call unless the caller's file gives evidence for the target.
    • StdMemberGate (C++/Rust): one row per language. Its comment says why Java/Kotlin get no name row, because in-tree types implement add/size on purpose. That is the same reason your plan rejects a name-only veto.

    Your rule is FE-A-shaped. The trailing lambda is a call-site proof that this is the stdlib scope function, not a guess from the name. So please implement it as a Kotlin arm of FalseEdgeRules:

    Then update FE-A's STATED FLOORS header and the #kotlin-extraction paragraph of docs/ARCHITECTURE.md with the floors you leave: receiver typing, SAM conversion, and parenthesised lambdas if you exclude them. If FE-A's structure fights you, say so in the plan reply before you write code. Keeping it beside keepOwnJvmLanguageCandidates is acceptable, but then the PR should say why it isn't an FE-A arm.

    One more thing to keep in mind: #59/#163 is the same false-edge class for TypeScript. If the table hangs off a per-language row (the StdMemberRow pattern) keyed on a call-site fact, TypeScript can reuse it later as data, not as a second rule.

    Two coordination notes

    The acceptance arms and measurements in the kit stand as written, red on a named main commit, plus test/falseedgecheck.sh if you take the FE-A route. Looking forward to the plan's numbers on retrofit, nowinandroid and ktor.

  4. added a commit that references this issue on Oct 26, 2026
    481c6f8
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

    help wantedExtra attention is needed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions