Status: PORT COMPLETE (2026-07-20) — walker codegraph-kernel/src/kotlin.rs
.kt/.kts file exercises, with file:line anchors as of
a6c62d7 (HEAD at survey time, clean main). Every grammar-shape claim below
was probed against both the production tree-sitter-wasms build and a fresh
fwcd 0.3.8 tag build (probe scripts + dumps in the session scratchpad
svy-kotlin/ — see §Probe artifacts), and every extraction-behavior claim was
pinned against the real dist/ extractor (extract-*.txt ground-truth
dumps), not derived from code reading alone. Read WITH
docs/design/rust-kernel-migration-plan.md (§0a recipe, §2 boundary, §4
tracker row "kotlin", §5 gates) and the format precedents
(rust-lang-kernel-port-checklist.md, ruby-kernel-port-checklist.md,
php-kernel-port-checklist.md, csharp-kernel-port-checklist.md).Blocking findings: none — but two eyes-open items, one of them a NOVEL
mechanism. (1) The grammar bump is behavior-neutral (rust-style gate:
byte-identical CSTs on all 1,984 gate-repo files, 0 error disagreements) —
but the crates.io crate tree-sitter-kotlin = 0.3.8 is UNUSABLE by the
kernel (it pins tree-sitter >= 0.21, < 0.23; the kernel links 0.25), and
the successor crate tree-sitter-kotlin-ng is a different grammar (8
fields vs 0, 289 vs 357 symbols, renamed kinds — would break every kotlin.ts
branch). The port must take the vendored-grammar-C route: compile the
sha-matched 0.3.8 parser.c+scanner.c inside codegraph-kernel via
build.rs — the FIRST language to exercise the mechanism the §4 tracker
prescribes for vendored grammars (§Grammar prep). (2) Both-arm parse-error
incidence is 4.7–8.5% on the gate repos (fun-interface misparses, phantom
single-line-class-body errors, soft-keyword identifiers, call().prop = x
LHS shapes — all grammar-inherent, all identical across arms). The default
--max-deferral 0.1 HOLDS but with only ~1.2–2× headroom (okhttp 8.45%) —
expect double-digit deferral COUNTS on kotlin sweeps and don't misread them
as walker bugs (§Architecture decisions #6).
kotlin is NOT in VENDORED_WASM_LANGS (grammars.ts:291-317) — production
loads node_modules/tree-sitter-wasms/out/tree-sitter-kotlin.wasm
(mapping kotlin: 'tree-sitter-kotlin.wasm' at grammars.ts:35;
tree-sitter-wasms 0.1.13 builds it from npm tree-sitter-kotlin ^0.3.1,
the fwcd lineage; production wasm sha256 b5cb00c8…, 4,052,705 bytes, ABI 14).
tree-sitter-kotlin 0.3.8 (crates.io max_stable, published
2024-08-03; repo fwcd/tree-sitter-kotlin — OUR wasm's lineage; repo still
active but no crate release since). Tag 0.3.8 (annotated tag 9e7e624 →
commit e1a2d5ad1f61f5740677183cd4125bb071cd2f30); sha256-verified
crate-tarball ↔ tag, BOTH generated artifacts (kotlin HAS an external
scanner):src/parser.c 54104a7ef1555c265b746c790e0f8bb953cc17806e9df0c3af82f7f62c06a70asrc/scanner.c 27f73337ec357fc341fa57538f34c14277b0346980c3405dc30beab6202ec6d0tree-sitter-kotlin-ng 1.1.0 (tree-sitter-grammars org, 2025-01) —
REJECTED: a different grammar, not a re-publish (STATE_COUNT 11432 vs
10155, SYMBOL_COUNT 289 vs 357, FIELD_COUNT 8 vs 0, kinds renamed —
additive_expression→binary_expression, call_suffix gone,
binding_pattern_kind gone…). Adopting it is an extractor REWRITE, not a
port. Do not revisit until/unless the TS side migrates grammars.table-compare.cjs); the 258-line torture file's full CST dump
is byte-identical OLD↔NEW; and the gate-repo sweep (error-sweep.cjs
<repo> --sexp) found 0 error disagreements and 0 s-expression mismatches
on every clean file across all three repos. Expect the standalone bump
gate's old-vs-new full-init dump diff to be byte-identical on all three
(rust-style "expect zero", NOT php's enumerate+classify).LANGUAGE_VERSION 14 — content parity, ABI stays 14 (ruby precedent).
kernel-grammar-parity must assert same-revision, not an ABI change.childForFieldName/getChildByField lookup in the kotlin path returns
null, which is what makes several TS hooks dead code (§Extractor config).
The walker must reproduce the null-field world exactly — do NOT "helpfully"
use -ng-style fields that don't exist here.Wasm build (from the tag's CHECKED-IN parser.c — never tree-sitter
generate):
git clone --depth 1 --branch 0.3.8 https://github.com/fwcd/tree-sitter-kotlin
cd tree-sitter-kotlin
# the 0.3.8 tag predates tree-sitter.json, which cli 0.25.10 requires —
# add the METADATA-ONLY shim (grammar name/scope; nothing regenerated):
# {"grammars":[{"name":"kotlin","scope":"source.kotlin","path":".",
# "file-types":["kt","kts"]}],"metadata":{"version":"0.3.8","license":"MIT"}}
npx -y tree-sitter-cli@0.25.10 build --wasm -o tree-sitter-kotlin.wasm .
(brew emcc present; survey artifact sha256
c80c88867a589a1a0959bcea89de84b7e9684b3693b2cdb2944812458e62ff48,
4,052,313 bytes, at scratchpad svy-kotlin/tree-sitter-kotlin-NEW.wasm.
Do NOT use tree-sitter-cli 0.24 — it drops \p{...} classes, the #1164
vbnet lesson, and this grammar's identifiers use them.)
Kernel side — the NOVEL part (crate pin impossible): the 0.3.8 crate's
[dependencies.tree-sitter] version = ">= 0.21, < 0.23" + old-style
pub fn language() -> tree_sitter::Language bindings cannot link against
the kernel's tree-sitter = "0.25". Instead of a crate dep, vendor the
grammar C into the kernel (the §4 tracker's prescription for
vendored-grammar languages — kotlin is the first to need it):
src/parser.c, src/scanner.c, and src/tree_sitter/*.h
to codegraph-kernel/grammars/kotlin/ (shas above, recorded in a comment);codegraph-kernel/build.rs: cc::Build compiling both C files with the
crate's own flag set (-Wno-unused-parameter,
-Wno-unused-but-set-variable, -Wno-trigraphs; msvc -utf-8 — crib
the tarball's bindings/rust/build.rs);tree-sitter-language = "0.1" (the version-agnostic
LanguageFn shim every modern grammar crate uses) + cc as a
build-dependency (if not already present);langs.rs:
extern "C" { fn tree_sitter_kotlin() -> *const (); }
// …
"kotlin" => Some(unsafe { tree_sitter_language::LanguageFn::from_raw(tree_sitter_kotlin) }.into()),
plus LANGUAGES += "kotlin" (14 entries).
__tests__/kernel-grammar-parity.test.ts:39 GRAMMAR_LANGUAGES += 'kotlin'
— the id-by-id ABI/kind/field-table compare against the vendored wasm is
the proof the C build and the wasm build are the same revision.
Staging plan (bump PR, before any walker exists): vendor the wasm to
src/extraction/wasm/tree-sitter-kotlin.wasm; VENDORED_WASM_LANGS +=
'kotlin' (grammars.ts:291) with an R7b comment (tag + sha-matched note +
"crate unusable — kernel compiles vendored C, see codegraph-kernel/grammars/
kotlin"); the kernel C vendor + build.rs + langs.rs + grammar-parity row can
land WITH the bump (they're inert until a walker exists) or with the walker —
but wasm + C must be same-tag from day one. copy-assets already globs
src/extraction/wasm/*.wasm. MIT license (fwcd), same family as the rest.
Error incidence (both arms, all .kt/.kts ≤1MiB, error-sweep.cjs):
| Repo | files | OLD hasError | NEW hasError | disagreements | sexp mismatches (clean files) | |---|---|---|---|---|---| | okio | 322 | 23 (7.14%) | 23 (7.14%) | 0 | 0 | | okhttp | 580 | 49 (8.45%) | 49 (8.45%) | 0 | 0 | | kotlinx.coroutines | 1,082 | 51 (4.71%) | 51 (4.71%) | 0 | 0 |
Error classes (sampled + probed): (a) fun interface — unsupported by
the grammar, ALWAYS errors (okhttp 10/49, kotlinx 6/51; okhttp3's core
Call.kt/Authenticator.kt/Dns.kt are in this class); (b) phantom
single-line class bodies — class X { fun f() {} } sets hasError=true
with ZERO ERROR/missing nodes and a COMPLETE, correct CST (probed both
arms; okio 1, okhttp 7, kotlinx 6); (c) soft-keyword identifiers
(var final = false; final = true errors — final is reserved by the
grammar); (d) call("x").prop = value navigation-off-call assignment
LHS (errors; plain obj.prop = x is fine); (e) assorted expect-header
and gradle-kts DSL shapes. class Foo private constructor(x) and
@Inject constructor parse CLEAN (probed — don't blame ctor visibility).
All classes error on BOTH arms → defer-to-wasm keeps parity; only the
speedup is lost on those files.
…/scratchpad/
svy-kotlin/): table-compare.cjs, shape-probe-kotlin.cjs +
torture-{OLD,NEW}.txt (byte-identical), mini-probes.cjs +
mini-probes.out (17 targeted shapes, all OLD==NEW), error-sweep.cjs +
errors-<repo>.txt, extract-probe.cjs (runs the REAL dist extractor —
its extract-{torture,vref,vref-nopkg,docs,bodiless,crlf,funiface,lfpkg,kts}.txt
dumps are the pinned ground truth cited throughout and double as walker
test expectations), torture.kt + the small fixtures, the 0.3.8 tag clone +
crate tarball + ng tarball with matching shas. Scratch is throwaway —
re-derive from this doc if gone.kotlinExtractor has no preParse hook (languages/
kotlin.ts — whole file, no such key), so preParsedSource
(kernel/index.ts:96) is a no-op — both arms parse raw bytes. Nothing to
hoist. No POST_PASSES entry either (kernel/index.ts:81) →
tryKernelExtractRaw stays eligible.extract() onto the
decoded extractFromSource path. Kotlin appears in:
springResolver (frameworks/java.ts:13, languages: ['java','kotlin',
'yaml','properties']; extract() at :197 regexes @GetMapping etc. over
raw .kt source) — detect (:23) = pom.xml/build.gradle/build.gradle.kts
containing spring-boot/springframework, or Spring annotations in any
.java file. okio/okhttp/kotlinx.coroutines: none match (grepped).expoModulesResolver (frameworks/expo-modules.ts:154, languages:
['swift','kotlin']) — detect = package.json expo-modules-core or an
Expo Module DSL source scan. Not present.fabricViewResolver (frameworks/fabric.ts:366, kotlin in languages) —
detect needs codegenNativeComponent. Not present.
So all three parity repos exercise the raw buffers-to-store transport;
a Spring-Boot Kotlin app or an Expo/RN app is the decoded-path smoke check.codegraph-kernel/src/kotlin.rs; no crate
collision since there is no kotlin crate dep), registered in langs.rs;
per-file has_error() → defer: like every walker. java.rs is the
closest crib (JVM package→namespace node via extractFilePackage,
class-like scope stack, methods-in-class-like, dotted imports,
STATIC_MEMBER + TYPE_ANNOTATION + VALUE_REF membership, annotation
decorators, node_ids vec). Kotlin diverges from it in nine places, each
detailed below: (a) a visitNode hook whose PROPERTY branch is the only
live-in-kernel part (fun-interface recovery is defer-shielded); (b)
getReceiverType — extension functions → receiver-qualified method QNs +
the owner-contains fallback (NO ported walker has this surface yet); (c)
extractModifiers — expect/actual → node DECORATORS (also a first); (d)
resolveBody by TYPE (zero-field grammar); (e) extraClassNodeTypes
(object_declaration); (f) classifyClassNode keyword sniffing
(interface/enum reuse class_declaration); (g) the #750 kotlin re-encode
in extractCall (namedChild(0), NOT a function field); (h) a fn-ref spec
with EMPTY idTypes + callable_reference/navigation_expression
specials; (i) dead-field lookups everywhere (signatures, type
annotations) that must stay dead..kt/.kts → kotlin at detectLanguage (grammars.ts:106-107), no
content sniffing, no dialect. .kts scripts are ordinary kotlin files
whose top-level statements attribute calls to the FILE node (pinned:
extract-kts.txt — calls println from=file, top-level val →
constant). MAX_FILE_SIZE (1 MiB, extraction/index.ts:132) and
generated-file skips are orchestrator/TS-side and shared.--max-deferral 0.1 (it holds on all
three) but EXPECT these counts; a kotlin sweep at ~8% deferral is normal,
one at >10% means a walker bug. No c/cpp 0.5 exemption.filePath (the visitNode hook only
creates nodes; verified across every ground-truth dump — zero refs printed
a filePath). The ruby/php trait-mixin bit stays unused here.Types: functionTypes=[function_declaration]; classTypes=class_declaration; methodTypes=
function_declaration; interfaceTypes=[] ; structTypes=[]; enumTypes=[];
enumMemberTypes=[enum_entry]; typeAliasTypes=[type_alias];
importTypes=[import_header]; callTypes=[call_expression];
variableTypes=[property_declaration]; fieldTypes=property_declaration;
extraClassNodeTypes=[object_declaration]. nameField=simple_identifier,
bodyField=function_body, paramsField=function_value_parameters,
returnField=type.
FIELD_COUNT 0 consequences (the dead-field cluster — reproduce the deadness):
nameField/bodyField/paramsField/returnField are node-TYPE names used
as FIELD names — every getChildByField on them returns null. Names resolve
via extractName's FALLBACK (first direct namedChild of type
identifier|type_identifier|simple_identifier|constant,
tree-sitter.ts:178-189); bodies resolve via the resolveBody hook (by
type); params/return field walks are DEAD (§Type-annotation refs).getChildByField(node, 'function_value_parameters') → null → early return.
Ground truth: every function/method in extract-torture.txt has
sig=undefined. The walker must emit NO signature for functions/methods.typeNode = node.childForFieldName('type')
(kotlin.ts:125) is DEAD → property signature is always undefined too
(val topVal: Int = 3 → sig undefined — pinned).Hooks PRESENT (port each exactly):
property_declaration (:98-131) — the LIVE branch. varDecl = first
namedChild of type variable_declaration; nameNode = ITS first
simple_identifier; either missing (destructuring's
multi_variable_declaration) → return false (fall to the ladder). Then
the SCOPE WALK up the parent chain, first match wins:
function_body|function_declaration|lambda_literal|
anonymous_initializer|control_structure_body|getter|setter → 'local' →
return true, extract nothing (this is how init-block/getter-body/
top-level-control-flow locals reached via visitNode recursion are
skipped); companion_object|object_declaration → 'const';
class_declaration → 'instance'; nothing matches (top level) → 'const'
(the initial value). Kind: instance → field; else val (a
binding_pattern_kind child with text exactly val) → constant,
var → variable (const val is just a val; a delegated
by lazy {} property has no = but still a binding → same rule).
ctx.createNode(kind, name, node, { signature: undefined }) — extra
carries ONLY the (always-undefined) signature: no docstring, no
visibility, no isStatic, no returnType on kotlin property nodes —
but createNode's extractModifiers merge still runs, so expect val /
actual val DO get decorators. Return true → the dispatcher runs
scanFnRefSubtree(node, 0) (capture-only, halts at nested
function/lambda types) and NEVER descends → property initializers
emit NO calls/instantiates refs anywhere (val SHARED = WidgetK(0)
→ nothing; by lazy { compute() } → nothing, the scan halts at the
lambda_literal). Consequences pinned in extract-torture.txt.lambda_literal after a fun-interface ERROR (:139-143) andisFunInterfaceNode :46; Pattern 1 walks
the sibling lambda's statements with a synthesized interface node
pushed — ground truth extract-funiface.txt: interface node at the
ERROR's extent + transform as its method) — both branches are
DEFER-SHIELDED in the kernel: every fun interface (either pattern,
probed) makes the tree hasError=true, so the kernel defers the whole
FILE to wasm before the walker would run. Do NOT port branches 2-3.
Walker rule: port branch 1 only; a defer: on has_error covers the
rest. (The parity suite still needs a fun-interface fixture asserting
the kernel defers and the wasm arm serves the pinned output.)ERROR child whose child(0) is { (the fun-interface parent-body
case — unreachable on non-erroring files, keep for wasm-parity of the TS
side only), else first function_body | class_body | enum_class_body.
Used by extractFunction/Method/Class/Enum body resolution AND by
createNode's endLine extension (tree-sitter.ts:1329-1333, function/method
kinds only). Single-expression bodies (fun f() = expr) are a
function_body starting with = — resolved and walked like any body.interface → 'interface'; enum → 'enum'; else
'class'. annotation class / data class / sealed class → 'class'
(their class_modifier children don't match); sealed interface →
'interface'.user_type; on a
. (anon) child WITH a remembered user_type → return that user_type's
FIRST type_identifier child's text (else the whole user_type text); on
simple_identifier or function_value_parameters → break (past the name;
no receiver). Probed shapes:
fun WidgetK.extend() → WidgetK.fun <T> List<T>.genericExt() → List (type_parameters is skipped;
generic args live in a type_arguments child of the user_type, and the
FIRST type_identifier is the base).fun com.example.Qualified.qext() → com — a qualified receiver's
user_type holds MULTIPLE type_identifiers (com,example,Qualified)
and the find takes the FIRST segment. QN becomes com::qext. BUG,
PRESERVE.infix fun Int.pow() → Int; operator fun WidgetK.plus() → WidgetK
(modifiers don't disturb the walk).modifiers:
TEXT .includes('public'|'private'|'protected'|'internal') in that order;
no modifiers/no match → 'public'. QUIRKS, PRESERVE: (a) kotlin emits a
visibility value no other language does — 'internal'; (b) the probe
file's private internal fun (invalid kotlin, parses fine) → 'private'
(order); (c) TEXT-includes false positives — annotations live inside
modifiers, so @publicize-style lowercase annotation text containing a
keyword flips visibility (e.g. a lone @internalApi annotation → 'internal'
instead of 'public'). Match the includes-on-raw-text semantics exactly.isStatic: false.modifiers child whose TEXT
.includes('suspend') → true, else false. The real shape is
modifiers > function_modifier > suspend. TEXT-includes false positive,
PRESERVE (probed, mini-probes.out suspendFalsePos): @suspendMarker fun
g() → isAsync true (the annotation text contains lowercase 'suspend').modifiers → their platform_modifier children → their
children of NODE TYPE expect/actual (anon keyword nodes; matched by
type, not text) → collect in order; empty → undefined. Runs inside
createNode (tree-sitter.ts:1355-1358) for EVERY node kind — merged
newNode.decorators = [...(existing ?? []), ...mods]. Ground truth:
expect fun/expect class → dec=["expect"]; actual fun/class/val →
["actual"]; actual typealias PlatformClock → type_alias node with
dec=["actual"] (the synthesizer's KMP_TYPE_KINDS depends on this);
members of an expect class are NOT marked (no platform_modifier of their
own) but an actual fun inside an actual class IS. decorators on
kotlin nodes come ONLY from this hook — the annotation channel is
decorates REFS, never the node list (§Decorators).source.substring(node.startIndex, node.endIndex) (UTF-16); moduleName =
the first namedChild of type identifier's substring (the dotted path).
No identifier → null (doesn't occur; even import a.b.* has the
identifier). No handledRefs → the generic imports ref also fires
(§Imports).package_header] + extractPackage (kotlin.ts:347-352) —
first namedChild of type identifier → trimmed substring
(com.example.torture); none → null. §Namespace capture.Hooks ABSENT (the walker must NOT do these): preParse, resolveName,
recoverMangledName, isMisparsedFunction, isConst, isExported
(undefined on every node except the file node's literal false),
classifyMethodNode, extractPropertyName, propertyTypes,
interfaceKind (→ kind interface), extractBareCall, synthesizeMembers,
skipBodilessClass (a bodiless class Foo still mints a node — the
1685 comment names Kotlin as the deliberate case), methodsAreTopLevel,
resolveTypeAliasKind.
a6c62d7)| Node | Branch | Behavior |
|---|---|---|
| every node | visitNode hook first (943) | property_declarations (non-destructuring) consumed there; handled → scanFnRefSubtree + STOP |
| every node | maybeCaptureFnRefs (990) | fires for value_arguments/assignment (the KOTLIN_SPEC keys) in visitNode context too — how top-level/class-scope callable refs in call args are captured |
function_declaration |
functionTypes:994 | inside class-like AND ∈ methodTypes → extractMethod:1737; else extractFunction:1517 (which itself diverts to extractMethod when getReceiverType fires — extension fns at any scope). skipChildren |
class_declaration |
classTypes:1005 → classify | 'interface' → extractInterface:1834; 'enum' → extractEnum:1914; else extractClass:1679 |
object_declaration |
extraClassNodeTypes:1022 | extractClass(node) → kind class (objects and sealed-class object members are class nodes; extractInheritance runs → their delegation_specifiers emit extends) |
companion_object |
no branch | recursed → its class_body children visited with the OUTER CLASS still on top: properties → hook ('const' scope → constant/variable under the class, class: parent ⇒ value-ref targets), functions → extractMethod of the outer class. A NAMED companion (companion object Named) is identical — the name mints nothing |
property_declaration (hook-declined = destructuring) |
fieldTypes:1084 (in class-like) else variableTypes:1098 | extractField / extractVariable — both emit NOTHING for kotlin destructuring (no variable_declarator/variable_declaration/identifier direct children; extractVariable's generic fallback :2863-2881 finds no identifier-typed child — kotlin names are simple_identifier). skipChildren + scanFnRefSubtree → the RHS call is invisible too. isClassScopeConstantAssignment (1508) needs node.type assignment → always false for kotlin |
type_alias |
typeAliasTypes:1071 → extractTypeAlias:2890 | plain type_alias node (no resolveTypeAliasKind). QUIRK: the alias-value ref walk reads getChildByField(node,'value') → null (no fields) → NO reference to the aliased type; returns false → the alias's children ARE re-visited (harmless — user_type/modifiers match nothing) |
import_header |
importTypes:1209 → extractImport:3170 | §Imports (the import_list wrapper has no branch and recurses into each header) |
package_header |
consumed by extractFilePackage BEFORE the walk (1397) | during the walk it's recursed, nothing matches |
call_expression (top level / class body / object body / .kts statements) |
callTypes:1248 → extractCall:3684 | attributes to the nodeStack top (file/namespace/class). Note class-BODY calls only occur via init blocks etc. (below) |
anonymous_initializer (init { }) |
no branch | recursed → its statements' calls → calls refs FROM THE CLASS node; its val locals → hook 'local' → nothing (pinned: calls "register" from=class:WidgetK) |
secondary_constructor |
no branch | NO constructor node; recursed → body calls attribute to the CLASS (calls "log" from=class:WidgetK); the constructor_delegation_call's value_arguments still feed fn-ref capture |
getter/setter as SIBLINGS (accessor on its own line) |
no branch | recursed → accessor-body calls attribute to the CLASS (or file). See §Properties for the sibling/child split |
object_literal (object : T { … } initializer) |
no branch anywhere | never a node; see §Body walker for the method-leak quirk |
file_annotation (@file:JvmName("x")) |
no branch | recursed; its value_arguments feed fn-ref capture (string args → nothing). No decorates ref |
| INSTANTIATION_KINDS (354-361) | no kotlin member | extractInstantiation:4610 is UNREACHABLE for kotlin — constructor calls Foo() are call_expressions → plain calls refs named Foo (capitalized). Kotlin emits zero instantiates refs, ever |
impl_item:1274 / property_signature:1282 / export_statement / swift property:1121 |
never | not kotlin node kinds (the swift property_declaration branch at 1121-1193 is gated language === 'swift' — kotlin property_declarations never enter it) |
generateNodeId(filePath, kind, name, startRow+1)
= `${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex.slice(0,32)}`
(tree-sitter-helpers.ts:18-30). FILE node id = literal file:${filePath}
(509), name = basename, qualifiedName = filePath, endLine =
source.split('\n').length, isExported false. Dedupe/self-checks compare
ID STRINGS (node_ids vec pattern).package_header (a leading file_annotation
or KDoc is skipped by the type filter); extractPackage → dotted text →
createNode('namespace', 'com.example.torture', pkgNode) = node #2
after the file node, pushed for the WHOLE walk. Every top-level symbol's
qualifiedName = com.example.torture::Name (buildQualifiedName:1447 joins
stack names with ::; namespacePrefix always empty outside C/C++). No
package header (scripts) → no namespace node, bare QNs, file: parents.extraProps.qualifiedName =
composeReceiverQualifiedName(receiverType, name) (1790-1792) =
`${receiverType}::${name}` verbatim (1435-1436; prefix empty → pass
through) — NO package prefix: fun WidgetK.extend in package
com.example.torture has QN WidgetK::extend (pinned). This is the first
ported walker with the receiver-QN surface — get the two QN builders'
divergence exactly right.namespace does NOT count (top-level
fns under the package namespace stay functions).function "`weird name`"). <anonymous> unreachable (grammar requires
the name). Extras: docstring (§Docstrings), signature undefined (dead
hook), visibility (hook), isExported undefined, isAsync (hook), isStatic
false, returnType (hook — §below). extractTypeAnnotations → emits
NOTHING (§Type-annotation refs); extractDecoratorsFor → §Decorators.
Push, body via resolveBody, visitFunctionBody, pop.this.nodes with name ===
receiverType && filePath === this.filePath && kind ∈ {struct, class, enum,
trait} → contains edge owner→method. QUIRKS, PRESERVE: interface is
NOT in the kind set — fun Drawable.ext() never gets an owner edge even
with Drawable in-file; source-order dependent (extension above its class →
no edge); the qualified-receiver bug (com::qext) looks up a node named
com (never found). Extension fns keep their normal contains edge from
the nodeStack top (namespace/file) REGARDLESS — the owner edge is
additive.expect fun has no body → resolveBody null → no body walk; node still
minted with dec=["expect"]. Interface bodiless methods likewise.fun inside a body → visitFunctionBody:5245 →
extractFunction → a function node contained by the enclosing
function/method (QN …::caller::localFn), receiver check applies (a
nested extension fn becomes a method with owner-contains).Positional (no fields): iterate namedChildren; before
function_value_parameters → skip; after it, the FIRST user_type |
nullable_type wins; hitting function_body or type_constraints first →
undefined. nullable_type unwraps to its inner user_type (?? child). Name =
the user_type's first type_identifier's text (?? the whole user_type),
trimmed; must match /^[A-Za-z_]\w*$/; Unit/Nothing
(KOTLIN_NON_CLASS_RETURN kotlin.ts:6) → undefined. Pinned: : WidgetK →
WidgetK; : WidgetK? → WidgetK; : Unit → undefined; inferred = expr →
undefined; : (Int) -> Unit (function_type) → undefined; : T (generic
param) → T (leaks as a returnType — preserve); extension receiver types
never mistaken (they sit BEFORE the params). Methods and functions both.
resolvedBody = resolveBody (class_body by type; null for bodiless). NO
skipBodilessClass → bodiless classes mint nodes. Extras: docstring,
visibility (hook), isExported undefined; decorators via createNode's
extractModifiers (expect/actual classes). Then extractInheritance (§below) —
BEFORE the body walk, so extends refs precede member emissions. Then
extractCsharpPrimaryCtorParamRefs (no-op — needs language csharp… actually
gated at :5939 by language, cheap early-out) and extractDecoratorsFor
(§Decorators — the @MyMarker class ref). Push, walk, pop.
primary_constructor is a DIRECT child of class_declaration, NOT of
class_body → constructor properties (class Foo(val a: Int)) mint NO
field nodes, ctor default-value calls emit NOTHING, and data class
components are invisible (pinned: DataK has zero members).call_expressions → calls refs from the CLASS node, and
delegation_specifier recursion reaches super-ctor argument calls too.
Ground truth (extract-bodiless.txt): class Bodiless(val b: Int =
initB()) : Base(readCfg2()) → extends Base + calls initB + calls
readCfg2, all from class:Bodiless; the IDENTICAL bodied class emits
ONLY extends Base. Reproduce the asymmetry exactly; also note the
header's value_arguments feed fn-ref capture in the bodiless case only.getter/setter siblings + anonymous_initializer + secondary_
constructor → plain recursion (calls attribute to the class).interface; docstring, isExported undefined —
NO visibility (extractInterface never calls getVisibility; pinned
vis=undefined). extractInheritance runs; body walk with the interface
pushed (bodiless member funs still mint method nodes; a val prop with
same-line getter → hook → field). Bodiless interface (sealed interface
SealedIface) → body ?? node fallback (1856) → header children re-visited
(nothing emits — but keep the traversal).enum_class_body) —
a bodiless enum would mint nothing (doesn't occur). docstring, visibility,
isExported undefined. extractInheritance (enum delegation_specifiers).
Body loop (1941-1950): enum_entry ∈ enumMemberTypes →
extractEnumMembers(entry); everything else (function_declaration after the
;, companion_object, secondary constructors) → visitNode with the enum
pushed → methods of the enum, companion constants under it.getChildByField(node,'name') → null (no
fields) → the identifier-children scan (1967-1974): one enum_member node
per direct simple_identifier child, positioned at the IDENTIFIER node
(createNode('enum_member', text, child)) — one per entry in practice.
QUIRKS, PRESERVE: an entry's value_arguments (OK(200)) and an entry's
class_body (OK(200) { override fun label() … }) are NEVER visited — the
override methods and any calls inside them are COMPLETELY INVISIBLE
(pinned: Http has enum_members OK/ERR + methods label/common/of only).val a: Int get() = compute()) → the getter is a
CHILD of property_declaration → the hook consumes everything → the
getter body is never walked (no calls refs).val b: Int\n get() = compute()) → the getter is
a SIBLING (child of class_body / source_file) → after the hook handles
the property, the walker visits the getter separately → its body's calls
attribute to the CLASS (or file/namespace at top level). Pin BOTH variants.val→constant / var→variable,
contained by the namespace (or file). Class body → field. companion/
object body → constant/variable under the CLASS node (stack top). Interface
body → field (class_declaration parent matches 'instance').val fn = { … } lambda
initializer IS walked, so its inner calls attribute to the enclosing
function (pinned: println from caller), unlike hook-consumed
properties. Local val x: T = … type annotations emit nothing
(§Type-annotation refs).lateinit var svc: Service in a class → field svc (modifiers don't
matter to the hook).Hook returns {moduleName: dotted path, signature: trimmed full text}: import
node (name = com.example.other.OtherClass; QN = namespace-prefixed) + the
generic imports ref (3183-3194): {fromNodeId: nodeStack top — the
namespace node when a package exists, else the file node, referenceName:
the dotted path, line: import_header startRow+1, column: startColumn}. NO
kotlin-specific emit pass (the 3197-3234 rust/php/ruby/python emitters are
all gated off). Shapes (probed):
import a.b.C → identifier text a.b.C.import com.example.wild.* → the identifier covers only
com.example.wild (the .* is a sibling wildcard_import) → a normal
import node/ref named com.example.wild (NOT null, unlike rust's
wildcard).import a.b.LongName as Short → identifier = a.b.LongName
(the import_alias child is ignored) — the SOURCE path, the alias binds
nothing.signature (trimmed full text) INCLUDES the comment lines
verbatim (pinned: sig "import com.example.alias.LongName as Short\n\n//
line comment run 1\n// line comment run 2", node L7-10); the namespace
node similarly spans to the last glued comment (extract-lfpkg.txt:
namespace L1-4). Ref line/column stay at the header START. Downstream:
those comments are NOT siblings of the next declaration → its docstring is
LOST (§Docstrings).OtherClass,
helper, wild, LongName) — the fn-ref gate is "defined in this file ∪
imported simple names" (unlike rust/ruby).Entry: not vbnet/erlang/ruby/arkts. func = getChildByField(node,'function')
?? node.namedChild(0) (4313) → always namedChild(0) for kotlin (no fields).
The cpp operator recovery (4324) is language-gated off.
Member branch (4364) — func.type === navigation_expression:
child1 = func.namedChild(1);
navigation_suffix → its first simple_identifier (?? the suffix itself
— unreachable in valid code: suffixes carry the identifier; ::class
suffixes never appear under a call's function position). methodName =
its text. Safe-call suffixes (?.) carry the same simple_identifier —
x?.render() emits exactly like x.render().func.namedChild(0)
(4385-4389).string_literal ("literal".uppercase()), integer_literal
(5.toString()) → emit NOTHING (pinned). Kotlin's other literal kinds
(boolean_literal, character_literal, null) also appear in the set —
port the WHOLE set verbatim.simple_identifier (4401; kotlin's name kind IS in the check
list) not in SKIP_RECEIVERS {self,this,cls,super} (by TEXT — kotlin
receivers are never those texts as simple_identifiers) →
`${recv}.${method}` (w.render, Registry.register,
Short.static, instances.add, it.render, anon.draw).this_expression/super_expression → none of the branches →
fall to the ELSE → bare methodName (this.toString() → toString;
super.hashCode() → hashCode). Same net effect as SKIP, different
path.call_expression + kotlin in the gate list (4408-4418) — the
#750 re-encode (4429-4442): innerNav = receiver.namedChild(0) (NOT a
function field!) → its text with /\s+/g stripped; re-encode ONLY when
/^[A-Z]/ → `${innerCallee}().${methodName}`. Pinned:
WidgetK.create().render() → WidgetK.create().render (+ the inner
WidgetK.create from recursion); Foo.getInstance().bar() →
Foo.getInstance().bar + Foo.getInstance; Foo().bar() would →
Foo().bar; lowercase chains fall to bare: lowerFactory().chain() →
chain + lowerFactory; w.chainInner().render() → render +
w.chainInner; listOf(1).forEach {…} → forEach + listOf.navigation_expression (2-hop a.b.method()), postfix_
expression (x!!.draw()), parenthesized, etc. → bare methodName.Else branch (4518-4520) — calleeName = RAW func text: bare
helper/run/WidgetK (constructor calls are plain capitalized calls
refs — kotlin emits NO instantiates, §dispatch table); backticked
`weird name` verbatim; generic calls generic<Int>(1) → generic
(type_arguments live in the call_suffix, not the callee). QUIRKS, PRESERVE
(all pinned in extract-torture.txt):
trailing() { it * 3 } parses as
call(call(trailing,()), annotated_lambda) → TWO refs: the outer's callee is
the inner call's RAW TEXT trailing() (garbage, unresolvable) + the
inner trailing. A no-paren trailing call (trailing { … },
run { … }) is ONE call → one clean ref.fn(3) followed by a line starting (
continues the expression (kotlin grammar) → fn(3)\n(fn)(4) is one
3-deep call chain emitting callees fn(3)\n (fn) (raw text with
embedded newline), fn(3), and fn. Deterministic garbage — reproduce
byte-for-byte./^\(\s*\*?\s*([A-Za-z_][\w.]*)\s*\)$/) applies to kotlin calleeNames —
a clean single-line (handler)(4) (as the FIRST statement of a body,
un-glued) has func text (handler) → rewritten to handler. Port the
regex."… ${w.render()} …" → calls w.render at the inner call's position
(interpolated_expression recursion). $topVal (interpolated_identifier)
emits nothing anywhere.Called ONLY from the body walker (5218) — top-level/class-scope reads emit
nothing (hook-consumed property initializers doubly so).
navigation_expression ∈ MEMBER_ACCESS_TYPES (326). Mechanics:
Registry.register(w)'s nav emits no
references ref).simple_identifier (4792); text must match
/^[A-Z][A-Za-z0-9_]*$/ → references ref at the RECEIVER's position
(pushStaticMemberRef 4800).Registry.count (statement) → references Registry; Color.RED
→ references Color; com.example.Fq.CONST_READ → NOTHING (nested
navs — the outer recv is a navigation_expression, not accepted; the
innermost recv com is lowercase); listOf(1).size → nothing (recv is a
call). Assignment WRITES emit nothing: Registry.count = 5 /
+= 1 parse as assignment > directly_assignable_expression — that node
type is NOT in MEMBER_ACCESS_TYPES (pinned: assignRefs() emits zero refs).Foo.Bar nav nested inside a bigger expression is visited on its own
as the walker recurses — each nav node is evaluated once.decorates (unlike csharp/php), asymmetricallyextractDecoratorsFor (4897) runs for functions/methods/classes (NOT
hook-created properties/fields/constants — the hook never calls it; pinned:
@field:JvmField val fielded → NO ref). Kotlin annotations live at
modifiers > annotation > … — scan #1 (4976-4987) descends into modifiers
children (the comment at 4979 names Kotlin) and consider() accepts node type
annotation (4928):
@JvmStatic / @MyMarker (no args) → annotation > user_type —
user_type IS in the target list (4950) → name = its text (<-strip +
last-./::-segment normalization at 4959-4962 apply) → decorates
ref {from: the decorated node, name, line/col of the ANNOTATION node}.
Pinned: decorates MyMarker from class Annotated, JvmStatic from method
jvmStatic.@Deprecated("gone", ReplaceWith("new")) (with args) → annotation >
constructor_invocation — NOT call_expression, NOT in the identifier
list → NO ref at all (and the argument expressions are never visited
by anything — no calls refs either). Pinned: method old has zero
decorator refs.@field:JvmField on a FUNCTION/CLASS would emit (the
use_site_target child is skipped, the user_type matches) — but on
properties (their usual home) the hook path never runs the extractor, so
in practice they're silent.platform_modifier children of modifiers (expect/actual) → not accepted
types → no decorates refs (they ride node.decorators instead).modifiers, never preceding siblings
(file_annotation has no branch that reaches consider()).extractInheritance's child loop runs over the CLASS NODE's direct
namedChildren — kotlin's delegation_specifiers are direct children
(probed; no wrapper node). Per specifier:
user_type; ctorInv = find direct
constructor_invocation; target = userType ?? ctorInv; none → skip.type_identifier (?? itself);
constructor_invocation → its user_type's FIRST type_identifier (?? the
user_type ?? the invocation).extends ref per specifier {name: typeId text, line/col: the
typeId node}. Kotlin NEVER emits implements — interfaces ride extends
too (pinned: SubK → extends OpenBase + Drawable + Comparable).: com.example.deep.RemoteBase()
→ ref named com (first type_identifier of the multi-segment
user_type — pinned); generic supertype Comparable<SubK> → Comparable
(type_arguments' identifiers aren't direct children); by-delegation
(: Drawable by d) emits NOTHING — the specifier's only child is
explicit_delegation and the direct-child finds miss (pinned:
DelegatedImpl has zero extends).object Add : SealedOp() → extends SealedOp ✓. Anonymous
object_literals never reach it (no class node).extractTypeAnnotations (5788) for kotlin takes the GENERIC path: params =
getChildByField(node, 'function_value_parameters') (5844) → null (zero
fields); returnType = getChildByField(node, 'type') (5851) → null; the
type_annotation direct-child search (5873) → no such node kind in this
grammar. extractVariableTypeAnnotation (6074) needs a type_annotation
child → dead; the body-walker variable_declarator branch (5230) needs node
kind variable_declarator → kotlin has none. property_signature/
method_signature (1282) are TS-only kinds. Net: kotlin emits ZERO
type-annotation references refs — no param types, no return types, no
property types, no local types (pinned: torture has no such refs).
BUILTIN_TYPES is never consulted for kotlin. The walker needs cheap
early-outs that preserve exactly this nothing.
Kotlin comment node kinds: line_comment (//) and multiline_comment
(/* */ AND KDoc /** */). getPrecedingDocstring accepts only
{comment, line_comment, block_comment, documentation_comment} —
multiline_comment is NOT in the set → KDoc NEVER becomes a docstring,
and a KDoc sitting between a // run and the declaration BREAKS the chain
(it's a non-comment named sibling to the scan). Pinned
(extract-docs.txt): /** KDoc */ + // line one + // line two + fun →
doc = "line one\nline two"; KDoc-only → doc undefined; /** kdoc */ then
// trailing line then fun → "trailing line" only. DOCSTRING_WRAPPER_TYPES
(55-62) contains no kotlin kinds → no anchor climbing. cleanCommentMarkers
(77-90): only the ^\/\/[/!]?\s? per-line strip fires for kotlin line
comments — all gm strips ride js_multiline_strip in docstring.rs (#1329
CRLF semantics) — call the shared docstring.rs, port nothing. Properties
(hook-created) never get docstrings at all. The import/package
comment-gluing (§Imports) eats the docstring of the first declaration after
the import block — pinned: topLevel has doc=undefined despite two //
lines directly above it.
Port the full machinery (crib java.rs/go.rs): CODEGRAPH_VALUE_REFS=0 kill;
MAX_VALUE_REF_NODES = 20,000 caps the prune DFS and each reader scan;
isGeneratedFile skip.
/[A-Z_]/ (file_table qualifies via _; count does
not), parent id prefix ∈ {file:, class:, module:, struct:, enum:}.
QUIRK (the php-namespace analogue, pinned): in ANY file with a package
header, top-level properties' parent is the namespace: node → NOT
accepted → top-level kotlin constants are NEVER value-ref targets. Only
un-packaged files (scripts, .kts) keep file-level targets
(extract-vref-nopkg.txt), and class/object/companion-scope constants
(class: parent) are the working population (extract-vref.txt:
readTop → TOP_LIMIT, readBoth → SHARED_TABLE).val's initializer reads nothing;
a hook-'const' object property IS a reader — its whole
property_declaration subtree incl. by lazy { } lambda contents is
DFS'd; the reader DFS has NO lambda halt, unlike the fn-ref scan).property_declaration (856-869) — vd = find direct
variable_declaration → its first simple_identifier → bump (the
Swift half of that case — name field / value_binding_pattern — is null
path for kotlin); destructuring (multi_variable_declaration) bumps
NOTHING (a destructured local shadow never prunes — quirk, preserve).
bump() counts identifier/simple_identifier nodes (807 — the comment
names Kotlin). Every val/var ANYWHERE in the tree bumps its name:
the target's own declarator + any body-local re-declaration → declCount
> fileScopeCount → target deleted. Pinned: companion RETRY_MAX +
a method-local val RETRY_MAX = 9 → RETRY_MAX pruned (readBoth emits
only SHARED_TABLE).simple_identifier
(906-909 — the comment names Kotlin; identifier/constant/name never
occur in kotlin trees). Any textual occurrence whose text maps to a
target: nav members (X.SHARED_TABLE's member half), ${TARGET}
interpolations (interpolated_expression > simple_identifier) — but NOT
$TARGET (node kind interpolated_identifier, not accepted — pinned).
Skip self-id, same-name, dedupe per (scope,target) → EDGE
{kind:'references', metadata:{valueRef:true}}, appended AFTER the walk
(flush order below). The Dart/Pascal sibling pull (891) is inert — a
kotlin property's next sibling can be a getter, which is neither
function_body nor block.idTypes = EMPTY (bare simple_identifiers are NEVER candidates;
explicitRef always true — irrelevant, no addressOfOnly). dispatch:
value_arguments → args; assignment → rhs with NO field (RHS = LAST
named child; the lhs for the param-storage skip comes from
namedChild(0) — the directly_assignable_expression; the skip
(408:430-443) compares the LHS's trailing identifier to the FULL rhs text —
callable refs start ::, so it effectively never fires for kotlin, but port
the comparison). layers: value_argument → null (fan out namedChildren).
special: {callable_reference, navigation_expression}. No
unwrap/ungatedModes/addressOfOnly.
value_argument label-forward skip (547-557) is DEAD for kotlin —
it reads getChildByField(node,'name') → null (zero fields), so a named
argument f(cb = cb) is NOT skipped; the fan-out visits both the label
and value identifiers (bare ids → nothing anyway, idTypes empty). Only
Swift exercises the skip. Reproduce the fan-out.callable_reference special (649-665): scan namedChildren — receiver =
last type_identifier child, member = last simple_identifier child. No
member → [] (String::class — the class is an anon keyword → member
null → nothing). No receiver → bare member (::topLevel → topLevel,
gated). Receiver present → /^[A-Z]/ on its text: OtherClass::handle →
candidate OtherClass::handle (the :: rule at flush:709 —
ALWAYS-flush); w::render → [] — the grammar parses even a lowercase
variable receiver as type_identifier, and the CASE regex (not the node
type) is what drops it (pinned: no ref).navigation_expression special (671-681): only when the WHOLE node's
text starts this:: → the navigation_suffix starting :: → its LAST
named child → candidate this.<member> (ALWAYS-flush). this::caller
pinned. Ordinary a.b navs in args → [].val x = register(::f) captures via the inner
value_arguments; the scan halts at lambda_literal (610), so refs
inside by lazy { }/trailing lambdas under a hook-consumed property are
NOT captured). NOT captured anywhere: property/local initializer
callable refs (val m = ::caller, val bound = w::render) — kotlin's
dispatch has NO property_declaration/varinit key (unlike SWIFT_SPEC —
do not borrow it). Pinned: torture emits exactly three function_refs —
topLevel (definedHere), OtherClass::handle, this.caller.this.-prefixed +
::-containing candidates always flush; bare names need definedHere
(same-file function/method names) ∪ importedNames (dotted-import last
segments — §Imports). Dedupe ${fromNodeId}|${name} →
{referenceKind:'function_ref'} (FUNCTION_REF_CODE 200 on the wire).macro_invocation branch rust-gated.call_expression → extractCall (5143), NO return → children recursed
(chains/args re-visited).function_declaration (5245) → named → extractFunction (→
receiver check → possibly extractMethod). Local funs become function
nodes (QN …::caller::localFn).class LocalClass { … } → full
extractClass (kind via classify), contained by the enclosing function —
its methods extract normally (pinned: caller::LocalClass::lm).object_declaration in a body is NOT dispatched (extraClassNodeTypes
is not checked in visitForCallsAndStructure) → recursed → its class_body's
funs hit 5245 → extractFunction: a local object's methods become
FUNCTIONS contained by the enclosing function (pinned: caller::om),
its properties mint nothing (no hook here). Same for object_literal
(anonymous objects): val anon = object : Drawable { override fun
draw() … } in a body → NO class node, NO extends ref, draw leaks out as
a function under the enclosing fn with its body calls attributed to it
(pinned: caller::draw, calls helper from=function:draw). At
TOP-LEVEL/class scope the same object_literal sits inside a hook-consumed
property → completely invisible (methods and all — scan halts at
nothing relevant but extraction never runs). Pin the asymmetry.!!, labels (label@), lambdas
(annotated_lambda/lambda_literal — enclosing-fn attribution), string
templates (interpolated_expression emits calls; interpolated_identifier
is inert).line = startPosition.row + 1, column = startPosition.column
— UTF-16 code units (textutil::col16), as are startIndex/endIndex
substrings (getNodeText everywhere) and the import-signature .trim().function_ref = wire code 200.extract() wrap: file node → namespace node (if package) → walk order →
flushFnRefCandidates then flushValueRefs (538-539, both while the
namespace is still pushed) → pops. Table order: nodes in creation order;
contains edges interleaved with creation, value-ref EDGES appended LAST;
walk-order refs, then function_ref refs at flush. Store/harness are
rowid-order-sensitive — reproduce exactly./^[A-Za-z_]\w*$/ and the
visibility .includes are single-token); the shared paths' regexes that
fire for kotlin are extractCall's parenthesized-conversion (4530,
single-name, \s* can eat \r — port as-is with \s semantics),
the #750 \s+ strip (JS \s ⊇ \r — Rust regex \s matches \r too,
parity holds), decorator name normalization (4959-4962), and
cleanCommentMarkers' gm strips → js_multiline_strip in docstring.rs
(#1329), call it. Grammar-level CRLF probed clean (identical shapes,
no errors, extract-crlf.txt byte-sane; the comment-gluing reproduces on
CRLF identically).has_error() → defer:; expected incidence
4.7–8.5% (§arch-6) including the PHANTOM class (hasError with no
ERROR/missing node — trust the flag, not node presence). wasm recovery is
canonical; fun-interface files always land here.queries.iterateNodesByLanguageWithDecorator(
'kotlin','actual') (db/queries.ts:1082 — a LIKE pre-filter over the
node DECORATORS column) then exact decorators.includes('actual'),
getNodesByQualifiedNameExact(act.qualifiedName), kind compatibility via
KMP_TYPE_KINDS {class, interface, struct, enum, type_alias} (:982 —
actual typealias fulfillment), different file, counterpart NOT marked
actual → synthesized calls edge decl→actual. Walker obligations:
decorators content+order from extractModifiers; EXACT qualifiedNames
(package-prefixed — com.example::PlatformFile); kinds; filePath;
startLine. Validate on kotlinx.coroutines (the KMP gate repo: 30
expect-files / 95 actual-files at survey time) — spot-check
synthesizedBy: 'kotlin-expect-actual' edge counts are IDENTICAL under
kernel and wasm arms after a full init of each.language === 'kotlin' it re-reads source and slices
sliceLines(content, m.startLine, m.endLine), regexing
.forEach { it( dispatchers and .append/.add/.push/.insert(
registrars. Walker obligations: method/function node startLine/endLine
spans (incl. the resolveBody endLine extension) and the language tag —
a truncated endLine silently drops dispatch edges..kt source in extractFromSource:6736-6758; their route
nodes carry literal ids (route:${filePath}:${line}:…) and their refs
carry filePath+language (framework refs, unlike extraction refs). No
walker dependency beyond method/class node names for their handler-ref
resolution.WidgetK::extend) does not enter generateNodeId.has_error() → defer: — INCLUDING phantom errors
(complete CSTs; do not "optimize" by checking for ERROR nodes) and every
fun-interface file. Sweep with the default --max-deferral 0.1; expected
deferral counts okio 23 / okhttp 49 / kotlinx.coroutines 51.node_ids vec pattern (same-(kind,name,line)
collisions are routine — e.g. one-line class X { fun x() … } shapes).VENDORED_WASM_LANGS += 'kotlin' (+ the kernel C
vendor/build.rs/langs.rs/grammar-parity row staged with it), then
old-vs-new full-init dump-diffs (scripts/dump-graph.mjs, cmp) on the
three gate repos with the kernel OFF both arms. Expected:
byte-identical on all three (behavior-neutral bump — any hunk at all
blocks). Full suite green ×2.## Fixtures to build below, exercised by a new
__tests__/kernel-kotlin-parity.test.ts.scripts/kernel-parity.mjs <dir>, order-sensitive
full-object, default --max-deferral 0.1):
/private/tmp/claude-501/-Users-colby-Development-CodeGraph-codegraph/0c11bda1-0b19-4fec-bcd9-d0cb4b2d6e8a/scratchpad/gate-repos/okio (small, 322 kt/kts files)…/gate-repos/okhttp (medium, 580)…/gate-repos/kotlinx.coroutines (large, 1,082 — the KMP/expect-actual gate)
(cloned fresh at survey; re-clone public OSS if gone — agent-eval policy).
Expect 0-diff on every NON-deferred file and exactly the §arch-6 deferral
counts. Then full-init dump-diffs byte-identical (kernel arm vs
CODEGRAPH_KERNEL=0, dump-graph.mjs, cmp) on the same three.select count(*) from edges where
json_extract(metadata,'$.synthesizedBy')='kotlin-expect-actual' equal
across arms (the dump gate already implies it; assert it explicitly once).fun interface file — asserts kernel defer: + wasm-served
output matches extract-funiface.txt shape) + a phantom-error fixture
(single-line class body — kernel defers despite the complete CST); full
suite ×2 green with CODEGRAPH_KERNEL_EXPECT=1.DEFAULT_ROUTED += 'kotlin' (kernel/index.ts:37) only after ALL of
the above; changelog rides the existing kernel entry.__tests__/fixtures/kernel-parity/torture.kt — the survey's
svy-kotlin/torture.kt is the seed (its extract-torture.txt is the
expected-output pin). Inventory, by branch: package header + file KDoc +
@file: annotation; imports: plain, dotted, wildcard (.* → package
path), alias (as — source path), comment-glued signature (comments
after the last import); top-level fn with params/return (ret from
user_type; sig undefined); extension fns: plain (WidgetK::extend),
generic receiver (List), qualified receiver (com::qext bug),
infix (Int::pow), operator; suspend fn (isAsync true) + the
@suspendMarker text-includes false positive; private internal
(visibility order) + an internal fun ('internal'); inferred return /
Unit / nullable / lambda return / : T generic leak; expect fun
(bodiless + dec) / actual fun; tailrec self-call in expression body;
top-level val/var/const val/by lazy {} (constant/variable kinds,
NO initializer refs, NO capture inside the delegate lambda) +
destructuring (val (a,b) → nothing, both scopes) + next-line-getter
top-level val (getter calls → file/namespace); class with primary ctor
(props invisible, defaults not walked), class-body val/var/computed
(fields; same-line getter consumed vs next-line getter → class-attributed
calls), init block + secondary ctor (class-attributed calls), methods,
companion (constants under the class + method + named companion
Named minting nothing); bodiless-vs-bodied header asymmetry (class
Bodiless(val b = initB()) : Base(readCfg2()) → extends + 2 class-level
calls; the bodied twin → extends only); data class (no members);
abstract/open one-liners (phantom-error shapes — but keep the PARITY
fixture erroring-free: single-line bodies go in the DEFER fixture
instead, since the kernel defers them!); interface (no visibility;
bodiless method nodes; default impl; val prop w/ same-line getter →
field); sealed class + nested object/data class members (extends
SealedOp); sealed interface (bodiless); enum: simple entries (positions =
identifier), ctor'd entries, entry-with-body (overrides invisible),
post-; methods + companion-in-enum; object declaration (class kind;
members; value-ref targets); annotation class; annotated class/method
(@MyMarker/@JvmStatic → decorates; @Deprecated("x", …) → NOTHING;
@field:/@get: on properties → nothing); typealias ×2 (no value refs)
actual typealias (dec on type_alias); expect class +
actual class (+ marked member); call shapes: bare, constructor
(WidgetK calls ref, NO instantiates), this./super. (bare), member,
aliased-import receiver, literal receivers (nothing), capitalized-chain
re-encode (WidgetK.create().render + inner) + lowercase chains (bare +
inner), 2-hop nav call, safe-call x?.render() (plain encoding), !!
receiver (bare), elvis-arm ctor call, trailing lambda (run {} /
w.let {} / listOf(1).forEach {}), paren-then-lambda
(trailing() {} → trailing() + trailing), generic call
(generic<Int>(1) → generic), backtick call, glued newline-invoke
chain (raw-text callee — or a note excluding it if the fixture keeps
statements separated; pin ONE of the two deliberately), (handler)(4)
first-in-body (conv-regex rewrite), interpolation call
("${w.render()}") + $id (nothing); static-member reads:
Registry.count / Color.RED (references at receiver pos),
com.example.Fq.X (nothing), Cls.member as callee (skip),
assignment LHS writes (nothing); delegation specifiers: plain +
ctor'd + generic + qualified (com bug) + by delegation
(nothing); local declarations in bodies: named local fn, local class
(full), local object (methods leak as functions), object :
Iface {} literal (function leak; and the top-level-property twin —
invisible); callable refs: ::topLevel (defined-here gate),
OtherClass::handle (import gate irrelevant — always-flush),
w::render (dropped), this::caller (this. flush),
String::class (nothing), val m = ::caller (NOT captured),
assignment obj.cb = ::handler (captured), named-arg
f(cb = ::handler) (fan-out capture); value refs: companion/object
constants + readers (incl. a ${CONST} interpolation read and a
$CONST non-read), the local-shadow prune, count-style
non-target names, and the namespace-drop (packaged file: top-level
consts are NOT targets — plus the un-packaged .kts/no-package twin
where they ARE); docstrings: // runs (kept), KDoc (dropped), KDoc
above a // run (run kept), KDoc between run and decl (chain broken),
comment-after-imports (lost to gluing); a non-ASCII (UTF-16) line
before a symbol; a when/for/labeled-loop body..kts fixture — top-level statements (calls from the FILE node),
top-level val (file-parent value-ref target), no package header.fun interface (Pattern 1 shape) — kernel defers
(defer:), wasm output serves the pinned interface+method recovery
(extract-funiface.txt).abstract class A { abstract fun
i(): Int } one-liner — kernel defers on has_error() despite a complete,
ERROR-node-free CST; wasm output is byte-normal.expect class + fun in one file, actual class
actual typealias in another (same QNs, different files) — feeds the
expect/actual synthesizer identically under both arms (can fold into the
frameworks-integration or synthesizer suites if simpler).svy-kotlin/)table-compare.cjs (kind/field/ABI tables), shape-probe-kotlin.cjs +
torture-{OLD,NEW}.txt (full CST dumps, byte-identical) + torture.diff
(empty), mini-probes.cjs + mini-probes.out (fun-interface ×2,
destructuring ×2, safe-call/elvis, assignment shapes, file annotation,
delegates, named-arg refs, getter nesting, KDoc runs, CRLF, suspend
false-positive, lateinit, top-level object literal — all OLD==NEW),
error-sweep.cjs + errors-{okio,okhttp,kotlinx.coroutines}.txt,
extract-probe.cjs + extract-{torture,vref,vref-nopkg,docs,bodiless,crlf,
funiface,lfpkg,kts}.txt (dist-extractor ground truth), fixtures
(torture.kt, vref.kt, vref-nopkg.kt, docs.kt, bodiless.kt,
crlf.kt, funiface.kt, lfpkg.kt, script.kts), grammar material
(tree-sitter-kotlin/ 0.3.8 tag clone + the tree-sitter.json shim,
crate-extract/ = crates.io 0.3.8 tarball, ng-extract/ = kotlin-ng 1.1.0
tarball, tree-sitter-kotlin-NEW.wasm = the staged-candidate build,
sha256s in §Grammar prep). Scratch dirs are throwaway — re-derive from this
doc if gone.