diff --git a/.gitignore b/.gitignore index 5e17a8a..9d60cde 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ # binary executable main +dist/ # nimble and nim specifics nimcache/ diff --git a/src/error.nim b/src/error.nim index 782dc6b..e3210b8 100644 --- a/src/error.nim +++ b/src/error.nim @@ -6,7 +6,7 @@ type #Cause = object # matchLen*, matchMax*: Natural ErrorType* = enum - errTransient, errSameEvent + errTransient, errSameEvent, errInfiniteLoop SyntaxError* = NPegException SemanticError* = object of CatchableError @@ -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: diff --git a/src/graph.nim b/src/graph.nim index c505dc3..487c688 100644 --- a/src/graph.nim +++ b/src/graph.nim @@ -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]()) diff --git a/src/parser.nim b/src/parser.nim index 0bf3861..441f0ec 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -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 @@ -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) @@ -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