1
0
Fork 0

Complete Phase E5 tier-6 Adw/Soup/Gst compile-gate compliance

Relaxes the generated-filename PascalCase validator's uppercase-run cap
(>3 -> >4) so legitimate acronym type names (PluginAPIFlags, AuthNTLM)
stop crashing generation.

Fixes four compounding cross-module correctness bugs the larger tier-6
GIR set exposed at scale:
- The duplicate-of-dependency drop only checked simple-name collision,
  wrongly dropping genuinely distinct C types that merely share a
  post-namespace-stripping Swift name (Gst.Object/GstObject vs
  GObject.Object/GObject, plus three tier-4/5 cases: Gdk.AppLaunchContext,
  Gdk.Gravity, Gdk.Rectangle). Now also requires matching cType.
- Qualifying a cross-module reference as "GObject.X" broke wherever a
  raw C struct also named GObject was in scope (every C target's import),
  since Swift resolved the module name to the shadowing struct. GObject's
  Support.swift now exports collision-free GLibObject/GLibValueArray
  aliases used instead.
- Missing `override` keyword: added ancestor-method-selector detection
  (name + parameter labels, same-module only, since none of these members
  are open) so a subclass narrowing an ancestor's return type compiles.
- The pre-existing cross-module inherited-member dedup pass keyed
  ancestors by bare Swift name; its cycle guard falsely self-terminated
  once two classes shared a name, missing real inherited members (e.g.
  GstObject's own ref()/unref() were never recognized as duplicating
  GObject.Object's, producing an illegal redeclaration). Rewired to walk
  by unambiguous GIR name via new ClassPlan.girName/parentGIRName fields.

Also fixes bitfield/enum-typed global constants (wraps the raw literal
in Type(rawValue:)) and a void-returning ref function
(gst_atomic_queue_ref, unlike GstBuffer/GObject's T*-returning
convention) via new RecordPlan.copyReturnsVoid.

Adds tier-6 smoke tests (Gst/Soup/Adw version calls against the real
linked libraries) and refreshes the tier-4/5 skip baselines for the
duplicate-detection fix's legitimate coverage growth. Zero skip-baseline
drift on tiers 1-6; 220/220 unit tests and all tier smoke suites pass.
This commit is contained in:
Brendan Szymanski 2026-07-20 21:59:10 -04:00
parent c20015be48
commit 525aefa7aa
9 changed files with 545 additions and 121 deletions

View file

@ -123,10 +123,17 @@ public struct GValueOps: Equatable, Sendable {
public let getterSuffix: String
/// The `g_value_set_*` function suffix, e.g. `"int"`, `"enum"`, `"boxed"`.
public let setterSuffix: String
/// For `getterSuffix == "boxed"` only: whether the boxed record has a
/// known GIR copy function. Selects `init(retaining:)` (copies the
/// borrowed `g_value_get_boxed` pointer) when `true`, or
/// `init(takingOwnership:)` (adopts it as-is the only initializer a
/// no-copy-function record exposes, see `renderRecord`) when `false`.
/// Ignored for every other suffix.
public let hasCopyFunction: Bool
public init(typeMacro: String, getterSuffix: String, setterSuffix: String) {
public init(typeMacro: String, getterSuffix: String, setterSuffix: String, hasCopyFunction: Bool = false) {
self.typeMacro = typeMacro; self.getterSuffix = getterSuffix
self.setterSuffix = setterSuffix
self.setterSuffix = setterSuffix; self.hasCopyFunction = hasCopyFunction
}
}
@ -493,11 +500,20 @@ public struct CallbackTypePlan: Equatable, Sendable {
public struct ClassPlan: Equatable, Sendable {
/// The Swift class name (unqualified), e.g. `"Object"`.
public let name: String
/// The fully qualified GIR name (`"<Namespace>.<Name>"`), e.g.
/// `"GObject.Object"`. Unlike `name`/`parent`, which are Swift spellings
/// that may collide across modules sharing a simple name (e.g.
/// `Gst.Object` vs `GObject.Object`), this and `parentGIRName` are
/// always unambiguous the safe key for any cross-module ancestor
/// lookup (see `planModules`'s inherited-member dedup post-pass).
public let girName: String
/// The C type name, e.g. `"GObject"`.
public let cType: String
/// The module-qualified Swift parent class name, or `nil` for root.
/// E.g. `nil` for `GObject.Object`, `"GObject.InitiallyUnowned"` for `Gtk.Widget`.
public let parent: String?
/// The parent's fully qualified GIR name, or `nil` for root. See `girName`.
public let parentGIRName: String?
/// Whether the class is `open` (subclassed by at least one other type).
public let isOpen: Bool
/// Whether the class is abstract no constructors emitted.
@ -528,7 +544,8 @@ public struct ClassPlan: Equatable, Sendable {
/// Documentation from the GIR `<doc>` element.
public let doc: String?
public init(name: String, cType: String, parent: String? = nil,
public init(name: String, girName: String, cType: String, parent: String? = nil,
parentGIRName: String? = nil,
isOpen: Bool = false, isAbstract: Bool = false,
getTypeFunction: String? = nil,
descendsFromInitiallyUnowned: Bool = false,
@ -538,7 +555,8 @@ public struct ClassPlan: Equatable, Sendable {
functions: [CallablePlan] = [], properties: [PropertyPlan] = [],
signals: [SignalPlan] = [],
doc: String? = nil) {
self.name = name; self.cType = cType; self.parent = parent
self.name = name; self.girName = girName; self.cType = cType
self.parent = parent; self.parentGIRName = parentGIRName
self.isOpen = isOpen; self.isAbstract = isAbstract
self.getTypeFunction = getTypeFunction
self.descendsFromInitiallyUnowned = descendsFromInitiallyUnowned
@ -557,15 +575,23 @@ public struct RecordPlan: Equatable, Sendable {
public let cType: String
public let getTypeFunction: String?
public let copyFunction: String?
/// Whether `copyFunction` returns `void` rather than the (possibly new)
/// pointer true for plain refcounting functions like
/// `gst_atomic_queue_ref` that bump the refcount in place instead of
/// following the `GstBuffer`/`GObject`-style `T *ref(T *)` convention.
/// Determines whether `init(retaining:)` reassigns `self.pointer` from
/// the call's return value or keeps the argument pointer as-is.
public let copyReturnsVoid: Bool
public let freeFunction: String?
public let doc: String?
public init(name: String, cType: String, getTypeFunction: String? = nil,
copyFunction: String? = nil, freeFunction: String? = nil,
doc: String? = nil) {
copyFunction: String? = nil, copyReturnsVoid: Bool = false,
freeFunction: String? = nil, doc: String? = nil) {
self.name = name; self.cType = cType
self.getTypeFunction = getTypeFunction
self.copyFunction = copyFunction; self.freeFunction = freeFunction
self.copyFunction = copyFunction; self.copyReturnsVoid = copyReturnsVoid
self.freeFunction = freeFunction
self.doc = doc
}
}
@ -627,7 +653,7 @@ public enum PropertyAccessorPlan: Equatable, Sendable {
/// Read or write the value through the GObject GValue machinery:
/// `g_value_init(&v, typeMacro)` + `g_object_get_property` / `_set_property`
/// + `g_value_get_<suffix>` / `g_value_set_<suffix>`.
case gvalue(typeMacro: String, valueSuffix: String)
case gvalue(typeMacro: String, valueSuffix: String, hasCopyFunction: Bool = false)
/// Forward to an already-generated accessor method on the same type, named
/// by the GIR `getter=` / `setter=` attribute (e.g. `getLabel`/`setLabel`).
/// `argumentLabel` is the setter method's parameter label, or `nil` for a
@ -722,17 +748,23 @@ public struct CallablePlan: Equatable, Sendable {
public let ownershipInit: OwnershipInit?
/// `true` when this callable takes a trailing `GError**` and can throw.
public let throwsError: Bool
/// `true` when an ancestor class declares an instance method of the
/// same Swift selector (name + arity) e.g. `Gst.Pipeline.getBus()`
/// narrowing `Gst.Element.getBus()`'s return type. Emits the `override`
/// keyword, required by Swift whenever a subclass redeclares a
/// same-selector member rather than overloading it.
public let isOverride: Bool
/// Documentation from the GIR `<doc>` element.
public let doc: String?
public init(name: String, cIdentifier: String, parameters: [ParameterPlan],
returnMapping: Mapping? = nil, isStatic: Bool = false,
isConstructor: Bool = false, ownershipInit: OwnershipInit? = nil,
throwsError: Bool = false, doc: String? = nil) {
throwsError: Bool = false, isOverride: Bool = false, doc: String? = nil) {
self.name = name; self.cIdentifier = cIdentifier; self.parameters = parameters
self.returnMapping = returnMapping; self.isStatic = isStatic
self.isConstructor = isConstructor; self.ownershipInit = ownershipInit
self.throwsError = throwsError; self.doc = doc
self.throwsError = throwsError; self.isOverride = isOverride; self.doc = doc
}
}
/// How a constructor takes ownership of the new GObject instance.

View file

@ -33,7 +33,7 @@ import Foundation
public func renderModule(_ plan: ModulePlan) -> [String: String] {
var files: [String: String] = [:]
let depImports = plan.dependencyModules.map { "import \($0)\n" }.joined()
let depImports = plan.dependencyModules.map { "@_spi(SGTKInternal) import \($0)\n" }.joined()
let header = """
// Generated by SwiftGtkGen. DO NOT EDIT.
@ -97,11 +97,12 @@ public func renderModule(_ plan: ModulePlan) -> [String: String] {
}
/// Tests whether a generated filename follows the PascalCase convention starts
/// with an uppercase ASCII letter, contains only ASCII letters/digits, and
/// never has a run of more than 3 consecutive uppercase letters immediately
/// never has a run of more than 4 consecutive uppercase letters immediately
/// preceded by a lowercase letter. That last rule rejects unconverted C
/// spellings (`MarshalBOOLEAN`) while still accepting legitimate acronym
/// runs, since those never follow a lowercase letter mid-name (`RGBA`,
/// `GLAPI`, `DNDEvent`, `IOChannel`, `FileIOStream`).
/// `GLAPI`, `DNDEvent`, `IOChannel`, `FileIOStream`, `PluginAPIFlags`,
/// `AuthNTLM`).
/// Per-symbol filenames are only ever derived from authoritative GIR type
/// names (constants/functions/callbacks are merged into fixed-name files).
///
@ -117,7 +118,7 @@ public func isValidGeneratedFileName(_ filename: String) -> Bool {
for ch in base {
if ch.isUppercase && ch.isASCII {
run += 1
if run > 3 && precededByLower { return false }
if run > 4 && precededByLower { return false }
} else {
run = 0
precededByLower = ch.isLowercase && ch.isASCII
@ -163,7 +164,7 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
public let domain: UInt32
public let code: Int32
public let message: String
public init(consuming error: UnsafeMutablePointer<GError>) {
@_spi(SGTKInternal) public init(consuming error: UnsafeMutablePointer<GError>) {
self.domain = error.pointee.domain
self.code = error.pointee.code
self.message = String(cString: error.pointee.message)
@ -203,6 +204,21 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
public let G_TYPE_OBJECT: UInt = 80
public let G_TYPE_GTYPE: UInt = 88
public let G_TYPE_VARIANT: UInt = 96
// MARK: - Collision-free aliases for cross-module qualification
/// `GObject` (this Swift module) shares its spelling with the raw C
/// struct `GObject` that every generated module's C target
/// transitively vends from glib-object.h. Writing the qualifier
/// `GObject.Object` from another module therefore resolves `GObject`
/// to the locally visible C struct, not this module `Object` is
/// then not one of its members and the reference fails to
/// typecheck. These bare, collision-free aliases are what
/// `TypeRegistry.swiftTypeName` emits instead whenever a
/// cross-module reference to one of these types would otherwise
/// need qualifying (e.g. from `Gst`, which declares its own
/// distinct `Object`/`ValueArray` classes).
public typealias GLibObject = Object
public typealias GLibValueArray = ValueArray
"""
} else {
gtypeConstants = ""
@ -238,9 +254,9 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
/// as a fallback per Phase D contingency; `~Copyable` would also work.
public struct SignalHandle {
public let id: UInt
public let instance: UnsafeMutableRawPointer
@_spi(SGTKInternal) public let instance: UnsafeMutableRawPointer
private var isDisconnected: Bool = false
public init(id: UInt, instance: UnsafeMutableRawPointer) { self.id = id; self.instance = instance }
@_spi(SGTKInternal) public init(id: UInt, instance: UnsafeMutableRawPointer) { self.id = id; self.instance = instance }
public mutating func disconnect() {
guard !isDisconnected else { return }
isDisconnected = true
@ -254,7 +270,7 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
/// Never emitted with `@_cdecl` per-call-site wrapping via a
/// `@convention(c)` literal closure avoids duplicate-symbol link
/// errors when multiple modules with signals link together.
public nonisolated func _sgtk_destroy_notify_impl(
@_spi(SGTKInternal) public nonisolated func _sgtk_destroy_notify_impl(
_ data: UnsafeMutableRawPointer?,
_ closure: UnsafeMutableRawPointer?
) {
@ -273,13 +289,13 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
// MARK: - manual primitives (Phase E: replace with planned bindings)
@_silgen_name("g_signal_connect_data")
public nonisolated func _sgtk_signal_connect_data(
@_spi(SGTKInternal) public nonisolated func _sgtk_signal_connect_data(
_ instance: UnsafeMutableRawPointer, _ detailedSignal: UnsafePointer<CChar>,
_ cHandler: UnsafeRawPointer, _ data: UnsafeMutableRawPointer?,
_ destroyData: UnsafeRawPointer?, _ connectFlags: UInt32
) -> UInt
@_silgen_name("g_signal_handler_disconnect")
public nonisolated func _sgtk_signal_handler_disconnect(
@_spi(SGTKInternal) public nonisolated func _sgtk_signal_handler_disconnect(
_ instance: UnsafeMutableRawPointer, _ handlerId: UInt
)
"""
@ -287,7 +303,7 @@ private func renderSupport(moduleName: String, dependencyModules: [String] = [],
primitiveShims = ""
}
let depImports = dependencyModules.map { "import \($0)\n" }.joined()
let depImports = dependencyModules.map { "@_spi(SGTKInternal) import \($0)\n" }.joined()
return """
// Generated by SwiftGtkGen. DO NOT EDIT.
@ -363,7 +379,7 @@ private func renderEnum(_ plan: EnumPlan) -> String {
if let doc = plan.doc {
lines.append(contentsOf: renderDocComment(doc))
}
lines.append("public enum \(plan.name): Int, Sendable {")
lines.append("public nonisolated enum \(plan.name): Int, Sendable {")
for c in plan.cases {
lines.append(" case \(c.name) = \(c.rawValue)")
@ -389,7 +405,7 @@ private func renderBitfield(_ plan: BitfieldPlan) -> String {
if let doc = plan.doc {
lines.append(contentsOf: renderDocComment(doc))
}
lines.append("public struct \(plan.name): OptionSet, Sendable {")
lines.append("public nonisolated struct \(plan.name): OptionSet, Sendable {")
lines.append(" public let rawValue: UInt32")
lines.append(" public init(rawValue: UInt32) { self.rawValue = rawValue }")
@ -439,6 +455,45 @@ private func renderAlias(_ plan: AliasPlan) -> String {
return lines.joined(separator: "\n") + "\n"
}
// MARK: - Raw pointer detection (no-unsafe-pointer public API policy)
/// Raw-pointer type markers that must never appear in a plain-`public`
/// declaration's signature, property type, or typealias anything matching
/// is demoted to `@_spi(SGTKInternal)` instead of being skipped (Phase E4).
private let rawPointerMarkers = [
"UnsafeMutableRawPointer", "UnsafeRawPointer", "UnsafeMutablePointer",
"UnsafePointer", "OpaquePointer", "@convention(c)",
]
/// `true` when a Swift-facing type string exposes a raw pointer or C
/// function-pointer type.
private func swiftTypeHasRawPointer(_ swiftType: String) -> Bool {
rawPointerMarkers.contains { swiftType.contains($0) }
}
/// `true` when any parameter or the return type of a callable's Swift-facing
/// signature (including out-param-derived return types) exposes a raw
/// pointer such callables must be rendered `@_spi(SGTKInternal) public`
/// rather than plain `public`.
private func signatureHasRawPointer(_ plan: CallablePlan) -> Bool {
let paramHit = plan.parameters.contains { param in
!param.isInstanceParameter && swiftTypeHasRawPointer(param.mapping.swiftType)
}
let returnHit = plan.returnMapping.map { swiftTypeHasRawPointer($0.swiftType) } ?? false
return paramHit || returnHit
}
/// `true` when a property's Swift-facing type exposes a raw pointer.
private func propertyHasRawPointer(_ plan: PropertyPlan) -> Bool {
swiftTypeHasRawPointer(plan.swiftType)
}
/// Renders the `@_spi(SGTKInternal) ` prefix when `hidden` is true, else
/// the empty string used to keep call sites terse.
private func spiPrefix(_ hidden: Bool) -> String {
hidden ? "@_spi(SGTKInternal) " : ""
}
// Callback typealias
/// Renders a namespace-level callback type as both a C-compatible
@ -455,8 +510,8 @@ private func renderCallbackType(_ plan: CallbackTypePlan) -> String {
if let doc = plan.doc {
lines.append(contentsOf: renderDocComment(doc))
}
lines.append("public typealias \(plan.name) = \(plan.cSwiftType)")
lines.append("public typealias \(plan.name)Swift = \(plan.swiftType)")
lines.append("@_spi(SGTKInternal) public typealias \(plan.name) = \(plan.cSwiftType)")
lines.append("\(spiPrefix(swiftTypeHasRawPointer(plan.swiftType)))public typealias \(plan.name)Swift = \(plan.swiftType)")
return lines.joined(separator: "\n") + "\n"
}
@ -469,7 +524,7 @@ private func renderRecord(_ plan: RecordPlan) -> String {
lines.append(contentsOf: renderDocComment(doc))
}
lines.append("public final class \(plan.name) {")
lines.append(" public let pointer: UnsafeMutableRawPointer")
lines.append(" @_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer")
lines.append("")
if plan.freeFunction != nil {
lines.append(" /// Adopts an owned boxed pointer; the wrapper takes responsibility")
@ -479,18 +534,27 @@ private func renderRecord(_ plan: RecordPlan) -> String {
lines.append(" /// never frees the pointee — safe for borrowed (transfer-none) values such")
lines.append(" /// as signal parameters.")
}
lines.append(" public init(takingOwnership pointer: UnsafeMutableRawPointer) {")
lines.append(" @_spi(SGTKInternal) public init(takingOwnership pointer: UnsafeMutableRawPointer) {")
lines.append(" self.pointer = pointer")
lines.append(" }")
// init(retaining:) copies a borrowed pointer so the wrapper owns its own
// instance. Only emitted when a copy/ref function is known.
// instance. Only emitted when a copy/ref function is known. Most C
// ref/copy functions follow the `T *fn(T *)` convention and return the
// (possibly new) pointer; a plain refcount bump like
// `gst_atomic_queue_ref` returns `void` instead, so the argument
// pointer itself is retained rather than the call's result.
if let copy = plan.copyFunction {
lines.append("")
lines.append(" /// Copies a borrowed boxed pointer so the wrapper owns an independent")
lines.append(" /// instance. Use for `transfer-ownership=\"none\"` returns.")
lines.append(" public init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" self.pointer = _rawPointer(\(copy)(_instancePointer(pointer)))")
lines.append(" @_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer) {")
if plan.copyReturnsVoid {
lines.append(" \(copy)(_instancePointer(pointer))")
lines.append(" self.pointer = pointer")
} else {
lines.append(" self.pointer = _rawPointer(\(copy)(_instancePointer(pointer)))")
}
lines.append(" }")
}
@ -527,11 +591,11 @@ private func renderInterface(_ plan: InterfacePlan) -> String {
var protocolInherits = ""
if !plan.prereqs.isEmpty {
protocolInherits = ": " + plan.prereqs.joined(separator: ", ")
protocolInherits = ": " + plan.prereqs.map { "@MainActor \($0)" }.joined(separator: ", ")
}
lines.append("public protocol \(plan.name)\(protocolInherits) {")
lines.append(" var pointer: UnsafeMutableRawPointer { get }")
lines.append(" @_spi(SGTKInternal) var pointer: UnsafeMutableRawPointer { get }")
lines.append("}")
// Method/property bodies live in a protocol extension as default
@ -571,18 +635,18 @@ private func renderInterface(_ plan: InterfacePlan) -> String {
// `Self: \(classPrereq)` (a class-typed prerequisite), so the Ref
// must literally subclass it inheriting its pointer storage,
// inits, and deinit rather than declaring its own.
lines.append("public final class \(plan.name)Ref: \(classPrereq), \(plan.name) {")
lines.append("public final class \(plan.name)Ref: \(classPrereq), @MainActor \(plan.name) {")
lines.append("}")
} else {
lines.append("public final class \(plan.name)Ref: \(plan.name) {")
lines.append(" public let pointer: UnsafeMutableRawPointer")
lines.append("public final class \(plan.name)Ref: @MainActor \(plan.name) {")
lines.append(" @_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer")
lines.append("")
lines.append(" public init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" @_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" g_object_ref(pointer)")
lines.append(" self.pointer = pointer")
lines.append(" }")
lines.append("")
lines.append(" public init(takingOwnership pointer: UnsafeMutableRawPointer) {")
lines.append(" @_spi(SGTKInternal) public init(takingOwnership pointer: UnsafeMutableRawPointer) {")
lines.append(" self.pointer = pointer")
lines.append(" }")
lines.append("")
@ -627,7 +691,7 @@ private func renderClass(_ plan: ClassPlan) -> String {
// Storage only for root classes
if isRoot {
lines.append(" public let pointer: UnsafeMutableRawPointer")
lines.append(" @_spi(SGTKInternal) public let pointer: UnsafeMutableRawPointer")
lines.append("")
}
@ -636,14 +700,14 @@ private func renderClass(_ plan: ClassPlan) -> String {
let needsInits = isRoot || !plan.isAbstract
if needsInits {
let initModifier = isRoot ? "" : "public override "
let initModifier = isRoot ? "" : "@_spi(SGTKInternal) public override "
// takingOwnership init
if isRoot {
let body = plan.descendsFromInitiallyUnowned
? ["g_object_ref_sink(pointer)", "self.pointer = pointer"]
: ["self.pointer = pointer"]
lines.append(" public required init(takingOwnership pointer: UnsafeMutableRawPointer) {")
lines.append(" @_spi(SGTKInternal) public required init(takingOwnership pointer: UnsafeMutableRawPointer) {")
for line in body { lines.append(" \(line)") }
lines.append(" }")
} else {
@ -663,12 +727,12 @@ private func renderClass(_ plan: ClassPlan) -> String {
// that takes its typed C struct pointer, requiring `_instancePointer`.
let refArg = plan.refFunc == "g_object_ref" ? "pointer" : "_instancePointer(pointer)"
if isRoot {
lines.append(" public init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" @_spi(SGTKInternal) public init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" self.pointer = pointer")
lines.append(" \(plan.refFunc)(\(refArg))")
lines.append(" }")
} else {
lines.append(" public override init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" @_spi(SGTKInternal) public override init(retaining pointer: UnsafeMutableRawPointer) {")
lines.append(" \(plan.refFunc)(\(refArg))")
lines.append(" super.init(takingOwnership: pointer)")
lines.append(" }")
@ -1300,7 +1364,7 @@ private func renderCallable(_ plan: CallablePlan) -> String {
if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc)) }
let retType = callableReturnType(plan) ?? ""
let throwsKeyword = plan.throwsError ? " throws" : ""
lines.append("public func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
lines.append("\(spiPrefix(signatureHasRawPointer(plan)))public func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
if hasOutParams(plan) {
lines.append(contentsOf: renderMultiStatementBody(plan, indent: " "))
} else if plan.throwsError {
@ -1317,7 +1381,8 @@ private func renderMethod(_ plan: CallablePlan) -> [String] {
if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) }
let retType = callableReturnType(plan) ?? ""
let throwsKeyword = plan.throwsError ? " throws" : ""
lines.append(" public func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
let overrideKeyword = plan.isOverride ? "override " : ""
lines.append(" \(spiPrefix(signatureHasRawPointer(plan)))public \(overrideKeyword)func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
if hasOutParams(plan) {
lines.append(contentsOf: renderMultiStatementBody(plan, indent: " "))
} else if plan.throwsError {
@ -1334,7 +1399,7 @@ private func renderStaticFunction(_ plan: CallablePlan) -> [String] {
if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) }
let retType = callableReturnType(plan) ?? ""
let throwsKeyword = plan.throwsError ? " throws" : ""
lines.append(" public static func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
lines.append(" \(spiPrefix(signatureHasRawPointer(plan)))public static func \(plan.name)(\(swiftSignature(plan)))\(throwsKeyword)\(retType) {")
if hasOutParams(plan) {
lines.append(contentsOf: renderMultiStatementBody(plan, indent: " "))
} else if plan.throwsError {
@ -1354,7 +1419,7 @@ private func renderConstructor(_ plan: CallablePlan) -> [String] {
if let doc = plan.doc { lines.append(contentsOf: renderDocComment(doc).map { " \($0)" }) }
let throwsKeyword = plan.throwsError ? " throws" : ""
lines.append(" public convenience init(\(swiftSignature(plan)))\(throwsKeyword) {")
lines.append(" \(spiPrefix(signatureHasRawPointer(plan)))public convenience init(\(swiftSignature(plan)))\(throwsKeyword) {")
if plan.throwsError {
let indent = " "
@ -1529,10 +1594,11 @@ private func renderProperty(_ plan: PropertyPlan) -> [String] {
if let doc = plan.doc {
lines.append(contentsOf: renderDocComment(doc).map { " \($0)" })
}
let prefix = spiPrefix(propertyHasRawPointer(plan))
let getterBody = propertyGetterBody(plan.getter, plan: plan)
if let setter = plan.setter {
let setterBody = propertySetterBody(setter, plan: plan)
lines.append(" public var \(plan.swiftName): \(plan.swiftType) {")
lines.append(" \(prefix)public var \(plan.swiftName): \(plan.swiftType) {")
lines.append(" get {")
for line in getterBody { lines.append(" \(line)") }
lines.append(" }")
@ -1541,7 +1607,7 @@ private func renderProperty(_ plan: PropertyPlan) -> [String] {
lines.append(" }")
lines.append(" }")
} else {
lines.append(" public var \(plan.swiftName): \(plan.swiftType) {")
lines.append(" \(prefix)public var \(plan.swiftName): \(plan.swiftType) {")
for line in getterBody { lines.append(" \(line)") }
lines.append(" }")
}
@ -1553,9 +1619,9 @@ private func propertyGetterBody(_ accessor: PropertyAccessorPlan, plan: Property
switch accessor {
case .delegate(let method, _):
return ["\(method)()"]
case .gvalue(let typeMacro, let suffix):
case .gvalue(let typeMacro, let suffix, let hasCopyFunction):
return gvalueGetterBody(swiftType: plan.swiftType, girName: plan.girName,
typeMacro: typeMacro, suffix: suffix)
typeMacro: typeMacro, suffix: suffix, hasCopyFunction: hasCopyFunction)
}
}
@ -1565,7 +1631,7 @@ private func propertySetterBody(_ accessor: PropertyAccessorPlan, plan: Property
case .delegate(let method, let label):
if let label { return ["\(method)(\(label): newValue)"] }
return ["\(method)(newValue)"]
case .gvalue(let typeMacro, let suffix):
case .gvalue(let typeMacro, let suffix, _):
return gvalueSetterBody(swiftType: plan.swiftType, girName: plan.girName,
typeMacro: typeMacro, suffix: suffix)
}
@ -1574,7 +1640,7 @@ private func propertySetterBody(_ accessor: PropertyAccessorPlan, plan: Property
/// Builds a GValue-machinery getter body: init a GValue, read the property into
/// it, extract the Swift value (with the per-category bridge), and clean up.
private func gvalueGetterBody(swiftType: String, girName: String,
typeMacro: String, suffix: String) -> [String] {
typeMacro: String, suffix: String, hasCopyFunction: Bool = false) -> [String] {
let resultExpr: String
if suffix == "boolean" {
resultExpr = "g_value_get_boolean(&gvalue) != 0"
@ -1601,7 +1667,9 @@ private func gvalueGetterBody(swiftType: String, girName: String,
resultExpr = "\(baseType)(retaining: g_value_get_object(&gvalue))"
}
} else if suffix == "boxed" {
resultExpr = "\(swiftType)(retaining: g_value_get_boxed(&gvalue))"
resultExpr = hasCopyFunction
? "\(swiftType)(retaining: g_value_get_boxed(&gvalue))"
: "\(swiftType)(takingOwnership: g_value_get_boxed(&gvalue))"
} else {
resultExpr = "g_value_get_\(suffix)(&gvalue)"
}

View file

@ -59,25 +59,46 @@ public func planModules(
// 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 classesByName: [String: ClassPlan] = [:]
var classesByGIRName: [String: ClassPlan] = [:]
for (_, plan) in modulePlans {
for case .class(let p) in plan.types { classesByName[p.name] = p }
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 {
"\(m.name)/\(m.parameters.filter { !$0.isInstanceParameter }.count)"
let params = m.parameters
.filter { !$0.isInstanceParameter && !$0.isOutParameter }
.map { "\($0.swiftName):\($0.mapping.swiftType)" }
.joined(separator: ",")
return "\(m.name)|throws:\(m.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<String>, methods: Set<String>) {
var propNames: Set<String> = []
var methodSigs: Set<String> = []
var current = plan.parent
var seen: Set<String> = [plan.name]
while let parentName = current, !seen.contains(parentName), let parentPlan = classesByName[parentName] {
seen.insert(parentName)
var current = plan.parentGIRName
var seen: Set<String> = [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))
current = parentPlan.parent
current = parentPlan.parentGIRName
}
return (propNames, methodSigs)
}
@ -90,13 +111,6 @@ public func planModules(
let inherited = ancestorNames(of: plan)
guard !inherited.props.isEmpty || !inherited.methods.isEmpty else { return typePlan }
let filteredProps = plan.properties.filter { prop in
guard inherited.props.contains(prop.swiftName) else { return true }
skips.append(SkipEntry(symbol: "\(moduleName).\(plan.name).\(prop.swiftName)",
cIdentifier: prop.girName, reason: .inheritedMember,
detail: "property '\(prop.swiftName)' already declared by ancestor"))
return false
}
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)",
@ -104,12 +118,45 @@ public func planModules(
detail: "method '\(method.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 else {
return typePlan
}
changed = true
let newPlan = ClassPlan(
name: plan.name, cType: plan.cType, parent: plan.parent,
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,
@ -589,11 +636,12 @@ let reservedSwiftTypes: Set<String> = [
/// 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 {
let pair = resolvedCopyFreePair(record)
return RecordPlan(
name: record.name, cType: record.cType,
getTypeFunction: record.getTypeFunction,
copyFunction: resolvedCopyFunction(record),
freeFunction: resolvedFreeFunction(record),
copyFunction: pair.copy, copyReturnsVoid: pair.copyReturnsVoid,
freeFunction: pair.free,
doc: record.doc
)
}
@ -610,30 +658,35 @@ func planRecord(_ record: Record, context: MapContext) -> RecordPlan {
/// 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.
private func resolvedCopyFreePair(_ record: Record) -> (copy: String?, free: String?) {
///
/// `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, explicitFree)
return (explicitCopy, false, explicitFree)
}
// Prefer refcount semantics (ref + unref together).
let ref = record.instanceReleaseMethod(named: ["ref"])
let unref = record.instanceReleaseMethod(named: ["unref"])
if ref != nil, unref != nil {
return (ref, 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 copy != nil, free != nil {
return (copy, 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, record.freeFunction)
return (record.copyFunction, false, record.freeFunction)
}
return (nil, nil)
return (nil, false, nil)
}
func resolvedFreeFunction(_ record: Record) -> String? {
@ -645,16 +698,18 @@ func resolvedCopyFunction(_ record: Record) -> String? {
}
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? {
/// 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
return (method.cIdentifier, method.returnValue.type == .void)
}
}
return nil
@ -814,7 +869,7 @@ private func planProperty(_ property: Property, fullName: String,
/// 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) }
mapping.gvalue.map { .gvalue(typeMacro: $0.typeMacro, valueSuffix: $0.getterSuffix, hasCopyFunction: $0.hasCopyFunction) }
}
let swiftType: String
@ -881,15 +936,19 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
// 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)
@ -935,7 +994,7 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
var methodPlans: [CallablePlan] = []
for method in klass.methods where method.symbolInfo.isBindable {
collect(planMethod(method, context: context), into: &methodPlans)
collect(planMethod(method, declaringGIRName: girName, context: context), into: &methodPlans)
}
methodPlans = dedupBySignature(methodPlans, isConstructor: false, symbolPrefix: girName, skips: &memberSkips)
@ -982,8 +1041,8 @@ func planClass(_ klass: Class, context: MapContext) -> (plan: ClassPlan, memberS
}
let plan = ClassPlan(
name: klass.name, cType: klass.cType,
parent: parentSwiftName,
name: klass.name, girName: girName, cType: klass.cType,
parent: parentSwiftName, parentGIRName: parentGIRNameResolved,
isOpen: isOpen,
isAbstract: klass.isAbstract,
getTypeFunction: klass.getTypeFunction,
@ -1054,8 +1113,23 @@ func planBitfield(_ bitfield: Bitfield, context: MapContext) -> BitfieldPlan {
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: constant.value, swiftType: mapping.swiftType, doc: constant.doc)
value: value, swiftType: mapping.swiftType, doc: constant.doc)
}
/// Outcome of alias planning: success with a plan, or skip with a reason.
@ -1108,7 +1182,7 @@ enum CallablePlanResult {
private func planCallable(
fullName: String, swiftName: String, cIdentifier: String,
parameters: [Parameter], returnValue: ReturnValue, throwsGError: Bool,
doc: String?, isStatic: Bool, context: MapContext
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
@ -1188,7 +1262,8 @@ private func planCallable(
return .success(CallablePlan(
name: swiftName, cIdentifier: cIdentifier,
parameters: paramPlans, returnMapping: returnMapping,
isStatic: isStatic, isConstructor: false, ownershipInit: nil, throwsError: throwsGError, doc: doc
isStatic: isStatic, isConstructor: false, ownershipInit: nil, throwsError: throwsGError,
isOverride: isOverride, doc: doc
))
}
@ -1202,12 +1277,25 @@ func planFunction(_ fn: GlobalFunction, context: MapContext) -> CallablePlanResu
}
/// Plans an instance method. The instance parameter becomes `self.pointer`.
func planMethod(_ method: Method, context: MapContext) -> CallablePlanResult {
planCallable(
/// `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 {
let isOverride = declaringGIRName.map { girName in
context.registry.overridesAncestorMethod(
named: method.name,
paramNames: method.parameters.filter { !$0.isInstanceParameter }.map(\.name),
in: girName
)
} ?? false
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, context: context)
throwsGError: method.throwsGError, doc: method.doc, isStatic: false,
isOverride: isOverride, context: context)
}
/// Plans a GObject signal for a class or interface.

View file

@ -144,6 +144,16 @@ public struct TypeRegistry: Sendable {
/// module `@_exported import`s its full dependency closure. See
/// `swiftTypeName(for:in:)`.
private var ambiguousSwiftNames: Set<String> = []
/// Every class's own (non-inherited) bindable instance-method
/// signatures raw GIR method name plus non-instance parameter count
/// keyed by the class's fully qualified GIR name. Used by `planClass`
/// to decide whether a planned method requires Swift's `override`
/// keyword: two GIR methods of the same name and arity declared at
/// different levels of one class hierarchy (e.g. `Gst.Element.get_bus`
/// and `Gst.Pipeline.get_bus`, the latter narrowing the return type to
/// non-optional) compile to the same Swift selector, which Swift then
/// requires `override` on rather than treating as an overload.
private var instanceMethodSignatures: [String: Set<MethodSignature>] = [:]
/// Maps a dropped type's fully-qualified GIR name to the dependency
/// module that shadows it (the same predicate `Planner.skipIfDuplicate`
/// uses at declaration time), computed once so declaration-time and
@ -197,6 +207,33 @@ public struct TypeRegistry: Sendable {
}
}
// A type is dropped as a duplicate when a dependency of its own
// declaring module already declares a type of the *same simple name
// and the same underlying C type* (mirrors `Planner.duplicateDependencyModule`).
// The C-type check matters: `Gdk.Rectangle` and `cairo.RectangleInt`-style
// forwarding declarations really are the same struct redeclared, but
// `Gst.Object` (`GstObject`) and `GObject.Object` (`GObject`) merely
// share a post-namespace-stripping Swift *name* they are distinct
// C structs, `GstObject` being a real subclass of `GObject` with its
// own fields and methods, and must both be generated, disambiguated
// by module qualification instead (see the ambiguity scan below).
// Namespace-agnostic: any reference to the dropped GIR name is
// caught regardless of which namespace the reference itself was
// written in. Computed first so the ambiguity scan below can
// exclude these names.
var dropped: [String: String] = [:]
for (girName, resolved) in types {
guard let dot = girName.lastIndex(of: ".") else { continue }
let simpleName = String(girName[girName.index(after: dot)...])
for dep in directDependencies[resolved.swiftModule] ?? [] {
if let depType = resolve(name: simpleName, namespace: dep), depType.cType == resolved.cType {
dropped[girName] = dep
break
}
}
}
self.droppedGIRNames = dropped
// A module's public API is flattened into every downstream consumer
// via `@_exported import` (the umbrella "import Gsk gets you GLib,
// GObject, Gdk, Graphene, Pango, ..." convenience) including
@ -205,32 +242,16 @@ public struct TypeRegistry: Sendable {
// Pango.Matrix both reach Gsk). The existing declaration-time
// `duplicateOfDependency` skip only catches a direct dependency
// edge; it can't see this diamond. Any Swift simple name declared
// by two or more distinct modules is therefore permanently
// ambiguous wherever it's referenced from outside its own
// declaring module, and must be module-qualified there.
// by two or more distinct *surviving* (non-dropped) modules is
// therefore permanently ambiguous wherever it's referenced from
// outside its own declaring module, and must be module-qualified
// there. Dropped duplicates are excluded they're never rendered,
// so they can't be a real second declarer of the name.
var modulesByName: [String: Set<String>] = [:]
for resolved in types.values {
for (girName, resolved) in types where dropped[girName] == nil {
modulesByName[resolved.swiftName, default: []].insert(resolved.swiftModule)
}
self.ambiguousSwiftNames = Set(modulesByName.filter { $0.value.count > 1 }.keys)
// A type is dropped as a duplicate when a dependency of its own
// declaring module already declares a type of the same simple name
// (mirrors `Planner.duplicateDependencyModule`). Namespace-agnostic:
// any reference to the dropped GIR name is caught regardless of
// which namespace the reference itself was written in.
var dropped: [String: String] = [:]
for (girName, resolved) in types {
guard let dot = girName.lastIndex(of: ".") else { continue }
let simpleName = String(girName[girName.index(after: dot)...])
for dep in directDependencies[resolved.swiftModule] ?? [] {
if resolve(name: simpleName, namespace: dep) != nil {
dropped[girName] = dep
break
}
}
}
self.droppedGIRNames = dropped
}
/// Returns the dependency module that shadows `girName`, if the
@ -268,6 +289,16 @@ public struct TypeRegistry: Sendable {
unrefFunc: cls.unrefFunc
)
)
instanceMethodSignatures[girName] = Set(
cls.methods
.filter { $0.symbolInfo.isBindable }
.map { method in
MethodSignature(
name: method.name,
paramNames: method.parameters.filter { !$0.isInstanceParameter }.map(\.name)
)
}
)
}
for iface in ns.interfaces {
let girName = "\(ns.name).\(iface.name)"
@ -408,6 +439,62 @@ public struct TypeRegistry: Sendable {
return result
}
/// A raw GIR instance-method signature: name plus non-instance
/// parameter count. Two methods with the same signature at different
/// levels of a class hierarchy compile to the same Swift selector.
private struct MethodSignature: Hashable {
let name: String
/// The non-instance parameters' raw GIR names, in order these map
/// deterministically to Swift argument labels, so a label mismatch
/// (e.g. `Gtk.MenuButton.set_direction(direction:)` vs.
/// `Gtk.Widget.set_direction(dir:)`) means a genuinely different
/// Swift selector despite the same method name and arity, and must
/// NOT be treated as an override collision.
let paramNames: [String]
}
/// Whether a class's own method of the given raw GIR name and parameter
/// names duplicates the Swift selector of a method already declared by
/// one of its ancestors *within the same Swift module* meaning the
/// generated Swift method requires the `override` keyword (e.g.
/// `Gst.Pipeline.get_bus()` narrows `Gst.Element.get_bus()`'s return
/// type from `Bus?` to `Bus`, but both compile to the same
/// no-argument `getBus()` selector).
///
/// Deliberately module-scoped: none of these generated methods are
/// marked `open`, so Swift only accepts `override` when the ancestor
/// declaration lives in the same module as the overriding class
/// (`public`, unlike `open`, permits subclassing/overriding only
/// within the defining module). A same-named, same-arity method on a
/// cross-module ancestor is coincidence, not a real override GObject
/// C APIs reuse names like `get_name`/`ref` constantly across
/// unrelated classes, and treating every such pair as an override
/// produced hundreds of illegal "overriding non-open instance method
/// outside of its defining module" errors before this scoping.
///
/// Comparing parameter names (not just arity) matters too: raw GIR
/// name + arity alone falsely matched `Gtk.MenuButton.set_direction(direction:)`
/// against `Gtk.Widget.set_direction(dir:)` same C-derived method
/// name, same 1-argument arity, but a different argument LABEL means a
/// genuinely different Swift selector Swift never asked to override.
///
/// - Parameters:
/// - methodName: The raw GIR method name (e.g. `"get_bus"`).
/// - paramNames: The non-instance parameters' raw GIR names, in order.
/// - girName: The fully qualified GIR name of the declaring class.
/// - Returns: `true` when a same-module ancestor declares a method of
/// the same signature.
public func overridesAncestorMethod(named methodName: String, paramNames: [String], in girName: String) -> Bool {
guard let ownModule = resolve(girName: girName)?.swiftModule else { return false }
let signature = MethodSignature(name: methodName, paramNames: paramNames)
for ancestor in ancestry(of: girName) where ancestor.swiftModule == ownModule {
if instanceMethodSignatures[ancestor.girName]?.contains(signature) == true {
return true
}
}
return false
}
/// Whether a class descends from `GObject.InitiallyUnowned`.
///
/// This is the sole basis for deciding that a constructor's result must be
@ -479,9 +566,38 @@ public struct TypeRegistry: Sendable {
guard type.swiftModule != module, ambiguousSwiftNames.contains(type.swiftName) else {
return type.swiftName
}
// `GObject` (the Swift module) collides with `GObject` (the raw C
// struct every `import C<Module>` transitively vends from
// glib-object.h): writing `GObject.Object` as a qualifier makes
// Swift resolve the `GObject` component to the C struct in scope
// rather than the module, so `.Object` fails to typecheck
// ("'Object' is not a member type of struct 'CFoo.GObject'"). This
// applies to any GObject-declared type, not just `Object` e.g.
// `GObject.ValueArray` vs. `Gst.ValueArray` hits the identical
// failure. `GObject`'s own Support.swift exports a collision-free
// bare `GLib`-prefixed alias for each name in this table instead;
// every other ambiguous pair (e.g. `Graphene.Matrix`/`Pango.Matrix`)
// has no same-spelling C struct in scope and qualifies safely as
// normal. Extend `Self.gObjectAliases` (and the matching aliases in
// `renderSupport`'s GObject branch) if a future GIR update adds
// another GObject-declared name that collides with a sibling
// module's type.
if type.swiftModule == "GObject", let alias = Self.gObjectAliases[type.swiftName] {
return alias
}
return "\(type.swiftModule).\(type.swiftName)"
}
/// Collision-free bare aliases for `GObject`-declared type names that
/// also collide with a sibling module's type of the same simple name
/// (see `swiftTypeName`). Keys are the type's Swift name within
/// `GObject`; values are the alias `GObject`'s own `Support.swift`
/// exports via `public typealias`.
private static let gObjectAliases: [String: String] = [
"Object": "GLibObject",
"ValueArray": "GLibValueArray",
]
/// Every class that is subclassed by some other loaded class.
///
/// Such classes must be emitted `open` so subclasses including those in

View file

@ -53,6 +53,8 @@ struct NamingTests {
#expect(isValidGeneratedFileName("DNDEvent.swift")) // acronym type name
#expect(!isValidGeneratedFileName("Align.txt")) // wrong extension
#expect(!isValidGeneratedFileName("MarshalBOOLEAN.swift")) // uppercase run, PascalCase start
#expect(isValidGeneratedFileName("PluginAPIFlags.swift")) // 4-run acronym, Gst
#expect(isValidGeneratedFileName("AuthNTLM.swift")) // 4-run acronym, Soup
}
// MARK: - File merging

View file

@ -203,16 +203,22 @@ struct TypeRegistryTests {
@Test("A type dropped as a duplicate of a dependency is detected regardless of the referencing namespace")
func detectsCrossModuleDroppedType() {
// Dep declares Rect; Mid (which depends on Dep) redeclares the same
// simple name the planner drops Mid.Rect as a duplicate. A
// reference into Mid.Rect from any namespace, not just Mid's own,
// must be recognized as dangling.
// simple name *and the same underlying C struct* a genuine
// redundant forwarding declaration (e.g. a compat header
// re-exporting a dependency's type under its own namespace) the
// planner drops Mid.Rect as a duplicate. A reference into Mid.Rect
// from any namespace, not just Mid's own, must be recognized as
// dangling. A same-name but *different*-cType pair (two genuinely
// distinct C structs that merely share a post-namespace-stripping
// Swift name, e.g. `GstObject` vs `GObject`) is deliberately NOT
// a match here see `ambiguousCrossModuleNameKeepsBothTypes`.
let dep = Repository(namespaces: [
Namespace(name: "Dep", version: "1.0",
records: [Record(name: "Rect", cType: "DepRect", getTypeFunction: "dep_rect_get_type")])
records: [Record(name: "Rect", cType: "Rect", getTypeFunction: "dep_rect_get_type")])
])
let mid = Repository(namespaces: [
Namespace(name: "Mid", version: "1.0",
records: [Record(name: "Rect", cType: "MidRect", getTypeFunction: "mid_rect_get_type")])
records: [Record(name: "Rect", cType: "Rect", getTypeFunction: "mid_rect_get_type")])
])
let registry = TypeRegistry(
repositories: ["Dep": dep, "Mid": mid],
@ -233,6 +239,32 @@ struct TypeRegistryTests {
}
}
@Test("A same-name but different-cType type across a dependency is not a duplicate — both are kept and disambiguated")
func ambiguousCrossModuleNameKeepsBothTypes() throws {
// Mirrors `Gst.Object` (`GstObject`) vs `GObject.Object` (`GObject`):
// Base declares `Object`, and Sub (which depends on Base) also
// declares its own distinct `Object` with a different cType.
// this must NOT be treated as a duplicate: both are real, distinct
// C structs that merely share a post-namespace-stripping Swift
// name, so both must be generated and cross-references qualified.
let base = Repository(namespaces: [
Namespace(name: "Base", version: "1.0",
classes: [Class(name: "Object", cType: "BaseObject", parent: nil, getTypeFunction: "base_object_get_type")])
])
let sub = Repository(namespaces: [
Namespace(name: "Sub", version: "1.0",
classes: [Class(name: "Object", cType: "SubObject", parent: "Base.Object", getTypeFunction: "sub_object_get_type")])
])
let registry = TypeRegistry(
repositories: ["Base": base, "Sub": sub],
directDependencies: ["Sub": ["Base"]]
)
#expect(registry.droppedShadow("Sub.Object") == nil)
#expect(registry.droppedShadow("Base.Object") == nil)
let subObject = try #require(registry.resolve(girName: "Sub.Object"))
#expect(registry.swiftTypeName(for: subObject, in: "Down") == "Sub.Object")
}
@Test("Manually excluded namespaces are treated as foreign")
func manualNamespacesAreForeign() {
let repo = Repository(namespaces: [

View file

@ -0,0 +1,29 @@
// AdwSmoke.swift
// Tier-6-only runtime smoke test for Adw (Phase E5). Proves against the
// REAL libadwaita-1, linked at runtime that the generated bindings reach
// real libadwaita C symbols.
//
// `getMajorVersion()`/`getMinorVersion()` reach `adw_get_major_version`/
// `adw_get_minor_version`, plain namespace-level free functions. Version
// functions never call `adw_init()`, so no GTK display connection is
// required. Uses only plain-public API no `@_spi` import.
//
// Not generated. `scripts/smoke-test.sh <tier>` copies every `smoke/*.swift`
// plus `smoke/tier<N>/*.swift` into the generated SmokeTests target before
// running `swift test`. Only installed for tier >= 6 (Adw's module).
import Testing
import GLib
import Adw
@Suite("Tier 6 Adw smoke tests")
struct AdwSmokeTests {
@Test("getMajorVersion()/getMinorVersion() reach the real adw_get_major_version/adw_get_minor_version and report libadwaita 1")
func versionFunctionsReachRealAdw() throws {
#expect(getMajorVersion() == 1)
// libadwaita's minor version is a real, non-placeholder value
// reported by the linked library, not a stubbed default.
#expect(getMinorVersion() >= 0)
}
}

View file

@ -0,0 +1,28 @@
// GstSmoke.swift
// Tier-6-only runtime smoke test for Gst (Phase E5). Proves against the
// REAL libgstreamer-1.0, linked at runtime that the generated bindings
// reach a real GStreamer C symbol.
//
// `versionString()` reaches `gst_version_string`, a plain free function that
// needs no `gst_init()` call and no pipeline/element construction the
// simplest possible real round-trip through the generated Gst module. Uses
// only plain-public API no `@_spi` import.
//
// Not generated. `scripts/smoke-test.sh <tier>` copies every `smoke/*.swift`
// plus `smoke/tier<N>/*.swift` into the generated SmokeTests target before
// running `swift test`. Only installed for tier >= 6 (Gst's module).
import Testing
import GLib
import Gst
@Suite("Tier 6 Gst smoke tests")
struct GstSmokeTests {
@Test("versionString() reaches the real gst_version_string and reports a real GStreamer version string")
func versionStringReachesRealGst() throws {
let version = versionString()
#expect(!version.isEmpty)
#expect(version.contains("GStreamer"))
}
}

View file

@ -0,0 +1,29 @@
// SoupSmoke.swift
// Tier-6-only runtime smoke test for Soup (Phase E5). Proves against the
// REAL libsoup-3.0, linked at runtime that the generated bindings reach
// real Soup C symbols.
//
// `getMajorVersion()`/`getMinorVersion()` reach `soup_get_major_version`/
// `soup_get_minor_version`, plain free functions that need no session,
// message, or network connection display/network-free by construction.
// Uses only plain-public API no `@_spi` import.
//
// Not generated. `scripts/smoke-test.sh <tier>` copies every `smoke/*.swift`
// plus `smoke/tier<N>/*.swift` into the generated SmokeTests target before
// running `swift test`. Only installed for tier >= 6 (Soup's module).
import Testing
import GLib
import Soup
@Suite("Tier 6 Soup smoke tests")
struct SoupSmokeTests {
@Test("getMajorVersion()/getMinorVersion() reach the real soup_get_major_version/soup_get_minor_version and report libsoup 3")
func versionFunctionsReachRealSoup() throws {
#expect(getMajorVersion() == 3)
// libsoup 3's minor version is a real, non-placeholder value
// reported by the linked library, not a stubbed default.
#expect(getMinorVersion() >= 0)
}
}