swift-kernel-port-checklist.md 72 KB

Swift kernel port (R7b) — the bug-for-bug checklist

Status: PORT COMPLETE (2026-07-20) — walker codegraph-kernel/src/swift.rs, all gates passed (bump validated standalone with the diff classified per the error-union rule + ripple pairing + two gate-found categories 7/8 below; parity sweeps 0-diff Alamofire 89/98 / vapor 224/247 / swift-nio 407/554 at --max-deferral 0.3 with exactly the predicted deferral counts; dump gates byte-identical ×3; the Alamofire census reproduced property=348 on the kernel arm; kernel-swift-parity suite; DEFAULT_ROUTED += swift). One survey correction found by the swift-nio sweep: §Shadow prune understated the case list — the shared assignment prune case (tree-sitter.ts:829) is ALSO swift-live (a declared-then-assigned let X: T + X = … branches bumps per assignment via the directly_assignable_expression's simple_identifier, pruning X — swift-nio's main.swift files pin it). Survey basis: every TS-side branch a .swift file exercises, with file:line anchors as of a6c62d7 (HEAD at survey time, clean main). Every grammar-shape and emission claim below was probed against both the current production wasm (tree-sitter-wasms 0.1.13 build of npm tree-sitter-swift ^0.4.0, ABI 13) and a fresh crate-0.7.3 build (ABI 15) — CST probes for shapes, plus the built dist/ extractor run end-to-end on a 106-line torture file and on all 98 Alamofire files (emissions, order, counts pinned from real output, not inference). Probe scripts + raw outputs: session scratchpad svy-swift/ (§Probe artifacts). Read WITH docs/design/rust-kernel-migration-plan.md (§0a recipe, §2 boundary, §5 gates) and the format precedents (rust-lang-, csharp-, ruby-, php-kernel-port-checklist.md — php's non-neutral-bump "enumerate + classify" gate is the template here).

Blocking findings: none. Three eyes-open findings that shape the port: (1) the grammar bump is NOT graph-neutral — the old→new delta is confined to error-set membership (65 files across the three gate repos parse differently-erroneously per arm; three NEW-only regression construct classes enumerated in §Grammar-bump deltas) while every clean-parse extractor-relevant shape probed byte-identical (53-line CST diff across the whole battery, all classified inert); the bump gate is php-style enumerate+classify, not expect-zero. (2) Swift error incidence is structurally high — 9.2–26.5% on the NEW arm (both-arm reality, not a bump artifact) — the parity-sweep deferral guard MUST be raised for swift (§Deferral policy); a 0.1 guard fails the sweep on all three gate repos. (3) The port's center of gravity is the dedicated in-class property branch in tree-sitter.ts (#1020) — it is NOT in languages/swift.ts, it attributes its refs to the enclosing type rather than the property node, and Alamofire's 348 computed-property nodes (re-counted at survey time: exactly 348) ride it (§Dedicated property branch).

Grammar prep (NOT staged — land FIRST, before any walker exists)

swift is not in VENDORED_WASM_LANGS (grammars.ts:291) — production loads require.resolve('tree-sitter-wasms/out/tree-sitter-swift.wasm') (mapping swift: 'tree-sitter-swift.wasm' at grammars.ts:34; .swift → swift at grammars.ts:105), a 2023/24-era ABI-13 build of npm tree-sitter-swift ^0.4.0 (sha256 41c4fdb2…, 3,147,876 bytes; 540 node types / 45 fields).

  • Version: crate tree-sitter-swift = "=0.7.3" (crates.io max_stable, published 2026-06-01; repo alex-pinkus/tree-sitter-swift).
  • PROVENANCE TWIST — the crate tarball is the canonical generated-file source, NOT the git tag. alex-pinkus keeps generated files off main; tag 0.7.3-with-generated-files (commit 31d17fe7e818a2048c808b5c6fdc2dc792f4f5b5) ships an ABI-14 parser.c (LANGUAGE_VERSION 14, SYMBOL_COUNT 565 — an older-generator run, plus parser_abi13.c/parser_abi14.c variants), while the crates.io tarball ships an ABI-15 regeneration (LANGUAGE_VERSION 15, SYMBOL_COUNT 558, STATE_COUNT 10321). The two are the SAME grammar — grammar.json rules and externals are JSON-equal between tag and crate (probed) — but the parser.c bytes differ, so a tag-side sha-match is impossible. Build the wasm from the CRATE TARBALL's src/ (that is literally what the kernel's cargo build compiles → table-identity by construction, the csharp-precedent verification posture). Never touch the tag's parser.c and never run tree-sitter generate.
    • crate src/parser.c sha256 d3edff6effe31b9a507f496577407987343b101b23eb7bee7a9b050e8ab5d27a (20,642,243 bytes — expect slow builds)
    • crate src/scanner.c sha256 f3d6271d64f58c39eed544104a70ca2cf9ecbf80c5d900620f1afd38836542cb (== the tag's scanner.c byte-for-byte). External scanner: YES — the crate build compiles it automatically; the wasm build picks it up from src/.
  • Build (survey-verified commands):

    curl -sL https://crates.io/api/v1/crates/tree-sitter-swift/0.7.3/download -o ts-swift-0.7.3.crate
    tar xzf ts-swift-0.7.3.crate && cd tree-sitter-swift-0.7.3
    # crate tarball lacks tree-sitter.json (metadata only — no effect on tables);
    # copy it from the 0.7.3-with-generated-files tag clone:
    cp ../tag-clone/tree-sitter.json .
    npx -y tree-sitter-cli@0.25.10 build --wasm -o tree-sitter-swift.wasm .
    

    (brew emcc present; one benign scanner warning. Survey artifact: ABI 15, 3,726,622 bytes, sha256 cc77a63b8487956270e2f385e29a03ba0773ba532a3c8a8844a26b4c98793843, 563 node types / 46 fields, at scratchpad svy-swift/tree-sitter-swift.wasm.)

  • Version-choice rationale (evaluated, not defaulted): the feared alex-pinkus shape churn did NOT materialize for extraction — the full OLD-vs-NEW CST battery (types/funcs/props/calls/imports/statics/docs) diffs in only 53 lines, all inert (§deltas). An older crate (0.5.0/0.6.0) was considered as a closer-match alternative and REJECTED: clean-parse shapes are already identical on 0.7.3, so an older pin buys no shape proximity and loses the error-set wins that dominate the delta — swift-testing #expect (Alamofire's whole test suite), #Preview/#GET-style freestanding macros (vapor 57→23 error files), package access, and typed throws(E) all parse ONLY on 0.7.x. 0.7.3's three regression construct classes (§deltas) are net-smaller (21 files) than its fixes (63 files) on the gate repos.

  • Staging plan: vendor to src/extraction/wasm/tree-sitter-swift.wasm; add 'swift' to VENDORED_WASM_LANGS (grammars.ts:291) with an R7b comment noting the crate-tarball provenance (NOT tag-sha-matched — state why); pin tree-sitter-swift = "=0.7.3" in codegraph-kernel/Cargo.toml (crate + wasm move TOGETHER); add 'swift' to GRAMMAR_LANGUAGES in __tests__/kernel-grammar-parity.test.ts:39; kernel symbol tree_sitter_swift::LANGUAGE in langs.rs grammar_for + the LANGUAGES const (langs.rs:18, [&str; 13] → 14). MIT license. copy-assets already globs src/extraction/wasm/*.wasm.

  • Bump lands FIRST, standalone, full suite green + the enumerate+classify dump gate (§Gates) — the diff is expected NON-empty and must be confined to the classified categories.

Grammar-bump deltas (old ^0.4.0 → 0.7.3), every one classified

Clean-parse shapes: byte-identical. The whole probe battery (class-family, functions, properties incl. wrappers/observers/tuple-lets, every call shape, imports, static reads, docstrings) produced a 53-line CST diff, fully decomposed into:

Inert (verified against every consuming branch):

  1. #selector token split: OLD one anon #selector token; NEW anon # + selector. selector_expression's NAMED children are unchanged → normalizeSpecial (function-ref.ts:685) and the body walker see identical trees. Inert.
  2. #warning(…) internals: OLD = one diagnostic leaf swallowing the full text; NEW = diagnostic with an anon # child. diagnostic matches no type list on either arm; no named children either way; no stray sibling call on NEW (probed clean-file). Inert.
  3. ABI/table renumber (540→563 node kinds, 45→46 fields) — kind-id churn only; every consumer keys on type STRINGS.

Behavior-changing — ALL of it is error-set membership (the wasm arm never defers: production extracts from error-recovery trees, so files whose error-status or recovery changes WILL diff in the bump gate's dump):

  1. OLD-error → NEW-clean (the bump working as intended), 63 files across the gate repos. Construct classes, each probed to a minimal repro: freestanding macros — #Preview { } (OLD: ERROR + orphan lambda; NEW: macro_invocation — a NEW-only node type, in NO type list → recursed, its lambda's calls attribute to the enclosing scope), swift-testing #expect(…) (all 4 OLD-only Alamofire error files are Tests using it), vapor's #GET route macros (drives vapor 57→23); package access modifier; typed throws throws(ErrorType) (swift-nio ByteBuffer-views). Node/edge/ref diffs in these files are expected and accepted.
  2. NEW-only error regressions, 21 files (3 Alamofire / 1 vapor / 17 swift-nio), three construct classes probed to minimal repros:
    • #if/#elseif between enum cases (enum E { case a\n#if DEBUG\n case b\n#endif }) → NEW ERROR, OLD clean. (#if at top level, between CLASS members, around postfix .modifier(x) chains, and with || conditions all stay clean on both.)
    • Parenthesized-compound directive conditions (#if (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && …)) → NEW ERROR (swift-nio's lock/NIOLock family).
    • Optional-subscript + cast + coalesce chain (info?["k"] as? String ?? "d") → NEW ERROR (Alamofire HTTPHeaders).
    • (vapor's one file combines a backtick-escaped `default` parameter label with other syntax; the label alone parses clean on both.) These files' dumps diff (different recovery trees). Accept as classified; count them; they are the deferral set the kernel will hand to wasm anyway.
  3. BOTH-arm error files that diff anyway: two grammars recover differently from the SAME error (e.g. vapor Authenticator's if let c = try? await … — errors on both, different trees). Any hunk in a file that is in EITHER arm's error list falls under this category — the gate's mechanical rule is per-file, not per-construct (below).
  4. (Found at bump-gate time, survey-missed.) Docstring boundaries near #if directives on CLEAN files. The NEW grammar's directive/comment sibling structure changes what getPrecedingDocstring accumulates next to #if/#endif lines: 7 clean-on-both files (5 Alamofire, 2 swift-nio Mocking.swift) diff in the docstring field ONLY (verified mechanically: every other node field byte-equal). Direction: NEW gains a docstring the OLD chain dropped (webSocketRequest gains "Only Apple platforms…") or extends it with directive-adjacent text (_withWindowsPaths gains a leading "ENABLE_MOCKING" line). The walker reproduces this automatically (same shared docstring code over the same NEW trees).
  5. (Found at bump-gate time, survey-missed.) Array-literal-callee call refs. One clean file (swift-nio IPv4Header.swift) emits 2 NEW-only calls refs whose callee text is a whole multi-line array literal — a call_expression whose namedChild(0) is the array literal on NEW where OLD parsed the same bytes as separate expressions. Same garbage-but- deterministic family as m[i][j]; the walker reproduces it via the raw func-text rule.

Bump-gate rule (php precedent, adapted): run the old-wasm vs new-wasm full-init dump diff on all three gate repos; every diffing file must be in the union of the two arms' hasError file lists (compute both lists first — svy-swift/error-incidence.cjs does exactly this); a diff in a clean-on-both file blocks the bump. Expect RESOLUTION ripple beyond the erroring files (recovered symbols re-resolve refs in OTHER files — php's category-3 mechanism); prove ripple hunks mechanically by ref↔edge pairing (php checklist's ripple-proof.mjs pattern) instead of re-litigating per file.

Error incidence (probed, all .swift ≤1 MiB, both arms)

Repo files OLD (^0.4.0 ABI-13) NEW (0.7.3) OLD-only NEW-only both
Alamofire 98 10 (10.20%) 9 (9.18%) 4 3 6
vapor 247 57 (23.08%) 23 (9.31%) 35 1 22
swift-nio 554 154 (27.80%) 147 (26.53%) 24 17 130

Deferral policy — swift needs a c/cpp-style exemption

Swift sits FAR outside the ts/java/py/go norm (0–0.42%): heavy #if platform-conditionalization (swift-nio), try? await in conditions, and @storageRestrictions init-accessors error on BOTH arms. Per-file has_error()defer: like every walker; run parity sweeps with --max-deferral 0.3 (covers swift-nio's 26.5% with headroom; Alamofire/vapor sit under 10%). Double-digit deferral on swift is grammar reality, not a walker bug — but a deferral-rate JUMP vs the table above is a walker bug. Deferred files are served by wasm and byte-match by construction; the sweep's compared set is the ~73–91% that parse clean.

Architecture decisions

  1. No preParse. swiftExtractor has no preParse hook (languages/swift.ts — whole file, no such key) → preParsedSource (kernel/index.ts:96) is a no-op; both arms parse raw bytes. Swift's #if is grammar-native (directive nodes, both branches kept as siblings — probed) — nothing to blank, unlike C/C#.
  2. Framework detection decides decoded vs raw path per repo. THREE swift resolvers carry extract() hooks (parse-worker.ts:93-99 forces any language with an applicable framework extract() onto the decoded extractFromSource path): swiftUIResolver (frameworks/swift.ts:11, detect = any .swift containing import SwiftUI, else any file path ending .xcodeproj/.xcworkspace — note getAllFiles() returns indexed files, so the xcodeproj leg rarely fires; the pbxproj inside doesn't end with .xcodeproj), uikitResolver (:136, detect = any .swift containing import UIKit/UIViewController/UIView), vaporResolver (:269, detect = Package.swift containing vapor, else import Vapor). Also expoModulesResolver (expo-modules.ts:154, languages swift+kotlin, has extract) and swiftObjcBridgeResolver (swift-objc.ts:251 — resolve-only, NO extract, doesn't force decode). Gate-repo reality (verified): Alamofire → decoded (Example/ imports UIKit; watchOS example imports SwiftUI), vapor → decoded (Package.swift), swift-nio → RAW buffers path (no detector fires) — one gate repo per transport, keep all three.
  3. Framework extractors themselves need NO port (regex-over-raw-source TS, run in extractFromSource after either arm). Their input contract on the walker: node names/kinds (struct/class/component resolution by name, frameworks/swift.ts:432) and — for the closure-collection pass — function/method node extents (§CC pass). Vapor's route refs carry filePath+language but those are FRAMEWORK refs (TS-side, post-walk).
  4. No extraction ref carries filePath — swift has no visitNode hook and no framework-independent denormalized refs (probed: every ref in the torture/Alamofire runs has filePath undefined). The v2 REF_FLAG_FILE_PATH wire slot (buffers.rs:125 / layout.ts:98) stays UNUSED for swift — flag 0 on every ref, like rust/csharp.
  5. One walker module (suggest codegraph-kernel/src/swift.rs), registered in langs.rs (grammar_fortree_sitter_swift::LANGUAGE.into(), LANGUAGES += "swift"). Skeleton cribs: java.rs for the class-like scope stack + static-member refs + decorators; ruby.rs/php.rs for hook-heavy dispatch; rustlang.rs for the node_ids dedupe + import hooks. The dedicated property branch has no precedent in any walker — it is new logic, transcribe from §Dedicated property branch.
  6. .swift → swift only (grammars.ts:105); no content sniffing, no dialect. MAX_FILE_SIZE (1 MiB) and generated-file skips are orchestrator/TS-side and shared.
  7. No POST_PASSES entry (kernel/index.ts:81 — none for swift) → tryKernelExtractRaw stays eligible.

Extractor config (languages/swift.ts — 138 lines, read it whole)

Types: functionTypes=[function_declaration]; classTypes=[class_declaration]; methodTypes=function_declaration; interfaceTypes=[protocol_declaration]; structTypes=struct_declaration; enumTypes=enum_declaration; enumMemberTypes=[enum_entry]; typeAliasTypes=[typealias_declaration]; importTypes=[import_declaration]; callTypes=[call_expression]; variableTypes=property_declaration, constant_declaration. nameField=name, bodyField=body, paramsField=parameter, returnField=return_type.

THE FIELD-LOOKUP SUBTLETY (do not trust CST dumps here): fieldNameForChild labels a function's return type and a typealias's value name: — but childForFieldName resolves MULTIPLE field names on those children through hidden rules. Probed truth-table, IDENTICAL on both grammars (svy-swift/field-probe.cjs):

lookup result
function_declaration name the simple_identifier (function name)
function_declaration parameter NULL (parameter children are unfielded)
function_declaration return_type the return type node (user_type/optional_type/…)
function_declaration body function_body
typealias_declaration name type_identifier (alias name)
typealias_declaration value the aliased type node
property_declaration name the pattern node
property_declaration value the initializer expression
property_declaration computed_value computed_property
enum_entry name the FIRST case-name simple_identifier only

The Rust walker must reproduce these exact resolutions (tree-sitter's native child_by_field_name has the same semantics — but pin them in the parity fixture, this is the branch the whole type-ref story hangs on).

Hooks PRESENT (port each exactly):

  • getReturnType = extractSwiftReturnType (swift.ts:14) — POSITIONAL scan of namedChildren: skip everything until the first simple_identifier (the name; modifiers/type_parameters before it are skipped via the seenName latch); after it, function_body → undefined (body reached); user_type → that node; optional_type → its first namedChild of type user_type (?? null); first such hit wins: text = getNodeText(typeNode).trim() .replace(/<[^>]*>/g, ''), last .-segment (KF.BuilderBuilder — the comment explains why NOT the first type_identifier), must match /^[A-Za-z_]\w*$/ and ≠ 'Void' else undefined. Probed results: -> WidgetWidget; -> Foo?Foo; -> KF.BuilderBuilder; -> Result<Foo, Err>Result (single-level generic strip); nested generics break like rustResult<Vec<Foo>, E> → strip leaves Result, E> → regex fails → undefined; -> Void/-> [Foo](array_type)/ -> (Int, Foo)(tuple_type)/-> (Int) -> Foo(function_type) → undefined. Parameters never match (their node type is parameter, not user_type — the scan does NOT descend).
  • resolveName (swift.ts:60) — class_declaration ONLY, name field must be a user_type with >1 type_identifier children → the LAST one's text (extension KF.BuilderBuilder, so extension members merge with the extended type's simple name, #750). Single-segment (extension Plain) and non-user_type names → undefined (default extraction).
  • getSignature (swift.ts:76)getChildByField(node,'parameter') is ALWAYS NULL (table above) → if (!params) return undefinedsignature is undefined on EVERY swift function/method (probed: sig=null across the torture file and Alamofire). The returnType concatenation is dead code. The walker emits NO signature for swift, ever.
  • getVisibility (swift.ts:87) — scan ALL children (anon included) for a child of type modifiers; on its WHOLE text: .includes('public') → 'public', .includes('private') → 'private', .includes('internal') → 'internal', .includes('fileprivate') → 'private' (dead arm — 'fileprivate' contains 'private', the second check already caught it); no modifiers / no match → 'internal'. QUIRKS, PRESERVE: open → 'internal' (not checked — probed: open class func classFunc → vis=internal); public private(set) → 'public' (first match); substring-matching runs over ATTRIBUTE text inside modifiers too (an attribute whose lowercase text contains 'private'/'public' would flip visibility — not expressible with real-world wrappers, but the walker must substring-match the same way).
  • isStatic (swift.ts:101) — any modifiers child whose text includes 'static' OR 'class' → true. class func/class var → static ✓ deliberate. Same substring hazard (an @objc(myclassthing) attribute text would set isStatic — match bug-for-bug).
  • classifyClassNode (swift.ts:112) — scan ALL children for a child of TYPE struct → 'struct', TYPE enum → 'enum', else 'class'. The grammar puts the keyword in the declaration_kind: anon child (class/struct/ enum/actor/extension) → actor → 'class', extension → 'class'.
  • isAsync (swift.ts:121) — modifiers text includes 'async'. Effectively always FALSE (rust-precedent dead code): the grammar puts async as an anon child AFTER the parameter list, never inside modifiers (probed: func asyncy() async throws → isAsync=false). The walker must reproduce false — note extractFunction/extractMethod store isAsync: false (hook present, returns false), NOT undefined.
  • extractImport (swift.ts:130) — signature = trimmed full node text; identifier = namedChildren find(type === 'identifier') → moduleName = its FULL text (Foundation, UIKit.UIView — dotted kept). Kinded imports (import class Darwin.FILE — anon class child) and @testable import (modifiers first — find skips it) still find the identifier (probed). No identifier → null → because the hook exists, tree-sitter.ts:3350 fires → nothing emitted (no generic fallback; not constructible in valid swift). handledRefs never set → generic imports ref {from file node, name = moduleName, line/col of the import node} (tree-sitter.ts:3183-3194). None of the language-gated binding emitters (TS/py/rust/php/ruby, :3197-3234) match swift.

Hooks ABSENT (the walker must NOT do these): preParse, isConst (but kinds still split let/const — via the swift-specific branches, not the hook), isExported (undefined on every node except the file node's literal false and extractVariable's ?? false), isStatic IS present (above), getReceiverType (never diverts extractFunction at :1522; methods happen ONLY via isInsideClassLikeNode; no receiver QN override, no :1799 owner-contains fallback), resolveBody, recoverMangledName, isMisparsedFunction, classifyMethodNode, extractPropertyName, propertyTypes, fieldTypes, extraClassNodeTypes, packageTypes/ extractPackage (no namespace node — top-level QNs are bare), extractModifiers, synthesizeMembers, extractBareCall, visitNode hook, skipBodilessClass (but extractStruct/extractEnum have their own bodiless skip — §class-family), methodsAreTopLevel, interfaceKind (protocols are kind interface, NOT protocol), resolveTypeAliasKind.

Registration: EXTRACTORS.swift (languages/index.ts:54), FN_REF_SPECS.swift (function-ref.ts:392).

tree-sitter.ts branches (anchors as of a6c62d7)

visitNode dispatch — what each swift node hits

Node Branch Behavior
function_declaration (top level) functionTypes:994 → extractFunction:1517 no receiver hook → always extractFunction at file scope
function_declaration (inside class-like) :995 → extractMethod:1737 methodTypes includes it + isInsideClassLikeNode (nodeStack top kind ∈ class/struct/interface/trait/enum/module, :1486)
class_declaration (class/struct/enum/actor/extension — ALL of them) classTypes:1005 → classify → extractClass:1679 / extractStruct:1869 / extractEnum:1914 §class-family
protocol_declaration interfaceTypes:1054 → extractInterface:1834 kind interface; §class-family
typealias_declaration typeAliasTypes:1071 → extractTypeAlias:2890 §Type aliases. Returns false → children ALSO recursed (nothing matches — user_type/function_type have no branches)
property_declaration (top level) variableTypes:1098 (!isInsideClassLikeNode()) → extractVariable:2538 swift branch :2851 §extractVariable. skipChildren=true + scanFnRefSubtree — top-level initializers are NEVER walked: no calls/instantiates from them (probed: var topVar = compute() emits nothing)
property_declaration (inside class-like) DEDICATED swift branch :1121-1193 §Dedicated property branch. The :1098 gate fails first (isClassScopeConstantAssignment:1508 needs node.type assignment → false)
protocol_property_declaration same dedicated branch (:1123) protocol pushed = interface = class-like ✓
constant_declaration variableTypes DEAD — node type doesn't exist
import_declaration importTypes:1209 → extractImport:3170 hook (§Extractor config)
call_expression (top-level statement / class-body initializer descent) callTypes:1248 → extractCall:3684 §extractCall. skipChildren stays false → nested calls recursed
init_declaration / deinit_declaration / subscript_declaration NO branch recursed by the generic loop :1295. No node minted. Their body statements are visited by visitNode (NOT visitFunctionBody) with the CLASS still on the stack → their calls attribute to the CLASS node (probed: init { setupMonitor() }calls setupMonitor from=class; deinit's cleanup, subscript accessor bodies same). Consequences of the visitNode route: fn-ref capture fires (:990) but extractStaticMemberRef does NOT (body-walker-only, :5218) — a Color.red read inside init emits NOTHING
enum_entry (via extractEnum's body loop only) §class-family
associatedtype_declaration, protocol_function_declaration, macro_invocation (NEW), directive, diagnostic, operator_declaration-family, precedence_group_declaration no branch recursed, nothing extracted. Protocol method requirements mint NO nodes (probed — func didFinish(_:)/static func build() in a protocol produce nothing; only protocol_property_declarations become nodes via the dedicated branch)
navigation_expression (top level / class body) no branch in visitNode recursed. Static-member refs come ONLY from the body walker + walkAttrArgs — a top-level Type.self statement emits nothing
INSTANTIATION_KINDS (:354) contains NO swift node typesFoo() is a plain calls Foo ref via extractCall; NO instantiates refs exist for swift, ever (probed)

Node creation, IDs, order

  • createNode (:1308): id = generateNodeId(filePath, kind, name, startRow+1) = `${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex.slice(0,32)}` (tree-sitter-helpers.ts:18). 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) — same-(kind,name,line) collisions are legal.
  • endLine extension via resolveBody (:1329) — no hook → no-op.
  • contains edge from nodeStack top for every created node (:1363).
  • qualifiedName = nodeStack names joined :: (buildQualifiedName:1447; namespacePrefix always empty outside C/C++). No package node → top-level QN = bare name; members Session::request. Extension members ride the extension's class node name (Builder::done for extension KF.Builder).
  • extractModifiers hook absent → no decorators-from-modifiers merge (:1355 no-op); the decorators node field stays unset (probed dec=null everywhere) — decorator refs are separate unresolved refs.
  • Emission order (pinned by the torture probe, svy-swift/extract-probe.out): file node → source-order walk (per construct: node + contains edge → its refs in extractor-internal order) → fn-ref refs (flushFnRefCandidates :538) → value-ref EDGES (flushValueRefs :539, appended to edges LAST). Per class: class node → extends refs (one per inheritance_specifier, source order) → (extractCsharpPrimaryCtorParamRefs no-op — needs parameter_list child type, absent) → decorates refs of the class → body members in source order. Per dedicated-branch property: property/field/constant/variable node → decorates (owner) → type-annotation refs (owner) → walkAttrArgs static-member refs (owner) → then (stored only) initializer-descent refs. Per method: node → type-annotation (return-type) refs → decorates → body refs.

The class family — extractClass / extractStruct / extractEnum / extractInterface

classify → 'struct': extractStruct (:1869) — body = getChildByField(node, 'body') (class_body); bodiless → NO node (:1876; record_declaration exemption is C#-only; a bodiless struct isn't valid swift anyway). Node kind struct {docstring, visibility, isExported:undefined}. extractInheritance → §below. NO extractDecoratorsFor@main struct MyApp emits NO decorates ref (probed; the asymmetry: class YES, struct/enum/interface NO). Body namedChildren visited with the struct pushed.

classify → 'enum': extractEnum (:1914) — body field (enum_class_body); bodiless → no node. Kind enum {docstring, visibility, isExported:undefined}. extractInheritance (the raw-value type rides it — enum Suit: Stringextends String; extends refs have NO builtin filter, probed extends Int). NO decorates. Body loop: enum_entry ∈ enumMemberTypes → extractEnumMembers (:1958): name field path — getChildByField(node,'name') returns the FIRST case name → ONE enum_member node, positioned at the enum_entry, then RETURN. QUIRKS, PRESERVE (probed): multi-case case put, delete → ONLY put (delete DROPPED — the identifier-scan fallback :1967 is dead for swift since the field always exists); associated values (case success(Data)data_contents: enum_type_parameters) and raw values (= "H") are never walked → NO refs from them; indirect case same. Non-enum_entry children (computed properties, methods, nested types) → visitNode with the enum pushed.

classify → 'class' (incl. actor + extension): extractClass (:1679) — no skipBodilessClass → a bodiless class_declaration would still mint (not valid swift). Kind class {docstring, visibility, isExported:undefined}. extractInheritance; extractCsharpPrimaryCtorParamRefs no-op; extractDecoratorsFor DOES run (@Observable class Modeldecorates Observable). Push, visit body namedChildren via visitNode, no synthesizeMembers, pop. Extensions: name via resolveName (multi-segment) else raw name-field text — extension Point → class Point (a SECOND class node named like the original — same name, different line → distinct id); extension [ServerTrustEvaluating] → class named literally [ServerTrustEvaluating] (raw sugar text, probed); extension Array where Element: Equatable → class Array, and the type_constraints where clause emits NOTHING (inheritance_constraint is not an inheritance_specifier).

extractInterface (:1834) — protocol_declaration → kind interface (no interfaceKind hook) {docstring, isExported:undefined — NO visibility, NO decorates}. extractInheritance on the protocol node (protocol inheritance : AnyObject, Identifiable → extends refs). Body (protocol_body) namedChildren visited with the interface pushed: protocol_property_declaration → dedicated branch (§below); protocol_function_declaration / init_declaration / associatedtype_declaration → recursed, nothing.

Inheritance — extractInheritance (:5291), the swift case (:5619-5632)

Direct namedChildren scan of the declaration node; ONLY the inheritance_specifier case matches swift (one specifier per conformance, each inherits_from: user_type): userType = child.namedChildren find(type==='user_type') → typeId = ITS namedChildren find(type==='type_identifier') — the FIRST — → one extends ref {from class/struct/enum/interface node, name = typeId text, line/col of the TYPE IDENTIFIER}. QUIRKS, PRESERVE: everything is extends — protocol conformance, protocol inheritance, raw-value types; swift emits NO implements refs from extraction (resolution's supertype machinery handles both kinds); a qualified base : Module.Base takes the FIRST segment (Module) — the outer namespace, not the type. Where-clause constraints and generic bounds (type_parameter name: bounds) emit nothing. None of the other clause cases (extends_clause/base_class_clause/delegation_specifier/…) match swift node types.

THE DEDICATED PROPERTY BRANCH (tree-sitter.ts:1113-1193) — #1020, the port's core

Fires for property_declaration | protocol_property_declaration when language === 'swift' AND isInsideClassLikeNode(). Position in the else-if ladder: AFTER variableTypes (:1098 — which in-class properties fail, see dispatch table), BEFORE importTypes. Alamofire baseline (re-measured at survey HEAD on the production path): 98 files → 3,988 nodes = method 1801 / field 588 / class 379 / property 348 / enum_member 230 / import 183 / struct 111 / file 98 / constant 97 / enum 77 / type_alias 39 / interface 27 / function 10; 16,426 refs. The 348 property nodes are exactly this branch's computed path — the #1020 number reproduced to the digit.

swiftPropertyInfo (tree-sitter.ts:277-291) — transcribe exactly

pattern  = childForFieldName('name')
        ?? namedChildren.find(t === 'value_binding_pattern' || t === 'pattern')
        ?? null
binding  = namedChildren.find(t === 'value_binding_pattern')      // DIRECT children only
isLet    = binding != null && text(binding).trimStart().startsWith('let')
isComputed = namedChildren.some(t === 'computed_property' || t === 'protocol_property_requirements')
nameNode = firstSimpleIdentifier(pattern)

firstSimpleIdentifier (:261-273): BFS (shift from a queue) over namedChildren, guard: at most 40 nodes popped; first simple_identifier wins. Tuple pattern let (a, b)a only (one node for the whole declaration, PRESERVE).

Shape notes (probed, identical both grammars):

  • class/struct/enum property_declaration: value_binding_pattern (mutability let/var) and name: pattern > bound_identifier: simple_identifier are SIBLING direct children → binding found → isLet real.
  • protocol_property_declaration NESTS the value_binding_pattern INSIDE the name: pattern → the direct-children find MISSES it → binding null → isLet always false for protocol requirements (irrelevant today — they are all computed — but transcribe the miss).
  • computed_value: computed_property carries the getter; the code matches by TYPE not field.
  • willSet/didSet observers are a willset_didset_block, NOT a computed_property → an observed stored property (var observed: Int = 0 { willSet {…} didSet {…} }) is isComputed=false → a field node, and (stored-path descent, below) the observer bodies' calls attribute to the CLASS (probed: calls prepare / calls react from=class).

The branch body (:1126-1193), step by step

  1. ownerId = nodeStack top (the class/struct/enum/interface node).
  2. If nameNode:
    • isComputed → createNode('property', name, node, {visibility: hook, isStatic: hook ?? false}) — computed properties become property nodes (the #1020 "var isCloudProxy: Bool" fix; SwiftUI var body: some View is the canonical case). Note extras: NO docstring, NO signature, NO returnType — visibility + isStatic ONLY (probed doc=null on all).
    • else **stored: isStatic = hook ?? false; kind = isStatic ? (isLet ? 'constant' : 'variable') : 'field'** —static letconstant, static varvariable(class var→ isStatic via 'class' substring →variable), instance stored (let OR var) →field`. Same extras.
    • Node position = the property_declaration (multi-line observed props span the block). nameNode null (no simple_identifier in pattern) → NO node, but steps 3-5 still run.
  3. If ownerId: extractDecoratorsFor(node, ownerId)refs attach to the ENCLOSING TYPE, not the property (probed: @Published private var wrappeddecorates Published from=class:Session). §Decorators for mechanics.
  4. If ownerId: extractVariableTypeAnnotation(node, ownerId) (:6074) — finds the direct type_annotation child → extractTypeRefsFromSubtree (:6090) → one references ref per type_identifier leaf not in BUILTIN_TYPES, from the OWNER (probed: let rootQueue: DispatchQueuereferences DispatchQueue from=class at the leaf's position). BUILTIN_TYPES nuances (:5768): Int/String/Double/Float/Boolean/Long/Char are suppressed (Scala rows), Bool is NOT (→ real references Bool noise ref, probed), Void/Character/Self NOT. Wrappers recurse: [Tag]Tag; Session?Session; some View (opaque_type) → View.
  5. If ownerId: walkAttrArgs (:1165-1175) — find the modifiers direct child; if present, recursively run extractStaticMemberRef(n) over EVERY node of the modifiers subtree. This is the Vapor @Siblings(through: PivotModel.self, from: \.$left, …) mechanism: the attribute-argument navigation_expression with target simple_identifier PivotModel + suffix .selfreferences PivotModel from the OWNER at the RECEIVER's position. Keypath args (\.$left — target is key_path_expression) and the wrapper's own user_type are self-filtered (not MEMBER_ACCESS_TYPES / not an accepted recv type). §Static-member refs for the shared mechanics.
  6. Computed only: getter = namedChildren find(computed_property | protocol_property_requirements); if found: push property id, visitFunctionBody(getter, ''), pop; skipChildren = true. The getter's calls/reads attribute to the PROPERTY node (probed: calls check from=property:isCloudProxy; SwiftUI body's VStack/Text subtree becomes the property's callees). protocol_property_requirements contains only getter/setter specifiers → walks emit nothing.
  7. Stored: skipChildren stays FALSE → the generic loop (:1295) descends into the property_declaration's children via visitNode: the initializer's call_expressions hit callTypes → calls refs from the CLASS (let stored = Session()calls Session from=class; lazy var expensive = Cache.build()calls Cache.build from=class); willset/didset bodies likewise; the modifiers subtree is re-walked harmlessly (attributes have no visitNode branches; walkAttrArgs already emitted the static refs — different ref kinds, no dupes). Value-position navigation reads (let m = Suit.hearts as an in-class initializer) emit NOTHING here (no static-member call in visitNode) — contrast bodies.
  8. fn-ref capture: maybeCaptureFnRefs ran at :990 BEFORE the branch — property_declaration ∈ SWIFT_SPEC dispatch (varinit field value) → a stored let cb: Handler = onFire captures onFire; computed properties have no value field → nothing.

extractVariable — the swift top-level branch (tree-sitter.ts:2851-2862)

Top-level property_declaration only (dispatch table). swiftPropertyInfo again: nameNode && !isComputed → createNode(isLet ? 'constant' : 'variable', name, NODE, {docstring, isExported})position = the whole declaration, extras: docstring (getPrecedingDocstring — top-level consts DO get docstrings, unlike the in-class branch), isExported = ?? false → literal false (:2549; probed exp=false — the only swift nodes besides the file node with a non-undefined isExported), NO signature (unlike the TS/Go branches — no initializer capture), no visibility/isStatic. Computed top-level (var topComputed: Int { 9 }) → skipped entirely (no node). One node per declaration — tuple let (a, b) = …a only. The :1098 dispatch sets skipChildren=true + scanFnRefSubtree (capture-only, halts at functionTypes + lambda_literal :610) → top-level initializers emit NO calls/instantiates refs (probed: var topVar: Int = compute() → nothing but the node; no type-annotation refs either — extractVariableTypeAnnotation is NOT called on this path, PRESERVE the asymmetry with the in-class branch).

extractFunction / extractMethod for swift (:1517 / :1737)

  • extractFunction: no getReceiverType → never diverts. Name via extractName (:90): resolveName (class_declaration-gated → undefined for functions) → nameField name → the simpleidentifier. Operator functions: func <+> (lhs:…) has name: custom_operator → extractName returns the raw operator text → a function node named <+> (probed clean-parse on both arms). <anonymous> never occurs (grammar requires a name; the arrow/function expression paths are TS-only).
  • Node extras: docstring (§Docstrings), signature undefined (dead hook), visibility (hook), isExported undefined, isAsync false (dead hook), isStatic (hook — real for static/class modifiers), returnType (hook).
  • Then extractTypeAnnotations (:1594) — §Type-annotation refs: return-type refs ONLY. Then extractDecoratorsFor (:1599) — attributes inside modifiers → real decorates refs (@objc func attributeddecorates objc, position = the attribute node). Push, walk body field (function_body) via visitFunctionBody, pop.
  • extractMethod (function_declaration inside class-like): gate :1747 passes via isInsideClassLikeNode. Same extras (visibility/isAsync-false/isStatic/ returnType, signature undefined). receiverType undefined → no QN override (:1790 skipped), no owner-contains fallback (:1799). Bodiless is impossible for function_declaration in valid swift (protocol requirements are a different node type, unhandled).
  • Nested named function in a body → visitFunctionBody:5245 → functionTypes + named → extractFunction → function node contained by the enclosing method/function (its QN prefixes the enclosing chain via nodeStack). Closures (lambda_literal) are NOT functionTypes → no nodes; their calls attribute to the enclosing symbol (probed: rootQueue.async { self.perform(req) }calls perform from=method:request).
  • Body-level class/struct/enum/protocol declarations → visitForCallsAndStructure :5255-5275 dispatches classTypes (with classify)/interfaceTypes → full extraction, contained by the enclosing function.

Type aliases — extractTypeAlias (:2890)

typealias_declaration → name field (alias name). No resolveTypeAliasKind → plain type_alias node {docstring, isExported:undefined}. swift ∈ TYPE_ANNOTATION_LANGUAGES → value = getChildByField(node,'value')the multi-field lookup WORKS (table above) → extractTypeRefsFromSubtree over the aliased type → refs per type_identifier leaf (probed: typealias Handler = (Data) -> Voidreferences Data + references Void; typealias BuilderAlias = KF.Builder → refs KF AND Builder — one per segment). The TS-only member extraction (:2983) is gated away. Returns false → children recursed (nothing matches).

extractCall (:3684) — the swift paths

Swift never hits the vbnet/erlang/objc/php/java branches (no name+object fields on call_expression). Generic else :4312: func = childForFieldName('function') ?? namedChild(0) — swift has NO function field → namedChild(0) always. cpp operator recovery (:4324) is cpp-gated. The full observed matrix (every row probed via the built extractor):

Call shape func node Path Emitted ref
helper() simple_identifier else :4518 raw text calls helper
Foo() simple_identifier same calls Foo (constructor = plain call; NO instantiates, ever)
Foo.init(raw:) navigation_expression :4364 member branch calls Foo.init
obj.method(1) navigation_expression member calls obj.method
self.own() nav, target self_expression receiver not identifier-typed → fallthrough calls own (bare — swift self is its own node TYPE; SKIP_RECEIVERS:4400 {self,this,cls,super} is reached only for identifier-typed receivers, so the same net effect arrives via a different path than kotlin)
super.parent() nav, target super_expression same calls parent
a.b.deep() nav, target inner nav fallthrough calls deep (bare — 2-hop receivers drop)
x?.optCall() nav, targets [simple_identifier x, anon ?] receiver = namedChild(0) = x calls x.optCall (optional chaining keeps the receiver — the ? is a second anon target: child, invisible to namedChild)
y!.forced() nav, target postfix_expression fallthrough calls forced
Foo.make().draw() nav, target call_expression #750 re-encode :4408-4442: swift-gated; innerNav = receiver.namedChild(0) (Foo.make nav or bare identifier), text ws-stripped; /^[A-Z]/ gate calls Foo.make().draw + (recursion) calls Foo.make
foo.bar().baz() same innerCallee foo.bar lowercase → reencode false calls baz + calls foo.bar
"lit".upper() nav, target line_string_literal LITERAL_RECEIVER_TYPES :4397 (line_string_literal ∈ set :377) NOTHING
"""…""".trimmed() nav, target multi_line_string_literal NOT in the literal set calls trimmed (bare) — PRESERVE
5.times() / [1,2].reduce(…) integer_literal / array_literal literal set NOTHING
["k":1].lookup() dictionary_literal NOT in the set (dictionary is, dictionary_literal isn't) calls lookup — PRESERVE
(freestanding)() tuple_expression else raw text (freestanding)parenthesized-conversion regex :4530 FIRES calls freestanding
closureTaking { } / multi-trailing simple_identifier (lambda in call_suffix) else calls closureTaking
arr[0] (subscript READ) simple_identifier (bracket call_suffix) else calls arr — every subscript access parses as call_expression; huge-volume quirk, PRESERVE
m[i][j] inner call_expression receiver is call but func = call_expression not nav → else raw text calls m[i] (garbage, PRESERVE)
self.items[k] = 1 nav self.items member, self receiver calls items
defer { cleanup() } simple_identifier defer else calls defer + inner calls cleanup — defer parses as a trailing-closure call (both arms, probed)
try f() / try? f() / await f() (wrapped in try_expression/await_expression) recursion reaches the inner call_expression normal ref, position = the CALL node (after the keyword)
"count \(counter.next())" (interpolated_expression inside the literal) recursion descends into string literals calls counter.next — interpolation calls DO emit
.make() (implicit member call) prefix_expression else raw text calls .make (leading dot, unresolvable — PRESERVE)
macro #Preview { ContentView() } (NEW) (macro_invocation unhandled) recursion inner calls ContentView from the enclosing scope

Post-processing: parenthesized-conversion (:4529) as noted; template-strip (:4542) and cpp fn-ptr fan-out (:4556) are c/cpp-gated. Final ref: {callerId = nodeStack top, name, line = call startRow+1, column = call startColumn (UTF-16)}. Inner nav/call children are ALSO visited after extractCall (callTypes doesn't skipChildren; body walker recurses) — chains emit inner refs, and the callee-position navigation_expression is exempted from static-member refs by the :4771 callee-of-call check.

Static-member / value-read refs (:4750-4808) — swift IS in STATIC_MEMBER_LANGS (:345)

Called from the body walker ONLY (:5218) plus the property branch's walkAttrArgs. navigation_expression ∈ MEMBER_ACCESS_TYPES (:326). Mechanics for swift: callee-of-call skip (:4771-4779 — parent ∈ callTypes and parent.namedChild(0).startIndex === node.startIndex → the callee nav of a call emits nothing); recv = object/expression/scope fields (all null for swift — nav's fields are target/suffix) → namedChild(0) = the target. Accepted recv types (:4791): identifier/type_identifier/simple_identifier/ name/scoped_type_identifier — swift targets are simple_identifier → capitalized regex ^[A-Z][A-Za-z0-9_]*$references <target> from the enclosing symbol at the RECEIVER's position. Probed rows: Color.redreferences Color; Suit.hearts.rawValue → outer nav recv = inner nav → nothing, inner nav → references Suit; UserModel.selfreferences UserModel; Deep.Nested.leafreferences Deep (inner nav only); .implicitMember → prefix_expression, not a nav → NOTHING; lowercase.field → nothing; keypath \Foo.bar → recv = key_path_expression → nothing (the type_identifier inside the keypath is never read). Remember the visitNode gap: class-level initializer reads and init/deinit/subscript bodies emit NO static-member refs (dispatch table).

Type-annotation references — swift ∈ TYPE_ANNOTATION_LANGUAGES (:5753)

For every function/method (extractTypeAnnotations :5788, called at :1594/:1816): params = getChildByField(node, 'parameter')NULL → NO parameter type refs, ever (probed: @escaping (Int) -> Void param emits nothing — Void would have shown). returnType = getChildByField(node, 'return_type')WORKS (field table) → extractTypeRefsFromSubtree over the return node → one references per type_identifier leaf not in BUILTIN_TYPES: -> DataRequest → ref; -> KF.Builder → refs KF AND Builder; -> Session?Session; -> Result<Foo, Err>Result, Foo, Err (leaves); builtins suppressed per the §property-branch nuance list (Bool NOT suppressed). The trailing direct type_annotation find (:5873) is null for function_declarations. extractVariableTypeAnnotation (:6074): the dedicated property branch (owner-attributed) and the body walker's variable_declarator gate (:5230-5236 — dead for swift, no such node type; body-local let x: Foo property_declarations emit NO type refs — PRESERVE). property_signature/method_signature (:1282) — TS-only types.

Decorators — extractDecoratorsFor (:4897), swift = attribute nodes

consider() (:4898) accepts type attribute; the target scan (:4937) finds the attribute's first namedChild of accepted types — swift attribute → user_type (:4950) → name = user_type text, strip <…> generic suffix, strip to last ./:: segment, trim → decorates ref {from the decorated node id (or the OWNER for the property branch), name, line/col of the ATTRIBUTE node}. Attributes live INSIDE the declaration's modifiers child → found via the modifiers descent (:4983); the preceding-sibling scan (:5013) finds nothing for swift (attributes are never siblings) and stops at the first non-decorator sibling. Coverage matrix (probed): function ✓ (@objcdecorates objc), method ✓, class ✓ (@Observable), dedicated-branch property ✓ (owner-attributed) — struct/enum/interface ✗ NO decorates (extractStruct/extractEnum/extractInterface never call it — @main struct, @objc enum, attributed protocols emit nothing, probed). Attribute ARGUMENTS are never walked here — only walkAttrArgs (property branch) reads them, and only for static-member shapes. Parameter attributes (@escaping etc. inside parameter/parameter_modifiers) are never reached by any pass.

Docstrings (tree-sitter-helpers.ts:95)

/// doc lines AND // plain comments are node type comment — accepted by the sibling scan; consecutive runs accumulate (unshift → source order) and // vs /// are NOT distinguished (a plain comment right above a decl IS its docstring). /** */ and /* */ block comments are multiline_comment — NOT in the accepted set (comment/line_comment/block_comment/ documentation_comment) → block docs are IGNORED and BREAK the accumulation chain (probed: /** block doc */ func blockDoc → doc=null). PRESERVE. DOCSTRING_WRAPPER_TYPES: no swift wrappers → no climbing. Attributes do NOT break the chain (they're inside the declaration node — /// doc + @objc func keeps its doc, probed). cleanCommentMarkers (:77): swift hits ^\/\/[/!]?\s? (gm) and — only if a multiline_comment ever reached it, which it can't — the block strips. The gm per-line strips are the ONLY CRLF-sensitive regexes on the swift path (#1329): use js_multiline_strip in docstring.rs. Docstrings attach to: functions, methods, classes (incl. extensions/actors), structs, enums, interfaces, top-level constants/variables (extractVariable), type_aliases. NOT to: dedicated-branch properties/fields/constants (extras carry no docstring — probed), enum members, import nodes.

Value-reference edges (:398-931) — swift IS in VALUE_REF_LANGS (:401)

Port the full machinery (crib go.rs/java.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 ≥3 chars AND /[A-Z_]/, parent id prefix ∈ {file:, class:, module:, struct:, enum:} — the struct:/enum: rows exist FOR swift's static-let-namespacing idiom (enum Constants { static let X }, comment at :748-750). Swift targets: top-level let/var (under file:) and in-class static let/static var (constant/variable under class:/struct:/enum:). interface: NOT accepted → protocol statics (hypothetical) excluded. Instance fields and computed propertys are NOT targets.
  • Reader scopes (:764): function/method/constant/variable nodes — property and field are NOT readers (probed: isCloudProxy's read of SHARED_MAX emits no edge; method request's does).
  • Shadow prune (:803-878): the declarator switch's property_declaration case (:856-869) RESOLVES for swift (unlike php): vd = find variable_declaration (Kotlin) — absent for swift → else firstSimpleIdentifier(childForField('name') ?? find(value_binding_pattern | pattern)) — the swift shape → bump() counts it when the text is a target name (bump accepts identifier|simple_identifier :807). EVERY property_declaration in the tree bumps — targets' own declarations, body locals (let SHARED_MAX = 5 in a method — the shadow source), and same-named instance properties. declCount > fileScopeCount → target deleted. guard-let/if-let bindings do NOT bump (guard_statement/ if_statement hold bound_identifier directly, no property_declaration — probed) → a guard let X = … shadow does NOT prune. PRESERVE both sides.
  • Emission (:880-930): per reader scope DFS (bodies are children — the Dart/Pascal sibling pull :891 is inert); match node type simple_identifier (:908 — swift's every name reference) whose text maps to a target, target ≠ self, name ≠ scope's own name, dedupe per (scope,target) → EDGE {source: scopeId, target, kind:'references', metadata:{valueRef:true}}, appended AFTER all other edges. Because every simple_identifier matches — nav suffixes included — Session.SHARED_MAX AND a bare SHARED_MAX both emit (probed: the single torture valueRef edge method:request → constant:SHARED_MAX).

Function-as-value capture (#756) — SWIFT_SPEC (function-ref.ts:288)

idTypes:  { simple_identifier }
dispatch: value_arguments        → args
          assignment             → rhs   (field 'result')
          array_literal          → list
          property_declaration   → varinit (field 'value')
layers:   value_argument → 'value'        // FIELD 'value', not null
special:  { selector_expression }

No unwrap/ungatedModes/addressOfOnly. Mechanics (function-ref.ts:408-597 + :685-696), all probed:

  • args/list: every namedChild → normalizeValue. Bare simple_identifier → candidate (NAME_STOPLIST drops self/true/nil/…). reg2(onFire)onFire ✓; [cbA, cbB] → both ✓ (the array_literal's OWN dispatch — varinit's normalizeValue over an array_literal yields [], the capture happens when the walker/scan VISITS the array node).
  • value_argument layer (:547-557): the label-forward skip — label = childForField('name') (a value_argument_label), value = childForField('value') ?? last namedChild; label text === value text → DROPPED (forward(value: value) → nothing; the Alamofire A/B finding). Else descend the value field.
  • rhs (assignment, field result): param-storage skip — lhs = left/lhs/target fields ?? namedChild(0) (swift's field is target, a directly_assignable_expression); lhs text's last identifier === rhs text → skip (self.raw = raw → nothing, probed). o.cb = handlerhandler ✓.
  • varinit (property_declaration, field value): destructuring check reads childForField('name') — swift's is type pattern, NOT in {object_pattern, array_pattern, tuple_pattern, struct_pattern} → never skipped; tuple-let values are non-normalizable expressions anyway. Computed properties have no value field → nothing.
  • selector_expression special (:685-696): namedChild(0) is identifier/simple_identifier → its text (#selector(fire)fire); else rightmost simple_identifier descendant (#selector(Holder.fire)fire; QUIRK: #selector(onNote(_:)) → the rightmost simple_identifier is the argument label _ → candidate _, dropped by the gate in practice); else trimmed inner text. explicitRef=true (not an idType).
  • Capture fires from visitNode:990 (incl. BEFORE the dedicated property branch and on init/deinit/subscript descents), visitFunctionBody:5137, and scanFnRefSubtree (top-level initializer scan; halt list :606-612 includes lambda_literal — closures halt the scan — and functionTypes).
  • Flush gate (:639): definedHere (same-file function/method names) ∪ importedNames. Swift import refs are module names — Foundation (SIMPLE) and UIKit.UIView (QUALIFIED_IMPORT → last segment UIView) both land in importedNames but almost never match a candidate → the swift gate is effectively "defined in this file" (probed: #selector(Holder.fire)'s fire dropped; onFire/handler/cbA/cbB survive as same-file functions). Survivors dedupe on ${fromNodeId}|${name} → {referenceKind:'function_ref'}, appended after all walk refs.

Closure-collection pass & other synthesis consumers (no port — pin the contract)

  • swift IS in CC_LANGUAGES (resolution/callback-synthesizer.ts:77, with kotlin — the #1235 gate). The pass re-reads function/method nodes' filePath + startLine..endLine source slices (callback-synthesizer.ts:270- 297) and regexes for .forEach/.append(/.add(/.push(/.insert( dispatch/registration pairs. Input contract on the walker: node kinds, languages, and LINE EXTENTS byte-match — a wrong endLine silently changes synthesis. The dump gate covers this; nothing extra to build.
  • swift-objc bridge (swiftObjcBridgeResolver) + the sendEvent scan (callback-synthesizer.ts:1387-1438) read raw .swift file text — file-set contract only.
  • The :811 and :3507 language gates consume node kinds/names — covered by parity.

Frameworks (stay TS-side — behavior pinned in §Architecture #2)

swiftUIResolver / uikitResolver / vaporResolver (resolution/frameworks/ swift.ts:11/:136/:269): extract() hooks emit component/class/route nodes with LITERAL ids (view:${filePath}:${name}:${line}, route:${filePath}:${line}:${METHOD}:${path} — NOT hashed) + vapor handler refs (which DO carry filePath+language — framework refs, not extraction refs). resolve() consumes plain references names by suffix + kind + dir conventions (getNodesByName — walker's node names/kinds are the contract). Vapor's @Siblings metatype refs (§property branch step 5) are the extraction-side feed that keeps pivot models un-orphaned on Fluent repos.

Parity mechanics (all have bitten before)

  • Emission order per §Node creation — file → source-order walk → fn-refs → value-ref edges. Refs interleave with nodes exactly as the TS call sites do: extends before members; per-property decorates → type-refs → attr-arg refs → initializer-descent refs; per-function/method the return-type refs come FIRST (extractTypeAnnotations :1594/:1816 runs before extractDecoratorsFor :1599/:1819), then decorates, then body refs.
  • generateNodeId inputs: (filePath, kind, name, startRow+1) — name is the bare bound identifier for properties (no pattern text), the LAST segment for multi-segment extensions, raw sugar text ([ServerTrustEvaluating]) for sugar extensions, <+> for operator functions, the full dotted module (UIKit.UIView) for import nodes; line = declaration start (= the modifiers/attribute start when attributes precede the keyword — the declaration node INCLUDES its modifiers).
  • UTF-16 columns + slices (textutil::col16/slice_utf16): every ref/node column, startIndex/endIndex substrings (getNodeText — getVisibility/ isStatic read child.text, same substring), and the .slice(0,100)-class truncations (unused on the swift path — no signature/initializer capture). Swift sources are emoji/CJK-heavy in tests — the torture fixture needs a non-ASCII line before a symbol.
  • CRLF: the ONLY multiline regexes on the swift path are cleanCommentMarkers' gm strips (§Docstrings) → js_multiline_strip in docstring.rs. trimStart()/startsWith('let') in swiftPropertyInfo are whitespace-semantics-identical in Rust (trim_start). CRLF fixture variants derived in-memory, per the tsjs pattern.
  • Defer policy: per-file has_error()defer:; expected incidence 9–27% (§table); sweep with --max-deferral 0.3 — the c/cpp-style exemption, justified by both-arm measurement, NOT a walker allowance: a JUMP vs the table is a walker bug.
  • Refs carry NO filePath/language (§Architecture #4 — REF_FLAG_FILE_PATH unused); the wire contract is exactly extractFromSource's return.
  • No POST_PASS; no preParse; sourceIsPreParsed never set for swift.

Gates (per plan §5, no exceptions)

  • Grammar bump lands FIRST, standalone (php pattern): vendor wasm + =0.7.3 crate pin + VENDORED_WASM_LANGS + kernel-grammar-parity GRAMMAR_LANGUAGES += 'swift' in one change, full suite green, before any walker exists. Old-wasm vs new-wasm full-init dump diff on all three gate repos (scripts/dump-graph.mjs, cmp): expected NON-empty; every diffing file must be in the union of the two arms' hasError lists (regenerate with svy-swift/error-incidence.cjs; categories in §Grammar-bump deltas — macro/testing/package/typed-throws fixes, the three NEW-regression classes, both-arm recovery drift); prove residual resolution ripple mechanically by parked-ref↔edge pairing (php's ripple-proof pattern). Any clean-on-both-arms file diffing blocks the bump.
  • Torture fixtures per ## Fixtures to build (+ CRLF variants derived in-memory), exercised by the new parity suite.
  • Parity sweeps (scripts/kernel-parity.mjs <dir>, order-sensitive full-object, --max-deferral 0.3):
    • …/scratchpad/gate-repos/Alamofire (small, 98 files — the mandatory #1020 gate, decoded path)
    • …/scratchpad/gate-repos/vapor (medium, 247 files, decoded path, Fluent/@Siblings + route shapes)
    • …/scratchpad/gate-repos/swift-nio (large, 554 files, raw-buffers path — no framework detects) (all three cloned at survey time; re-clone fresh if gone). Then full-init dump-diffs byte-identical (kernel arm vs CODEGRAPH_KERNEL=0, dump-graph.mjs, cmp) on the same three.
  • Alamofire #1020 spot-check (belt to the dump gate's suspenders): after a kernel-arm index, node-kind census must match the wasm arm EXACTLY — survey baseline at a6c62d7 + current Alamofire HEAD: property = 348 (the #1020 number, re-measured), field 588, method 1801, class 379 (extensions!), constant 97, struct 111, enum 77, enum_member 230, interface 27, type_alias 39, function 10, import 183 (98 files, 3,988 nodes, 16,426 refs — svy-swift/count-props.cjs). A missing dedicated branch shows up here as property→0 / field→0 before the dump diff even runs.
  • Suite: new __tests__/kernel-swift-parity.test.ts — torture + CRLF variants + an intentionally-erroring defer fixture (use a NEW-only regression construct — e.g. #if DEBUG between enum cases — asserting the kernel defers and wasm output is served); full suite ×2 green with CODEGRAPH_KERNEL_EXPECT=1.
  • DEFAULT_ROUTED += 'swift' (kernel/index.ts:37) only after ALL of the above; changelog rides the existing kernel entry.
  • Post-route sanity: §Architecture #2 — Alamofire/vapor ride the decoded path, swift-nio the raw transport; measure the parse loop accordingly.

Fixtures to build

torture.swift (seed: the survey's svy-swift/torture.swift, already validated against the built extractor — extend it with the rows below), a CRLF variant derived in-memory, one defer fixture (a NEW-only regression construct, above), and keep every line traceable to a branch:

  • class family: plain class; public final class C: Base, Proto1, Proto2 (extends ×3, all 'extends'); struct + bodiless-skip N/A; enum backed (: String → extends String) + case a = "H" + multi-case case put, delete (ONLY put minted) + associated values (no refs) + indirect + computed property + static method in enum; actor (→ class); extension Point: Hashable (second class node, extends); extension KF.Builder (resolveName → Builder); extension Array where Element: Equatable (class Array, NO constraint refs); extension [Proto] (class named [Proto]); protocol with inheritance + var requirement {get set} (→ property node, isLet=false via the nested-binding miss) + static var requirement + func/init/associatedtype requirements (NO nodes).
  • the dedicated branch, exhaustively: stored let with type+initializer (field + owner type-ref + owner-attributed initializer call); untyped var; static let SHARED_MAX (constant, value-ref target); static var (variable); class var computed (property, isStatic via 'class'); lazy var x: Cache = Cache.build() (field + calls Cache.build from class); public private(set) (→ 'public'); @Published private var (decorates from OWNER + vis 'private'); @Siblings(through: Pivot.self, from: \.$left) var siblings: [Tag] (decorates Siblings + walkAttrArgs references Pivot + type-ref Tag, all from OWNER); computed var isCloudProxy: Bool { check() } (property + calls check FROM the property + references Bool — Bool not builtin); SwiftUI-shaped var body: some View { VStack { Text(label) } } (property; VStack/Text calls from the property; references View); observed var x = 0 { willSet{prepare(newValue)} didSet{react(oldValue)} } (field, observer calls from the CLASS); weak var delegate: SessionDelegate?; tuple let (a, b) = makePair() (ONE node a, calls makePair from class); open visibility (→ 'internal').
  • functions/methods: free func with params+return (sig=null, return-type ref, NO param refs — include a Void-bearing param type to prove the negative); -> KF.Builder (returnType 'Builder' + refs KF AND Builder); -> Result<Foo, Err> (returnType 'Result'); -> Result<Vec<Foo>, E>-shaped nested generic (returnType undefined); -> Foo? / -> [Foo] / -> (A,B) / -> (Int) -> Foo / -> Void; func f() async throws (isAsync FALSE); static func / open class func (isStatic true, vis 'internal'); operator func <+> (lhs:…) (node named <+>); nested named func in a body; @objc func (decorates objc); @main struct + @objc enum + attributed protocol (NO decorates).
  • init/deinit/subscript: init(raw:) { self.raw = raw; setupMonitor() } (no node; calls setupMonitor from class; NO fn-ref for the param-storage assignment); convenience init? (calls init bare from class); deinit; subscript with get/set (calls store ×2 from class).
  • calls: every row of the §extractCall matrix, verbatim — bare, Foo(), Foo.init(), member, self/super (bare), 2-hop (bare), x?.m() (receiver kept), y!.m() (bare), Foo.make().draw() re-encode + inner, lowercase chain (bare + inner), "lit".upper() (nothing), """m""".t() (bare t), 5.times() / [1,2].reduce (nothing), ["k":1].lookup() (bare), (f)() (conv-regex → f), trailing + multi- trailing closures, arr[0] (calls arr), m[i][j] (calls m[i]), defer {} (calls defer), try/try?/await wraps, string-interpolation call, implicit-member call .make() (calls .make), Task { await f() }.
  • static-member reads (in a BODY): Color.red, Suit.hearts.rawValue (Suit only), UserModel.self, Deep.Nested.leaf (Deep only), .implicit (nothing), lowercase.f (nothing), keypath \Foo.bar (nothing) — plus the SAME reads at class-initializer scope and inside init (nothing — the visitNode gap).
  • value refs: static let SHARED_MAX in a class/struct/enum read from a method (edge) and from a computed property (NO edge — property not a reader); a method-local let SHARED_MAX = 5 in a SECOND fixture variant (prune kills the target); a guard let SHARED_MAX variant (NO prune); top-level let TOP_LEVEL_MAX + reader.
  • fn-refs: reg(onFire), reg(cb: onFire) (label≠value → survives), forward(value: value) (label-forward skip), o.cb = handler, self.x = x (param-storage skip), let table = [cbA, cbB], #selector(fire) / #selector(Holder.fire) / #selector(onNote(_:)) (the _ gate-drop), an undefined-name arg (gate-drop).
  • imports: import Foundation, dotted import UIKit.UIView, kinded import class Darwin.FILE, @testable import.
  • typealias: function-type value (refs Data+Void) + member-type value (refs KF+Builder).
  • docstrings: /// run (joined), // plain (IS a docstring), /** */ (ignored), /* */ between /// and decl (breaks chain), doc over an attributed decl (kept), doc on top-level const (kept) vs in-class property (dropped).
  • misc: #if os(iOS) around members (both branches extracted), #warning, a macro_invocation file (NEW arm; on OLD this file is a defer candidate — keep it in the DEFER fixture instead), a non-ASCII (UTF-16) line before a symbol, guard/if-let/for-await bodies with calls.

Probe artifacts (session scratchpad svy-swift/)

probe-swift.cjs + shape-{OLD,NEW}.txt + shape.diff (the 53-line classified battery diff), mini-probes.cjs + mini-probes.out (node-type inventory, per-construct error matrix, defer/subscript/#if/async-let/regex shapes), field-probe.cjs (the childForFieldName truth table), regress-probe.cjs (NEW-regression minimal repros), error-incidence.cjs (the §incidence table + per-file lists), error-diag.cjs (first-ERROR diagnosis used for the delta classification), extract-probe.cjs + torture.swift + extract-probe.out (the built-extractor emission pin), sugar-probe.cjs (extension sugar/decorates asymmetry/implicit-member call), count-props.cjs (the Alamofire census), tree-sitter-swift.wasm (the staged-candidate 0.7.3 build), tree-sitter-swift-0.7.3/ (crate extract) + tag-clone/ (0.7.3-with-generated-files) with the sha evidence.