# Copyright 2018 Xored Software, Inc. import streams, json, os, strutils, times, sets, tables, options import sequtils, algorithm import compiler/ast, compiler/renderer, compiler/idents, compiler/astalgo include "internal/backwardcompat.inc.nim" when (NimMajor, NimMinor, NimPatch) >= (0, 19, 0): import compiler/lineinfos when (NimMajor, NimMinor, NimPatch) >= (0, 19, 0): var gic = newIdentCache() proc getIdent(ident: string): PIdent = getIdent(gic, ident) proc ident(ident: string): PNode = result = PNode(kind: nkIdent, ident: getIdent(ident)) proc newPNode(kind: TNodeKind): PNode = when (NimMajor, NimMinor, NimPatch) < (0, 19, 0): result = newNode(kind) else: result = PNode(kind: kind) result.info.fileIndex = InvalidFileIdx result.info.col = int16(-1) result.info.line = uint16(0) proc addChain(node: PNode, others: varargs[PNode]): PNode {.discardable.} = for other in others: node.add(other) result = node proc newIdentDefs(name, typ: PNode, defaultVal: PNode = nil): PNode = result = newPNode(nkIdentDefs) result.add(name) result.add(typ) if defaultVal.isNil: result.add(newPNode(nkEmpty)) else: result.add(defaultVal) proc newProc(name: PNode, params: openArray[PNode], body = newPNode(nkEmpty), procType = nkProcDef): PNode = result = newPNode(procType) result.add(name) result.add(newPNode(nkEmpty)) result.add(newPNode(nkEmpty)) let formalParams = newPNode(nkFormalParams) for param in params: formalParams.add(param) result.add(formalParams) result.add(newPNode(nkEmpty)) result.add(newPNode(nkEmpty)) result.add(body) proc newIfStmt(condition, body: PNode): PNode = result = newPNode(nkIfStmt) let branch = newPNode(nkElifBranch) result.add(branch) branch.add(condition) branch.add(body) proc newCall(theProc: PNode, args: varargs[PNode]): PNode = result = newPNode(nkCall) result.add(theProc) for arg in args: result.add(arg) proc newCall(theProc: string, args: varargs[PNode]): PNode = newCall(ident(theProc), args) proc newStrLit(s: string): PNode = result = newPNode(nkStrLit) result.strVal = s proc newRStrLit(s: string): PNode = result = newPNode(nkRStrLit) result.strVal = s proc newCStringLit(s: string): PNode = newPNode(nkCallStrLit).addChain(ident("cstring"), newRStrLit(s)) proc newIntLit(val: BiggestInt): PNode = result = newPNode(nkIntLit) result.intVal = val proc newInt64Lit(val: BiggestInt): PNode = result = newPNode(nkInt64Lit) result.intVal = val proc newFloatLit(val: BiggestFloat): PNode = result = newPNode(nkFloatLit) result.floatVal = val proc newFloat64Lit(val: BiggestFloat): PNode = result = newPNode(nkFloat64Lit) result.floatVal = val proc newNilLit(): PNode = result = newPNode(nkNilLit) proc newDotExpr(left, right: PNode): PNode = result = newPNode(nkDotExpr) result.add(left) result.add(right) proc newCommand(left, right: PNode): PNode = result = newPNode(nkCommand) result.add(left) result.add(right) proc infix(left, op, right: PNode): PNode = result = newPNode(nkInfix) result.add(op) result.add(left) result.add(right) proc postfix(left: PNode, op: string): PNode = result = newPNode(nkPostfix) result.add(ident(op)) result.add(left) proc prefix(op: string, right: PNode): PNode = result = newPNode(nkPrefix) result.add(ident(op)) result.add(right) proc newBracketExpr(first, bracket: PNode): PNode = newPNode(nkBracketExpr).addChain(first, bracket) proc newEmptyNode(): PNode = newPNode(nkEmpty) type GodotType = ref object name: string godotName: string baseName: string doc: string derivedCount: int isSingleton: bool jsonNode: JsonNode proc findCommonRoot(typeRegistry: Table[string, GodotType], types: seq[string]): string = if types.len == 0: return "Object" var chains = newSeq[seq[string]](types.len) for idx, typ in types: chains[idx] = newSeq[string]() chains[idx].add(typ) var curTyp = typeRegistry[typ] while curTyp.baseName != "NimGodotObject": chains[idx].add(curTyp.baseName) curTyp = typeRegistry[curTyp.baseName] chains[idx].reverse() var idx = 0 while true: var curType = none(string) var allEqual = true for chain in chains: if idx >= chain.len: allEqual = false break if curType.isNone: curType = some(chain[idx]) elif curType.get != chain[idx]: allEqual = false break if allEqual: result = curType.get else: break inc idx proc incDerivedCount(types: var Table[string, GodotType], className: string) = let typ = types.getOrDefault(className) if not typ.isNil: inc typ.derivedCount types.incDerivedCount(typ.baseName) proc toNimType(godotType: string): string = result = case godotType: of "float": "float64" of "int": "int64" of "String": "string" of "File": "godottypes.File" else: godotType if result.startsWith("enum."): result = if result == "enum.Error": "Error" else: "int64" proc toNimType(types: Table[string, GodotType], godotType: string): string = let origType = if godotType.contains(','): findCommonRoot(types, godotType.split(',')) else: godotType result = toNimType(origType) proc toNimStyle(name: string): string = result = newStringOfCap(name.len + 1) var i = 0 var makeUpperCase: bool while i < name.len: if name[i] == '_': if result.len != 0: makeUpperCase = true elif name[i] != '/': # remove slashes if makeUpperCase: result.add(name[i].toUpperAscii()) makeUpperCase = false else: result.add(name[i]) inc i while isKeyword(getIdent(result)): result.add(result[result.len - 1]) if result == "result": result = "resultVal" proc newRefTypeNode(typeSection: PNode, typ, base, doc: string, isExported = true) = let typeDef = newPNode(nkTypeDef) typeSection.add(typeDef) if isExported: typeDef.add(postfix(ident(typ), "*")) else: typeDef.add(ident(typ)) typeDef.add(newPNode(nkEmpty)) # generic let objTy = newPNode(nkObjectTy) let refTy = newPNode(nkRefTy) refTy.add(objTy) typeDef.add(refTy) objTy.add(newPNode(nkEmpty)) let inherit = newPNode(nkOfInherit) inherit.add(ident(base)) objTy.add(inherit) if doc.len > 0: let recList = newPNode(nkRecList) let docNode = newPNode(nkCommentStmt) docNode.comment = doc recList.add(docNode) objTy.add(recList) else: objTy.add(newPNode(nkEmpty)) proc makeConstSection(constObj: JsonNode): PNode = if constObj.len == 0: return newPNode(nkEmpty) result = newPNode(nkConstSection) for field, val in constObj: let def = newPNode(nkConstDef) def.add(postfix(ident(field), "*")) def.add(newPNode(nkEmpty)) # infer type var valNode: PNode if val.kind == JInt: valNode = newInt64Lit(val.num) # valNode.intVal = val.num elif val.kind == JFloat: valNode = newFloat64Lit(val.fnum) elif val.kind == JBool: valNode = ident($val.bval) elif val.kind == JString: valNode = newPNode(nkStrLit) valNode.strVal = val.str else: raise newException(ValueError, "Unexpected constant kind: " & $val.kind) def.add(valNode) result.add(def) type ArgKind = enum Normal, VarArgs Bound MethodArg = object name: string typ: string defaultVal: JsonNode kind: ArgKind MethodInfo = ref object name: PNode godotName: string typ: GodotType args: seq[MethodArg] returnType: Option[string] isVirtual: bool isBase: bool isDiscardable: bool const standardTypes = toHashSet( ["bool", "cint", "int", "uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "Error", "int64", "float32", "cfloat", "float64", "GodotString", "Vector2", "Rect2", "Vector3", "Transform2D", "Plane", "Quat", "AABB", "Basis", "Transform", "Color", "RID", "NodePath", "Dictionary", "Variant", "Array", "PoolByteArray", "PoolIntArray", "PoolRealArray", "PoolStringArray", "PoolVector2Array", "PoolVector3Array", "PoolColorArray"]) const variantPrimitives = [ "bool", "cint", "int", "uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "Error", "int64", "float32", "cfloat", "float64", "GodotString", "Vector2", "Rect2", "Vector3", "Transform2D", "Plane", "Quat", "AABB", "Basis", "Transform", "Color", "RID"] const smallIntTypes = toHashSet(["uint8", "int8", "uint16", "int16", "uint32", "int32", "cint", "int", "Error"]) const int64Types = toHashSet(["uint64", "int64"]) const intTypes = union(smallIntTypes, int64Types) const float64Types = toHashSet(["float64", "cdouble"]) const float32Types = toHashSet(["float32", "cfloat"]) const floatTypes = union(float64Types, float32Types) const arrayTypes = toHashSet(["Array", "PoolByteArray", "PoolIntArray", "PoolRealArray", "PoolStringArray", "PoolVector2Array", "PoolVector3Array", "PoolColorArray"]) const wrapperTypes = union(arrayTypes, toHashSet(["NodePath", "Dictionary", "Variant"])) proc getInternalPtr(varName: PNode, typ: string): PNode = assert(typ in wrapperTypes or typ == "Object") result = newDotExpr(varName, ident("godot" & typ)) proc newNilCheck(ident, toAssign: PNode): PNode = newIfStmt(newCall("isNil", ident), newPNode(nkAsgn).addChain(ident).addChain( toAssign ) ) proc singletonName(typ: string): PNode = ident("singleton" & typ) proc makeDefaultValue(arg: MethodArg): PNode = result = newEmptyNode() if not arg.defaultVal.isNil: var failed = false if arg.typ == "bool": result = ident(arg.defaultVal.str.toLowerAscii()) elif arg.typ in intTypes: result = newInt64Lit(parseBiggestInt(arg.defaultVal.str)) elif arg.typ == "string" or arg.typ == "NodePath": result = newStrLit(arg.defaultVal.str) elif arg.typ in floatTypes: result = newFloatLit(parseFloat(arg.defaultVal.str)) elif arg.defaultVal.str == "Null": if arg.typ == "Variant": result = newCall("newVariant") else: result = newNilLit() elif arg.typ == "Color": result = newCall("initColor", arg.defaultVal.str.split(',').mapIt(newFloatLit(parseFloat(it)))) elif arg.typ == "RID" and arg.defaultVal.str == "[RID]": result = newCall("initRID") elif arg.typ in arrayTypes and (arg.defaultVal.str == "[]" or arg.defaultVal.str == "[" & arg.typ & "]"): result = newCall("new" & arg.typ) elif arg.typ == "Vector2" or arg.typ == "Vector3" or arg.typ == "Rect2": let parts = arg.defaultVal.str.replace("(", "").replace(")", ""). replace(" ", ""). split(',').mapIt(newFloatLit(parseFloat(it))) if arg.typ.endsWith("r2"): result = newCall("vec2", parts) elif arg.typ.endsWith('3'): result = newCall("vec3", parts) else: result = newCall("initRect2", parts) elif arg.defaultVal.str == "[Object:null]": result = newNilLit() elif arg.typ == "Transform": let transformParts = arg.defaultVal.str.replace(" ", "").split('-') doAssert(transformParts.len == 2) let basisParts = transformParts[0].split(',').mapIt( newFloatLit(parseFloat(it))) let originParts = transformParts[1].split(',').mapIt( newFloatLit(parseFloat(it))) let basis = newCall("initBasis", basisParts) let origin = newCall("vec3", originParts) result = newCall("initTransform", basis, origin) elif arg.typ == "Transform2D": let parts = arg.defaultVal.str. replace(" ", ""). replace(")", ""). replace("(", ""). split(',').mapIt(newFloatLit(parseFloat(it))) doAssert(parts.len == 6) let xAxis = newCall("vec2", parts[0..1]) let yAxis = newCall("vec2", parts[2..3]) let origin = newCall("vec2", parts[4..5]) result = newCall("initTransform2D", xAxis, yAxis, origin) elif arg.typ == "Variant": if arg.defaultVal.str == "0": result = newCall("newVariant", newIntLit(0)) elif arg.defaultVal.str == "Null": result = newCall("newVariant") else: failed = true elif arg.typ == "Dictionary": result = newCall("newDictionary") else: failed = true if failed: raise newException( ValueError, "Cannot build default value from \"$#\" ($#) - please report this at https://github.com/pragmagic/godot-nim/issues" % [$arg.defaultVal, arg.typ]) proc boundArgName(idx: int): string = "godotBoundArg" & $idx proc doGenerateMethod(tree: PNode, methodBindRegistry: var HashSet[string], meth: MethodInfo, withImplementation: bool) = var body: PNode if not withImplementation: body = newPNode(nkEmpty) elif meth.godotName == "_init" and meth.typ.godotName == "Object": # It's not real (calling it crashes) and will be removed later, # but we use init() convention in Nim, so it needs to work. body = newPNode(nkStmtList).addChain( newPNode(nkDiscardStmt).addChain(newPNode(nkEmpty))) else: let methGodotName = if meth.godotName.startsWith('_'): "underscore" & meth.godotName[1..^1] else: meth.godotName var methodBindName = toNimStyle( meth.typ.name & "_" & methGodotName & "_method_bind") if methodBindName[0].isUpperAscii(): methodBindName[0] = methodBindName[0].toLowerAscii() if methodBindName notin methodBindRegistry: let methodBindDecl = newPNode(nkVarSection).addChain( newIdentDefs( newPNode(nkPragmaExpr).addChain( ident(methodBindName)).addChain( newPNode(nkPragma).addChain(ident("threadvar"))), newPNode(nkPtrTy).addChain(ident("GodotMethodBind")) )) tree.add(methodBindDecl) methodBindRegistry.incl(methodBindName) let nilCheck = newNilCheck(ident(methodBindName), newCall("getMethod", newCStringLit(meth.typ.godotName), newCStringLit(meth.godotName)) ) let vars = newPNode(nkVarSection) var varargsName = none(string) var isVarargs: bool var staticArgsLen: int var argLenNode = newIntLit(0) let argsName = ident("argsToPassToGodot") if meth.args.len > 0: for idx, arg in meth.args: if arg.kind == ArgKind.VarArgs: varargsName = some(arg.name) isVarargs = true vars.add(newIdentDefs(ident("callError"), ident("VariantCallError"))) elif arg.kind == ArgKind.Bound: let argName = boundArgName(idx) vars.add(newIdentDefs( ident(argName), newPNode(nkEmpty), arg.makeDefaultValue())) let argsAlloc = newPNode(nkCast).addChain( newPNode(nkPtrTy).addChain( newPNode(nkBracketExpr).addChain( ident("array"), ident("MAX_ARG_COUNT"), if isVarargs: newPNode(nkPtrTy).addChain(ident("GodotVariant")) else: ident("pointer")))) staticArgsLen = if varargsName.isNone: meth.args.len else: meth.args.len - 1 if varargsName.isSome: argLenNode = newCall("cint", infix(newIntLit(staticArgsLen), ident("+"), newDotExpr(ident(varargsName.get), ident("len")))) argsAlloc.add(newCall("godotAlloc", newCall("cint", infix( newCall("sizeof", newPNode(nkPtrTy).addChain(ident("GodotVariant"))), ident("*"), newPNode(nkPar).addChain(argLenNode)))) ) else: argLenNode = newCall("cint", newIntLit(staticArgsLen)) vars.add(newIdentDefs( ident("argsStatic"), newPNode(nkBracketExpr).addChain( ident("array"), newIntLit(staticArgsLen), ident("pointer")), newPNode(nkEmpty))) argsAlloc.add(newCommand(ident("addr"), ident("argsStatic"))) vars.add(newIdentDefs(argsName, newPNode(nkEmpty), argsAlloc)) let argConversions = newPNode(nkStmtList) let destructors = newPNode(nkStmtList) for idx, arg in meth.args: var argName = if arg.kind == ArgKind.Bound: ident(boundArgName(idx)) else: ident(arg.name) if arg.kind == ArgKind.VarArgs: argName = newPNode(nkBracketExpr).addChain( ident(varargsName.get), infix(ident("idx"), ident("-"), newIntLit(staticArgsLen))) let argIdx = if arg.kind == ArgKind.VarArgs: ident("idx") else: newIntLit(idx) let isStandardType = arg.typ in standardTypes let isWrappedType = arg.typ in wrapperTypes let convArg = if varargsName.isSome: let isAlreadyVariant = arg.typ == "Variant" let variantIdent = if isAlreadyVariant: getInternalPtr(argName, "Variant") else: ident("variant" & $idx) if not isAlreadyVariant: argConversions.add(newPNode(nkVarSection).addChain( newIdentDefs(variantIdent, ident("GodotVariant"), newEmptyNode()) )) destructors.add( newCall("deinit", variantIdent) ) if arg.typ in variantPrimitives: argConversions.add( newCall("initGodotVariant", variantIdent, argName) ) elif arg.typ in wrapperTypes or arg.typ == "Object": argConversions.add( newCall("initGodotVariant", variantIdent, getInternalPtr(argName, arg.typ)) ) elif arg.typ == "string": let stringIdent = ident("variantString" & $idx) argConversions.addChain( newPNode(nkVarSection).addChain( newIdentDefs(stringIdent, ident("GodotString"), newCall("toGodotString", argName)) ) ).addChain( newCall("initGodotVariant", variantIdent, stringIdent) ) destructors.add(newCall("deinit", stringIdent)) else: raise newException(ValueError, "Non-standard type $# for varargs params of method $# of type $#" % [arg.typ, meth.godotName, meth.typ.godotName]) if isAlreadyVariant: variantIdent else: newCall("addr", variantIdent) elif isWrappedType: getInternalPtr(argName, arg.typ) elif isStandardType: newCall("unsafeAddr", argName) elif arg.typ == "string": newCall("unsafeAddr", ident("argToPassToGodot" & $idx)) else: ident("argToPassToGodot" & $idx) # object if not isStandardType and arg.kind == ArgKind.VarArgs: raise newException(ValueError, "Non-standard type $# for varargs params of method $# of type $#" % [arg.typ, meth.godotName, meth.typ.godotName]) if not isStandardType and varargsName.isNone: if arg.typ == "string": argConversions.add(newPNode(nkVarSection).addChain( newIdentDefs(ident("argToPassToGodot" & $idx), newEmptyNode(), newCall("toGodotString", argName)))) else: let objVal = newIfStmt( prefix("not", newDotExpr(argName, ident"isNil")), newDotExpr(argName, ident("godotObject")) ).addChain(newPNode(nkElse).addChain(newNilLit())) argConversions.add(newPNode(nkLetSection).addChain( newIdentDefs(ident("argToPassToGodot" & $idx), newEmptyNode(), objVal))) let argAsgn = newPNode(nkAsgn).addChain( newPNode(nkBracketExpr).addChain( newPNode(nkBracketExpr).addChain(argsName), argIdx), convArg ) if arg.kind != ArgKind.VarArgs: argConversions.add( argAsgn ) else: argConversions.add(newPNode(nkVarSection).addChain( newIdentDefs(ident("idx"), newPNode(nkEmpty), newIntLit(staticArgsLen)))) let argLoop = newPNode(nkWhileStmt) argConversions.add(argLoop) argLoop.add(infix(ident("idx"), ident("<"), argLenNode) ) argLoop.add(newPNode(nkStmtList).addChain( argAsgn, newCommand(ident("inc"), ident("idx")))) let retName = if isVarargs: "callRet" else: "ptrCallRet" let theCall = newCall( newDotExpr(ident(methodBindName), ident(if isVarargs: "call" else: "ptrCall")), newDotExpr(ident("self"), ident("godotObject")), if meth.args.len > 0: argsName else: newPNode(nkNilLit), argLenNode, if not isVarargs: ident(retName) else: ident("callError") ) if not isVarargs: theCall.sons.delete(3) let freeCall = newCall("godotFree", argsName) body = newPNode(nkStmtList) if meth.typ.isSingleton: let varName = singletonName(meth.typ.name) body.add(newNilCheck(varName, newCall( newBracketExpr(ident("getSingleton"), ident(meth.typ.name))))) body.add(newPNode(nkLetSection).addChain(newIdentDefs( ident("self"), newPNode(nkEmpty), varName ))) body.add(nilCheck) body.add(vars) body.add(argConversions) if not isVarargs: let retPtrDecl = newPNode(nkVarSection).addChain( newIdentDefs(ident(retName), ident("pointer"), newEmptyNode()) ) body.add(retPtrDecl) var isVariantRet: bool var isConversionRet: bool var isObjRet: bool var isStringRet: bool var isWrapperRet: bool let retValIdent = if not isVarargs: ident("ptrCallVal") else: ident(retName) if meth.returnType.isSome and not isVarargs: let returnType = meth.returnType.get var addrToAssign = newCall("addr", retValIdent) if returnType in smallIntTypes: isConversionRet = true body.add(newPNode(nkVarSection).addChain(newIdentDefs( retValIdent, ident("int64"), ))) elif returnType in float32Types: isConversionRet = true body.add(newPNode(nkVarSection).addChain(newIdentDefs( retValIdent, ident("float64"), ))) elif returnType in wrapperTypes: isWrapperRet = true body.add(newPNode(nkVarSection).addChain(newIdentDefs( retValIdent, ident("Godot" & returnType), ))) if returnType != "NodePath": body.add(newCall("initGodot" & returnType, retValIdent)) elif returnType in standardTypes: addrToAssign = newCall("addr", ident("result")) elif returnType == "string": isStringRet = true body.add(newPNode(nkVarSection).addChain(newIdentDefs( retValIdent, ident("GodotString"), ))) elif returnType != "void": isObjRet = true body.add(newPNode(nkVarSection).addChain(newIdentDefs( retValIdent, newPNode(nkPtrTy).addChain(ident("GodotObject")), ))) body.add(newPNode(nkAsgn).addChain(ident(retName), addrToAssign)) if not isVarargs: body.add(theCall) else: if meth.returnType.isSome: isVariantRet = true let callStmt = newPNode(nkLetSection).addChain( newIdentDefs( newPNode(nkPragmaExpr).addChain( ident(retName), newPNode(nkPragma).addChain(ident("used"))), newPNode(nkEmpty), theCall)) body.add(callStmt) if varargsName.isNone: for idx, arg in meth.args: if arg.typ == "string": body.add(newCall("deinit", ident("argToPassToGodot" & $idx))) if varargsName.isSome: if meth.args.len > 0: body.add(freeCall) body.add(destructors) let errCheck = newIfStmt( infix(newDotExpr(ident("callError"), ident("error")), ident("!="), newDotExpr(ident("VariantCallErrorType"), ident("OK"))), newPNode(nkRaiseStmt).addChain( newCall("newCallError", ident("callError"))) ) body.add(errCheck) if isVariantRet: let convErrDef = newPNode(nkLetSection).addChain( newIdentDefs(ident("convErr"), newPNode(nkEmpty), newCall("fromVariant", ident("result"), newCall("newVariant", retValIdent)))) let convCheck = newIfStmt( infix(ident("convErr"), ident("!="), newDotExpr(ident("ConversionResult"), ident("OK"))), newPNode(nkRaiseStmt).addChain( newCall("newConversionError", ident("convErr"))) ) body.add(convErrDef) body.add(convCheck) if isStringRet: body.add(newPNode(nkAsgn).addChain( ident("result"), prefix("$", retValIdent))) body.add(newCall("deinit", retValIdent)) elif isConversionRet: body.add(newPNode(nkAsgn).addChain( ident("result"), newCall(meth.returnType.get, retValIdent))) elif isWrapperRet: body.add(newPNode(nkAsgn).addChain( ident("result"), newCall("new" & meth.returnType.get, retValIdent))) elif isObjRet: body.add(newPNode(nkAsgn).addChain(ident("result"), newCall(newBracketExpr(ident("asNimGodotObject"), newCall("type", ident("result"))), retValIdent, ident("false"), ident("true")))) elif isStringRet: body.add(newPNode(nkAsgn).addChain(ident("result"), newCall("$", retValIdent))) let procType = if meth.isVirtual: nkMethodDef else: nkProcDef var params = newSeqOfCap[PNode](meth.args.len + 2) if meth.returnType.isSome: params.add(ident(meth.returnType.get)) else: params.add(newPNode(nkEmpty)) if not meth.typ.isSingleton: params.add(newIdentDefs(ident("self"), ident(toNimType(meth.typ.name)))) for arg in meth.args: case arg.kind: of ArgKind.Normal: let defaultValue = arg.makeDefaultValue() params.add(newIdentDefs(ident(arg.name), ident(arg.typ), defaultValue)) of ArgKind.VarArgs: params.add(newIdentDefs(ident(arg.name), newPNode(nkBracketExpr).addChain(ident("varargs"), ident(arg.typ)))) of ArgKind.Bound: discard let procDecl = newProc(if not withImplementation: postfix(meth.name, "*") else: meth.name, params, body, procType) if not withImplementation: let pragma = newPNode(nkPragma) pragma.add(ident("gcsafe")) pragma.add(newPNode(nkExprColonExpr).addChain(ident("locks"), newIntLit(0))) if meth.isVirtual and meth.isBase and meth.typ.name != "PhysicsBody": # Nim doesn't like `base` on PhysicsBody methods - wtf pragma.add(ident("base")) if meth.isDiscardable and meth.returnType.isSome: pragma.add(ident("discardable")) procDecl.sons[4] = pragma tree.add(procDecl) proc generateMethod(tree: PNode, methodBindRegistry: var HashSet[string], meth: MethodInfo, withImplementation: bool) = doGenerateMethod(tree, methodBindRegistry, meth, withImplementation) proc getMethodInfo(methodObj: JsonNode, types: Table[string, GodotType], typ: GodotType): MethodInfo = var origArgs = methodObj["arguments"] var isBase = methodObj["is_virtual"].bval if methodObj["is_virtual"].bval: proc getMethod(typNode: JsonNode, methName: string): JsonNode = for meth in typNode["methods"]: if meth["is_virtual"].bval and meth["name"].str == methName: return meth var curTyp = types.getOrDefault(typ.baseName) while not curTyp.isNil: let meth = curTyp.jsonNode.getMethod(methodObj["name"].str) if not meth.isNil: origArgs = meth["arguments"] isBase = false curTyp = types.getOrDefault(curTyp.baseName) var args = newSeqOfCap[MethodArg](methodObj["arguments"].len + 1) for arg in origArgs: let typ = toNimType(types, arg["type"].str) let defaultVal = if arg["has_default_value"].bval: arg["default_value"] else: nil args.add(MethodArg( name: toNimStyle(arg["name"].str), typ: typ, defaultVal: defaultVal)) if methodObj["has_varargs"].bval: args.add(MethodArg(name: "variantArgs", typ: "Variant", kind: ArgKind.VarArgs)) let godotName = methodObj["name"].str var nimName = toNimStyle(godotName) if not godotName.startsWith('_'): for meth in typ.jsonNode["methods"]: if meth["name"].str == '_' & godotName: nimName = nimName & "Impl" let returnType = if methodObj["return_type"].str != "void": some(toNimType(types, methodObj["return_type"].str)) else: none(string) const discardableMethods = toHashSet(["emit_signal"]) result = MethodInfo( name: ident(nimName), typ: typ, godotName: methodObj["name"].str, args: args, returnType: returnType, isVirtual: methodObj["is_virtual"].bval, isBase: isBase, isDiscardable: typ.godotName == "Object" and methodObj["name"].str in discardableMethods ) proc findMethod(obj: JsonNode, methName: string): JsonNode = for meth in obj["methods"]: if meth["name"].str == methName: return meth proc makeProperty(types: Table[string, GodotType], tree: PNode, methodBindRegistry: var HashSet[string], typ: GodotType, propertyObj: JsonNode, obj: JsonNode, withImplementation: bool) = for meth in obj["methods"]: if meth["name"].str == '_' & propertyObj["name"].str: # Don't generate properties if there is a name clash - otherwise # using the property would not compile due to ambiguity. return let getterNameStr = toNimStyle(propertyObj["name"].str) let getterName = ident(getterNameStr) let setterName = newPNode(nkAccQuoted).addChain(ident(getterNameStr & "=")) let getter = findMethod(obj, propertyObj["getter"].str) let nimType = if getter.isNil: toNimType(types, propertyObj["type"].str) else: getMethodInfo(getter, types, typ).returnType.get("void") let getterInfo = MethodInfo( name: getterName, typ: typ, godotName: propertyObj["getter"].str, args: newSeq[MethodArg](), returnType: some(nimType) ) let setterInfo = MethodInfo( name: setterName, typ: typ, godotName: propertyObj["setter"].str, args: @[MethodArg(name: "val", typ: nimType)] ) if "index" in propertyObj and propertyObj["index"] != newJInt(-1): getterInfo.args = @[MethodArg(defaultVal: newJString($propertyObj["index"]), typ: "int", kind: ArgKind.Bound)] & getterInfo.args setterInfo.args = @[MethodArg(defaultVal: newJString($propertyObj["index"]), typ: "int", kind: ArgKind.Bound)] & setterInfo.args generateMethod(tree, methodBindRegistry, getterInfo, withImplementation) generateMethod(tree, methodBindRegistry, setterInfo, withImplementation) proc makeMethod(types: Table[string, GodotType], tree: PNode, methodBindRegistry: var HashSet[string], typ: GodotType, methodObj: JsonNode, withImplementation: bool) = if not typ.isSingleton: # for singletons we don't generate Nim setters/getters, # but use plain procedures instead. That's because setter syntax is # supposed to be used on an object (have at least 2 params). for prop in typ.jsonNode["properties"]: if prop["getter"].str == methodObj["name"].str or prop["setter"].str == methodObj["name"].str: var hasDuplicateMethod = false for meth in typ.jsonNode["methods"]: if meth["name"].str == '_' & prop["name"].str: hasDuplicateMethod = true break if not hasDuplicateMethod: return let methodInfo = getMethodInfo(methodObj, types, typ) generateMethod(tree, methodBindRegistry, methodInfo, withImplementation) proc addAttributeToDoc(doc: var string, propName, propValue: string) = if doc.len > 0: doc.add("\n") doc.add(propName & ": " & propValue) const classAttributes = ["singleton", "instanciable", "is_reference"] proc typeNameToModuleName(name: string): string = var wasUpperOrDigit = true result = newStringOfCap(name.len + 10) for idx, c in name: if c.isUpperAscii() or c.isDigit(): if not wasUpperOrDigit or idx > 0 and idx < name.len - 1 and name[idx + 1].isLowerAscii(): result.add('_') result.add(c.toLowerAscii()) wasUpperOrDigit = true else: result.add(c) wasUpperOrDigit = false if result == "object": # keyword result = "objects" if result == "os": # to avoid clash with stdlib result = "gd_os" proc newRegisterClassNode(typ: GodotType): PNode = newCall("registerClass", ident(typ.name), newStrLit(typ.godotName), ident("true") # isNative ) proc genSingletonDecl(typ: string): PNode = result = newPNode(nkVarSection) let name = singletonName(typ) let threadvar = newPNode(nkPragma).addChain(ident("threadvar")) result.add(newIdentDefs(newPNode(nkPragmaExpr).addChain(name, threadvar), ident(typ))) proc shouldExport(typ: GodotType, types: Table[string, GodotType]): bool = var curTyp = typ while not curTyp.isNil and not curTyp.isSingleton: curTyp = types.getOrDefault(curTyp.baseName) result = curTyp.isNil proc sortByDerivedCount(types: Table[string, GodotType]): seq[GodotType] = result = toSeq(types.values()) result.sort do (x, y: GodotType) -> int: cmp(y.derivedCount, x.derivedCount) proc genTypeFile(types: Table[string, GodotType], targetDir: string) = let sortedTypes = sortByDerivedCount(types) let godotApiTypesTree = newPNode(nkStmtList) godotApiTypesTree.add(newPNode(nkImportStmt).addChain(ident("godot"))) let typeSection = newPNode(nkTypeSection) godotApiTypesTree.add(typeSection) for typ in sortedTypes: if not typ.shouldExport(types): continue newRefTypeNode(typeSection, typ.name, typ.baseName, typ.doc) for typ in sortedTypes: if not typ.shouldExport(types): continue let regNode = newCall("registerClass", ident(typ.name), newStrLit(typ.godotName), ident("true") # isNative ) godotApiTypesTree.add(regNode) writeFile(targetDir / "godottypes.nim", renderTree(godotApiTypesTree)) proc genSingletonWithDerived(tree: PNode, typ: GodotType, types: Table[string, GodotType]) = let sortedTypes = sortByDerivedCount(types) var parents = initHashSet[string]() parents.incl(typ.name) let typeDecl = newPNode(nkTypeSection) tree.add(typeDecl) proc genDecl(typ: GodotType) = newRefTypeNode(typeDecl, typ.name, typ.baseName, typ.doc, isExported = false) tree.add(newRegisterClassNode(typ)) genDecl(typ) for otherType in sortedTypes: if otherType.baseName in parents: parents.incl(otherType.name) genDecl(otherType) tree.add(genSingletonDecl(typ.name)) proc genApi*(targetDir: string, apiJsonFile: string) = ## Generates .nim files in the ``targetDir`` based on the ``apiJsonFile``. ## The JSON file can be generated by executing Godot with ## ``--gdnative-generate-json-api `` parameters. let apiJson = parseJson(newFileStream(apiJsonFile), apiJsonFile.extractFilename()) var megaImport = newStringOfCap(64 * 1024) var types = initTable[string, GodotType]() for obj in apiJson: let godotClassName = obj["name"].str let className = godotClassName.replace("_", "") var baseClassName = obj["base_class"].str baseClassName = toNimType(baseClassName.replace("_", "")) if baseClassName.len == 0: baseClassName = "NimGodotObject" var doc = "" for attr in classAttributes: if attr in obj: addAttributeToDoc(doc, attr, $obj[attr]) types[className] = GodotType( name: className, godotName: godotClassName, baseName: baseClassName, doc: doc, jsonNode: obj, isSingleton: obj["singleton"].bval ) for typ in types.values(): types.incDerivedCount(typ.baseName) for typ in types.values(): let moduleName = typeNameToModuleName(typ.name) echo "Generating ", moduleName, ".nim..." let tree = newPNode(nkStmtList) let importStmt = newPNode(nkImportStmt) importStmt.add(ident("godot")) importStmt.add(ident("godottypes")) importStmt.add(ident("godotinternal")) tree.add(importStmt) let exportStmt = newPNode(nkExportStmt) exportStmt.add(ident("godottypes")) tree.add(exportStmt) var methodBindRegsitry = initHashSet[string]() let obj = typ.jsonNode if typ.baseName != "NimGodotObject": let baseModule = typeNameToModuleName(typ.baseName) let baseModuleNode = if getIdent(baseModule).isKeyword: newStrLit(baseModule & ".nim") else: ident(baseModule) importStmt.add(baseModuleNode) exportStmt.add(baseModuleNode) let constSection = makeConstSection(obj["constants"]) if constSection.kind != nkEmpty: # renderer doesn't properly render empty stmt after type section tree.add(makeConstSection(obj["constants"])) if typ.isSingleton: genSingletonWithDerived(tree, typ, types) if "enums" in obj: # Quite often Godot enums are flags that need to be bitwise ORed # Nim's enums don't serve well for that purpose, so we just generate # integer constants. The only enum used in API is Error. for enumDef in obj["enums"]: let uniqueConstants = newJObject() for k, v in enumDef["values"]: if k notin obj["constants"]: uniqueConstants[k] = v if uniqueConstants.len > 0: tree.add(makeConstSection(uniqueConstants)) # first, generate declarations only for ease of # human readability of the file if not typ.isSingleton: for property in obj["properties"]: makeProperty(types, tree, methodBindRegsitry, typ, property, obj, withImplementation = false) for meth in obj["methods"]: makeMethod(types, tree, methodBindRegsitry, typ, meth, withImplementation = false) if not typ.isSingleton: for property in obj["properties"]: makeProperty(types, tree, methodBindRegsitry, typ, property, obj, withImplementation = true) for meth in obj["methods"]: makeMethod(types, tree, methodBindRegsitry, typ, meth, withImplementation = true) writeFile(targetDir / moduleName & ".nim", renderTree(tree)) megaImport.add("import \"" & moduleName & "\"\n") genTypeFile(types, targetDir) writeFile(targetDir / "godotall.nim", megaImport) writeFile(targetDir / splitPath(targetDir)[1] & ".nimble", "") when isMainModule: import os const usage = """Tool for wrapping Godot API into Nim godotapigen [godot binary path] [target directory]""" proc main() = if paramCount() < 2: echo usage quit(-1) let godotBin = paramStr(1) let targetDir = paramStr(2) if not dirExists(targetDir): echo "The specified directory does not exist" quit(-1) if not fileExists(godotBin): echo "Invalid godot binary path" quit(-1) let jsonFile = targetDir / "api.json" if fileExists(jsonFile) and godotBin.getLastModificationTime() <= jsonFile.getLastModificationTime(): echo "api.json is up to date - doing nothing." return discard execShellCmd( "\"" & godotBin & "\" --gdnative-generate-json-api \"" & jsonFile & "\"") if not fileExists(jsonFile): echo "Failed to generate api.json" quit(-1) genApi(targetDir, jsonFile) main()