Fifth R7b batch-3 port, checklist-first recipe (docs/design/swift-kernel-port-checklist.md, 1,056 lines — the largest of the arc, with a built-extractor-validated emission pin and a childForFieldName truth table). Grammar bump first, validated standalone: tree-sitter-wasms ^0.4.0 (ABI 13) → crate 0.7.3 — with a provenance twist: the wasm is built from the CRATE TARBALL's src/ (alex-pinkus keeps generated files off main and the 0.7.3-with-generated-files tag ships an older ABI-14 generation that can never sha-match; grammar.json rules are JSON-equal; the tarball is byte-for-byte what the kernel's cargo build compiles — table identity by construction). Older crates evaluated and rejected: clean-parse shapes are byte-identical on 0.7.3 (53-line CST battery diff, all inert), so an older pin buys nothing and loses the macro-era wins. Delta = error-set membership (63 old-error files parse clean: swift-testing #expect, #Preview/#GET macros, package access, typed throws — vapor 23.1%→9.3%; 21 NEW-only regressions in 3 probed construct classes) + two gate-found categories: docstring boundaries near #if directives (7 clean files, docstring-field-only — verified mechanically) and array-literal-callee call refs (2 refs, 1 file). Every hunk classified via the error-union rule + parked-ref↔edge ripple pairing. Walker (the arc's biggest) centers on the #1020 DEDICATED property branch: computed properties → property nodes with the getter walked under the property (SwiftUI body), static let/var → constant/variable, stored → field, decorator/type-annotation/@Siblings-attr-arg refs all attached to the ENCLOSING TYPE, stored initializer calls attributed to the class. Preserved bug-for-bug: the never-resolving 'parameter' field (zero param type refs, zero signatures), present-false isAsync, open→internal visibility, everything-is-extends inheritance (first type_identifier per specifier), no instantiates refs ever, subscript reads as `calls arr`, `defer` as `calls defer`, multi-case enum entries minting only the first case, /** */ block docs ignored AND chain-breaking, init/deinit/subscript minting no nodes with visitNode-routed bodies (calls → class, static reads → nothing), multi- segment extension resolveName, sugar extension names, the #selector shapes, and the value_argument label-forward skip. ONE fix found by the sweep (then pinned in the fixture + checklist): the shared `assignment` shadow-prune case is swift-live — declared-then-assigned `let X: T` prunes X as a value-ref target. Gates: sweeps 0-diff Alamofire 89/98, vapor 224/247, swift-nio 407/554 (--max-deferral 0.3 — swift error incidence is 9–27% on BOTH arms, structural; every deferral count matches the survey's table exactly); full-init dumps byte-identical ×3 (31.9k/20.7k/126.3k lines); the Alamofire census reproduces property=348 (the #1020 number) on the kernel arm; kernel-swift-parity suite (206-line torture + CRLF + the #if-between-enum- cases defer fixture) + swift grammar-parity row; full suite 2,626 green ×2 under CODEGRAPH_KERNEL_EXPECT=1. DEFAULT_ROUTED += swift (14 langs). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
72 KiB
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; tag0.7.3-with-generated-files(commit31d17fe7e818a2048c808b5c6fdc2dc792f4f5b5) ships an ABI-14 parser.c (LANGUAGE_VERSION 14, SYMBOL_COUNT 565 — an older-generator run, plusparser_abi13.c/parser_abi14.cvariants), 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.jsonrulesandexternalsare 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'ssrc/(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 runtree-sitter generate.- crate
src/parser.csha256d3edff6effe31b9a507f496577407987343b101b23eb7bee7a9b050e8ab5d27a(20,642,243 bytes — expect slow builds) - crate
src/scanner.csha256f3d6271d64f58c39eed544104a70ca2cf9ecbf80c5d900620f1afd38836542cb(== the tag's scanner.c byte-for-byte). External scanner: YES — the crate build compiles it automatically; the wasm build picks it up fromsrc/.
- crate
- Build (survey-verified commands):
(brew emcc present; one benign scanner warning. Survey artifact: ABI 15, 3,726,622 bytes, sha256
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 .cc77a63b8487956270e2f385e29a03ba0773ba532a3c8a8844a26b4c98793843, 563 node types / 46 fields, at scratchpadsvy-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),packageaccess, and typedthrows(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'toVENDORED_WASM_LANGS(grammars.ts:291) with an R7b comment noting the crate-tarball provenance (NOT tag-sha-matched — state why); pintree-sitter-swift = "=0.7.3"in codegraph-kernel/Cargo.toml (crate + wasm move TOGETHER); add'swift'toGRAMMAR_LANGUAGESin__tests__/kernel-grammar-parity.test.ts:39; kernel symboltree_sitter_swift::LANGUAGEin langs.rsgrammar_for+ theLANGUAGESconst (langs.rs:18,[&str; 13]→ 14). MIT license.copy-assetsalready globssrc/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):
#selectortoken split: OLD one anon#selectortoken; NEW anon#+selector.selector_expression's NAMED children are unchanged →normalizeSpecial(function-ref.ts:685) and the body walker see identical trees. Inert.#warning(…)internals: OLD = onediagnosticleaf swallowing the full text; NEW =diagnosticwith an anon#child.diagnosticmatches no type list on either arm; no named children either way; no stray sibling call on NEW (probed clean-file). Inert.- 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):
- 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#GETroute macros (drives vapor 57→23);packageaccess modifier; typed throwsthrows(ErrorType)(swift-nio ByteBuffer-views). Node/edge/ref diffs in these files are expected and accepted. - NEW-only error regressions, 21 files (3 Alamofire / 1 vapor / 17
swift-nio), three construct classes probed to minimal repros:
#if/#elseifbetween enum cases (enum E { case a\n#if DEBUG\n case b\n#endif }) → NEW ERROR, OLD clean. (#ifat 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.
- 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). - (Found at bump-gate time, survey-missed.) Docstring boundaries near
#ifdirectives on CLEAN files. The NEW grammar's directive/comment sibling structure changes what getPrecedingDocstring accumulates next to#if/#endiflines: 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 (webSocketRequestgains "Only Apple platforms…") or extends it with directive-adjacent text (_withWindowsPathsgains a leading "ENABLE_MOCKING" line). The walker reproduces this automatically (same shared docstring code over the same NEW trees). - (Found at bump-gate time, survey-missed.) Array-literal-callee call
refs. One clean file (swift-nio IPv4Header.swift) emits 2 NEW-only
callsrefs 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 asm[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
- No preParse.
swiftExtractorhas nopreParsehook (languages/swift.ts — whole file, no such key) →preParsedSource(kernel/index.ts:96) is a no-op; both arms parse raw bytes. Swift's#ifis grammar-native (directivenodes, both branches kept as siblings — probed) — nothing to blank, unlike C/C#. - 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 frameworkextract()onto the decodedextractFromSourcepath):swiftUIResolver(frameworks/swift.ts:11, detect = any .swift containingimport SwiftUI, else any file path ending.xcodeproj/.xcworkspace— notegetAllFiles()returns indexed files, so the xcodeproj leg rarely fires; the pbxproj inside doesn't end with.xcodeproj),uikitResolver(:136, detect = any .swift containingimport UIKit/UIViewController/UIView),vaporResolver(:269, detect =Package.swiftcontainingvapor, elseimport Vapor). AlsoexpoModulesResolver(expo-modules.ts:154, languages swift+kotlin, has extract) andswiftObjcBridgeResolver(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. - 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/componentresolution 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). - 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.
- One walker module (suggest
codegraph-kernel/src/swift.rs), registered in langs.rs (grammar_for→tree_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 thenode_idsdedupe + import hooks. The dedicated property branch has no precedent in any walker — it is new logic, transcribe from §Dedicated property branch. .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.- No POST_PASSES entry (kernel/index.ts:81 — none for swift) →
tryKernelExtractRawstays eligible.
Extractor config (languages/swift.ts — 138 lines, read it whole)
Types: functionTypes=[function_declaration]; classTypes=[class_declaration];
methodTypes=[function_declaration] (== functionTypes — the 994 branch handles
both; the 1027 methodTypes branch NEVER fires for swift);
interfaceTypes=[protocol_declaration]; structTypes=[struct_declaration]
(DEAD — node type absent from BOTH grammars, probed against the kind
tables); enumTypes=[enum_declaration] (DEAD likewise);
enumMemberTypes=[enum_entry]; typeAliasTypes=[typealias_declaration];
importTypes=[import_declaration]; callTypes=[call_expression];
variableTypes=[property_declaration, constant_declaration]
(constant_declaration DEAD — absent from both grammars; swift let/var
at every scope is property_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 theseenNamelatch); after it,function_body→ undefined (body reached);user_type→ that node;optional_type→ its first namedChild of typeuser_type(?? null); first such hit wins: text =getNodeText(typeNode).trim() .replace(/<[^>]*>/g, ''), last.-segment (KF.Builder→Builder— the comment explains why NOT the first type_identifier), must match/^[A-Za-z_]\w*$/and ≠'Void'else undefined. Probed results:-> Widget→Widget;-> Foo?→Foo;-> KF.Builder→Builder;-> Result<Foo, Err>→Result(single-level generic strip); nested generics break like rust —Result<Vec<Foo>, E>→ strip leavesResult, E>→ regex fails → undefined;-> Void/-> [Foo](array_type)/-> (Int, Foo)(tuple_type)/-> (Int) -> Foo(function_type) → undefined. Parameters never match (their node type isparameter, not user_type — the scan does NOT descend). - resolveName (swift.ts:60) — class_declaration ONLY,
namefield must be auser_typewith >1type_identifierchildren → the LAST one's text (extension KF.Builder→Builder, 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 undefined→ signature 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
modifierschild 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', TYPEenum→ 'enum', else 'class'. The grammar puts the keyword in thedeclaration_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
asyncas an anon child AFTER the parameter list, never insidemodifiers(probed:func asyncy() async throws→ isAsync=false). The walker must reproduce false — note extractFunction/extractMethod storeisAsync: 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— anonclasschild) 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).handledRefsnever set → genericimportsref {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 types → Foo() 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 = literalfile:${filePath}(:509), name = basename, qualifiedName = filePath, endLine =source.split('\n').length, isExported false. Dedupe/self-checks compare ID STRINGS (node_idsvec 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; membersSession::request. Extension members ride the extension's class node name (Builder::doneforextension KF.Builder). - extractModifiers hook absent → no decorators-from-modifiers merge (:1355
no-op); the
decoratorsnode 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 — needsparameter_listchild 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: String →
extends 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 Model → decorates 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) andname: pattern > bound_identifier: simple_identifierare SIBLING direct children → binding found → isLet real. - protocol_property_declaration NESTS the value_binding_pattern INSIDE the
name: pattern→ the direct-childrenfindMISSES it → binding null → isLet always false for protocol requirements (irrelevant today — they are all computed — but transcribe the miss). computed_value: computed_propertycarries the getter; the code matches by TYPE not field.willSet/didSetobservers are awillset_didset_block, NOT a computed_property → an observed stored property (var observed: Int = 0 { willSet {…} didSet {…} }) is isComputed=false → afieldnode, and (stored-path descent, below) the observer bodies' calls attribute to the CLASS (probed:calls prepare/calls reactfrom=class).
The branch body (:1126-1193), step by step
ownerId = nodeStack top(the class/struct/enum/interface node).- If nameNode:
- isComputed →
createNode('property', name, node, {visibility: hook, isStatic: hook ?? false})— computed properties becomepropertynodes (the #1020 "var isCloudProxy: Bool" fix; SwiftUIvar body: some Viewis 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 let→constant,static var→variable(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.
- isComputed →
- If ownerId:
extractDecoratorsFor(node, ownerId)— refs attach to the ENCLOSING TYPE, not the property (probed:@Published private var wrapped→decorates Published from=class:Session). §Decorators for mechanics. - If ownerId:
extractVariableTypeAnnotation(node, ownerId)(:6074) — finds the directtype_annotationchild → extractTypeRefsFromSubtree (:6090) → onereferencesref pertype_identifierleaf not in BUILTIN_TYPES, from the OWNER (probed:let rootQueue: DispatchQueue→references DispatchQueue from=classat the leaf's position). BUILTIN_TYPES nuances (:5768):Int/String/Double/Float/Boolean/Long/Charare suppressed (Scala rows),Boolis NOT (→ realreferences Boolnoise ref, probed),Void/Character/SelfNOT. Wrappers recurse:[Tag]→Tag;Session?→Session;some View(opaque_type) →View. - If ownerId: walkAttrArgs (:1165-1175) — find the
modifiersdirect child; if present, recursively runextractStaticMemberRef(n)over EVERY node of the modifiers subtree. This is the Vapor@Siblings(through: PivotModel.self, from: \.$left, …)mechanism: the attribute-argumentnavigation_expressionwith targetsimple_identifier PivotModel+ suffix.self→references PivotModelfrom the OWNER at the RECEIVER's position. Keypath args (\.$left— target iskey_path_expression) and the wrapper's ownuser_typeare self-filtered (not MEMBER_ACCESS_TYPES / not an accepted recv type). §Static-member refs for the shared mechanics. - 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'sVStack/Textsubtree becomes the property's callees). protocol_property_requirements contains only getter/setter specifiers → walks emit nothing. - Stored: skipChildren stays FALSE → the generic loop (:1295) descends
into the property_declaration's children via visitNode: the initializer's
call_expressions hit callTypes →
callsrefs 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.heartsas an in-class initializer) emit NOTHING here (no static-member call in visitNode) — contrast bodies. - fn-ref capture: maybeCaptureFnRefs ran at :990 BEFORE the branch —
property_declaration ∈ SWIFT_SPEC dispatch (varinit field
value) → a storedlet cb: Handler = onFirecapturesonFire; computed properties have novaluefield → 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 simple_identifier. Operator functions:func <+> (lhs:…)hasname: 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/classmodifiers), returnType (hook). - Then extractTypeAnnotations (:1594) — §Type-annotation refs: return-type
refs ONLY. Then extractDecoratorsFor (:1599) — attributes inside
modifiers→ real decorates refs (@objc func attributed→decorates objc, position = the attribute node). Push, walkbodyfield (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 →
functionnode 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) -> Void → references 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.red →
references Color; Suit.hearts.rawValue → outer nav recv = inner nav →
nothing, inner nav → references Suit; UserModel.self → references UserModel; Deep.Nested.leaf → references 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 ✓ (@objc →
decorates 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-levellet/var(under file:) and in-classstatic let/static var(constant/variable under class:/struct:/enum:).interface:NOT accepted → protocol statics (hypothetical) excluded. Instancefields and computedpropertys are NOT targets. - Reader scopes (:764): function/method/constant/variable nodes —
propertyandfieldare 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_declarationcase (:856-869) RESOLVES for swift (unlike php): vd = findvariable_declaration(Kotlin) — absent for swift → elsefirstSimpleIdentifier(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 = 5in 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) → aguard 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 everysimple_identifiermatches — nav suffixes included —Session.SHARED_MAXAND a bareSHARED_MAXboth 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 thevaluefield. - rhs (assignment, field
result): param-storage skip — lhs = left/lhs/target fields ?? namedChild(0) (swift's field istarget, a directly_assignable_expression); lhs text's last identifier === rhs text → skip (self.raw = raw→ nothing, probed).o.cb = handler→handler✓. - varinit (property_declaration, field
value): destructuring check reads childForField('name') — swift's is typepattern, NOT in {object_pattern, array_pattern, tuple_pattern, struct_pattern} → never skipped; tuple-let values are non-normalizable expressions anyway. Computed properties have novaluefield → 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) andUIKit.UIView(QUALIFIED_IMPORT → last segmentUIView) both land in importedNames but almost never match a candidate → the swift gate is effectively "defined in this file" (probed:#selector(Holder.fire)'sfiredropped;onFire/handler/cbA/cbBsurvive 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) + thesendEventscan (callback-synthesizer.ts:1387-1438) read raw.swiftfile 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 (= themodifiers/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/endIndexsubstrings (getNodeText — getVisibility/ isStatic readchild.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'
gmstrips (§Docstrings) →js_multiline_stripin 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;
sourceIsPreParsednever set for swift.
Gates (per plan §5, no exceptions)
- Grammar bump lands FIRST, standalone (php pattern): vendor wasm +
=0.7.3crate pin + VENDORED_WASM_LANGS + kernel-grammar-parityGRAMMAR_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 withsvy-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 vsCODEGRAPH_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 DEBUGbetween enum cases — asserting the kernel defers and wasm output is served); full suite ×2 green withCODEGRAPH_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-casecase put, delete(ONLYputminted) + 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(classArray, 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 varcomputed (property, isStatic via 'class');lazy var x: Cache = Cache.build()(field +calls Cache.buildfrom class);public private(set)(→ 'public');@Published private var(decorates from OWNER + vis 'private');@Siblings(through: Pivot.self, from: \.$left) var siblings: [Tag](decorates Siblings + walkAttrArgsreferences Pivot+ type-refTag, all from OWNER); computedvar isCloudProxy: Bool { check() }(property +calls checkFROM the property +references Bool— Bool not builtin); SwiftUI-shapedvar body: some View { VStack { Text(label) } }(property; VStack/Text calls from the property;references View); observedvar x = 0 { willSet{prepare(newValue)} didSet{react(oldValue)} }(field, observer calls from the CLASS);weak var delegate: SessionDelegate?; tuplelet (a, b) = makePair()(ONE nodea,calls makePairfrom class);openvisibility (→ 'internal'). - functions/methods: free func with params+return (
sig=null, return-type ref, NO param refs — include aVoid-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'); operatorfunc <+> (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 setupMonitorfrom class; NO fn-ref for the param-storage assignment);convenience init?(calls initbare 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(Suitonly),UserModel.self,Deep.Nested.leaf(Deeponly),.implicit(nothing),lowercase.f(nothing), keypath\Foo.bar(nothing) — plus the SAME reads at class-initializer scope and insideinit(nothing — the visitNode gap). - value refs:
static let SHARED_MAXin a class/struct/enum read from a method (edge) and from a computed property (NO edge — property not a reader); a method-locallet SHARED_MAX = 5in a SECOND fixture variant (prune kills the target); aguard let SHARED_MAXvariant (NO prune); top-levellet 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, dottedimport UIKit.UIView, kindedimport 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.