Skip to content

Commit 831cd7a

Browse files
committed
sketch syntax
1 parent 860f676 commit 831cd7a

8 files changed

Lines changed: 254 additions & 16 deletions

File tree

keten.nimble

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ srcDir = "src"
1111

1212
requires "nim >= 1.6.0"
1313
requires "https://github.com/holo-nim/glaze"
14-
#requires "https://github.com/holo-nim/cosm"
14+
requires "https://github.com/holo-nim/cosm" # already required by glaze but also used for caseutils
1515

1616
task docs, "build docs for all modules":
1717
exec "nim r tasks/build_docs.nim"

src/keten.nim

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1+
import keten/[schema, syntax]
2+
export SchemaId, FrozenError, NotFrozenError, freeze, isFrozen
3+
export syntax
14

src/keten/data.nim

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ proc addRawRow*(id: SchemaId, row: RawRow) =
1010
raise newException(FrozenError, "schema " & $id & " is frozen, no rows can be added")
1111
getSchemaData(id).add glaze(row)
1212

13-
macro defineRowAdd*(id: static SchemaId, name: untyped) =
14-
var name = name
15-
let isExported = name.kind in {nnkPrefix, nnkPostfix}
16-
if isExported: name = name[1]
17-
expectKind name, {nnkIdent, nnkAccQuoted, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}
18-
name = ident repr name
13+
proc defineRowAdd*(id: SchemaId, rawName: NimNode, initialParams: seq[NimNode] = @[newEmptyNode()]): NimNode =
14+
var rawName = rawName
15+
let isExported = rawName.kind in {nnkPrefix, nnkPostfix}
16+
if isExported: rawName = rawName[1]
17+
expectKind rawName, {nnkIdent, nnkAccQuoted, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}
18+
#name = ident repr name
1919
let schema = getSchema(id)
20-
var params: seq[NimNode] = @[newEmptyNode()]
21-
var row = newNimNode(nnkBracket, name)
20+
var params: seq[NimNode] = initialParams
21+
var row = newNimNode(nnkBracket, rawName)
2222
for field in schema.fields:
2323
var typeNode = field.type.constraint.NimNode
2424
case field.type.kind
@@ -47,17 +47,20 @@ macro defineRowAdd*(id: static SchemaId, name: untyped) =
4747
of ExprAtom: discard
4848
of TypeAtom:
4949
# maybe implement glaze for typedesc
50+
# might need wrapping in `type()`
5051
serializeNode = newCall(bindSym"getTypeInst", serializeNode)
5152
of StaticAtom:
5253
serializeNode = newCall(bindSym("glaze", brForceOpen), serializeNode)
5354
row.add newCall(bindSym"RawNimNode", serializeNode)
5455
result = newProc(
5556
procType = nnkMacroDef,
56-
name = name,
57+
name = rawName,
5758
params = params,
5859
body = newCall(bindSym"addRawRow", glaze id, newCall(bindSym"@", row)))
5960

6061
iterator eachRawRow*(id: SchemaId): RawRow =
62+
if readRequiresFreeze(id) and not isFrozen(id):
63+
raise newException(NotFrozenError, "schema " & $id & " requires being frozen to read but is not frozen")
6164
for rowNode in getSchemaData(id):
6265
yield deglaze(rowNode, RawRow)
6366

src/keten/schema.nim

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
1-
import glaze, std/macrocache
1+
import glaze, std/[macrocache, macros]
22

33
type
4-
AtomKind* = enum
4+
KetenAtomKind* = enum
55
ExprAtom
66
TypeAtom
77
StaticAtom
8-
AtomType* = object
9-
case kind*: AtomKind
8+
KetenAtomType* = object
9+
case kind*: KetenAtomKind
1010
of ExprAtom, TypeAtom, StaticAtom:
1111
constraint*: RawNimNode
1212
KetenField* = object
1313
# or "strand"
1414
column*: int
1515
name*: string
16-
`type`*: AtomType # can be untyped, typed, static, typedesc, or other typed node
16+
`type`*: KetenAtomType # can be untyped, typed, static, typedesc, or other typed node
1717
default*: RawNimNode
1818
KetenSchema* = object
1919
# or "fabric"
2020
fields*: seq[KetenField]
21+
readRequiresFreeze*: bool
2122

2223
proc getColumn*(schema: KetenSchema, name: string): int =
2324
result = -1
@@ -41,7 +42,18 @@ proc addSchema*(id: SchemaId, schema: KetenSchema) =
4142
proc getSchema*(id: SchemaId): KetenSchema =
4243
toSchema(schemas[id])
4344

44-
type FrozenError* = object of CatchableError
45+
proc readRequiresFreeze*(id: SchemaId): bool =
46+
result = false
47+
let node = schemas[id]
48+
for i in 1 ..< node.len:
49+
assert node[i].kind == nnkExprColonExpr
50+
if node[i][0].eqIdent"readRequiresFreeze":
51+
result = deglaze(node[i][1], bool)
52+
return result
53+
54+
type
55+
FrozenError* = object of CatchableError
56+
NotFrozenError* = object of CatchableError
4557

4658
const freezes* = CacheTable"keten.freezes"
4759

src/keten/syntax.nim

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import glaze, ./[schema, data], std/[macros, macrocache], cosm/caseutils
2+
3+
type TypeSectionGen = enum Statement, TypeSection, TypeDef
4+
5+
proc wrap(gen: TypeSectionGen, typeSection: NimNode, remaining: seq[NimNode]): NimNode =
6+
case gen
7+
of Statement:
8+
result = newStmtList()
9+
if not typeSection.isNil: result.add typeSection
10+
result.add remaining
11+
of TypeSection:
12+
result = if typeSection.isNil: newNimNode(nnkTypeSection) else: typeSection
13+
var val = newNimNode(nnkStmtListType, typeSection)
14+
val.add remaining
15+
val.add bindSym"void"
16+
result.add newTree(nnkTypeDef, genSym(nskType, "_"), newEmptyNode(), val)
17+
of TypeDef:
18+
if not typeSection.isNil and typeSection.len == 1:
19+
result = typeSection[0]
20+
else:
21+
var val = newNimNode(nnkStmtListType)
22+
if not typeSection.isNil: val.add typeSection
23+
val.add remaining
24+
val.add bindSym"void"
25+
result = newTree(nnkTypeDef, genSym(nskType, "_"), newEmptyNode(), val)
26+
27+
type
28+
FabricKind* = enum
29+
Warm, Cold
30+
FabricOptions* = object
31+
requiresFreeze*: bool
32+
id*: string
33+
34+
proc processFabricType(node: NimNode): KetenAtomType =
35+
if node.kind in nnkCallKinds + {nnkBracket} and node.len == 2:
36+
if node[0].eqIdent"typedesc" or node[0].eqIdent"type":
37+
return KetenAtomType(kind: TypeAtom, constraint: RawNimNode node[1])
38+
elif node[0].eqIdent"static":
39+
return KetenAtomType(kind: StaticAtom, constraint: RawNimNode node[1])
40+
elif node.kind in {nnkIdent, nnkAccQuoted, nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}:
41+
if node.eqIdent"typedesc" or node.eqIdent"type":
42+
return KetenAtomType(kind: TypeAtom, constraint: RawNimNode nil)
43+
elif node.eqIdent"static":
44+
return KetenAtomType(kind: StaticAtom, constraint: RawNimNode nil)
45+
result = KetenAtomType(kind: ExprAtom, constraint: RawNimNode node)
46+
47+
proc iterFabricFields(fields: var seq[KetenField], node: NimNode) =
48+
case node.kind
49+
of nnkRecList, nnkTupleTy:
50+
for n in node:
51+
iterFabricFields(fields, n)
52+
of nnkRecWhen:
53+
error("when not supported in fabric", node)
54+
of nnkRecCase:
55+
error("case not allowed in fabric", node)
56+
of nnkIdentDefs:
57+
let typPos = node.len - 2
58+
let typ = processFabricType(node[typPos])
59+
for i in 0 ..< typPos:
60+
var nameNode = node[i]
61+
if nameNode.kind == nnkPragmaExpr: nameNode = nameNode[0]
62+
if nameNode.kind == nnkPostfix: nameNode = nameNode[1]
63+
let name = $nameNode
64+
let col = fields.len
65+
fields.add KetenField(column: col, name: name, type: typ, default: RawNimNode(copy node[typPos + 1]))
66+
of nnkDiscardStmt, nnkNilLit, nnkEmpty: discard
67+
else:
68+
error("invalid type ast for fabric: " & $node.kind, node)
69+
70+
proc buildFabric(options: FabricOptions, body: NimNode): tuple[typeSection: NimNode, remaining: seq[NimNode]] =
71+
if body.kind in {nnkStmtList, nnkTypeSection}:
72+
for b in body:
73+
let (types, remaining) = buildFabric(options, b)
74+
if result.typeSection.isNil:
75+
result.typeSection = types
76+
elif result.remaining.len == 0:
77+
for td in types: result.typeSection.add td
78+
else:
79+
result.remaining.add types
80+
result.remaining.add remaining
81+
return result
82+
expectKind body, nnkTypeDef
83+
var nameNode = body[0]
84+
if nameNode.kind == nnkPragmaExpr: nameNode = nameNode[0]
85+
let isPostfix = nameNode.kind == nnkPostfix
86+
if isPostfix: nameNode = nameNode[1]
87+
let name = $nameNode
88+
var schema = KetenSchema(readRequiresFreeze: options.requiresFreeze)
89+
if body[2].kind != nnkObjectTy:
90+
error "expected object type for fabric", body[2]
91+
iterFabricFields(schema.fields, body[2][^1])
92+
var rawId = options.id
93+
if rawId.len == 0:
94+
rawId = $schemas.len & "-" & toKebabCase(name)
95+
let id = SchemaId(rawId)
96+
addSchema id, schema
97+
var newType = newNimNode(nnkTypeDef, body)
98+
newType.add body[0]
99+
newType.add body[1]
100+
newType.add newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), newEmptyNode())
101+
result.typeSection = newTree(nnkTypeSection, newType)
102+
result.remaining.add newProc(
103+
procType = nnkTemplateDef,
104+
name =
105+
if isPostfix: newTree(nnkPostfix, ident"*", ident"getFabricSchemaId")
106+
else: ident"getFabricSchemaId",
107+
params = [bindSym"SchemaId",
108+
newTree(nnkIdentDefs, ident"T", newTree(nnkBracketExpr, ident"typedesc", ident name), newEmptyNode())],
109+
body = newLit id
110+
)
111+
var stitchName = ident"stitch"
112+
if isPostfix: stitchName = newTree(nnkPostfix, ident"*", stitchName)
113+
var stitchParams = @[newEmptyNode()]
114+
stitchParams.add newTree(nnkIdentDefs, ident"T", newTree(nnkBracketExpr, ident"typedesc", ident name), newEmptyNode())
115+
result.remaining.add defineRowAdd(id, stitchName, stitchParams)
116+
117+
proc fabricImpl(options: FabricOptions, body: NimNode): NimNode =
118+
let gen =
119+
case body.kind
120+
of nnkTypeDef:
121+
when (NimMajor, NimMinor) >= (2, 0): TypeSection
122+
else: TypeDef
123+
of nnkTypeSection: TypeSection
124+
else: Statement
125+
let (typeSection, remaining) = buildFabric(options, body)
126+
result = wrap(gen, typeSection, remaining)
127+
128+
macro fabric*(option: static tuple[], body: untyped): untyped =
129+
fabricImpl(FabricOptions(), body)
130+
131+
macro fabric*(option: static FabricKind, body: untyped): untyped =
132+
fabricImpl(FabricOptions(requiresFreeze: option == Cold), body)
133+
134+
macro fabric*(option: static string, body: untyped): untyped =
135+
fabricImpl(FabricOptions(id: option), body)
136+
137+
macro fabric*(option: static FabricOptions, body: untyped): untyped =
138+
fabricImpl(option, body)
139+
140+
macro dispatchImpl(id: static SchemaId, column: untyped, value: typed, toApply: untyped, elses: varargs[untyped]): untyped =
141+
let schema = getSchema(id)
142+
let realColumn =
143+
if column.kind in {nnkIntLit..nnkUint64Lit}: column.intVal.int
144+
else: getColumn(schema, $column)
145+
var callPattern = toApply
146+
result = newStmtList()
147+
if toApply.kind in {nnkLambda, nnkDo}:
148+
let (kind, symkind, kindname) =
149+
if toApply.kind == nnkLambda: (nnkProcDef, nskProc, "Proc")
150+
else: (nnkTemplateDef, nskTemplate, "Templ")
151+
var routine = newNimNode(kind, toApply)
152+
for child in toApply: routine.add child
153+
let routineName = genSym(symkind, "dispatch" & kindname)
154+
routine[0] = routineName
155+
callPattern = routineName
156+
result.add routine
157+
var realElse: NimNode = nil
158+
if elses.len > 0:
159+
if elses.len != 1: error "only 1 else supported", elses
160+
if elses[0].kind != nnkElse: error "expected else", elses[0]
161+
realElse = elses[0][0]
162+
result.add dispatch(id, realColumn, value, callPattern, realElse)
163+
164+
macro dispatch*(T: typedesc, column: untyped, value: typed, toApply: untyped, elses: varargs[untyped]): untyped =
165+
result = newCall(bindSym"dispatchImpl", newCall(ident"getFabricSchemaId", T), column, value, toApply)
166+
for e in elses: result.add e

src/keten/typeids.nim

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import std/[macros, macrocache]
2+
3+
const typeIdCounter = CacheCounter"keten.typeids"
4+
5+
type TypeId* = distinct int
6+
# currently uses a counter
7+
# if this causes problems, can use something like `signatureHash` instead
8+
9+
macro newId(_: typed): TypeId =
10+
# typed argument is to prevent early macro instantiation in generic,
11+
# not needed with https://github.com/nim-lang/Nim/pull/22517
12+
inc typeIdCounter
13+
result = newCall(bindSym"TypeId", newLit(typeIdCounter.value))
14+
15+
proc getTypeId*(T: type): TypeId =
16+
const id = newId(0)
17+
# only generated when proc is compiled, i.e. instantiated according to `T`
18+
id

tests/test1.nim

Whitespace-only changes.

tests/test_basic.nim

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import keten
2+
3+
type Foo {.fabric: ().} = object
4+
id: static int
5+
name: static string
6+
typ: typedesc
7+
8+
stitch Foo, 0, "abc", int
9+
Foo.stitch 1, "def", float
10+
Foo.stitch 2, "ghi", string
11+
12+
import std/strutils
13+
14+
proc take(s: string, T: type int): T = parseInt(s)
15+
proc take(s: string, T: type float): T = parseFloat(s)
16+
proc take(s: string, T: type string): T = s
17+
18+
proc print(x: int): string = "got int: " & $x
19+
proc print(x: float): string = "got float: " & $x
20+
proc print(x: string): string = "got string: " & $x
21+
22+
proc process(a: int, b: string): string =
23+
Foo.dispatch(id, a) do (id, name, typ):
24+
let val = take(b, typ)
25+
result = print(val) & " for " & name
26+
else: result = "invalid id: " & $a
27+
28+
doAssert process(0, "123") == "got int: 123 for abc"
29+
doAssert process(0, "4000") == "got int: 4000 for abc"
30+
doAssert process(0, "-5") == "got int: -5 for abc"
31+
doAssert process(1, "1.23") == "got float: 1.23 for def"
32+
doAssert process(1, "NaN") == "got float: nan for def"
33+
doAssert process(2, "xyz") == "got string: xyz for ghi"
34+
doAssert process(3, "...") == "invalid id: 3"
35+
doAssert process(5, "") == "invalid id: 5"
36+
doAssert process(-1, "...") == "invalid id: -1"

0 commit comments

Comments
 (0)