From 4c51c674887193c45f56021a935bb8e5028e9b1e Mon Sep 17 00:00:00 2001 From: Ethan Huang Date: Sun, 11 Jan 2026 16:25:15 +0800 Subject: [PATCH 01/10] Fix PartiallyGenerated for array properties --- .../GenerableMacro.swift | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/Sources/AnyLanguageModelMacros/GenerableMacro.swift b/Sources/AnyLanguageModelMacros/GenerableMacro.swift index 31ff2f8..a6ea708 100644 --- a/Sources/AnyLanguageModelMacros/GenerableMacro.swift +++ b/Sources/AnyLanguageModelMacros/GenerableMacro.swift @@ -190,6 +190,35 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { return (key: parts[0], value: parts[1]) } + private static func baseTypeName(_ type: String) -> String { + let trimmed = type.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.hasSuffix("?") { + return String(trimmed.dropLast()).trimmingCharacters(in: .whitespacesAndNewlines) + } + return trimmed + } + + private static func arrayElementType(from type: String) -> String? { + let trimmed = type.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.hasPrefix("[") && trimmed.hasSuffix("]") && !trimmed.contains(":") { + return String(trimmed.dropFirst().dropLast()).trimmingCharacters(in: .whitespacesAndNewlines) + } + if trimmed.hasPrefix("Array<") && trimmed.hasSuffix(">") { + return String(trimmed.dropFirst("Array<".count).dropLast()) + .trimmingCharacters(in: .whitespacesAndNewlines) + } + return nil + } + + private static func partiallyGeneratedTypeName(for type: String) -> String { + let baseType = baseTypeName(type) + if let elementType = arrayElementType(from: baseType) { + let elementPartial = partiallyGeneratedTypeName(for: elementType) + return "[\(elementPartial)]" + } + return "\(baseType).PartiallyGenerated" + } + private static func getDefaultValue(for type: String) -> String { let trimmedType = type.trimmingCharacters(in: .whitespacesAndNewlines) @@ -383,19 +412,20 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { propertyName: String, propertyType: String ) -> String { - switch propertyType { - case "String", "String?": + let baseType = baseTypeName(propertyType) + + switch baseType { + case "String": return "self.\(propertyName) = try? properties[\"\(propertyName)\"]?.value(String.self)" - case "Int", "Int?": + case "Int": return "self.\(propertyName) = try? properties[\"\(propertyName)\"]?.value(Int.self)" - case "Double", "Double?": + case "Double": return "self.\(propertyName) = try? properties[\"\(propertyName)\"]?.value(Double.self)" - case "Float", "Float?": + case "Float": return "self.\(propertyName) = try? properties[\"\(propertyName)\"]?.value(Float.self)" - case "Bool", "Bool?": + case "Bool": return "self.\(propertyName) = try? properties[\"\(propertyName)\"]?.value(Bool.self)" default: - let baseType = propertyType.replacingOccurrences(of: "?", with: "") if isDictionaryType(baseType) { return """ if let value = properties[\"\(propertyName)\"] { @@ -404,10 +434,21 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { self.\(propertyName) = nil } """ + } else if let elementType = arrayElementType(from: baseType) { + let elementPartial = partiallyGeneratedTypeName(for: elementType) + let arrayPartial = "[\(elementPartial)]" + return """ + if let value = properties[\"\(propertyName)\"] { + self.\(propertyName) = try? \(arrayPartial)(value) + } else { + self.\(propertyName) = nil + } + """ } else { + let partialType = partiallyGeneratedTypeName(for: baseType) return """ if let value = properties[\"\(propertyName)\"] { - self.\(propertyName) = try? \(propertyType)(value) + self.\(propertyName) = try? \(partialType)(value) } else { self.\(propertyName) = nil } @@ -676,12 +717,8 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { properties: [PropertyInfo] ) -> DeclSyntax { let optionalProperties = properties.map { prop in - let propertyType = prop.type - if propertyType.hasSuffix("?") { - return "public let \(prop.name): \(propertyType)" - } else { - return "public let \(prop.name): \(propertyType)?" - } + let partialType = partiallyGeneratedTypeName(for: prop.type) + return "public var \(prop.name): \(partialType)?" }.joined(separator: "\n ") let propertyExtractions = properties.map { prop in From 4b223cf3213a84ef24380e4177f8f6b1306c306a Mon Sep 17 00:00:00 2001 From: Ethan Huang Date: Sun, 11 Jan 2026 16:29:57 +0800 Subject: [PATCH 02/10] Add tests for array partial types --- .../GenerableMacroTests.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Tests/AnyLanguageModelTests/GenerableMacroTests.swift b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift index 3607b6c..ab65a97 100644 --- a/Tests/AnyLanguageModelTests/GenerableMacroTests.swift +++ b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift @@ -34,6 +34,18 @@ struct TestArguments { var age: Int } +@Generable +private struct ArrayItem { + @Guide(description: "A name") + var name: String +} + +@Generable +private struct ArrayContainer { + @Guide(description: "Items", .count(2)) + var items: [ArrayItem] +} + @Suite("Generable Macro") struct GenerableMacroTests { @Test("@Guide description with multiline string") @@ -140,6 +152,25 @@ struct GenerableMacroTests { #expect(args.age == 25) #expect(args.asPartiallyGenerated().id == generationID) } + + @Test("Array properties use partially generated element types") + func arrayPropertyPartialTypes() throws { + let content = GeneratedContent( + properties: [ + "items": GeneratedContent( + kind: .array([ + GeneratedContent(properties: ["name": "Alpha"]), + GeneratedContent(properties: ["name": "Beta"]), + ]) + ), + ] + ) + + let container = try ArrayContainer(content) + let partial = container.asPartiallyGenerated() + #expect(partial.items?.count == 2) + #expect(partial.items?.first?.name == "Alpha") + } } // MARK: - #Playground Usage From 0d6bc415e592ba9a62f12705655ac4d16fadd975 Mon Sep 17 00:00:00 2001 From: Ethan Huang Date: Sun, 11 Jan 2026 16:49:52 +0800 Subject: [PATCH 03/10] Keep PartiallyGenerated properties as let --- Sources/AnyLanguageModelMacros/GenerableMacro.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AnyLanguageModelMacros/GenerableMacro.swift b/Sources/AnyLanguageModelMacros/GenerableMacro.swift index a6ea708..7be8a08 100644 --- a/Sources/AnyLanguageModelMacros/GenerableMacro.swift +++ b/Sources/AnyLanguageModelMacros/GenerableMacro.swift @@ -718,7 +718,7 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { ) -> DeclSyntax { let optionalProperties = properties.map { prop in let partialType = partiallyGeneratedTypeName(for: prop.type) - return "public var \(prop.name): \(partialType)?" + return "public let \(prop.name): \(partialType)?" }.joined(separator: "\n ") let propertyExtractions = properties.map { prop in From 13589db59152ecdde88ff835d4796b1396e35e92 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 15 Jan 2026 05:02:11 -0800 Subject: [PATCH 04/10] Improve handling of array and dictionary types --- .../GenerableMacro.swift | 86 ++++++++++++++++--- .../GenerableMacroTests.swift | 51 ++++++++++- 2 files changed, 123 insertions(+), 14 deletions(-) diff --git a/Sources/AnyLanguageModelMacros/GenerableMacro.swift b/Sources/AnyLanguageModelMacros/GenerableMacro.swift index 7be8a08..76ade90 100644 --- a/Sources/AnyLanguageModelMacros/GenerableMacro.swift +++ b/Sources/AnyLanguageModelMacros/GenerableMacro.swift @@ -168,26 +168,60 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { return GuideInfo(description: nil, guides: [], pattern: nil) } - private static func isDictionaryType(_ type: String) -> Bool { - let trimmed = type.trimmingCharacters(in: .whitespacesAndNewlines) - return trimmed.hasPrefix("[") && trimmed.contains(":") && trimmed.hasSuffix("]") + private static func topLevelColonIndex(in text: String) -> String.Index? { + var squareDepth = 0 + var angleDepth = 0 + var parenDepth = 0 + + for index in text.indices { + switch text[index] { + case "[": + squareDepth += 1 + case "]": + squareDepth = max(0, squareDepth - 1) + case "<": + angleDepth += 1 + case ">": + angleDepth = max(0, angleDepth - 1) + case "(": + parenDepth += 1 + case ")": + parenDepth = max(0, parenDepth - 1) + case ":" where squareDepth == 0 && angleDepth == 0 && parenDepth == 0: + return index + default: + break + } + } + + return nil } private static func extractDictionaryTypes(_ type: String) -> (key: String, value: String)? { let trimmed = type.trimmingCharacters(in: .whitespacesAndNewlines) - guard trimmed.hasPrefix("[") && trimmed.hasSuffix("]") && trimmed.contains(":") else { + guard trimmed.hasPrefix("[") && trimmed.hasSuffix("]") else { return nil } let inner = String(trimmed.dropFirst().dropLast()) - let parts = inner.split(separator: ":", maxSplits: 1).map { - $0.trimmingCharacters(in: .whitespacesAndNewlines) + guard let colonIndex = topLevelColonIndex(in: inner) else { + return nil + } + + let key = inner[.. Bool { + extractDictionaryTypes(type) != nil } private static func baseTypeName(_ type: String) -> String { @@ -200,8 +234,12 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { private static func arrayElementType(from type: String) -> String? { let trimmed = type.trimmingCharacters(in: .whitespacesAndNewlines) - if trimmed.hasPrefix("[") && trimmed.hasSuffix("]") && !trimmed.contains(":") { - return String(trimmed.dropFirst().dropLast()).trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.hasPrefix("[") && trimmed.hasSuffix("]") { + let inner = String(trimmed.dropFirst().dropLast()) + guard topLevelColonIndex(in: inner) == nil else { + return nil + } + return inner.trimmingCharacters(in: .whitespacesAndNewlines) } if trimmed.hasPrefix("Array<") && trimmed.hasSuffix(">") { return String(trimmed.dropFirst("Array<".count).dropLast()) @@ -210,10 +248,32 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { return nil } + private static let primitiveTypes: Set = [ + "String", + "Int", + "Double", + "Float", + "Bool", + "Decimal", + ] + private static func partiallyGeneratedTypeName(for type: String) -> String { - let baseType = baseTypeName(type) + partiallyGeneratedTypeName(for: type, preserveOptional: false) + } + + private static func partiallyGeneratedTypeName(for type: String, preserveOptional: Bool) -> String { + let trimmed = type.trimmingCharacters(in: .whitespacesAndNewlines) + if preserveOptional, trimmed.hasSuffix("?") { + let inner = String(trimmed.dropLast()) + return "\(partiallyGeneratedTypeName(for: inner, preserveOptional: true))?" + } + + let baseType = baseTypeName(trimmed) + if primitiveTypes.contains(baseType) || isDictionaryType(baseType) { + return baseType + } if let elementType = arrayElementType(from: baseType) { - let elementPartial = partiallyGeneratedTypeName(for: elementType) + let elementPartial = partiallyGeneratedTypeName(for: elementType, preserveOptional: true) return "[\(elementPartial)]" } return "\(baseType).PartiallyGenerated" @@ -435,7 +495,7 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { } """ } else if let elementType = arrayElementType(from: baseType) { - let elementPartial = partiallyGeneratedTypeName(for: elementType) + let elementPartial = partiallyGeneratedTypeName(for: elementType, preserveOptional: true) let arrayPartial = "[\(elementPartial)]" return """ if let value = properties[\"\(propertyName)\"] { diff --git a/Tests/AnyLanguageModelTests/GenerableMacroTests.swift b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift index ab65a97..625d28e 100644 --- a/Tests/AnyLanguageModelTests/GenerableMacroTests.swift +++ b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift @@ -46,6 +46,21 @@ private struct ArrayContainer { var items: [ArrayItem] } +@Generable +private struct PrimitiveContainer { + @Guide(description: "A title") + var title: String + + @Guide(description: "A count") + var count: Int +} + +@Generable +private struct PrimitiveArrayContainer { + @Guide(description: "Names", .count(2)) + var names: [String] +} + @Suite("Generable Macro") struct GenerableMacroTests { @Test("@Guide description with multiline string") @@ -162,7 +177,7 @@ struct GenerableMacroTests { GeneratedContent(properties: ["name": "Alpha"]), GeneratedContent(properties: ["name": "Beta"]), ]) - ), + ) ] ) @@ -171,6 +186,40 @@ struct GenerableMacroTests { #expect(partial.items?.count == 2) #expect(partial.items?.first?.name == "Alpha") } + + @Test("Primitive properties use concrete partial types") + func primitivePropertyPartialTypes() throws { + let content = GeneratedContent( + properties: [ + "title": "Hello", + "count": 3, + ] + ) + + let container = try PrimitiveContainer(content) + let partial = container.asPartiallyGenerated() + #expect(partial.title == "Hello") + #expect(partial.count == 3) + } + + @Test("Array primitives use concrete element types") + func arrayPrimitivePartialTypes() throws { + let content = GeneratedContent( + properties: [ + "names": GeneratedContent( + kind: .array([ + GeneratedContent("Alpha"), + GeneratedContent("Beta"), + ]) + ) + ] + ) + + let container = try PrimitiveArrayContainer(content) + let partial = container.asPartiallyGenerated() + #expect(partial.names?.count == 2) + #expect(partial.names?.first == "Alpha") + } } // MARK: - #Playground Usage From b6d49dc365a2f23664aeb343bbefddeea1e9ae5b Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 15 Jan 2026 05:12:08 -0800 Subject: [PATCH 05/10] Simplify topLevelColonIndex --- .../GenerableMacro.swift | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Sources/AnyLanguageModelMacros/GenerableMacro.swift b/Sources/AnyLanguageModelMacros/GenerableMacro.swift index 76ade90..8f0ba83 100644 --- a/Sources/AnyLanguageModelMacros/GenerableMacro.swift +++ b/Sources/AnyLanguageModelMacros/GenerableMacro.swift @@ -169,29 +169,31 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { } private static func topLevelColonIndex(in text: String) -> String.Index? { - var squareDepth = 0 - var angleDepth = 0 - var parenDepth = 0 + var totalDepth = 0 for index in text.indices { switch text[index] { case "[": - squareDepth += 1 + totalDepth += 1 case "]": - squareDepth = max(0, squareDepth - 1) + totalDepth -= 1 case "<": - angleDepth += 1 + totalDepth += 1 case ">": - angleDepth = max(0, angleDepth - 1) + totalDepth -= 1 case "(": - parenDepth += 1 + totalDepth += 1 case ")": - parenDepth = max(0, parenDepth - 1) - case ":" where squareDepth == 0 && angleDepth == 0 && parenDepth == 0: + totalDepth -= 1 + case ":" where totalDepth == 0: return index default: break } + + if totalDepth < 0 { + return nil + } } return nil From 0fcc0f79a613e49f62ffdc7dbda117065fc8d3e4 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 15 Jan 2026 05:18:22 -0800 Subject: [PATCH 06/10] Expand macro test coverage further --- .../GenerableMacroTests.swift | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/Tests/AnyLanguageModelTests/GenerableMacroTests.swift b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift index 625d28e..b8d419c 100644 --- a/Tests/AnyLanguageModelTests/GenerableMacroTests.swift +++ b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift @@ -61,6 +61,42 @@ private struct PrimitiveArrayContainer { var names: [String] } +@Generable +private struct OptionalArrayContainer { + @Guide(description: "Optional names", .count(2)) + var names: [String]? +} + +@Generable +private struct NestedArrayContainer { + @Guide(description: "Nested items", .count(2)) + var items: [[ArrayItem]] +} + +@Generable +private struct OptionalPrimitiveContainer { + @Guide(description: "Optional title") + var title: String? + + @Guide(description: "Optional count") + var count: Int? + + @Guide(description: "Optional flag") + var flag: Bool? +} + +@Generable +private struct OptionalItemContainer { + @Guide(description: "Optional item") + var item: ArrayItem? +} + +@Generable +private struct OptionalItemsContainer { + @Guide(description: "Optional items", .count(2)) + var items: [ArrayItem]? +} + @Suite("Generable Macro") struct GenerableMacroTests { @Test("@Guide description with multiline string") @@ -220,6 +256,135 @@ struct GenerableMacroTests { #expect(partial.names?.count == 2) #expect(partial.names?.first == "Alpha") } + + @Test("Optional array properties are partially generated") + func optionalArrayPartialTypes() throws { + let content = GeneratedContent( + properties: [ + "names": GeneratedContent( + kind: .array([ + GeneratedContent("Alpha"), + GeneratedContent("Beta"), + ]) + ) + ] + ) + + let container = try OptionalArrayContainer(content) + let partial = container.asPartiallyGenerated() + #expect(partial.names?.count == 2) + #expect(partial.names?.first == "Alpha") + } + + @Test("Nested arrays of generable types are handled") + func nestedArrayPartialTypes() throws { + let content = GeneratedContent( + properties: [ + "items": GeneratedContent( + kind: .array([ + GeneratedContent( + kind: .array([ + GeneratedContent(properties: ["name": "Alpha"]), + GeneratedContent(properties: ["name": "Beta"]), + ]) + ), + GeneratedContent( + kind: .array([ + GeneratedContent(properties: ["name": "Gamma"]) + ]) + ), + ]) + ) + ] + ) + + let container = try NestedArrayContainer(content) + let partial = container.asPartiallyGenerated() + #expect(partial.items?.count == 2) + #expect(partial.items?.first?.count == 2) + #expect(partial.items?.first?.first?.name == "Alpha") + #expect(partial.items?.last?.first?.name == "Gamma") + } + + @Test("Optional primitive properties are handled") + func optionalPrimitivePartialTypes() throws { + let content = GeneratedContent( + properties: [ + "title": "Hello", + "count": 3, + "flag": true, + ] + ) + + let container = try OptionalPrimitiveContainer(content) + let partial = container.asPartiallyGenerated() + #expect(partial.title == "Hello") + #expect(partial.count == 3) + #expect(partial.flag == true) + } + + @Test("Optional generable properties are handled") + func optionalItemPartialTypes() throws { + let content = GeneratedContent( + properties: [ + "item": GeneratedContent(properties: ["name": "Alpha"]) + ] + ) + + let container = try OptionalItemContainer(content) + let partial = container.asPartiallyGenerated() + #expect(partial.item?.name == "Alpha") + } + + @Test("Optional arrays of generable types are handled") + func optionalItemsPartialTypes() throws { + let content = GeneratedContent( + properties: [ + "items": GeneratedContent( + kind: .array([ + GeneratedContent(properties: ["name": "Alpha"]), + GeneratedContent(properties: ["name": "Beta"]), + ]) + ) + ] + ) + + let container = try OptionalItemsContainer(content) + let partial = container.asPartiallyGenerated() + #expect(partial.items?.count == 2) + #expect(partial.items?.first?.name == "Alpha") + } + + @Test("Missing optional properties become nil in partials") + func missingOptionalProperties() throws { + let content = GeneratedContent(properties: [:]) + + let primitive = try OptionalPrimitiveContainer(content).asPartiallyGenerated() + #expect(primitive.title == nil) + #expect(primitive.count == nil) + #expect(primitive.flag == nil) + + let item = try OptionalItemContainer(content).asPartiallyGenerated() + #expect(item.item == nil) + + let items = try OptionalItemsContainer(content).asPartiallyGenerated() + #expect(items.items == nil) + + let names = try OptionalArrayContainer(content).asPartiallyGenerated() + #expect(names.names == nil) + } + + @Test("Schema generation includes optional properties") + func schemaIncludesOptionalProperties() throws { + let schema = OptionalPrimitiveContainer.generationSchema + let encoder = JSONEncoder() + let jsonData = try encoder.encode(schema) + let jsonString = String(data: jsonData, encoding: .utf8) ?? "" + + #expect(jsonString.contains("\"title\"")) + #expect(jsonString.contains("\"count\"")) + #expect(jsonString.contains("\"flag\"")) + } } // MARK: - #Playground Usage From 7a96ec3c246ec886edc43d6ab7a2c85a3d9b4864 Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 15 Jan 2026 05:24:27 -0800 Subject: [PATCH 07/10] Handle ?? syntax --- Sources/AnyLanguageModelMacros/GenerableMacro.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/AnyLanguageModelMacros/GenerableMacro.swift b/Sources/AnyLanguageModelMacros/GenerableMacro.swift index 8f0ba83..14d61e4 100644 --- a/Sources/AnyLanguageModelMacros/GenerableMacro.swift +++ b/Sources/AnyLanguageModelMacros/GenerableMacro.swift @@ -265,6 +265,13 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { private static func partiallyGeneratedTypeName(for type: String, preserveOptional: Bool) -> String { let trimmed = type.trimmingCharacters(in: .whitespacesAndNewlines) + if preserveOptional, trimmed.hasSuffix("??") { + var normalized = trimmed + while normalized.hasSuffix("??") { + normalized = String(normalized.dropLast()) + } + return partiallyGeneratedTypeName(for: normalized, preserveOptional: true) + } if preserveOptional, trimmed.hasSuffix("?") { let inner = String(trimmed.dropLast()) return "\(partiallyGeneratedTypeName(for: inner, preserveOptional: true))?" From fb61b9d51165bdab916084029da91b4a76bdc9bf Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 15 Jan 2026 05:24:45 -0800 Subject: [PATCH 08/10] Make test function names more accurate --- .../GenerableMacroTests.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Tests/AnyLanguageModelTests/GenerableMacroTests.swift b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift index b8d419c..e75c4c9 100644 --- a/Tests/AnyLanguageModelTests/GenerableMacroTests.swift +++ b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift @@ -205,7 +205,7 @@ struct GenerableMacroTests { } @Test("Array properties use partially generated element types") - func arrayPropertyPartialTypes() throws { + func arrayPropertiesUsePartiallyGeneratedElements() throws { let content = GeneratedContent( properties: [ "items": GeneratedContent( @@ -224,7 +224,7 @@ struct GenerableMacroTests { } @Test("Primitive properties use concrete partial types") - func primitivePropertyPartialTypes() throws { + func primitivePropertiesRemainUnchanged() throws { let content = GeneratedContent( properties: [ "title": "Hello", @@ -239,7 +239,7 @@ struct GenerableMacroTests { } @Test("Array primitives use concrete element types") - func arrayPrimitivePartialTypes() throws { + func arrayPrimitivesRemainConcrete() throws { let content = GeneratedContent( properties: [ "names": GeneratedContent( @@ -258,7 +258,7 @@ struct GenerableMacroTests { } @Test("Optional array properties are partially generated") - func optionalArrayPartialTypes() throws { + func optionalPrimitiveArraysRemainConcrete() throws { let content = GeneratedContent( properties: [ "names": GeneratedContent( @@ -277,7 +277,7 @@ struct GenerableMacroTests { } @Test("Nested arrays of generable types are handled") - func nestedArrayPartialTypes() throws { + func nestedArraysGenerateNestedPartialTypes() throws { let content = GeneratedContent( properties: [ "items": GeneratedContent( @@ -307,7 +307,7 @@ struct GenerableMacroTests { } @Test("Optional primitive properties are handled") - func optionalPrimitivePartialTypes() throws { + func optionalPrimitivePropertiesHandled() throws { let content = GeneratedContent( properties: [ "title": "Hello", @@ -324,7 +324,7 @@ struct GenerableMacroTests { } @Test("Optional generable properties are handled") - func optionalItemPartialTypes() throws { + func optionalGenerableItemBecomesPartial() throws { let content = GeneratedContent( properties: [ "item": GeneratedContent(properties: ["name": "Alpha"]) @@ -337,7 +337,7 @@ struct GenerableMacroTests { } @Test("Optional arrays of generable types are handled") - func optionalItemsPartialTypes() throws { + func optionalGenerableArraysTransformToPartialArrays() throws { let content = GeneratedContent( properties: [ "items": GeneratedContent( From 8553bec3cf06c519ac4adf2dbf2e46d26668f96a Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 15 Jan 2026 05:32:41 -0800 Subject: [PATCH 09/10] Further improve ?? handling --- .../AnyLanguageModelMacros/GenerableMacro.swift | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Sources/AnyLanguageModelMacros/GenerableMacro.swift b/Sources/AnyLanguageModelMacros/GenerableMacro.swift index 14d61e4..f22c640 100644 --- a/Sources/AnyLanguageModelMacros/GenerableMacro.swift +++ b/Sources/AnyLanguageModelMacros/GenerableMacro.swift @@ -265,16 +265,19 @@ public struct GenerableMacro: MemberMacro, ExtensionMacro { private static func partiallyGeneratedTypeName(for type: String, preserveOptional: Bool) -> String { let trimmed = type.trimmingCharacters(in: .whitespacesAndNewlines) - if preserveOptional, trimmed.hasSuffix("??") { + if preserveOptional { var normalized = trimmed - while normalized.hasSuffix("??") { + var optionalCount = 0 + while normalized.hasSuffix("?") { normalized = String(normalized.dropLast()) + optionalCount += 1 + } + if optionalCount > 1 { + return "\(partiallyGeneratedTypeName(for: normalized, preserveOptional: false))?" + } + if optionalCount == 1 { + return "\(partiallyGeneratedTypeName(for: normalized, preserveOptional: true))?" } - return partiallyGeneratedTypeName(for: normalized, preserveOptional: true) - } - if preserveOptional, trimmed.hasSuffix("?") { - let inner = String(trimmed.dropLast()) - return "\(partiallyGeneratedTypeName(for: inner, preserveOptional: true))?" } let baseType = baseTypeName(trimmed) From 5553f6a6134e65a4c58d235a32640b3176cad27b Mon Sep 17 00:00:00 2001 From: Mattt Zmuda Date: Thu, 15 Jan 2026 05:32:50 -0800 Subject: [PATCH 10/10] Update test description --- Tests/AnyLanguageModelTests/GenerableMacroTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/AnyLanguageModelTests/GenerableMacroTests.swift b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift index e75c4c9..7134821 100644 --- a/Tests/AnyLanguageModelTests/GenerableMacroTests.swift +++ b/Tests/AnyLanguageModelTests/GenerableMacroTests.swift @@ -257,7 +257,7 @@ struct GenerableMacroTests { #expect(partial.names?.first == "Alpha") } - @Test("Optional array properties are partially generated") + @Test("Optional primitive arrays remain concrete") func optionalPrimitiveArraysRemainConcrete() throws { let content = GeneratedContent( properties: [