diff --git a/.github/workflows/c.yml b/.github/workflows/c.yml index bfcbfb1..17506c5 100644 --- a/.github/workflows/c.yml +++ b/.github/workflows/c.yml @@ -32,3 +32,5 @@ jobs: run: gcc -o tests/test_pixie_c tests/test_pixie_c.c -I ../pixie/bindings/generated -L ../pixie/bindings/generated -l:libpixie.so -lm -Wl,-rpath,'$ORIGIN/../../pixie/bindings/generated' - name: Run Pixie C test run: ./tests/test_pixie_c + - name: Compare Pixie render goldens + run: nim r --mm:arc --path:../pixie/src tests/test_no_gold_diff.nim diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml index db82812..f8214f9 100644 --- a/.github/workflows/cpp.yml +++ b/.github/workflows/cpp.yml @@ -32,3 +32,5 @@ jobs: run: g++ -o tests/test_pixie_cpp tests/test_pixie_cpp.cpp -I ../pixie/bindings/generated -L ../pixie/bindings/generated -l:libpixie.so -Wl,-rpath,'$ORIGIN/../../pixie/bindings/generated' - name: Run Pixie C++ test run: ./tests/test_pixie_cpp + - name: Compare Pixie render goldens + run: nim r --mm:arc --path:../pixie/src tests/test_no_gold_diff.nim diff --git a/.github/workflows/nim.yml b/.github/workflows/nim.yml index dabae0d..98804c0 100644 --- a/.github/workflows/nim.yml +++ b/.github/workflows/nim.yml @@ -34,3 +34,5 @@ jobs: run: | nim c --mm:arc -o:tests/test_pixie_nim tests/test_pixie_nim.nim PIXIE_ROOT=../pixie LD_LIBRARY_PATH=$PWD/../pixie/bindings/generated ./tests/test_pixie_nim + - name: Compare Pixie render goldens + run: nim r --mm:arc --path:../pixie/src tests/test_no_gold_diff.nim diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index e7eaefb..8408f0b 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -37,3 +37,5 @@ jobs: run: cd ../pixie && nim c --mm:arc --app:lib -d:gennyNode --path:../genny/src --path:src -o:bindings/generated/libpixie.so bindings/bindings.nim - name: Run Pixie Node tests run: NODE_PATH=$PWD/tests/node_modules PIXIE_ROOT=../pixie PIXIE_BINDINGS_DIR=../pixie/bindings/generated LD_LIBRARY_PATH=$PWD/../pixie/bindings/generated node tests/test_pixie_node.js + - name: Compare Pixie render goldens + run: nim r --mm:arc --path:../pixie/src tests/test_no_gold_diff.nim diff --git a/.github/workflows/python-native.yml b/.github/workflows/python-native.yml index 154403a..2dc32bd 100644 --- a/.github/workflows/python-native.yml +++ b/.github/workflows/python-native.yml @@ -39,3 +39,5 @@ jobs: - name: Run non-gating benchmark continue-on-error: true run: python tests/bench_python_native.py + - name: Compare Pixie render goldens + run: nim r --mm:arc --path:../pixie/src tests/test_no_gold_diff.nim diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index b5cd84b..e0c44b1 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -34,3 +34,5 @@ jobs: - name: Run non-gating benchmark continue-on-error: true run: python tests/bench_python.py + - name: Compare Pixie render goldens + run: nim r --mm:arc --path:../pixie/src tests/test_no_gold_diff.nim diff --git a/.github/workflows/zig.yml b/.github/workflows/zig.yml index 8e1fac3..e5e342d 100644 --- a/.github/workflows/zig.yml +++ b/.github/workflows/zig.yml @@ -45,3 +45,5 @@ jobs: cp ../../pixie/bindings/generated/pixie.zig generated/pixie.zig zig build-exe test_pixie_zig.zig -lc -L../../pixie/bindings/generated -lpixie LD_LIBRARY_PATH=$PWD/../../pixie/bindings/generated ./test_pixie_zig + - name: Compare Pixie render goldens + run: nim r --mm:arc --path:../pixie/src tests/test_no_gold_diff.nim diff --git a/.gitignore b/.gitignore index 55b8f35..9194bea 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ nimcache __pycache__/ tests/generated/*_native.c +tests/generated/pixie_images/*.png *.pdb *.ilk .* diff --git a/src/genny.nim b/src/genny.nim index 63c0649..5184852 100644 --- a/src/genny.nim +++ b/src/genny.nim @@ -1,4 +1,4 @@ -import genny/internal, macros, strformat +import genny/[common, internal], macros, strformat, strutils when defined(gennyC): import genny/languages/c when defined(gennyCpp): import genny/languages/cpp @@ -86,6 +86,22 @@ proc fieldUntyped(clause, owner: NimNode): NimNode = obj: `owner` f = obj.`clause` +proc objectFieldUntyped(clause: NimNode): NimNode = + result = emptyBlockStmt() + if clause.kind notin {nnkExprColonExpr, nnkCall}: + error("Object fields need explicit types, for example `x: float32`", clause) + let fieldName = clause[0] + let fieldType = + if clause.kind == nnkExprColonExpr: + clause[1] + elif clause.len == 2 and clause[1].kind == nnkStmtList and clause[1].len == 1: + clause[1][0] + else: + error("Object fields need explicit types, for example `x: float32`", clause) + let fieldVar = ident("gennyField_" & fieldName.repr) + result[1].add quote do: + var `fieldVar`: `fieldType` + proc procUntyped(clause: NimNode): NimNode = result = emptyBlockStmt() @@ -119,6 +135,25 @@ proc procTypedSym(entry: NimNode): NimNode = else: entry[1][^1][0][0] +proc objectFieldsTyped(fieldsBlock: NimNode): seq[ObjectField] = + if fieldsBlock[1].len == 0: + return + + for entry in fieldsBlock[1].asStmtList: + var fieldEntry = entry + if fieldEntry.kind == nnkBlockStmt: + fieldEntry = fieldEntry[1] + if fieldEntry.kind == nnkStmtList and fieldEntry.len == 1: + fieldEntry = fieldEntry[0] + if fieldEntry.kind != nnkVarSection: + error("Invalid generated object field entry", fieldEntry) + let identDefs = fieldEntry[0] + for i in 0 .. identDefs.len - 3: + let fieldSym = identDefs[i] + var fieldName = fieldSym.repr.split("`")[0] + fieldName.removePrefix("gennyField_") + result.add((fieldName, fieldSym.getTypeInst())) + proc procTyped( entry: NimNode, owner: NimNode = nil, @@ -156,6 +191,7 @@ macro exportObjectUntyped(sym, body: untyped) = result.add varSection var + fieldsBlock = emptyBlockStmt() constructorBlock = emptyBlockStmt() procsBlock = emptyBlockStmt() @@ -164,6 +200,9 @@ macro exportObjectUntyped(sym, body: untyped) = continue case section[0].repr: + of "fields": + for field in section[1]: + fieldsBlock[1].add objectFieldUntyped(field) of "constructor": constructorBlock[1].add procUntyped(section[1][0]) of "procs": @@ -172,14 +211,23 @@ macro exportObjectUntyped(sym, body: untyped) = else: error("Invalid section", section) + result.add fieldsBlock result.add constructorBlock result.add procsBlock macro exportObjectTyped(body: typed) = let - sym = body[0][0][1] - constructorBlock = body[1] - procsBlock = body[2] + sym = body[0][0][0].getTypeInst() + typeExpr = body[0][0][1] + fieldsBlock = body[1] + constructorBlock = body[2] + procsBlock = body[3] + fields = objectFieldsTyped(fieldsBlock) + nimModule = + if typeExpr.kind == nnkDotExpr: + typeExpr[0].repr + else: + "" let constructor = if constructorBlock[1].len > 0: @@ -187,14 +235,20 @@ macro exportObjectTyped(body: typed) = else: nil - exportObjectInternal(sym, constructor) - when defined(gennyNim): exportObjectNim(sym, constructor) - when defined(gennyPython): exportObjectPy(sym, constructor) - when defined(gennyPythonNative): exportObjectPyNative(sym, constructor) - when defined(gennyNode): exportObjectNode(sym, constructor) - when defined(gennyC): exportObjectC(sym, constructor) - when defined(gennyCpp): exportObjectCpp(sym, constructor) - when defined(gennyZig): exportObjectZig(sym, constructor) + registerValueObjectType(sym) + if constructor != nil: + let constructorType = constructor.getTypeInst() + if constructorType[0][0].kind != nnkEmpty: + registerValueObjectTypeAlias(sym.repr, constructorType[0][0]) + exportObjectInternal(sym, fields, constructor, nimModule.len > 0) + when defined(gennyNim): + exportObjectNim(sym, fields, constructor, nimModule) + when defined(gennyPython): exportObjectPy(sym, fields, constructor) + when defined(gennyPythonNative): exportObjectPyNative(sym, fields, constructor) + when defined(gennyNode): exportObjectNode(sym, fields, constructor) + when defined(gennyC): exportObjectC(sym, fields, constructor) + when defined(gennyCpp): exportObjectCpp(sym, fields, constructor) + when defined(gennyZig): exportObjectZig(sym, fields, constructor) if procsBlock[1].len > 0: var procsSeen: seq[string] @@ -202,10 +256,12 @@ macro exportObjectTyped(body: typed) = var procSym = procTypedSym(entry) prefixes: seq[NimNode] + let procType = procSym.getTypeInst() + if procType[0].len > 1: + registerValueObjectTypeAlias(sym.repr, procType[0][1][^2]) if procSym.repr notin procsSeen: procsSeen.add procSym.repr else: - let procType = procSym.getTypeInst() if procType[0].len > 2: prefixes.add(procType[0][2][1]) exportProcInternal(procSym, sym, prefixes) @@ -217,6 +273,7 @@ macro exportObjectTyped(body: typed) = when defined(gennyCpp): exportProcCpp(procSym, sym, prefixes) when defined(gennyZig): exportProcZig(procSym, sym, prefixes) + when defined(gennyPython): exportCloseObjectPy(sym) when defined(gennyZig): exportCloseObjectZig() when defined(gennyCpp): exportCloseObjectCpp() diff --git a/src/genny/common.nim b/src/genny/common.nim index 3ce4583..bcf4fc0 100644 --- a/src/genny/common.nim +++ b/src/genny/common.nim @@ -1,5 +1,11 @@ import macros, strformat, strutils +type ObjectField* = tuple[name: string, typ: NimNode] + +var + exportedValueTypes {.compiletime.}: seq[(string, NimNode)] + exportedValueTypeAliases {.compiletime.}: seq[(string, string)] + const basicTypes* = [ "bool", "int8", @@ -68,6 +74,48 @@ proc toVarCase*(s: string): string = if i < s.len: result.add s[i .. ^1] +proc stripSinkCommon(sym: NimNode): NimNode = + if sym.kind == nnkBracketExpr and sym[0].repr == "sink": + sym[1] + else: + sym + +proc normalizeTypeRepr(s: string): string = + s.replace("system.", "") + +proc registerValueObjectTypeAliasRepr*(name, alias: string) = + let normalizedAlias = alias.normalizeTypeRepr() + for (existingAlias, existingName) in exportedValueTypeAliases: + if existingAlias == normalizedAlias and existingName == name: + return + exportedValueTypeAliases.add((normalizedAlias, name)) + +proc registerValueObjectTypeAlias*(name: string, typ: NimNode) = + registerValueObjectTypeAliasRepr(name, typ.repr) + +proc registerValueObjectType*(sym: NimNode) = + let name = sym.repr + for (existingName, _) in exportedValueTypes: + if existingName == name: + return + exportedValueTypes.add((name, sym)) + var aliases = @[sym.repr, sym.getTypeInst().repr, sym.getType().repr] + let impl = sym.getImpl() + if impl.kind == nnkTypeDef: + aliases.add(impl[2].repr) + for alias in aliases: + registerValueObjectTypeAliasRepr(name, alias) + +proc exportedValueTypeName*(sym: NimNode): string = + let typ = sym.stripSinkCommon() + let typRepr = typ.repr.normalizeTypeRepr() + for (alias, name) in exportedValueTypeAliases: + if typRepr == alias: + return name + for (name, exportedType) in exportedValueTypes: + if typ.sameType(exportedType) or typ.sameType(exportedType.getTypeInst()): + return name + proc getSeqName*(sym: NimNode): string = if sym.kind == nnkBracketExpr: result = &"Seq{sym[1]}" @@ -76,11 +124,87 @@ proc getSeqName*(sym: NimNode): string = result[3] = toUpperAscii(result[3]) proc getName*(sym: NimNode): string = - if sym.kind == nnkBracketExpr: + let valueName = sym.exportedValueTypeName() + if valueName.len > 0: + valueName + elif sym.kind == nnkBracketExpr: sym.getSeqName() else: sym.repr +proc getParamName*(sym: NimNode): string = + sym.repr.split("`")[0] + +proc usePrefixName*(sym: NimNode): bool = + if sym.kind != nnkSym: + return true + let impl = sym.getImpl() + impl.kind != nnkNilLit and impl[2].kind != nnkEnumTy + +proc arrayCount*(sym: NimNode): int = + let bounds = sym[1].repr + if ".." in bounds: + let parts = bounds.split("..") + parseInt(parts[1].strip()) - parseInt(parts[0].strip()) + 1 + else: + parseInt(bounds) + +proc normalizedOperatorName*(name: string): string = + result = name + result.removePrefix("`") + result.removeSuffix("`") + +proc isOperatorName*(name: string): bool = + name.normalizedOperatorName() in ["+", "-", "*", "/"] + +proc operatorProcName*(name: string): string = + case name.normalizedOperatorName() + of "+": "add" + of "-": "sub" + of "*": "mul" + of "/": "div" + else: name + +proc nimCallableName*(name: string): string = + let normalized = name.normalizedOperatorName() + if normalized.isOperatorName: + &"`{normalized}`" + else: + name + +proc cppOperatorName*(name: string): string = + "operator" & name.normalizedOperatorName() + +proc pythonOperatorName*(name: string): string = + case name.normalizedOperatorName() + of "+": "__add__" + of "-": "__sub__" + of "*": "__mul__" + of "/": "__truediv__" + else: name + +proc objectFieldName*(property: NimNode): string = + if property.kind == nnkPostfix: + property[1].repr + else: + property.repr + +proc objectFields*(sym: NimNode, explicitFields: seq[ObjectField]): seq[ObjectField] = + if explicitFields.len > 0: + return explicitFields + + let typ = sym.getType() + if typ.len > 2: + for fieldSym in typ[2]: + result.add((fieldSym.repr, fieldSym.getTypeInst())) + + if result.len == 0: + let impl = sym.getImpl() + if impl.kind == nnkTypeDef and impl[2].kind == nnkObjectTy: + for identDefs in impl[2][2]: + for property in identDefs[0 .. ^3]: + result.add((property.objectFieldName(), identDefs[^2])) + proc raises*(procSym: NimNode): bool = for pragma in procSym.getImpl()[4]: if pragma.kind == nnkExprColonExpr and pragma[0].repr == "raises": diff --git a/src/genny/internal.nim b/src/genny/internal.nim index 0247edc..4c21307 100644 --- a/src/genny/internal.nim +++ b/src/genny/internal.nim @@ -6,6 +6,11 @@ const exportProcPragmas = "{.raises: [], cdecl, exportc, dynlib.}" var internal {.compiletime.}: string +proc isSeqLike(sym: NimNode): bool = + sym.exportedValueTypeName().len == 0 and + sym.kind == nnkBracketExpr and + sym[0].repr in ["seq", "openArray"] + proc exportConstInternal*(sym: NimNode) = discard @@ -20,12 +25,12 @@ proc exportProcInternal*( ) = let procName = sym.repr - procNameSnaked = toSnakeCase(procName) + procNameSnaked = toSnakeCase(procName.operatorProcName()) procType = sym.getTypeInst() procParams = procType[0][1 .. ^1] procReturn = procType[0][0] procRaises = sym.raises() - procReturnsSeq = procReturn.kind == nnkBracketExpr + procReturnsSeq = procReturn.isSeqLike() shouldGcRefResult = gcRefResult or procReturnsSeq or ( procReturn.kind != nnkEmpty and procReturn.isRefObjectLike ) @@ -43,7 +48,7 @@ proc exportProcInternal*( var paramType = param[^2] if paramType.repr.endsWith(":type"): paramType = prefixes[0] - internal.add &"{toSnakeCase(param[i].repr)}: {exportTypeNim(paramType)}, " + internal.add &"{toSnakeCase(param[i].getParamName())}: {exportTypeNim(paramType)}, " internal.removeSuffix ", " internal.add ")" if procReturn.kind != nnkEmpty: @@ -57,10 +62,10 @@ proc exportProcInternal*( internal.add " result = " else: internal.add " " - var callExpr = &"{procName}(" + var callExpr = &"{procName.nimCallableName()}(" for param in procParams: for i in 0 .. param.len - 3: - callExpr.add convertImportExprNim(toSnakeCase(param[i].repr), param[^2]) + callExpr.add convertImportExprNim(toSnakeCase(param[i].getParamName()), param[^2]) callExpr.add ", " callExpr.removeSuffix ", " callExpr.add ")" @@ -81,10 +86,16 @@ proc exportProcInternal*( internal.add "\n" internal.add "\n" -proc exportObjectInternal*(sym: NimNode, constructor: NimNode) = +proc exportObjectInternal*( + sym: NimNode, + fields: seq[ObjectField], + constructor: NimNode, + external = false +) = let objName = sym.repr objNameSnaked = toSnakeCase(objName) + objFields = sym.objectFields(fields) if constructor != nil: let constructorType = constructor.getTypeInst() @@ -104,31 +115,30 @@ proc exportObjectInternal*(sym: NimNode, constructor: NimNode) = internal.add "\n" else: internal.add &"proc $lib_{objNameSnaked}*(" - let objType = sym.getType() - for fieldSym in objType[2]: + for field in objFields: let - fieldName = fieldSym.repr - fieldType = fieldSym.getTypeInst() + fieldName = field.name + fieldType = field.typ internal.add &"{toSnakeCase(fieldName)}: {exportTypeNim(fieldType)}, " internal.removeSuffix ", " internal.add &"): {objName} {exportProcPragmas} =\n" - for fieldSym in objType[2]: + for field in objFields: let - fieldName = fieldSym.repr - fieldType = fieldSym.getTypeInst() + fieldName = field.name + fieldType = field.typ internal.add &" result.{toSnakeCase(fieldName)} = " internal.add convertImportExprNim(toSnakeCase(fieldName), fieldType) internal.add "\n" internal.add "\n" internal.add &"proc $lib_{objNameSnaked}_eq*(a, b: {objName}): bool {exportProcPragmas}=\n" - let objType = sym.getType() - internal.add " " - for fieldSym in objType[2]: - let - fieldName = fieldSym.repr - internal.add &"a.{toSnakeCase(fieldName)} == b.{toSnakeCase(fieldName)} and " - internal.removeSuffix " and " + if external: + internal.add " a == b" + else: + internal.add " " + for field in objFields: + internal.add &"a.{toSnakeCase(field.name)} == b.{toSnakeCase(field.name)} and " + internal.removeSuffix " and " internal.add "\n\n" proc exportRefObjectInternal*( diff --git a/src/genny/languages/c.nim b/src/genny/languages/c.nim index 5f28f5c..6d1e325 100644 --- a/src/genny/languages/c.nim +++ b/src/genny/languages/c.nim @@ -18,10 +18,13 @@ proc isSeqLike(sym: NimNode): bool = proc exportTypeC(sym: NimNode): string = let typ = sym.stripSink + let valueName = typ.exportedValueTypeName() + if valueName.len > 0: + return valueName if typ.kind == nnkBracketExpr: if typ[0].repr == "array": let - entryCount = typ[1].repr + entryCount = $typ.arrayCount() entryType = exportTypeC(typ[2]) result = &"{entryType}[{entryCount}]" elif typ.isSeqLike: @@ -48,8 +51,6 @@ proc exportTypeC(sym: NimNode): string = of "float64": "double" of "float": "double" of "Rune": "int32_t" - of "Vec2": "Vector2" - of "Mat3": "Matrix3" of "", "nil": "void" of "None": "void" else: @@ -64,10 +65,13 @@ proc exportReturnTypeC(sym: NimNode): string = proc exportTypeC(sym: NimNode, name: string): string = let typ = sym.stripSink + let valueName = typ.exportedValueTypeName() + if valueName.len > 0: + return valueName & " " & name if typ.kind == nnkBracketExpr: if typ[0].repr == "array": let - entryCount = typ[1].repr + entryCount = $typ.arrayCount() entryType = exportTypeC(typ[2], &"{name}[{entryCount}]") result = &"{entryType}" elif typ.isSeqLike: @@ -88,7 +92,7 @@ proc dllProc*(procName: string, args: openarray[string], restype: string) = proc dllProc*(procName: string, args: openarray[(NimNode, NimNode)], restype: string) = var argsConverted: seq[string] for (argName, argType) in args: - argsConverted.add exportTypeC(argType, toSnakeCase(argName.getName())) + argsConverted.add exportTypeC(argType, toSnakeCase(argName.getParamName())) dllProc(procName, argsConverted, restype) proc dllProc*(procName: string, restype: string) = @@ -112,7 +116,7 @@ proc exportProcC*( ) = let procName = sym.repr - procNameSnaked = toSnakeCase(procName) + procNameSnaked = toSnakeCase(procName.operatorProcName()) procType = sym.getTypeInst() procParams = procType[0][1 .. ^1] procReturn = procType[0][0] @@ -151,22 +155,26 @@ proc exportProcC*( dllParams.add((param[0], param[1])) dllProc(&"$lib_{apiProcName}", dllParams, exportReturnTypeC(procReturn)) -proc exportObjectC*(sym: NimNode, constructor: NimNode) = - let objName = sym.repr +proc exportObjectC*( + sym: NimNode, + fields: seq[ObjectField], + constructor: NimNode +) = + let + objName = sym.repr + objFields = sym.objectFields(fields) types.add &"typedef struct {objName} " & "{\n" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - types.add &" {exportTypeC(identDefs[^2], toSnakeCase(property[1].repr))};\n" + for field in objFields: + types.add &" {exportTypeC(field.typ, toSnakeCase(field.name))};\n" types.add "} " & &"{objName};\n\n" if constructor != nil: exportProcC(constructor) else: procs.add &"{objName} $lib_{toSnakeCase(objName)}(" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - procs.add &"{exportTypeC(identDefs[^2], toSnakeCase(property[1].repr))}, " + for field in objFields: + procs.add &"{exportTypeC(field.typ, toSnakeCase(field.name))}, " procs.removeSuffix ", " procs.add ");\n\n" diff --git a/src/genny/languages/cpp.nim b/src/genny/languages/cpp.nim index 70011f0..87fc4a6 100644 --- a/src/genny/languages/cpp.nim +++ b/src/genny/languages/cpp.nim @@ -29,10 +29,13 @@ proc isStringType(sym: NimNode): bool = proc exportTypeCpp(sym: NimNode, abi = false): string = let typ = sym.stripSink + let valueName = typ.exportedValueTypeName() + if valueName.len > 0: + return valueName if typ.kind == nnkBracketExpr: if typ[0].repr == "array": let - entryCount = typ[1].repr + entryCount = $typ.arrayCount() entryType = exportTypeCpp(typ[2], abi) result = &"{entryType}[{entryCount}]" elif typ.isSeqLike: @@ -60,8 +63,6 @@ proc exportTypeCpp(sym: NimNode, abi = false): string = of "float": "double" of "Rune": if abi: "std::int32_t" else: "char32_t" - of "Vec2": "Vector2" - of "Mat3": "Matrix3" of "", "nil": "void" of "None": "void" else: @@ -87,10 +88,13 @@ proc exportReturnTypeCppAbi(sym: NimNode): string = proc exportTypeCpp(sym: NimNode, name: string, abi = false): string = let typ = sym.stripSink + let valueName = typ.exportedValueTypeName() + if valueName.len > 0: + return valueName & " " & name if typ.kind == nnkBracketExpr: if typ[0].repr == "array": let - entryCount = typ[1].repr + entryCount = $typ.arrayCount() entryType = exportTypeCpp(typ[2], &"{name}[{entryCount}]", abi) result = &"{entryType}" elif typ.isSeqLike: @@ -128,7 +132,7 @@ proc dllProc*(procName: string, args: openarray[string], restype: string) = proc dllProc*(procName: string, args: openarray[(NimNode, NimNode)], restype: string) = var argsConverted: seq[string] for (argName, argType) in args: - argsConverted.add exportTypeCppAbi(argType, toSnakeCase(argName.getName())) + argsConverted.add exportTypeCppAbi(argType, toSnakeCase(argName.getParamName())) dllProc(procName, argsConverted, restype) proc dllProc*(procName: string, restype: string) = @@ -152,7 +156,7 @@ proc exportProcCpp*( ) = let procName = sym.repr - procNameSnaked = toSnakeCase(procName) + procNameSnaked = toSnakeCase(procName.operatorProcName()) procType = sym.getTypeInst() procParams = procType[0][1 .. ^1] procReturn = procType[0][0] @@ -182,7 +186,7 @@ proc exportProcCpp*( members.add procName members.add "(" for param in procParams: - members.add exportTypeCpp(param[1], param[0].getName()) + members.add exportTypeCpp(param[1], param[0].getParamName()) members.add ", " members.removeSuffix ", " members.add ") {\n" @@ -191,7 +195,7 @@ proc exportProcCpp*( members.add "return " var call = &"$lib_{apiProcName}(" for param in procParams: - call.add cppArgValue(param[1], param[0].getName()) + call.add cppArgValue(param[1], param[0].getParamName()) call.add ", " call.removeSuffix ", " call.add ")" @@ -216,16 +220,22 @@ proc exportProcCpp*( classes.add &" * {line}\n" classes.add " */\n" - classes.add &" {exportReturnTypeCpp(procReturn)} {procName}(" + let methodName = + if procName.isOperatorName: + procName.cppOperatorName() + else: + procName + + classes.add &" {exportReturnTypeCpp(procReturn)} {methodName}(" for param in procParams[1..^1]: - classes.add exportTypeCpp(param[1], param[0].getName()) + classes.add exportTypeCpp(param[1], param[0].getParamName()) classes.add ", " classes.removeSuffix ", " classes.add ");\n\n" - members.add &"{exportReturnTypeCpp(procReturn)} {owner.getName()}::{procName}(" + members.add &"{exportReturnTypeCpp(procReturn)} {owner.getName()}::{methodName}(" for param in procParams[1..^1]: - members.add exportTypeCpp(param[1], param[0].getName()) + members.add exportTypeCpp(param[1], param[0].getParamName()) members.add ", " members.removeSuffix ", " members.add ") " @@ -236,7 +246,7 @@ proc exportProcCpp*( members.add &" return " var call = &"$lib_{apiProcName}(*this, " for param in procParams[1..^1]: - call.add cppArgValue(param[1], param[0].getName()) + call.add cppArgValue(param[1], param[0].getParamName()) call.add ", " call.removeSuffix ", " call.add ")" @@ -244,40 +254,42 @@ proc exportProcCpp*( members.add ";\n" members.add "};\n\n" -proc exportObjectCpp*(sym: NimNode, constructor: NimNode) = - let objName = sym.repr +proc exportObjectCpp*( + sym: NimNode, + fields: seq[ObjectField], + constructor: NimNode +) = + let + objName = sym.repr + objFields = sym.objectFields(fields) types.add &"struct {objName};\n\n" classes.add &"struct {objName} " & "{\n" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - classes.add &" {exportTypeCpp(identDefs[^2], toSnakeCase(property[1].repr))};\n" + for field in objFields: + classes.add &" {exportTypeCpp(field.typ, toSnakeCase(field.name))};\n" if constructor != nil: exportProcCpp(constructor) else: procs.add &"{objName} $lib_{toSnakeCase(objName)}(" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - procs.add &"{exportTypeCppAbi(identDefs[^2], toSnakeCase(property[1].repr))}, " + for field in objFields: + procs.add &"{exportTypeCppAbi(field.typ, toSnakeCase(field.name))}, " procs.removeSuffix ", " procs.add ");\n\n" members.add &"{objName} {objName.unCapitalize()}(" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - members.add &"{exportTypeCpp(identDefs[^2], property[1].repr)}" - members.add ", " + for field in objFields: + members.add &"{exportTypeCpp(field.typ, field.name)}" + members.add ", " members.removeSuffix ", " members.add ") " members.add "{\n" members.add &" return " members.add &"$lib_{toSnakeCase(objName)}(" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - members.add cppArgValue(identDefs[^2], property[1].repr) - members.add ", " + for field in objFields: + members.add cppArgValue(field.typ, field.name) + members.add ", " members.removeSuffix ", " members.add ");\n" members.add "};\n\n" @@ -411,14 +423,14 @@ proc exportRefObjectCpp*( classes.add &" {objName}(" for param in constructorParams: - classes.add exportTypeCpp(param[1], param[0].getName()) + classes.add exportTypeCpp(param[1], param[0].getParamName()) classes.add ", " classes.removeSuffix ", " classes.add ");\n\n" members.add &"{objName}::{objName}(" for param in constructorParams: - members.add exportTypeCpp(param[1], param[0].getName()) + members.add exportTypeCpp(param[1], param[0].getParamName()) members.add ", " members.removeSuffix ", " members.add ")" @@ -426,7 +438,7 @@ proc exportRefObjectCpp*( members.add &" this->reference = " members.add &"{constructorLibProc}(" for param in constructorParams: - members.add cppArgValue(param[1], param[0].getName()) + members.add cppArgValue(param[1], param[0].getParamName()) members.add ", " members.removeSuffix ", " members.add ").reference;\n" diff --git a/src/genny/languages/nim.nim b/src/genny/languages/nim.nim index 9b7d944..899afc0 100644 --- a/src/genny/languages/nim.nim +++ b/src/genny/languages/nim.nim @@ -5,7 +5,9 @@ import var types {.compiletime.}: string procs {.compiletime.}: string + imports {.compiletime.}: seq[string] refObjectNames {.compiletime.}: HashSet[string] + nimWrapperSignatures {.compiletime.}: HashSet[string] proc isSeqLike(sym: NimNode): bool = ## Returns true for sequence-shaped types that Genny should expose through @@ -23,15 +25,6 @@ proc stripSink(sym: NimNode): NimNode = else: return sym -proc normalizeValueTypeName(sym: NimNode): string = - case sym.repr - of "Vec2": - "Vector2" - of "Mat3": - "Matrix3" - else: - sym.repr - proc typeBody(sym: NimNode): NimNode = let typ = sym.stripSink if typ.kind == nnkSym: @@ -44,9 +37,34 @@ proc isRefObjectLike*(sym: NimNode): bool = let typ = sym.stripSink typ.repr in refObjectNames or typ.typeBody.kind == nnkRefTy +proc addImport(module: string) = + if module.len > 0 and module notin imports: + imports.add(module) + +proc importBlock(): string = + if imports.len == 0: + return "" + + let modules = imports.join(", ") + result.add &"import {modules}\n\n" + result.add &"export {modules}\n\n" + +proc stripExportMark(name: string): string = + result = name + result.removeSuffix("*") + +proc exportedFieldName(name: string): string = + name.stripExportMark() & "*" + +proc localFieldName(name: string): string = + toSnakeCase(name.stripExportMark()) + proc exportTypeNim*(sym: NimNode): string = ## Returns type for Nim wrapper proc signature. let typ = sym.stripSink + let valueName = typ.exportedValueTypeName() + if valueName.len > 0: + return valueName if typ.kind == nnkBracketExpr: if not typ.isSeqLike: error(&"Unexpected bracket expression {typ[0].repr}[", typ) @@ -57,7 +75,7 @@ proc exportTypeNim*(sym: NimNode): string = elif typ.repr == "Rune": return "int32" else: - return typ.normalizeValueTypeName() + return typ.repr proc exportReturnTypeNim*(sym: NimNode): string = ## Returns type for internal ABI return values. Returned strings cross the @@ -72,16 +90,24 @@ proc exportArgTypeNim(sym: NimNode): string = ## Returns the friendly type for Nim wrapper parameters. Unlike return values, ## string parameters stay as Nim strings and are converted at the C boundary. let typ = sym.stripSink + let valueName = typ.exportedValueTypeName() + if valueName.len > 0: + return valueName if typ.kind == nnkBracketExpr: if not typ.isSeqLike: error(&"Unexpected bracket expression {typ[0].repr}[", typ) return typ.getSeqName() else: - return typ.normalizeValueTypeName() + if typ.repr == "Rune": + addImport("unicode") + return typ.repr proc exportTypeCImport*(sym: NimNode): string = ## Returns type for C import declaration. Ref objects become pointer. let typ = sym.stripSink + let valueName = typ.exportedValueTypeName() + if valueName.len > 0: + return valueName if typ.kind == nnkBracketExpr: if not typ.isSeqLike: error(&"Unexpected bracket expression {typ[0].repr}[", typ) @@ -94,7 +120,7 @@ proc exportTypeCImport*(sym: NimNode): string = elif typ.isRefObjectLike: return "pointer" else: - return typ.normalizeValueTypeName() + return typ.repr proc exportReturnTypeCImport*(sym: NimNode): string = ## Returns type for C import return declarations in the generated Nim wrapper. @@ -107,6 +133,8 @@ proc exportReturnTypeCImport*(sym: NimNode): string = proc convertExportFromNim*(sym: NimNode): string = ## Converts Nim value to C value (used by DLL side). let typ = sym.stripSink + if typ.exportedValueTypeName().len > 0: + return "" if typ.kind == nnkBracketExpr: if not typ.isSeqLike: error(&"Unexpected bracket expression {typ[0].repr}[", typ) @@ -123,13 +151,7 @@ proc convertExportExprNim*(expr: string, sym: NimNode): string = ## Converts a Nim expression to the ABI-facing value used by generated ## internal exports. let typ = sym.stripSink - case typ.repr - of "Vec2": - return &"cast[Vector2]({expr})" - of "Mat3": - return &"cast[Matrix3]({expr})" - else: - return expr & convertExportFromNim(typ) + expr & convertExportFromNim(typ) proc convertExportReturnExprNim*(expr: string, sym: NimNode): string = ## Converts a Nim expression to the ABI-facing return value used by generated @@ -143,6 +165,8 @@ proc convertExportReturnExprNim*(expr: string, sym: NimNode): string = proc convertToPointer*(sym: NimNode): string = ## Converts client-side wrapper to pointer for C call. let typ = sym.stripSink + if typ.exportedValueTypeName().len > 0: + return "" if typ.kind == nnkBracketExpr: if not typ.isSeqLike: error(&"Unexpected bracket expression {typ[0].repr}[", typ) @@ -159,6 +183,8 @@ proc convertToPointer*(sym: NimNode): string = proc convertImportToNim*(sym: NimNode): string = let typ = sym.stripSink + if typ.exportedValueTypeName().len > 0: + return "" if typ.kind == nnkBracketExpr: if not typ.isSeqLike: error(&"Unexpected bracket expression {typ[0].repr}[", typ) @@ -175,13 +201,7 @@ proc convertImportExprNim*(expr: string, sym: NimNode): string = ## Converts an ABI-facing expression to the Nim value expected by the source ## library implementation. let typ = sym.stripSink - case typ.repr - of "Vec2": - return &"cast[Vec2]({expr})" - of "Mat3": - return &"cast[Mat3]({expr})" - else: - return expr & convertImportToNim(typ) + expr & convertImportToNim(typ) proc convertImportReturnExprNim*(expr: string, sym: NimNode): string = ## Converts an ABI-facing return expression to the friendly Nim wrapper type. @@ -189,25 +209,11 @@ proc convertImportReturnExprNim*(expr: string, sym: NimNode): string = case typ.repr of "string": return &"gennyBufferToString({expr})" - of "Vec2": - return &"cast[Vector2]({expr})" - of "Mat3": - return &"cast[Matrix3]({expr})" else: return convertImportExprNim(expr, typ) proc exportDefaultValueNim(default, sym: NimNode): string = - ## Keeps wrapper defaults in the same ABI-facing type family as the generated - ## wrapper signature. Some Pixie APIs use vmath defaults like vec2()/mat3() - ## while the wrapper exposes ABI-compatible Vector2/Matrix3 value objects. - let typ = sym.stripSink - case typ.repr - of "Vec2": - return &"cast[Vector2]({default.repr})" - of "Mat3": - return &"cast[Matrix3]({default.repr})" - else: - return default.repr + default.repr proc exportConstNim*(sym: NimNode) = let impl = sym.getImpl() @@ -228,7 +234,7 @@ proc exportProcNim*( ) = let procName = sym.repr - procNameSnaked = toSnakeCase(procName) + procNameSnaked = toSnakeCase(procName.operatorProcName()) procType = sym.getTypeInst() procParams = procType[0][1 .. ^1] procReturn = procType[0][0] @@ -260,7 +266,7 @@ proc exportProcNim*( var paramType = param[^2] if paramType.repr.endsWith(":type"): paramType = owner - procs.add &"{toSnakeCase(param[i].repr)}: {exportTypeCImport(paramType)}, " + procs.add &"{toSnakeCase(param[i].getParamName())}: {exportTypeCImport(paramType)}, " procs.removeSuffix ", " procs.add ")" if procReturn.kind != nnkEmpty: @@ -272,15 +278,28 @@ proc exportProcNim*( procs.add "\n" # Nim wrapper proc - procs.add &"proc {procName}*(" + var wrapperSignature = &"{procName.nimCallableName()}(" + for param in procParams: + var paramType = param[^2] + if paramType.repr.endsWith(":type"): + paramType = owner + for i in 0 .. param.len - 3: + wrapperSignature.add exportArgTypeNim(paramType) + wrapperSignature.add "," + wrapperSignature.add ")" + if wrapperSignature in nimWrapperSignatures: + return + nimWrapperSignatures.incl(wrapperSignature) + + procs.add &"proc {procName.nimCallableName()}*(" for i, param in procParams: var paramType = param[1] if paramType.repr.endsWith(":type"): paramType = owner if param[^2].kind == nnkBracketExpr or paramType.repr.startsWith("Some"): - procs.add &"{param[0].repr}: {exportTypeNim(paramType)}, " + procs.add &"{param[0].getParamName()}: {exportTypeNim(paramType)}, " else: - procs.add &"{param[0].repr}: {exportArgTypeNim(paramType)}" + procs.add &"{param[0].getParamName()}: {exportArgTypeNim(paramType)}" if defaults[i][1].kind != nnkEmpty: procs.add &" = {exportDefaultValueNim(defaults[i][1], paramType)}" procs.add ", " @@ -292,7 +311,7 @@ proc exportProcNim*( var callExpr = &"{apiProcName}(" for param in procParams: for i in 0 .. param.len - 3: - callExpr.add &"{param[i].repr}{convertToPointer(param[^2])}, " + callExpr.add &"{param[i].getParamName()}{convertToPointer(param[^2])}, " callExpr.removeSuffix ", " callExpr.add ")" @@ -315,31 +334,38 @@ proc exportProcNim*( procs.add " result = gennyBufferToString(gennyBuffer)\n" procs.add "\n" -proc exportObjectNim*(sym: NimNode, constructor: NimNode) = - let objName = sym.repr +proc exportObjectNim*( + sym: NimNode, + fields: seq[ObjectField], + constructor: NimNode, + externalModule: string = "" +) = + let + objName = sym.repr + objFields = sym.objectFields(fields) - if objName in ["Rect", "Color"]: + if externalModule.len > 0: + addImport(externalModule) + if constructor != nil: + exportProcNim(constructor) return types.add &"type {objName}* {{.bycopy.}} = object\n" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - types.add &" {property.repr}: {identDefs[^2].repr}\n" + for field in objFields: + types.add &" {field.name.exportedFieldName()}: {field.typ.repr}\n" types.add "\n" if constructor != nil: exportProcNim(constructor) else: types.add &"proc {toVarCase(objName)}*(" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - types.add &"{toSnakeCase(property[1].repr)}: {identDefs[^2].repr}, " + for field in objFields: + types.add &"{field.name.localFieldName()}: {field.typ.repr}, " types.removeSuffix ", " types.add &"): {objName} =\n" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - types.add &" result.{toSnakeCase(property[1].repr)} = " - types.add &"{toSnakeCase(property[1].repr)}\n" + for field in objFields: + types.add &" result.{field.name.localFieldName()} = " + types.add &"{field.name.localFieldName()}\n" types.add "\n" proc genRefObject(objName: string) = @@ -558,10 +584,6 @@ proc exportSeqNim*(sym: NimNode) = procs.add "\n" const header = """ -import bumpy, chroma, unicode, vmath - -export bumpy, chroma, unicode, vmath - when defined(windows): const libName = "$lib.dll" elif defined(macosx): @@ -594,6 +616,6 @@ type $LibError = object of ValueError proc writeNim*(dir, lib: string) = createDir(dir) - writeFile( &"{dir}/{toSnakeCase(lib)}.nim", (header & types & procs) + writeFile( &"{dir}/{toSnakeCase(lib)}.nim", (importBlock() & header & types & procs) .replace("$Lib", lib).replace("$lib", toSnakeCase(lib)) ) diff --git a/src/genny/languages/node.nim b/src/genny/languages/node.nim index 03f1e89..82abee6 100644 --- a/src/genny/languages/node.nim +++ b/src/genny/languages/node.nim @@ -43,10 +43,13 @@ proc isEnumLike(sym: NimNode): bool = proc exportTypeNode(sym: NimNode): string = let typ = sym.stripSink + let valueName = typ.exportedValueTypeName() + if valueName.len > 0: + return valueName if typ.kind == nnkBracketExpr: if typ[0].repr == "array": let - entryCount = typ[1].repr + entryCount = $typ.arrayCount() entryType = exportTypeNode(typ[2]) result = &"koffi.array({entryType}, {entryCount})" elif typ.isSeqLike: @@ -74,8 +77,6 @@ proc exportTypeNode(sym: NimNode): string = of "float": "'double'" of "proc () {.cdecl.}": "'pointer'" of "Rune": "'int32'" - of "Vec2": "Vector2" - of "Mat3": "Matrix3" of "": "'void'" else: if typ.isEnumLike: @@ -147,7 +148,7 @@ proc exportProcNode*( ) = let procName = sym.repr - procNameSnaked = toSnakeCase(procName) + procNameSnaked = toSnakeCase(procName.operatorProcName()) procType = sym.getTypeInst() procParams = procType[0][1 .. ^1] procReturn = procType[0][0] @@ -194,30 +195,27 @@ proc exportProcNode*( types.add &"{owner.getName()}.prototype." var name = "" if prefixes.len > 0: - if prefixes[0].getImpl().kind != nnkNilLIt: - if prefixes[0].getImpl()[2].kind != nnkEnumTy: - name.add &"{prefixes[0].repr}_" - name.add sym.repr + if prefixes[0].usePrefixName(): + name.add &"{prefixes[0].getName()}_" + name.add sym.repr.operatorProcName() types.add &"{toVarCase(toCamelCase(name))} = function(" elif onClass: # Value object with method - generate as standalone function var name = owner.getName() & "_" if prefixes.len > 0: - if prefixes[0].getImpl().kind != nnkNilLIt: - if prefixes[0].getImpl()[2].kind != nnkEnumTy: - name.add &"{prefixes[0].repr}_" - name.add sym.repr + name.add &"{prefixes[0].getName()}_" + name.add sym.repr.operatorProcName() types.add &"function {toVarCase(toCamelCase(name))}(" exports.add &"exports.{toVarCase(toCamelCase(name))} = {toVarCase(toCamelCase(name))};\n" else: - types.add &"function {sym.repr}(" - exports.add &"exports.{sym.repr} = {sym.repr};\n" + types.add &"function {sym.repr.operatorProcName()}(" + exports.add &"exports.{sym.repr.operatorProcName()} = {sym.repr.operatorProcName()};\n" for i, param in procParams[0 .. ^1]: if isRefObject and i == 0: discard else: - types.add toSnakeCase(param[0].repr) + types.add toSnakeCase(param[0].getParamName()) types.add &", " types.removeSuffix ", " types.add ") {\n" @@ -229,7 +227,7 @@ proc exportProcNode*( if isRefObject and i == 0: call.add "this.ref" else: - let argName = toSnakeCase(param[0].repr) + let argName = toSnakeCase(param[0].getParamName()) call.add jsArgValue(param[^2], argName) call.add &", " call.removeSuffix ", " @@ -238,15 +236,20 @@ proc exportProcNode*( types.add ";\n" types.add "}\n\n" -proc exportObjectNode*(sym: NimNode, constructor: NimNode) = - let objName = sym.repr +proc exportObjectNode*( + sym: NimNode, + fields: seq[ObjectField], + constructor: NimNode +) = + let + objName = sym.repr + objFields = sym.objectFields(fields) objects.incl(objName) # Define struct type with koffi types.add &"const {objName} = koffi.struct('{objName}', {{\n" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - types.add &" {property[1].repr}: {exportTypeNode(identDefs[^2])},\n" + for field in objFields: + types.add &" {field.name}: {exportTypeNode(field.typ)},\n" types.removeSuffix ",\n" types.add "\n});\n\n" exports.add &"exports.{objName} = {objName};\n" @@ -254,15 +257,13 @@ proc exportObjectNode*(sym: NimNode, constructor: NimNode) = # Constructor function exports.add &"exports.{toVarCase(objName)} = {toVarCase(objName)};\n" types.add &"function {toVarCase(objName)}(" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - types.add &"{toSnakeCase(property[1].repr)}, " + for field in objFields: + types.add &"{toSnakeCase(field.name)}, " types.removeSuffix ", " types.add ") {\n" types.add " return {\n" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - types.add &" {property[1].repr}: {toSnakeCase(property[1].repr)},\n" + for field in objFields: + types.add &" {field.name}: {toSnakeCase(field.name)},\n" types.removeSuffix ",\n" types.add "\n };\n" types.add "}\n\n" @@ -349,13 +350,13 @@ proc exportRefObjectNode*( exports.add &"exports.new{objName} = new{objName};\n" types.add &"function new{objName}(" for i, param in constructorParams[0 .. ^1]: - types.add &"{toSnakeCase(param[0].repr)}" + types.add &"{toSnakeCase(param[0].getParamName())}" types.add ", " types.removeSuffix ", " types.add ") {\n" types.add &" const ref = {constructorLibProc}(" for i, param in constructorParams[0 .. ^1]: - let argName = toSnakeCase(param[0].repr) + let argName = toSnakeCase(param[0].getParamName()) types.add jsArgValue(param[^2], argName) types.add ", " types.removeSuffix ", " diff --git a/src/genny/languages/python.nim b/src/genny/languages/python.nim index 6eb5ff9..67b569a 100644 --- a/src/genny/languages/python.nim +++ b/src/genny/languages/python.nim @@ -1,12 +1,18 @@ import - std/[os, strformat, strutils, macros], + std/[algorithm, os, strformat, strutils, macros, tables], ../common +type OperatorCase = tuple[ + rhsType: NimNode, + returnType: NimNode, + apiProcName: string, + procRaises: bool +] + var types {.compiletime.}: string procs {.compiletime.}: string - -const operators = ["add", "sub", "mul", "div"] + operatorMethods {.compiletime.}: Table[string, Table[string, seq[OperatorCase]]] proc stripSink(sym: NimNode): NimNode = ## Removes Nim's `sink[T]` ownership wrapper before mapping a type to ctypes. @@ -29,10 +35,13 @@ proc isStringType(sym: NimNode): bool = proc exportTypePy(sym: NimNode): string = let typ = sym.stripSink + let valueName = typ.exportedValueTypeName() + if valueName.len > 0: + return valueName if typ.kind == nnkBracketExpr: if typ[0].repr == "array": let - entryCount = typ[1].repr + entryCount = $typ.arrayCount() entryType = exportTypePy(typ[2]) result = &"{entryType} * {entryCount}" elif typ.isSeqLike: @@ -61,8 +70,6 @@ proc exportTypePy(sym: NimNode): string = of "float64": "c_double" of "float": "c_double" of "Rune": "c_int" - of "Vec2": "Vector2" - of "Mat3": "Matrix3" of "", "nil": "None" else: typ.repr @@ -103,6 +110,20 @@ proc importExprPy(expr: string, sym: NimNode): string = else: expr +proc pyTypeCheck(expr: string, sym: NimNode): string = + let typ = sym.stripSink + case typ.repr: + of "bool": + &"isinstance({expr}, bool)" + of "int8", "byte", "int16", "int32", "int64", "int", "uint8", "uint16", "uint32", "uint64", "uint": + &"isinstance({expr}, int)" + of "float32", "float64", "float": + &"isinstance({expr}, (int, float))" + of "string", "cstring", "Rune": + &"isinstance({expr}, str)" + else: + &"isinstance({expr}, {exportTypePy(typ)})" + proc toArgTypes(args: openarray[NimNode]): seq[string] = for arg in args: result.add exportTypePy(arg) @@ -131,7 +152,7 @@ proc exportProcPy*( ) = let procName = sym.repr - procNameSnaked = toSnakeCase(procName) + procNameSnaked = toSnakeCase(procName.operatorProcName()) procType = sym.getTypeInst() procParams = procType[0][1 .. ^1] procReturn = procType[0][0] @@ -151,25 +172,41 @@ proc exportProcPy*( for entry in identDefs[0 .. ^3]: defaults.add((entry.repr, default)) + if onClass and procName.isOperatorName and procReturn.kind != nnkEmpty: + if procParams.len < 2: + error("Python operator overloads need a right-hand operand", sym) + + let methodName = procName.pythonOperatorName() + var ownerOps = operatorMethods.getOrDefault(owner.getName()) + var cases = ownerOps.getOrDefault(methodName) + cases.add(( + rhsType: procParams[1][^2], + returnType: procReturn, + apiProcName: &"dll.$lib_{apiProcName}", + procRaises: procRaises + )) + ownerOps[methodName] = cases + operatorMethods[owner.getName()] = ownerOps + + var dllParams: seq[NimNode] + for param in procParams: + dllParams.add(param[1]) + dllProc(&"dll.$lib_{apiProcName}", toArgTypes(dllParams), exportReturnTypePy(procReturn)) + return + if onClass: types.add " def " - if sym.repr in operators and - procReturn.kind != nnkEmpty and - prefixes.len == 0: - types.add &"__{sym.repr}__(" - else: - if prefixes.len > 0: - if prefixes[0].getImpl().kind != nnkNilLIt: - if prefixes[0].getImpl()[2].kind != nnkEnumTy: - types.add &"{toSnakeCase(prefixes[0].repr)}_" - types.add &"{toSnakeCase(sym.repr)}(" + if prefixes.len > 0: + if prefixes[0].usePrefixName(): + types.add &"{toSnakeCase(prefixes[0].getName())}_" + types.add &"{toSnakeCase(sym.repr.operatorProcName())}(" else: types.add &"def {apiProcName}(" for i, param in procParams[0 .. ^1]: if onClass and i == 0: types.add "self" else: - types.add toSnakeCase(param[0].repr) + types.add toSnakeCase(param[0].getParamName()) case defaults[i][1].kind: of nnkIntLit, nnkFloatLit: types.add &" = {defaults[i][1].repr}" @@ -210,10 +247,10 @@ proc exportProcPy*( if defaults[i][1].kind notin {nnkEmpty, nnkIntLit, nnkFloatLit, nnkIdent}: if onClass: types.add " " - types.add &" if {toSnakeCase(param[0].repr)} is None:\n" + types.add &" if {toSnakeCase(param[0].getParamName())} is None:\n" if onClass: types.add " " - types.add &" {toSnakeCase(param[0].repr)} = " + types.add &" {toSnakeCase(param[0].getParamName())} = " types.add &"{exportTypePy(param[1])}(" if defaults[i][1].kind == nnkCall: for d in defaults[i][1][1 .. ^1]: @@ -231,7 +268,7 @@ proc exportProcPy*( if onClass and i == 0: call.add "self" else: - call.add exportExprPy(toSnakeCase(param[0].repr), param[1]) + call.add exportExprPy(toSnakeCase(param[0].getParamName()), param[1]) call.add &", " call.removeSuffix ", " call.add ")" @@ -257,16 +294,21 @@ proc exportProcPy*( dllParams.add(param[1]) dllProc(&"dll.$lib_{apiProcName}", toArgTypes(dllParams), exportReturnTypePy(procReturn)) -proc exportObjectPy*(sym: NimNode, constructor: NimNode) = - let objName = sym.repr +proc exportObjectPy*( + sym: NimNode, + fields: seq[ObjectField], + constructor: NimNode +) = + let + objName = sym.repr + objFields = sym.objectFields(fields) types.add &"class {objName}(Structure):\n" types.add " _fields_ = [\n" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - types.add &" (\"{toSnakeCase(property[1].repr)}\"" - types.add ", " - types.add &"{exportTypePy(identDefs[^2])}),\n" + for field in objFields: + types.add &" (\"{toSnakeCase(field.name)}\"" + types.add ", " + types.add &"{exportTypePy(field.typ)}),\n" types.removeSuffix ",\n" types.add "\n" types.add " ]\n" @@ -278,20 +320,19 @@ proc exportObjectPy*(sym: NimNode, constructor: NimNode) = constructorParams = constructorType[0][1 .. ^1] types.add " def __init__(self, " for param in constructorParams: - types.add &"{toSnakeCase(param[0].repr)}" + types.add &"{toSnakeCase(param[0].getParamName())}" types.add ", " types.removeSuffix ", " types.add "):\n" types.add &" tmp = dll.$lib_{toSnakeCase(objName)}(" for param in constructorParams: - types.add exportExprPy(toSnakeCase(param[0].repr), param[1]) + types.add exportExprPy(toSnakeCase(param[0].getParamName()), param[1]) types.add ", " types.removeSuffix ", " types.add ")\n" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - types.add &" self.{toSnakeCase(property[1].repr)} = " - types.add &"tmp.{toSnakeCase(property[1].repr)}\n" + for field in objFields: + types.add &" self.{toSnakeCase(field.name)} = " + types.add &"tmp.{toSnakeCase(field.name)}\n" types.add "\n" var dllParams: seq[NimNode] for param in constructorParams: @@ -299,31 +340,51 @@ proc exportObjectPy*(sym: NimNode, constructor: NimNode) = dllProc(&"dll.$lib_{toSnakeCase(objName)}", toArgTypes(dllParams), objName) else: types.add " def __init__(self, " - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - types.add &"{toSnakeCase(property[1].repr)}, " + for field in objFields: + types.add &"{toSnakeCase(field.name)}, " types.removeSuffix ", " types.add "):\n" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - types.add " " - types.add &"self.{toSnakeCase(property[1].repr)} = " - types.add &"{toSnakeCase(property[1].repr)}\n" + for field in objFields: + types.add " " + types.add &"self.{toSnakeCase(field.name)} = " + types.add &"{toSnakeCase(field.name)}\n" types.add "\n" types.add " def __eq__(self, obj):\n" types.add " return " - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - if identDefs[^2].len > 0 and identDefs[^2][0].repr == "array": - for i in 0 ..< identDefs[^2][1].intVal: - types.add &"self.{toSnakeCase(property[1].repr)}[{i}] == obj.{toSnakeCase(property[1].repr)}[{i}] and " - else: - types.add &"self.{toSnakeCase(property[1].repr)} == obj.{toSnakeCase(property[1].repr)} and " + for field in objFields: + if field.typ.len > 0 and field.typ[0].repr == "array": + for i in 0 ..< field.typ.arrayCount(): + types.add &"self.{toSnakeCase(field.name)}[{i}] == obj.{toSnakeCase(field.name)}[{i}] and " + else: + types.add &"self.{toSnakeCase(field.name)} == obj.{toSnakeCase(field.name)} and " types.removeSuffix " and " types.add "\n" types.add "\n" +proc exportCloseObjectPy*(sym: NimNode) = + let objName = sym.getName() + if objName notin operatorMethods: + return + + var methodNames: seq[string] + for methodName in operatorMethods[objName].keys: + methodNames.add(methodName) + methodNames.sort() + + for methodName in methodNames: + let cases = operatorMethods[objName][methodName] + types.add &" def {methodName}(self, other):\n" + for opCase in cases: + types.add &" if {pyTypeCheck(\"other\", opCase.rhsType)}:\n" + types.add &" result = {opCase.apiProcName}(self, {exportExprPy(\"other\", opCase.rhsType)})\n" + if opCase.procRaises: + types.add &" if check_error():\n" + types.add " raise $LibError(take_error())\n" + types.add &" return {importExprPy(\"result\", opCase.returnType)}\n" + types.add " return NotImplemented\n" + types.add "\n" + proc genRefObject(objName: string) = types.add &"class {objName}(Structure):\n" types.add " _fields_ = [(\"ref\", c_ulonglong)]\n" @@ -408,14 +469,14 @@ proc exportRefObjectPy*( types.add " def __init__(self, " for i, param in constructorParams[0 .. ^1]: - types.add &"{toSnakeCase(param[0].repr)}" + types.add &"{toSnakeCase(param[0].getParamName())}" types.add ", " types.removeSuffix ", " types.add "):\n" types.add &" result = " types.add &"{constructorLibProc}(" for param in constructorParams: - types.add exportExprPy(toSnakeCase(param[0].repr), param[1]) + types.add exportExprPy(toSnakeCase(param[0].getParamName()), param[1]) types.add ", " types.removeSuffix ", " types.add ")\n" diff --git a/src/genny/languages/python_native.nim b/src/genny/languages/python_native.nim index c4ed601..2832a4e 100644 --- a/src/genny/languages/python_native.nim +++ b/src/genny/languages/python_native.nim @@ -1,9 +1,15 @@ import - std/[compilesettings, macros, os, sets, strformat, strutils, tables], + std/[algorithm, compilesettings, macros, os, sets, strformat, strutils, tables], ../common type FieldInfo = tuple[name: string, typ: NimNode] + OperatorCase = tuple[ + rhsType: NimNode, + returnType: NimNode, + apiProcName: string, + procRaises: bool + ] var cTypes {.compiletime.}: string @@ -18,6 +24,7 @@ var seqObjectNames {.compiletime.}: HashSet[string] valueObjectFields {.compiletime.}: Table[string, seq[FieldInfo]] valueObjectConstructors {.compiletime.}: Table[string, string] + valueObjectOperators {.compiletime.}: Table[string, Table[string, seq[OperatorCase]]] seqEntryTypes {.compiletime.}: Table[string, NimNode] seqNewProcs {.compiletime.}: Table[string, string] typeMethods {.compiletime.}: Table[string, string] @@ -53,13 +60,13 @@ proc stripSink(sym: NimNode): NimNode = proc nativeName(sym: NimNode): string = let typ = sym.stripSink - if typ.kind == nnkBracketExpr: + let valueName = typ.exportedValueTypeName() + if valueName.len > 0: + valueName + elif typ.kind == nnkBracketExpr: typ.getSeqName() else: - case typ.repr - of "Vec2": "Vector2" - of "Mat3": "Matrix3" - else: typ.repr + typ.repr proc typeObjName(name: string): string = "GennyPy_" & cIdent(name) & "_Type" @@ -86,7 +93,7 @@ proc cType(sym: NimNode): string proc cArraySuffix(sym: NimNode): string = let typ = sym.stripSink if typ.isArrayType: - result.add &"[{typ[1].repr}]" + result.add &"[{typ.arrayCount()}]" result.add cArraySuffix(typ[2]) proc cBaseType(sym: NimNode): string = @@ -164,15 +171,14 @@ proc apiProcName(sym: NimNode, owner: NimNode = nil, prefixes: openarray[NimNode result.add &"{toSnakeCase(owner.getName())}_" for prefix in prefixes: result.add &"{toSnakeCase(prefix.getName())}_" - result.add toSnakeCase(sym.repr) + result.add toSnakeCase(sym.repr.operatorProcName()) proc pyProcName(sym: NimNode, owner: NimNode = nil, prefixes: openarray[NimNode] = []): string = if owner != nil: for prefix in prefixes: - if prefix.getImpl().kind != nnkNilLit: - if prefix.getImpl()[2].kind != nnkEnumTy: - result.add &"{toSnakeCase(prefix.repr)}_" - result.add toSnakeCase(sym.repr) + if prefix.usePrefixName(): + result.add &"{toSnakeCase(prefix.getName())}_" + result.add toSnakeCase(sym.repr.operatorProcName()) proc getDefaults(sym: NimNode): seq[NimNode] = let procType = sym.getTypeInst() @@ -277,6 +283,36 @@ proc convertPyToC(pyExpr, outExpr: string, typ: NimNode, label: string): string proc convertPyToCInt(pyExpr, outExpr: string, typ: NimNode, label: string): string = convertPyToC(pyExpr, outExpr, typ, label).replace("return NULL", "return -1") +proc pyNativeTypeCheck(expr: string, typ: NimNode): string = + let baseTyp = typ.stripSink + let name = baseTyp.pyTypeName() + if baseTyp.isValueObjectType: + return &"PyObject_TypeCheck({expr}, &{typeObjName(name)})" + if baseTyp.isRefLikeObjectType: + return &"PyObject_TypeCheck({expr}, &{typeObjName(name)})" + if baseTyp.isSeqType: + return &"PyObject_TypeCheck({expr}, &{typeObjName(baseTyp.getSeqName())})" + + case baseTyp.nativeName() + of "bool": + &"PyBool_Check({expr})" + of "byte", "uint8", "uint16", "uint32", "uint64", "uint", "int8", "int16", "int32", "int64", "int": + &"PyLong_Check({expr})" + of "float32", "float64", "float": + &"(PyFloat_Check({expr}) || PyLong_Check({expr}))" + of "string", "cstring", "Rune": + &"PyUnicode_Check({expr})" + else: + "1" + +proc pyNumberSlot(name: string): string = + case name.normalizedOperatorName() + of "+": "nb_add" + of "-": "nb_subtract" + of "*": "nb_multiply" + of "/": "nb_true_divide" + else: "" + proc addProto(procName: string, params: seq[(string, NimNode)], ret: NimNode) = cProtos.add "extern " & cReturnType(ret) & " " & procName & "(" for (name, typ) in params: @@ -290,16 +326,16 @@ proc addModuleMethod(pyName, wrapperName: string) = proc addErrorCheck(procRaises: bool): string = if procRaises: needsErrorBridge = true - result.add " if (pixie_native_check_error != NULL && pixie_native_check_error()) {\n" - result.add " genny_set_error_from_buffer(pixie_native_take_error());\n" + result.add " if ($lib_native_check_error != NULL && $lib_native_check_error()) {\n" + result.add " genny_set_error_from_buffer($lib_native_take_error());\n" result.add " return NULL;\n" result.add " }\n" proc addErrorCheckInt(procRaises: bool): string = if procRaises: needsErrorBridge = true - result.add " if (pixie_native_check_error != NULL && pixie_native_check_error()) {\n" - result.add " genny_set_error_from_buffer(pixie_native_take_error());\n" + result.add " if ($lib_native_check_error != NULL && $lib_native_check_error()) {\n" + result.add " genny_set_error_from_buffer($lib_native_take_error());\n" result.add " return -1;\n" result.add " }\n" @@ -312,7 +348,7 @@ proc declareFields(objName: string, fields: seq[FieldInfo]) = proc arrayToPy(fieldExpr: string, fieldType: NimNode): string = if not fieldType.isArrayType: return pyFromC(fieldExpr, fieldType) - let count = fieldType[1].intVal + let count = fieldType.arrayCount() let elemType = fieldType[2] let listName = "genny_list" result.add &" PyObject *{listName} = PyList_New({count});\n" @@ -325,7 +361,7 @@ proc arrayToPy(fieldExpr: string, fieldType: NimNode): string = result.add &" return {listName};\n" proc arraySetFromPy(fieldExpr: string, fieldType: NimNode): string = - let count = fieldType[1].intVal + let count = fieldType.arrayCount() let elemType = fieldType[2] result.add " if (!PySequence_Check(value) || PySequence_Size(value) != " & $count & ") {\n" result.add &" PyErr_SetString(PyExc_TypeError, \"expected a sequence of length {count}\");\n" @@ -365,7 +401,7 @@ proc emitParamSetup( continue for j in 0 .. param.len - 3: let - paramName = toSnakeCase(param[j].repr) + paramName = toSnakeCase(param[j].getParamName()) argVar = "arg_" & paramName cVar = "c_" & paramName paramType = param[^2] @@ -429,12 +465,30 @@ proc exportProcPyNative*( var protoParams: seq[(string, NimNode)] for param in procParams: for i in 0 .. param.len - 3: - protoParams.add((toSnakeCase(param[i].repr), param[^2])) + protoParams.add((toSnakeCase(param[i].getParamName()), param[^2])) addProto(apiName, protoParams, procReturn) if procRaises: needsErrorBridge = true + if onType and sym.repr.isOperatorName and ownerTypeName in valueObjectNames and not procReturn.isVoid: + if procParams.len < 2: + error("Python native operator overloads need a right-hand operand", sym) + let slotName = pyNumberSlot(sym.repr) + if slotName.len == 0: + error("Unsupported Python native operator overload", sym) + var ownerOps = valueObjectOperators.getOrDefault(ownerTypeName) + var cases = ownerOps.getOrDefault(slotName) + cases.add(( + rhsType: procParams[1][^2], + returnType: procReturn, + apiProcName: apiName, + procRaises: procRaises + )) + ownerOps[slotName] = cases + valueObjectOperators[ownerTypeName] = ownerOps + return + cForwardDecls.add &"static PyObject *{wrapperName}(PyObject *self, PyObject *args, PyObject *kwargs);\n" let skipFirst = onType @@ -507,7 +561,7 @@ proc emitValueObjectType(objName: string, fields: seq[FieldInfo], constructor: N var protoParams: seq[(string, NimNode)] for param in ctorParams: for i in 0 .. param.len - 3: - protoParams.add((toSnakeCase(param[i].repr), param[^2])) + protoParams.add((toSnakeCase(param[i].getParamName()), param[^2])) addProto(ctorName, protoParams, ident(objName)) else: var protoParams: seq[(string, NimNode)] @@ -604,6 +658,9 @@ proc emitValueObjectType(objName: string, fields: seq[FieldInfo], constructor: N cTypeBlocks.add &"static PyMethodDef GennyPy_{cIdent(objName)}_methods[] = {{\n" cTypeBlocks.add &"/* GENNY_METHODS_{cIdent(objName)} */\n" cTypeBlocks.add " {NULL}\n};\n\n" + cTypeBlocks.add &"static PyNumberMethods GennyPy_{cIdent(objName)}_number = {{\n" + cTypeBlocks.add &"/* GENNY_NUMBER_{cIdent(objName)} */\n" + cTypeBlocks.add "};\n\n" cTypeBlocks.add &"static PyTypeObject {typeObj} = {{\n" cTypeBlocks.add " PyVarObject_HEAD_INIT(NULL, 0)\n" cTypeBlocks.add &" .tp_name = {cString(\"$lib.\" & objName)},\n" @@ -615,6 +672,7 @@ proc emitValueObjectType(objName: string, fields: seq[FieldInfo], constructor: N cTypeBlocks.add &" .tp_new = {newName},\n" cTypeBlocks.add &" .tp_init = (initproc){initName},\n" cTypeBlocks.add &" .tp_richcompare = {richName},\n" + cTypeBlocks.add &" .tp_as_number = &GennyPy_{cIdent(objName)}_number,\n" cTypeBlocks.add &" .tp_methods = GennyPy_{cIdent(objName)}_methods,\n" cTypeBlocks.add &" .tp_getset = GennyPy_{cIdent(objName)}_getset,\n" cTypeBlocks.add "};\n\n" @@ -622,18 +680,21 @@ proc emitValueObjectType(objName: string, fields: seq[FieldInfo], constructor: N cModuleInit.add &" Py_INCREF(&{typeObj});\n" cModuleInit.add &" if (PyModule_AddObject(m, {cString(objName)}, (PyObject *)&{typeObj}) < 0) return NULL;\n" -proc exportObjectPyNative*(sym: NimNode, constructor: NimNode) = +proc exportObjectPyNative*( + sym: NimNode, + fields: seq[ObjectField], + constructor: NimNode +) = let objName = sym.nativeName() valueObjectNames.incl(objName) - var fields: seq[FieldInfo] - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - fields.add((property[1].repr, identDefs[^2])) - valueObjectFields[objName] = fields + var objFields: seq[FieldInfo] + for field in sym.objectFields(fields): + objFields.add((field.name, field.typ)) + valueObjectFields[objName] = objFields valueObjectConstructors[objName] = if constructor != nil: "$lib_" & toSnakeCase(objName) else: "" - declareFields(objName, fields) - emitValueObjectType(objName, fields, constructor) + declareFields(objName, objFields) + emitValueObjectType(objName, objFields, constructor) proc emitRefLikeType(objName: string, constructor: NimNode, isSeq = false, entryType: NimNode = nil) proc emitSeqMethods(objName, procPrefix, refExpr, typePrefix: string, entryType: NimNode, abiObjName = "") @@ -704,7 +765,7 @@ proc emitRefLikeType(objName: string, constructor: NimNode, isSeq = false, entry var protoParams: seq[(string, NimNode)] for param in ctorParams: for i in 0 .. param.len - 3: - protoParams.add((toSnakeCase(param[i].repr), param[^2])) + protoParams.add((toSnakeCase(param[i].getParamName()), param[^2])) addProto(constructorApi, protoParams, ident(objName)) cTypeBlocks.add setup.namesArray cTypeBlocks.add setup.declarations @@ -952,6 +1013,54 @@ proc pyConfig(): Table[string, string] = proc toUnixPath(s: string): string = s.replace("\\", "/") +proc generateNativeOperatorWrappers(finalTypeBlocks: var string): tuple[decls, wrappers: string] = + var objNames: seq[string] + for objName in valueObjectOperators.keys: + objNames.add(objName) + objNames.sort() + + for objName in objNames: + let + pyStruct = pyStructName(objName) + typeObj = typeObjName(objName) + objOps = valueObjectOperators[objName] + marker = &"/* GENNY_NUMBER_{cIdent(objName)} */\n" + + var slotNames: seq[string] + for slotName in objOps.keys: + slotNames.add(slotName) + slotNames.sort() + + var slotLines = "" + for slotName in slotNames: + let + wrapperName = &"GennyPy_{cIdent(objName)}_{slotName}" + cases = objOps[slotName] + result.decls.add &"static PyObject *{wrapperName}(PyObject *left, PyObject *right);\n" + slotLines.add &" .{slotName} = {wrapperName},\n" + + result.wrappers.add &"static PyObject *{wrapperName}(PyObject *left, PyObject *right) {{\n" + result.wrappers.add &" if (!PyObject_TypeCheck(left, &{typeObj})) {{ Py_RETURN_NOTIMPLEMENTED; }}\n" + result.wrappers.add &" {objName} c_self = (({pyStruct} *)left)->value;\n" + for i, opCase in cases: + let rhsName = &"c_other_{i}" + result.wrappers.add &" if ({pyNativeTypeCheck(\"right\", opCase.rhsType)}) {{\n" + result.wrappers.add &" {cDecl(opCase.rhsType, rhsName)};\n" + result.wrappers.add convertPyToC("right", rhsName, opCase.rhsType, "other").indent(2) + if opCase.returnType.isVoid: + result.wrappers.add &" {opCase.apiProcName}(c_self, {rhsName});\n" + result.wrappers.add addErrorCheck(opCase.procRaises).indent(2) + result.wrappers.add " Py_RETURN_NONE;\n" + else: + result.wrappers.add &" {cReturnType(opCase.returnType)} genny_result = {opCase.apiProcName}(c_self, {rhsName});\n" + result.wrappers.add addErrorCheck(opCase.procRaises).indent(2) + result.wrappers.add &" return {pyFromC(\"genny_result\", opCase.returnType)};\n" + result.wrappers.add " }\n" + result.wrappers.add " Py_RETURN_NOTIMPLEMENTED;\n" + result.wrappers.add "}\n\n" + + finalTypeBlocks = finalTypeBlocks.replace(marker, slotLines) + proc writePyNative*(dir, lib: string): NimNode = createDir(dir) let cfg = pyConfig() @@ -962,6 +1071,7 @@ proc writePyNative*(dir, lib: string): NimNode = &"/* GENNY_METHODS_{cIdent(typeName)} */\n", entries ) + let nativeOperators = generateNativeOperatorWrappers(finalTypeBlocks) var code = "" code.add "#define PY_SSIZE_T_CLEAN\n" @@ -1030,16 +1140,18 @@ proc writePyNative*(dir, lib: string): NimNode = if needsErrorBridge: code.add "extern char $lib_check_error(void);\n" code.add "extern void *$lib_take_error(void);\n" - code.add "static char (*pixie_native_check_error)(void) = $lib_check_error;\n" - code.add "static void *(*pixie_native_take_error)(void) = $lib_take_error;\n\n" + code.add "static char (*$lib_native_check_error)(void) = $lib_check_error;\n" + code.add "static void *(*$lib_native_take_error)(void) = $lib_take_error;\n\n" code.add cTypes code.add "\n" code.add cForwardDecls + code.add nativeOperators.decls code.add "\n" code.add cProtos code.add "\n" code.add finalTypeBlocks code.add "\n" + code.add nativeOperators.wrappers code.add cWrappers code.add "static PyMethodDef GennyPy_ModuleMethods[] = {\n" code.add cModuleMethods diff --git a/src/genny/languages/zig.nim b/src/genny/languages/zig.nim index 038b778..0419884 100644 --- a/src/genny/languages/zig.nim +++ b/src/genny/languages/zig.nim @@ -20,10 +20,13 @@ proc isStringType(sym: NimNode): bool = proc exportTypeZig(sym: NimNode): string = let typ = sym.stripSink + let valueName = typ.exportedValueTypeName() + if valueName.len > 0: + return valueName if typ.kind == nnkBracketExpr: if typ[0].repr == "array": let - entryCount = typ[1].repr + entryCount = $typ.arrayCount() entryType = exportTypeZig(typ[2]) result = &"[{entryCount}]{entryType}" elif typ.isSeqLike: @@ -53,8 +56,6 @@ proc exportTypeZig(sym: NimNode): string = of "float64": "f64" of "float": "f64" of "Rune": "u21" - of "Vec2": "Vector2" - of "Mat3": "Matrix3" of "", "nil": "void" else: typ.repr @@ -93,7 +94,7 @@ proc exportTypeZigAbi(sym: string): string = proc toArgSeq(args: seq[NimNode]): seq[(string, string)] = for i, arg in args[0 .. ^1]: - result.add (arg[0].repr, arg[1].exportTypeZig()) + result.add (arg[0].getParamName(), arg[1].exportTypeZig()) proc dllProc*(procName: string, args: openarray[string], resType: string) = discard @@ -227,9 +228,9 @@ proc exportProcZig*( rename = "", ) = var - procName = sym.repr + procName = sym.repr.operatorProcName() let - procNameSnaked = toSnakeCase(procName) + procNameSnaked = toSnakeCase(sym.repr.operatorProcName()) procType = sym.getTypeInst() procParams = procType[0][1 .. ^1].toArgSeq() procReturn = procType[0][0].exportReturnTypeZig() @@ -250,7 +251,7 @@ proc exportProcZig*( apiProcName.add &"{toSnakeCase(owner.getName())}_" for prefix in prefixes: apiProcName.add &"{toSnakeCase(prefix.getName())}_" - procName.add prefix.getName() + procName.add capitalizeAscii(prefix.getName()) apiProcName.add &"{procNameSnaked}" if rename != "": @@ -271,40 +272,43 @@ proc exportCloseObjectZig*() = code.removeSuffix "\n" code.add "};\n\n" -proc exportObjectZig*(sym: NimNode, constructor: NimNode) = - let objName = sym.repr +proc exportObjectZig*( + sym: NimNode, + fields: seq[ObjectField], + constructor: NimNode +) = + let + objName = sym.repr + objFields = sym.objectFields(fields) code.add &"pub const {objName} = extern struct " & "{\n" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - code.add &" {toSnakeCase(property[1].repr)}" - code.add ": " - code.add exportTypeZig(identDefs[^2]) - code.add ",\n" + for field in objFields: + code.add &" {toSnakeCase(field.name)}" + code.add ": " + code.add exportTypeZig(field.typ) + code.add ",\n" code.add "\n" if constructor != nil: exportProcZig(constructor, indent = true, rename = "init") else: code.add " pub fn init(" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - code.add toSnakeCase(property[1].repr) - code.add ": " - code.add exportTypeZig(identDefs[^2]) - code.add ", " + for field in objFields: + code.add toSnakeCase(field.name) + code.add ": " + code.add exportTypeZig(field.typ) + code.add ", " code.removeSuffix ", " code.add ") " code.add objName code.add " {\n" code.add &" return {objName}" & "{\n" - for identDefs in sym.getImpl()[2][2]: - for property in identDefs[0 .. ^3]: - code.add &" .{toSnakeCase(property[1].repr)}" - code.add " = " - code.add &"{toSnakeCase(property[1].repr)}" - code.add ",\n" + for field in objFields: + code.add &" .{toSnakeCase(field.name)}" + code.add " = " + code.add &"{toSnakeCase(field.name)}" + code.add ",\n" code.add " };\n" code.add " }\n\n" diff --git a/tests/generated/external_types.nim b/tests/generated/external_types.nim new file mode 100644 index 0000000..3823124 --- /dev/null +++ b/tests/generated/external_types.nim @@ -0,0 +1,7 @@ +type ExternalObj* {.bycopy.} = object + externalA*: int32 + externalB*: bool + +proc externalObj*(externalA: int32, externalB: bool): ExternalObj = + result.externalA = externalA + result.externalB = externalB diff --git a/tests/generated/internal.nim b/tests/generated/internal.nim index a70eab5..272e703 100644 --- a/tests/generated/internal.nim +++ b/tests/generated/internal.nim @@ -123,6 +123,13 @@ proc test_simple_obj_with_proc_eq*(a, b: SimpleObjWithProc): bool {.raises: [], proc test_simple_obj_with_proc_extra_proc*(s: SimpleObjWithProc) {.raises: [], cdecl, exportc, dynlib.} = extraProc(s) +proc test_external_obj*(external_a: int32, external_b: bool): ExternalObj {.raises: [], cdecl, exportc, dynlib.} = + result.external_a = external_a + result.external_b = external_b + +proc test_external_obj_eq*(a, b: ExternalObj): bool {.raises: [], cdecl, exportc, dynlib.}= + a == b + type SeqString* = ref object s: seq[string] diff --git a/tests/generated/pixie_images/placeholder.txt b/tests/generated/pixie_images/placeholder.txt new file mode 100644 index 0000000..02ab650 --- /dev/null +++ b/tests/generated/pixie_images/placeholder.txt @@ -0,0 +1 @@ +Generated Pixie render outputs are written here during tests. diff --git a/tests/generated/test.h b/tests/generated/test.h index 32244a0..fc37e98 100644 --- a/tests/generated/test.h +++ b/tests/generated/test.h @@ -30,6 +30,11 @@ typedef struct SimpleObjWithProc { char simple_c; } SimpleObjWithProc; +typedef struct ExternalObj { + int32_t external_a; + char external_b; +} ExternalObj; + typedef struct SeqStringHandle* SeqString; #ifdef __cplusplus @@ -103,6 +108,10 @@ char test_simple_obj_with_proc_eq(SimpleObjWithProc a, SimpleObjWithProc b); void test_simple_obj_with_proc_extra_proc(SimpleObjWithProc s); +ExternalObj test_external_obj(int32_t external_a, char external_b); + +char test_external_obj_eq(ExternalObj a, ExternalObj b); + void test_seq_string_unref(SeqString seq_string); SeqString test_new_seq_string(); diff --git a/tests/generated/test.hpp b/tests/generated/test.hpp index bdbc877..8920046 100644 --- a/tests/generated/test.hpp +++ b/tests/generated/test.hpp @@ -22,6 +22,8 @@ struct RefObjWithSeq; struct SimpleObjWithProc; +struct ExternalObj; + struct SeqString; struct GennyBuffer { @@ -120,6 +122,11 @@ struct SimpleObjWithProc { }; +struct ExternalObj { + std::int32_t external_a; + bool external_b; +}; + struct SeqString { private: @@ -206,6 +213,10 @@ bool test_simple_obj_with_proc_eq(SimpleObjWithProc a, SimpleObjWithProc b); void test_simple_obj_with_proc_extra_proc(SimpleObjWithProc s); +ExternalObj test_external_obj(std::int32_t external_a, bool external_b); + +bool test_external_obj_eq(ExternalObj a, ExternalObj b); + void test_seq_string_unref(SeqString seq_string); SeqString test_new_seq_string(); @@ -363,6 +374,10 @@ void SimpleObjWithProc::extraProc() { test_simple_obj_with_proc_extra_proc(*this); }; +ExternalObj externalObj(std::int32_t externalA, bool externalB) { + return test_external_obj(externalA, externalB); +}; + SeqString::SeqString(){ this->reference = test_new_seq_string().reference; } diff --git a/tests/generated/test.js b/tests/generated/test.js index f534678..95eea7f 100644 --- a/tests/generated/test.js +++ b/tests/generated/test.js @@ -202,6 +202,18 @@ function simpleObjWithProcExtraProc(s) { test_simple_obj_with_proc_extra_proc(s); } +const ExternalObj = koffi.struct('ExternalObj', { + externalA: 'int32', + externalB: 'bool' +}); + +function externalObj(external_a, external_b) { + return { + externalA: external_a, + externalB: external_b + }; +} + class SeqString { constructor(ref) { this.ref = ref; @@ -297,6 +309,8 @@ exports.newRefObjWithSeq = newRefObjWithSeq; exports.SimpleObjWithProc = SimpleObjWithProc; exports.simpleObjWithProc = simpleObjWithProc; exports.simpleObjWithProcExtraProc = simpleObjWithProcExtraProc; +exports.ExternalObj = ExternalObj; +exports.externalObj = externalObj; exports.SeqString = SeqString; exports.newSeqString = newSeqString; exports.getDatas = getDatas; diff --git a/tests/generated/test.nim b/tests/generated/test.nim index 64ca5d6..fc8e81d 100644 --- a/tests/generated/test.nim +++ b/tests/generated/test.nim @@ -1,6 +1,6 @@ -import bumpy, chroma, unicode, vmath +import external_types -export bumpy, chroma, unicode, vmath +export external_types when defined(windows): const libName = "test.dll" diff --git a/tests/generated/test.py b/tests/generated/test.py index 444158b..abb01cb 100644 --- a/tests/generated/test.py +++ b/tests/generated/test.py @@ -225,6 +225,19 @@ def __eq__(self, obj): def extra_proc(self): dll.test_simple_obj_with_proc_extra_proc(self) +class ExternalObj(Structure): + _fields_ = [ + ("external_a", c_int), + ("external_b", c_bool) + ] + + def __init__(self, external_a, external_b): + self.external_a = external_a + self.external_b = external_b + + def __eq__(self, obj): + return self.external_a == obj.external_a and self.external_b == obj.external_b + class SeqString(Structure): _fields_ = [("ref", c_ulonglong)] diff --git a/tests/generated/test.zig b/tests/generated/test.zig index 7c108b5..5f827a3 100644 --- a/tests/generated/test.zig +++ b/tests/generated/test.zig @@ -205,6 +205,23 @@ pub const SimpleObjWithProc = extern struct { } }; +pub const ExternalObj = extern struct { + external_a: i32, + external_b: bool, + + pub fn init(external_a: i32, external_b: bool) ExternalObj { + return ExternalObj{ + .external_a = external_a, + .external_b = external_b, + }; + } + + extern fn test_external_obj_eq(self: ExternalObj, other: ExternalObj) bool; + pub inline fn eql(self: ExternalObj, other: ExternalObj) bool { + return test_external_obj_eq(self, other); + } +}; + pub const SeqString = opaque { extern fn test_seq_string_unref(self: *SeqString) void; pub inline fn deinit(self: *SeqString) void { diff --git a/tests/goldens/pixie_render_step1.png b/tests/goldens/pixie_render_step1.png new file mode 100644 index 0000000..e3b65e0 Binary files /dev/null and b/tests/goldens/pixie_render_step1.png differ diff --git a/tests/goldens/pixie_render_step2.png b/tests/goldens/pixie_render_step2.png new file mode 100644 index 0000000..aa8180f Binary files /dev/null and b/tests/goldens/pixie_render_step2.png differ diff --git a/tests/goldens/pixie_render_step3.png b/tests/goldens/pixie_render_step3.png new file mode 100644 index 0000000..b62f2e1 Binary files /dev/null and b/tests/goldens/pixie_render_step3.png differ diff --git a/tests/pixie_python_checks.py b/tests/pixie_python_checks.py index c05cc25..179d195 100644 --- a/tests/pixie_python_checks.py +++ b/tests/pixie_python_checks.py @@ -1,6 +1,8 @@ import os from pathlib import Path +RENDER_OUTPUT_DIR = Path(__file__).resolve().parent / "generated" / "pixie_images" + def pixie_root(): return Path(os.environ.get("PIXIE_ROOT", Path(__file__).resolve().parents[2] / "pixie")) @@ -18,7 +20,55 @@ def matrix_values(matrix): return list(matrix.values) -def run(pixie): +def write_render_step(image, label, step): + RENDER_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + actual_path = RENDER_OUTPUT_DIR / f"{label}_{step}.png" + image.write_file(str(actual_path)) + + +def write_render_images(pixie, label): + image = pixie.Image(32, 32) + image.fill(pixie.parse_color("#112233")) + + orange = pixie.parse_color("#f29e4c") + for y in range(2, 10): + for x in range(2, 10): + image.set_color(x, y, orange) + write_render_step(image, label, "step1") + + rect_paint = pixie.Paint(pixie.SOLID_PAINT) + rect_paint.color = pixie.parse_color("#209cee") + rect_path = pixie.Path() + rect_path.rect(12, 3, 14, 16) + image.fill_path(rect_path, rect_paint, pixie.translate(1, 2), pixie.NON_ZERO) + write_render_step(image, label, "step2") + + circle_paint = pixie.Paint(pixie.SOLID_PAINT) + circle_paint.color = pixie.parse_color("#8ac926") + circle_path = pixie.Path() + circle_path.circle(12, 22, 7) + image.fill_path(circle_path, circle_paint, pixie.translate(0, 0), pixie.NON_ZERO) + + stroke_paint = pixie.Paint(pixie.SOLID_PAINT) + stroke_paint.color = pixie.parse_color("#ffffff") + border_path = pixie.Path() + border_path.rect(0.75, 0.75, 30.5, 30.5) + dashes = pixie.SeqFloat32() + image.stroke_path( + border_path, + stroke_paint, + pixie.translate(0, 0), + 1.5, + pixie.BUTT_CAP, + pixie.MITER_JOIN, + pixie.DEFAULT_MITER_LIMIT, + dashes, + ) + image.set_color(31, 31, pixie.parse_color("#ff00ff")) + write_render_step(image, label, "step3") + + +def run(pixie, label="python"): font_path = asset("tests", "fonts", "Inter-Regular.ttf") image_path = asset("tests", "images", "turtle.png") ppm = "P3\n2 1\n255\n255 0 0 0 255 0\n" @@ -37,6 +87,12 @@ def run(pixie): mat = pixie.translate(3, 4) assert matrix_values(mat)[6] == 3 assert matrix_values(pixie.inverse(mat))[6] == -3 + a = pixie.Vec2(1, 2) + b = pixie.Vec2(3, 4) + assert a + b == pixie.Vec2(4, 6) + assert a * b == pixie.Vec2(3, 8) + assert a * 2.0 == pixie.Vec2(2, 4) + assert mat * a == pixie.Vec2(4, 6) assert pixie.snap_to_pixels(pixie.Rect(1, 2, 3, 4)) == pixie.Rect(1, 2, 3, 4) assert pixie.miter_limit_to_angle(2) > 0 assert pixie.angle_to_miter_limit(1) > 0 @@ -76,7 +132,7 @@ def run(pixie): assert resized.height == 6 assert resized.sub_image(0, 0, 2, 2).width == 2 assert resized.rect_sub_image(pixie.Rect(0, 0, 1, 1)).height == 1 - assert resized.shadow(pixie.Vector2(1, 2), 3, 4, red).width == resized.width + assert resized.shadow(pixie.Vec2(1, 2), 3, 4, red).width == resized.width assert resized.super_image(-1, -1, resized.width + 2, resized.height + 2).width == resized.width + 2 assert resized.opaque_bounds().w > 0 @@ -89,9 +145,9 @@ def run(pixie): assert paint.kind == pixie.LINEAR_GRADIENT_PAINT approx(paint.opacity, 0.5) - paint.gradient_handle_positions.append(pixie.Vector2(0.25, 0.0)) - paint.gradient_handle_positions.append(pixie.Vector2(0.75, 1.0)) - paint.gradient_handle_positions[1] = pixie.Vector2(0.8, 1.0) + paint.gradient_handle_positions.append(pixie.Vec2(0.25, 0.0)) + paint.gradient_handle_positions.append(pixie.Vec2(0.75, 1.0)) + paint.gradient_handle_positions[1] = pixie.Vec2(0.8, 1.0) assert len(paint.gradient_handle_positions) == 2 approx(paint.gradient_handle_positions[1].x, 0.8) @@ -119,9 +175,9 @@ def run(pixie): rect_path = pixie.Path() rect_path.rect(0, 0, 10, 10) solid_dashes = pixie.SeqFloat32() - assert rect_path.fill_overlaps(pixie.Vector2(5, 5), None, pixie.NON_ZERO) + assert rect_path.fill_overlaps(pixie.Vec2(5, 5), None, pixie.NON_ZERO) assert rect_path.stroke_overlaps( - pixie.Vector2(0, 5), None, 2, pixie.BUTT_CAP, pixie.MITER_JOIN, pixie.DEFAULT_MITER_LIMIT, solid_dashes + pixie.Vec2(0, 5), None, 2, pixie.BUTT_CAP, pixie.MITER_JOIN, pixie.DEFAULT_MITER_LIMIT, solid_dashes ) typeface = pixie.read_typeface(font_path) @@ -150,13 +206,13 @@ def run(pixie): assert font.scale() > 0 assert font.default_line_height() > 0 assert font.layout_bounds("abcd").x > 0 - assert font.typeset("abcd", pixie.Vector2(100, 100), pixie.LEFT_ALIGN, pixie.TOP_ALIGN, True).layout_bounds().x > 0 + assert font.typeset("abcd", pixie.Vec2(100, 100), pixie.LEFT_ALIGN, pixie.TOP_ALIGN, True).layout_bounds().x > 0 span = pixie.Span("hi", font) span.text = "hello" spans = pixie.SeqSpan() spans.append(span) - arrangement = spans.typeset(pixie.Vector2(100, 100), pixie.CENTER_ALIGN, pixie.BOTTOM_ALIGN, True) + arrangement = spans.typeset(pixie.Vec2(100, 100), pixie.CENTER_ALIGN, pixie.BOTTOM_ALIGN, True) assert spans[0].text == "hello" assert arrangement.layout_bounds().x > 0 assert spans.layout_bounds().y > 0 @@ -164,10 +220,10 @@ def run(pixie): canvas = pixie.Image(64, 64) canvas.fill(pixie.parse_color("#ffffff")) - canvas.fill_text(font, "abc", mat, pixie.Vector2(60, 60), pixie.LEFT_ALIGN, pixie.TOP_ALIGN) + canvas.fill_text(font, "abc", mat, pixie.Vec2(60, 60), pixie.LEFT_ALIGN, pixie.TOP_ALIGN) canvas.arrangement_fill_text(arrangement, mat) canvas.stroke_text( - font, "abc", mat, 2, pixie.Vector2(60, 60), pixie.LEFT_ALIGN, pixie.TOP_ALIGN, + font, "abc", mat, 2, pixie.Vec2(60, 60), pixie.LEFT_ALIGN, pixie.TOP_ALIGN, pixie.BUTT_CAP, pixie.MITER_JOIN, pixie.DEFAULT_MITER_LIMIT, dashes ) canvas.arrangement_stroke_text(arrangement, mat, 2, pixie.BUTT_CAP, pixie.MITER_JOIN, pixie.DEFAULT_MITER_LIMIT, dashes) @@ -240,6 +296,7 @@ def run(pixie): assert pixie.read_image_dimensions(image_path).height == 40 assert pixie.read_font(font_path).size == 12 assert pixie.parse_path("M0 0 L10 0 L10 10 Z").compute_bounds().w == 10 + write_render_images(pixie, label) try: pixie.parse_color("bad") diff --git a/tests/test.nim b/tests/test.nim index ba8e3cb..36a4b5a 100644 --- a/tests/test.nim +++ b/tests/test.nim @@ -1,4 +1,4 @@ -import genny +import genny, generated/external_types const simpleConst = 123 @@ -78,6 +78,9 @@ exportObject SimpleObjWithProc: procs: extraProc +exportObject external_types.ExternalObj: + discard + # type ArrayObj = object # arr1*: array[3, int] # arr2*: array[3, array[3, int]] diff --git a/tests/test_nim.nim b/tests/test_nim.nim index f81e26a..9ee31c9 100644 --- a/tests/test_nim.nim +++ b/tests/test_nim.nim @@ -61,6 +61,11 @@ let objWithProc = simpleObjWithProc(1, 2, false) doAssert objWithProc.simpleA == 1, "simpleA should be 1" objWithProc.extraProc() +echo "Testing external value object import" +let external = externalObj(7, true) +doAssert external.externalA == 7, "externalA should be 7" +doAssert external.externalB == true, "externalB should be true" + echo "Testing getMessage" doAssert getMessage() == "alpha\0omega", "getMessage should preserve embedded NUL" diff --git a/tests/test_no_gold_diff.nim b/tests/test_no_gold_diff.nim new file mode 100644 index 0000000..b42a6fc --- /dev/null +++ b/tests/test_no_gold_diff.nim @@ -0,0 +1,70 @@ +import os, strformat, strutils +import pixie + +const + renderOutputDir = "tests" / "generated" / "pixie_images" + goldenDir = "tests" / "goldens" + maxChannelDelta = 0.02'f32 + maxAvgDelta = 0.002'f64 + +proc fail(message: string) = + stderr.writeLine(message) + quit(1) + +proc recordDelta(delta: float32; totalDelta: var float64; maxDelta: var float32) = + totalDelta += delta.float64 + if delta > maxDelta: + maxDelta = delta + +proc compareImages(actualPath, goldenPath: string) = + if not fileExists(goldenPath): + fail(&"missing golden image for {actualPath}: {goldenPath}") + + let + actual = readImage(actualPath) + golden = readImage(goldenPath) + + if actual.width != golden.width or actual.height != golden.height: + fail(&"{actualPath} dimensions {actual.width}x{actual.height} differ from {goldenPath} {golden.width}x{golden.height}") + + var + totalDelta = 0'f64 + maxDelta = 0'f32 + for y in 0 ..< actual.height: + for x in 0 ..< actual.width: + let + actualColor = actual.getColor(x, y) + goldenColor = golden.getColor(x, y) + recordDelta(abs(actualColor.r - goldenColor.r), totalDelta, maxDelta) + recordDelta(abs(actualColor.g - goldenColor.g), totalDelta, maxDelta) + recordDelta(abs(actualColor.b - goldenColor.b), totalDelta, maxDelta) + recordDelta(abs(actualColor.a - goldenColor.a), totalDelta, maxDelta) + + let avgDelta = totalDelta / float64(actual.width * actual.height * 4) + if maxDelta > maxChannelDelta or avgDelta > maxAvgDelta: + fail(&"{actualPath} differs from {goldenPath}: max delta {maxDelta}, avg delta {avgDelta}") + +proc goldenFor(actualPath: string): string = + let filename = actualPath.extractFilename() + if not filename.endsWith(".png"): + fail(&"not a PNG render output: {actualPath}") + + let stepStart = filename.rfind("_step") + if stepStart < 0: + fail(&"render output does not include a _step suffix: {actualPath}") + + let step = filename[stepStart + 1 .. filename.len - ".png".len - 1] + result = goldenDir / ("pixie_render_" & step & ".png") + +if not dirExists(renderOutputDir): + fail(&"missing Pixie render output directory: {renderOutputDir}") + +var checked = 0 +for actualPath in walkFiles(renderOutputDir / "*.png"): + compareImages(actualPath, goldenFor(actualPath)) + inc checked + +if checked == 0: + fail(&"no Pixie render output images found in {renderOutputDir}") + +echo &"Pixie render gold diff passed for {checked} image(s)." diff --git a/tests/test_pixie_c.c b/tests/test_pixie_c.c index b40814a..9ab14af 100644 --- a/tests/test_pixie_c.c +++ b/tests/test_pixie_c.c @@ -3,6 +3,11 @@ #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif #include "pixie.h" #ifndef PIXIE_ROOT @@ -11,6 +16,7 @@ #define FONT_PATH PIXIE_ROOT "/tests/fonts/Inter-Regular.ttf" #define IMAGE_PATH PIXIE_ROOT "/tests/images/turtle.png" +#define PIXIE_RENDER_DIR "tests/generated/pixie_images" static void approx(float value, float expected) { assert(fabsf(value - expected) < 0.0001f); @@ -47,6 +53,71 @@ static void assert_buffer_equals(GennyBuffer buffer, const char *expected) { pixie_genny_buffer_unref(buffer); } +static void ensure_render_output_dir(void) { +#ifdef _WIN32 + _mkdir("tests/generated"); + _mkdir(PIXIE_RENDER_DIR); +#else + mkdir("tests/generated", 0777); + mkdir(PIXIE_RENDER_DIR, 0777); +#endif +} + +static void write_render_step(Image image, const char *label, const char *step) { + char actual_path[256]; + snprintf(actual_path, sizeof(actual_path), "%s/%s_%s.png", PIXIE_RENDER_DIR, label, step); + pixie_image_write_file(image, actual_path); + assert(!pixie_check_error()); +} + +static void write_render_images(const char *label) { + ensure_render_output_dir(); + + Image image = pixie_new_image(32, 32); + pixie_image_fill(image, pixie_parse_color("#112233")); + Color orange = pixie_parse_color("#f29e4c"); + for (intptr_t y = 2; y < 10; y++) { + for (intptr_t x = 2; x < 10; x++) { + pixie_image_set_color(image, x, y, orange); + } + } + write_render_step(image, label, "step1"); + + Paint rect_paint = pixie_new_paint(SOLID_PAINT); + pixie_paint_set_color(rect_paint, pixie_parse_color("#209cee")); + Path rect_path = pixie_new_path(); + pixie_path_rect(rect_path, 12, 3, 14, 16, 1); + pixie_image_fill_path(image, rect_path, rect_paint, pixie_translate(1, 2), NON_ZERO); + assert(!pixie_check_error()); + write_render_step(image, label, "step2"); + + Paint circle_paint = pixie_new_paint(SOLID_PAINT); + pixie_paint_set_color(circle_paint, pixie_parse_color("#8ac926")); + Path circle_path = pixie_new_path(); + pixie_path_circle(circle_path, 12, 22, 7); + pixie_image_fill_path(image, circle_path, circle_paint, pixie_translate(0, 0), NON_ZERO); + assert(!pixie_check_error()); + + Paint stroke_paint = pixie_new_paint(SOLID_PAINT); + pixie_paint_set_color(stroke_paint, pixie_parse_color("#ffffff")); + Path border_path = pixie_new_path(); + pixie_path_rect(border_path, 0.75f, 0.75f, 30.5f, 30.5f, 1); + SeqFloat32 dashes = pixie_new_seq_float32(); + pixie_image_stroke_path(image, border_path, stroke_paint, pixie_translate(0, 0), 1.5f, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, dashes); + assert(!pixie_check_error()); + pixie_image_set_color(image, 31, 31, pixie_parse_color("#ff00ff")); + write_render_step(image, label, "step3"); + + pixie_seq_float32_unref(dashes); + pixie_path_unref(border_path); + pixie_paint_unref(stroke_paint); + pixie_path_unref(circle_path); + pixie_paint_unref(circle_paint); + pixie_path_unref(rect_path); + pixie_paint_unref(rect_paint); + pixie_image_unref(image); +} + int main() { const char *ppm = "P3\n2 1\n255\n255 0 0 0 255 0\n"; @@ -61,10 +132,24 @@ int main() { approx(mixed.r, 0.75f); approx(mixed.g, 0.25f); - Matrix3 mat = pixie_translate(3, 4); - Matrix3 identity = pixie_translate(0, 0); + Mat3 mat = pixie_translate(3, 4); + Mat3 identity = pixie_translate(0, 0); assert(mat.values[6] == 3); assert(pixie_inverse(mat).values[6] == -3); + Vec2 a = pixie_vec2(1, 2); + Vec2 b = pixie_vec2(3, 4); + Vec2 sum = pixie_vec2_add(a, b); + assert(sum.x == 4); + assert(sum.y == 6); + Vec2 product = pixie_vec2_mul(a, b); + assert(product.x == 3); + assert(product.y == 8); + Vec2 scaled = pixie_vec2_float32_mul(a, 2.0f); + assert(scaled.x == 2); + assert(scaled.y == 4); + Vec2 moved = pixie_mat3_vec2_mul(mat, a); + assert(moved.x == 4); + assert(moved.y == 6); assert(pixie_rect_eq(pixie_snap_to_pixels(pixie_rect(1, 2, 3, 4)), pixie_rect(1, 2, 3, 4))); assert(pixie_miter_limit_to_angle(2) > 0); assert(pixie_angle_to_miter_limit(1) > 0); @@ -105,7 +190,7 @@ int main() { assert(pixie_image_get_height(resized) == 6); assert(pixie_image_get_width(pixie_image_sub_image(resized, 0, 0, 2, 2)) == 2); assert(pixie_image_get_height(pixie_image_rect_sub_image(resized, pixie_rect(0, 0, 1, 1))) == 1); - assert(pixie_image_get_width(pixie_image_shadow(resized, pixie_vector2(1, 2), 3, 4, red)) == pixie_image_get_width(resized)); + assert(pixie_image_get_width(pixie_image_shadow(resized, pixie_vec2(1, 2), 3, 4, red)) == pixie_image_get_width(resized)); assert(pixie_image_get_width(pixie_image_super_image(resized, -1, -1, pixie_image_get_width(resized) + 2, pixie_image_get_height(resized) + 2)) == pixie_image_get_width(resized) + 2); assert(pixie_image_opaque_bounds(resized).w > 0); @@ -117,9 +202,9 @@ int main() { pixie_paint_set_image_mat(paint, pixie_scale(2, 3)); assert(pixie_paint_get_kind(paint) == LINEAR_GRADIENT_PAINT); approx(pixie_paint_get_opacity(paint), 0.5f); - pixie_paint_gradient_handle_positions_add(paint, pixie_vector2(0.25f, 0)); - pixie_paint_gradient_handle_positions_add(paint, pixie_vector2(0.75f, 1)); - pixie_paint_gradient_handle_positions_set(paint, 1, pixie_vector2(0.8f, 1)); + pixie_paint_gradient_handle_positions_add(paint, pixie_vec2(0.25f, 0)); + pixie_paint_gradient_handle_positions_add(paint, pixie_vec2(0.75f, 1)); + pixie_paint_gradient_handle_positions_set(paint, 1, pixie_vec2(0.8f, 1)); assert(pixie_paint_gradient_handle_positions_len(paint) == 2); approx(pixie_paint_gradient_handle_positions_get(paint, 1).x, 0.8f); pixie_paint_gradient_stops_add(paint, pixie_color_stop(red, 0)); @@ -146,8 +231,8 @@ int main() { Path rect_path = pixie_new_path(); pixie_path_rect(rect_path, 0, 0, 10, 10, 1); SeqFloat32 solid_dashes = pixie_new_seq_float32(); - assert(pixie_path_fill_overlaps(rect_path, pixie_vector2(5, 5), identity, NON_ZERO)); - assert(pixie_path_stroke_overlaps(rect_path, pixie_vector2(0, 5), identity, 2, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, solid_dashes)); + assert(pixie_path_fill_overlaps(rect_path, pixie_vec2(5, 5), identity, NON_ZERO)); + assert(pixie_path_stroke_overlaps(rect_path, pixie_vec2(0, 5), identity, 2, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, solid_dashes)); Typeface typeface = pixie_read_typeface(FONT_PATH); assert_buffer_contains(pixie_typeface_get_file_path(typeface), "Inter-Regular.ttf"); @@ -169,13 +254,13 @@ int main() { assert(pixie_font_scale(font) > 0); assert(pixie_font_default_line_height(font) > 0); assert(pixie_font_layout_bounds(font, "abcd").x > 0); - assert(pixie_arrangement_layout_bounds(pixie_font_typeset(font, "abcd", pixie_vector2(100, 100), LEFT_ALIGN, TOP_ALIGN, 1)).x > 0); + assert(pixie_arrangement_layout_bounds(pixie_font_typeset(font, "abcd", pixie_vec2(100, 100), LEFT_ALIGN, TOP_ALIGN, 1)).x > 0); Span span = pixie_new_span("hi", font); pixie_span_set_text(span, "hello"); SeqSpan spans = pixie_new_seq_span(); pixie_seq_span_add(spans, span); - Arrangement arrangement = pixie_seq_span_typeset(spans, pixie_vector2(100, 100), CENTER_ALIGN, BOTTOM_ALIGN, 1); + Arrangement arrangement = pixie_seq_span_typeset(spans, pixie_vec2(100, 100), CENTER_ALIGN, BOTTOM_ALIGN, 1); assert_buffer_equals(pixie_span_get_text(pixie_seq_span_get(spans, 0)), "hello"); assert(pixie_arrangement_layout_bounds(arrangement).x > 0); assert(pixie_seq_span_layout_bounds(spans).y > 0); @@ -183,9 +268,9 @@ int main() { Image canvas = pixie_new_image(64, 64); pixie_image_fill(canvas, pixie_parse_color("#ffffff")); - pixie_image_fill_text(canvas, font, "abc", mat, pixie_vector2(60, 60), LEFT_ALIGN, TOP_ALIGN); + pixie_image_fill_text(canvas, font, "abc", mat, pixie_vec2(60, 60), LEFT_ALIGN, TOP_ALIGN); pixie_image_arrangement_fill_text(canvas, arrangement, mat); - pixie_image_stroke_text(canvas, font, "abc", mat, 2, pixie_vector2(60, 60), LEFT_ALIGN, TOP_ALIGN, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, dashes); + pixie_image_stroke_text(canvas, font, "abc", mat, 2, pixie_vec2(60, 60), LEFT_ALIGN, TOP_ALIGN, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, dashes); pixie_image_arrangement_stroke_text(canvas, arrangement, mat, 2, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, dashes); pixie_image_fill_path(canvas, rect_path, solid, mat, NON_ZERO); pixie_image_stroke_path(canvas, rect_path, solid, mat, 2, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, dashes); @@ -258,6 +343,7 @@ int main() { assert(pixie_read_image_dimensions(IMAGE_PATH).height == 40); approx(pixie_font_get_size(pixie_read_font(FONT_PATH)), 12); assert(pixie_path_compute_bounds(pixie_parse_path("M0 0 L10 0 L10 10 Z"), identity).w == 10); + write_render_images("c"); pixie_parse_color("bad"); assert(pixie_check_error()); assert_buffer_contains(pixie_take_error(), "bad"); diff --git a/tests/test_pixie_cpp.cpp b/tests/test_pixie_cpp.cpp index 298d6df..286dc3c 100644 --- a/tests/test_pixie_cpp.cpp +++ b/tests/test_pixie_cpp.cpp @@ -3,6 +3,11 @@ #include #include #include +#ifdef _WIN32 +#include +#else +#include +#endif #include "pixie.hpp" #ifndef PIXIE_ROOT @@ -12,6 +17,8 @@ #define FONT_PATH PIXIE_ROOT "/tests/fonts/Inter-Regular.ttf" #define IMAGE_PATH PIXIE_ROOT "/tests/images/turtle.png" +static const std::string renderOutputDir = "tests/generated/pixie_images"; + static void approx(float value, float expected, float eps = 0.0001f) { assert(std::fabs(value - expected) <= eps); } @@ -23,6 +30,57 @@ static void assertColor(Color actual, Color expected) { approx(actual.a, expected.a); } +static void ensureRenderOutputDir() { +#ifdef _WIN32 + _mkdir("tests/generated"); + _mkdir(renderOutputDir.c_str()); +#else + mkdir("tests/generated", 0777); + mkdir(renderOutputDir.c_str(), 0777); +#endif +} + +static void writeRenderStep(Image &image, const std::string &label, const std::string &step) { + std::string actualPath = renderOutputDir + "/" + label + "_" + step + ".png"; + image.writeFile(actualPath.c_str()); +} + +static void writeRenderImages(const std::string &label) { + ensureRenderOutputDir(); + + Image image(32, 32); + image.fill(parseColor("#112233")); + Color orange = parseColor("#f29e4c"); + for (int y = 2; y < 10; y++) { + for (int x = 2; x < 10; x++) { + image.setColor(x, y, orange); + } + } + writeRenderStep(image, label, "step1"); + + Paint rectPaint(SOLID_PAINT); + rectPaint.setColor(parseColor("#209cee")); + Path rectPath; + rectPath.rect(12, 3, 14, 16, true); + image.fillPath(rectPath, rectPaint, translate(1, 2), NON_ZERO); + writeRenderStep(image, label, "step2"); + + Paint circlePaint(SOLID_PAINT); + circlePaint.setColor(parseColor("#8ac926")); + Path circlePath; + circlePath.circle(12, 22, 7); + image.fillPath(circlePath, circlePaint, translate(0, 0), NON_ZERO); + + Paint strokePaint(SOLID_PAINT); + strokePaint.setColor(parseColor("#ffffff")); + Path borderPath; + borderPath.rect(0.75f, 0.75f, 30.5f, 30.5f, true); + SeqFloat32 dashes; + image.strokePath(borderPath, strokePaint, translate(0, 0), 1.5f, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, dashes); + image.setColor(31, 31, parseColor("#ff00ff")); + writeRenderStep(image, label, "step3"); +} + int main() { const char *ppm = "P3\n2 1\n255\n255 0 0 0 255 0\n"; @@ -37,10 +95,24 @@ int main() { approx(mixed.r, 0.75f); approx(mixed.g, 0.25f); - Matrix3 mat = translate(3, 4); - Matrix3 identity = translate(0, 0); + Mat3 mat = translate(3, 4); + Mat3 identity = translate(0, 0); assert(mat.values[6] == 3); assert(inverse(mat).values[6] == -3); + Vec2 a = vec2(1, 2); + Vec2 b = vec2(3, 4); + Vec2 sum = a + b; + assert(sum.x == 4); + assert(sum.y == 6); + Vec2 product = a * b; + assert(product.x == 3); + assert(product.y == 8); + Vec2 scaled = a * 2.0f; + assert(scaled.x == 2); + assert(scaled.y == 4); + Vec2 moved = mat * a; + assert(moved.x == 4); + assert(moved.y == 6); assert(pixie_rect_eq(snapToPixels(rect(1, 2, 3, 4)), rect(1, 2, 3, 4))); assert(miterLimitToAngle(2) > 0); assert(angleToMiterLimit(1) > 0); @@ -80,7 +152,7 @@ int main() { assert(resized.getHeight() == 6); assert(resized.subImage(0, 0, 2, 2).getWidth() == 2); assert(resized.subImage(rect(0, 0, 1, 1)).getHeight() == 1); - assert(resized.shadow(vector2(1, 2), 3, 4, red).getWidth() == resized.getWidth()); + assert(resized.shadow(vec2(1, 2), 3, 4, red).getWidth() == resized.getWidth()); assert(resized.superImage(-1, -1, resized.getWidth() + 2, resized.getHeight() + 2).getWidth() == resized.getWidth() + 2); assert(resized.opaqueBounds().w > 0); @@ -92,9 +164,9 @@ int main() { paint.setImageMat(scale(2, 3)); assert(paint.getKind() == LINEAR_GRADIENT_PAINT); approx(paint.getOpacity(), 0.5f); - paint.addGradientHandlePositions(vector2(0.25f, 0)); - paint.addGradientHandlePositions(vector2(0.75f, 1)); - paint.setGradientHandlePositions(1, vector2(0.8f, 1)); + paint.addGradientHandlePositions(vec2(0.25f, 0)); + paint.addGradientHandlePositions(vec2(0.75f, 1)); + paint.setGradientHandlePositions(1, vec2(0.8f, 1)); assert(paint.gradientHandlePositionsSize() == 2); approx(paint.getGradientHandlePositions(1).x, 0.8f); paint.addGradientStops(colorStop(red, 0)); @@ -121,8 +193,8 @@ int main() { Path rectPath; rectPath.rect(0, 0, 10, 10, true); SeqFloat32 solidDashes; - assert(rectPath.fillOverlaps(vector2(5, 5), identity, NON_ZERO)); - assert(rectPath.strokeOverlaps(vector2(0, 5), identity, 2, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, solidDashes)); + assert(rectPath.fillOverlaps(vec2(5, 5), identity, NON_ZERO)); + assert(rectPath.strokeOverlaps(vec2(0, 5), identity, 2, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, solidDashes)); Typeface typeface = readTypeface(FONT_PATH); assert(typeface.getFilePath().find("Inter-Regular.ttf") != std::string::npos); @@ -144,13 +216,13 @@ int main() { assert(font.scale() > 0); assert(font.defaultLineHeight() > 0); assert(font.layoutBounds("abcd").x > 0); - assert(font.typeset("abcd", vector2(100, 100), LEFT_ALIGN, TOP_ALIGN, true).layoutBounds().x > 0); + assert(font.typeset("abcd", vec2(100, 100), LEFT_ALIGN, TOP_ALIGN, true).layoutBounds().x > 0); Span span("hi", font); span.setText("hello"); SeqSpan spans; spans.add(span); - Arrangement arrangement = spans.typeset(vector2(100, 100), CENTER_ALIGN, BOTTOM_ALIGN, true); + Arrangement arrangement = spans.typeset(vec2(100, 100), CENTER_ALIGN, BOTTOM_ALIGN, true); assert(spans[0].getText() == "hello"); assert(arrangement.layoutBounds().x > 0); assert(spans.layoutBounds().y > 0); @@ -158,9 +230,9 @@ int main() { Image canvas(64, 64); canvas.fill(parseColor("#ffffff")); - canvas.fillText(font, "abc", mat, vector2(60, 60), LEFT_ALIGN, TOP_ALIGN); + canvas.fillText(font, "abc", mat, vec2(60, 60), LEFT_ALIGN, TOP_ALIGN); canvas.fillText(arrangement, mat); - canvas.strokeText(font, "abc", mat, 2, vector2(60, 60), LEFT_ALIGN, TOP_ALIGN, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, dashes); + canvas.strokeText(font, "abc", mat, 2, vec2(60, 60), LEFT_ALIGN, TOP_ALIGN, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, dashes); canvas.strokeText(arrangement, mat, 2, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, dashes); canvas.fillPath(rectPath, solid, mat, NON_ZERO); canvas.strokePath(rectPath, solid, mat, 2, BUTT_CAP, MITER_JOIN, DEFAULT_MITER_LIMIT, dashes); @@ -233,6 +305,7 @@ int main() { assert(readImageDimensions(IMAGE_PATH).height == 40); approx(readFont(FONT_PATH).getSize(), 12); assert(parsePath("M0 0 L10 0 L10 10 Z").computeBounds(identity).w == 10); + writeRenderImages("cpp"); parseColor("bad"); assert(checkError()); assert(takeError().find("bad") != std::string::npos); diff --git a/tests/test_pixie_nim.nim b/tests/test_pixie_nim.nim index 99cbf09..ead2404 100644 --- a/tests/test_pixie_nim.nim +++ b/tests/test_pixie_nim.nim @@ -1,9 +1,50 @@ import os, strutils import ../../pixie/bindings/generated/pixie +const + renderOutputDir = "tests" / "generated" / "pixie_images" + proc approx(value, expected: float32; eps: float32 = 0.0001) = doAssert abs(value - expected) <= eps +proc writeRenderStep(image: Image; label, step: string) = + createDir(renderOutputDir) + let + actualPath = renderOutputDir / (label & "_" & step & ".png") + image.writeFile(actualPath) + +proc writeRenderImages(label: string) = + let image = newImage(32, 32) + image.fill(parseColor("#112233")) + + let orange = parseColor("#f29e4c") + for y in 2 ..< 10: + for x in 2 ..< 10: + image.setColor(x, y, orange) + writeRenderStep(image, label, "step1") + + let rectPaint = newPaint(SolidPaint) + rectPaint.color = parseColor("#209cee") + let rectPath = newPath() + rectPath.rect(12, 3, 14, 16, true) + image.fillPath(rectPath, rectPaint, translate(1, 2), NonZero) + writeRenderStep(image, label, "step2") + + let circlePaint = newPaint(SolidPaint) + circlePaint.color = parseColor("#8ac926") + let circlePath = newPath() + circlePath.circle(12, 22, 7) + image.fillPath(circlePath, circlePaint, translate(0, 0), NonZero) + + let strokePaint = newPaint(SolidPaint) + strokePaint.color = parseColor("#ffffff") + let borderPath = newPath() + borderPath.rect(0.75, 0.75, 30.5, 30.5, true) + let dashes = newSeqFloat32() + image.strokePath(borderPath, strokePaint, translate(0, 0), 1.5, ButtCap, MiterJoin, defaultMiterLimit, dashes) + image.setColor(31, 31, parseColor("#ff00ff")) + writeRenderStep(image, label, "step3") + let pixieRoot = getEnv("PIXIE_ROOT", "../pixie") fontPath = pixieRoot / "tests" / "fonts" / "Inter-Regular.ttf" @@ -22,8 +63,15 @@ let let mat = translate(3, 4) identity = translate(0, 0) -doAssert mat.values[6] == 3 -doAssert inverse(mat).values[6] == -3 +doAssert mat[2, 0] == 3 +doAssert inverse(mat)[2, 0] == -3 +let + a = vec2(1, 2) + b = vec2(3, 4) +doAssert a + b == vec2(4, 6) +doAssert a * b == vec2(3, 8) +doAssert a * 2'f32 == vec2(2, 4) +doAssert mat * a == vec2(4, 6) doAssert snapToPixels(rect(1, 2, 3, 4)) == rect(1, 2, 3, 4) doAssert miterLimitToAngle(2) > 0 doAssert angleToMiterLimit(1) > 0 @@ -63,7 +111,7 @@ doAssert resized.width == 5 doAssert resized.height == 6 doAssert resized.subImage(0, 0, 2, 2).width == 2 doAssert resized.subImage(rect(0, 0, 1, 1)).height == 1 -doAssert resized.shadow(vector2(1, 2), 3, 4, red).width == resized.width +doAssert resized.shadow(vec2(1, 2), 3, 4, red).width == resized.width doAssert resized.superImage(-1, -1, resized.width + 2, resized.height + 2).width == resized.width + 2 doAssert resized.opaqueBounds().w > 0 @@ -75,9 +123,9 @@ paint.color = green paint.imageMat = scale(2, 3) doAssert paint.kind == LinearGradientPaint approx(paint.opacity, 0.5) -paint.gradientHandlePositions.add(vector2(0.25, 0)) -paint.gradientHandlePositions.add(vector2(0.75, 1)) -paint.gradientHandlePositions[1] = vector2(0.8, 1) +paint.gradientHandlePositions.add(vec2(0.25, 0)) +paint.gradientHandlePositions.add(vec2(0.75, 1)) +paint.gradientHandlePositions[1] = vec2(0.8, 1) doAssert paint.gradientHandlePositions.len == 2 approx(paint.gradientHandlePositions[1].x, 0.8) paint.gradientStops.add(colorStop(red, 0)) @@ -104,8 +152,8 @@ doAssert pathShape.computeBounds(identity).w > 0 let rectPath = newPath() rectPath.rect(0, 0, 10, 10, true) let solidDashes = newSeqFloat32() -doAssert rectPath.fillOverlaps(vector2(5, 5), identity, NonZero) -doAssert rectPath.strokeOverlaps(vector2(0, 5), identity, 2, ButtCap, MiterJoin, defaultMiterLimit, solidDashes) +doAssert rectPath.fillOverlaps(vec2(5, 5), identity, NonZero) +doAssert rectPath.strokeOverlaps(vec2(0, 5), identity, 2, ButtCap, MiterJoin, defaultMiterLimit, solidDashes) let typeface = readTypeface(fontPath) doAssert ($typeface.filePath).endsWith("Inter-Regular.ttf") @@ -127,13 +175,13 @@ doAssert font.paints.len >= 1 doAssert font.scale() > 0 doAssert font.defaultLineHeight() > 0 doAssert font.layoutBounds("abcd").x > 0 -doAssert font.typeset("abcd", vector2(100, 100), LeftAlign, TopAlign, true).layoutBounds().x > 0 +doAssert font.typeset("abcd", vec2(100, 100), LeftAlign, TopAlign, true).layoutBounds().x > 0 let span = newSpan("hi", font) span.text = "hello" let spans = newSeqSpan() spans.add(span) -let arrangement = spans.typeset(vector2(100, 100), CenterAlign, BottomAlign, true) +let arrangement = spans.typeset(vec2(100, 100), CenterAlign, BottomAlign, true) doAssert $spans[0].text == "hello" doAssert arrangement.layoutBounds().x > 0 doAssert spans.layoutBounds().y > 0 @@ -141,9 +189,9 @@ doAssert arrangement.computeBounds(mat).x > 0 let canvas = newImage(64, 64) canvas.fill(parseColor("#ffffff")) -canvas.fillText(font, "abc", mat, vector2(60, 60), LeftAlign, TopAlign) +canvas.fillText(font, "abc", mat, vec2(60, 60), LeftAlign, TopAlign) canvas.fillText(arrangement, mat) -canvas.strokeText(font, "abc", mat, 2, vector2(60, 60), LeftAlign, TopAlign, ButtCap, MiterJoin, defaultMiterLimit, dashes) +canvas.strokeText(font, "abc", mat, 2, vec2(60, 60), LeftAlign, TopAlign, ButtCap, MiterJoin, defaultMiterLimit, dashes) canvas.strokeText(arrangement, mat, 2, ButtCap, MiterJoin, defaultMiterLimit, dashes) canvas.fillPath(rectPath, solid, mat, NonZero) canvas.strokePath(rectPath, solid, mat, 2, ButtCap, MiterJoin, defaultMiterLimit, dashes) @@ -160,7 +208,7 @@ ctx.textAlign = RightAlign doAssert ctx.textAlign == RightAlign doAssert ctx.measureText("abcd").width > 0 ctx.setTransform(mat) -doAssert ctx.getTransform().values[6] == 3 +doAssert ctx.getTransform()[2, 0] == 3 ctx.transform(scale(2, 2)) ctx.resetTransform() ctx.setLineDash(solidDashes) @@ -214,6 +262,7 @@ doAssert readImage(imagePath).width == 40 doAssert readImageDimensions(imagePath).height == 40 approx(readFont(fontPath).size, 12) doAssert parsePath("M0 0 L10 0 L10 10 Z").computeBounds(identity).w == 10 +writeRenderImages("nim") try: discard parseColor("bad") doAssert false diff --git a/tests/test_pixie_node.js b/tests/test_pixie_node.js index acda31f..24bdcf8 100644 --- a/tests/test_pixie_node.js +++ b/tests/test_pixie_node.js @@ -1,4 +1,5 @@ const assert = require('assert'); +const fs = require('fs'); const Module = require('module'); const path = require('path'); @@ -10,6 +11,7 @@ Module._initPaths(); const pixieRoot = path.resolve(process.env.PIXIE_ROOT || path.join(__dirname, '..', '..', 'pixie')); const bindingsDir = path.resolve(process.env.PIXIE_BINDINGS_DIR || path.join(pixieRoot, 'bindings', 'generated')); const pixie = require(path.join(bindingsDir, 'pixie.js')); +const renderOutputDir = path.join(__dirname, 'generated', 'pixie_images'); function asset(...parts) { return path.join(pixieRoot, ...parts); @@ -26,6 +28,47 @@ function assertColor(actual, expected) { approx(actual.a, expected.a); } +function writeRenderStep(image, label, step) { + fs.mkdirSync(renderOutputDir, { recursive: true }); + const actualPath = path.join(renderOutputDir, `${label}_${step}.png`); + image.writeFile(actualPath); +} + +function writeRenderImages(label) { + const render = pixie.newImage(32, 32); + render.fill(pixie.parseColor('#112233')); + + const orange = pixie.parseColor('#f29e4c'); + for (let y = 2; y < 10; y++) { + for (let x = 2; x < 10; x++) { + render.setColor(x, y, orange); + } + } + writeRenderStep(render, label, 'step1'); + + const rectPaint = pixie.newPaint(pixie.SOLID_PAINT); + rectPaint.color = pixie.parseColor('#209cee'); + const rectPath = pixie.newPath(); + rectPath.rect(12, 3, 14, 16, true); + render.fillPath(rectPath, rectPaint, pixie.translate(1, 2), pixie.NON_ZERO); + writeRenderStep(render, label, 'step2'); + + const circlePaint = pixie.newPaint(pixie.SOLID_PAINT); + circlePaint.color = pixie.parseColor('#8ac926'); + const circlePath = pixie.newPath(); + circlePath.circle(12, 22, 7); + render.fillPath(circlePath, circlePaint, pixie.translate(0, 0), pixie.NON_ZERO); + + const strokePaint = pixie.newPaint(pixie.SOLID_PAINT); + strokePaint.color = pixie.parseColor('#ffffff'); + const borderPath = pixie.newPath(); + borderPath.rect(0.75, 0.75, 30.5, 30.5, true); + const dashes = pixie.newSeqFloat32(); + render.strokePath(borderPath, strokePaint, pixie.translate(0, 0), 1.5, pixie.BUTT_CAP, pixie.MITER_JOIN, pixie.DEFAULT_MITER_LIMIT, dashes); + render.setColor(31, 31, pixie.parseColor('#ff00ff')); + writeRenderStep(render, label, 'step3'); +} + const fontPath = asset('tests', 'fonts', 'Inter-Regular.ttf'); const imagePath = asset('tests', 'images', 'turtle.png'); const ppm = 'P3\n2 1\n255\n255 0 0 0 255 0\n'; @@ -45,6 +88,12 @@ const mat = pixie.translate(3, 4); const identity = pixie.translate(0, 0); assert.strictEqual(mat.values[6], 3); assert.strictEqual(pixie.inverse(mat).values[6], -3); +const a = pixie.vec2(1, 2); +const b = pixie.vec2(3, 4); +assert.deepStrictEqual(pixie.vec2Add(a, b), pixie.vec2(4, 6)); +assert.deepStrictEqual(pixie.vec2Mul(a, b), pixie.vec2(3, 8)); +assert.deepStrictEqual(pixie.vec2Float32Mul(a, 2), pixie.vec2(2, 4)); +assert.deepStrictEqual(pixie.mat3Vec2Mul(mat, a), pixie.vec2(4, 6)); assert.deepStrictEqual(pixie.snapToPixels(pixie.rect(1, 2, 3, 4)), pixie.rect(1, 2, 3, 4)); assert(pixie.miterLimitToAngle(2) > 0); assert(pixie.angleToMiterLimit(1) > 0); @@ -84,7 +133,7 @@ assert.strictEqual(resized.width, 5); assert.strictEqual(resized.height, 6); assert.strictEqual(resized.subImage(0, 0, 2, 2).width, 2); assert.strictEqual(resized.rectSubImage(pixie.rect(0, 0, 1, 1)).height, 1); -assert.strictEqual(resized.shadow(pixie.vector2(1, 2), 3, 4, red).width, resized.width); +assert.strictEqual(resized.shadow(pixie.vec2(1, 2), 3, 4, red).width, resized.width); assert.strictEqual(resized.superImage(-1, -1, resized.width + 2, resized.height + 2).width, resized.width + 2); assert(resized.opaqueBounds().w > 0); @@ -96,9 +145,9 @@ paint.color = green; paint.imageMat = pixie.scale(2, 3); assert.strictEqual(paint.kind, pixie.LINEAR_GRADIENT_PAINT); approx(paint.opacity, 0.5); -paint.gradientHandlePositions.add(pixie.vector2(0.25, 0)); -paint.gradientHandlePositions.add(pixie.vector2(0.75, 1)); -paint.gradientHandlePositions.set(1, pixie.vector2(0.8, 1)); +paint.gradientHandlePositions.add(pixie.vec2(0.25, 0)); +paint.gradientHandlePositions.add(pixie.vec2(0.75, 1)); +paint.gradientHandlePositions.set(1, pixie.vec2(0.8, 1)); assert.strictEqual(paint.gradientHandlePositions.length(), 2); approx(paint.gradientHandlePositions.get(1).x, 0.8); paint.gradientStops.add(pixie.colorStop(red, 0)); @@ -125,8 +174,8 @@ assert(pathShape.computeBounds(identity).w > 0); const rectPath = pixie.newPath(); rectPath.rect(0, 0, 10, 10, true); const solidDashes = pixie.newSeqFloat32(); -assert(rectPath.fillOverlaps(pixie.vector2(5, 5), identity, pixie.NON_ZERO)); -assert(rectPath.strokeOverlaps(pixie.vector2(0, 5), identity, 2, pixie.BUTT_CAP, pixie.MITER_JOIN, pixie.DEFAULT_MITER_LIMIT, solidDashes)); +assert(rectPath.fillOverlaps(pixie.vec2(5, 5), identity, pixie.NON_ZERO)); +assert(rectPath.strokeOverlaps(pixie.vec2(0, 5), identity, 2, pixie.BUTT_CAP, pixie.MITER_JOIN, pixie.DEFAULT_MITER_LIMIT, solidDashes)); const typeface = pixie.readTypeface(fontPath); assert(typeface.filePath.endsWith('Inter-Regular.ttf')); @@ -150,13 +199,13 @@ assert(font.paints.length() >= 1); assert(font.scale() > 0); assert(font.defaultLineHeight() > 0); assert(font.layoutBounds('abcd').x > 0); -assert(font.typeset('abcd', pixie.vector2(100, 100), pixie.LEFT_ALIGN, pixie.TOP_ALIGN, true).layoutBounds().x > 0); +assert(font.typeset('abcd', pixie.vec2(100, 100), pixie.LEFT_ALIGN, pixie.TOP_ALIGN, true).layoutBounds().x > 0); const span = pixie.newSpan('hi', font); span.text = 'hello'; const spans = pixie.newSeqSpan(); spans.add(span); -const arrangement = spans.typeset(pixie.vector2(100, 100), pixie.CENTER_ALIGN, pixie.BOTTOM_ALIGN, true); +const arrangement = spans.typeset(pixie.vec2(100, 100), pixie.CENTER_ALIGN, pixie.BOTTOM_ALIGN, true); assert.strictEqual(spans.get(0).text, 'hello'); assert(arrangement.layoutBounds().x > 0); assert(spans.layoutBounds().y > 0); @@ -164,9 +213,9 @@ assert(arrangement.computeBounds(mat).x > 0); const canvas = pixie.newImage(64, 64); canvas.fill(pixie.parseColor('#ffffff')); -canvas.fillText(font, 'abc', mat, pixie.vector2(60, 60), pixie.LEFT_ALIGN, pixie.TOP_ALIGN); +canvas.fillText(font, 'abc', mat, pixie.vec2(60, 60), pixie.LEFT_ALIGN, pixie.TOP_ALIGN); canvas.arrangementFillText(arrangement, mat); -canvas.strokeText(font, 'abc', mat, 2, pixie.vector2(60, 60), pixie.LEFT_ALIGN, pixie.TOP_ALIGN, pixie.BUTT_CAP, pixie.MITER_JOIN, pixie.DEFAULT_MITER_LIMIT, dashes); +canvas.strokeText(font, 'abc', mat, 2, pixie.vec2(60, 60), pixie.LEFT_ALIGN, pixie.TOP_ALIGN, pixie.BUTT_CAP, pixie.MITER_JOIN, pixie.DEFAULT_MITER_LIMIT, dashes); canvas.arrangementStrokeText(arrangement, mat, 2, pixie.BUTT_CAP, pixie.MITER_JOIN, pixie.DEFAULT_MITER_LIMIT, dashes); canvas.fillPath(rectPath, solid, mat, pixie.NON_ZERO); canvas.strokePath(rectPath, solid, mat, 2, pixie.BUTT_CAP, pixie.MITER_JOIN, pixie.DEFAULT_MITER_LIMIT, dashes); @@ -237,6 +286,7 @@ assert.strictEqual(pixie.readImage(imagePath).width, 40); assert.strictEqual(pixie.readImageDimensions(imagePath).height, 40); assert.strictEqual(pixie.readFont(fontPath).size, 12); assert.strictEqual(pixie.parsePath('M0 0 L10 0 L10 10 Z').computeBounds(identity).w, 10); +writeRenderImages('node'); pixie.parseColor('bad'); assert(pixie.checkError()); assert(pixie.takeError().includes('bad')); diff --git a/tests/test_pixie_python.py b/tests/test_pixie_python.py index 9302dad..d5d667d 100644 --- a/tests/test_pixie_python.py +++ b/tests/test_pixie_python.py @@ -28,5 +28,5 @@ def load_ctypes_pixie(): if __name__ == "__main__": add_dll_dirs() - run(load_ctypes_pixie()) + run(load_ctypes_pixie(), "python") print("All Pixie Python tests passed!") diff --git a/tests/test_pixie_python_native.py b/tests/test_pixie_python_native.py index ee2e18c..42416fe 100644 --- a/tests/test_pixie_python_native.py +++ b/tests/test_pixie_python_native.py @@ -19,5 +19,5 @@ def bindings_dir(): if __name__ == "__main__": - run(pixie) + run(pixie, "python_native") print("All Pixie native Python tests passed!") diff --git a/tests/test_pixie_zig.zig b/tests/test_pixie_zig.zig index ece8973..823b27f 100644 --- a/tests/test_pixie_zig.zig +++ b/tests/test_pixie_zig.zig @@ -13,6 +13,59 @@ fn approxEps(value: f32, expected: f32, eps: f32) !void { try expect(@abs(value - expected) <= eps); } +const render_output_dir = "generated/pixie_images"; + +fn writeRenderStep(image: *pixie.Image, label: []const u8, step: []const u8) !void { + var actual_path_buffer: [128]u8 = undefined; + const actual_path = try std.fmt.bufPrintZ(&actual_path_buffer, "{s}/{s}_{s}.png", .{ render_output_dir, label, step }); + try image.writeFile(actual_path); +} + +fn writeRenderImages() !void { + const image = try pixie.Image.init(32, 32); + defer image.deinit(); + image.fill(try pixie.parseColor("#112233")); + + const orange = try pixie.parseColor("#f29e4c"); + var y: isize = 2; + while (y < 10) : (y += 1) { + var x: isize = 2; + while (x < 10) : (x += 1) { + image.setColor(x, y, orange); + } + } + try writeRenderStep(image, "zig", "step1"); + + const rect_paint = pixie.Paint.init(.solid_paint); + defer rect_paint.deinit(); + rect_paint.setColor(try pixie.parseColor("#209cee")); + const rect_path = pixie.Path.init(); + defer rect_path.deinit(); + rect_path.rect(12, 3, 14, 16, true); + try image.fillPath(rect_path, rect_paint, pixie.translate(1, 2), .non_zero); + try writeRenderStep(image, "zig", "step2"); + + const circle_paint = pixie.Paint.init(.solid_paint); + defer circle_paint.deinit(); + circle_paint.setColor(try pixie.parseColor("#8ac926")); + const circle_path = pixie.Path.init(); + defer circle_path.deinit(); + circle_path.circle(12, 22, 7); + try image.fillPath(circle_path, circle_paint, pixie.translate(0, 0), .non_zero); + + const stroke_paint = pixie.Paint.init(.solid_paint); + defer stroke_paint.deinit(); + stroke_paint.setColor(try pixie.parseColor("#ffffff")); + const border_path = pixie.Path.init(); + defer border_path.deinit(); + border_path.rect(0.75, 0.75, 30.5, 30.5, true); + const dashes = pixie.SeqFloat32.init(); + defer dashes.deinit(); + try image.strokePath(border_path, stroke_paint, pixie.translate(0, 0), 1.5, .butt_cap, .miter_join, pixie.default_miter_limit, dashes); + image.setColor(31, 31, try pixie.parseColor("#ff00ff")); + try writeRenderStep(image, "zig", "step3"); +} + pub fn main() !void { @setEvalBranchQuota(10000); @@ -36,6 +89,12 @@ pub fn main() !void { const identity = pixie.translate(0, 0); try expect(mat.values[6] == 3); try expect(pixie.inverse(mat).values[6] == -3); + const a = pixie.Vec2.init(1, 2); + const b = pixie.Vec2.init(3, 4); + try expect(a.add(b).eql(pixie.Vec2.init(4, 6))); + try expect(a.mul(b).eql(pixie.Vec2.init(3, 8))); + try expect(a.mulFloat32(2.0).eql(pixie.Vec2.init(2, 4))); + try expect(mat.mulVec2(a).eql(pixie.Vec2.init(4, 6))); try expect(pixie.snapToPixels(pixie.Rect.init(1, 2, 3, 4)).eql(pixie.Rect.init(1, 2, 3, 4))); try expect(pixie.miterLimitToAngle(2) > 0); try expect(pixie.angleToMiterLimit(1) > 0); @@ -87,7 +146,7 @@ pub fn main() !void { const rect_sub = try resized.subImageRect(pixie.Rect.init(0, 0, 1, 1)); defer rect_sub.deinit(); try expect(rect_sub.getHeight() == 1); - const shadow = try resized.shadow(pixie.Vector2.init(1, 2), 3, 4, red); + const shadow = try resized.shadow(pixie.Vec2.init(1, 2), 3, 4, red); defer shadow.deinit(); try expect(shadow.getWidth() == resized.getWidth()); const super_image = try resized.superImage(-1, -1, resized.getWidth() + 2, resized.getHeight() + 2); @@ -104,9 +163,9 @@ pub fn main() !void { paint.setImageMat(pixie.scale(2, 3)); try expect(paint.getKind() == .linear_gradient_paint); try approx(paint.getOpacity(), 0.5); - paint.appendGradientHandlePositions(pixie.Vector2.init(0.25, 0)); - paint.appendGradientHandlePositions(pixie.Vector2.init(0.75, 1)); - paint.setGradientHandlePositions(1, pixie.Vector2.init(0.8, 1)); + paint.appendGradientHandlePositions(pixie.Vec2.init(0.25, 0)); + paint.appendGradientHandlePositions(pixie.Vec2.init(0.75, 1)); + paint.setGradientHandlePositions(1, pixie.Vec2.init(0.8, 1)); try expect(paint.lenGradientHandlePositions() == 2); try approx(paint.getGradientHandlePositions(1).x, 0.8); paint.appendGradientStops(pixie.ColorStop.init(red, 0)); @@ -136,8 +195,8 @@ pub fn main() !void { rect_path.rect(0, 0, 10, 10, true); const solid_dashes = pixie.SeqFloat32.init(); defer solid_dashes.deinit(); - try expect(try rect_path.fillOverlaps(pixie.Vector2.init(5, 5), identity, .non_zero)); - try expect(try rect_path.strokeOverlaps(pixie.Vector2.init(0, 5), identity, 2, .butt_cap, .miter_join, pixie.default_miter_limit, solid_dashes)); + try expect(try rect_path.fillOverlaps(pixie.Vec2.init(5, 5), identity, .non_zero)); + try expect(try rect_path.strokeOverlaps(pixie.Vec2.init(0, 5), identity, 2, .butt_cap, .miter_join, pixie.default_miter_limit, solid_dashes)); const typeface = try pixie.readTypeface(font_path); defer typeface.deinit(); @@ -165,7 +224,7 @@ pub fn main() !void { try expect(font.scale() > 0); try expect(font.defaultLineHeight() > 0); try expect(font.layoutBounds("abcd").x > 0); - const font_arrangement = font.typeset("abcd", pixie.Vector2.init(100, 100), .left_align, .top_align, true); + const font_arrangement = font.typeset("abcd", pixie.Vec2.init(100, 100), .left_align, .top_align, true); defer font_arrangement.deinit(); try expect(font_arrangement.layoutBounds().x > 0); @@ -175,7 +234,7 @@ pub fn main() !void { const spans = pixie.SeqSpan.init(); defer spans.deinit(); spans.append(span); - const arrangement = spans.typeset(pixie.Vector2.init(100, 100), .center_align, .bottom_align, true); + const arrangement = spans.typeset(pixie.Vec2.init(100, 100), .center_align, .bottom_align, true); defer arrangement.deinit(); const span_text = try spans.get(0).getText(allocator); defer allocator.free(span_text); @@ -187,9 +246,9 @@ pub fn main() !void { const canvas = try pixie.Image.init(64, 64); defer canvas.deinit(); canvas.fill(try pixie.parseColor("#ffffff")); - try canvas.fillText(font, "abc", mat, pixie.Vector2.init(60, 60), .left_align, .top_align); + try canvas.fillText(font, "abc", mat, pixie.Vec2.init(60, 60), .left_align, .top_align); try canvas.fillTextArrangement(arrangement, mat); - try canvas.strokeText(font, "abc", mat, 2, pixie.Vector2.init(60, 60), .left_align, .top_align, .butt_cap, .miter_join, pixie.default_miter_limit, dashes); + try canvas.strokeText(font, "abc", mat, 2, pixie.Vec2.init(60, 60), .left_align, .top_align, .butt_cap, .miter_join, pixie.default_miter_limit, dashes); try canvas.strokeTextArrangement(arrangement, mat, 2, .butt_cap, .miter_join, pixie.default_miter_limit, dashes); try canvas.fillPath(rect_path, solid, mat, .non_zero); try canvas.strokePath(rect_path, solid, mat, 2, .butt_cap, .miter_join, pixie.default_miter_limit, dashes); @@ -274,6 +333,7 @@ pub fn main() !void { const parsed_path = try pixie.parsePath("M0 0 L10 0 L10 10 Z"); defer parsed_path.deinit(); try expect((try parsed_path.computeBounds(identity)).w == 10); + try writeRenderImages(); if (pixie.parseColor("bad")) |_| { return error.TestFailed; } else |err| {