Boxed records (C4): resolve each record's copy/free function from the GIR copy-function/free-function attribute, else its own ref/copy and unref/free method, and render init(retaining:) plus an isolated deinit. The deinit is isolated because the package's default MainActor isolation makes accessing the non-Sendable pointer from a nonisolated deinit a hard error. The registry uses the same resolution so transfer=none returns copy instead of adopting a borrowed pointer. This supersedes the earlier deferral: the hypothesised OpaquePointer/Sendable barriers either did not hold or were solvable. Interfaces (C5): plan each interface method through planMethod and render it as a protocol requirement (no body); the conforming class supplies the C call.
885 lines
40 KiB
Swift
885 lines
40 KiB
Swift
// 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
|
|
)
|
|
|
|
let plan = planNamespace(namespace, context: context)
|
|
modulePlans[moduleName] = plan
|
|
}
|
|
|
|
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
|
|
|
|
// ── Enumerations ──
|
|
for enumeration in ns.enumerations {
|
|
totalTypes += 1; boundTypes += planEnum(into: &types, skips: &skips, enumeration: enumeration, context: context)
|
|
}
|
|
|
|
// ── Bitfields ──
|
|
for bitfield in ns.bitfields {
|
|
totalTypes += 1; boundTypes += planBit(into: &types, skips: &skips, bitfield: bitfield, context: context)
|
|
}
|
|
|
|
// ── Constants ──
|
|
for constant in ns.constants {
|
|
totalTypes += 1; boundTypes += planConst(into: &types, skips: &skips, constant: constant, context: context)
|
|
}
|
|
|
|
// ── Aliases ──
|
|
for alias in ns.aliases {
|
|
totalTypes += 1; boundTypes += planAliasType(into: &types, skips: &skips, alias: alias, context: context)
|
|
}
|
|
|
|
// ── Classes, Interfaces, Records, Callbacks — skip with reasons (Phase B6+) ──
|
|
for klass in ns.classes {
|
|
totalTypes += 1
|
|
boundTypes += skipClass(into: &skips, into: &types,
|
|
boundCallables: &boundCallables, totalCallables: &totalCallables,
|
|
klass: klass, namespace: ns.name, context: context)
|
|
}
|
|
for iface in ns.interfaces {
|
|
totalTypes += 1; boundTypes += skipInterface(into: &skips, into: &types, iface: iface, namespace: ns.name, context: context)
|
|
}
|
|
for record in ns.records {
|
|
totalTypes += 1; boundTypes += skipRecord(into: &skips, into: &types, record: record, namespace: ns.name, context: context)
|
|
}
|
|
for callback in ns.callbacks {
|
|
totalTypes += 1; boundTypes += skipCallback(into: &skips, callback: callback, namespace: ns.name)
|
|
}
|
|
for fn in ns.functions {
|
|
totalCallables += 1
|
|
boundCallables += planOrSkipFunction(into: &skips, into: &types, fn: fn, namespace: ns.name, context: context)
|
|
}
|
|
|
|
let coverage = CoverageStats(
|
|
boundCallables: boundCallables, totalCallables: totalCallables,
|
|
boundTypes: boundTypes, totalTypes: totalTypes
|
|
)
|
|
|
|
return ModulePlan(module: context.currentModule, types: types, skips: skips, coverage: coverage)
|
|
}
|
|
|
|
// 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],
|
|
constant: Constant, context: MapContext
|
|
) -> Int {
|
|
let fullName = "\(context.currentNamespace).\(constant.name)"
|
|
if let plan = planConstant(constant, context: context) {
|
|
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
|
|
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], record: Record, namespace: String, context: MapContext) -> Int {
|
|
let fullName = "\(namespace).\(record.name)"
|
|
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
|
|
}
|
|
types.append(.record(planRecord(record, context: context)))
|
|
return 1
|
|
}
|
|
skips.append(SkipEntry(symbol: fullName, cIdentifier: record.cType,
|
|
reason: .plainRecord, detail: "no GType registration"))
|
|
return 0
|
|
}
|
|
|
|
private func skipCallback(into skips: inout [SkipEntry], callback: Callback, namespace: String) -> Int {
|
|
let fullName = "\(namespace).\(callback.name)"
|
|
skips.append(SkipEntry(symbol: fullName, cIdentifier: callback.cType,
|
|
reason: .callbackWithoutUserData, detail: "callback planned for Phase D2"))
|
|
return 0
|
|
}
|
|
|
|
/// Plans a namespace-level function, appending a `.callable` type plan on
|
|
/// success or a `SkipEntry` on failure. Returns 1 when bound, 0 when skipped.
|
|
private func planOrSkipFunction(into skips: inout [SkipEntry], into types: inout [TypePlan], fn: GlobalFunction, namespace: String, context: MapContext) -> Int {
|
|
let fullName = "\(namespace).\(fn.name)"
|
|
|
|
// Symbols the system library does not export (macros, inline functions,
|
|
// or GType getters absent from the shared object) cannot be called.
|
|
if knownMissingCFunctions.contains(fn.cIdentifier) {
|
|
skips.append(SkipEntry(symbol: fullName, cIdentifier: fn.cIdentifier,
|
|
reason: .unknownType, detail: "C symbol '\(fn.cIdentifier)' is not exported by the system library"))
|
|
return 0
|
|
}
|
|
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
|
|
}
|
|
|
|
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<String> = [
|
|
"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",
|
|
]
|
|
|
|
let knownMisleadingCFunctions: Set<String> = [
|
|
"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> = [
|
|
"String", "Int", "Bool", "Double", "Float", "Array", "Dictionary",
|
|
"Set", "Optional", "Data", "Date", "URL", "Error", "Void", "Any",
|
|
"Object", "Type", "Protocol",
|
|
]
|
|
|
|
/// Plans a boxed record as an opaque pointer wrapper, resolving the copy and
|
|
/// free functions that give it correct memory management (C4).
|
|
func planRecord(_ record: Record, context: MapContext) -> RecordPlan {
|
|
return RecordPlan(
|
|
name: record.name, cType: record.cType,
|
|
getTypeFunction: record.getTypeFunction,
|
|
copyFunction: resolvedCopyFunction(record),
|
|
freeFunction: resolvedFreeFunction(record),
|
|
doc: record.doc
|
|
)
|
|
}
|
|
|
|
/// 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.
|
|
func resolvedFreeFunction(_ record: Record) -> String? {
|
|
if let explicit = record.freeFunction { return explicit }
|
|
return record.instanceReleaseMethod(named: ["unref", "free"])
|
|
}
|
|
|
|
/// Resolves a boxed record's copy/ref C identifier.
|
|
///
|
|
/// Prefers the explicit GIR `copy-function` attribute; otherwise falls back to
|
|
/// the record's own parameterless `ref` (preferred, ref-counted) or `copy`
|
|
/// method. Returns `nil` when none exists — the wrapper then renders without
|
|
/// `init(retaining:)`, and borrowed (`transfer-ownership="none"`) returns of
|
|
/// this type must not be adopted as owned.
|
|
func resolvedCopyFunction(_ record: Record) -> String? {
|
|
if let explicit = record.copyFunction { return explicit }
|
|
return record.instanceReleaseMethod(named: ["ref", "copy"])
|
|
}
|
|
|
|
extension Record {
|
|
/// The C identifier of 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.
|
|
func instanceReleaseMethod(named names: [String]) -> String? {
|
|
for wanted in names {
|
|
if let method = methods.first(where: {
|
|
$0.name == wanted && $0.symbolInfo.isBindable
|
|
&& $0.parameters.allSatisfy(\.isInstanceParameter)
|
|
}) {
|
|
return method.cIdentifier
|
|
}
|
|
}
|
|
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)
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
let plan = InterfacePlan(
|
|
name: iface.name, cType: iface.cType,
|
|
prereqs: prereqSwiftNames,
|
|
getTypeFunction: iface.getTypeFunction,
|
|
methods: methodPlans,
|
|
doc: iface.doc
|
|
)
|
|
return (plan, memberSkips)
|
|
}
|
|
|
|
/// 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?
|
|
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)
|
|
} else {
|
|
parentSwiftName = nil // foreign or unknown — treat as root
|
|
}
|
|
} else {
|
|
parentSwiftName = nil
|
|
}
|
|
|
|
let isOpen = registry.subclassedTypes().contains(girName)
|
|
let descendsIU = registry.descendsFromInitiallyUnowned(girName)
|
|
|
|
// Resolve implemented interfaces to their Swift names
|
|
let interfaceNames: [String] = klass.implements.compactMap { ifaceName in
|
|
let qualified = ifaceName.contains(".") ? ifaceName : "\(context.currentNamespace).\(ifaceName)"
|
|
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)
|
|
}
|
|
}
|
|
|
|
var methodPlans: [CallablePlan] = []
|
|
for method in klass.methods where method.symbolInfo.isBindable {
|
|
collect(planMethod(method, context: context), into: &methodPlans)
|
|
}
|
|
|
|
var functionPlans: [CallablePlan] = []
|
|
for fn in klass.functions where fn.symbolInfo.isBindable {
|
|
collect(planFunction(fn, context: context), into: &functionPlans)
|
|
}
|
|
|
|
let plan = ClassPlan(
|
|
name: klass.name, cType: klass.cType,
|
|
parent: parentSwiftName,
|
|
isOpen: isOpen,
|
|
isAbstract: klass.isAbstract,
|
|
getTypeFunction: klass.getTypeFunction,
|
|
descendsFromInitiallyUnowned: descendsIU,
|
|
interfaces: interfaceNames,
|
|
constructors: constructorPlans,
|
|
methods: methodPlans,
|
|
functions: functionPlans,
|
|
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.
|
|
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 }
|
|
return ConstantPlan(name: constant.name, value: constant.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, context: MapContext
|
|
) -> CallablePlanResult {
|
|
// 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) }) {
|
|
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 .objectPointer.
|
|
if case .unsupported(let reason) = returnMap.marshalOut {
|
|
return .skip(SkipEntry(symbol: fullName, cIdentifier: cIdentifier,
|
|
reason: .unknownType, detail: "return type: \(reason)"))
|
|
}
|
|
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, 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`.
|
|
func planMethod(_ method: Method, context: MapContext) -> CallablePlanResult {
|
|
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, context: context)
|
|
}
|
|
|
|
/// 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)"
|
|
// 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"))
|
|
}
|
|
return .success(CallablePlan(
|
|
name: swiftFunctionName(ctor.name), cIdentifier: ctor.cIdentifier,
|
|
parameters: paramPlans, returnMapping: nil,
|
|
isStatic: false, isConstructor: true,
|
|
ownershipInit: descendsIU ? .sinkingRef : .takingOwnership,
|
|
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] = []
|
|
|
|
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 <varargs/> 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 {
|
|
// Out-params are collected and returned as Swift values rather than
|
|
// passed as arguments. Map the VALUE type (not the pointer-to-pointer).
|
|
let mappingResult = Result { try map(param.type, nullable: param.isNullable,
|
|
transfer: param.transferOwnership, context: context) }
|
|
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"))
|
|
}
|
|
|
|
// Map the type
|
|
let mappingResult = Result { try map(param.type, nullable: param.isNullable,
|
|
transfer: param.transferOwnership, context: context) }
|
|
switch mappingResult {
|
|
case .success(let paramMapping):
|
|
if !paramMapping.isReadyForCallables {
|
|
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
|
|
reason: .unknownType,
|
|
detail: "parameter '\(param.name)' type '\(paramMapping.swiftType)' is not yet generated"))
|
|
}
|
|
|
|
let isString = paramMapping.marshalIn == .stringToC
|
|
// A nullable string needs an optional-aware bridge; deferred.
|
|
if isString && paramMapping.swiftType.hasSuffix("?") {
|
|
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
|
|
reason: .unknownType, detail: "parameter '\(param.name)' is a nullable string"))
|
|
}
|
|
// 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
|
|
// boxed records need address-of / wrapper marshalling — deferred.
|
|
// Object and boxed params have their own marshalIn paths (.objectPointer,
|
|
// .boxedPointer) that pass the wrapper's pointer.
|
|
if param.cType.hasSuffix("*") && !isString,
|
|
paramMapping.category != .needsClass, paramMapping.category != .needsRecord {
|
|
return .skip(SkipEntry(symbol: "", cIdentifier: nil,
|
|
reason: .unknownType, detail: "parameter '\(param.name)' C type '\(param.cType)' is a pointer"))
|
|
}
|
|
|
|
let swiftName = swiftParameterName(param.name)
|
|
plans.append(ParameterPlan(
|
|
swiftName: swiftName, cArgIndex: index,
|
|
mapping: paramMapping, isInstanceParameter: param.isInstanceParameter
|
|
))
|
|
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.
|
|
///
|
|
/// 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 parts = snake.split(separator: "_", omittingEmptySubsequences: true).map(String.init)
|
|
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
|
|
}
|
|
|
|
/// 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 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 {
|
|
var name = girName
|
|
// Strip leading underscore (GObject naming convention)
|
|
if name.hasPrefix("_") {
|
|
let stripped = String(name.dropFirst())
|
|
// If stripping the underscore leaves a name starting with a digit,
|
|
// keep the underscore — Swift requires identifiers to start with a
|
|
// letter or underscore, and backtick escaping doesn't lift this
|
|
// restriction.
|
|
if stripped.first?.isNumber != true {
|
|
name = stripped
|
|
}
|
|
}
|
|
// Names starting with a digit cannot be identifiers even backtick-escaped;
|
|
// prefix with underscore.
|
|
if name.first?.isNumber == true {
|
|
name = "_\(name)"
|
|
}
|
|
// Escaped keyword
|
|
if swiftKeywords.contains(name) { return "`\(name)`" }
|
|
return name
|
|
}
|
|
|
|
/// Swift reserved words that need backtick escaping when used as identifiers.
|
|
private let swiftKeywords: Set<String> = [
|
|
"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",
|
|
]
|