R7b batch 4 #3 (docs/design/scala-kernel-port-checklist.md is the authoritative quirk list). The third vendored-grammar-C language and the biggest grammar in the tree (35MB parser.c): the vendored wasm is tree-sitter/tree-sitter-scala master@0aca5d0a6f — a post-v0.26.0 generation sync that is not a release (the 0.26.0 crate is 30 states BEHIND, so a crate pin would be a silent downgrade). NO wasm change: production has parsed with this exact revision since #91 — the kernel-grammar-parity row (ABI 15, 26,650 states, 32 fields, id-by-id tables) is the whole alignment proof. Preserved bug-for-bug (all probe-pinned): the leak-through asymmetries — extension methods mint NO nodes (first def's body calls leak to the enclosing scope, later defs invisible, and the braced form resolves its body field to the `{` TOKEN via first-match-wins field lookup → whole extension invisible); anonymous `new T { … }` template_body members leak to the enclosing scope (findAnonymousClassBody misses template_body); the bodied-vs-bodiless class asymmetry (bodiless headers walk class_parameters → default-value calls emit FROM the class; bodied ones never see them) — plus first-segment import names (`import com.example.C` → `com`), the val/var hook keyed on the enclosing-definition NODE TYPE (object vals → constants/value-ref targets, class/trait/enum/given vals → fields) with consumed initializers, every def routed through extractMethod with the top-level function fallback, nested defs in bodies minting NOTHING (the inverse of kotlin) while body-local classes extract fully, curried signatures keeping only the FIRST parameter list (type params win the `parameters` field), enum cases positioned at the CASE node with invisible params/extends tails, extends with-chains via scalaBaseTypeName, `@deprecated(args)` decorates, the #750 capitalized-chain re-encode (`WidgetS.create().render`), literal-receiver silence, static-member reads AND writes, infix invisibility, `derives` silence, scaladoc retention with the CRLF `\r` pin, full value-reference machinery (shadow prune, last-wins same-name targets, `$X`/`${X}` interpolation reads), and SCALA_SPEC fn-refs (bare ids + postfix eta unwrap + varinit, var-init non-capture). Gates: parity sweeps first-run 0-diff on os-lib/cats/scala3-compiler-src/ scala3-library-src — 1,935 clean files byte-parity, deferrals 0/15/57/116 matching the survey's predictions exactly (scala-3's PHANTOM hasError files — flag-true, zero ERROR nodes, capture-checking `^` — defer on the FLAG); full-init dumps byte-identical ×3 (os-lib, cats, scala3 whole-repo 950,889 dump lines); kernel-scala-parity suite (9 fixtures + 9 in-memory CRLF variants incl. Scala-3 indentation through the external scanner + phantom/real-error defer pins + first-segment/namespace/value-ref pins); full suite 2,669 green ×3 with CODEGRAPH_KERNEL_EXPECT=1 (kernel-scaffold's stays-wasm example moved scala → pascal). DEFAULT_ROUTED += scala (19 langs). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
63 KiB
Scala kernel port (R7b) — the bug-for-bug checklist
Status: SURVEY COMPLETE (2026-07-20), port not started. Survey basis: every
TS-side branch a .scala/.sc file exercises, with file:line anchors as of
45a53eb (HEAD at survey time, clean main; a6c62d7..45a53eb touched only
grammars.ts/kernel/index.ts/wasm inside src/extraction — tree-sitter.ts,
languages/scala.ts, function-ref.ts, and tree-sitter-helpers.ts are
byte-unchanged, so kotlin/swift-checklist anchors into shared code remain
valid). Every grammar-shape claim below was probed against the
production vendored wasm (dist/extraction/wasm/tree-sitter-scala.wasm,
sha256 7945b13e…, identical to src/…), and every extraction-behavior claim
was pinned against the real dist/ extractor (extract-*.txt ground-truth
dumps) — not derived from code reading. childForFieldName truth tables were
probed directly (brace-field.out, the FIELDS sections of cst-snippets.txt)
because this grammar attaches field names to ANONYMOUS tokens — CST dump
labels alone mislead (the swift lesson, §Extension). Probe scripts + dumps in
the session scratchpad svy-scala/ (§Probe artifacts). Read WITH
docs/design/rust-kernel-migration-plan.md (§0a recipe, §2 boundary, §4
tracker row "scala", §5 gates) and the format precedents
(kotlin-kernel-port-checklist.md — the closest sibling: JVM family, hook
property branch, re-encode gate — and swift-kernel-port-checklist.md).
Blocking findings: none — but three eyes-open items. (1) The wasm is
already vendored and needs NO bump: VENDORED_WASM_LANGS has contained
scala since 2026-05-07 (#91), and the vendored wasm is table-identical to
tree-sitter/tree-sitter-scala master@0aca5d0a6f (verified twice: the
batch-4 positional table compare, and this survey's clone-sha match). The port
is vendored-grammar-C only (kotlin mechanism) — there is no behavior-delta
gate to run. (2) Error incidence is bimodal: mainstream Scala-2-style
repos are clean (os-lib 0.00%, cats 1.80%) but bleeding-edge Scala-3 code is
not (scala3 compiler/src 9.88%, library/src 17.79% — capture-checking ^
types), and ~40–60% of erroring files are PHANTOMS (hasError=true, zero
ERROR/missing nodes — trust the flag). Sweep guidance in §Architecture #6.
(3) scala has 32 real fields but three load-bearing places where
childForFieldName returns the FIRST of SEVERAL same-named fields — or an
anonymous { token (import path, curried parameters, extension body).
The walker must reproduce first-match-wins over the full (named+anonymous)
child list, not "the" field (§Extractor config, §Extension).
Grammar prep (NO wasm bump — vendored-C kernel build only)
- Production wasm:
src/extraction/wasm/tree-sitter-scala.wasm, sha2567945b13e6f9b15b578c5e5e4e60253c049fec07c531518163f3415a76c0621aa(src == dist byte-identical), ABI 15, STATE_COUNT 26650, 357+5 symbols, FIELD_COUNT 32. InVENDORED_WASM_LANGS(grammars.ts:292) since #91 (2026-05-07). Mappingscala: 'tree-sitter-scala.wasm'(grammars.ts:38). - Provenance (verified): tree-sitter/tree-sitter-scala
master@
0aca5d0a6fe115b16d55cb100e1bb05e7fb11385(2026-04-22 "chore: generate and sync from ec71cd9d51" — the post-v0.26.0 fix batch: scala2-compiler-100%, lambda-body restrict, wildcard self-type). The clone'ssrc/parser.csha256 equals the batch-4 probe's positional-table-verified copy (bc3c3c79…), and that probe found the wasm's kind/field tables positionally identical to it. The v0.26.0 tag == crate 0.26.0 is 26620 states — 30 states BEHIND our wasm — so a crate pin would be a silent downgrade; later master (fc99b1bd, Apr-27) is a 23959-state restructure — a future-bump candidate, NOT this port. Full record:../scratchpad/batch4-grammar-probe.md. - Vendored-C route (kotlin mechanism, second use) — copy from the
0aca5d0a6fclone intocodegraph-kernel/grammars/scala/(shas recorded in a comment; survey recordgrammar-shas.txt):src/parser.cbc3c3c794f19461d99d04de6c31d57fa3e41243509b9ab023a9b88ed3273d102(34,970,232 bytes — 35 MB, the biggest grammar in the tree; expect a slowccstep)src/scanner.ce4ba242568ee3493015598997bf60f613802616eade62717c21109287ef64752(17,731 bytes — a REAL external scanner: significant-indentation + interpolation; it handles\rexplicitly, scanner.c:476)src/tree_sitter/alloc.hb29c1c9f…,array.h31e60a1b…,parser.h180b893c…- build.rs: crib the upstream
bindings/rust/build.rsflag set —cc::Build.std("c11").include(grammars/scala).flag_if_supported("-Wno-unused"), msvc-utf-8; compile parser.c + scanner.c; symboltree_sitter_scala(parser.c:1199034). Same shape as the kotlin block (build.rs:14-22). - Cargo:
tree-sitter-languageshim already present (kotlin). langs.rs:extern "C" { fn tree_sitter_scala() -> *const (); }+"scala" => LanguageFn::from_raw(...)+LANGUAGES15 → 16. __tests__/kernel-grammar-parity.test.ts:39GRAMMAR_LANGUAGES += 'scala'— the id-by-id table compare against the vendored wasm proves the C build is the same revision (ABI 15 / 26650 states / 32 fields must match).- License: MIT (tree-sitter org). No metadata shim needed (repo has
tree-sitter.json-era layout; we never run
tree-sitter generate).
- NO grammar-bump gate. Unlike every other R7b language there is no old-vs-new wasm diff to run — production already parses with this exact revision. The kernel-grammar-parity row IS the alignment proof.
- Scanner-state parity risks — probed clean: the indentation scanner
handles CRLF: LF-vs-CRLF parses are s-expression-identical on every
fixture including Scala-3 indentation syntax (
crlf-cst.cjs: indent/ torture/docs/vref/misc/ext/script allsexpEqual=true, no error flips). Extraction under CRLF is byte-identical except the multi-line-block-comment docstring\rretention (§Docstrings — a sharedjs_multiline_stripconcern, not a scanner one). The kernel parses UTF-8 while wasm parses UTF-16 — error-recovery differences are exactly what per-filehas_error() → defer:guards; nothing scala-specific beyond the elevated incidence below.
Error incidence (production wasm, all .scala/.sc ≤1 MiB, error-sweep.cjs)
| Repo | files | hasError | rate | of which PHANTOM |
|---|---|---|---|---|
| os-lib (small) | 59 | 0 | 0.00% | — |
| cats (medium) | 835 | 15 | 1.80% | 4 (27%) |
| scala3 compiler/src | 577 | 57 | 9.88% | — |
| scala3 library/src | 652 | 116 | 17.79% | 69 (59%) |
| scala3 whole repo | 18,411 | 1,991 | 10.81% | (incl. tests/ = deliberately-invalid neg fixtures, 11.78%) |
.sc files: 14 in scala3 (1 error); os-lib/cats have none. Error classes
(sampled err-samples.out, minimized errvariants.out / phantom-min.out):
- (a) PHANTOM hasError — the dominant scala-3 class. hasError=true with a
COMPLETE, correct CST and ZERO ERROR/missing nodes. Minimal repro:
capture-checking postfix
^—def f(x: List[Int]^): Int = 1(the whole scala3 library usesimport language.experimental.captureChecking). cats' scala-2 macro files (FreeFoldStep.scala) phantom too. The kernel must defer on the FLAG, never on ERROR-node presence (kotlin lesson, worse here). - (b)
endused as an identifier —endis hard-reserved by the grammar (end match,val end = 1both ERROR). catsAndThen.scala. - (c) generic + curried super-constructor args —
extends Eq[A]()(ev)ERRORS (plainBase(1)(2)parses clean —extract-super2.txt). cats kernel instances. - (d) Unicode symbolic type names —
type ⊥ = Nothing(catspackage.scala). - (e) scala 3.0–3.3
given … with { }syntax —given x: C with { … }ERRORS (the= new C {}and colon forms parse fine). - (f) assorted dotty-frontier syntax (union-of-singleton
.typeunions, compactcatch case, bracelessmatchat margin) — scala3 compiler files.
All classes are grammar-inherent and identical across arms by construction (same grammar revision compiled twice). Deferral guidance: §Architecture #6.
Architecture decisions
- No preParse, no POST_PASSES.
scalaExtractorhas nopreParse(languages/scala.ts — whole file) → the kernel/index.ts preParse hoist is a no-op; noPOST_PASSESentry →tryKernelExtractRawstays eligible. - One framework resolver can force the DECODED path for scala:
playResolver(resolution/frameworks/play.ts:30,languages: ['scala', 'java', 'yaml'], and it HASextract()), via parse-worker.ts's frameworksNeedDecode check (parse-worker.ts:95-100). detect() (play.ts:32):build.sbtmatching/playframework|"play"|sbt-plugin|PlayScala|PlayJava/i, ORconf/routesexists, ORconf/application.confexists. None of the three gate repos trips it (noconf/, no playframework in build.sbt) — they exercise the raw buffers-to-store transport; a Play app (or any repo with a rootconf/application.conf— akka-style apps can!) is the decoded-path smoke check. The Play extract() itself only produces output forconf/routes/*.routesfiles (isPlayRoutesFile, grammars.ts:222-228) which are NOT scala files (extensionless → no-grammar path) — the cost of detection is only the decode, not wrong output. - One walker module (
codegraph-kernel/src/scala.rs), registered in langs.rs; per-filehas_error()→defer:. kotlin.rs is the closest crib (visitNode-hook property branch, classify-by-node-type, re-encode gate, JVM import shapes) but scala diverges in TEN places, each detailed below: (a) no namespace node ever (no packageTypes — package headers are ignored; QNs are bare); (b) functionTypes EMPTY → every def routes through the methodTypes branch (extractMethod → top-level fallback to extractFunction); (c) the val/var hook keys on the enclosing-definition NODE TYPE walk, not the stack; (d) getSignature is LIVE (fields exist) with the curried/type-params first-field quirk; (e) extension/given/ package_object have NO ladder branch — their leak-through behaviors are the port's hardest part; (f)instance_expression∈ INSTANTIATION_KINDS + scalaBaseTypeName; (g) the scala extends branch iterates ALL supertypes; (h) scala-only type-annotation walks (everyparameters+ type_parameters context bounds) plus the hook's own emitScalaTypeRefs; (i) fn-ref spec with bare-identifier idTypes + postfix eta unwrap; (j) imports named by the FIRST path segment. No c/cpp-style dialect, no content sniffing:.scalaand.scboth →scala(grammars.ts:120-121;.sbtis NOT mapped — build.sbt files are never indexed). .scfiles are ordinary scala files whose top-level statements attribute calls to the FILE node (pinnedextract-script.txt:calls println/runTop from=file, top-levelval→ constant with file parent). Same for.scalafiles with top-level statements (grammar accepts them).- REF_FLAG_FILE_PATH (wire v2 slot) is NOT needed. No scala path emits
refs carrying
filePath(hook refs via emitScalaTypeRefs carry fromNodeId/name/kind/line/column only; extractImport sets no handledRefs; verified across every ground-truth dump — zero refs printed a filePath). Nodedecoratorsare likewise never set (no extractModifiers hook) — the decorator channel isdecoratesREFS only. - Deferral expectations: os-lib 0, cats 15/835 = 1.8%, but Scala-3-heavy
repos run 10–18% (§Grammar prep table) with phantom-dominated error sets.
Default
--max-deferral 0.1HOLDS on os-lib/cats and on mainstream Scala-2 style; sweeps over scala3-style repos need--max-deferral 0.3(swift precedent). A deferral-rate JUMP on cats/os-lib is the bug signal; a big number on dotty-frontier code is grammar reality.
Extractor config (languages/scala.ts — 212 lines, read it whole)
Types: functionTypes=[] (comment: "top-level function_definition is
handled via methodTypes"); classTypes=[class_definition, object_definition,
trait_definition]; methodTypes=[function_definition,
function_declaration]; interfaceTypes=[]; structTypes=[];
enumTypes=[enum_definition]; enumMemberTypes=[] (hook-handled);
typeAliasTypes=[type_definition]; importTypes=[import_declaration];
callTypes=[call_expression]; variableTypes=[] and fieldTypes=[] (hook);
extraClassNodeTypes=[]. nameField=name, bodyField=body,
paramsField=parameters, returnField=return_type. interfaceKind=trait
(unused in practice — extractInterface is unreachable, see §dispatch).
Field semantics (32 real fields — but first-match-wins bites 3×):
childForFieldName(f) returns the FIRST child carrying field f, and in this
grammar (i) several parents attach the same field to MULTIPLE children, and
(ii) anonymous tokens can carry fields (probed, brace-field.out):
braced extension (t: Int) { … } puts field body on {, the
function_definition, and } — first match is the { token with
namedChildCount 0; a for header puts enumerators on {/enumerators/}.
The walker's field lookup must scan the FULL child list (named + anonymous)
in order, exactly like tree-sitter's ts_node_child_by_field_name.
Hooks PRESENT (port each exactly):
-
visitNode (scala.ts:131-198) — runs for EVERY node the main walker visits (tree-sitter.ts:943-953; NOT in visitFunctionBody). Three branches:
val_definition/var_definition(:135-170) — the LIVE branch. Name via getValVarName (:5-11):patternfield;identifier→ its text; else the pattern's first DIRECT namedChild of typeidentifier(val (ta, tb)→ ONE node namedtaspanning the whole val;val Some(v)→v;val multiA, multiB = 5→multiAonly — pinned, unlike kotlin's mint-nothing destructuring); no identifier → return false (falls through to… nothing — no other branch matches, children recursed). Then the enclosing-definition walk up node.parent (:146-156): first ofclass_definition | trait_definition | enum_definition | given_definition | object_definitionwins. isInstanceField = class/trait/enum/given → kindfield; object_definition or NOTHING (top level, package_object, braced package) →val→constant/var→variable. NOTE vs kotlin: there is NO 'local' arm — the hook never runs inside bodies (visitFunctionBody doesn't call it), so body-local vals are handled by plain recursion (§Body walker).lazy val→ constant (modifiers don't matter). Extra:signature=`val|var ${name}: ${typeText}`ONLY when atypefield exists (else undefined —val x = 1has NO signature; pinned),visibilityvia extractVisibility (:69-80). Then emitScalaTypeRefs(typeNode, created.id) (:27-45): everytype_identifierin the type subtree EXCEPT SCALA_BUILTIN_TYPES (:14-17 — Int/Long/Short/Byte/Float/Double/Boolean/Char/Unit/String/ Any/AnyRef/AnyVal/Nothing/Null) →referencesref FROM THE VAL NODE at the type_identifier's position (pinned:val SHARED_TABLE: Map[String, Int]→ referencesMaponly — String/Int builtin-skipped;var cb: () => Unit→ nothing). Return true → dispatcher runsscanFnRefSubtree(node, 0)and NEVER descends → top-level/class-scope property initializers emit NO calls/instantiates refs (val topInit = WidgetS.create(),val topLazy = compute(),val n = new Foo {…}→ nothing — pinned) — but the SCAN still captures fn-ref candidates (§Function-refs; note the scan's nested-def halt checks functionTypes, which is EMPTY for scala, so it descends into nested function_definitions inside a hook-consumed val — it halts only atlambda_expression(tree-sitter.ts:611):val fnField = (x) => runLam(x)captures nothing, pinnedextract-edge2.txt).enum_case_definitions(:173-183) — for each directsimple_enum_case|full_enum_casechild:enum_membernode named by the case'snamefield, positioned at the CASE node (socase Custom(rgb: Int)spans the params andcase Earth extends Planet(5.9)spans the extends — pinned cols inextract-torture.txt). One wrapper percaseline;case Red, Green= one wrapper, two cases. Return true → scanFnRefSubtree; consequences: case parameters are invisible, a case's extends_clause emits NO extends ref, and calls inside case ctor-args emit nothing.extension_definition(:186-195) —body = childForFieldName('body')then visit the body's namedChildren. Because fieldbodyis attached to EACH def (and to{/}in braced form), this is a triple quirk, all pinned (extract-ext.txt,brace-field.out):- paren/indent form: body = the FIRST
function_definition→ its CHILDREN are visited → no node is ever minted for any extension method; the first def's body expressions reach the ladder → its calls emit FROM THE ENCLOSING SCOPE (file/class) at their own positions (calls concat from=file); non-call bodies (s.length) emit nothing. - every def after the first is COMPLETELY invisible (never visited).
- braced form (
extension (t) { … }): body resolves to the{TOKEN → namedChildCount 0 → the whole extension is invisible (zero nodes, zero refs —extract-ext.txtext2). Return true always (even when body lookup finds nothing).
- paren/indent form: body = the FIRST
-
getSignature (scala.ts:110-117) — LIVE.
params = childForFieldName('parameters'),ret = childForFieldName('return_type'); none → undefined; sig = paramsText + (ret ?: ${retText}: ''). QUIRKS, PRESERVE (pinned inextract-torture.txt):- Curried defs: FIRST parameter list only —
def curried(a: Int)(b: String)(implicit ord: Ordering[Int]): Int→ sig(a: Int): Int. - A def with type parameters: the TYPE param list wins — on
function_definitionthe type_parameters node carries field nameparametersand precedes the value list →def genericDef[A: Numeric, B <: BoundT](x: A): B→ sig[A: Numeric, B <: BoundT]: B;def genericLeak[T](t: T): T→[T]: T. - No params, ret only →
: Int(RichIntS::twice). Empty parens →(). Secondary ctordef this()→().
- Curried defs: FIRST parameter list only —
-
getReturnType = extractScalaReturnType (scala.ts:56-67) —
return_typefield text, trimmed:this.-prefixed (fluentthis.type) → undefined; strip\[[^\]]*\]generic args (non-greedy single pass:List[Bar]→List), strip all\s, take last.-segment; must match/^[A-Za-z_]\w*$/. Pinned:: WidgetS→WidgetS;: com.example.other. Remote→Remote;: T→T(generic leak, preserve); inferred → undefined;Unit/Nothingare NOT filtered (unlike kotlin —unitRethas ret="Unit", pinned). Feeds matchDottedCallChain (§Frameworks). -
getVisibility → extractVisibility (scala.ts:69-80) — scan direct namedChildren of type
modifiersORaccess_modifier; TEXT.includes('private')→ 'private',.includes('protected')→ 'protected'; default 'public'. Kotlin-style includes-on-raw-text:private[b]/private[this]→ private (pinned QualPriv). Applied to functions/methods/classes(+objects)/enums AND (via the hook) vals/vars. NOTE: in the real CSTaccess_modifiersits INSIDEmodifiers— the modifiers arm is what fires; keep both arms anyway. -
isAsync (scala.ts:121) — literally
() => false: every function/method carriesisAsync: false. -
isStatic (scala.ts:123-129) — modifiers text
.includes('static')→ scala has nostatickeyword → always false in practice (annotations are NOT inside modifiers in this grammar, so no kotlin-style text false positive channel — but port the text scan, not a constant). -
classifyClassNode (scala.ts:105-108) —
trait_definition→ 'trait', else 'class'. So object_definition → kindclass(companions/case objects included) and trait → kindtraitviaextractClass(node, 'trait')(ladder :1014-1015 — extractInterface/ interfaceKind is DEAD code for scala). -
extractImport (scala.ts:200-211) — signature = trimmed full node text; moduleName =
childForFieldName('path')text. Each dotted segment is a separatepath-fielded identifier → first-match-wins → the import node/ref is named the FIRST SEGMENT:import com.example.other.OtherClass→ name/refcom(pinned ×5 inextract-torture.txt;import single→single;import a.b→a). The identifier/stable_identifier fallback (:204-209) is dead (path always present). Consequence for the fn-ref gate: importedNames = {com,single, …} — imported class simple names NEVER enter the gate (unlike kotlin's last-segment rule — flushFnRefCandidates' QUALIFIED_IMPORT never sees the full path because the ref name is only the first segment).
Hooks ABSENT (the walker must NOT invent them): preParse, resolveName,
recoverMangledName, isMisparsedFunction, isConst, isExported
(undefined everywhere except the file node's literal false),
classifyMethodNode, extractPropertyName, propertyTypes, packageTypes/
extractPackage (→ extractFilePackage returns null → NO namespace node,
EVER — package headers are ignored and every top-level QN is bare; pinned),
getReceiverType (no receiver-QN surface, no owner-contains fallback),
resolveBody (body via the body FIELD everywhere), extractModifiers (no
node.decorators), extractBareCall, synthesizeMembers, skipBodilessClass
(bodiless class Foo mints a node — the :1685 comment names Scala),
methodsAreTopLevel, resolveTypeAliasKind, interfaceTypes machinery.
tree-sitter.ts branches (anchors as of 45a53eb)
visitNode dispatch — what each scala node hits (ladder at 936-1303)
| Node | Branch | Behavior |
|---|---|---|
| every node | visitNode hook first (:943) | val/var, enum_case_definitions, extension consumed; handled → scanFnRefSubtree + STOP |
| every node | maybeCaptureFnRefs (:990) | fires for arguments/assignment_expression/val_definition (SCALA_SPEC keys) in visitNode context too |
function_definition/function_declaration |
methodTypes:1027 (functionTypes EMPTY — :994 never fires) | extractMethod:1737 → gate :1747: inside class-like → method; top level (no receiver hook, no methodsAreTopLevel, parent never object/object_expression) → falls to extractFunction:1517 → function. function_declaration = bodiless def (def m(): Int in traits/abstract classes) — same routing, no body walk |
class_definition |
classTypes:1005 → classify | 'class' → extractClass:1679. Includes case class, implicit class, abstract class |
object_definition |
classTypes:1005 | extractClass → kind class (companion objects, case object, object X extends App) |
trait_definition |
classTypes:1005 | classify 'trait' → extractClass(node, 'trait') → kind trait (extractInterface:1834 is UNREACHABLE) |
enum_definition |
enumTypes:1064 → extractEnum:1914 | §Enums |
type_definition |
typeAliasTypes:1071 → extractTypeAlias:2890 | plain type_alias node (top level AND as a class member — Outer2::Member pinned). QUIRK: the alias-value ref walk reads field 'value' → scala's field is 'type' → NO reference to the aliased type (returns false → children re-visited, nothing matches). opaque type identical |
import_declaration |
importTypes:1209 → extractImport:3170 | §Imports |
package_clause |
no branch | recursed. Header form: nothing extracted, nothing pushed — contents stay file-parented with bare QNs. Braced form package a.b { class X }: recursion reaches the members (class X → file-parented, QN X — pinned extract-misc.txt). Multiple/chained package clauses likewise ignored |
package_object |
no branch | recursed → template_body members visited with FILE on stack: defs → extractMethod → not-class-like → functions, vals → hook (enclosingDef walk finds NOTHING — package_object is not in the list) → constants/variables; all bare-QN file children (pinned: pkgHelper function, pkgShared constant) |
given_definition |
no branch | recursed. given x: T = new T {…} → instance_expression child → :1255 → instantiates from the enclosing scope + (anon-body via recursion) §instance_expression row. given T = expr → expr recursed (calls emit from enclosing scope — summonOrd() pinned). No given node is ever minted; a given's name binds nothing |
extension_definition |
hook | §Extractor config — first-def leak / braced invisibility |
call_expression (top level / template_body statements) |
callTypes:1248 → extractCall:3684 | class-body statements (require(size > 0) after the primary ctor, object Boot extends App { bootUp() }) attribute to the CLASS node — pinned extract-edge2.txt |
instance_expression |
INSTANTIATION_KINDS:1255 (:360 names scala) → extractInstantiation:4610 | §Instantiation. findAnonymousClassBody:4815 looks for class_body/declaration_list — scala's anon body is template_body → null → extractAnonymousClass NEVER runs → skipChildren stays false → children recursed: the template_body's defs hit methodTypes → extractMethod → (not class-like at top level) → anon-class methods leak out as functions/methods of the ENCLOSING scope — pinned: top-level given's compare → function compare (bare QN); given-inside-object's compare/innerVal → method/field OF the object (Registry::compare, extract-given2.txt) |
infix_expression |
no branch | recursed; left/operator/right fields. Infix calls are INVISIBLE: list map transform, a foo b, 1 :: rest, x + y, counter += 1 emit NOTHING (no calls ref, no fn-ref — pinned) |
assignment_expression |
no ladder branch; SCALA_SPEC dispatch | fn-ref rhs capture (§Function-refs); children recursed (LHS field_expression reaches extractStaticMemberRef in bodies — §Static-member) |
annotation |
no branch (child of its definition) | consumed by extractDecoratorsFor from the decorated node (§Decorators) |
lambda_expression / case_block / match_expression / for_expression / indented_block / block |
no branch | recursed transparently (in bodies via visitForCallsAndStructure) |
INSTANTIATION_KINDS others / impl_item / swift property / property_signature / export_statement (TS) |
never | not scala node kinds; the swift property branch :1121 is language-gated |
Node creation, IDs, qualified names
- createNode (:1308): id =
generateNodeId(filePath, kind, name, startRow+1)=`${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex.slice(0,32)}`. FILE node id =file:${filePath}(:509), name = basename, qualifiedName = filePath, endLine =source.split('\n').length, isExported false. - endLine extension via resolveBody (:1329-1334) calls the hook — ABSENT for
scala →
getChildByField(node, 'body')… note :1330 only callsthis.extractor.resolveBody?.()— hook absent → no extension ever (scala bodies are child nodes, in-range anyway). Kernel: skip. - contains edge from stack top for every node (:1363); extractModifiers merge (:1355) inert; captureValueRefScope (:1374) live (§Value-refs).
- buildQualifiedName (:1447-1460): stack names joined
::, file node excluded, namespacePrefix always empty. No namespace node → top-level QN = bare name even in a packaged file (pinned everywhere). Companion objects: class + object are TWOclass-kind nodes with the SAME name (different start lines → different ids) and their members share one QN namespace —WidgetS::render(class) andWidgetS::create(companion) are indistinguishable by QN (pinned; no disambiguation — preserve). - isInsideClassLikeNode (:1486): stack-top kind ∈ {class, struct, interface, trait, enum, module} — objects count (kind class), traits count, enums count.
- isClassScopeConstantAssignment (:1508): needs node.type
assignment— scala's isassignment_expression→ always false (Ruby-only, dead).
extractFunction / extractMethod (:1517 / :1737) — every def
Route recap: ALL defs → extractMethod first. In class-like → method node.
Top level → :1747 gate fails → extractFunction (the object/
object_expression parent check :1751 never matches scala) → function node.
extras (both): docstring (§Docstrings), signature (LIVE hook — §config),
visibility, isAsync false, isStatic false, returnType (hook); isExported only
on extractFunction path → undefined (hook absent). extractTypeAnnotations
(§Type-annotation refs) then extractDecoratorsFor (§Decorators), push, body =
getChildByField(node, 'body') (block / indented_block / EXPRESSION — a
single-expression body like = new WidgetS(a) or = a + 1 is walked as the
body; pinned instantiates from topLevel), visitFunctionBody, pop.
- Names:
namefield. operator defs keep the operator text as the name (+,::—operator_identifiernode;unary_-is a plain identifier; pinned methodWidgetS::+). Backtick names keep backticks (class `Weird Name`/method `strange def`pinned). Secondary constructors are methods namedthis(def this() = this(0)→ methodWidgetS::this+ calls refthis— pinned; the NAME_STOPLIST only applies to fn-refs, not calls). function_declaration(bodiless def): method node minted, sig/ret intact, no body walk (AbsS::abstractMpinned).
extractClass (:1679) — classes, objects, traits + the bodiless-header asymmetry
resolvedBody = hook(absent) ?? getChildByField(node, 'body') → template_body
(braces or scala-3 colon form — same node type) / enum_body for enums. NO
skipBodilessClass → bodiless mints. extras: docstring, visibility, isExported
undefined. Then extractInheritance (§below) — extends refs precede member
emissions. extractCsharpPrimaryCtorParamRefs — csharp-gated no-op.
extractDecoratorsFor (annotations on the class — @deprecated class Old →
decorates). Push, walk body children via visitNode, pop.
- Bodied class: only template_body children visited → the
class_parameters(primary ctor) child is NEVER walked: ctor params mint no nodes, their types emit NO references, their default-value calls emit NOTHING (pinned: WidgetS'slabel: String = defaultLabel()→ silent; case-class fields invisible — DataS has zero members). - Bodiless class/object: body = the node itself (:1714) → HEADER children
visited: class_parameters recursion reaches default-value
call_expressions → calls from the CLASS node (pinned:case class DataS(x: Int, y: String = mkY())→calls mkY from=class:DataS), and extends_clauseargumentsrecursion reaches super-ctor arg calls the same way. Reproduce the asymmetry exactly. (The re-visited extends_clause emits nothing extra — extends refs come only from extractInheritance; type nodes/class_parameter children match no ladder branch.) - Class-body members: val/var → hook (fields — or constants/variables inside
OBJECTS via the enclosingDef walk, making them value-ref targets); defs →
extractMethod; nested class/object/trait/enum/type_definition → their
branches (QN chains pinned:
Outer2::InnerObj::IC); template_body STATEMENTS (calls) → extractCall from the class (§dispatch); secondary ctor → methodthis. - Traits: extractClass with kind trait — visibility IS emitted for traits
(extractClass path, not extractInterface — vis="public" pinned) unlike
kotlin's interface path. Trait vals → hook 'instance' → field
(
Drawable::traitValpinned); bodiless trait defs → methods. - Self-types (
trait X { self: Y => … }): the self-type is invisible (no refs, no node); members extract normally (pinnedextract-misc.txt).
Enums (:1914 extractEnum + hook branch 2)
body = enum_body (REQUIRED — a bodiless enum would mint nothing; doesn't
occur). extras: docstring, visibility, isExported undefined.
extractInheritance runs (an enum's own extends_clause). Body loop
(:1941-1950): enumMemberTypes is EMPTY → every child goes through visitNode:
enum_case_definitions → hook → enum_member nodes positioned at the case
nodes (simple case = just the name extent; param/extends cases span their
tails — cols pinned); function_definition → extractMethod (enum is
class-like → Http::describe); vals → hook → fields. extractEnumMembers
(:1958) is DEAD for scala. QUIRKS, PRESERVE: case parameters
(Custom(rgb: Int)) and per-case extends (case Earth extends Planet(5.9))
are completely invisible — no field nodes, no extends refs, no calls from
ctor args (hook consumption). Enum class_parameters (enum Planet(mass: Double)) are invisible like any bodied class's.
Imports (:3170-3236)
Hook returns {moduleName: FIRST path segment, signature: trimmed full text} →
import node (name/QN = first segment) + the generic imports ref
(:3183-3194): {fromNodeId: always the file node (no namespace),
referenceName: first segment, line/column of the import_declaration}. No
scala-specific emit pass (:3197-3234 all gated to other languages). Shapes
(CST pinned in cst-snippets.txt §imports):
import a.b.C→ threepathidentifiers → namea.- selectors
{C, D}(namespace_selectors), wildcard_/*/given(namespace_wildcard), renames{X => Y}(arrow_renamed_identifier) /{X as Y}(as_renamed_identifier, name/alias fields) — ALL invisible: the name is still the firstpathsegment; selectors/aliases bind nothing. import single(one segment) →single.- No comment-gluing (kotlin's quirk does NOT reproduce — scala comments stay siblings; docs pinned separately).
extractCall (:3684) — the scala paths
Entry: not vbnet/erlang/ruby/arkts. func = getChildByField(node, 'function') ?? namedChild(0) (:4313) — scala call_expression HAS a real
function field. cpp operator recovery :4324 gated off.
Member branch (:4364) — func.type === field_expression (in the :4364
list):
- property = getChildByField('property') → null; getChildByField('field')
→ the member identifier (scala field_expression fields:
value+field). The kotlin navigation_suffix fallback is dead. - receiver = object/operand/argument fields → all null →
func.namedChild(0)= thevaluechild. - LITERAL_RECEIVER_TYPES (:4397, set :373-388): scala hits
string("lit".toUpperCase()) andinteger_literal(5.toString()) → emit NOTHING (pinned). Port the whole set. - receiver
identifier(:4401) not in SKIP_RECEIVERS {self,this,cls,super} →`${recv}.${method}`(w.render,Registry.register,obj.method).this/superreceivers are plainidentifiernodes with those TEXTS → SKIP → bare methodName (this.mine()→mine,super.hashCode()→hashCode— pinned; unlike kotlin's this_expression path, same net effect). - receiver
call_expression+ scala in the gate (:4408-4418) → the #750 re-encode, scala arm (:4443-4464): innerFn =getChildByField(receiver, 'function')(a REAL field here — NOT kotlin's namedChild(0)) → text with->→.then\s+stripped; re-encode ONLY when/^[A-Z]/(:4461) →`${innerCallee}().${methodName}`. Pinned:WidgetS.create().render()→WidgetS.create().render+ innerWidgetS.create(recursion);Foo(1).bar()→Foo().bar+Foo(companion-apply chain);lowerFactory().chain()→ barechain+lowerFactory. - receiver
field_expression(2-hopa.b.method3()) or anything else → bare methodName (pinned).
Else branch (:4518-4520) — calleeName = RAW func text: bare helper;
apply-sugar WidgetS(1) → calls ref WidgetS (capitalized bare —
resolution's CONSTRUCTS_VIA_BARE_CALL handles it, §Frameworks);
this(0) in a secondary ctor → calls this. QUIRKS, PRESERVE (pinned):
generic_functioncallee keeps its type args:genericCall[Int](1)→ callsgenericCall[Int](the c/cpp<-strip :4542 is gated off and targets<anyway; deterministic garbage — reproduce byte-for-byte).- Curried calls emit raw-text inners:
curried(1)(2)→ outer callee = func (a call_expression) raw textcurried(1)+ innercurried(recursion);Foo(1)(2)→Foo(1)+Foo(pinnedextract-misc.txt). - The parenthesized-conversion regex (:4530) applies (single-name parens rewrite) — port it.
- Final ref: {callerId = stack top, name, line = call startRow+1, column = call startColumn (UTF-16)}. Chains/args re-visited by recursion.
- Calls inside string-interpolations EMIT (in bodies):
s"… ${w.render()} …"→ callsw.renderat the inner call's position (interpolation > block > call recursion);$id(interpolation > identifier) emits no call (pinned). - Lambda args (
xs.map(el => …)/{ el => … }/ partial-fn{ case q => … }) — arguments node isarguments/block/case_blockrespectively; all recursed; inner calls attribute to the ENCLOSING function (lambdas mint nothing).
Instantiation — instance_expression (:4610, scala arm :4647-4662)
ctor = constructor/type/name fields → all null on instance_expression
(only arguments is a field on it) → namedChild(0) = the type node →
scalaBaseTypeName (:201-224): type_identifier/identifier → text;
generic_type → recurse namedChild(0) (new Monoid[Int] → Monoid);
stable_type_identifier/stable_identifier → LAST identifier segment
(new a.b.C() → C); default → first direct type_identifier child ?? null.
→ instantiates ref at the instance_expression's position (the new).
Emitted from: expression bodies (def f = new W(a) — from the function),
body statements/initializers in bodies, given RHS at any scope (from
file/class), ladder-visited statement positions. NOT emitted from
hook-consumed val initializers (val topInit = new W(…) at top/class
scope → nothing — pinned). Anonymous-class bodies: §dispatch row — never an
anon class node; members leak to the enclosing scope; no extends ref to
the instantiated type (extractAnonymousClass never runs).
Static-member / value-read refs (:4750-4808) — scala IS in STATIC_MEMBER_LANGS (:346)
Called ONLY from the body walker (:5218). field_expression ∈
MEMBER_ACCESS_TYPES (:327). Mechanics: callee-of-call skip (:4772-4778 —
Registry.register(w)'s callee nav emits nothing); recv = object/expression/
scope fields (null) ?? namedChild(0); accepted type identifier (:4792);
text /^[A-Z][A-Za-z0-9_]*$/ → references ref at the RECEIVER's
position. Pinned (extract-torture.txt StaticReads):
val c1 = Registry.count→ referencesRegistry;Http.Ok→ referencesHttp;process(Registry.count)(argument position) → referencesRegistry(plus theprocesscall).com.example.Fq.CONST_READ→ NOTHING (outer recv is a field_expression; the innermostcomis lowercase).- Assignment WRITES DO emit (unlike kotlin):
Registry.count = 5— the LHS field_expression is a plain child ofassignment_expression, visited by body recursion → referencesRegistry(pinned). Preserve. - Top-level/class-scope reads emit nothing (body walker only); hook-consumed initializers doubly so.
Decorators — scala annotations DO emit decorates, args included
extractDecoratorsFor (:4897) runs for functions/methods/classes/objects/
traits/enums (NOT hook-created vals/enum members — pinned: @volatile var
→ nothing). Scala annotations are DIRECT CHILDREN of the definition node
(annotation node with fields name: type_identifier, arguments) — scan #1
(:4976-4978) hits them; consider() accepts type annotation (:4928); the
target loop finds the type_identifier name child (:4951) → name text →
<-strip + last-./::-segment normalization (:4959-4962) →
decorates ref {from: decorated node, name, line/col of the ANNOTATION
node}. Pinned: @main def entry → decorates main; @inline def fast →
inline; @deprecated("gone", "1.0") def old → decorates deprecated
(WITH-args annotations still emit — the name field precedes the arguments;
kotlin's constructor_invocation dropout does NOT reproduce). Annotation
ARGUMENT expressions are never visited (no calls refs from them). The
modifiers-descent (:4983) and backward-sibling scan (:5013) are inert for
scala (annotations are neither inside modifiers nor preceding siblings).
Inheritance — extends_clause, the scala branch (:5339-5360)
extractInheritance loops the DEFINITION node's direct namedChildren for
extends_clause (field extend; a direct child on class/object/trait/enum
definitions). The scala branch iterates ALL namedChildren of the
extends_clause and maps each through scalaBaseTypeName → one extends
ref per supertype {name, line/col of the supertype node}. Pinned:
extends BaseW(size) with Drawable with Ordered[WidgetS]→ extendsBaseW+Drawable+Ordered(generic unwrapped; theargumentschild yields null via the default case → skipped).- scala-3 comma form
extends B, C→ both. Singleextends Shapeon objects (object Circle extends Shape) ✓ (pinned). - Curried super-ctor args
extends Base(1)(2)(plain, parse-clean) → extendsBaseonly (the secondargumentschild → null → skipped;extract-super2.txt). derives Show(derives_clause, fieldderive) emits NOTHING — not an extends_clause; preserve the silence.- Scala NEVER emits
implements— traits ride extends. - Enum CASE extends (
case Earth extends Planet(5.9)) — hook-consumed, nothing (§Enums). Anonymousnew T {…}— no class node, no extends (§Instantiation).
Type-annotation references — scala ∈ TYPE_ANNOTATION_LANGUAGES (:5753), THREE live walks + the hook's
extractTypeAnnotations (:5788) for functions/methods:
- scala-only params walk (:5839-5842): EVERY direct
parameterschild (all curried lists — the trailing implicit list included) → extractTypeRefsFromSubtree (:6090): everytype_identifierleaf except BUILTIN_TYPES (:5768-5782 — includes the scala block Int/Long/…/Null PLUS the cross-language entries; note lowercaseerroretc. can't occur) →referencesref at the leaf. The type_parameters node also carries field nameparametersbut is NOT typeparameters→ not matched by this walk (it's matched by walk 3). - return walk (:5851):
getChildByField('return_type')subtree → refs (generic returnOption[A]→Option+A). - scala-only type-parameters walk (:5863-5870): the first
type_parameterschild → subtree refs — context/upper bounds emit ([A: Numeric, B <: BoundT]→Numeric+BoundT; the declared names A/B areidentifiernodes → silent). Pinned order forgenericDef: params-walk refs (A) → return (B) → bounds (Numeric,BoundT). - the
type_annotationdirect-child search (:5873) — no such node kind in scala → dead.
Plus the hook's emitScalaTypeRefs on val/var type ascriptions
(§Extractor config — its OWN builtin set SCALA_BUILTIN_TYPES, which lacks
the cross-language entries; val m: Monoid[Int] → references Monoid).
extractVariableTypeAnnotation (:6074) needs a type_annotation child → dead;
the body-walker variable_declarator branch (:5230) — no such kind → dead.
Net: param/return/bound types on defs + ascribed val/var types emit;
class_parameters types (bodied classes), local val types in bodies (no hook
there, no variable_declarator), and pattern types (case ws: WidgetS) emit
NOTHING. Class primary-ctor types are the notable coverage hole — preserve
it.
Docstrings (tree-sitter-helpers.ts:95-127) — Scaladoc is KEPT
Scala comment node kinds: comment (//) and block_comment (/* */ and
Scaladoc /** */). BOTH are in getPrecedingDocstring's accepted set
(:110-115) — unlike kotlin, Scaladoc survives, and block/line runs CHAIN in
either order. cleanCommentMarkers (:77-90): /*-open strip + per-line *
continuation strip + // strip. Pinned (extract-docs.txt): Scaladoc →
"Scaladoc kept?\nsecond line with star"; // runs joined with \n;
/** block */ + // trailing → "block doc\ntrailing line"; a blank
line does NOT break the chain (// detached two lines above still
attaches — previousNamedSibling skips whitespace); hook-created vals get NO
docstring (docVal pinned) but class/method/function/trait/enum/type_alias
do. DOCSTRING_WRAPPER_TYPES contains no scala kinds → no climbing.
CRLF: multi-line block comments keep a stray \r at each internal line
break ("Scaladoc kept?\rsecond line with star" — pinned
extract-docs-crlf.txt diff) — the JS ^/m-matches-after-\r semantics
(#1329): call the shared js_multiline_strip in docstring.rs, port
nothing. Line-comment runs are CRLF-clean (per-comment trim eats the
\r).
Value-reference edges (:398-931) — scala IS in VALUE_REF_LANGS (:401)
Port the full machinery (crib kotlin.rs): CODEGRAPH_VALUE_REFS=0 kill;
MAX_VALUE_REF_NODES 20,000; isGeneratedFile skip.
- Targets (captureValueRefScope:735): kind constant|variable, name len ≥3
AND
/[A-Z_]/, parent id prefix ∈ {file:, class:, module:, struct:, enum:}. Because scala mints NO namespace node, top-level constants ARE targets (kotlin's namespace-drop quirk does NOT apply —TOP_LIMITpinned with a class-method reader). Object/class/trait members (class: parent) are targets;count(no uppercase/underscore) is not (pinned). Same-name targets: LAST registration wins the map slot — withConfig.TIMEOUT_MSandConfig2.TIMEOUT_MSin one file, readers of EITHER get an edge to Config2's node (fileScopeValues.set overwrite — pinnedvref2.out; wrong-target quirk, PRESERVE). - Reader scopes: every function/method/constant/variable node (:764) — fields are NOT readers.
- Shadow prune (:803-878): the scala declarator case is
val_definition/var_definition(:838-843) —patternfield, bump ONLY when the pattern IS anidentifier(tuple/case-class patterns bump nothing → a destructured local shadow never prunes). bump() countsidentifiernodes (:807). Every val/var ANYWHERE (the target's own declarator + body locals — the hook never sees body locals but the prune DFS does) — pinned: companion-ishConfig.RETRY_MAX+ method-localval RETRY_MAX = 9→ RETRY_MAX pruned (readBoth emits only TIMEOUT_MS). The kotlin/swiftproperty_declarationcase and Dart/Pascal cases are null paths here. - Emission (:880-930): per reader scope, stack-based DFS
(reverse-source-order pop — ruby precedent; edge order follows), reader
node type
identifier(:907 —simple_identifier/constant/namenever occur in scala trees). Reads throughConfig.TIMEOUT_MSmember positions count (the member half is anidentifier). String interpolations: BOTH$CONSTand${CONST}count (interpolation > identifier / > block > identifier — pinnedreadInterp×2; kotlin's$X-inert quirk does NOT apply). Skip self/same-name, dedupe per (scope,target) → EDGE {kind:'references', metadata:{valueRef:true}}, appended after the walk (§Emission order). The Dart/Pascal next-sibling pull (:891) is inert (scala bodies nest).
Function-as-value capture (#756) — SCALA_SPEC (function-ref.ts:300-308)
idTypes = {identifier} (bare identifiers ARE candidates — unlike
kotlin). dispatch: arguments → args; assignment_expression → rhs field
right; val_definition → varinit field value (var_definition is
ABSENT — var cb = other initializers are NEVER captured, pinned
extract-fnref.txt). unwrap: postfix_expression → null field → first
namedChild (eta-expansion handler _ → handler, explicitRef=true).
No layers/special/ungatedModes/addressOfOnly. NAME_STOPLIST applies
(this, null, …).
- Capture points: ladder :990 (top-level/template statements),
visitFunctionBody :5137 (body nodes), scanFnRefSubtree (hook-consumed
val/enum-case/extension subtrees — the scan visits the val_definition
itself at depth 0, so class/object/top-level
val x = fnIS captured; the nested-def halt checks functionTypes=[] so onlylambda_expressionhalts it —val fnField = (x) => runLam(x)captures nothing, pinned). - Pinned channels (
extract-fnref.txt,extract-fnref2.txt,extract-edge2.txt): args bare idregister(handler)✓; args etaregisterEta(other _)✓; body varinitval stored = alpha✓; class-scope varinitval topStored = handler✓ (from the CLASS node); assignment rhsobj.cb = beta✓; named-argumentwire(cb = cbTarget)— parses as an assignment_expression inside arguments → rhs capture ✓ (at the rhs id's position) with the param-forward skip whencb = cb(lhs tail == rhs text, :430-443); listList(delta)inside a hook-consumed val ✓ (args mode viaarguments). - NOT captured:
fn.membervalues (field_expression — no special), infix positions (list map transform— no dispatch key), var initializers, lambda bodies under hook-consumed vals. - Flush gate (:639-728): definedHere (same-file function/method names — a
top-level def IS kind function, so
register(missing)drops but same-file names pass) ∪ importedNames — which for scala is first path segments only (§Imports; effectively: cross-file callables never pass the gate). No::/this.always-flush forms are ever produced. Dedupe${fromNodeId}|${name}(pinned: three same-scopehandlercaptures → one ref). referenceKindfunction_ref(wire code 200).
visitFunctionBody (:5129-5286) — scala rows
- maybeCaptureFnRefs (:5137) per node (assignment/varinit/args captures in bodies).
call_expression→ extractCall (:5143), children recursed after.instance_expression→ :5145 extractInstantiation; findAnonymousClassBody → null (template_body) → NO return → children recursed → anon-body defs are NOT dispatched here (functionTypes empty, methodTypes not checked in this walker) →new Ordering[Int] { def compare … }in a body: the instantiates ref only;comparemints NOTHING and its body's calls attribute to the enclosing method (pinned).- extractBareCall — absent.
- Nested named defs mint NOTHING (:5245 checks functionTypes — EMPTY):
def innerFn(k) = …inside a body is recursed-through; its calls attribute to the ENCLOSING method (pinned:innerFn/lam/helperCallall fromCallSites::localHost). This is the inverse of kotlin (which minted local fns) — preserve. - classTypes (:5255): body-local
class LocalClass/object LocalObj/trait → FULL extractClass (kind class/trait) contained by the enclosing method, members extract normally (pinned QNsCallSites::localHost::LocalClass::lm). enumTypes (:5268) likewise. typeAliasTypes NOT checked → body-localtype X = …is invisible. - extractStaticMemberRef (:5218) every node (§Static-member).
- variable_declarator branch (:5230) dead. Body-local val/var: no hook here →
plain recursion → initializer calls/instantiations EMIT from the
enclosing function (
val local = compute()→ calls compute — pinned) and no local nodes are minted. Match/for/try/if recursed transparently (case_clause patternSome(n)emits nothing;for … yield combine(x,y)emits the call — pinned).
Misc shared paths
- Positions: line = startRow+1, column = startColumn — UTF-16 code units
(pinned
extract-uni.txt:😀counts 2,écounts 1), as are startIndex/endIndex substrings (getNodeText) and the import-signature trim. - Refs carry NO filePath/language (§arch-5).
extract()wrap (:454-577): file node → (no namespace) → walk →flushFnRefCandidates→flushValueRefs→ pop. Table order: nodes in creation order; contains edges interleaved; value-ref EDGES appended LAST (pinned — after all contains); walk-order refs, then function_ref refs at flush (pinned last in every dump). Store/harness are rowid-order-sensitive — reproduce exactly.- CRLF hazards inventory for the scala path: scala.ts has ONE regex over
node text (extractScalaReturnType's strips —
\s+handles\r, JS/Rust parity holds) and the visibility/isStatic.includes(single-token). The shared regexes that fire: extractCall's parenthesized-conversion (:4530), decorator name normalization (:4959-4962), and cleanCommentMarkers' gm strips →js_multiline_stripin docstring.rs (#1329), call it (the block-comment\rretention is the observable — §Docstrings). Grammar CRLF probed clean (§Grammar prep).extract-torture.txtvsextract-torture-crlf.txtdiff is EMPTY (torture's only multi-line comment is//-run based… its scaladoc is single-line); the docs fixture carries the multi-line pin. - Defer policy: per-file
has_error()→defer:— including phantoms (§Grammar prep (a): flag-true with zero ERROR nodes is COMMON in scala-3 code; trust the flag). wasm recovery is canonical. - MAX_FILE_SIZE (1 MiB) / generated-file skips: shared, nothing scala-specific.
Frameworks & synthesis consumers (stay TS-side — pin the walker's output contract)
- playResolver (resolution/frameworks/play.ts) — §Architecture #2 for
detection/decoded-path. Its extract() only fires on
conf/routes/*.routesfiles (NOT .scala — they ride the no-grammar path with language 'yaml'); route nodes carry literal ids (route:${filePath}:${line}:${method}:${routePath}) andlanguage: 'scala'; resolve() mapsController.methodhandler refs viagetNodesByName(className)(kind class) +getNodesInFile(cls.filePath)(kind method|function, exact name). Walker obligations: class/method node names, kinds, filePath — nothing else. - matchDottedCallChain (name-matcher.ts:2145-2166, scala gated) —
consumes the
Foo.create().barre-encode + the inner method's returnType (extractScalaReturnType's product — whyUnitis left unfiltered doesn't matter here, but the last-segment/generic-strip rules do). CHAIN_LANGUAGES (resolution/index.ts:42) includes scala (the().split). - CONSTRUCTS_VIA_BARE_CALL (name-matcher.ts:918) —
Foo(args).method()receivers resolve as class Foo (companion-apply convention). - Local-variable receiver inference (name-matcher.ts:1197-1201) —
re-reads SOURCE inside the enclosing node's span with
`\bval lg = (new )?Logger`/`\blg: Logger`regexes. Walker obligations: accurate node startLine/endLine spans + language tag. - JVM name-family (name-matcher.ts:141: scala → 'jvm') — scala refs can bind to java/kotlin nodes and vice versa; nothing extra to emit.
- NO synthesis passes are scala-gated: callback-synthesizer has zero scala mentions (CC_LANGUAGES = swift/kotlin; expect/actual kotlin-only; rnCrossPlatform NATIVE set has no scala). Generic passes (interface-impl Phase 5.5 over extends refs + method names, function_ref resolution) consume the standard tables.
Parity mechanics (all have bitten before)
- Emission order per §Misc: file → source-order walk (per construct: node + contains → extends refs BEFORE member emissions → extractor-order refs) → function_ref refs → value-ref EDGES last.
- generateNodeId inputs: (filePath, kind, name, startRow+1) — name keeps
backticks (
`Weird Name`) and operator glyphs (+,::); import nodes are named the FIRST path segment; enum_member line/column = the CASE node's (which for simple cases equals the identifier); companion pairs collide on (kind,name) but differ on line.node_idsdedupe-vec pattern still required — same-(kind,name,line) CAN collide (one-lineclass X { … }; object Xconstructions, secondary-ctorthistwins). - First-match-wins field lookups over named+anonymous children — import
path, defparameters, extension/forbody/enumerators(§config). Implementchild_by_fieldexactly once, tree-sitter-faithful. - UTF-16 columns/slices (textutil::col16/slice_utf16) — pinned via
uni.scala(surrogate-pair emoji counts 2). - CRLF: variants of every fixture derived in-memory (kernel-tsjs-parity
pattern); the docs fixture MUST include a multi-line
/** … */(the\rretention pin) and a Scala-3 indented file (scanner CRLF). - Defer policy:
has_error()→defer:including phantoms; sweep defaults--max-deferral 0.1for os-lib/cats-style repos, 0.3 for scala3-style (§arch-6).
Gates (per plan §5 — NOTE: no grammar-bump gate, §Grammar prep)
- Kernel C vendor + build.rs + langs.rs + grammar-parity row land first
(inert until the walker exists):
GRAMMAR_LANGUAGES += 'scala'(kernel-grammar-parity.test.ts:39) must show ABI 15 / 26650 states / 32 fields / identical kind+field tables vs the production wasm. - Torture fixtures per
## Fixtures to build, exercised by a new__tests__/kernel-scala-parity.test.ts. - Parity sweeps (
scripts/kernel-parity.mjs <dir>, order-sensitive full-object):…/scratchpad/svy-scala/gate-repos/os-lib(small, 59 files, expect 0 deferrals)…/gate-repos/cats(medium, 835 files, expect ~15 deferrals ≈1.8%)…/gate-repos/scala3— sweepcompiler/src(577 files, ~57 deferrals ≈10%,--max-deferral 0.3) andlibrary/src(652 files, ~116 ≈18%, the phantom-heavy set) rather than the whole repo (tests/ is deliberately-invalid input — fine for a robustness pass, useless as a parity denominator). (cloned fresh at survey; re-clone public OSS if gone — agent-eval policy.) Expect 0-diff on every NON-deferred file. Then full-init dump-diffs byte-identical (kernel arm vsCODEGRAPH_KERNEL=0,dump-graph.mjs, cmp) on all three.
- Suite: kernel-scala-parity torture + CRLF variants +
.scfixture + defer fixtures (§Fixtures 5-6) ×2 green withCODEGRAPH_KERNEL_EXPECT=1. DEFAULT_ROUTED += 'scala'(kernel/index.ts:37 list) only after ALL of the above; changelog rides the existing kernel entry.- Post-route perf sanity: gate repos ride the raw path; a Play-detected repo (e.g. playframework/playframework itself) is the decoded-path smoke check.
Fixtures to build
__tests__/fixtures/kernel-parity/torture.scala— seed fromsvy-scala/torture.scala(itsextract-torture.txtis the expected-output pin: 80 nodes / 82 edges / 75 refs). Inventory: package header (ignored, no namespace); imports ×5 (first-segment names; selectors/wildcards/renames); scaladoc +//-run docstrings; top-level defs (function kind) incl. curried (first-list sig), generic (type-params-first sig + bound refs), inferred/Unit/qualified/generic-leak returns, private,@main/@deprecated(args)decorates; top-level val/var/lazy/tuple/pattern destructuring (first-identifier names) + ascribed types (refs) + initializer-invisibility; bodied class with primary ctor (params invisible), fields (val/var/private/annotated), secondary ctor (thismethod +thiscall), methods incl. operator+, member calls, extends with-chain; companion object (second class:WidgetS + shared QN space + DEFAULT_SIZE value-ref target read by both create/readDefault); case class bodiless-header asymmetry (mkY()call from class) vs bodied twin; case object extends; abstract class + function_declaration; trait with default method + val field; sealed trait + object/class extends; enum (simple/param/extends cases at case-node positions, invisible tails, method); enum with class_parameters; given → instantiates + leaked function; anonymous given; extension (first-def call leak, second-def invisibility) + single-def extension; implicit class; type aliases (no value refs) + opaque; call shapes: bare, apply-sugarWidgetS(1), member, chained cap re-encode + inner, lower chain, 2-hop, this/super bare, literal receivers (nothing),genericCall[Int]bracket callee, curried-call raw inner,newin expression body, anonnew … {}in body (instantiates + leaked-nothing), interpolation call +$id, eta + bare + named-arg fn-refs, infix invisibility, match/for; static reads (Registry.count,Http.Ok,com.example.Fq.Xnothing, write LHS EMITS, callee skip); local defs (innerFn invisible, LocalClass/LocalObj full); package object (flattened); UTF-16 line. Keep it parse-clean (avoidgiven … with {},endidentifiers, curried+generic super-args).- CRLF variants in-memory (kernel-tsjs-parity pattern) — plus the docs
fixture's multi-line scaladoc (the
\rpin) and an indented Scala-3 fixture (external-scanner CRLF —indent.scalaseed, extraction byte-identical pinned). .scfixture — top-level statements → calls from FILE; file-parent value-ref target (script.scseed).- given/extension/anon-class fixture —
given2.scala+ext-probe.scalaseeds (the leak asymmetries are the port's likeliest regression site). - Defer fixture #1: PHANTOM —
def f(x: List[Int]^): Int = 1— kernel defers on has_error() despite a complete, ERROR-free CST; wasm output byte-normal. - Defer fixture #2: real error — a
given x: C with { … }orend-identifier file — kernel defers; wasm-served output pinned. - vref fixture —
vref.scalaseed: object + top-level targets, shadow prune,$X/${X}interpolation reads, lowercase non-target, the same-name last-wins mis-target (Config/Config2 TIMEOUT_MS → Config2's node). - fnref fixture —
fnref.scala/fnref2.scalaseeds: all five capture channels + var-init non-capture + dedupe +register(missing)gate drop.
Probe artifacts (session scratchpad svy-scala/)
cst-probe.cjs + snippets.json + cst-snippets.txt (20 snippet CST dumps
WITH per-node childForFieldName truth tables), brace-field.out (the
anonymous-token field proof: extension {-body, for enumerators),
extract-probe.cjs (runs the REAL dist extractor) + ground-truth dumps
extract-{torture,torture-crlf,docs,docs-crlf,vref,fnref,fnref2,misc,indent, indent-crlf,script,given2,edge2,ext,super2,imp2,uni}.txt (pinned outputs
cited throughout; double as walker test expectations), vref2.out
(value-ref target identity), crlf-cst.cjs (LF/CRLF s-exp equality ×7),
error-sweep.cjs + errors-{os-lib,cats,scala3,scala3-compiler, scala3-library,scala3-tests}.txt, err-sample.cjs + err-samples.out
(error-class contexts), errvariants.out (17 minimal shapes),
phantom-scan.cjs + phantom-scan-{lib,cats}.out + phantom-min.out (the
^-postfix one-liner), fixtures (torture.scala, docs.scala,
vref.scala, fnref.scala, fnref2.scala, misc.scala, given2.scala,
edge2.scala, ext-probe.scala, indent.scala, uni.scala,
script.sc, …), tsscala-clone/ (the 0aca5d0a6f checkout) +
grammar-shas.txt, gate-repos/{os-lib,cats,scala3}. Scratch is
throwaway — re-derive from this doc if gone.