Files
codegraph/docs/design/kotlin-kernel-port-checklist.md
T
45a53eb5b5 feat(kernel): R7b Kotlin walker — kotlin module, vendored-grammar-C build, kotlin default-routed (#1382)
Sixth R7b port — the T1½ batch finale. Checklist-first recipe
(docs/design/kotlin-kernel-port-checklist.md, 1,121 lines, dist-extractor
ground truth); parity passed FIRST RUN on all three repos.

THE NOVEL MECHANISM — vendored-grammar-C (the §4 tracker's prescription,
first use): the crates.io tree-sitter-kotlin 0.3.8 pins `tree-sitter >= 0.21,
< 0.23` (the kernel links 0.25) and tree-sitter-kotlin-ng is a DIFFERENT
grammar (8 fields vs 0, renamed kinds — extractor-breaking), so no crate dep
is possible. The fwcd 0.3.8 tag's sha-matched parser.c + scanner.c are
vendored into codegraph-kernel/grammars/kotlin and compiled by build.rs (cc),
exposed via tree-sitter-language::LanguageFn. The wasm re-vendor is
behavior-NEUTRAL (0 CST/error disagreements across 1,984 gate-repo files;
old-vs-new full-init dumps byte-identical ×3) — a reproducibility re-vendor,
ABI stays 14.

Walker firsts: extension-function receivers (getReceiverType →
`WidgetK::extend` QN OVERRIDE with no package prefix, the qualified-receiver
`com::qext` first-segment bug, and the owner-contains fallback that excludes
`interface` kinds and is source-order dependent) and extractModifiers
(expect/actual platform modifiers → the node DECORATORS wire field on every
created node — the KMP synthesizer's feed, incl. `actual typealias`).
Preserved bug-for-bug: the FIELD_COUNT-0 dead cluster (no signatures, ZERO
type-annotation refs), hook-consumed property initializers emitting nothing
(incl. `by lazy {}`), the bodiless-vs-bodied class header asymmetry, enum-
entry bodies being invisible, KDoc never a docstring AND chain-breaking,
comment-gluing into import/package extents, `@Anno(args)` emitting nothing
while `@Marker` decorates, zero instantiates refs, the paren-then-lambda
`trailing()` garbage callee, text-includes visibility/suspend false
positives, and the packaged-file value-ref target drop. The fun-interface
misparse-recovery hook is DEFER-SHIELDED (every such file has_error) and
deliberately not ported. The swift-sweep lesson pre-applied: the shared
`assignment` shadow-prune case is implemented alongside the
property_declaration case.

Gates: sweeps 0-diff okio 299/322, okhttp 531/580, kotlinx.coroutines
1031/1082 (deferrals exactly the predicted 23/49/51 — both-arm grammar
reality incl. PHANTOM hasError files with complete CSTs; the kernel trusts
the flag); full-init dumps byte-identical ×3 (46.5k/108.9k/92.3k lines); KMP
expect/actual synthesis IDENTICAL across arms (412 edges on
kotlinx.coroutines — the tracker's KMP validation); kernel-kotlin-parity
suite (torture reflowed off the phantom shapes + .kts script + CRLF variants
+ fun-interface and phantom defer pins) + kotlin grammar-parity row (the
C-build ↔ wasm table identity proof); full suite 2,633 green ×2 under
CODEGRAPH_KERNEL_EXPECT=1. DEFAULT_ROUTED += kotlin (15 langs).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 17:33:18 -05:00

1132 lines
74 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Kotlin kernel port (R7b) — the bug-for-bug checklist
**Status: PORT COMPLETE (2026-07-20)** — walker `codegraph-kernel/src/kotlin.rs`
+ the vendored-grammar-C build (codegraph-kernel/grammars/kotlin via build.rs
cc — the mechanism's first use), all gates passed (bump dumps byte-identical
old-vs-new ×3 as predicted; parity sweeps 0-diff okio 299/322 / okhttp
531/580 / kotlinx.coroutines 1031/1082 with exactly the predicted 23/49/51
deferrals; kernel-arm dumps byte-identical ×3; KMP expect/actual synthesis
IDENTICAL — 412 edges both arms; kernel-kotlin-parity suite; DEFAULT_ROUTED
+= kotlin — 15 languages). One fixture note: the survey's torture.kt itself
tripped the PHANTOM-error class (one-line class/object bodies) and deferred —
the checked-in parity fixture reflows those to multi-line, and the phantom
shape is pinned in the defer test instead. Survey basis: every
TS-side branch a `.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.78.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.22× headroom (okhttp 8.45%) —
expect double-digit deferral COUNTS on kotlin sweeps and don't misread them
as walker bugs (§Architecture decisions #6).
## Grammar prep (behavior-neutral re-vendor + the vendored-C kernel build)
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).
- **Lineage decision (investigated, not assumed):** two crates exist.
- `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` `54104a7ef1555c265b746c790e0f8bb953cc17806e9df0c3af82f7f62c06a70a`
- `src/scanner.c` `27f73337ec357fc341fa57538f34c14277b0346980c3405dc30beab6202ec6d0`
- `tree-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.
- **The 0.3.8 build is behavior-IDENTICAL to the production wasm** (this bump
is a reproducibility re-vendor, csharp-flavored, not a version change):
kind/field tables identical (360 node types, 134 named kinds, 0 fields,
ABI 14 both — `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).
- **ABI note:** the 0.3.8 tag's checked-in parser.c declares
`LANGUAGE_VERSION 14` — content parity, ABI stays 14 (ruby precedent).
kernel-grammar-parity must assert same-revision, not an ABI change.
- **FIELD_COUNT 0 is load-bearing for the whole port**: every
`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):
- copy the tag's `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`);
- Cargo: add `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`:
```rust
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.
- Probe scripts + outputs live in the survey scratchpad (`…/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.
## Architecture decisions
1. **No preParse.** `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.
2. **Three framework resolvers can force the DECODED path for kotlin; none of
the gate repos trips any of them (verified).** parse-worker.ts:93-100
forces any language with an applicable framework `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.
3. **The framework extractors themselves need NO port** — regex over raw
source, run in extractFromSource:6736-6758 after either arm. §Frameworks
pins their input contracts.
4. **One walker module** (suggest `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.
5. **`.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.
6. **Deferral expectations:** okio 23/322 = 7.14%, okhttp 49/580 = 8.45%,
kotlinx.coroutines 51/1,082 = 4.71% — grammar-inherent, both-arm (§Grammar
prep table). Keep the sweep default `--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.
7. **REF_FLAG_FILE_PATH (wire v2 slot) is NOT needed for kotlin.** No kotlin
extraction path emits refs carrying `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.
## Extractor config (languages/kotlin.ts — 353 lines, read it whole)
Types: functionTypes=[`function_declaration`]; classTypes=[`class_declaration`]
(covers class/interface/enum via classifyClassNode); methodTypes=
[`function_declaration`] (same list — the 994/995 gate routes in-class-like
functions to extractMethod); interfaceTypes=[] ; structTypes=[]; enumTypes=[];
enumMemberTypes=[`enum_entry`]; typeAliasTypes=[`type_alias`];
importTypes=[`import_header`]; callTypes=[`call_expression`];
variableTypes=[`property_declaration`]; fieldTypes=[`property_declaration`]
(both lists — the hook consumes nearly all of them first);
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).
- **getSignature (kotlin.ts:277) is DEAD CODE — always undefined.** It reads
`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.
- **The property hook's `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):
- **visitNode (kotlin.ts:87-215)** — runs for EVERY node the main walker
visits (tree-sitter.ts:943-953; NOT in visitFunctionBody). Three branches:
1. **`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`.
2. **`lambda_literal` after a fun-interface ERROR (:139-143)** and
3. **fun-interface misparse recovery (:145-214)** (ERROR/
function_declaration shapes; `isFunInterfaceNode` :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.)
- **resolveBody (kotlin.ts:219-241)** — find by TYPE among namedChildren:
first `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.
- **classifyClassNode (kotlin.ts:242-255)** — scan ALL children (anon
included): child.type `interface` → 'interface'; `enum` → 'enum'; else
'class'. `annotation class` / `data class` / `sealed class` → 'class'
(their `class_modifier` children don't match); `sealed interface` →
'interface'.
- **getReceiverType (kotlin.ts:256-276) — LIVE, the extension-function
surface.** Walk ALL children in order: remember the last `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).
- **getVisibility (kotlin.ts:288-301)** — for each child of type `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 (kotlin.ts:302-305)** — always **false** (not undefined): every
function/method node carries `isStatic: false`.
- **isAsync (kotlin.ts:306-315)** — any `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').
- **extractModifiers (kotlin.ts:316-338) — expect/actual, the KMP surface.**
Scan children for `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).
- **extractImport (kotlin.ts:339-346)** — signature = trimmed
`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).
- **packageTypes=[`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`.
## tree-sitter.ts branches (anchors as of `a6c62d7`)
### visitNode dispatch — what each kotlin node hits (ladder at 936-1303)
| 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) |
### Node creation, IDs, qualified names
- createNode (1308): id = `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).
- endLine extension via resolveBody (1329) is LIVE for kotlin
function/method nodes (body found by type; in-range for this grammar, so
in practice a no-op extension — but CALL the hook, the ERROR-body branch
is part of the contract).
- contains edge from nodeStack top for every created node (1363);
extractModifiers merge (1355-1358); captureValueRefScope (1374).
- **Namespace capture** — extractFilePackage (1397): scan the ROOT's direct
namedChildren for the first `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.
- **Receiver-QN override**: extension methods get `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.
- isInsideClassLikeNode (1486): stack-top node kind ∈ {class, struct,
interface, trait, enum, module} — **`namespace` does NOT count** (top-level
fns under the package namespace stay functions).
### extractFunction / extractMethod (1517 / 1737)
- extractFunction: **line 1522 — getReceiverType short-circuit is LIVE**: any
function_declaration with a receiver (top-level extension fns, and nested
ones inside bodies) diverts to extractMethod. Name via extractName fallback
(first simple_identifier — backtick names keep their backticks:
`` 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.
- extractMethod (in-class-like functions + receiver-diverted extension fns):
receiverType recomputed (1742); gate 1747 passes via class-like OR
receiver. Same extras. **Receiver path (extension fns): QN override
(1790) + the owner-contains fallback (1799-1813)** — receiver present AND
not class-like → find the FIRST node in `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.
- Nested named `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).
### getReturnType = extractKotlinReturnType (kotlin.ts:17-43)
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.
### extractClass (1679) — and the bodiless-header asymmetry
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.
- **Bodied class: body = class_body → ONLY class_body children are visited.**
The `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).
- **Bodiless class: body = the class node itself (1714) → the HEADER children
are visited**: the primary_constructor recursion reaches default-value
`call_expression`s → **`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.
- Class-body members: hook properties (fields/constants), function_
declarations → extractMethod, nested class/object/enum → their branches,
`getter`/`setter` siblings + `anonymous_initializer` + `secondary_
constructor` → plain recursion (calls attribute to the class).
- extractInterface (1834): kind `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).
- extractEnum (1914): body REQUIRED (resolveBody finds `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.
- **extractEnumMembers (1958)**: `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).
### Properties — the hook rules + the getter-position split (probed)
- Same-line accessor (`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).
- Next-line accessor (`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.
- Top-level properties: scope 'const' → `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').
- Body-context locals NEVER reach the hook (visitFunctionBody doesn't run
it) — instead they're plain-recursed: **a local `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).
### Imports (3170-3236) — and the comment-gluing trap
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`.
- **wildcard `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).
- **alias `import a.b.LongName as Short`** → identifier = `a.b.LongName`
(the `import_alias` child is ignored) — the SOURCE path, the alias binds
nothing.
- **Comment-gluing (grammar quirk, LF and CRLF alike): comments FOLLOWING an
import (or the package header) attach INSIDE the import_header /
package_header node** → the node's extent extends over them and the
hook's `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).
- flushFnRefCandidates' QUALIFIED_IMPORT (665) matches dotted paths → kotlin
imports contribute their LAST segment to importedNames (`OtherClass`,
`helper`, `wild`, `LongName`) — the fn-ref gate is "defined in this file
imported simple names" (unlike rust/ruby).
### extractCall (3684) — the kotlin paths
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`:
1. property (4369-4378): field lookups null → `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()`**.
2. receiver = object/operand/argument fields (null) ?? `func.namedChild(0)`
(4385-4389).
3. **LITERAL_RECEIVER_TYPES (4397, set at 373-388)**: kotlin members that
occur: `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.
4. receiver `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`).
5. receiver `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.
6. **receiver `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`.
7. receiver `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`):
- **Paren-then-lambda `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.
- **Newline-glued invoke chains**: `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.
- The parenthesized-conversion regex (4530,
`/^\(\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.
- Template strip (4542) + cpp fn-ptr fan-out (4556) are c/cpp-gated — no.
- Final ref: {callerId = stack top, name, line = call startRow+1, column =
call startColumn (UTF-16)}. Inner calls of every chain are ALSO visited
(the body walker recurses after extractCall — no consumption).
- Calls inside string-template interpolations EMIT (in bodies):
`"… ${w.render()} …"` → calls `w.render` at the inner call's position
(interpolated_expression recursion). `$topVal` (interpolated_identifier)
emits nothing anywhere.
### Static-member / value-read refs (4750-4808) — kotlin IS in STATIC_MEMBER_LANGS (345-347)
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:
- callee-of-call skip (4772-4778): parent ∈ callTypes AND parent.namedChild(0)
starts at this node → skip (`Registry.register(w)`'s nav emits no
references ref).
- recv = object/expression/scope fields (null) ?? namedChild(0); accepted
types include `simple_identifier` (4792); text must match
`/^[A-Z][A-Za-z0-9_]*$/``references` ref **at the RECEIVER's position**
(pushStaticMemberRef 4800).
- Pinned: `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).
- A `Foo.Bar` nav nested inside a bigger expression is visited on its own
as the walker recurses — each nav node is evaluated once.
### Decorators — kotlin annotations DO emit `decorates` (unlike csharp/php), asymmetrically
extractDecoratorsFor (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.
- **Use-site targets `@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).
- The backward-sibling scan (5013-5022) is inert — kotlin annotations are
inside the declaration's `modifiers`, never preceding siblings
(file_annotation has no branch that reaches consider()).
### Inheritance — delegation_specifier (5595-5615)
extractInheritance's child loop runs over the CLASS NODE's direct
namedChildren — kotlin's `delegation_specifier`s are direct children
(probed; no wrapper node). Per specifier:
- userType = find direct `user_type`; ctorInv = find direct
`constructor_invocation`; target = userType ?? ctorInv; none → skip.
- typeId: user_type → its FIRST `type_identifier` (?? itself);
constructor_invocation → its user_type's FIRST type_identifier (?? the
user_type ?? the invocation).
- ONE **`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).
- QUIRKS, PRESERVE: **qualified supertype `: 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).
- Runs for classes, objects (extraClassNodeTypes → extractClass), interfaces,
enums. `object Add : SealedOp()` → extends SealedOp ✓. Anonymous
`object_literal`s never reach it (no class node).
### Type-annotation references — kotlin ∈ TYPE_ANNOTATION_LANGUAGES (5753) but the machinery is DEAD
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.
### Docstrings (tree-sitter-helpers.ts:95-127) — KDoc is DROPPED
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.
### Value-reference edges (398-931) — kotlin IS in VALUE_REF_LANGS (401)
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.
- **Targets** (captureValueRefScope:735): kind constant|variable, name
length ≥3 AND `/[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).
- **Reader scopes**: every function/method/constant/variable node (764) —
fields are NOT readers (a class `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).
- **Shadow prune** (803-878): the kotlin declarator case is
**`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).
- **Emission** (880-930): per reader scope DFS (stack-based, namedChildren
pushed in order and POPPED — reverse-source-order visitation, ruby
precedent; edge ORDER follows); reader node type `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`.
### Function-as-value capture (#756) — KOTLIN_SPEC (function-ref.ts:240-248)
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.
- The `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 → [].
- Capture points: visitNode:990 (top-level/class-scope call args),
visitFunctionBody:5137, scanFnRefSubtree (hook-consumed property
subtrees — `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`.
- Flush gate (639-728): generated-file skip; `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).
### visitFunctionBody (5129-5286) — kotlin rows
- maybeCaptureFnRefs (5137) per node; `macro_invocation` branch rust-gated.
- `call_expression` → extractCall (5143), NO return → children recursed
(chains/args re-visited).
- INSTANTIATION_KINDS (5145) — never for kotlin. extractBareCall — absent.
- extractStaticMemberRef (5218) — every body node (§Static-member).
- variable_declarator type-annotation branch (5230) — dead (no such kind).
- **Nested `function_declaration` (5245)** → named → extractFunction (→
receiver check → possibly extractMethod). Local funs become `function`
nodes (QN `…::caller::localFn`).
- classTypes (5255): a body-local `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
`fun`s 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.
- Bodies recursed transparently through when/if/for/try
(control_structure_body), elvis, postfix `!!`, labels (`label@`), lambdas
(annotated_lambda/lambda_literal — enclosing-fn attribution), string
templates (interpolated_expression emits calls; interpolated_identifier
is inert).
### Misc shared paths
- Positions: `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()`.
- Refs carry NO filePath/language (store denormalizes; §arch-7 — no
REF_FLAG_FILE_PATH use). `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.
- **CRLF hazards inventory for the kotlin path**: kotlin.ts has NO regexes
over multi-line source (getReturnType's `/^[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).
- Defer policy: per-file `has_error()``defer:`; expected incidence
4.78.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.
- MAX_FILE_SIZE / generated-file skips: shared, nothing kotlin-specific.
## Frameworks & synthesis consumers (stay TS-side — pin the walker's output contract)
- **kotlinExpectActualEdges (resolution/callback-synthesizer.ts:987-1026;
doc block :955-985)** — the tracker's "expect/actual pairing is
synthesis-side". Reads `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.
- **Closure-collection pass (callback-synthesizer.ts:252-326, CC_LANGUAGES
:77 = {swift, kotlin} — the #1235 gate)** — synthesis-side, no port, but
it consumes extraction artifacts: for every method/function node with
`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.
- **rnCrossPlatformEdges (callback-synthesizer.ts:1645+)** — kotlin ∈ NATIVE
set (:1649); pairs native method/function node NAMES across
java/kotlin/objc/cpp with JS callers. Standard node-table obligation only.
- **springResolver / expoModulesResolver / fabricViewResolver** (§arch-2) —
regex over raw `.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.
## Parity mechanics (all have bitten before)
- **Emission order** per §Misc: file → namespace → source-order walk (per
construct: node + contains edge → extends refs BEFORE body members →
extractor-order refs) → function_ref refs → value-ref EDGES last.
- **generateNodeId inputs**: (filePath, kind, name, startRow+1) — name keeps
backticks; import nodes are named the dotted path; enum_member line = the
IDENTIFIER's line (not the enum_entry's — same line in practice, but the
COLUMN and the node's position row both come from the identifier child);
namespace line = the package_header start; the glued-comment extents
affect endLine/endColumn (and import signatures), never the id line.
- **Receiver-QN methods**: id hashes the NAME only — the qualifiedName
override (`WidgetK::extend`) does not enter generateNodeId.
- **UTF-16 columns/slices** (textutil::col16/slice_utf16): every ref/node
column, getNodeText substrings, import-signature trim. Kotlin sources are
multibyte-heavy (string templates, KDoc) — the torture fixture needs a
non-ASCII line before a symbol.
- **CRLF**: §Misc inventory; CRLF variants of every fixture derived
in-memory (kernel-tsjs-parity pattern).
- **Defer policy**: `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-ID-string dedupe**: `node_ids` vec pattern (same-(kind,name,line)
collisions are routine — e.g. one-line `class X { fun x() … }` shapes).
## Gates (per plan §5, no exceptions)
- **Standalone GRAMMAR-BUMP gate first (rust pattern), before any walker:**
vendor the wasm + `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.
- **Torture fixtures** per `## Fixtures to build` below, exercised by a new
`__tests__/kernel-kotlin-parity.test.ts`.
- **Parity sweeps** (`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.
- **KMP synthesis spot-check** (tracker row requirement): after the
kotlinx.coroutines dumps, `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).
- **Suite**: kernel-kotlin-parity torture + CRLF variants + the defer
fixture (a `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.
- Post-route perf sanity: gate repos ride the raw path (§arch-2); a
Spring-Boot-Kotlin or Expo repo is the decoded-path smoke check. Deferral
costs mean the kotlin speedup lands on ~92-95% of files — measure
accordingly.
## Fixtures to build
1. `__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.
2. **CRLF variants** of the fixtures derived in-memory (kernel-tsjs-parity
pattern) — docstring cleaning + comment-gluing + import signatures under
CRLF bytes.
3. **`.kts` fixture** — top-level statements (calls from the FILE node),
top-level val (file-parent value-ref target), no package header.
4. **Defer fixture #1: `fun interface`** (Pattern 1 shape) — kernel defers
(`defer:`), wasm output serves the pinned interface+method recovery
(`extract-funiface.txt`).
5. **Defer fixture #2: phantom error** — `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.
6. **KMP fixture pair** — `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).
## Probe artifacts (session scratchpad `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.