Skip to content

Commit

Permalink
Export enum (#476)
Browse files Browse the repository at this point in the history
Export enumeration support

Any enumeration that is blessed with `CaseIterable` can be then exported to Godot
by using `@Export(.enum)` on it, like this:

```
enum MyEnumeration: Int, CaseIterable {
    case first
    case second
}
```

To export, use the `.enum` parameter to Export:

```
@godot
class Demo: Node {
     @export(.enum)
     var myState: MyEnumeration
}
```

One limitation of the current change is that this works by using
`CaseIterable`, either by manually typing it, or using the
`PickerNameProvider` macro.
  • Loading branch information
migueldeicaza authored May 21, 2024
1 parent e7e8de3 commit 04e47f5
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 43 deletions.
2 changes: 1 addition & 1 deletion Generator/Generator/Enums.swift
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func generateEnums (_ p: Printer, cdef: JClassInfo?, values: [JGodotGlobalEnumEl
}
let extraConformances = enumDefName == "Error" ? ", Error" : ""

p ("public enum \(getGodotType (SimpleType (type: enumDefName))): Int64, CustomDebugStringConvertible\(extraConformances)") {
p ("public enum \(getGodotType (SimpleType (type: enumDefName))): Int64, CaseIterable, CustomDebugStringConvertible\(extraConformances)") {
var used = Set<Int> ()

func getName (_ enumVal: JGodotValueElement) -> String? {
Expand Down
19 changes: 19 additions & 0 deletions Sources/SwiftGodot/SwiftGodot.docc/Exports.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,3 +288,22 @@ To surface arrays in Godot, use a strong type for it, for example:
@Export
var myResources: VariantCollection<Resource>
```

### Enumeration Values

To surface enumeration values, use the `@Export(.enum)` marker on your variable,
and it is important that your enumeration conforms to `CaseIterable`, like this:

```
enum MyEnum: CaseIterable {
case first
case second
}

@Godot
class Sample: Node {
@Export(.enum)
var myValue: MyEnum
}
```

36 changes: 30 additions & 6 deletions Sources/SwiftGodotMacroLibrary/MacroExport.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// File.swift
// MacroExport.swift
//
//
// Created by Miguel de Icaza on 9/25/23.
Expand All @@ -15,8 +15,17 @@ import SwiftSyntaxMacros
public struct GodotExport: PeerMacro {


static func makeGetAccessor (varName: String, isOptional: Bool) -> String {
static func makeGetAccessor (varName: String, isOptional: Bool, isEnum: Bool) -> String {
let name = "_mproxy_get_\(varName)"
if isEnum {
return
"""
func \(name) (args: [Variant]) -> Variant? {
return Variant (\(varName).rawValue)
}
"""

}
if isOptional {
return
"""
Expand All @@ -35,11 +44,18 @@ public struct GodotExport: PeerMacro {
}
}

static func makeSetAccessor (varName: String, typeName: String, isOptional: Bool) -> String {
static func makeSetAccessor (varName: String, typeName: String, isOptional: Bool, isEnum: Bool) -> String {
let name = "_mproxy_set_\(varName)"
var body: String = ""

if typeName == "Variant" {
if isEnum {
body =
"""
if let iv = Int (args [0]), let ev = \(typeName)(rawValue: numericCast (iv)) {
self.\(varName) = ev
}
"""
} else if typeName == "Variant" {
body = "\(varName) = args [0]"
} else if godotVariants [typeName] == nil {
let optBody = isOptional ? " else { \(varName) = nil }" : ""
Expand Down Expand Up @@ -113,6 +129,14 @@ public struct GodotExport: PeerMacro {
throw GodotMacroError.requiresNonOptionalGArrayCollection
}

var isEnum = false
if case let .argumentList (arguments) = node.arguments, let expression = arguments.first?.expression {
isEnum = expression.description.trimmingCharacters(in: .whitespacesAndNewlines) == ".enum"
}
if isEnum && isOptional {
throw GodotMacroError.noSupportForOptionalEnums

}
var results: [DeclSyntax] = []

for singleVar in varDecl.bindings {
Expand Down Expand Up @@ -162,8 +186,8 @@ public struct GodotExport: PeerMacro {
results.append (DeclSyntax(stringLiteral: makeGArrayCollectionGetProxyAccessor(varName: varName, elementTypeName: elementTypeName)))
results.append (DeclSyntax(stringLiteral: makeGArrayCollectionSetProxyAccessor(varName: varName, elementTypeName: elementTypeName)))
} else if let typeName = type.as(IdentifierTypeSyntax.self)?.name.text {
results.append (DeclSyntax(stringLiteral: makeSetAccessor(varName: varName, typeName: typeName, isOptional: isOptional)))
results.append (DeclSyntax(stringLiteral: makeGetAccessor(varName: varName, isOptional: isOptional)))
results.append (DeclSyntax(stringLiteral: makeSetAccessor(varName: varName, typeName: typeName, isOptional: isOptional, isEnum: isEnum)))
results.append (DeclSyntax(stringLiteral: makeGetAccessor(varName: varName, isOptional: isOptional, isEnum: isEnum)))
}
}

Expand Down
44 changes: 38 additions & 6 deletions Sources/SwiftGodotMacroLibrary/MacroGodot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,11 @@ class GodotMacroProcessor {
ctor.append ("\tclassInfo.registerMethod(name: StringName(\"\(funcName)\"), flags: .default, returnValue: \(retProp ?? "nil"), arguments: \(funcArgs == "" ? "[]" : "\(funcName)Args"), function: \(className)._mproxy_\(funcName))\n")
}

func processVariable (_ varDecl: VariableDeclSyntax, prefix: String?) throws {
// Returns true if it used "tryCase"
func processVariable (_ varDecl: VariableDeclSyntax, prefix: String?) throws -> Bool {
var usedTryCase = false
guard hasExportAttribute(varDecl.attributes) else {
return
return false
}
guard let last = varDecl.bindings.last else {
throw GodotMacroError.noVariablesFound
Expand All @@ -232,6 +234,13 @@ class GodotMacroProcessor {
guard let ips = singleVar.pattern.as(IdentifierPatternSyntax.self) else {
throw GodotMacroError.expectedIdentifier(singleVar)
}
guard let last = varDecl.bindings.last else {
throw GodotMacroError.noVariablesFound
}
guard let ta = last.typeAnnotation?.type.description.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) else {
throw GodotMacroError.noTypeFound(varDecl)
}

let varNameWithPrefix = ips.identifier.text
let varNameWithoutPrefix = String(varNameWithPrefix.trimmingPrefix(prefix ?? ""))
let proxySetterName = "_mproxy_set_\(varNameWithPrefix)"
Expand Down Expand Up @@ -275,16 +284,24 @@ class GodotMacroProcessor {
}
}
}
let propType = godotTypeToProp (typeName: typeName)
let mappedType = godotTypeToProp (typeName: typeName)
let pinfo = "_p\(varNameWithPrefix)"
let isEnum = firstLabeledExpression?.description == "enum"


let propType = isEnum ? ".int" : mappedType
let fallback = isEnum ? "tryCase (\(ta).self)" : "\"\""
if isEnum {
usedTryCase = true
}
ctor.append (
"""
let \(pinfo) = PropInfo (
propertyType: \(propType),
propertyName: "\(varNameWithPrefix)",
className: className,
hint: .\(firstLabeledExpression?.description ?? "none"),
hintStr: \(secondLabeledExpression?.description ?? "\"\""),
hintStr: \(secondLabeledExpression?.description ?? fallback),
usage: .default)
""")
Expand All @@ -293,6 +310,10 @@ class GodotMacroProcessor {
ctor.append("\tclassInfo.registerMethod (name: \"\(setterName)\", flags: .default, returnValue: nil, arguments: [\(pinfo)], function: \(className).\(proxySetterName))\n")
ctor.append("\tclassInfo.registerProperty (\(pinfo), getter: \"\(getterName)\", setter: \"\(setterName)\")\n")
}
if usedTryCase {
return true
}
return false
}

func processGArrayCollectionVariable(_ varDecl: VariableDeclSyntax, prefix: String?) throws {
Expand Down Expand Up @@ -402,7 +423,7 @@ class GodotMacroProcessor {
"""
var previousGroupPrefix: String? = nil
var previousSubgroupPrefix: String? = nil

var needTrycase = false
for member in classDecl.memberBlock.members.enumerated() {
let decl = member.element.decl

Expand All @@ -420,12 +441,23 @@ class GodotMacroProcessor {
if varDecl.isGArrayCollection {
try processGArrayCollectionVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix)
} else {
try processVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix)
if try processVariable(varDecl, prefix: previousSubgroupPrefix ?? previousGroupPrefix) {
needTrycase = true
}
}
} else if let macroDecl = MacroExpansionDeclSyntax(decl) {
try classInitSignals(macroDecl)
}
}
if needTrycase {
ctor.append (
"""
func tryCase <T : RawRepresentable & CaseIterable> (_ type: T.Type) -> GString {
GString (type.allCases.map { v in "\\(v):\\(v.rawValue)" }.joined(separator: ","))
}
func tryCase <T : RawRepresentable> (_ type: T.Type) -> String { "" }
""")
}
ctor.append("} ()\n")
return ctor
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/SwiftGodotMacroLibrary/MacroSharedApi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ enum GodotMacroError: Error, DiagnosticMessage {
case expectedIdentifier(PatternBindingListSyntax.Element)
case unknownError(Error)
case unsupportedCallableEffect
case noSupportForOptionalEnums

var severity: DiagnosticSeverity {
return .error
Expand Down Expand Up @@ -77,6 +78,8 @@ enum GodotMacroError: Error, DiagnosticMessage {
"@Export optional Collections are not supported"
case .unsupportedCallableEffect:
"@Callable does not support asynchronous or throwing functions"
case .noSupportForOptionalEnums:
"@Export(.enum) does not support optional values for the enumeration"
}
}

Expand Down
8 changes: 1 addition & 7 deletions Sources/SwiftGodotMacroLibrary/PickerNameProviderMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public struct PickerNameProviderMacro: ExtensionMacro {
case .notAnEnum:
return "@PickerNameProvider can only be applied to an 'enum'"
case .missingInt:
return "@PickerNameProvider requires an Int backing"
return "@PickerNameProvider requires an Int64 backing"
}
}

Expand Down Expand Up @@ -58,12 +58,6 @@ public struct PickerNameProviderMacro: ExtensionMacro {
let types = inheritors.map { $0.type.as(IdentifierTypeSyntax.self) }
let names = types.map { $0?.name.text }

guard names.contains("Int") else {
let missingInt = Diagnostic(node: declaration.root, message: ProviderDiagnostic.missingInt)
context.diagnose(missingInt)
return []
}

let members = enumDecl.memberBlock.members
let cases = members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
let elements = cases.flatMap { $0.elements }
Expand Down
111 changes: 111 additions & 0 deletions Tests/SwiftGodotMacrosTests/MacroGodotExportEnumTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
//
// MacroGodotExportEnumTests.swift
// SwiftGodotMacrosTests
//
// Created by Estevan Hernandez on 11/29/23.
//

import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
import SwiftGodotMacroLibrary

final class MacroGodotExportEnumTests: XCTestCase {
let testMacros: [String: Macro.Type] = [
"Godot": GodotMacro.self,
"Export": GodotExport.self,
]

func testExportEnumGodot() {
assertMacroExpansion(
"""
enum Demo: Int, CaseIterable {
case first
}
enum Demo64: Int64, CaseIterable {
case first
}
@Godot
class SomeNode: Node {
@Export(.enum) var demo: Demo
@Export(.enum) var demo64: Demo64
}
""",
expandedSource:
"""
enum Demo: Int, CaseIterable {
case first
}
enum Demo64: Int64, CaseIterable {
case first
}
class SomeNode: Node {
var demo: Demo
func _mproxy_set_demo (args: [Variant]) -> Variant? {
if let iv = Int (args [0]), let ev = Demo(rawValue: numericCast (iv)) {
self.demo = ev
}
return nil
}
func _mproxy_get_demo (args: [Variant]) -> Variant? {
return Variant (demo.rawValue)
}
var demo64: Demo64
func _mproxy_set_demo64 (args: [Variant]) -> Variant? {
if let iv = Int (args [0]), let ev = Demo64(rawValue: numericCast (iv)) {
self.demo64 = ev
}
return nil
}
func _mproxy_get_demo64 (args: [Variant]) -> Variant? {
return Variant (demo64.rawValue)
}
override open class var classInitializer: Void {
let _ = super.classInitializer
return _initializeClass
}
private static var _initializeClass: Void = {
let className = StringName("SomeNode")
assert(ClassDB.classExists(class: className))
let classInfo = ClassInfo<SomeNode> (name: className)
let _pdemo = PropInfo (
propertyType: .int,
propertyName: "demo",
className: className,
hint: .enum,
hintStr: tryCase (Demo.self),
usage: .default)
classInfo.registerMethod (name: "_mproxy_get_demo", flags: .default, returnValue: _pdemo, arguments: [], function: SomeNode._mproxy_get_demo)
classInfo.registerMethod (name: "_mproxy_set_demo", flags: .default, returnValue: nil, arguments: [_pdemo], function: SomeNode._mproxy_set_demo)
classInfo.registerProperty (_pdemo, getter: "_mproxy_get_demo", setter: "_mproxy_set_demo")
let _pdemo64 = PropInfo (
propertyType: .int,
propertyName: "demo64",
className: className,
hint: .enum,
hintStr: tryCase (Demo64.self),
usage: .default)
classInfo.registerMethod (name: "_mproxy_get_demo64", flags: .default, returnValue: _pdemo64, arguments: [], function: SomeNode._mproxy_get_demo64)
classInfo.registerMethod (name: "_mproxy_set_demo64", flags: .default, returnValue: nil, arguments: [_pdemo64], function: SomeNode._mproxy_set_demo64)
classInfo.registerProperty (_pdemo64, getter: "_mproxy_get_demo64", setter: "_mproxy_set_demo64")
func tryCase <T : RawRepresentable & CaseIterable> (_ type: T.Type) -> GString {
GString (type.allCases.map { v in
"\\(v):\\(v.rawValue)"
} .joined(separator: ","))
}
func tryCase <T : RawRepresentable> (_ type: T.Type) -> String {
""
}
} ()
}
""",
macros: testMacros
)
}
}
Loading

0 comments on commit 04e47f5

Please sign in to comment.