flowscript-vm 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
flowscript/parser.py ADDED
@@ -0,0 +1,578 @@
1
+ """FlowScript parser: tokens -> AST.
2
+
3
+ The AST is made of plain dicts (JSON ready). It *is* the visual model: the flowchart
4
+ is rendered from it and the formatter turns it back into source. Every node has
5
+ ``id`` (unique per parse), ``type`` and ``line``; statements and declarations may
6
+ carry a ``note`` (comment text shown as an annotation).
7
+ """
8
+
9
+ import sys
10
+
11
+ from .lexer import tokenize, FlowError, TYPE_KEYWORDS
12
+
13
+ BINARY_LEVELS = [
14
+ ("or",),
15
+ ("xor",),
16
+ ("and",),
17
+ None, # not (prefix)
18
+ ("==", "!=", "<", "<=", ">", ">="),
19
+ ("|",),
20
+ ("^",),
21
+ ("&",),
22
+ ("<<", ">>"),
23
+ ("+", "-"),
24
+ ("*", "/", "%"),
25
+ ]
26
+ COMPARE_OPS = ("==", "!=", "<", "<=", ">", ">=")
27
+ MAX_NESTING = 100 # parenthesis depth; keeps parser/compiler recursion bounded
28
+
29
+ # expressions are parsed/compiled/printed recursively (~15 frames per nesting level)
30
+ if sys.getrecursionlimit() < 6000:
31
+ sys.setrecursionlimit(6000)
32
+
33
+
34
+ class Parser:
35
+ def __init__(self, src):
36
+ self.toks = tokenize(src)
37
+ self.pos = 0
38
+ self.next_id = 1
39
+ self.depth = 0
40
+
41
+ # -- helpers -----------------------------------------------------------
42
+ def node(self, type_, tok, **fields):
43
+ d = {"id": self.next_id, "type": type_, "line": tok.line if tok else 0}
44
+ self.next_id += 1
45
+ d.update(fields)
46
+ return d
47
+
48
+ @property
49
+ def tok(self):
50
+ return self.toks[self.pos]
51
+
52
+ def peek(self, k=1):
53
+ return self.toks[min(self.pos + k, len(self.toks) - 1)]
54
+
55
+ def advance(self):
56
+ t = self.toks[self.pos]
57
+ self.pos += 1
58
+ return t
59
+
60
+ def is_kw(self, *words):
61
+ return self.tok.kind == "KW" and self.tok.value in words
62
+
63
+ def is_op(self, *ops):
64
+ return self.tok.kind == "OP" and self.tok.value in ops
65
+
66
+ def error(self, msg, tok=None):
67
+ tok = tok or self.tok
68
+ return FlowError(msg, tok.line, tok.col)
69
+
70
+ def expect_kw(self, word):
71
+ if not self.is_kw(word):
72
+ raise self.error("expected '%s'" % word)
73
+ return self.advance()
74
+
75
+ def expect_op(self, op):
76
+ if not self.is_op(op):
77
+ raise self.error("expected '%s'" % op)
78
+ return self.advance()
79
+
80
+ def expect_name(self, what="name"):
81
+ if self.tok.kind != "NAME":
82
+ if self.tok.kind == "KW":
83
+ raise self.error("'%s' is a keyword and cannot be used as %s" % (self.tok.value, what))
84
+ raise self.error("expected %s" % what)
85
+ return self.advance().value
86
+
87
+ def end_of_line(self, node=None):
88
+ """Consume an optional trailing comment and the newline."""
89
+ if self.tok.kind == "COMMENT":
90
+ text = self.advance().value
91
+ if node is not None and text:
92
+ node["note"] = (node["note"] + "\n" + text) if node.get("note") else text
93
+ if self.tok.kind == "EOF":
94
+ return
95
+ if self.tok.kind != "NEWLINE":
96
+ raise self.error("unexpected %s at end of statement" % self.describe(self.tok))
97
+ self.advance()
98
+
99
+ @staticmethod
100
+ def describe(tok):
101
+ if tok.kind == "INDENT":
102
+ return "indentation"
103
+ if tok.kind in ("OP", "KW"):
104
+ return "'%s'" % tok.value
105
+ if tok.kind == "NAME":
106
+ return "name '%s'" % tok.value
107
+ if tok.kind in ("NUM", "FLOAT"):
108
+ return "number"
109
+ return tok.kind.lower()
110
+
111
+ def gather(self, out):
112
+ """Skip blank lines and comments.
113
+
114
+ Returns the note (comment lines directly above the next construct) or None.
115
+ Comment groups followed by a blank line or by the end of a block are emitted
116
+ into ``out`` as stand-alone comment nodes.
117
+ """
118
+ group = []
119
+ group_tok = None
120
+ prev_newline = True
121
+ while True:
122
+ t = self.tok
123
+ if t.kind == "COMMENT":
124
+ if not group:
125
+ group_tok = t
126
+ group.append(t.value)
127
+ self.advance()
128
+ prev_newline = False
129
+ continue
130
+ if t.kind == "NEWLINE":
131
+ self.advance()
132
+ if prev_newline and group:
133
+ out.append(self.node("comment", group_tok, text="\n".join(group)))
134
+ group = []
135
+ prev_newline = True
136
+ continue
137
+ break
138
+ if group:
139
+ if t.kind in ("EOF", "DEDENT"):
140
+ out.append(self.node("comment", group_tok, text="\n".join(group)))
141
+ return None
142
+ return "\n".join(group)
143
+ return None
144
+
145
+ def colon(self, what):
146
+ if not self.is_op(":"):
147
+ raise self.error("expected ':' at the end of the %s line" % what)
148
+ self.advance()
149
+
150
+ def open_block(self, node, what):
151
+ """``:`` + end of line + INDENT."""
152
+ self.colon(what)
153
+ self.end_of_line(node)
154
+ if self.tok.kind != "INDENT":
155
+ raise self.error("expected an indented block after the %s line" % what)
156
+ self.advance()
157
+
158
+ def skip_pass(self, out, note):
159
+ """'pass' marks an empty block; a note above it is kept as a comment node."""
160
+ t = self.advance()
161
+ if note:
162
+ out.append(self.node("comment", t, text=note))
163
+ self.end_of_line()
164
+
165
+ @staticmethod
166
+ def attach(node, note):
167
+ if note:
168
+ node["note"] = note + ("\n" + node["note"] if node.get("note") else "")
169
+ return node
170
+
171
+ # -- program -----------------------------------------------------------
172
+ def parse_program(self):
173
+ prog = {"id": 0, "type": "program", "line": 1, "name": None, "version": 0, "items": []}
174
+ items = prog["items"]
175
+ pre = []
176
+ note = self.gather(pre)
177
+ if self.is_kw("flow"):
178
+ prog["pre"] = pre
179
+ self.advance()
180
+ prog["name"] = self.expect_name("program name")
181
+ if self.is_kw("version"):
182
+ self.advance()
183
+ if self.tok.kind != "NUM":
184
+ raise self.error("expected version number")
185
+ prog["version"] = self.advance().value
186
+ if note:
187
+ prog["note"] = note
188
+ self.end_of_line(prog)
189
+ note = self.gather(items)
190
+ else:
191
+ items.extend(pre)
192
+ while self.tok.kind != "EOF":
193
+ item = self.parse_item()
194
+ self.attach(item, note)
195
+ items.append(item)
196
+ note = self.gather(items)
197
+ return prog
198
+
199
+ def parse_params(self, typed=False):
200
+ """'(a, b)' -> ([names], None) or, typed, '(int a, float b)' -> ([names], [types]).
201
+
202
+ Returns (None, None) if there is no parameter list."""
203
+ params, types = [], []
204
+ if self.is_op("("):
205
+ self.advance()
206
+ if not self.is_op(")"):
207
+ while True:
208
+ if typed:
209
+ if not self.is_type():
210
+ if self.tok.kind == "NAME":
211
+ raise self.error("give the parameter a type: 'int %s' (int, float, str or time)"
212
+ % self.tok.value)
213
+ raise self.error("expected a parameter type (int, float, str or time)")
214
+ types.append(self.advance().value)
215
+ params.append(self.expect_name("parameter name"))
216
+ if self.is_op(","):
217
+ self.advance()
218
+ continue
219
+ break
220
+ self.expect_op(")")
221
+ return params, (types if typed else None)
222
+ return None, None
223
+
224
+ def is_type(self):
225
+ return self.tok.kind == "KW" and self.tok.value in TYPE_KEYWORDS
226
+
227
+ def parse_returns(self):
228
+ """Optional '-> type'."""
229
+ if not self.is_op("->"):
230
+ return None
231
+ self.advance()
232
+ if not self.is_type():
233
+ raise self.error("expected a return type after '->' (int, float, str or time)")
234
+ return self.advance().value
235
+
236
+ def var_hint(self):
237
+ """'var x' was FlowScript v1: point to the typed declarations."""
238
+ if self.tok.kind == "NAME" and self.tok.value == "var" and self.peek().kind == "NAME":
239
+ raise self.error("declare variables with their type: 'int %s', 'float %s', 'str %s' or 'time %s'"
240
+ % ((self.peek().value,) * 4))
241
+
242
+ def parse_decl(self, node_type):
243
+ """'TYPE name [= expr]' (globals, state variables, locals)."""
244
+ t = self.advance()
245
+ name = self.expect_name("variable name")
246
+ value = None
247
+ if self.is_op("="):
248
+ self.advance()
249
+ value = self.parse_expr()
250
+ n = self.node(node_type, t, name=name, vtype=t.value, value=value)
251
+ self.end_of_line(n)
252
+ return n
253
+
254
+ def parse_item(self):
255
+ t = self.tok
256
+ if t.kind == "INDENT":
257
+ raise self.error("unexpected indentation")
258
+ self.check_end()
259
+ self.var_hint()
260
+ if t.kind != "KW":
261
+ raise self.error("expected a declaration (extern, event, timer, const, int/float/str/time "
262
+ "variable, func, state, initial state, any_state)")
263
+ kw = t.value
264
+ if kw in TYPE_KEYWORDS:
265
+ return self.parse_decl("var")
266
+ if kw == "extern":
267
+ self.advance()
268
+ name = self.expect_name("extern name")
269
+ params, types = self.parse_params(typed=True)
270
+ if params is None:
271
+ raise self.error("extern needs a parameter list, e.g. extern %s()" % name)
272
+ n = self.node("extern", t, name=name, params=params, ptypes=types, returns=self.parse_returns())
273
+ self.end_of_line(n)
274
+ return n
275
+ if kw == "event":
276
+ self.advance()
277
+ name = self.expect_name("event name")
278
+ params, types = self.parse_params(typed=True)
279
+ n = self.node("event", t, name=name, params=params or [], ptypes=types or [])
280
+ self.end_of_line(n)
281
+ return n
282
+ if kw == "timer":
283
+ self.advance()
284
+ n = self.node("timer", t, name=self.expect_name("timer name"))
285
+ self.end_of_line(n)
286
+ return n
287
+ if kw == "const":
288
+ self.advance()
289
+ name = self.expect_name()
290
+ if not self.is_op("="):
291
+ raise self.error("const needs a value")
292
+ self.advance()
293
+ n = self.node(kw, t, name=name, value=self.parse_expr())
294
+ self.end_of_line(n)
295
+ return n
296
+ if kw == "func":
297
+ self.advance()
298
+ name = self.expect_name("function name")
299
+ params, types = self.parse_params(typed=True)
300
+ if params is None:
301
+ raise self.error("func needs a parameter list, e.g. func %s()" % name)
302
+ n = self.node("func", t, name=name, params=params, ptypes=types, returns=self.parse_returns(), body=[])
303
+ self.open_block(n, "func")
304
+ n["body"] = self.parse_block()
305
+ return n
306
+ if kw == "on":
307
+ raise self.error("event handlers go inside a state, or inside 'any_state:' to "
308
+ "handle the event in every state")
309
+ if kw == "initial":
310
+ self.advance()
311
+ if not self.is_kw("state"):
312
+ raise self.error("'initial' marks a state: write 'initial state NAME:'")
313
+ n = self.parse_item()
314
+ n["initial"] = True
315
+ return n
316
+ if kw in ("state", "any_state"):
317
+ self.advance()
318
+ if kw == "state":
319
+ n = self.node("state", t, name=self.expect_name("state name"), vars=[], handlers=[])
320
+ else:
321
+ n = self.node("any_state", t, handlers=[])
322
+ self.open_block(n, kw)
323
+ while True:
324
+ loose = []
325
+ note = self.gather(loose)
326
+ # state variables come first; comments above them stay with them
327
+ in_vars = kw == "state" and self.is_type() and not n["handlers"]
328
+ (n["vars"] if in_vars else n["handlers"]).extend(loose)
329
+ if self.tok.kind == "DEDENT":
330
+ self.advance()
331
+ break
332
+ if self.is_kw("pass"):
333
+ self.skip_pass(n["handlers"], note)
334
+ continue
335
+ self.var_hint()
336
+ if self.is_type():
337
+ if kw == "any_state":
338
+ raise self.error("variables shared by every state are ordinary globals: "
339
+ "declare them at the top level")
340
+ if n["handlers"]:
341
+ raise self.error("state variables go at the top of the state, "
342
+ "before its handlers")
343
+ n["vars"].append(self.attach(self.parse_decl("var"), note))
344
+ continue
345
+ if not self.is_kw("on"):
346
+ raise self.error("only variables (int/float/str/time) and 'on' handlers are allowed inside a state" if kw == "state"
347
+ else "only 'on' handlers are allowed inside any_state")
348
+ n["handlers"].append(self.attach(self.parse_handler(), note))
349
+ return n
350
+ raise self.error("unexpected '%s' at top level" % kw)
351
+
352
+ def parse_handler(self):
353
+ t = self.expect_kw("on")
354
+ if self.tok.kind == "NAME" or self.is_kw("timer", "event"):
355
+ event = self.advance().value
356
+ else:
357
+ raise self.error("expected event name")
358
+ params, _ = self.parse_params()
359
+ n = self.node("on", t, event=event, params=params, body=[])
360
+ self.open_block(n, "on")
361
+ n["body"] = self.parse_block()
362
+ return n
363
+
364
+ # -- statements --------------------------------------------------------
365
+ def check_end(self):
366
+ t = self.tok
367
+ if t.kind == "NAME" and t.value == "goto" and self.peek().kind == "NAME":
368
+ raise self.error("use 'goto_state %s' to change state" % self.peek().value)
369
+ if t.kind == "NAME" and t.value == "emit" and self.peek().kind == "NAME":
370
+ raise self.error("use 'raise_event %s' to raise an event" % self.peek().value)
371
+ if t.kind == "NAME" and t.value == "end" and self.peek().kind in ("NEWLINE", "COMMENT", "EOF"):
372
+ raise self.error("'end' is not used: a block ends where the indentation goes back")
373
+
374
+ def parse_block(self):
375
+ """Statements up to the DEDENT that closes the block (INDENT already consumed)."""
376
+ body = []
377
+ while True:
378
+ note = self.gather(body)
379
+ if self.tok.kind == "DEDENT":
380
+ self.advance()
381
+ return body
382
+ if self.is_kw("pass"):
383
+ self.skip_pass(body, note)
384
+ continue
385
+ body.append(self.attach(self.parse_stmt(), note))
386
+
387
+ def parse_stmt(self):
388
+ t = self.tok
389
+ if t.kind == "INDENT":
390
+ raise self.error("unexpected indentation")
391
+ self.check_end()
392
+ self.var_hint()
393
+ if t.kind == "KW":
394
+ kw = t.value
395
+ if kw in TYPE_KEYWORDS and self.peek().kind == "NAME":
396
+ return self.parse_decl("local")
397
+ if kw in TYPE_KEYWORDS and self.peek().kind == "OP" and self.peek().value == "(":
398
+ call = self.parse_primary()
399
+ n = self.node("call", t, name=call["name"], args=call["args"])
400
+ self.end_of_line(n)
401
+ return n
402
+ if kw == "if":
403
+ self.advance()
404
+ n = self.node("if", t, branches=[], else_body=None)
405
+ cond = self.parse_expr()
406
+ self.open_block(n, "if")
407
+ n["branches"].append({"cond": cond, "body": self.parse_block()})
408
+ while self.is_kw("elif"):
409
+ self.advance()
410
+ cond = self.parse_expr()
411
+ self.open_block(None, "elif")
412
+ n["branches"].append({"cond": cond, "body": self.parse_block()})
413
+ if self.is_kw("else"):
414
+ self.advance()
415
+ self.open_block(None, "else")
416
+ n["else_body"] = self.parse_block()
417
+ return n
418
+ if kw == "while":
419
+ self.advance()
420
+ n = self.node("while", t, cond=None, body=[])
421
+ n["cond"] = self.parse_expr()
422
+ self.open_block(n, "while")
423
+ n["body"] = self.parse_block()
424
+ return n
425
+ if kw == "goto_state":
426
+ self.advance()
427
+ n = self.node("goto", t, state=self.expect_name("state name"))
428
+ self.end_of_line(n)
429
+ return n
430
+ if kw == "raise_event":
431
+ self.advance()
432
+ name = self.expect_name("event name")
433
+ args = self.parse_args() if self.is_op("(") else []
434
+ n = self.node("emit", t, event=name, args=args)
435
+ self.end_of_line(n)
436
+ return n
437
+ if kw == "start":
438
+ self.advance()
439
+ name = self.expect_name("timer name")
440
+ every = False
441
+ if self.is_kw("every"):
442
+ self.advance()
443
+ every = True
444
+ n = self.node("start", t, timer=name, every=every, ms=self.parse_expr())
445
+ self.end_of_line(n)
446
+ return n
447
+ if kw == "stop":
448
+ self.advance()
449
+ n = self.node("stop", t, timer=self.expect_name("timer name"))
450
+ self.end_of_line(n)
451
+ return n
452
+ if kw in ("elif", "else"):
453
+ raise self.error("'%s' without a matching 'if' (check the indentation)" % kw)
454
+ if kw == "return":
455
+ self.advance()
456
+ value = None
457
+ if self.tok.kind not in ("NEWLINE", "COMMENT", "EOF"):
458
+ value = self.parse_expr()
459
+ n = self.node("return", t, value=value)
460
+ self.end_of_line(n)
461
+ return n
462
+ raise self.error("unexpected '%s'" % kw)
463
+ if t.kind == "NAME":
464
+ nxt = self.peek()
465
+ if nxt.kind == "OP" and nxt.value == "=":
466
+ name = self.advance().value
467
+ self.advance()
468
+ n = self.node("assign", t, target=name, value=self.parse_expr())
469
+ self.end_of_line(n)
470
+ return n
471
+ if nxt.kind == "OP" and nxt.value == "(":
472
+ call = self.parse_primary()
473
+ n = self.node("call", t, name=call["name"], args=call["args"])
474
+ self.end_of_line(n)
475
+ return n
476
+ raise self.error("expected '=' or '(' after '%s'" % t.value, nxt)
477
+ raise self.error("expected a statement")
478
+
479
+ # -- expressions -------------------------------------------------------
480
+ def parse_args(self):
481
+ self.expect_op("(")
482
+ args = []
483
+ if not self.is_op(")"):
484
+ while True:
485
+ args.append(self.parse_expr())
486
+ if self.is_op(","):
487
+ self.advance()
488
+ continue
489
+ break
490
+ self.expect_op(")")
491
+ return args
492
+
493
+ def parse_expr(self):
494
+ return self.parse_level(0)
495
+
496
+ def parse_level(self, level):
497
+ if level >= len(BINARY_LEVELS):
498
+ return self.parse_unary()
499
+ ops = BINARY_LEVELS[level]
500
+ if ops is None: # prefix 'not'
501
+ if self.is_kw("not"):
502
+ t = self.advance()
503
+ n = self.node("unary", t, op="not", operand=None)
504
+ n["operand"] = self.parse_level(level)
505
+ return n
506
+ return self.parse_level(level + 1)
507
+ left = self.parse_level(level + 1)
508
+ while self.tok.kind in ("OP", "KW") and self.tok.value in ops:
509
+ t = self.advance()
510
+ right = self.parse_level(level + 1)
511
+ n = self.node("binary", t, op=t.value, left=left, right=right)
512
+ if t.value in COMPARE_OPS and self.tok.kind == "OP" and self.tok.value in COMPARE_OPS:
513
+ raise self.error("comparisons cannot be chained; use 'and'")
514
+ left = n
515
+ return left
516
+
517
+ def parse_unary(self):
518
+ if self.is_op("-", "~"):
519
+ t = self.advance()
520
+ operand = self.parse_unary()
521
+ if t.value == "-" and operand["type"] == "num" and operand["value"] >= 0:
522
+ operand["value"] = -operand["value"]
523
+ operand["text"] = "-" + operand.get("text", str(-operand["value"]))
524
+ return operand
525
+ n = self.node("unary", t, op=t.value, operand=operand)
526
+ return n
527
+ return self.parse_primary()
528
+
529
+ def parse_primary(self):
530
+ t = self.tok
531
+ if t.kind in ("NUM", "FLOAT"):
532
+ self.advance()
533
+ return self.node("num", t, value=t.value, text=t.text)
534
+ if t.kind == "KW" and t.value in TYPE_KEYWORDS and self.peek().kind == "OP" and self.peek().value == "(":
535
+ self.advance()
536
+ n = self.node("callx", t, name=t.value, args=[])
537
+ n["args"] = self.parse_args()
538
+ return n
539
+ if t.kind == "STR":
540
+ self.advance()
541
+ return self.node("str", t, value=t.value)
542
+ if t.kind == "KW" and t.value in ("true", "false"):
543
+ self.advance()
544
+ return self.node("bool", t, value=1 if t.value == "true" else 0)
545
+ if t.kind == "NAME":
546
+ self.advance()
547
+ if self.is_op("("):
548
+ n = self.node("callx", t, name=t.value, args=[])
549
+ n["args"] = self.parse_args()
550
+ return n
551
+ return self.node("name", t, name=t.value)
552
+ if self.is_op("("):
553
+ self.advance()
554
+ self.depth += 1
555
+ if self.depth > MAX_NESTING:
556
+ raise self.error("expression nested too deeply (max %d levels)" % MAX_NESTING)
557
+ e = self.parse_expr()
558
+ self.depth -= 1
559
+ self.expect_op(")")
560
+ return e
561
+ raise self.error("expected an expression, got %s" % self.describe(t))
562
+
563
+
564
+ def parse(src):
565
+ try:
566
+ return Parser(src).parse_program()
567
+ except RecursionError:
568
+ raise FlowError("program is nested too deeply")
569
+
570
+
571
+ def parse_expression(text):
572
+ p = Parser(text.strip())
573
+ while p.tok.kind == "NEWLINE":
574
+ p.advance()
575
+ e = p.parse_expr()
576
+ if p.tok.kind not in ("NEWLINE", "DEDENT", "EOF"):
577
+ raise p.error("unexpected %s after expression" % p.describe(p.tok))
578
+ return e