1
0
Fork 0

Tier 2 compile-gate compliance: interfaces, cross-module refs, Gio

This commit is contained in:
Brendan Szymanski 2026-07-19 00:27:08 -04:00
parent 5abb48e889
commit 0504b34d7f
18 changed files with 9186 additions and 304 deletions

View file

@ -1,9 +1,10 @@
// InterfaceConformanceTests.swift
// Covers Phase C5 conformance: classes emit `: Parent, Interface` headers and
// protocol requirements are filtered to those every implementing class can
// witness. Methods whose Swift signature drifts between the interface node and
// a class node are dropped from the protocol (recorded as a skip) so the
// conformance stays compilable.
// Covers interface conformance under the extension-default model (Phase E1):
// classes emit `: Parent, Interface` headers; the interface's methods/properties
// render as protocol-extension DEFAULT implementations (not bare requirements),
// so a class's own method of the same name coexists without redeclaration
// errors, and a concrete `<Name>Ref` wrapper is emitted so `any Interface`
// values can be constructed from a raw C pointer.
import Testing
@ -29,11 +30,13 @@ struct InterfaceConformanceTests {
return MapContext(registry: registry, currentModule: "GObject", currentNamespace: "GObject")
}
@Test("Class implementing interface emits conformance clause")
@Test("Class implementing interface emits conformance clause; interface method becomes an extension default")
func conformanceClauseEmitted() throws {
// Interface: TypePlugin.use returns Void
// Class: TypeModule implements TypePlugin, has its own `use` returning Bool
// (signature drifts requirement dropped, but conformance still emitted)
// Class: TypeModule implements TypePlugin, has its own `use` returning Bool.
// Under the extension-default model both coexist: the class's own
// method is a distinct overload from the protocol extension default,
// so no redeclaration/witnessing conflict is possible.
let iface = Interface(
name: "TypePlugin", cType: "GTypePlugin",
methods: [
@ -62,17 +65,12 @@ struct InterfaceConformanceTests {
]
)
// Plan both, build type plan array, then filter
let ctx = makeContext()
let (ifacePlan, _) = planInterface(iface, context: ctx)
let (classPlan, _) = planClass(klass, context: ctx)
var skips: [SkipEntry] = []
let types: [TypePlan] = [.class(classPlan), .interface(ifacePlan)]
let filteredTypes = filterInterfaceRequirements(types, skips: &skips)
// Render the filtered module
let module = ModulePlan(module: "GObject", types: filteredTypes, skips: skips,
let module = ModulePlan(module: "GObject", types: types, skips: [],
coverage: CoverageStats())
let files = renderModule(module)
let classSrc = files["TypeModule.swift"] ?? ""
@ -80,67 +78,21 @@ struct InterfaceConformanceTests {
#expect(classSrc.contains("class TypeModule: Object, TypePlugin {"))
// Drifted method `use` dropped from protocol
#expect(!ifaceSrc.contains("func use"))
// Protocol body is bare only the `pointer` requirement.
#expect(ifaceSrc.contains("public protocol TypePlugin {"))
#expect(ifaceSrc.contains("var pointer: UnsafeMutableRawPointer { get }"))
// Skip recorded for the drift
let driftSkips = skips.filter { $0.reason == .interfaceMethodSignatureDrift }
#expect(driftSkips[0].symbol == "TypePlugin.use")
}
// The interface method is a protocol EXTENSION default, not a
// requirement it keeps its own (Void) signature regardless of the
// implementing class's differently-signatured method.
#expect(ifaceSrc.contains("extension TypePlugin {"))
#expect(ifaceSrc.contains("public func use("))
@Test("Consistent interface/class signatures keep the requirement in the protocol")
func consistentSignaturesKeepRequirement() throws {
// Regression guard: when the interface and implementing class agree on
// a method's signature, the requirement MUST remain in the rendered
// protocol. Without this test the filter could over-drop every
// requirement and the suite would still pass (the drift test only
// asserts dropping; the no-interfaces test never exercises the keep
// branch at all).
let iface = Interface(
name: "TypePlugin", cType: "GTypePlugin",
methods: [
Method(name: "use", cIdentifier: "g_type_plugin_use",
parameters: [
Parameter(name: "self", type: .pointer,
cType: "GTypePlugin*",
isInstanceParameter: true)
],
returnValue: ReturnValue(type: .boolean)),
],
getTypeFunction: "g_type_plugin_get_type"
)
let klass = Class(
name: "TypeModule", cType: "GTypeModule", parent: "Object",
getTypeFunction: "g_type_module_get_type",
implements: ["TypePlugin"],
methods: [
Method(name: "use", cIdentifier: "g_type_module_use",
parameters: [
Parameter(name: "self", type: .pointer,
cType: "GTypeModule*",
isInstanceParameter: true)
],
returnValue: ReturnValue(type: .boolean)),
]
)
let ctx = makeContext()
let (ifacePlan, _) = planInterface(iface, context: ctx)
let (classPlan, _) = planClass(klass, context: ctx)
var skips: [SkipEntry] = []
let types: [TypePlan] = [.class(classPlan), .interface(ifacePlan)]
let filteredTypes = filterInterfaceRequirements(types, skips: &skips)
let module = ModulePlan(module: "GObject", types: filteredTypes, skips: skips,
coverage: CoverageStats())
let files = renderModule(module)
let ifaceSrc = files["TypePlugin.swift"] ?? ""
// Requirement kept: `func use` appears in the protocol with a Bool return.
#expect(ifaceSrc.contains("func use"))
// No drift skip was recorded.
#expect(skips.filter { $0.reason == .interfaceMethodSignatureDrift }.isEmpty)
// Concrete Ref wrapper is emitted so `any TypePlugin` is constructible.
#expect(ifaceSrc.contains("public final class TypePluginRef: TypePlugin {"))
#expect(ifaceSrc.contains("init(retaining pointer: UnsafeMutableRawPointer)"))
#expect(ifaceSrc.contains("init(takingOwnership pointer: UnsafeMutableRawPointer)"))
#expect(ifaceSrc.contains("isolated deinit"))
}
@Test("Class with no interfaces keeps existing header unchanged")

View file

@ -1,10 +1,11 @@
// InterfaceGenerationTests.swift
// Covers Phase C5: GObject interface method planning and rendering. An
// interface method becomes a protocol *requirement* a `func` signature with
// no `public` modifier and no body because the C symbol that implements it
// lives on the conforming class, not the interface. Unplannable methods
// (unsupported parameter types) are recorded as skips, mirroring class
// members. These tests lock in that surface so a regression surfaces here.
// Covers interface method planning and rendering under the extension-default
// model (Phase E1): an interface method's body is rendered as a `public`
// protocol-extension DEFAULT implementation (`extension Ping { public func }`)
// calling the C symbol via `self.pointer`, since interface protocols have no
// members of their own beyond `pointer`. Unplannable methods (unsupported
// parameter types) are recorded as skips, mirroring class members. These
// tests lock in that surface so a regression surfaces here.
import Testing
@ -32,8 +33,8 @@ struct InterfaceGenerationTests {
return renderModule(module)["\(plan.name).swift"] ?? ""
}
@Test("Interface instance methods become protocol requirements")
func methodsBecomeRequirements() throws {
@Test("Interface instance methods become protocol extension default implementations")
func methodsBecomeExtensionDefaults() throws {
let iface = Interface(
name: "Ping", cType: "GPing",
methods: [
@ -51,10 +52,13 @@ struct InterfaceGenerationTests {
#expect(plan.methods[0].name == "getId")
let source = render(plan)
// A requirement no `public`, no body.
#expect(source.contains("func getId() -> Int"))
#expect(!source.contains("public func getId"))
#expect(!source.contains("g_ping_get_id")) // no body means no C call
// Protocol body is bare only the `pointer` requirement.
#expect(source.contains("public protocol Ping {"))
#expect(source.contains("var pointer: UnsafeMutableRawPointer { get }"))
// A default implementation `public`, with a body calling the C symbol.
#expect(source.contains("extension Ping {"))
#expect(source.contains("public func getId() -> Int"))
#expect(source.contains("g_ping_get_id"))
}
@Test("Unplannable interface methods are recorded as skips, not requirements")

View file

@ -1,8 +1,8 @@
// PropertyGenerationTests.swift
// Covers Phase C6: GObject property planning and rendering. A property becomes
// a Swift computed `var` backed by the GValue machinery
// (`g_value_init` / `g_object_get_property` / `g_object_set_property`), or for
// interfaces a `{ get }` / `{ get set }` protocol requirement. Properties
// (`g_value_init` / `g_object_get_property` / `g_object_set_property`), rendered
// as a `public var` for interfaces, as a protocol-extension default. Properties
// whose type has no `GValueOps` are recorded as skips.
//
// These tests also lock in the numeric bridging of enum/flags getters: a
@ -235,10 +235,10 @@ struct PropertyGenerationTests {
#expect(!source.contains("internalThing"))
}
// Interface property requirements
// Interface property extension defaults
@Test("A readable-only interface property becomes a { get } requirement")
func interfaceReadOnlyPropertyIsGetRequirement() throws {
@Test("A readable-only interface property becomes a GValue-backed extension default getter")
func interfaceReadOnlyPropertyIsExtensionDefault() throws {
let iface = Interface(
name: "Orientable", cType: "GtkOrientable",
properties: [
@ -253,14 +253,16 @@ struct PropertyGenerationTests {
let module = ModulePlan(module: "GObject", types: [.interface(plan)], skips: [],
coverage: CoverageStats())
let source = renderModule(module)["Orientable.swift"] ?? ""
#expect(source.contains("var orientation: Orientation { get }"))
// A requirement no body, no GValue machinery.
#expect(!source.contains("g_object_get_property"))
#expect(!source.contains("public var orientation"))
// Protocol body is bare only the `pointer` requirement.
#expect(!source.contains("var orientation: Orientation { get }"))
// A default implementation, in a protocol extension, with a real body.
#expect(source.contains("extension Orientable {"))
#expect(source.contains("public var orientation: Orientation"))
#expect(source.contains("g_object_get_property"))
}
@Test("A writable interface property becomes a { get set } requirement")
func interfaceWritablePropertyIsGetSetRequirement() throws {
@Test("A writable interface property becomes a GValue-backed extension default getter+setter")
func interfaceWritablePropertyIsExtensionDefault() throws {
let iface = Interface(
name: "Editable", cType: "GtkEditable",
properties: [
@ -271,7 +273,10 @@ struct PropertyGenerationTests {
let module = ModulePlan(module: "GObject", types: [.interface(plan)], skips: [],
coverage: CoverageStats())
let source = renderModule(module)["Editable.swift"] ?? ""
#expect(source.contains("var text: String { get set }"))
#expect(!source.contains("var text: String { get set }"))
#expect(source.contains("extension Editable {"))
#expect(source.contains("public var text: String"))
#expect(source.contains("g_object_set_property"))
}
// getter=/setter= method delegation

View file

@ -185,4 +185,114 @@ struct RendererCallableTests {
let outSkip = skips.first { $0.reason == .constructorOutParams }
#expect(outSkip != nil)
}
// MARK: - E1: throwing constructor with string params doesn't nest self.init
@Test("Throwing constructor with a String param threads the result out of withCString instead of nesting self.init")
func throwingConstructorWithStringParamAvoidsNestedSelfInit() throws {
// `self.init` (a delegating initializer call) is illegal inside any
// closure, including a non-escaping `withCString` trailing closure.
// Regression guard for the tier-2 DBusConnection/DBusProxy bug where
// string-parameterised throwing constructors called self.init from
// inside the withCString closure.
let klass = Class(
name: "Proxy", cType: "GProxy", parent: "Object",
getTypeFunction: "g_proxy_get_type",
constructors: [
Constructor(name: "new_for_bus_sync", cIdentifier: "g_proxy_new_for_bus_sync",
parameters: [
Parameter(name: "name", type: .string, cType: "const char*"),
],
returnValue: ReturnValue(transferOwnership: .full),
throwsGError: true),
]
)
let (plan, _) = planClass(klass, context: makeContext())
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
coverage: CoverageStats())
let src = renderModule(module)["Proxy.swift"] ?? ""
// The C call (returning the result) lives INSIDE the withCString
// closure as its trailing/implicit-return expression...
#expect(src.contains("g_proxy_new_for_bus_sync(cString0, &error)"))
// ...while `self.init` runs OUTSIDE every closure, never nested.
#expect(!src.contains("cString0, &error)\n self.init"))
let lines = src.components(separatedBy: "\n")
guard let selfInitLine = lines.first(where: { $0.contains("self.init(takingOwnership:") }) else {
Issue.record("expected a self.init(takingOwnership:) line"); return
}
// Indentation of 8 spaces == top-level init body scope, not nested
// one level inside the withCString closure (12 spaces).
#expect(selfInitLine.hasPrefix(" self.init"))
}
// MARK: - E1: constructor signature deduplication
@Test("Two constructors with the same rendered init signature but different Swift names dedup to one, keeping the first")
func constructorsWithIdenticalSignatureDedup() throws {
// `init(...)` never carries the constructor's Swift name (it always
// renders as bare `init`), so two GIR constructors mapping to
// DIFFERENT Swift names (`newFinish` vs `newForAddressFinish`) but
// the IDENTICAL parameter/throws/return shape still collide at
// `init(res:)`. Regression guard for the tier-2 DBusConnection bug.
let resParam = Parameter(name: "res", type: .typeRef("AsyncResult", namespace: "GObject"),
cType: "GAsyncResult*")
let klass = Class(
name: "Connection", cType: "GConnection", parent: "Object",
getTypeFunction: "g_connection_get_type",
constructors: [
Constructor(name: "new_finish", cIdentifier: "g_connection_new_finish",
parameters: [resParam],
returnValue: ReturnValue(transferOwnership: .full),
throwsGError: true),
Constructor(name: "new_for_address_finish", cIdentifier: "g_connection_new_for_address_finish",
parameters: [resParam],
returnValue: ReturnValue(transferOwnership: .full),
throwsGError: true),
]
)
let ctx = MapContext(
registry: TypeRegistry(repositories: ["GObject": Repository(namespaces: [
Namespace(name: "GObject", version: "2.0", classes: [
Class(name: "Object", cType: "GObject", parent: nil, getTypeFunction: "g_object_get_type"),
Class(name: "AsyncResult", cType: "GAsyncResult", parent: "Object",
getTypeFunction: "g_async_result_get_type"),
])
])]),
currentModule: "GObject", currentNamespace: "GObject"
)
let (plan, skips) = planClass(klass, context: ctx)
#expect(plan.constructors.count == 1)
#expect(plan.constructors[0].cIdentifier == "g_connection_new_finish")
let dedupSkip = skips.first { $0.reason == .nameCollision }
#expect(dedupSkip?.cIdentifier == "g_connection_new_for_address_finish")
let module = ModulePlan(module: "GObject", types: [.class(plan)], skips: [],
coverage: CoverageStats())
let src = renderModule(module)["Connection.swift"] ?? ""
let occurrences = src.components(separatedBy: "convenience init(res: AsyncResult) throws").count - 1
#expect(occurrences == 1)
}
// MARK: - E1: pointer out-param zero-initializes to nil, not 0
@Test("A gpointer out-param local variable initializes to nil, not the integer 0")
func pointerOutParamInitializesToNil() throws {
// `.direct` marshalIn covers both numeric out-params (`gint*` `0`)
// and raw-pointer out-params (`gpointer*` `UnsafeMutableRawPointer?`,
// which `= 0` cannot initialize). Regression guard for the tier-2
// FileInfo.getAttributeData bug.
let fn = GlobalFunction(
name: "get_attribute_data", cIdentifier: "g_file_info_get_attribute_data",
parameters: [
Parameter(name: "attribute", type: .string, cType: "const char*"),
Parameter(name: "value_pp", type: .pointer, cType: "gpointer*", direction: .out),
],
returnValue: ReturnValue(type: .void)
)
guard case .success(let plan) = planFunction(fn, context: makeContext()) else {
Issue.record("expected get_attribute_data to plan successfully"); return
}
let body = renderCallable(plan)
#expect(body.contains("var out0: UnsafeMutableRawPointer? = nil"))
#expect(!body.contains("var out0: UnsafeMutableRawPointer? = 0"))
}
}

View file

@ -282,7 +282,7 @@ struct TypeMapperTests {
let ctx = makeContext()
let mapping = try map(.typeRef("Object", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.Object")
#expect(mapping.swiftType == "Object")
#expect(mapping.cSwiftType == "UnsafeMutableRawPointer?")
#expect(mapping.marshalIn == .objectPointer)
#expect(mapping.marshalOut == .objectWrap(sink: false))
@ -301,8 +301,9 @@ struct TypeMapperTests {
let ctx = makeContext()
let mapping = try map(.typeRef("TypePlugin", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.TypePlugin")
#expect(mapping.marshalIn == .objectPointer)
#expect(mapping.swiftType == "TypePlugin")
#expect(mapping.marshalIn == .interfacePointer)
#expect(mapping.marshalOut == .interfaceWrap(adopt: false))
}
@Test("Enum typeRef maps to enum raw-value wrapping")
@ -310,10 +311,10 @@ struct TypeMapperTests {
let ctx = makeContext()
let mapping = try map(.typeRef("ParamFlags", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.ParamFlags")
#expect(mapping.swiftType == "ParamFlags")
#expect(mapping.cSwiftType == "GParamFlags")
#expect(mapping.marshalIn == .enumRaw)
#expect(mapping.marshalOut == .enumFromRaw(swiftType: "GObject.ParamFlags"))
#expect(mapping.marshalOut == .enumFromRaw(swiftType: "ParamFlags"))
#expect(mapping.gvalue?.typeMacro == "G_TYPE_ENUM")
}
@ -322,10 +323,10 @@ struct TypeMapperTests {
let ctx = makeContext()
let mapping = try map(.typeRef("SignalFlags", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.SignalFlags")
#expect(mapping.swiftType == "SignalFlags")
#expect(mapping.cSwiftType == "GSignalFlags")
#expect(mapping.marshalIn == .bitfieldRaw)
#expect(mapping.marshalOut == .bitfieldFromRaw(swiftType: "GObject.SignalFlags"))
#expect(mapping.marshalOut == .bitfieldFromRaw(swiftType: "SignalFlags"))
#expect(mapping.gvalue?.typeMacro == "G_TYPE_FLAGS")
}
@ -334,7 +335,7 @@ struct TypeMapperTests {
let ctx = makeContext()
let mapping = try map(.typeRef("Value", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.Value")
#expect(mapping.swiftType == "Value")
#expect(mapping.cSwiftType == "UnsafeMutableRawPointer?")
#expect(mapping.marshalIn == .boxedPointer)
#expect(mapping.marshalOut == .boxedWrap(copy: true, copyFunction: "g_value_copy")) // transfer != .full copy
@ -524,7 +525,7 @@ struct TypeMapperTests {
// Should succeed without throwing.
let mapping = try map(.typeRef("Value", namespace: "GObject"),
nullable: false, transfer: .none, context: ctx)
#expect(mapping.swiftType == "GObject.Value")
#expect(mapping.swiftType == "Value")
#expect(mapping.marshalOut == .boxedWrap(copy: true, copyFunction: "g_value_copy"))
}
}

View file

@ -159,12 +159,15 @@ struct TypeRegistryTests {
#expect(!subclassed.contains("Gtk.Label"))
}
@Test("Swift spelling qualifies only cross-module references")
@Test("Swift spelling is always unqualified — per-file dependency imports resolve cross-module references")
func spellsSwiftTypeNames() throws {
let registry = makeRegistry()
let object = try #require(registry.resolve(girName: "GObject.Object"))
let widget = try #require(registry.resolve(girName: "Gtk.Widget"))
#expect(registry.swiftTypeName(for: object, in: "Gtk") == "GObject.Object")
// Cross-module references are unqualified (`Object`, not `GObject.Object`):
// qualifying is impossible when the C typedef of the same name (from
// the generated file's `import CGtk`) shadows the Swift module.
#expect(registry.swiftTypeName(for: object, in: "Gtk") == "Object")
#expect(registry.swiftTypeName(for: object, in: "GObject") == "Object")
#expect(registry.swiftTypeName(for: widget, in: "Gtk") == "Widget")
}