// Phase D7 (per-signature scope=notified destroy trampolines): DEFERRED. // Verified zero scope="notified" callback params flow through the plan // layer in tier 1 (GLib + GObject). All notified scopes are on // g_signal_connect_data (shortcircuited by the @_silgen_name shim in // renderSupport) or on callables whose callback params are skipped with // .callbackWithoutUserData until D4.3 lands. Deferral is plan-sanctioned // per local://phase-d-signals-callbacks-plan.md §D7 contingency. // When a tier-1 callable with a notified callback param surfaces, // renderCallable's callback-box branch must populate // destroyTrampoline="" and emit a per-signature // @_cdecl destroy trampoline. // Planner.swift // The binding planner: walks a parsed GIR namespace, resolves every type // through the registry and TypeMapper, and produces either a complete // `TypePlan` or a `SkipEntry` with a machine-readable reason. // // This is where Rule 3 of the rearchitecture lives: if any part of a symbol // cannot be planned, the WHOLE symbol is skipped with a reason. There is no // code path that emits a partially-understood binding. import Foundation // MARK: - Module Planner /// Plans every type in every namespace of a multi-package analysis, producing /// a `ModulePlan` per generated Swift module. /// /// - Parameters: /// - analysis: The resolved multi-package analysis with parsed repositories. /// - registry: The global type registry built from those repositories. /// - Returns: A dictionary mapping Swift module name to its `ModulePlan`. public func planModules( analysis: MultiPackageAnalysis, registry: TypeRegistry ) -> [String: ModulePlan] { var modulePlans: [String: ModulePlan] = [:] for (moduleName, repo) in analysis.repositories { let ns = repo.namespaces.first { $0.name == moduleName } ?? repo.namespaces.first guard let namespace = ns else { continue } let context = MapContext( registry: registry, currentModule: moduleName, currentNamespace: namespace.name, dependencyModules: (analysis.directDependencies[moduleName] ?? []).sorted() ) let plan = planNamespace(namespace, context: context) modulePlans[moduleName] = plan } // Global post-pass: drop class properties/methods already declared, // with the identical Swift name, by an ancestor class — possibly in a // different module (e.g. `Pango.Coverage.ref()`/`unref()` already // declared by `GObject.Object`). GObject inheritance provides these for // free, and Swift errors on the redundant redeclaration. Keyed by // unqualified Swift name: safe because `duplicateOfDependency` skipping // guarantees surviving cross-module type names are unique along // dependency edges, and inheritance only follows those edges. var classesByGIRName: [String: ClassPlan] = [:] for (_, plan) in modulePlans { for case .class(let p) in plan.types { classesByGIRName[p.girName] = p } } // Swift's override-conflict diagnostic ("requires an 'override' keyword" // / "overriding non-open instance method outside of its defining // module") matches candidates by SELECTOR — name + parameter labels/ // types — regardless of return type. So the ancestor-collision key must // do the same: excluding return type catches true selector collisions // (e.g. `Pango.Coverage.ref()` vs. inherited `GObject.Object.ref()` — // same selector, would silently violate the override rule if kept) while // still keeping legitimate different-selector overloads across an // inheritance edge (e.g. `MenuButton.setDirection(direction: ArrowType)` // vs. inherited `Widget.setDirection(dir: TextDirection)` — different // labels AND types, a different selector, no relation to include). func methodSignature(_ m: CallablePlan) -> String { let params = m.parameters .filter { !$0.isInstanceParameter && !$0.isOutParameter && $0.synthesizedLengthOf == nil } .map { "\($0.swiftName):\($0.mapping.swiftType)" } .joined(separator: ",") return "\(m.name)|throws:\(m.throwsError)|(\(params))" } // Same idea, but for an async method's rendered signature: the starter // carries the real parameter list (minus its synthesized callback/ // user-data pair, which never appears in the Swift signature) while // whether the call `throws` comes from the paired `*_finish` callable, // not the always-non-throwing void starter. func asyncMethodSignature(_ p: AsyncCallablePlan) -> String { let params = p.starter.parameters .filter { !$0.isInstanceParameter && !$0.isOutParameter && $0.synthesizedLengthOf == nil && $0.asyncRole == nil } .map { "\($0.swiftName):\($0.mapping.swiftType)" } .joined(separator: ",") return "\(p.starter.name)|throws:\(p.finish.throwsError)|(\(params))" } // Walked by GIR name, not Swift spelling: two classes in different // modules can share a Swift simple name (`Gst.Object` / `GObject.Object` // both spell `Object`), which would make a bare-name-keyed walk and its // cycle guard falsely conflate them — terminating the ancestor walk one // level early and missing real inherited members. GIR names are always // unique. func ancestorNames(of plan: ClassPlan) -> (props: Set, methods: Set) { var propNames: Set = [] var methodSigs: Set = [] var current = plan.parentGIRName var seen: Set = [plan.girName] while let parentGIRName = current, !seen.contains(parentGIRName), let parentPlan = classesByGIRName[parentGIRName] { seen.insert(parentGIRName) propNames.formUnion(parentPlan.properties.map(\.swiftName)) methodSigs.formUnion(parentPlan.methods.map(methodSignature)) methodSigs.formUnion(parentPlan.asyncMethods.map(asyncMethodSignature)) current = parentPlan.parentGIRName } return (propNames, methodSigs) } for (moduleName, modulePlan) in modulePlans { var skips = modulePlan.skips var changed = false let newTypes = modulePlan.types.map { typePlan -> TypePlan in guard case .class(let plan) = typePlan else { return typePlan } let inherited = ancestorNames(of: plan) guard !inherited.props.isEmpty || !inherited.methods.isEmpty else { return typePlan } let filteredMethods = plan.methods.filter { method in guard inherited.methods.contains(methodSignature(method)) else { return true } skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(method.name)", cIdentifier: method.cIdentifier, reason: .inheritedMember, detail: "method '\(method.name)' already declared by ancestor")) return false } let filteredAsyncMethods = plan.asyncMethods.filter { asyncMethod in guard inherited.methods.contains(asyncMethodSignature(asyncMethod)) else { return true } skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(asyncMethod.starter.name)", cIdentifier: asyncMethod.starter.cIdentifier, reason: .inheritedMember, detail: "async method '\(asyncMethod.starter.name)' already declared by ancestor")) return false } // Own-method selectors that survived filtering — used below to // catch properties left delegating to a method this pass just // removed (the property was planned before this global pass ran, // using the class's full pre-filter method list). let survivingSelectors = Set(filteredMethods.map { "\($0.name)/\($0.parameters.filter { !$0.isInstanceParameter && !$0.isOutParameter }.count)" }) func delegatesToDroppedMethod(_ accessor: PropertyAccessorPlan?) -> Bool { guard case .delegate(let method, let label) = accessor else { return false } let arity = label == nil ? 0 : 1 return !survivingSelectors.contains("\(method)/\(arity)") } let filteredProps = plan.properties.filter { prop in if inherited.props.contains(prop.swiftName) { skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(prop.swiftName)", cIdentifier: prop.girName, reason: .inheritedMember, detail: "property '\(prop.swiftName)' already declared by ancestor")) return false } if delegatesToDroppedMethod(prop.getter) || delegatesToDroppedMethod(prop.setter) { // The delegated getter/setter method collided with an // ancestor's identically-selectored method and was // dropped above — the property can no longer delegate to // it safely (it would silently resolve to the ancestor's // differently-typed method instead). skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(prop.swiftName)", cIdentifier: prop.girName, reason: .inheritedMember, detail: "property '\(prop.swiftName)' delegated to a getter=/setter= method that collided with an ancestor's identically-selectored method and was dropped")) return false } return true } guard filteredProps.count != plan.properties.count || filteredMethods.count != plan.methods.count || filteredAsyncMethods.count != plan.asyncMethods.count else { return typePlan } changed = true let newPlan = ClassPlan( name: plan.name, girName: plan.girName, cType: plan.cType, parent: plan.parent, parentGIRName: plan.parentGIRName, isOpen: plan.isOpen, isAbstract: plan.isAbstract, getTypeFunction: plan.getTypeFunction, descendsFromInitiallyUnowned: plan.descendsFromInitiallyUnowned, refFunc: plan.refFunc, unrefFunc: plan.unrefFunc, interfaces: plan.interfaces, constructors: plan.constructors, methods: filteredMethods, functions: plan.functions, properties: filteredProps, signals: plan.signals, asyncMethods: filteredAsyncMethods, doc: plan.doc ) return .class(newPlan) } guard changed else { continue } modulePlans[moduleName] = ModulePlan( module: modulePlan.module, dependencyModules: modulePlan.dependencyModules, types: newTypes, skips: skips, coverage: modulePlan.coverage ) } return modulePlans } // MARK: - Namespace-level planning /// Plans every type in a single namespace, delegating to per-category helpers. private func planNamespace(_ ns: Namespace, context: MapContext) -> ModulePlan { var types: [TypePlan] = [] var skips: [SkipEntry] = [] var boundTypes = 0 var totalTypes = 0 var boundCallables = 0 var totalCallables = 0 var boundCallbacks = 0 var totalCallbacks = 0 var boundSignals = 0 var totalSignals = 0 // A type declared here is skipped if a type of the same GIR name is // already declared in a dependency module — e.g. GObject re-declaring // GLib's `IOCondition`. Keeping both would make Step 1's unqualified // cross-module names ambiguous. Applies to type declarations only // (enums, bitfields, records, classes, interfaces, aliases), never to // constants/functions/callbacks. func duplicateDependencyModule(_ girSimpleName: String) -> String? { context.registry.droppedShadow("\(ns.name).\(girSimpleName)") } func skipIfDuplicate(_ girSimpleName: String) -> Bool { guard let dep = duplicateDependencyModule(girSimpleName) else { return false } skips.append(SkipEntry(symbol: "\(ns.name).\(girSimpleName)", cIdentifier: girSimpleName, reason: .duplicateOfDependency, detail: "'\(girSimpleName)' is already declared in dependency module '\(dep)'")) return true } // ── Enumerations ── for enumeration in ns.enumerations { totalTypes += 1 if skipIfDuplicate(enumeration.name) { continue } boundTypes += planEnum(into: &types, skips: &skips, enumeration: enumeration, context: context) } // ── Bitfields ── for bitfield in ns.bitfields { totalTypes += 1 if skipIfDuplicate(bitfield.name) { continue } boundTypes += planBit(into: &types, skips: &skips, bitfield: bitfield, context: context) } // ── Constants ── // Case conversion can merge distinct GIR names (`CSET_A_2_Z` and // `CSET_a_2_z` both become `csetA2Z`), so collisions are detected here // and the later occurrence is skipped. var seenConstantNames: Set = [] for constant in ns.constants { totalTypes += 1 boundTypes += planConst(into: &types, skips: &skips, seen: &seenConstantNames, constant: constant, context: context) } // ── Aliases ── for alias in ns.aliases { totalTypes += 1 if skipIfDuplicate(alias.name) { continue } boundTypes += planAliasType(into: &types, skips: &skips, alias: alias, context: context) } for klass in ns.classes { totalTypes += 1 if skipIfDuplicate(klass.name) { continue } boundTypes += skipClass(into: &skips, into: &types, boundCallables: &boundCallables, totalCallables: &totalCallables, klass: klass, namespace: ns.name, context: context) if case .class(let cp) = types.last { totalSignals += klass.signals.filter(\.symbolInfo.isBindable).count; boundSignals += cp.signals.count } } for iface in ns.interfaces { totalTypes += 1 if skipIfDuplicate(iface.name) { continue } boundTypes += skipInterface(into: &skips, into: &types, iface: iface, namespace: ns.name, context: context) if case .interface(let ip) = types.last { totalSignals += iface.signals.filter(\.symbolInfo.isBindable).count; boundSignals += ip.signals.count } } for record in ns.records { totalTypes += 1 if skipIfDuplicate(record.name) { continue } boundTypes += skipRecord(into: &skips, into: &types, boundCallables: &boundCallables, totalCallables: &totalCallables, record: record, namespace: ns.name, context: context) } for callback in ns.callbacks { totalCallbacks += 1 totalTypes += 1; boundTypes += planCallbackType(into: &skips, into: &types, callback: callback, namespace: ns.name, context: context) if case .callback(_) = types.last { boundCallbacks += 1 } } for fn in ns.functions { totalCallables += 1 boundCallables += planOrSkipFunction(into: &skips, into: &types, fn: fn, namespace: ns.name, context: context) } // ── GIO async pattern (namespace-level functions) ── // Excludes `moved-to` functions: those are routed into an owning // class/record's own `functions` list above, and that owner's // `planClass`/`planRecord` already ran its own async pairing pass over // its GIR-declared ``s before the routing happened. let namespaceAsyncCandidates = ns.functions .filter { $0.symbolInfo.isBindable && $0.symbolInfo.movedTo == nil } .map { AsyncStarterCandidate(name: $0.name, cIdentifier: $0.cIdentifier, parameters: $0.parameters, doc: $0.doc) } let plannedFunctions: [CallablePlan] = types.compactMap { if case .callable(let p) = $0 { return p } return nil } let namespaceAsyncPairs = planAsyncPairs( candidates: namespaceAsyncCandidates, planned: plannedFunctions, isStatic: true, fullNamePrefix: ns.name, context: context) if !namespaceAsyncPairs.isEmpty { removeStaleAsyncSkips(&skips, for: namespaceAsyncPairs) types.append(contentsOf: namespaceAsyncPairs.map { .asyncCallable($0) }) boundCallables += namespaceAsyncPairs.count } // ── Cross-category name collision detection ── // Constants and functions can case-fold to the same Swift name // (e.g. ATOMIC_REF_COUNT_INIT and g_atomic_ref_count_init both map to // atomicRefCountInit). Functions (callables) win over constants since // they are typically more commonly used. Intra-category collisions // (constant-vs-constant, callable-vs-callable) use GIR declaration order. do { // Pass 1: collect all callable names (callables have priority) var callableNames: Set = [] for typePlan in types { if case .callable(let plan) = typePlan { callableNames.insert(plan.name) } } // Pass 2: filter with priority var seenNames: Set = [] var filtered: [TypePlan] = [] var lostTypes = 0 var lostCallables = 0 for typePlan in types { switch typePlan { case .constant(let plan): // Constant loses if a callable has the same name (callables // have priority), or if another constant already claimed it. if callableNames.contains(plan.name) { skips.append(SkipEntry(symbol: "\(ns.name).\(plan.girName)", cIdentifier: plan.girName, reason: .nameCollision, detail: "Swift name '\(plan.name)' conflicts with a function in this module")) lostTypes += 1 } else if seenNames.contains(plan.name) { skips.append(SkipEntry(symbol: "\(ns.name).\(plan.girName)", cIdentifier: plan.girName, reason: .nameCollision, detail: "Swift name '\(plan.name)' already taken by an earlier constant")) lostTypes += 1 } else { seenNames.insert(plan.name) filtered.append(typePlan) } case .callable(let plan): // Callables only lose to earlier callables if seenNames.contains(plan.name) { skips.append(SkipEntry(symbol: "\(ns.name).\(plan.cIdentifier)", cIdentifier: plan.cIdentifier, reason: .nameCollision, detail: "Swift name '\(plan.name)' already taken by an earlier symbol")) lostCallables += 1 } else { seenNames.insert(plan.name) filtered.append(typePlan) } default: filtered.append(typePlan) } } types = filtered boundTypes -= lostTypes boundCallables -= lostCallables } let coverage = CoverageStats( boundCallables: boundCallables, totalCallables: totalCallables, boundTypes: boundTypes, totalTypes: totalTypes, boundCallbacks: boundCallbacks, totalCallbacks: totalCallbacks, boundSignals: boundSignals, totalSignals: totalSignals ) return ModulePlan(module: context.currentModule, dependencyModules: context.dependencyModules, types: types, skips: skips, coverage: coverage) } // MARK: - Signature deduplication /// Canonical key for comparing Swift functional signatures (parameter labels /// + types, throwing-ness, and return type). Excludes instance/out params. private func signatureKey(_ plan: CallablePlan) -> String { let retType = plan.returnMapping?.swiftType ?? "Void" let params = plan.parameters .filter { !$0.isInstanceParameter && !$0.isOutParameter && $0.synthesizedLengthOf == nil } .map { "\($0.swiftName):\($0.mapping.swiftType)" } .joined(separator: ",") return "throws:\(plan.throwsError)|ret:\(retType)|(\(params))" } /// Drops callables whose full Swift signature collides with an earlier one /// — e.g. async `_finish` constructors that all map to the SAME rendered /// `init(res: AsyncResult) throws` regardless of their distinct Swift names /// (`newFinish`, `newForAddressFinish`, …): constructors render as bare /// `init(...)`, so `plan.name` never appears in the emitted signature and /// MUST be excluded from the dedup key. Methods, by contrast, keep their own /// name in the emitted signature, so it stays part of their key. /// The first occurrence wins; later collisions are recorded as skips. /// Gives a colliding constructor a stable label prefix derived from its GIR /// constructor name (for example, `new_and` becomes `andCondition1`). This /// preserves both constructors when their parameter types and original labels /// are otherwise identical. private func disambiguatedConstructor(_ plan: CallablePlan) -> CallablePlan? { let stem = plan.name.hasPrefix("new") ? String(plan.name.dropFirst(3)) : plan.name guard !stem.isEmpty else { return nil } let prefix = stem.prefix(1).lowercased() + stem.dropFirst() var renamedNames: [String: String] = [:] for parameter in plan.parameters where !parameter.isInstanceParameter && !parameter.isOutParameter && parameter.synthesizedLengthOf == nil { let label = prefix + parameter.swiftName.prefix(1).uppercased() + parameter.swiftName.dropFirst() renamedNames[parameter.swiftName] = label } let parameters = plan.parameters.map { parameter in let swiftName = renamedNames[parameter.swiftName] ?? parameter.swiftName let lengthOf = parameter.synthesizedLengthOf.flatMap { renamedNames[$0] } ?? parameter.synthesizedLengthOf guard swiftName != parameter.swiftName || lengthOf != parameter.synthesizedLengthOf else { return parameter } return ParameterPlan( swiftName: swiftName, cArgIndex: parameter.cArgIndex, mapping: parameter.mapping, isInstanceParameter: parameter.isInstanceParameter, isOutParameter: parameter.isOutParameter, closureIndex: parameter.closureIndex, destroyIndex: parameter.destroyIndex, synthesizedLengthOf: lengthOf) } return CallablePlan( name: plan.name, cIdentifier: plan.cIdentifier, parameters: parameters, returnMapping: plan.returnMapping, isStatic: plan.isStatic, isConstructor: plan.isConstructor, ownershipInit: plan.ownershipInit, throwsError: plan.throwsError, isOverride: plan.isOverride, doc: plan.doc) } private func dedupBySignature( _ plans: [CallablePlan], isConstructor: Bool, symbolPrefix: String, skips: inout [SkipEntry] ) -> [CallablePlan] { var seen: Set = [] var result: [CallablePlan] = [] for plan in plans { let key = isConstructor ? signatureKey(plan) : "\(plan.name)|\(signatureKey(plan))" if seen.contains(key) { if isConstructor, let disambiguated = disambiguatedConstructor(plan) { let disambiguatedKey = signatureKey(disambiguated) if !seen.contains(disambiguatedKey) { seen.insert(disambiguatedKey) result.append(disambiguated) continue } } let renderedName = isConstructor ? "init" : plan.name skips.append(SkipEntry( symbol: "\(symbolPrefix).\(plan.cIdentifier)", cIdentifier: plan.cIdentifier, reason: .nameCollision, detail: "Swift signature '\(renderedName)(\(signatureKey(plan)))' already emitted by an earlier symbol" )) } else { seen.insert(key) result.append(plan) } } return result } // MARK: - GIO async pattern /// One raw candidate for GIO `*_async`/`*_finish` pairing: a method or /// global function whose GIR name ends `_async`. Built by each caller from /// its owner's raw, bindable `Method`/`GlobalFunction` list — mirrors the /// `where symbolInfo.isBindable` filter the ordinary planning loops apply. private struct AsyncStarterCandidate { let name: String let cIdentifier: String let parameters: [Parameter] let doc: String? } /// Pairs GIO-style `*_async` starters with their `*_finish` completions so /// the renderer can emit one Swift `async` method per operation. /// /// A starter qualifies only when every rule holds; otherwise it produces no /// pair. The caller still runs the normal `planMethod`/`planFunction` path /// on every raw candidate regardless (this function never mutates that /// path's inputs), so a starter that fails a rule here falls through to the /// existing D4.3 `callbackWithoutUserData` skip unchanged — no baseline /// entry moves to a new reason. For starters that DO pair successfully, the /// caller is responsible for dropping that now-stale skip entry (matched by /// `cIdentifier`) from its own skip list, since this function only adds. /// /// 1. (Pre-filtered by the caller via the GIR name.) The starter's GIR name /// and C identifier both end `_async`. /// 2. The last two parameters are, in order, a parameter whose resolved /// type is `Gio.AsyncReadyCallback` and a bare `gpointer` (the /// conventional trailing `user_data`). Any other shape disqualifies the /// starter. Checked structurally (position + type) rather than via the /// GIR `closure=` attribute, whose indexing is relative to `` /// children only (excluding ``) and would need a /// separate, error-prone offset to compare against this array's indices. /// 3. A sibling callable named `swiftFunctionName(_finish)` is /// already in `planned` (found by C identifier: the starter's /// `cIdentifier` with its `_async` suffix replaced by `_finish`), and it /// takes exactly one non-instance, non-out, non-synthesized parameter /// whose `mapping.swiftType == "AsyncResult"`. /// 4. No callable already in `planned` uses the starter's Swift name. /// 5. The starter's remaining parameters (everything but the trailing /// callback/user-data pair) all plan through the normal path. /// 6. The current module is `Gio` or depends on it — the rendered bridge /// references `Gio.AsyncResultRef`. /// /// - Parameters: /// - candidates: Every raw method/function whose GIR name ends `_async`. /// - planned: The owner's already-planned sibling callables (methods and /// functions together), searched for the `*_finish` completion. /// - isStatic: Whether the starter renders as a `static`/free function /// (record/class functions, namespace-level functions) or an instance /// method. /// - fullNamePrefix: `""` or `"."`, used to /// build the fully qualified GIR symbol name passed to `planCallable`. /// - ownerSwiftName: The owner's unqualified Swift type name (e.g. /// `"IOStream"`), used to qualify the `*_finish` call when it plans as /// `static` (GIR sometimes declares the completion as a bare /// `` with no instance parameter, e.g. /// `g_io_stream_splice_finish`, even though its starter is an instance /// method) — an unqualified call to a static sibling from an instance /// method body does not compile. `nil` for namespace-level pairs, whose /// `*_finish` is already a free function reachable unqualified. /// - context: The resolution context. /// - Returns: One `AsyncCallablePlan` per starter that satisfies every rule. private func planAsyncPairs( candidates: [AsyncStarterCandidate], planned: [CallablePlan], isStatic: Bool, fullNamePrefix: String, ownerSwiftName: String? = nil, context: MapContext ) -> [AsyncCallablePlan] { guard context.currentModule == "Gio" || context.dependencyModules.contains("Gio") else { return [] } guard !candidates.isEmpty else { return [] } var plannedNames = Set(planned.map(\.name)) var results: [AsyncCallablePlan] = [] for candidate in candidates { guard candidate.name.hasSuffix("_async"), candidate.cIdentifier.hasSuffix("_async") else { continue } guard candidate.parameters.count >= 2 else { continue } // Rule 2. let calleeParam = candidate.parameters[candidate.parameters.count - 2] let userDataParam = candidate.parameters[candidate.parameters.count - 1] guard userDataParam.type == .pointer else { continue } guard case .typeRef(let cbName, let cbNamespace) = calleeParam.type else { continue } guard cbName == "AsyncReadyCallback", (cbNamespace ?? context.currentNamespace) == "Gio" else { continue } // Rule 4. let starterName = swiftFunctionName(candidate.name) guard !plannedNames.contains(starterName) else { continue } // Rule 3. let finishCIdentifier = String(candidate.cIdentifier.dropLast("_async".count)) + "_finish" guard let finishPlan = planned.first(where: { $0.cIdentifier == finishCIdentifier }) else { continue } let finishRealParams = finishPlan.parameters.filter { !$0.isInstanceParameter && !$0.isOutParameter && $0.synthesizedLengthOf == nil } guard finishRealParams.count == 1, finishRealParams[0].mapping.swiftType == "AsyncResult" else { continue } // Rule 5: plan the starter with the trailing callback/user-data // pair dropped, through the exact same path an ordinary method or // function uses (so every other marshalling rule still applies). let trimmedParams = Array(candidate.parameters.dropLast(2)) let starterResult = planCallable( fullName: "\(fullNamePrefix).\(candidate.name)", swiftName: starterName, cIdentifier: candidate.cIdentifier, parameters: trimmedParams, returnValue: ReturnValue(), throwsGError: false, doc: candidate.doc, isStatic: isStatic, context: context) guard case .success(let trimmedPlan) = starterResult else { continue } // Re-append the callback/user-data pair, marked with their async // role so the renderer omits them from the Swift signature and // fills them from the generated bridge instead. Trailing position // in this array reproduces their trailing position in the C // argument list (`cArguments` builds the call in array order). let placeholder = Mapping(swiftType: "", cSwiftType: "", marshalIn: .direct, marshalOut: .direct) let starterPlan = CallablePlan( name: trimmedPlan.name, cIdentifier: trimmedPlan.cIdentifier, parameters: trimmedPlan.parameters + [ ParameterPlan(swiftName: "_asyncCallback", cArgIndex: candidate.parameters.count - 2, mapping: placeholder, asyncRole: .callback), ParameterPlan(swiftName: "_asyncUserData", cArgIndex: candidate.parameters.count - 1, mapping: placeholder, asyncRole: .userData), ], returnMapping: trimmedPlan.returnMapping, isStatic: trimmedPlan.isStatic, isConstructor: false, ownershipInit: nil, throwsError: trimmedPlan.throwsError, isOverride: false, doc: trimmedPlan.doc) plannedNames.insert(starterName) results.append(AsyncCallablePlan( starter: starterPlan, finish: finishPlan, finishOwner: finishPlan.isStatic ? ownerSwiftName : nil)) } return results } /// Drops skip entries for starters that `planAsyncPairs` successfully /// bound, so the skip report reflects the pairing rather than the stale /// per-parameter D4.3 failure the ordinary planning path also recorded for /// the same raw candidate. private func removeStaleAsyncSkips(_ skips: inout [SkipEntry], for asyncMethods: [AsyncCallablePlan]) { guard !asyncMethods.isEmpty else { return } let paired = Set(asyncMethods.map(\.starter.cIdentifier)) skips.removeAll { entry in entry.cIdentifier.map(paired.contains) ?? false } } // MARK: - Per-category planning (returns 1 for bound, 0 for skipped) private func planEnum( into types: inout [TypePlan], skips: inout [SkipEntry], enumeration: Enumeration, context: MapContext ) -> Int { let fullName = "\(context.currentNamespace).\(enumeration.name)" if let skip = checkBindable(enumeration.symbolInfo, fullName: fullName, cIdentifier: enumeration.cType) { skips.append(skip); return 0 } types.append(.enumeration(planEnumeration(enumeration, context: context))) return 1 } private func planBit( into types: inout [TypePlan], skips: inout [SkipEntry], bitfield: Bitfield, context: MapContext ) -> Int { let fullName = "\(context.currentNamespace).\(bitfield.name)" if let skip = checkBindable(bitfield.symbolInfo, fullName: fullName, cIdentifier: bitfield.cType) { skips.append(skip); return 0 } types.append(.bitfield(planBitfield(bitfield, context: context))) return 1 } private func planConst( into types: inout [TypePlan], skips: inout [SkipEntry], seen: inout Set, constant: Constant, context: MapContext ) -> Int { let fullName = "\(context.currentNamespace).\(constant.name)" if let plan = planConstant(constant, context: context) { if !seen.insert(plan.name).inserted { skips.append(SkipEntry(symbol: fullName, cIdentifier: constant.name, reason: .nameCollision, detail: "Swift name '\(plan.name)' already taken by an earlier constant")) return 0 } types.append(.constant(plan)); return 1 } skips.append(SkipEntry(symbol: fullName, cIdentifier: constant.name, reason: .configIgnored, detail: "constant type not mappable")) return 0 } private func planAliasType( into types: inout [TypePlan], skips: inout [SkipEntry], alias: Alias, context: MapContext ) -> Int { let planResult = planAlias(alias, context: context) switch planResult { case .success(let plan): types.append(.alias(plan)); return 1 case .skip(let entry): skips.append(entry); return 0 } } // MARK: - Skip helpers for not-yet-planned categories private func checkBindable(_ info: SymbolInfo, fullName: String, cIdentifier: String) -> SkipEntry? { if !info.isBindable { let reason: SkipReason = info.shadowedBy != nil ? .shadowedSymbol : !info.isIntrospectable ? .notIntrospectable : .deprecatedRemoved return SkipEntry(symbol: fullName, cIdentifier: cIdentifier, reason: reason, detail: reason.rawValue) } return nil } private func skipClass( into skips: inout [SkipEntry], into types: inout [TypePlan], boundCallables: inout Int, totalCallables: inout Int, klass: Class, namespace: String, context: MapContext ) -> Int { let fullName = "\(namespace).\(klass.name)" // Every constructor/method/static function is a callable considered for // coverage, whether or not it ends up planned. totalCallables += klass.constructors.count + klass.methods.count + klass.functions.count if !klass.symbolInfo.isBindable { skips.append(SkipEntry(symbol: fullName, cIdentifier: klass.cType, reason: .notIntrospectable, detail: "non-introspectable class")) return 0 } let (plan, memberSkips) = planClass(klass, context: context) skips.append(contentsOf: memberSkips) boundCallables += plan.constructors.count + plan.methods.count + plan.functions.count + plan.asyncMethods.count types.append(.class(plan)) return 1 } private func skipInterface(into skips: inout [SkipEntry], into types: inout [TypePlan], iface: Interface, namespace: String, context: MapContext) -> Int { let fullName = "\(namespace).\(iface.name)" if !iface.symbolInfo.isBindable { skips.append(SkipEntry(symbol: fullName, cIdentifier: iface.cType, reason: .notIntrospectable, detail: "non-introspectable interface")) return 0 } let (plan, memberSkips) = planInterface(iface, context: context) skips.append(contentsOf: memberSkips) types.append(.interface(plan)) return 1 } private func skipRecord( into skips: inout [SkipEntry], into types: inout [TypePlan], boundCallables: inout Int, totalCallables: inout Int, record: Record, namespace: String, context: MapContext ) -> Int { let fullName = "\(namespace).\(record.name)" totalCallables += record.constructors.count + record.methods.count + record.functions.count if let forType = record.isGTypeStructFor { skips.append(SkipEntry(symbol: fullName, cIdentifier: record.cType, reason: .gtypeStruct, detail: "GObject class struct for '\(forType)'")) return 0 } if record.isBoxed { if reservedSwiftTypes.contains(record.name) { skips.append(SkipEntry(symbol: fullName, cIdentifier: record.cType, reason: .unknownType, detail: "record name '\(record.name)' shadows Swift stdlib type")) return 0 } let (plan, memberSkips) = planRecord(record, context: context) skips.append(contentsOf: memberSkips) boundCallables += plan.constructors.count + plan.methods.count + plan.functions.count + plan.asyncMethods.count types.append(.record(plan)) return 1 } skips.append(SkipEntry(symbol: fullName, cIdentifier: record.cType, reason: .plainRecord, detail: "no GType registration")) return 0 } /// Plans a namespace-level callback as a `CallbackTypePlan`. /// /// Tries to map every parameter and return value through the TypeMapper. /// On success, produces a `.callback(CallbackTypePlan)` type plan. /// On failure (unmappable param/return), emits a `SkipEntry` with the /// existing `.callbackWithoutUserData` reason and a detail explaining why. /// - Returns: 1 when the callback was bound, 0 when skipped. private func planCallbackType(into skips: inout [SkipEntry], into types: inout [TypePlan], callback: Callback, namespace: String, context: MapContext) -> Int { let fullName = "\(namespace).\(callback.name)" // Map the callback through the TypeMapper — this recursively maps // every parameter and return value. If any fails, we skip with the // existing callbackWithoutUserData reason (baseline-stable). let refType = GIRType.typeRef(callback.name, namespace: namespace) do { let mapping = try map(refType, nullable: false, transfer: .none, context: context) let plan = CallbackTypePlan( name: callback.name, swiftType: mapping.swiftType, cSwiftType: mapping.cSwiftType, doc: callback.doc ) types.append(.callback(plan)) return 1 } catch let error as MapError { skips.append(SkipEntry(symbol: fullName, cIdentifier: callback.cType, reason: error.reason == .callbackWithoutUserData ? .callbackWithoutUserData : error.reason, detail: "callback '\(callback.name)' has unmappable param/return: \(error.detail)")) return 0 } catch { skips.append(SkipEntry(symbol: fullName, cIdentifier: callback.cType, reason: .callbackWithoutUserData, detail: "callback '\(callback.name)' unmappable")) return 0 } } /// Plans a namespace-level function, routing `moved-to` declarations into /// their record or class owner rather than emitting a free function. private func planOrSkipFunction(into skips: inout [SkipEntry], into types: inout [TypePlan], fn: GlobalFunction, namespace: String, context: MapContext) -> Int { let fullName = "\(namespace).\(fn.name)" if knownMisleadingCFunctions.contains(fn.cIdentifier) { skips.append(SkipEntry(symbol: fullName, cIdentifier: fn.cIdentifier, reason: .unknownType, detail: "return type '\(fn.cIdentifier)' maps as value but is actually a C pointer")) return 0 } if let movedTo = fn.symbolInfo.movedTo { let components = movedTo.split(separator: ".") guard components.count >= 2 else { skips.append(SkipEntry(symbol: fullName, cIdentifier: fn.cIdentifier, reason: .movedToTargetMissing, detail: "moved-to target '\(movedTo)' is not a type member")) return 0 } let targetName = String(components[components.count - 2]) let memberName = swiftFunctionName(String(components[components.count - 1])) guard let targetIndex = types.firstIndex(where: { typePlan in switch typePlan { case .record(let plan): return plan.name == targetName case .class(let plan): return plan.name == targetName default: return false } }) else { skips.append(SkipEntry(symbol: fullName, cIdentifier: fn.cIdentifier, reason: .movedToTargetMissing, detail: "moved-to target type '\(targetName)' was not planned")) return 0 } switch planFunction(fn, context: context) { case .skip(let entry): skips.append(entry) return 0 case .success(let plan): let routed = CallablePlan( name: memberName, cIdentifier: plan.cIdentifier, parameters: plan.parameters, returnMapping: plan.returnMapping, isStatic: true, isConstructor: false, ownershipInit: nil, throwsError: plan.throwsError, isOverride: false, doc: plan.doc) switch types[targetIndex] { case .record(let recordPlan): var functions = recordPlan.functions if functions.contains(where: { $0.cIdentifier == routed.cIdentifier }) { return 1 } functions.append(routed) functions = dedupBySignature(functions, isConstructor: false, symbolPrefix: "\(namespace).\(targetName)", skips: &skips) types[targetIndex] = .record(RecordPlan( name: recordPlan.name, cType: recordPlan.cType, getTypeFunction: recordPlan.getTypeFunction, copyFunction: recordPlan.copyFunction, copyReturnsVoid: recordPlan.copyReturnsVoid, freeFunction: recordPlan.freeFunction, constructors: recordPlan.constructors, methods: recordPlan.methods, functions: functions, asyncMethods: recordPlan.asyncMethods, doc: recordPlan.doc)) case .class(let classPlan): var functions = classPlan.functions if functions.contains(where: { $0.cIdentifier == routed.cIdentifier }) { return 1 } functions.append(routed) functions = dedupBySignature(functions, isConstructor: false, symbolPrefix: "\(namespace).\(targetName)", skips: &skips) types[targetIndex] = .class(ClassPlan( name: classPlan.name, girName: classPlan.girName, cType: classPlan.cType, parent: classPlan.parent, parentGIRName: classPlan.parentGIRName, isOpen: classPlan.isOpen, isAbstract: classPlan.isAbstract, getTypeFunction: classPlan.getTypeFunction, descendsFromInitiallyUnowned: classPlan.descendsFromInitiallyUnowned, refFunc: classPlan.refFunc, unrefFunc: classPlan.unrefFunc, interfaces: classPlan.interfaces, constructors: classPlan.constructors, methods: classPlan.methods, functions: functions, properties: classPlan.properties, signals: classPlan.signals, asyncMethods: classPlan.asyncMethods, doc: classPlan.doc)) default: fatalError("moved-to target changed while routing") } return 1 } } if let skipEntry = checkBindable(fn.symbolInfo, fullName: fullName, cIdentifier: fn.cIdentifier) { skips.append(skipEntry) return 0 } switch planFunction(fn, context: context) { case .success(let plan): types.append(.callable(plan)) return 1 case .skip(let entry): skips.append(entry) return 0 } } /// Known C symbols that aren't exported by the system library /// (macros, inline functions, or GType getters for types not in the .so). private let knownMissingCFunctions: Set = [ "g_fsync", "g_strv_get_type", "g_variant_get_gtype", "g_get_monotonic_time_ns", // returns uint64_t (UInt64), not guint64 (UInt) // gstdio.h wrappers: on non-Windows these are `#define g_open open` macros, // so no real symbol is exported and Swift cannot import them. "g_access", "g_chdir", "g_chmod", "g_creat", "g_fopen", "g_freopen", "g_fsync", "g_lstat", "g_mkdir", "g_open", "g_close", "g_remove", "g_rename", "g_rmdir", "g_stat", "g_unlink", "g_utime", // Removed from modern GLib (present in the GIR, absent from the shared // object) — these fail only at link time, not compile time. "g_thread_init", "g_thread_init_with_errorcheck_mutexes", // Declared in , which the public umbrella // () deliberately does not include (implementor-only API) — // exported by the .so but invisible to the Clang importer. "g_settings_backend_changed", "g_settings_backend_changed_tree", "g_settings_backend_get_default", "g_settings_backend_path_changed", "g_settings_backend_keys_changed", "g_settings_backend_path_writable_changed", "g_settings_backend_writable_changed", "g_null_settings_backend_new", "g_memory_settings_backend_new", "g_keyfile_settings_backend_new", // Declared in , likewise excluded from . "g_networking_init", // Declared in the GdkPixbuf GIR but not exported through the public // umbrella header. "gdk_pixbuf_non_anim_new", // Declared in , which the public // umbrella () deliberately does not include — Broadway is an // optional backend; unlike the GPU renderers (, // , both included), its header is excluded. "gsk_broadway_renderer_new", ] let knownMisleadingCFunctions: Set = [ "g_utf8_to_ucs4", // returns gunichar* (UInt32*) "g_utf8_to_ucs4_fast", // returns gunichar* (UInt32*) "g_utf8_to_utf16", // returns gunichar2* (UInt16*) "g_get_charset", // const gchar** out-param → const/mutable mismatch "g_get_console_charset", // same as g_get_charset ] /// Swift type names that boxed records must not shadow. let reservedSwiftTypes: Set = [ "String", "Int", "Bool", "Double", "Float", "Array", "Dictionary", "Set", "Optional", "Data", "Date", "URL", "Error", "Void", "Any", "Object", "Type", "Protocol", ] /// Plans a boxed record and excludes its lifetime-management methods from the /// public wrapper surface. `free` and `unref` are invoked by the wrapper's /// `deinit`; exposing either would permit a caller to double-release the pointer. func planRecord(_ record: Record, context: MapContext) -> (plan: RecordPlan, memberSkips: [SkipEntry]) { let girName = "\(context.currentNamespace).\(record.name)" let pair = resolvedCopyFreePair(record) var memberSkips: [SkipEntry] = [] func collect(_ result: CallablePlanResult, into plans: inout [CallablePlan]) { switch result { case .success(let plan): plans.append(plan) case .skip(let entry): memberSkips.append(entry) } } var constructorPlans: [CallablePlan] = [] for ctor in record.constructors where ctor.symbolInfo.isBindable { collect(planConstructor(ctor, className: record.name, descendsIU: false, context: context), into: &constructorPlans) } constructorPlans = dedupBySignature(constructorPlans, isConstructor: true, symbolPrefix: girName, skips: &memberSkips) var methodPlans: [CallablePlan] = [] for method in record.methods where method.symbolInfo.isBindable && !["free", "unref"].contains(method.name) { collect(planMethod(method, declaringGIRName: girName, context: context), into: &methodPlans) } methodPlans = dedupBySignature(methodPlans, isConstructor: false, symbolPrefix: girName, skips: &memberSkips) var functionPlans: [CallablePlan] = [] for fn in record.functions where fn.symbolInfo.isBindable { collect(planFunction(fn, context: context), into: &functionPlans) } functionPlans = dedupBySignature(functionPlans, isConstructor: false, symbolPrefix: girName, skips: &memberSkips) // ── GIO async pattern ── let allOwnerPlans = methodPlans + functionPlans let methodAsyncPairs = planAsyncPairs( candidates: record.methods.filter { $0.symbolInfo.isBindable && !["free", "unref"].contains($0.name) }.map { AsyncStarterCandidate(name: $0.name, cIdentifier: $0.cIdentifier, parameters: $0.parameters, doc: $0.doc) }, planned: allOwnerPlans, isStatic: false, fullNamePrefix: girName, ownerSwiftName: record.name, context: context) let functionAsyncPairs = planAsyncPairs( candidates: record.functions.filter(\.symbolInfo.isBindable).map { AsyncStarterCandidate(name: $0.name, cIdentifier: $0.cIdentifier, parameters: $0.parameters, doc: $0.doc) }, planned: allOwnerPlans, isStatic: true, fullNamePrefix: girName, ownerSwiftName: record.name, context: context) let asyncMethods = methodAsyncPairs + functionAsyncPairs removeStaleAsyncSkips(&memberSkips, for: asyncMethods) let plan = RecordPlan( name: record.name, cType: record.cType, getTypeFunction: record.getTypeFunction, copyFunction: pair.copy, copyReturnsVoid: pair.copyReturnsVoid, freeFunction: pair.free, constructors: constructorPlans, methods: methodPlans, functions: functionPlans, asyncMethods: asyncMethods, doc: record.doc ) return (plan, memberSkips) } /// Resolves a boxed record's destructor C identifier. /// /// Prefers the explicit GIR `free-function` attribute; otherwise falls back to /// the record's own parameterless `unref` (preferred, ref-counted) or `free` /// method. `g_boxed_free` is deliberately *not* used as a fallback: it lives in /// GObject, which the GLib module cannot link against (GObject depends on GLib, /// not the reverse). Returns `nil` when no safe destructor exists — the wrapper /// then renders without a `deinit` (documented leak) rather than risk a wrong /// free. /// Resolves a boxed record's paired copy and free functions, ensuring they /// come from a consistent category (refcount: `ref`/`unref`, or copy: /// `copy`/`free`). Returns `(nil, nil)` when no consistent pair exists. /// /// `copyReturnsVoid` flags a chosen copy/ref function whose C return type is /// `void` (e.g. `gst_atomic_queue_ref`) rather than the pointer — most /// `ref`/`copy` functions follow the `T *fn(T *)` self-returning convention /// `init(retaining:)` assumes, but some plain refcount bumps don't. private func resolvedCopyFreePair(_ record: Record) -> (copy: String?, copyReturnsVoid: Bool, free: String?) { if let explicitCopy = record.copyFunction, let explicitFree = record.freeFunction { return (explicitCopy, false, explicitFree) } // Prefer refcount semantics (ref + unref together). let ref = record.instanceReleaseMethod(named: ["ref"]) let unref = record.instanceReleaseMethod(named: ["unref"]) if let ref, let unref { return (ref.cIdentifier, ref.returnsVoid, unref.cIdentifier) } // Fall back to copy/free semantics. let copy = record.instanceReleaseMethod(named: ["copy"]) let free = record.instanceReleaseMethod(named: ["free"]) if let copy, let free { return (copy.cIdentifier, copy.returnsVoid, free.cIdentifier) } // No consistent pair found via method scan. If an explicit GIR attribute // was provided (e.g. only `copy-function`), trust it; when both sides come // from auto-detected methods of conflicting categories, return nil for both // to avoid mixing e.g. copy + unref. if record.copyFunction != nil || record.freeFunction != nil { return (record.copyFunction, false, record.freeFunction) } return (nil, false, nil) } func resolvedFreeFunction(_ record: Record) -> String? { resolvedCopyFreePair(record).free } func resolvedCopyFunction(_ record: Record) -> String? { resolvedCopyFreePair(record).copy } /// Returns whether the resolved boxed copy/ref function returns `void`. func resolvedCopyReturnsVoid(_ record: Record) -> Bool { resolvedCopyFreePair(record).copyReturnsVoid } extension Record { /// The first bindable instance method matching one of `names` (in /// priority order) that takes no arguments beyond the instance itself — /// the shape every canonical `ref`/`unref`/`copy`/`free` shares. Returns /// the C identifier alongside whether the method's return type is /// `void` (see `resolvedCopyFreePair`'s `copyReturnsVoid`). func instanceReleaseMethod(named names: [String]) -> (cIdentifier: String, returnsVoid: Bool)? { for wanted in names { if let method = methods.first(where: { $0.name == wanted && $0.symbolInfo.isBindable && $0.parameters.allSatisfy(\.isInstanceParameter) }) { return (method.cIdentifier, method.returnValue.type == .void) } } return nil } } /// Plans an interface as a Swift protocol, planning its instance methods as /// protocol requirements. /// /// A GObject interface method is dispatched through the implementing class's /// instance pointer — the concrete C symbol (e.g. `gtk_widget_get_buildable_id`) /// lives on the class, not the interface. So each interface method becomes a /// protocol *requirement* only; the implementing class supplies the body via /// its own `planClass` methods. Methods that cannot be planned (unsupported /// types, varargs, …) are returned as skip entries, mirroring `planClass`. /// /// - Returns: The interface plan and the skip entries for its unplannable /// methods. func planInterface(_ iface: Interface, context: MapContext) -> (plan: InterfacePlan, memberSkips: [SkipEntry]) { let registry = context.registry // Resolve prerequisite types through registry let prereqSwiftNames: [String] = iface.prereqs.compactMap { prereq in let qualified = prereq.contains(".") ? prereq : "\(context.currentNamespace).\(prereq)" guard let resolved = registry.resolve(girName: qualified) else { return nil } return registry.swiftTypeName(for: resolved, in: context.currentModule) } // A prerequisite that resolves to a *class* (not another interface) means // every conformer must inherit from that class (Swift: `Self: SomeClass` // constraint from the protocol's own inheritance clause). The concrete // `Ref` wrapper must then subclass it directly, rather than // declaring its own bare `pointer`/ref-count storage, to satisfy that // constraint (e.g. `TlsServerConnection` requires `Self: TlsConnection`). let classPrereq: String? = iface.prereqs.compactMap { prereq -> String? in let qualified = prereq.contains(".") ? prereq : "\(context.currentNamespace).\(prereq)" guard let resolved = registry.resolve(girName: qualified) else { return nil } guard case .object = resolved.category else { return nil } return registry.swiftTypeName(for: resolved, in: context.currentModule) }.first // Instance methods become protocol requirements. Unplannable ones are // recorded as skips, matching class member behaviour. var memberSkips: [SkipEntry] = [] var methodPlans: [CallablePlan] = [] for method in iface.methods where method.symbolInfo.isBindable { switch planMethod(method, context: context) { case .success(let plan): methodPlans.append(plan) case .skip(let entry): memberSkips.append(entry) } } // ── GIO async pattern ── let ifaceGirName = "\(context.currentNamespace).\(iface.name)" let asyncMethods = planAsyncPairs( candidates: iface.methods.filter(\.symbolInfo.isBindable).map { AsyncStarterCandidate(name: $0.name, cIdentifier: $0.cIdentifier, parameters: $0.parameters, doc: $0.doc) }, planned: methodPlans, isStatic: false, fullNamePrefix: ifaceGirName, context: context) removeStaleAsyncSkips(&memberSkips, for: asyncMethods) // ── Properties ── // Interfaces publish properties as `{ get }` / `{ get set }` requirements, // not bodies, so there is nothing to delegate to — the conforming class // supplies the implementation. Plan them with no delegation candidates. var propertyPlans: [PropertyPlan] = [] for prop in iface.properties where prop.symbolInfo.isBindable { switch planProperty(prop, fullName: "\(context.currentNamespace).\(iface.name).\(prop.name)", methods: [], context: context) { case .success(let plan): propertyPlans.append(plan) case .skip(let entry): memberSkips.append(entry) } } // ── Signals ── var signalPlans: [SignalPlan] = [] for signal in iface.signals where signal.symbolInfo.isBindable { let result = planSignal(signal, onClass: iface.name, namespace: context.currentNamespace, ownerIsInterface: true, context: context) if let plan = result.plan { signalPlans.append(plan) } else if let skip = result.skip { memberSkips.append(skip) } } // Compute the module-qualified name for matching ClassPlan.interfaces. let qualifiedName: String = { let girName = "\(context.currentNamespace).\(iface.name)" guard let resolved = registry.resolve(girName: girName) else { return iface.name } return registry.swiftTypeName(for: resolved, in: context.currentModule) }() let plan = InterfacePlan( name: iface.name, cType: iface.cType, prereqs: prereqSwiftNames, classPrereq: classPrereq, getTypeFunction: iface.getTypeFunction, methods: methodPlans, properties: propertyPlans, signals: signalPlans, asyncMethods: asyncMethods, qualifiedName: qualifiedName, doc: iface.doc ) return (plan, memberSkips) } // MARK: - Property planning /// Outcome of planning a single property. private enum PropertyResult { case success(PropertyPlan) case skip(SkipEntry) } /// The user-facing parameters of a callable, i.e. everything except the /// implicit instance parameter and any out-parameters. private func userParameters(_ plan: CallablePlan) -> [ParameterPlan] { plan.parameters.filter { !$0.isInstanceParameter && !$0.isOutParameter } } /// Finds an instance method usable as a property getter: named `name`, taking /// no user arguments, returning a value, and not throwing. private func delegableGetter(named name: String, in methods: [CallablePlan]) -> CallablePlan? { methods.first { m in m.name == name && !m.isStatic && !m.isConstructor && !m.throwsError && m.returnMapping != nil && m.parameters.contains(where: \.isInstanceParameter) && userParameters(m).isEmpty } } /// Finds an instance method usable as a property setter: named `name`, taking /// exactly one user argument, and not throwing. Returns the method paired with /// that sole argument. private func delegableSetter(named name: String, in methods: [CallablePlan]) -> (method: CallablePlan, argument: ParameterPlan)? { for m in methods where m.name == name && !m.isStatic && !m.isConstructor && !m.throwsError && m.parameters.contains(where: \.isInstanceParameter) { let args = userParameters(m) if args.count == 1 { return (m, args[0]) } } return nil } /// Plans a GObject property as a Swift computed property. /// /// Each accessor is resolved independently, preferring delegation to a /// `getter=` / `setter=` method (from `methods`) over the uniform GValue path, /// because the method already encodes the correct nullability and ownership. /// Delegation is only chosen when it yields a single consistent Swift type for /// the property (getter return type == setter argument type); otherwise the /// property falls back to GValue accessors, and is skipped if its type has no /// GValue support either. private func planProperty(_ property: Property, fullName: String, methods: [CallablePlan], context: MapContext) -> PropertyResult { func skip(_ reason: SkipReason, _ detail: String) -> PropertyResult { .skip(SkipEntry(symbol: fullName, cIdentifier: property.name, reason: reason, detail: detail)) } do { let mapping = try map(property.type, nullable: property.isNullable, transfer: property.transferOwnership, context: context) let writable = property.isWritable && !property.isConstructOnly let getM = property.getter.flatMap { delegableGetter(named: swiftFunctionName($0), in: methods) } let setM = property.setter.flatMap { delegableSetter(named: swiftFunctionName($0), in: methods) } /// The GValue fallback accessor for this property's mapped type, or nil /// if the type cannot be represented as a GValue. func gvalueAccessor() -> PropertyAccessorPlan? { mapping.gvalue.map { .gvalue(typeMacro: $0.typeMacro, valueSuffix: $0.getterSuffix, hasCopyFunction: $0.hasCopyFunction) } } let swiftType: String let getter: PropertyAccessorPlan let setter: PropertyAccessorPlan? if writable { // A writable computed property needs get and set to share one type. // Delegate both only when their types agree; otherwise use GValue // for both so the type is unambiguous. if let g = getM, let s = setM, let gType = g.returnMapping?.swiftType, gType == s.argument.mapping.swiftType { swiftType = gType getter = .delegate(method: g.name, argumentLabel: nil) setter = .delegate(method: s.method.name, argumentLabel: s.argument.swiftName) } else { guard let gv = gvalueAccessor() else { return skip(.unsupportedGValueCategory, "writable property type '\(mapping.swiftType)' has no GValue support and no matching getter=/setter= methods") } swiftType = mapping.swiftType getter = gv setter = gv } } else { // Read-only: delegate the getter when available, else GValue. if let g = getM, let gType = g.returnMapping?.swiftType { swiftType = gType getter = .delegate(method: g.name, argumentLabel: nil) setter = nil } else { guard let gv = gvalueAccessor() else { return skip(.unsupportedGValueCategory, "property type '\(mapping.swiftType)' has no GValue support and no matching getter= method") } swiftType = mapping.swiftType getter = gv setter = nil } } return .success(PropertyPlan( swiftName: swiftFunctionName(property.name), girName: property.name, swiftType: swiftType, getter: getter, setter: setter, doc: property.doc)) } catch let err as MapError { return skip(err.reason, err.detail) } catch { return skip(.unknownType, "unexpected error: \(error)") } } /// Plans a class as a GObject wrapper with cross-module inheritance, planning /// its constructors, instance methods, and static functions. Members that /// cannot be planned are returned as skip entries (the class itself is still /// generated — Rule 3 skips the *member*, not the whole type). /// /// - Returns: The class plan and the skip entries for its unplannable members. func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberSkips: [SkipEntry]) { let girName = "\(context.currentNamespace).\(klass.name)" let registry = context.registry // Resolve parent let parentSwiftName: String? let parentGIRNameResolved: String? if let parentGIR = klass.parent { let qualifiedParent = parentGIR.contains(".") ? parentGIR : "\(context.currentNamespace).\(parentGIR)" if let resolved = registry.resolve(girName: qualifiedParent) { parentSwiftName = registry.swiftTypeName(for: resolved, in: context.currentModule) parentGIRNameResolved = resolved.girName } else { parentSwiftName = nil // foreign or unknown — treat as root parentGIRNameResolved = nil } } else { parentSwiftName = nil parentGIRNameResolved = nil } let isOpen = registry.subclassedTypes().contains(girName) let descendsIU = registry.descendsFromInitiallyUnowned(girName) let (refFunc, unrefFunc) = registry.refUnrefFunctions(for: girName) // Resolve implemented interfaces to their Swift names, excluding any // already provided by an ancestor class (redundant conformance is a // Swift error, e.g. `DataInputStream: …, Seekable` when its ancestor // `BufferedInputStream` already conforms). let ancestorInterfaceGirNames: Set = Set(registry.ancestry(of: girName).flatMap { ancestor -> [String] in if case .object(_, _, _, let ifaces, _, _) = ancestor.category { return ifaces } return [] }) let interfaceNames: [String] = klass.implements.compactMap { ifaceName in let qualified = ifaceName.contains(".") ? ifaceName : "\(context.currentNamespace).\(ifaceName)" guard !ancestorInterfaceGirNames.contains(qualified) else { return nil } guard let resolved = registry.resolve(girName: qualified) else { return nil } return registry.swiftTypeName(for: resolved, in: context.currentModule) } // ── Members ── var memberSkips: [SkipEntry] = [] /// Records the outcome of planning one member. func collect(_ result: CallablePlanResult, into plans: inout [CallablePlan]) { switch result { case .success(let plan): plans.append(plan) case .skip(let entry): memberSkips.append(entry) } } var constructorPlans: [CallablePlan] = [] // Abstract classes cannot be instantiated directly — their constructors // belong to concrete subclasses, so skip them here. if !klass.isAbstract { for ctor in klass.constructors where ctor.symbolInfo.isBindable { collect(planConstructor(ctor, className: klass.name, descendsIU: descendsIU, context: context), into: &constructorPlans) } } constructorPlans = dedupBySignature(constructorPlans, isConstructor: true, symbolPrefix: girName, skips: &memberSkips) var methodPlans: [CallablePlan] = [] for method in klass.methods where method.symbolInfo.isBindable { collect(planMethod(method, declaringGIRName: girName, context: context), into: &methodPlans) } methodPlans = dedupBySignature(methodPlans, isConstructor: false, symbolPrefix: girName, skips: &memberSkips) var functionPlans: [CallablePlan] = [] for fn in klass.functions where fn.symbolInfo.isBindable { collect(planFunction(fn, context: context), into: &functionPlans) } // ── GIO async pattern ── // Runs once ordinary methods/functions are planned, so a `*_finish` // sibling declared later in `klass.methods`/`klass.functions` is // already available for rule 3 to match against. let allOwnerPlans = methodPlans + functionPlans let methodAsyncPairs = planAsyncPairs( candidates: klass.methods.filter(\.symbolInfo.isBindable).map { AsyncStarterCandidate(name: $0.name, cIdentifier: $0.cIdentifier, parameters: $0.parameters, doc: $0.doc) }, planned: allOwnerPlans, isStatic: false, fullNamePrefix: girName, ownerSwiftName: klass.name, context: context) let functionAsyncPairs = planAsyncPairs( candidates: klass.functions.filter(\.symbolInfo.isBindable).map { AsyncStarterCandidate(name: $0.name, cIdentifier: $0.cIdentifier, parameters: $0.parameters, doc: $0.doc) }, planned: allOwnerPlans, isStatic: true, fullNamePrefix: girName, ownerSwiftName: klass.name, context: context) let asyncMethods = methodAsyncPairs + functionAsyncPairs removeStaleAsyncSkips(&memberSkips, for: asyncMethods) // ── Properties ── // Pass the planned methods so accessors can delegate to a `getter=`/ // `setter=` method when GIR names one and it was itself successfully planned. var propertyPlans: [PropertyPlan] = [] for prop in klass.properties where prop.symbolInfo.isBindable { switch planProperty(prop, fullName: "\(girName).\(prop.name)", methods: methodPlans, context: context) { case .success(let plan): propertyPlans.append(plan) case .skip(let entry): memberSkips.append(entry) } } // A property whose Swift name collides with a planned method/constructor/ // function of the same class is dropped — Swift rejects the redundant // redeclaration (e.g. `Pango.FontFamily.isVariable` property vs. // `isVariable()` method, both derived from the same GIR member). let takenCallableNames: Set = Set( methodPlans.map(\.name) + constructorPlans.map(\.name) + functionPlans.map(\.name) ) propertyPlans = propertyPlans.filter { prop in guard takenCallableNames.contains(prop.swiftName) else { return true } memberSkips.append(SkipEntry(symbol: "\(girName).\(prop.swiftName)", cIdentifier: prop.girName, reason: .nameCollision, detail: "property '\(prop.swiftName)' collides with a method of the same name")) return false } // ── Signals ── var signalPlans: [SignalPlan] = [] for signal in klass.signals where signal.symbolInfo.isBindable { let result = planSignal(signal, onClass: klass.name, namespace: context.currentNamespace, context: context) if let plan = result.plan { signalPlans.append(plan) } else if let skip = result.skip { memberSkips.append(skip) } } let plan = ClassPlan( name: klass.name, girName: girName, cType: klass.cType, parent: parentSwiftName, parentGIRName: parentGIRNameResolved, isOpen: isOpen, isAbstract: klass.isAbstract, getTypeFunction: klass.getTypeFunction, descendsFromInitiallyUnowned: descendsIU, refFunc: refFunc, unrefFunc: unrefFunc, interfaces: interfaceNames, constructors: constructorPlans, methods: methodPlans, functions: functionPlans, properties: propertyPlans, signals: signalPlans, asyncMethods: asyncMethods, doc: klass.doc ) return (plan, memberSkips) } /// Plans an enumeration, deduplicating raw values. /// /// Members with duplicate raw values become `public static var` aliases /// pointing to the first case with that value. func planEnumeration(_ enumeration: Enumeration, context: MapContext) -> EnumPlan { var cases: [EnumCase] = [] var aliases: [EnumAlias] = [] var seenRawValues: [String: String] = [:] // rawValue → first case name for member in enumeration.members { let swiftName = swiftEnumCaseName(member.name) if let firstCase = seenRawValues[member.value] { aliases.append(EnumAlias(name: swiftName, targetCaseName: firstCase)) } else { cases.append(EnumCase(name: swiftName, rawValue: member.value, cIdentifier: member.cIdentifier)) seenRawValues[member.value] = swiftName } } return EnumPlan( name: enumeration.name, cType: enumeration.cType, cases: cases, aliases: aliases, hasGType: enumeration.getTypeFunction != nil, doc: enumeration.doc ) } /// Plans a bitfield as an `OptionSet` with verbatim raw values. func planBitfield(_ bitfield: Bitfield, context: MapContext) -> BitfieldPlan { let members = bitfield.members.map { member in EnumCase( name: swiftEnumCaseName(member.name), rawValue: member.value, cIdentifier: member.cIdentifier ) } return BitfieldPlan( name: bitfield.name, cType: bitfield.cType, members: members, hasGType: bitfield.getTypeFunction != nil, doc: bitfield.doc ) } /// Plans a global constant. Returns `nil` when the constant's type cannot /// be mapped, so the caller records a skip entry. /// /// The GIR name (`PARAM_MASK`) is converted to Swift's lowerCamelCase /// convention for constants (`paramMask`, like `Double.pi`); the original /// spelling is preserved in `girName` for the rendered doc comment. func planConstant(_ constant: Constant, context: MapContext) -> ConstantPlan? { let mappingResult = Result { try map(constant.type, nullable: false, transfer: .none, context: context) } guard case .success(let mapping) = mappingResult else { return nil } // A bitfield/enum-typed constant's GIR value is a bare C integer // literal (e.g. `"15"`), but the Swift type is an `OptionSet` struct or // a raw-value `enum` — neither is integer-literal-expressible, so the // literal must be routed through `Type(rawValue:)`. Every other // constant type (numeric, string, boolean) already carries a // Swift-literal-compatible value verbatim. let value: String switch mapping.marshalIn { case .bitfieldRaw: value = "\(mapping.swiftType)(rawValue: numericCast(\(constant.value)))" case .enumRaw: value = "\(mapping.swiftType)(rawValue: \(constant.value))!" default: value = constant.value } return ConstantPlan(name: swiftConstantName(constant.name), girName: constant.name, value: value, swiftType: mapping.swiftType, doc: constant.doc) } /// Outcome of alias planning: success with a plan, or skip with a reason. enum AliasPlanResult { case success(AliasPlan) case skip(SkipEntry) } /// Plans a type alias, resolving the target type through the mapper. func planAlias(_ alias: Alias, context: MapContext) -> AliasPlanResult { let fullName = "\(context.currentNamespace).\(alias.name)" let mappingResult = Result { try map(alias.target, nullable: false, transfer: .none, context: context) } switch mappingResult { case .success(let mapping): return .success(AliasPlan(name: alias.name, swiftType: mapping.swiftType, doc: alias.doc)) case .failure(let error as MapError): return .skip(SkipEntry(symbol: fullName, cIdentifier: alias.cType, reason: error.reason, detail: error.detail)) case .failure: return .skip(SkipEntry(symbol: fullName, cIdentifier: alias.cType, reason: .unknownType, detail: "alias target type could not be mapped")) } } // MARK: - Callable planning /// Outcome of trying to plan a callable: either a complete plan or a skip entry. enum CallablePlanResult { case success(CallablePlan) case skip(SkipEntry) } /// Plans a function or method: checks every parameter and the return type for /// mappability, skipping the whole callable if any part fails. The instance /// parameter (for methods) is handled by `planParameters` as a `self` /// passthrough. /// /// - Parameters: /// - fullName: Fully qualified GIR symbol name for skip reporting. /// - swiftName: The Swift function/method name (keyword-escaped). /// - cIdentifier: The C function symbol to call. /// - parameters: The callable's parameters (may include an instance parameter). /// - returnValue: The callable's return value. /// - throwsGError: Whether the callable takes a trailing `GError**`. /// - doc: Documentation from the GIR. /// - isStatic: `true` for free/static functions, `false` for instance methods. /// - context: The resolution context. /// - Returns: A complete plan, or a skip entry with a machine-readable reason. private func planCallable( fullName: String, swiftName: String, cIdentifier: String, parameters: [Parameter], returnValue: ReturnValue, throwsGError: Bool, doc: String?, isStatic: Bool, isOverride: Bool = false, context: MapContext ) -> CallablePlanResult { // Applies to methods too, not just free functions: some GIR-declared C // symbols (e.g. GSettingsBackend's `g_settings_backend_*` implementor // API) are exported by the .so but declared only in a header the public // umbrella (``) does not include, so Clang never sees the // prototype and the generated call is `cannot find in scope`. if knownMissingCFunctions.contains(cIdentifier) { return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier, reason: .unknownType, detail: "C symbol '\(cIdentifier)' is not visible through the public umbrella header")) } // Check parameters (GError** is implicit in throws="1", not in the // parameter list — the renderer synthesizes the &error arg via // cArguments(error: true). Do NOT strip the last parameter, it's real.) let paramPlanResult = planParameters(parameters, context: context) guard case .success(let paramPlans) = paramPlanResult else { if case .skip(let entry) = paramPlanResult { return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier, reason: entry.reason, detail: entry.detail)) } return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier, reason: .unknownType, detail: "parameter planning failed")) } // Map return type let returnMapping: Mapping? if returnValue.type != .void { switch Result(catching: { try map(returnValue.type, nullable: returnValue.isNullable, transfer: returnValue.transferOwnership, context: context, cType: returnValue.cType) }) { case .success(let returnMap): if !returnMap.isReadyForCallables { return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier, reason: .unknownType, detail: "return type '\(returnMap.swiftType)' is not yet generated (category: \(returnMap.category))")) } // Interfaces produce unsupported marshalOut (protocols can't be // constructed). Params still work via pointer marshal-in cases. if case .unsupported(let reason) = returnMap.marshalOut { return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier, reason: .unknownType, detail: "return type: \(reason)")) } // Callback return types cannot be constructed from C function // pointers yet — the renderer has no marshal-out support. if returnMap.category == .callback { return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier, reason: .callbackWithoutUserData, detail: "callback return type '\(returnMap.swiftType)' deferred")) } // A scalar type (`.direct`/`.numericCast` marshalOut) returned // through a pointer C type with no `` length (e.g. // `guint8` from `c:type="const guint8*"`) is a raw buffer // pointer the mapper's scalar-name match hid — cannot be // marshalled as the pointee value. if returnValue.cType.hasSuffix("*") { switch returnMap.marshalOut { case .direct, .numericCast: return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier, reason: .unknownType, detail: "return C type '\(returnValue.cType)' is a pointer to a scalar with no array length")) default: break } } returnMapping = returnMap case .failure(let error as MapError): return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier, reason: error.reason, detail: error.detail)) case .failure: return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier, reason: .unknownType, detail: "return type mapping failed")) } } else { returnMapping = nil } return .success(CallablePlan( name: swiftName, cIdentifier: cIdentifier, parameters: paramPlans, returnMapping: returnMapping, isStatic: isStatic, isConstructor: false, ownershipInit: nil, throwsError: throwsGError, isOverride: isOverride, doc: doc )) } /// Plans a namespace-level (or static) function. func planFunction(_ fn: GlobalFunction, context: MapContext) -> CallablePlanResult { planCallable( fullName: "\(context.currentNamespace).\(fn.name)", swiftName: swiftFunctionName(fn.name), cIdentifier: fn.cIdentifier, parameters: fn.parameters, returnValue: fn.returnValue, throwsGError: fn.throwsGError, doc: fn.doc, isStatic: true, context: context) } /// Plans an instance method. The instance parameter becomes `self.pointer`. /// `declaringGIRName`, when given, is the fully qualified GIR name of the /// class the method is planned for — used to detect whether an ancestor /// already declares a same-selector method (see /// `TypeRegistry.overridesAncestorMethod`), requiring Swift's `override` /// keyword. `nil` for interface methods, which have no class ancestry. func planMethod(_ method: Method, declaringGIRName: String? = nil, context: MapContext) -> CallablePlanResult { // Same-module override detection: `overridesAncestorMethod` checks only // ancestors in the same Swift module, where `override` works on `public`. let isOverride = declaringGIRName.map { girName in context.registry.overridesAncestorMethod( named: method.name, paramNames: method.parameters.filter { !$0.isInstanceParameter }.map(\.name), in: girName ) } ?? false // Cross-module conflict detection: a method that shadows a non-`open` // ancestor method from a *different* module must be skipped — Swift // rejects cross-module overrides of `public` methods. // // Same-module collisions are legitimate overrides and are NOT handled // here: they keep their `override` keyword, and the ancestor-dedup // post-pass in `planModules` drops the ones that would still clash. // The two checks are disjoint by module, and this one runs first, so a // method never reaches both. if !isOverride, let girName = declaringGIRName { if context.registry.shadowsCrossModuleAncestorMethod( named: method.name, paramNames: method.parameters.filter { !$0.isInstanceParameter }.map(\.name), in: girName ) { // Symbol and reason match the post-pass's ancestor-dedup skips so // that both mechanisms report one rule identically: the qualified // `Namespace.Class.method` form, keyed `.inheritedMember`. let shadowingName = swiftFunctionName(method.name) return .skip(SkipEntry(symbol: "\(girName).\(shadowingName)", cIdentifier: method.cIdentifier, reason: .inheritedMember, detail: "method '\(shadowingName)' shadows an identically-selectored ancestor method in a different module")) } } return planCallable( fullName: "\(context.currentNamespace).\(method.name)", swiftName: swiftFunctionName(method.name), cIdentifier: method.cIdentifier, parameters: method.parameters, returnValue: method.returnValue, throwsGError: method.throwsGError, doc: method.doc, isStatic: false, isOverride: isOverride, context: context) } /// Plans a GObject signal for a class or interface. /// /// Maps every signal parameter and the return value through the TypeMapper. /// The implicit instance parameter (C-arg 0) is synthesized using the owning /// class's type. Returns a ``SignalPlan`` on success or a skip entry on /// failure (e.g. unmappable parameter type). /// /// - Parameters: /// - signal: The signal to plan. /// - className: The owning class/interface Swift name. /// - namespace: The GIR namespace name. /// - context: The resolution context. /// - Returns: A tuple of optional plan and optional skip entry (exactly one is non-nil). private func planSignal(_ signal: Signal, onClass className: String, namespace: String, ownerIsInterface: Bool = false, context: MapContext) -> (plan: SignalPlan?, skip: SkipEntry?) { let girName = "\(context.currentNamespace).\(className).\(signal.name)" var signalParams: [ParameterPlan] = [] // Instance parameter: C-arg 0, the emitting GObject pointer. // Its Swift type is the owning class; its C type is UnsafeMutableRawPointer. let instanceParam = ParameterPlan( swiftName: "instance", cArgIndex: 0, mapping: Mapping(swiftType: className, cSwiftType: "UnsafeMutableRawPointer", marshalIn: .direct, marshalOut: .direct), isInstanceParameter: true ) signalParams.append(instanceParam) // Map each real signal parameter for (idx, param) in signal.parameters.enumerated() { do { let mapping = try map(param.type, nullable: param.isNullable, transfer: param.transferOwnership, context: context) guard mapping.isReadyForCallables else { return (nil, SkipEntry(symbol: girName, cIdentifier: nil, reason: .signalUnmappableParam, detail: "param '\(param.name)' type '\(mapping.swiftType)' not ready for callables")) } let pName = swiftParameterName(param.name) signalParams.append(ParameterPlan( swiftName: pName, cArgIndex: idx + 1, // +1 because instance param is index 0 mapping: mapping )) } catch let error as MapError { return (nil, SkipEntry(symbol: girName, cIdentifier: nil, reason: .signalUnmappableParam, detail: "param '\(param.name)': \(error.detail)")) } catch { return (nil, SkipEntry(symbol: girName, cIdentifier: nil, reason: .signalUnmappableParam, detail: "param '\(param.name)': unexpected error")) } } // Map return value let returnMapping: Mapping? if signal.returnValue.type != .void { do { let mapped = try map(signal.returnValue.type, nullable: signal.returnValue.isNullable, transfer: signal.returnValue.transferOwnership, context: context) guard mapped.isReadyForCallables else { return (nil, SkipEntry(symbol: girName, cIdentifier: nil, reason: .signalUnmappableParam, detail: "return type '\(mapped.swiftType)' not ready for callables")) } returnMapping = mapped } catch let error as MapError { return (nil, SkipEntry(symbol: girName, cIdentifier: nil, reason: .signalUnmappableParam, detail: "return: \(error.detail)")) } catch { return (nil, SkipEntry(symbol: girName, cIdentifier: nil, reason: .signalUnmappableParam, detail: "return: unexpected error")) } } else { returnMapping = nil } let swiftName = swiftFunctionName(signal.name) // The C/Swift identifier segment must be a valid identifier; GIR signal // names may contain hyphens (`"drive-changed"`). GObject treats `-` and // `_` as equivalent in signal names, so this cannot collide with a // distinct signal. The *string* passed to `g_signal_connect_data` keeps // the original hyphenated `girName` below. let trampolineSignalSegment = camelCased(signal.name.replacingOccurrences(of: "-", with: "_")) let trampolineCName = "_trampoline\(namespace)\(className)" + trampolineSignalSegment.prefix(1).uppercased() + trampolineSignalSegment.dropFirst() let plan = SignalPlan( owningClassName: className, girName: signal.name, swiftName: swiftName, isDetailed: signal.isDetailed, parameters: signalParams, returnMapping: returnMapping, trampolineCName: trampolineCName, ownerIsInterface: ownerIsInterface, doc: signal.doc ) return (plan, nil) } /// Plans a constructor as a Swift `convenience init`. The C constructor's /// returned instance pointer is adopted through the class's designated /// `init(takingOwnership:)`, which sinks a floating reference when the class /// descends from `InitiallyUnowned`. /// /// - Parameters: /// - ctor: The GIR constructor. /// - className: The owning class's name (for skip reporting). /// - descendsIU: Whether the class descends from `InitiallyUnowned`. /// - context: The resolution context. /// - Returns: A constructor plan, or a skip entry. func planConstructor(_ ctor: Constructor, className: String, descendsIU: Bool, context: MapContext) -> CallablePlanResult { let fullName = "\(context.currentNamespace).\(className).\(ctor.name)" if knownMissingCFunctions.contains(ctor.cIdentifier) { return .skip(SkipEntry(symbol: fullName, cIdentifier: ctor.cIdentifier, reason: .unknownType, detail: "C symbol '\(ctor.cIdentifier)' is not visible through the public umbrella header")) } // GError** is implicit in throws="1", not in the parameter list. let paramPlanResult = planParameters(ctor.parameters, context: context) guard case .success(let paramPlans) = paramPlanResult else { if case .skip(let entry) = paramPlanResult { return .skip(SkipEntry(symbol: fullName, cIdentifier: ctor.cIdentifier, reason: entry.reason, detail: entry.detail)) } return .skip(SkipEntry(symbol: fullName, cIdentifier: ctor.cIdentifier, reason: .unknownType, detail: "constructor parameter planning failed")) } // Constructors with out-params cannot be expressed as Swift inits, // which cannot also return out-value tuples. if paramPlans.contains(where: \.isOutParameter) { return .skip(SkipEntry(symbol: fullName, cIdentifier: ctor.cIdentifier, reason: .constructorOutParams, detail: "out-param constructors are not expressible as Swift init")) } // Derive ownership from the GIR return's transfer-ownership annotation. // Most GObject _new constructors return transfer-ownership="full" // (or a floating reference for InitiallyUnowned descendants). A // transfer-ownership="none" return indicates a borrowed reference, // which the convenience init must wrap via init(retaining:) instead // of init(takingOwnership:). // // CRITICAL: descendsIU takes precedence over the GIR annotation because // most widget constructors declare transfer-ownership="none" even though // they return a *floating* reference that must be sunk, not borrowed. let ownership: OwnershipInit if descendsIU { ownership = .sinkingRef } else if ctor.returnValue.transferOwnership == .none { ownership = .retaining } else { ownership = .takingOwnership } return .success(CallablePlan( name: swiftFunctionName(ctor.name), cIdentifier: ctor.cIdentifier, parameters: paramPlans, returnMapping: nil, isStatic: false, isConstructor: true, ownershipInit: ownership, throwsError: ctor.throwsGError, doc: ctor.doc)) } /// Plans the parameters of a callable. Returns `.skip` if any parameter /// has an unsupported direction, or if any type cannot be mapped. func planParameters( _ parameters: [Parameter], context: MapContext ) -> ParameterPlanResult { var plans: [ParameterPlan] = [] // Pre-scan: collect indices that are user-data targets for callbacks. // These are plain gpointer params that would otherwise fail the pointer // check — the callback-box mechanism handles them. let closureTargets = Set(parameters.compactMap(\.closureIndex)) // Pre-scan length parameters for in-parameter C arrays. The length C arg is // synthesized from the Swift array's `count` and dropped from the signature. let hasInstance = parameters.contains(where: \.isInstanceParameter) var lengthElision: [Int: String] = [:] for p in parameters where p.direction == .in { guard case .cArray(_, let info) = p.type, let li = info.lengthParameterIndex else { continue } let cIndex = li + (hasInstance ? 1 : 0) guard cIndex < parameters.count, parameters[cIndex].direction == .in else { continue } // Two arrays sharing one length arg can only be bridged if the caller // passes equal counts; nothing enforces that, and a mismatch overruns the // shorter buffer. Skip rather than guess (e.g. g_spawn_async_with_pipes_and_fds). if let existing = lengthElision[cIndex] { return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .arrayBridgingUnimplemented, detail: "parameters '\(existing)' and '\(p.name)' share length parameter index \(li)")) } lengthElision[cIndex] = swiftParameterName(p.name) } for (index, param) in parameters.enumerated() { // The instance parameter is always `self` — passed as `self.pointer`, // never type-checked (its type is the enclosing class, which is a // `.needsClass` mapping that would otherwise fail the ready check). if param.isInstanceParameter { plans.append(ParameterPlan( swiftName: "self", cArgIndex: index, mapping: Mapping(swiftType: "Self", cSwiftType: "UnsafeMutableRawPointer", marshalIn: .direct, marshalOut: .direct), isInstanceParameter: true)) continue } // Variadic parameter (GIR names it "..." with a child): // C variadic calls cannot be bridged from Swift. if param.name == "..." { return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .varargs, detail: "variadic ('...') parameter")) } // Direction check if param.direction == .out { // Caller-allocates out-params (`char *outbuf`, `gunichar *result`) // hand the C function a pointer to storage the CALLER must size and // own — unlike callee-allocates out-params (`T **out`) where the // callee fills in the pointee. Our out-param marshalling declares a // single Swift scalar and passes its address, which is correct only // for the callee-allocates shape; for caller-allocates it lets C // write buffer payload past a one-element slot (memory corruption) // or reinterpret payload as a pointer (segfault, e.g. // g_unichar_to_utf8). GIR carries no buffer-size field, so there is // nothing to allocate against — skip rather than guess. if param.callerAllocates { return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .outParameter, detail: "caller-allocates out-param '\(param.name)' (no buffer size in GIR)")) } // Out-params are collected and returned as Swift values rather than // passed as arguments. Map the VALUE type (not the pointer-to-pointer). // A const char** out-param points at borrowed storage even when // GIR incorrectly marks the out slot transfer-full (for example // GVariantType.string_scan's endptr). Never free a const pointee. let outTransfer: TransferOwnership = param.cType.contains("const") ? .none : param.transferOwnership let mappingResult = Result { try map(param.type, nullable: param.isNullable, transfer: outTransfer, context: context, cType: param.cType) } switch mappingResult { case .success(let paramMapping): if !paramMapping.isReadyForCallables { return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .unknownType, detail: "out-param '\(param.name)' type '\(paramMapping.swiftType)' not yet generated")) } // Skip object/boxed out-params — wrapper construction from a // raw pointer requires the type's init(takingOwnership:) which // is class-specific and deferred. if paramMapping.category == .needsClass || paramMapping.category == .needsRecord { return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .outParameter, detail: "object/boxed out-param '\(param.name)' deferred")) } let swiftName = swiftParameterName(param.name) plans.append(ParameterPlan( swiftName: swiftName, cArgIndex: index, mapping: paramMapping, isOutParameter: true)) continue case .failure(let error as MapError): return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: error.reason, detail: "out-param '\(param.name)': \(error.detail)")) case .failure: return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .unknownType, detail: "out-param '\(param.name)' type not mappable")) } } if param.direction == .inout_ { return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .inoutParameter, detail: "'\(param.name)' has direction=inout")) } // Intercept length params (argc) — synthesize from array's count. if let arrName = lengthElision[index] { plans.append(ParameterPlan( swiftName: swiftParameterName(param.name), cArgIndex: index, mapping: Mapping(swiftType: "Int", cSwiftType: "", marshalIn: .direct, marshalOut: .direct), synthesizedLengthOf: arrName)) continue } // Intercept in-parameter C arrays -> Swift [T]. Array returns, out-params, // and array-typed properties still go through `map`, which rejects them. if case .cArray(let element, let info) = param.type { switch Result(catching: { try mapArrayParameter(element: element, info: info, transfer: param.transferOwnership, context: context) }) { case .success(let arrayMapping): guard arrayMapping.isReadyForCallables else { return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .unknownType, detail: "array parameter '\(param.name)' element type '\(arrayMapping.swiftType)' not yet generated")) } plans.append(ParameterPlan( swiftName: swiftParameterName(param.name), cArgIndex: index, mapping: arrayMapping)) continue case .failure(let error as MapError): return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: error.reason, detail: "parameter '\(param.name)': \(error.detail)")) case .failure: return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .unknownType, detail: "parameter '\(param.name)' array type not mappable")) } } // Map the type let mappingResult = Result { try map(param.type, nullable: param.isNullable, transfer: param.transferOwnership, context: context) } switch mappingResult { case .success(var paramMapping): // D4.3 callback-param binding remains deferred: passing a Swift // closure captured as `@convention(c)` through generic // `_ClosureBox` storage triggers a Swift compiler ICE // ("failed to produce diagnostic for expression") on functions // like `g_qsort_with_data`/`g_dataset_foreach`. See HANDOFF.md. if case .callbackBox = paramMapping.marshalIn { return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .callbackWithoutUserData, detail: "callback param '\(param.name)' deferred to Phase D4.3")) } let isString = paramMapping.marshalIn == .stringToC // Only a single-level `const gchar*` input string can be bridged // from an immutable Swift `String` via `withCString`. A mutable // `gchar*` is a caller-allocated output buffer, and a `gchar**` // (two pointer levels, e.g. `const char* const*`) is a string // vector — both are deferred to array/out-parameter handling. if isString { let pointerLevels = param.cType.filter { $0 == "*" }.count if !param.cType.contains("const") || pointerLevels != 1 { return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .unknownType, detail: "parameter '\(param.name)' is not a single const input string ('\(param.cType)')")) } } // Non-string pointer parameters that aren't mapped as objects or if param.cType.hasSuffix("*") && !isString, paramMapping.category != .needsClass, paramMapping.category != .needsRecord, !closureTargets.contains(index) { return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .unknownType, detail: "parameter '\(param.name)' C type '\(param.cType)' is a pointer")) } let swiftName = swiftParameterName(param.name) // Only record a separate closureIndex when it differs from the // callback param's own C arg index (callback doubling as user-data // is the common case and needs no replacement). let planClosure: Int? = (param.closureIndex != index) ? param.closureIndex : nil plans.append(ParameterPlan( swiftName: swiftName, cArgIndex: index, mapping: paramMapping, isInstanceParameter: param.isInstanceParameter, closureIndex: planClosure, destroyIndex: param.destroyIndex )) case .failure(let error as MapError): return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: error.reason, detail: "parameter '\(param.name)': \(error.detail)")) case .failure: return .skip(SkipEntry(symbol: "", cIdentifier: nil, reason: .unknownType, detail: "parameter '\(param.name)' type not mappable")) } } return .success(plans) } enum ParameterPlanResult { case success([ParameterPlan]) case skip(SkipEntry) } // MARK: - Naming helpers /// Converts a snake_case identifier to lowerCamelCase without dropping any /// segment. The GIR `name` attribute is already namespace-stripped (e.g. /// `"set_application_name"`, not `"g_set_application_name"`), so no prefix /// removal is wanted — doing so was the bug that turned setters like /// `g_set_application_name` into `applicationName`, silently dropping the verb. /// /// Fully-uppercase segments are GIR *words*, not acronyms to preserve: /// `cclosure_marshal_BOOLEAN__BOXED_BOXED` means "boolean, boxed, boxed" and /// `PARAM_MASK` means "param mask". Passing them through verbatim produced /// unreadable runs (`cclosureMarshalBOOLEANBOXEDBOXED`), so each segment is /// case-normalized to a single capitalized word (`Boolean`, `Boxed`). /// /// A leading digit (which cannot begin a Swift identifier even when /// backtick-escaped) is guarded with an underscore. /// /// - Parameter snake: A snake_case identifier from the GIR. /// - Returns: The lowerCamelCase spelling. func camelCased(_ snake: String) -> String { let normalized = snake.replacingOccurrences(of: "-", with: "_") let parts = normalized.split(separator: "_", omittingEmptySubsequences: true) .map { normalizeUppercaseSegment(String($0)) } guard let first = parts.first else { return snake } var result = first.lowercased() for word in parts.dropFirst() { result += word.prefix(1).uppercased() + word.dropFirst() } if result.first?.isNumber == true { result = "_\(result)" } return result } /// Case-normalizes one underscore-delimited GIR segment: a segment that is /// entirely uppercase (letters/digits, at least two characters, e.g. `BOOLEAN`, /// `UINT`, `CSET`) is folded to a single capitalized word (`Boolean`, `Uint`, /// `Cset`); everything else passes through unchanged. private func normalizeUppercaseSegment(_ segment: String) -> String { guard segment.count >= 2, segment.contains(where: \.isUppercase), segment.allSatisfy({ $0.isUppercase || $0.isNumber }) else { return segment } return segment.prefix(1) + segment.dropFirst().lowercased() } /// Converts a GIR function/method name to a Swift name (lowerCamelCase), /// escaping keywords. Example: `"set_application_name"` → `"setApplicationName"`, /// `"show"` → `"show"`. func swiftFunctionName(_ girName: String) -> String { let camel = camelCased(girName) guard !camel.isEmpty else { return girName } return swiftKeywords.contains(camel) ? "`\(camel)`" : camel } /// Converts a GIR constant name (`SCREAMING_SNAKE`) to a Swift constant name /// (lowerCamelCase, per the API Design Guidelines — like `Double.pi`), /// escaping keywords. Example: `"PARAM_MASK"` → `"paramMask"`. func swiftConstantName(_ girName: String) -> String { let camel = camelCased(girName) guard !camel.isEmpty else { return girName } return swiftKeywords.contains(camel) ? "`\(camel)`" : camel } /// Converts a GIR parameter name to a Swift parameter name (lowerCamelCase), /// escaping keywords. Example: `"application_name"` → `"applicationName"`. func swiftParameterName(_ girName: String) -> String { let camel = camelCased(girName) guard !camel.isEmpty else { return girName } if swiftKeywords.contains(camel) { return "`\(camel)`" } return camel } /// Converts a GIR member name to a Swift enum case name, escaping keywords. /// /// Swift keywords and names starting with a digit are escaped with backticks. /// The `_` prefix is stripped from GObject naming conventions (e.g. `_normal` /// → `normal`). func swiftEnumCaseName(_ girName: String) -> String { let camel = camelCased(girName) guard !camel.isEmpty else { return girName } return swiftKeywords.contains(camel) ? "`\(camel)`" : camel } /// Swift reserved words that need backtick escaping when used as identifiers. private let swiftKeywords: Set = [ "as", "associativity", "break", "case", "catch", "class", "continue", "default", "defer", "deinit", "do", "else", "enum", "extension", "fallthrough", "false", "fileprivate", "for", "func", "get", "guard", "if", "import", "in", "init", "inout", "internal", "is", "let", "nil", "open", "operator", "optional", "override", "postfix", "prefix", "private", "protocol", "public", "repeat", "required", "return", "self", "set", "static", "struct", "subscript", "super", "switch", "throw", "throws", "true", "try", "typealias", "var", "weak", "where", "while", "willSet", "didSet", "PrecedenceGroup", "indirect", "left", "none", "nonmutating", "precedencegroup", "right", "Any", "Self", "Type", "Protocol", ]