Add preserves_schema_nim utilty

A utility for generating code from Preserves schema.
This commit is contained in:
Emery Hemingway 2021-09-01 11:35:44 +02:00
parent 91bf449f6b
commit 53642eef83
2 changed files with 458 additions and 1 deletions

View File

@ -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"

View File

@ -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)