Repository navigation
Conversation
|
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. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
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. |
4dfeb2c to
0d720d5
Compare
|
Rebased onto the reworked #2360 head ( What changed in the rebase:
Re-measured on finagle against the new #2360 head (table in the PR body updated): 815 companion nodes, DEFINES +866, CALLS 30,317 → 30,311, strategy mix unchanged ( Tests: |
DeusData
left a comment
There was a problem hiding this comment.
Hi @htarnacki, thank you for this one too, and for the clear rebase notes. Using scalac's Foo$ only for real companions is exactly the conservative shape we like: standalone objects keep their QNs, and existing graphs change only where nodes used to collide. Comparing edges with $ stripped, plus the case-by-case account of the re-attributions, answered most of our questions before we asked them. Moving the companion into #2360's top-level index instead of a special case is a nice simplification, and the cppcheck fix is right.
Two requests:
- Companion detection cost (
helpers.c:173).cbm_scala_is_companion_objectscans every named sibling in the scope and copies each class/trait/enum sibling's name into the arena. It runs about three times perobject_definition(inextract_class_def, and incompute_class_qnin both extract_defs.c and extract_unified.c). That is O(objects x definitions in the scope) per file, with an allocation per comparison. It's fine for hand-written code, but quadratic on large generated files, such as a sealed hierarchy with thousands of case classes and objects in one scope. We keep extraction free of lookup-inside-loop patterns. Could you compare the names in place against the source byte range, and collect a scope's class/trait/enum names once (for example, cached on the walk frame) instead of rescanning for every object? - The
$tolerance applies to every language (registry.c:1143-1175, the$probes inresolve_import_mapandresolve_same_module, andreceiver_chain_admits). These paths serve every language without an LSP.$is a legal identifier character in Java and JS/TS (think of the RxJSuser$convention), so the assumption that only Scala emits a$-terminated owner is likely true, but it isn't enforced. We keep changes tocbm_registry_resolvelanguage-scoped for this reason. Could you either gate it to Scala (for example, record companion owner QNs at registration and acceptFoo$only for a known Scala companion), or add a guard test showing that a JS/TS or Java classFoo$next toFooleavesFoo.m()resolution unchanged?
Leaving the inner-owner $ case (Types.Name$.Unnamed, Mux.Server$.SessionF) failing closed is fine. Thank you for pointing it out; a small follow-up issue would help us track it. The one red CI job (daemon_runtime_connection_cap_covers_slow_hello_and_stopping_is_terminal on macos-15-intel) is a known timing-sensitive daemon test on our side and unrelated to your change. Since this stacks on #2360, one rework plus one rebase of the stack is probably the easiest path. Thanks again!
0d720d5 to
8116eaa
Compare
|
Thanks — both points are in the new head 1. Companion detection cost. 2.
Re-measured on finagle against the new #2360 head (table in the body updated; Tests: |
8116eaa to
8109aec
Compare
|
Rebased onto the new #2360 head ( Suites |
|
CI status on
Locally on this head clang-format on touched files and |
Scala imports were parsed with the generic import fallback, which
ignored selector groups, aliases (`{A => B}`) and wildcards, and the
declared `package` clause was not recorded. IMPORTS edges then had to be
guessed from the physical directory layout, which does not have to
mirror the package in Scala projects.
- parse Scala `import` statements into one CBMImport per selector,
keeping the alias as the local name and the full path as the target
- record the `package` clause as the file namespace; consecutive
braceless clauses (`package a` / `package b`) are joined into `a.b`
- thread the file language through import resolution so Scala imports
are resolved against declared packages; the resolver fails closed at
every step instead of guessing
- build a (package, top-level name) -> node index once per pass, next
to the namespace map, so package membership is an O(1) lookup instead
of a scan of the package's file list per same-named hit; every file
declaring a namespace contributes, so a Scala import of a Java class
in a mixed package resolves. Only the Scala resolver reads it, so it
is built only when a Scala file is among the pass's files
- "top level" is decided against the file's language-aware module QN
(stem vs directory), so a Java nested type (`Request.Builder`, one
segment below the directory module) is not mistaken for a top-level
`Builder` of the package
- `import pkg.Name` binds only a top-level `Name` of a file declaring
`pkg`: nested classes and methods neither match nor make the real
top-level symbol look ambiguous; two top-level declarations of the
same name in a split package yield no edge
- `import pkg.Owner.member` binds the member under the owner's QN and
otherwise yields no edge, so a missing member never turns `member()`
into a CALLS edge to the owner class via resolve_import_map
- `import pkg._` binds the lexicographically smallest declaring file so
the target is identical on every platform; the index records that
file (and the runner-up, for self-import) per package, so the pick is
O(1) instead of a scan of the package's file list
- incremental parity: both incremental routes hand the passes only the
changed files, so the namespace map and the Scala index used to know
only the changed files' packages, and an import into an unchanged
package resolved through the symbol fallback (or, fail-closed, not at
all) until the next full index. The passes now persist each file's
package clause on its File node, and the incremental routes read the
clauses of the files that stay back into the pass context so the
maps cover the whole project exactly as on a full build
- Scala package clauses feed only the Scala resolver: the generic
first-declaring-file walk skips Scala files, so Java/Kotlin imports of
a shared package prefix keep resolving exactly as before
- import-map lookups therefore bind an aliased local name to the exact
imported object, so `Alias.method()` resolves with the `import_map`
strategy
Tests: extraction of selectors/aliases, the package namespace and
chained package clauses; edge_imports guards that packages may ignore
the physical layout, that a Java import never binds to a Scala prefix
declarer, that a Scala import binds a Java class of a mixed package,
that a Java import of a package declared by Java and Scala files still
binds the Java file, that two same-named top-level symbols in a split
package give no edge, that a nested same-named symbol is ignored, that
an unresolvable `Owner.member` gives neither an IMPORTS nor a CALLS edge
to the owner, that chained package clauses resolve, that a Java nested
type of the same name neither binds nor makes the top-level Scala
symbol ambiguous, and that an incremental reindex (closure-repair and
legacy-partial route, each proven by the route probe) keeps the IMPORTS
and CALLS into an unchanged package; pipeline tests for alias
resolution (sequential + parallel), for member imports in a split
package, and that the Scala index is built only when a Scala file is
among the importers.
Closes DeusData#2153
Signed-off-by: Hubert Tarnacki <hubert.tarnacki@gmail.com>
A Scala `class Foo` (or trait/enum) and its companion `object Foo` share a source name, so both containers and their methods were assigned the same qualified name and overwrote each other in the graph. Use scalac's `Foo$` suffix for the companion object in every place a class QN is computed. A standalone `object Foo` with no companion keeps its plain QN, so existing graphs only change where two nodes used to collide. Resolution treats the pair as one importable/callable name: - pass_pkgmap: the companion enters the top-level index under `Foo$`, so `import pkg.Foo` binds the class (which stands for the pair) and never trips the split-package ambiguity marker; `import pkg.Foo.member` looks the member up under `Foo$` first, since only object members are importable. - registry: `Foo.make()` reaches `Foo$.make` through import_map, same_module, the qualified-tail disambiguator and the receiver-chain guard. A `$`-terminated segment is emitted only by this change, so the comparison is exact equality for every other language. Class-body `val`/`var` definitions keep their module-level QN like every other language (parent_class already records the declaring owner). Closes DeusData#2154 Signed-off-by: Hubert Tarnacki <hubert.tarnacki@gmail.com>
8109aec to
726ee32
Compare
|
Rebased onto the new #2360 head ( One conflict, in Suites |
|
Thanks for addressing the caching and companion-name review points and keeping the branch current. The series direction question on #2360 needs a maintainer answer before more routine rebases; no additional rework is requested here. |
|
Thanks again for the companion-identity fix and the completed review updates. The series decision on #2360 remains ours; no further routine rebase is requested while that is pending. Sorry for the delay—I’ll follow up soon. |
What does this PR do?
Closes #2154. Stacked on #2360: the first commit here (
bb3f1cdd) is #2360, only the second commit is this PR. Once #2360 merges I'll rebase and the diff collapses to that one commit.A Scala
class Foo/trait Foo/enum Fooand its companionobject Fooshare a source name, so both containers and their methods got the same QN and overwrote each other. The companion now gets scalac'sFoo$suffix wherever a class QN is computed (extract_defs.c,extract_unified.c, viacbm_scala_is_companion_objectinhelpers.c). Detection is O(1) per object after one scan of its scope: the scope's class/trait/enum names are recorded once as byte ranges into the source (no copies) in a per-file cache that lives in the scratch arena, and every later query in that scope is a hash probe.On the question from the issue — standalone
object Foo: only companions get the$. A standalone object has nothing to collide with, and suffixing it would rename every Scala object in existing graphs for no gain. With this rule the only QNs that change are the ones that were previously lost to a collision. Testextract_scala_trait_companion_and_standalone_objectpins both sides.Because the pair is one name at the source level, resolution has to treat it as such:
pass_pkgmap.c:import pkg.Foono longer fails closed as "ambiguous" betweenFooandFoo$(the class stands for the pair);import pkg.Foo.memberlooks underFoo$first — only object members are importable.registry.c:Foo.make()reachesFoo$.makethroughimport_map,same_module, the qualified-tail disambiguator andreceiver_chain_admits. Every$probe is gated on the registry's own record of Scala companions:cbm_registry_add_defrecordsFoo$only for a type-like definition in a.scalafile, andcompanion_known()is the single check in front of each probe. A Java or JS/TS class that happens to be namedFoo$is plain text —Foo.m()next to it resolves exactly as onmain(pinned byresolve_java_ts_dollar_class_next_to_class_is_unchanged).Class-body
val/varkeep their module-level QN like every other language (parent_classalready records the owner). My original branch qualified them by owner; measured on finagle that dropped 608same_moduleCALLS (the resolver probesmodule.name), so I removed it.Before / after on
twitter/finagle(ca472de, 1,898.scalafiles)#2360 head vs this branch, fresh
CBM_CACHE_DIReach run. Edge identity is compared with$stripped, so a plain QN rename does not count as churn.Foo$) Class nodesCALLS into companion members after: 4,938. Strategy mix is stable (
import_map2,730 → 2,724;same_module1,419 → 1,418;qualified_suffix1,139 → 1,139) — that is what the registry changes are for; without themsame_modulefell to 1,323 andqualified_suffixto 388.Of the 151 removed / 141 added CALLS (normalized), 138 / 120 are the same call re-attributed to a different caller node in the same file — e.g. in
DestinationTracing.scalatheoverride def applyinsideobject ServerDestTracingFilter { def module = new Module1 { … } }used to be filed under the class'sServerDestTracingFilter.apply(L40) because the object and the class were one node; now that scope has no def and the call lands on the file node. The 13 genuinely dropped / 21 genuinely new pairs are mostly self-targeting fixes:Rng.scalanextDoubleused to bind toBackoffTest.ZeroRng.nextDouble, now binds toRng.nextDouble;Announcer.announcemoved fromAnnouncerTest.TestAnnouncer.announcetoAnnouncer.announce;MessageEncoder.toBuffrompostgresql.FrontendMessage.toBufto its owntoBuf;BindingFactory/NameInterpreterTestbindmoved from theNameInterpretertrait toLoadedNameInterpreter. One loss I could not recover:HttpServer.java→HttpMuxer.withHandler(Java caller into a Scala companion, resolved by the Java path).The −2 IMPORTS (3 removed / 1 added):
import Mux.Server.SessionFandimport Types.Name.Unnamedused to bind because the member lived under the class QN; it now lives under the inner owner's companion (Types.Name$.Unnamed,Mux.Server$.SessionF). TheOwner$.memberprobe only suffixes the top-level owner, so the class-scoped lookup finds nothing and fails closed per #2360's rule. Left as is — probing$at every nesting level would need the same treatment in the registry, which is out of scope here; tracked in #2426. The third pair is a Java test's already-wrong import of aAtype parameter retargeting fromParameter.AtoParameter$.NullParameter.A.Non-Scala files: 5 added / 7 removed edges, all
SEMANTICALLY_RELATEDranking noise between thrift-generated Java (same as in #2360).Tests
extraction(393): class+companion owners and methods are distinct; trait companion gets$; standalone object does not; class-body vals keep module QN;extract_scala_companion_detection_is_scoped— anobject Foowhoseclass Foosits in a different scope (nested inside another object) is not a companion, and a same-scope pair still is.pipeline(298):import numeric.Rational+Rational.make(1, 2)→Rational$.makeviaimport_map;import numeric.Rational.fromInt+fromInt(2)→Rational$.fromInt. Red without the pkgmap/registry changes (the member lookup underRational$is what findsfromInt); the parallel-resolver twin (pipeline_scala_companion_parallel_import_and_factory_call) pins the same edges throughpass_parallel.registry(71):Rational.makereachesRational$.makeviaimport_map,same_moduleandqualified_suffix; a$-less owner still requires an exact segment;resolve_dollar_owner_is_companion_only_when_registered_from_scala— the plaincbm_registry_addandcbm_registry_add_deffrom a.javafile record no companion, so none of the$probes fire;resolve_java_ts_dollar_class_next_to_class_is_unchanged— a Java/TSFoo$class next toFooleavesFoo.m()resolution byte-identical tomainfor all three strategies.edge_imports(72) unchanged.Checklist
git commit -s) — required, CI rejects unsigned commits (DCO, see CONTRIBUTING.md)extraction,pipeline,registry,edge_importssuites; fullmake -f Makefile.cbm testleft to CI)lint-formatwith clang-format 21,lint-no-suppress,lint-memory-core;cppcheck2.17.1 with thelint-cppcheckflag set clean on the touched files)