diff --git a/preserves.nimble b/preserves.nimble index 244ad92..b5c693b 100644 --- a/preserves.nimble +++ b/preserves.nimble @@ -6,7 +6,9 @@ description = "data model and serialization format" license = "Unlicense" srcDir = "src" +bin = @["preserves/private/preserves_schema_nim"] + # Dependencies -requires "nim >= 1.4.8", "bigints", "npeg" +requires "nim >= 1.4.8", "compiler >= 1.4.8", "bigints", "npeg" diff --git a/src/preserves/private/preserves_schema_nim.nim b/src/preserves/private/preserves_schema_nim.nim new file mode 100644 index 0000000..229f361 --- /dev/null +++ b/src/preserves/private/preserves_schema_nim.nim @@ -0,0 +1,455 @@ +# SPDX-FileCopyrightText: 2021 ☭ Emery Hemingway +# SPDX-License-Identifier: Unlicense + +import std/[os, strutils, tables, times] + +import compiler/[ast, idents, renderer, lineinfos] + +import ../../preserves, ../schemas + +type TypeTable = OrderedTable[string, PNode] + +proc add(parent, child: PNode): PNode {.discardable.} = + parent.sons.add child + parent + +proc add(parent: PNode; children: varargs[PNode]): PNode {.discardable.} = + parent.sons.add children + parent + +proc child(sn: SchemaNode): SchemaNode = + assert(sn.nodes.len == 1) + sn.nodes[0] + +proc nn(kind: TNodeKind; children: varargs[PNode]): PNode = + result = newNode(kind) + result.sons.add(children) + +proc nn(kind: TNodeKind; child: PNode): PNode = + result = newNode(kind) + result.sons.add(child) + +proc ident(s: string): PNode = + newIdentNode(PIdent(s: s), TLineInfo()) + +proc accQuote(n: PNode): Pnode = + nkAccQuoted.newNode.add(n) + +proc ident(sn: SchemaNode): PNode = + var s: string + case sn.kind + of snkAlt: + s = sn.altLabel.toLower.nimIdentNormalize + of snkLiteral: s = $sn.value + of snkRecord: + s = $sn.nodes[0] + of snkNamed: + s = sn.name + of snkDictionary, snkVariableTuple: + s = "data" + else: + raiseAssert("no ident for " & $sn.kind & " " & $sn) + ident(s) + +proc typeIdent(sn: SchemaNode): PNode = + case sn.kind + of snkAtom: + case sn.atom + of akBool: ident"bool" + of akFloat: ident"float32" + of akDouble: ident"float64" + of akInt: ident"BiggestInt" + of akString: ident"string" + of akBytes: nn(nkBracketExpr, ident"seq", ident"byte") + of akSymbol: ident"string" # TODO: distinct string type for symbols? + of snkNamed: + sn.pattern.typeIdent + of snkRef: + ident($sn) + else: + stderr.writeLine("no typeIdent for " & $sn.kind & " " & $sn) + ident"Preserve" + +proc toExport(n: sink PNode): PNode = + nkPostFix.newNode.add(ident"*", n) + +proc newEmpty(): PNode = newNode(nkEmpty) + +proc isConst(sn: SchemaNode): bool = + case sn.kind + of snkLiteral: true + else: false + +proc isSymbolEnum(sn: SchemaNode): bool = + if sn.kind == snkOr: + for bn in sn.nodes: + if bn.altBranch.kind != snkLiteral or + bn.altBranch.value.kind != pkSymbol: + return false + result = true + +proc toEnumTy(sn: SchemaNode): PNode = + result = nkEnumTy.newNode.add newEmpty() + for bn in sn.nodes: + result.add bn.altLabel.nimIdentNormalize.ident.accQuote + +proc toEnumDef(name: string; sn: SchemaNode): PNode = + nkTypeDef.newNode.add( + nkPragmaExpr.newNode.add( + name.ident.toExport, + nkPragma.newNode.add(ident"pure")), + newEmpty(), + sn.toEnumTy) + +proc nimTypeOf(known: var TypeTable; sn: SchemaNode; name = ""): PNode = + case sn.kind + of snkOr: + if sn.isSymbolEnum: + result = sn.toEnumTy + else: + let + enumName = name.nimIdentNormalize & "Kind" + enumIdent = nn(nkPostFix, ident"*", ident(enumName)) + if enumName notin known: + known[enumName] = toEnumDef(enumName, sn) + let recCase = nkRecCase.newNode.add( + nkIdentDefs.newNode.add( + "kind".ident.toExport, + enumName.ident, + newEmpty())) + for bn in sn.nodes: + assert(bn.kind == snkAlt, $bn.kind) + var recList = nkRecList.newNode + case bn.altBranch.kind + of snkRecord: + case bn.altBranch.nodes.len + of 0, 1: discard + of 2: + let label = bn.ident + recList.add nkIdentDefs.newNode.add( + label.accQuote.toExport, + nimTypeOf(known, bn.altBranch.nodes[1], $label), + newEmpty()) + else: + for i, field in bn.altBranch.nodes: + if i > 0: + let label = field.ident + recList.add nkIdentDefs.newNode.add( + label.accQuote.toExport, + nimTypeOf(known, field, $label), + newEmpty()) + else: + if bn.altBranch.isConst: + recList.add nkDiscardStmt.newNode.add(newEmpty()) + else: + let label = bn.ident + recList.add(nkIdentDefs.newNode.add( + label.accQuote.toExport, + nimTypeOf(known, bn.altBranch, $label), + newEmpty())) + let disc = nkDotExpr.newNode.add( + enumIdent, bn.altLabel.nimIdentNormalize.ident.accQuote) + recCase.add nkOfBranch.newNode.add(disc, recList) + result = nn(nkObjectTy, + newEmpty(), + newEmpty(), + nn(nkRecList, recCase)) + of snkAny: + result = ident"Preserve" + of snkAtom: + result = typeIdent(sn) + of snkEmbedded: + result = nimTypeOf(known, sn.embed) + of snkLiteral: + result = case sn.value.kind # nearly verbatim from ../../preserves/src/preserves.nim + of pkBoolean: ident"bool" + of pkFloat: ident"float32" + of pkDouble: ident"float64" + of pkSignedInteger: ident"BiggestInt" + of pkBigInteger: ident"BigInt" + of pkString: ident"string" + of pkByteString: nn( + nkBracketExpr, ident"seq", ident"byte") + of pkSymbol: ident"string" + of pkRecord: ident"Preserve" + of pkSequence: nn( + nkBracketExpr, ident"seq", ident"Preserve") + of pkSet: nn( + nkBracketExpr, ident"HashSet", ident"Preserve") + of pkDictionary: nn( + nkBracketExpr, ident"Table", ident"Preserve", ident"Preserve") + of pkEmbedded: nn(nkDiscardStmt, newEmpty()) # TODO: is this possible? + of snkSequenceOf: + result = nkBracketExpr.newNode.add( + ident"seq", + nimTypeOf(known, sn.child)) + of snkSetOf: + result = nkBracketExpr.newNode.add( + ident"HashedSet", + nimTypeOf(known, sn.child)) + of snkDictOf: + result = nkBracketExpr.newNode.add( + ident"Table", + nimTypeOf(known, sn.nodes[0]), + nimTypeOf(known, sn.nodes[1])) + of snkRecord: + case sn.nodes.len + of 0, 1: + result = nn(nkObjectTy, + newEmpty(), + newEmpty(), + nn(nkDiscardStmt, newEmpty())) + else: + let recList = nkRecList.newNode() + for i, field in sn.nodes: + if i > 0: + let id = field.ident + recList.add nkIdentDefs.newNode.add( + id.accQuote.toExport, + nimTypeOf(known, field, $id), + newEmpty()) + result = nn(nkObjectTy, + newEmpty(), + newEmpty(), + recList) + of snkTuple, snkVariableTuple: + # TODO: the variable part + result = nkTupleTy.newNode + for tn in sn.nodes: + result.add nkIdentDefs.newNode.add( + tn.ident.accQuote, nimTypeOf(known, tn), newEmpty()) + of snkDictionary: + result = nkTupleTy.newNode + for i in countup(0, sn.nodes.high, 2): + let id = ident(sn.nodes[i+0]) + result.add nkIdentDefs.newNode.add( + id.accQuote, + nimTypeOf(known, sn.nodes[i+1], $id), + newEmpty()) + of snkNamed: + result = nimTypeOf(known, sn.pattern, name) + of snkRef: + result = ident $sn + else: + result = nkCommentStmt.newNode + result.comment.add("Missing type generator for " & $sn.kind & " " & $sn) + +proc toConst(name: string; def: SchemaNode): Pnode = + case def.kind + of snkLiteral: + result = nkConstDef.newNode.add( + name.ident.accQuote, newEmpty()) + case def.value.kind + of pkSignedInteger: + discard result.add newIntNode(nkIntLit, def.value.int) + of pkSymbol: + discard result.add nn(nkCall, + ident"symbol", + PNode(kind: nkStrLit, strVal: def.value.symbol)) + else: + raiseAssert("cannot convert " & $def & " to a Nim literal") + else: discard + +proc toNimLit(sn: SchemaNode): PNode = + assert(sn.kind == snkLiteral, $sn) + case sn.value.kind + of pkSymbol: + nkCall.newNode.add( + ident"symbol", + PNode(kind: nkStrLit, strVal: sn.value.symbol)) + else: + raiseAssert("no Nim literal for " & $sn) + +proc preserveTypeOf(known: var TypeTable; sn: SchemaNode; name = ""): PNode = + case sn.kind + of snkOr: + if sn.isSymbolEnum: + result = sn.toEnumTy + else: + let enumName = name.nimIdentNormalize & "Kind" + if enumName notin known: + known[enumName] = toEnumDef(enumName, sn) + result = nkObjectTy.newNode.add( + newEmpty(), + newEmpty(), + nkRecList.newNode.add( + nkIdentDefs.newNode.add( + ident"value", ident"Preserve", newEmpty()), + nkIdentDefs.newNode.add( + "schemaKind".ident, + enumName.ident, + newEmpty()))) + of snkAny: + result = ident"Preserve" + of snkAtom: + result = case sn.atom + of akBool: ident"bool" + of akFloat: ident"float32" + of akDouble: ident"float64" + of akInt: ident"BiggestInt" + of akString: ident"string" + of akBytes: nkBracketExpr.newNode.add( + ident"seq", + ident"byte") + of akSymbol: ident"string" # TODO: distinct string type for symbols? + of snkRecord: + case sn.nodes.len + of 0, 1: + result = nn(nkObjectTy, + newEmpty(), + newEmpty(), + nn(nkDiscardStmt, newEmpty())) + else: + let recList = nkRecList.newNode() + for i, field in sn.nodes: + if i > 0: + let id = field.ident + recList.add nkIdentDefs.newNode.add( + id.accQuote.toExport, + nimTypeOf(known, field, $id), + newEmpty()) + result = nn(nkObjectTy, + newEmpty(), + newEmpty(), + recList) + of snkTuple: + let recList = nkRecList.newNode() + for i, field in sn.nodes: + let id = field.ident + recList.add nkIdentDefs.newNode.add( + id.accQuote, + nimTypeOf(known, field, $id), + newEmpty()) + result = nn(nkTupleTy, + newEmpty(), + newEmpty(), + recList) + of snkNamed: + result = preserveTypeOf(known, sn.pattern, sn.name) + of snkRef: + result = ident $sn + else: + result = nn(nkDistinctTy, ident"Preserve") + result.comment = "``" & $sn & "``" + +proc generateProcs(result: var seq[PNode]; name: string; sn: SchemaNode) = + proc exportIdent(id: string): PNode = nn(nkPostFix, ident"*", ident(id)) + case sn.kind + of snkRecord: + var + params = nn(nkFormalParams, ident"Preserve") + initRecordCall = nn(nkCall, ident"initRecord", sn.nodes[0].toNimLit) + for i, field in sn.nodes: + if i > 0: + let id = field.ident + params.add nn(nkIdentDefs, + id, + nn(nkInfix, + ident"|", + ident"Preserve", + field.typeIdent), + newEmpty()) + initRecordCall.add(id) + result.add nn(nkProcDef, + exportIdent("prs" & name), + newEmpty(), + newEmpty(), + params, + newEmpty(), + newEmpty(), + nn(nkStmtList, initRecordCall)) + else: discard + +proc generateNimFile*(scm: Schema; path: string) = + var + knownTypes: TypeTable + typeSection = newNode nkTypeSection + constSection = newNode nkConstSection + procs: seq[PNode] + if scm.embeddedType != "": + typeSection.add nn(nkTypeDef, + ident"Preserve", + newEmpty(), + nn(nkBracketExpr, + ident"PreserveGen", + ident(scm.embeddedType))) + for name, def in scm.definitions.pairs: + if def.isConst: + constSection.add toConst(name, def) + else: + let t = preserveTypeOf(knownTypes, def, name) + case def.kind + of snkAtom: + knownTypes[name] = nkTypeDef.newNode.add( + name.ident.toExport, newEmpty(), t) + else: + if def.kind == snkRecord: + knownTypes[name] = nn(nkTypeDef, + nn(nkPragmaExpr, + name.ident.toExport, + nn(nkPragma, + nn(nkExprColonExpr, + ident"record", + PNode(kind: nkStrLit, strVal: $def.nodes[0])))), + newEmpty(), + t) + else: + case t.kind + of nkEnumTy: + knownTypes[name] = nn(nkTypeDef, + nn(nkPragmaExpr, + name.ident.toExport, + nn(nkPragma, ident"pure")), + newEmpty(), + t) + of nkDistinctTy: + knownTypes[name] = nn(nkTypeDef, + nn(nkPragmaExpr, + name.ident.toExport, + nn(nkPragma, + nn(nkExprColonExpr, + ident"borrow", + accQuote(ident".")))), + newEmpty(), + t) + else: + knownTypes[name] = nn(nkTypeDef, + name.ident.toExport, newEmpty(), t) + generateProcs(procs, name, def) + for typeDef in knownTypes.values: + typeSection.add typeDef + var + dateComment = PNode(kind: nkCommentStmt, comment: + "Date of generation: " & now().format("yyyy-MM-dd HH:mm")) + imports = nkImportStmt.newNode.add( + ident"std/typetraits", + ident"preserves") + module = newNode(nkStmtList).add( + dateComment, + imports, + typeSection, + constSection + ).add(procs) + echo path + writeFile(path, renderTree(module, {renderNone, renderIr})) + +when isMainModule: + import std/parseopt + var inputs: seq[string] + for kind, key, val in getopt(): + case kind + of cmdLongOption: + case key + else: quit("unhandled option " & key) + of cmdShortOption: + case key + else: quit("unhandled option " & key) + of cmdArgument: + inputs.add absolutePath(key) + of cmdEnd: discard + for filepath in inputs: + let + scm = parsePreservesSchema(readFile filepath, filepath) + (dir, name, _) = splitFile(filepath) + outputPath = dir / name & ".nim" + generateNimFile(scm, outputPath)