Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

# binary executable
main
dist/

# nimble and nim specifics
nimcache/
Expand Down
3 changes: 2 additions & 1 deletion src/error.nim
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type
#Cause = object
# matchLen*, matchMax*: Natural
ErrorType* = enum
errTransient, errSameEvent
errTransient, errSameEvent, errInfiniteLoop

SyntaxError* = NPegException
SemanticError* = object of CatchableError
Expand All @@ -27,6 +27,7 @@ proc `$`(kind: ErrorType): string =
case kind:
of errTransient: "is transient state. It must not have another transition"
of errSameEvent: "state must not have transitions with the same event"
of errInfiniteLoop: "state is looping. Loop transition must be triggered by event"

proc explain*(e: ref SemanticError, source: string): string =
for state in e.causes.keys:
Expand Down
8 changes: 3 additions & 5 deletions src/graph.nim
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,9 @@ proc addEdge*(transition: var StateDiagram,
current: string, next: string, trigger: string) =
case transition.diagram:
of TransitionTable:
if current.State in transition.table:
transition.table[current.State][trigger.Event] = next.State
else:
transition.table.add(current.State,
{trigger.Event: next.State}.toTable)
if transition.table.hasKeyOrPut(current.State,
toTable {trigger.Event: next.State}):
transition.table[current.State].add(trigger.Event, next.State)

if next.State notin toSeq(transition.table.keys):
transition.table.add(next.State, initTable[Event, State]())
Expand Down
80 changes: 49 additions & 31 deletions src/parser.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import sets, tables, unicode, sequtils, sugar
import sets, tables, strformat, unicode, sequtils, sugar
import npeg, npeg/lib/utf8

import graph, error
Expand All @@ -7,46 +7,50 @@ import graph, error
type
Track = Table[State, Table[Event, Natural]]

Arrow {.pure.} = enum
Forward, Backward, Bidirectional
Transition {.pure.} = enum
Normal, Loop, Bidirectional


proc parse*(graph: var StateDiagram, input: string) =
proc parse*(diagram: var StateDiagram, input: string) =
var
loc: Natural = 1 # workaround since matchLen/Max not in NPeg codeblock 😞
loc: Natural = 1 # TODO: remove this and use NPeg @n in codeblock to get the match index
track: Track
error = new SemanticError
current, next: string
direction: Arrow
g = graph
currents: seq[string]
next: string
direction: Transition
g = diagram

grammar "arrow": # I wonder if NPeg can pass value like pom-rs 🤔
forward <- +'-' * '>':
direction = Forward
backward <- '<' * +'-':
direction = Backward
forward <- +'-' * '>' * >?'>':
direction = if $1 == "": Normal else: Loop
backward <- '<' * >?'<' * +'-':
direction = if $1 == "": Normal else: Loop
bidirectional <- '<' * +'-' * '>':
direction = Bidirectional

let parser = peg "result":
Newline <- {'\c', '\n'}: loc += 1 #TODO: make PR for adding this in NPeg
# TODO: make PR for adding Newline in NPeg since Nim has this by default
Newline <- {'\c', '\n'}: loc += 1
state(format) <- *Blank * format * *Blank
event(format) <- *Blank * '@' * *Blank * format
sep(sep, value) <- +(value * ?(*Blank * sep * *Blank))

comment <- '#' * *(utf8.any - {'\c', '\n'})
tIdent <- +( (Alpha * ?Digit) | '_') # any non-space case
tOps <- arrow.bidirectional | arrow.forward | arrow.backward |
E"must be one of ->, <-, <->"
ident <- +((Alpha * ?Digit)|'_') # any non-space case

result <- +( *(comment|Newline) * transition)
result <- +( *(comment|Newline) * transition): # reset temp value
(currents, next) = (newSeq[string](), "")


transition <- transient * >?event( > tIdent|E"missing event name"):
transition <- transient * >?event( > ident|E"missing event name"):
let trigger = if $1 == "": "" else: $2

proc errCheck(current: string) = # TODO: refactor this
# TODO: refactor this. Also fully use SemanticError and remove every err-related tmp-val
proc errCheck(current: string) =
if track.hasKeyOrPut(current.State, {trigger.Event: loc}.toTable):
track[current.State].add(trigger.Event, loc)

if current.State in g.transient or
track[current.State].len > 1 and trigger == "":
let events = toSeq(track[current.State].keys)
Expand All @@ -55,29 +59,43 @@ proc parse*(graph: var StateDiagram, input: string) =
g.error[current.State].incl(events)
let lines = toSeq(track[current.State].values)
error.addCause(current.State, errTransient, lines)

if trigger.Event in g[current]:
if g.error.hasKeyOrPut(current.State, [trigger].toHashSet):
g.error[current.State].incl(trigger)
let line = track[current.State][trigger.Event]
error.addCause(current.State, errSameEvent, [line, loc].toSeq)

errCheck(current)
if direction == Bidirectional:
errCheck(next)
if next in currents and trigger == "":
if g.error.hasKeyOrPut(next.State, [trigger].toHashSet):
g.error[next.State].incl(trigger)
error.addCause(next.State, errInfiniteLoop, [loc].toSeq)

g.addEdge(current, next, trigger)
if direction == Bidirectional:
g.addEdge(next, current, trigger)
case direction:
of Bidirectional, Loop: errCheck(next)
else: discard
for current in currents:
errCheck(current)
g.addEdge(current, next, trigger)
if direction == Bidirectional: g.addEdge(next, current, trigger)
if direction == Loop: g.addEdge(next, next, trigger)


transient <- state( > tIdent) * tOps *
state( > tIdent|E"missing state name"):
case direction:
of Forward, Bidirectional: (current, next) = ($1, $2)
of Backward: (current, next) = ($2, $1)
transient <- forward|bidirectional|backward

# TODO: prevent same states by using back references
forward <- state(sep(',', >ident)) * arrow.forward * state( >ident * !','):
for c in capture[1..^2]: currents.add(c.s)
next = capture[^1].s
backward <- state( >ident * !',') * arrow.backward * state(sep(',', >ident)):
for c in capture[2..^1]: currents.add(c.s)
next = capture[1].s
bidirectional <- state( >ident * !',') * arrow.bidirectional * state( >ident * !','):
currents.add($1) ; next = $2


let p = parser.match(input)
if p.ok: graph = g
if p.ok: diagram = g
else: raise newException(SyntaxError, &"Syntax error at {p.matchLen}:{p.matchMax}")
if error.causes.len > 0:
raise error