summary refs log tree commit diff stats
path: root/tests/macros/tstructuredlogging.nim
blob: 05bb52a4021adb9eaf5a70a23e07027b958bd616 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
discard """
output: '''
main started: a=10, b=inner-b, c=10, d=some-d, x=16, z=20
exiting: a=12, b=overriden-b, c=100, msg=bye bye, x=16
'''
"""

import macros, tables

template scopeHolder =
  0 # scope revision number

type
  BindingsSet = Table[string, NimNode]

proc actualBody(n: NimNode): NimNode =
  # skip over the double StmtList node introduced in `mergeScopes`
  result = n.body
  if result.kind == nnkStmtList and result[0].kind == nnkStmtList:
    result = result[0]

iterator bindings(n: NimNode, skip = 0): (string, NimNode) =
  for i in skip ..< n.len:
    let child = n[i]
    if child.kind in {nnkAsgn, nnkExprEqExpr}:
      let name = $child[0]
      let value = child[1]
      yield (name, value)

proc scopeRevision(scopeHolder: NimNode): int =
  # get the revision number from a scopeHolder sym
  assert scopeHolder.kind == nnkSym
  var revisionNode = scopeHolder.getImpl.actualBody[0]
  result = int(revisionNode.intVal)

proc lastScopeHolder(scopeHolders: NimNode): NimNode =
  # get the most recent scopeHolder from a symChoice node
  if scopeHolders.kind in {nnkClosedSymChoice, nnkOpenSymChoice}:
    var bestScopeRev = 0
    assert scopeHolders.len > 0
    for scope in scopeHolders:
      let rev = scope.scopeRevision
      if result == nil or rev > bestScopeRev:
        result = scope
        bestScopeRev = rev
  else:
    result = scopeHolders

  assert result.kind == nnkSym

macro mergeScopes(scopeHolders: typed, newBindings: untyped): untyped =
  var
    bestScope = scopeHolders.lastScopeHolder
    bestScopeRev = bestScope.scopeRevision

  var finalBindings = initTable[string, NimNode]()
  for k, v in bindings(bestScope.getImpl.actualBody, skip = 1):
    finalBindings[k] = v

  for k, v in bindings(newBindings):
    finalBindings[k] = v

  var newScopeDefinition = newStmtList(newLit(bestScopeRev + 1))

  for k, v in finalBindings:
    newScopeDefinition.add newAssignment(newIdentNode(k), v)

  result = quote:
    template scopeHolder = `newScopeDefinition`

template scope(newBindings: untyped) {.dirty.} =
  mergeScopes(bindSym"scopeHolder", newBindings)

type
  TextLogRecord = object
    line: string

  StdoutLogRecord = object

template setProperty(r: var TextLogRecord, key: string, val: string, isFirst: bool) =
  if not first: r.line.add ", "
  r.line.add key
  r.line.add "="
  r.line.add val

template setEventName(r: var StdoutLogRecord, name: string) =
  stdout.write(name & ": ")

template setProperty(r: var StdoutLogRecord, key: string, val: auto, isFirst: bool) =
  when not isFirst: stdout.write ", "
  stdout.write key
  stdout.write "="
  stdout.write $val

template flushRecord(r: var StdoutLogRecord) =
  stdout.write "\n"
  stdout.flushFile

macro logImpl(scopeHolders: typed,
              logStmtProps: varargs[untyped]): untyped =
  let lexicalScope = scopeHolders.lastScopeHolder.getImpl.actualBody
  var finalBindings = initOrderedTable[string, NimNode]()

  for k, v in bindings(lexicalScope, skip = 1):
    finalBindings[k] = v

  for k, v in bindings(logStmtProps, skip = 1):
    finalBindings[k] = v

  finalBindings.sort(system.cmp)

  let eventName = logStmtProps[0]
  assert eventName.kind in {nnkStrLit}
  let record = genSym(nskVar, "record")

  result = quote:
    var `record`: StdoutLogRecord
    setEventName(`record`, `eventName`)

  var isFirst = true
  for k, v in finalBindings:
    result.add newCall(newIdentNode"setProperty",
                       record, newLit(k), v, newLit(isFirst))
    isFirst = false

  result.add newCall(newIdentNode"flushRecord", record)

template log(props: varargs[untyped]) {.dirty.} =
  logImpl(bindSym"scopeHolder", props)

scope:
  a = 12
  b = "original-b"

scope:
  x = 16
  b = "overriden-b"

scope:
  c = 100

proc main =
  scope:
    c = 10

  scope:
    z = 20

  log("main started", a = 10, b = "inner-b", d = "some-d")

main()

log("exiting", msg = "bye bye")