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.
@@ -0,0 +1,216 @@
1
+ :root {
2
+ --bg: #15181e; --bg2: #1c2028; --bg3: #252a34; --line: #303643; --fg: #d7dce5; --muted: #7d8696;
3
+ --accent: #5aa9ff; --cur: #ffcc4d; --cur-bg: rgba(255, 204, 77, .16); --stmt-bg: rgba(90, 169, 255, .09);
4
+ --ok: #4fd18b; --bad: #ff6b6b; --warn: #ffb454; --kw: #c792ea; --num: #f78c6c; --str: #c3e88d;
5
+ --cmt: #697386; --fn: #82aaff; --ev: #ffcb6b;
6
+ --n-proc: #2b3a55; --n-io: #2f4a3a; --n-sub: #3b3357; --n-dec: #4a3b22; --n-goto: #5a2c3a;
7
+ --n-emit: #2a4b55; --n-timer: #3d4423; --n-ret: #3a3a3a; --n-ev: #55401f; --n-note: #2a2d33;
8
+ --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
9
+ }
10
+ * { box-sizing: border-box; }
11
+ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--fg);
12
+ font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
13
+ body { display: flex; flex-direction: column; overflow: hidden; }
14
+ button, select, input { font: inherit; color: var(--fg); background: var(--bg3); border: 1px solid var(--line);
15
+ border-radius: 5px; padding: 3px 8px; }
16
+ button { cursor: pointer; white-space: nowrap; }
17
+ button:hover { border-color: var(--accent); }
18
+ button.primary { background: #1f4e86; border-color: #2f6db5; }
19
+ button.small { padding: 1px 6px; font-size: 11px; }
20
+ button:disabled { opacity: .45; cursor: default; }
21
+ input:focus, select:focus, textarea:focus { outline: 1px solid var(--accent); }
22
+ .muted { color: var(--muted); font-weight: normal; }
23
+ .hidden { display: none !important; }
24
+ .chk { display: inline-flex; align-items: center; gap: 3px; color: var(--muted); font-size: 12px; }
25
+
26
+ #toolbar { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; padding: 6px 10px;
27
+ background: var(--bg2); border-bottom: 1px solid var(--line); }
28
+ #toolbar .group { display: flex; gap: 4px; align-items: center; }
29
+ .brand { font-weight: 600; letter-spacing: .3px; }
30
+ .logo { color: var(--accent); }
31
+ .status { margin-left: auto; font-family: var(--mono); font-size: 12px; padding: 2px 8px; border-radius: 4px; }
32
+ .status.ok { color: var(--ok); } .status.bad { color: var(--bad); } .status.run { color: var(--cur); }
33
+
34
+ #views { flex: 1; display: grid; grid-template-columns: 1fr 5px .8fr 5px 1.4fr; min-height: 0; }
35
+ .splitter { background: var(--line); cursor: col-resize; }
36
+ .splitter:hover { background: var(--accent); }
37
+ .pane { display: flex; flex-direction: column; min-width: 0; min-height: 0; position: relative; }
38
+ .pane-head { display: flex; gap: 10px; align-items: center; padding: 4px 8px; background: var(--bg2);
39
+ border-bottom: 1px solid var(--line); min-height: 30px; flex-wrap: wrap; }
40
+ .pane-title { font-weight: 600; }
41
+ .seg { display: inline-flex; }
42
+ .seg button { border-radius: 0; margin-left: -1px; padding: 1px 8px; }
43
+ .seg button:first-child { border-radius: 5px 0 0 5px; }
44
+ .seg button:last-child { border-radius: 0 5px 5px 0; }
45
+ .seg button.on { background: #1f4e86; }
46
+
47
+ /* --- source editor --- */
48
+ .editor { flex: 1; display: flex; overflow: auto; font: 13px/19px var(--mono); position: relative; min-height: 0; }
49
+ .gutter { flex: none; min-width: 46px; padding: 6px 0; text-align: right; color: var(--muted); user-select: none;
50
+ background: var(--bg2); border-right: 1px solid var(--line); position: sticky; left: 0; z-index: 2; }
51
+ .gutter div { padding: 0 8px 0 16px; height: 19px; cursor: pointer; position: relative; }
52
+ .gutter div.bp::before { content: ""; position: absolute; left: 4px; top: 5px; width: 9px; height: 9px;
53
+ border-radius: 50%; background: var(--bad); }
54
+ .gutter div.cur { color: var(--cur); font-weight: bold; }
55
+ .gutter div.err { color: var(--bad); }
56
+ .code-wrap { position: relative; flex: 1; min-width: max-content; }
57
+ .code-hl, #code { margin: 0; padding: 6px 10px; font: inherit; white-space: pre; tab-size: 2; border: 0; }
58
+ .code-hl { pointer-events: none; color: var(--fg); min-height: 100%; }
59
+ #code { position: absolute; inset: 0; width: 100%; height: 100%; resize: none; background: transparent;
60
+ color: transparent; caret-color: var(--fg); overflow: hidden; outline: none; }
61
+ #code::selection { background: rgba(90, 169, 255, .35); }
62
+ .code-hl .ln { display: block; height: 19px; }
63
+ .code-hl .ln.cur { background: var(--cur-bg); box-shadow: inset 3px 0 0 var(--cur); }
64
+ .code-hl .ln.err { background: rgba(255, 107, 107, .15); }
65
+ .code-hl .ln.hov { background: rgba(255, 255, 255, .05); }
66
+ .code-hl .tk-kw { color: var(--kw); } .code-hl .tk-num { color: var(--num); } .code-hl .tk-str { color: var(--str); }
67
+ .code-hl .tk-cmt { color: var(--cmt); font-style: italic; } .code-hl .tk-fn { color: var(--fn); }
68
+ .code-hl .tk-op { color: #89ddff; } .code-hl .tk-ev { color: var(--ev); }
69
+ .code-hl .seg-cur { background: rgba(255, 204, 77, .35); border-radius: 2px; }
70
+ #errors { font-family: var(--mono); font-size: 12px; color: var(--bad); padding: 0 8px; max-height: 80px; overflow: auto; }
71
+ #errors div { padding: 4px 0; cursor: pointer; }
72
+
73
+ /* --- bytecode --- */
74
+ .bytecode { flex: 1; overflow: auto; font: 12px/18px var(--mono); padding: 4px 0; }
75
+ .bytecode .lbl { color: var(--ev); padding: 8px 8px 2px; font-weight: bold; }
76
+ .bytecode .ins { display: grid; grid-template-columns: 14px 42px 118px 64px 1fr; padding: 0 8px; cursor: default; white-space: nowrap; }
77
+ .bytecode .ins .pcx { color: var(--muted); }
78
+ .bytecode .ins .hex { color: #5f6a7c; overflow: hidden; text-overflow: ellipsis; }
79
+ .bytecode .ins .op { color: var(--kw); }
80
+ .bytecode .ins .arg { color: var(--fg); overflow: hidden; text-overflow: ellipsis; }
81
+ .bytecode .ins .arg i { color: var(--cmt); font-style: normal; }
82
+ .bytecode .ins.stmt { background: var(--stmt-bg); }
83
+ .bytecode .ins.cur { background: var(--cur-bg); box-shadow: inset 3px 0 0 var(--cur); }
84
+ .bytecode .ins.cur .mk::before { content: "▶"; color: var(--cur); }
85
+ .bytecode .ins.hov { background: rgba(255, 255, 255, .06); }
86
+ .bytecode .header { color: var(--muted); padding: 2px 8px 6px; border-bottom: 1px solid var(--line); margin-bottom: 4px; }
87
+
88
+ /* --- flowchart --- */
89
+ .chart-wrap { flex: 1; overflow: auto; background:
90
+ radial-gradient(circle, #232833 1px, transparent 1px) 0 0 / 18px 18px; min-height: 0; }
91
+ #chart svg { display: block; }
92
+ #chart text { font: 12px var(--mono); fill: var(--fg); pointer-events: none; }
93
+ #chart .small { font-size: 10px; fill: var(--muted); }
94
+ #chart .lbl-yes { fill: var(--ok); font-size: 10px; } #chart .lbl-no { fill: var(--bad); font-size: 10px; }
95
+ #chart .edge { stroke: #6d7789; stroke-width: 1.4; fill: none; }
96
+ #chart .edge.back { stroke-dasharray: 4 3; }
97
+ #chart .wire { stroke: #8792a5; stroke-width: 1.2; fill: none; }
98
+ #chart .wire.t { stroke: var(--ok); } #chart .wire.f { stroke: var(--bad); }
99
+ #chart .nd { cursor: pointer; }
100
+ #chart .nd > .sh { stroke: #8a94a8; stroke-width: 1.2; }
101
+ #chart .nd:hover > .sh, #chart .nd.hov > .sh { stroke: #fff; }
102
+ #chart .nd.sel > .sh { stroke: var(--accent); stroke-width: 2.5; }
103
+ #chart .nd.cur > .sh { stroke: var(--cur); stroke-width: 3; filter: drop-shadow(0 0 6px rgba(255, 204, 77, .7)); }
104
+ #chart .nd.bp > .sh { stroke-dasharray: 5 2; }
105
+ #chart .bpdot { fill: var(--bad); }
106
+ #chart .k-proc > .sh { fill: var(--n-proc); } #chart .k-io > .sh { fill: var(--n-io); }
107
+ #chart .k-sub > .sh { fill: var(--n-sub); } #chart .k-dec > .sh { fill: var(--n-dec); }
108
+ #chart .k-goto > .sh { fill: var(--n-goto); } #chart .k-emit > .sh { fill: var(--n-emit); }
109
+ #chart .k-timer > .sh { fill: var(--n-timer); } #chart .k-ret > .sh { fill: var(--n-ret); }
110
+ #chart .k-ev > .sh { fill: var(--n-ev); } #chart .k-end > .sh { fill: #444; }
111
+ #chart .k-note > .sh { fill: var(--n-note); stroke-dasharray: 3 2; }
112
+ #chart .k-note text { fill: var(--cmt); font-style: italic; }
113
+ #chart .k-leaf > .sh { fill: #222833; }
114
+ #chart .k-gate > .sh { fill: #2a303c; }
115
+ #chart .t > .sh { fill: #1d4430 !important; stroke: var(--ok); }
116
+ #chart .f > .sh { fill: #4a2228 !important; stroke: var(--bad); }
117
+ #chart .val { font-size: 10px; fill: var(--cur); }
118
+ #chart .note-line { stroke: var(--cmt); stroke-dasharray: 2 3; fill: none; }
119
+ #chart .state-box { fill: rgba(90, 169, 255, .04); stroke: #3d5a80; stroke-width: 1.5; }
120
+ #chart .state-box.cur { stroke: var(--cur); fill: rgba(255, 204, 77, .05); }
121
+ #chart .state-title { font: bold 14px var(--mono); fill: #9ec5ff; }
122
+ #chart .state-box.cur + .state-title { fill: var(--cur); }
123
+ #chart .sec-title { font: bold 13px var(--mono); fill: var(--muted); }
124
+ #chart .decl-box { fill: #1b1f27; stroke: var(--line); }
125
+ #chart .decl-k { fill: var(--kw); } #chart .decl-n { fill: var(--fg); }
126
+ #chart .plus { cursor: pointer; }
127
+ #chart .plus circle { fill: #1f4e86; stroke: var(--accent); }
128
+ #chart .plus text { fill: #fff; font-weight: bold; text-anchor: middle; }
129
+ #chart .plus:hover circle { fill: var(--accent); }
130
+ #chart .sd-state > .sh { fill: #22324a; stroke: #5a7fb0; stroke-width: 2; }
131
+ #chart .sd-state.cur > .sh { fill: #4a3d1a; stroke: var(--cur); stroke-width: 3; }
132
+ #chart .sd-edge { stroke: #8792a5; stroke-width: 1.5; fill: none; }
133
+ #chart .sd-label { font-size: 11px; fill: var(--ev); }
134
+ #chart .initial { fill: var(--fg); }
135
+ #chart .addbtn { cursor: pointer; }
136
+ #chart .addbtn rect { fill: #1f3555; stroke: var(--accent); stroke-dasharray: 3 2; }
137
+ #chart .addbtn text { fill: var(--accent); }
138
+
139
+ .inspector { position: absolute; right: 10px; top: 40px; width: 330px; max-height: calc(100% - 50px); overflow: auto;
140
+ background: var(--bg2); border: 1px solid var(--line); border-radius: 8px; padding: 10px; box-shadow: 0 8px 30px rgba(0,0,0,.5); z-index: 5; }
141
+ .inspector h4 { margin: 0 0 8px; display: flex; justify-content: space-between; align-items: center; }
142
+ .inspector label { display: block; color: var(--muted); font-size: 11px; margin: 6px 0 2px; }
143
+ .inspector input[type=text], .inspector textarea, .inspector select { width: 100%; font-family: var(--mono); font-size: 12px;
144
+ background: var(--bg); border: 1px solid var(--line); border-radius: 4px; color: var(--fg); padding: 4px 6px; }
145
+ .inspector textarea { resize: vertical; min-height: 40px; }
146
+ .inspector .row { display: flex; gap: 4px; flex-wrap: wrap; margin-top: 8px; }
147
+ .inspector .err { color: var(--bad); font-size: 12px; margin-top: 6px; }
148
+ .inspector .hint { color: var(--muted); font-size: 11px; margin-top: 6px; }
149
+ .inspector .danger { border-color: #7a3030; }
150
+
151
+ .menu { position: fixed; background: var(--bg2); border: 1px solid var(--line); border-radius: 6px; padding: 4px; z-index: 20;
152
+ box-shadow: 0 8px 24px rgba(0,0,0,.5); min-width: 170px; }
153
+ .menu div { padding: 4px 10px; border-radius: 4px; cursor: pointer; white-space: nowrap; }
154
+ .menu div:hover { background: var(--bg3); }
155
+ .menu div .muted { font-family: var(--mono); font-size: 11px; margin-left: 8px; }
156
+
157
+ /* --- bottom panels --- */
158
+ #bottom { display: flex; gap: 1px; background: var(--line); border-top: 1px solid var(--line); height: 190px; flex: none; }
159
+ #bottom .panel { background: var(--bg2); padding: 6px 8px; overflow: auto; min-width: 150px; flex: 0 1 220px; }
160
+ #bottom .panel.grow { flex: 1 1 300px; }
161
+ #bottom h3 { margin: 0 0 4px; font-size: 11px; text-transform: uppercase; letter-spacing: .6px; color: var(--muted);
162
+ display: flex; gap: 8px; align-items: center; }
163
+ .kv { display: grid; grid-template-columns: auto 1fr; gap: 1px 10px; font: 12px/17px var(--mono); }
164
+ .kv .k { color: var(--muted); }
165
+ .kv .v { color: var(--fg); }
166
+ .kv .v.chg { color: var(--cur); }
167
+ .kv input { width: 80px; padding: 0 4px; font: 12px var(--mono); }
168
+ #log { font: 12px/17px var(--mono); }
169
+ #log .call { color: var(--ok); } #log .emit { color: #7fdbff; } #log .state { color: var(--cur); font-weight: bold; }
170
+ #log .fault { color: var(--bad); font-weight: bold; } #log .dispatch { color: var(--ev); } #log .sys { color: var(--muted); }
171
+ #log .post, #log .tick { color: var(--kw); }
172
+ .stackv { font: 12px var(--mono); color: var(--fg); }
173
+ .frame { margin-bottom: 4px; }
174
+ .frame b { color: var(--ev); font-weight: normal; }
175
+
176
+ /* size view (bytecode pane, "Size" tab) */
177
+ .sizeview { flex: 1; overflow: auto; padding: 4px 10px 16px; font-size: 12px; }
178
+ .sizeview h4 { margin: 14px 0 4px; font-size: 12px; font-weight: 600; }
179
+ .sizeview h4 b { color: var(--accent); margin: 0 6px 0 2px; }
180
+ .sizeview h4 .muted { font-size: 11px; }
181
+ table.sz { border-collapse: collapse; width: 100%; }
182
+ table.sz td { padding: 1px 6px 1px 0; vertical-align: middle; white-space: nowrap; }
183
+ table.sz td:last-child { white-space: normal; }
184
+ table.sz td.n { text-align: right; font-variant-numeric: tabular-nums; width: 1%; }
185
+ table.sz .mono, .sz-flags code { font-family: var(--mono); }
186
+ .sz-bar { display: inline-block; width: 90px; height: 7px; background: var(--bg3); border-radius: 2px; vertical-align: middle; }
187
+ .sz-bar > span { display: block; height: 100%; background: var(--accent); border-radius: 2px; }
188
+ tr.sz-rt { cursor: pointer; }
189
+ tr.sz-rt:hover td { background: var(--bg3); }
190
+ .sz-flags { margin-top: 6px; }
191
+ .sz-flags code { display: block; margin-top: 2px; padding: 4px 6px; background: var(--bg3); border-radius: 4px;
192
+ white-space: pre-wrap; word-break: break-all; user-select: all; }
193
+ .sz-hints { margin: 2px 0; padding-left: 18px; }
194
+ .sz-hints li { margin: 2px 0; }
195
+
196
+ /* any_state */
197
+ #chart .any-box { fill: rgba(160, 130, 255, .04); stroke: #7a68b8; stroke-dasharray: 6 4; }
198
+ #chart .any-title { font: bold 14px var(--mono); fill: #c3b5ff; }
199
+ #chart .any-frame { fill: rgba(160, 130, 255, .025); stroke: #7a68b8; stroke-width: 1.5; stroke-dasharray: 6 4; }
200
+ #chart .any-tag { fill: #2c2742; stroke: #7a68b8; }
201
+ #chart text.any-tag-text { font: bold 12px var(--mono); fill: #c3b5ff; }
202
+ /* state x event table (States view) */
203
+ .se-wrap { padding: 4px 16px 24px; font-size: 12px; }
204
+ .se-title { font-weight: 600; margin: 4px 0 6px; }
205
+ .se-table { border-collapse: collapse; }
206
+ .se-table th, .se-table td { border: 1px solid var(--line); padding: 3px 10px; text-align: center; white-space: nowrap; }
207
+ .se-table th { background: var(--bg2); font-family: var(--mono); font-weight: 600; }
208
+ .se-table td.se-ev { text-align: left; font-family: var(--mono); }
209
+ .se-table td.se-own { color: #9ec5ff; cursor: pointer; }
210
+ .se-table td.se-over { background: rgba(255, 204, 77, .08); }
211
+ .se-table td.se-over span { color: var(--cur); font-size: 11px; }
212
+ .se-table td.se-any { color: #c3b5ff; cursor: pointer; }
213
+ .se-table td.se-none { color: var(--muted); }
214
+ .se-table td[data-sel]:hover { background: var(--bg3); }
215
+ .se-table td.sel { outline: 2px solid var(--accent); }
216
+ #errors div.warn { color: var(--cur); }
flowscript/intops.py ADDED
@@ -0,0 +1,63 @@
1
+ """32-bit integer semantics shared by the compiler (constant folding) and the Python VM.
2
+
3
+ They match the C VM exactly: wrap-around, truncating division, masked shifts.
4
+ """
5
+
6
+
7
+ class DivZero(Exception):
8
+ pass
9
+
10
+
11
+ def wrap(v):
12
+ v &= 0xFFFFFFFF
13
+ return v - 0x100000000 if v & 0x80000000 else v
14
+
15
+
16
+ def div(a, b):
17
+ if b == 0:
18
+ raise DivZero()
19
+ q = abs(a) // abs(b)
20
+ return wrap(-q if (a < 0) != (b < 0) else q)
21
+
22
+
23
+ def mod(a, b):
24
+ if b == 0:
25
+ raise DivZero()
26
+ return wrap(a - div(a, b) * b)
27
+
28
+
29
+ def shl(a, b):
30
+ return wrap((a & 0xFFFFFFFF) << (b & 31))
31
+
32
+
33
+ def shr(a, b):
34
+ return wrap(a >> (b & 31))
35
+
36
+
37
+ BINARY = {
38
+ "+": lambda a, b: wrap(a + b),
39
+ "-": lambda a, b: wrap(a - b),
40
+ "*": lambda a, b: wrap(a * b),
41
+ "/": div,
42
+ "%": mod,
43
+ "&": lambda a, b: wrap(a & b),
44
+ "|": lambda a, b: wrap(a | b),
45
+ "^": lambda a, b: wrap(a ^ b),
46
+ "<<": shl,
47
+ ">>": shr,
48
+ "==": lambda a, b: int(a == b),
49
+ "!=": lambda a, b: int(a != b),
50
+ "<": lambda a, b: int(a < b),
51
+ "<=": lambda a, b: int(a <= b),
52
+ ">": lambda a, b: int(a > b),
53
+ ">=": lambda a, b: int(a >= b),
54
+ "and": lambda a, b: int(bool(a) and bool(b)),
55
+ "or": lambda a, b: int(bool(a) or bool(b)),
56
+ "xor": lambda a, b: int(bool(a) != bool(b)),
57
+ }
58
+
59
+ UNARY = {
60
+ "-": lambda a: wrap(-a),
61
+ "~": lambda a: wrap(~a),
62
+ "not": lambda a: int(not a),
63
+ }
flowscript/lexer.py ADDED
@@ -0,0 +1,265 @@
1
+ """FlowScript lexer: source text -> list of tokens."""
2
+
3
+ KEYWORDS = {
4
+ "flow", "version", "extern", "event", "timer", "const", "func", "on",
5
+ "int", "float", "str", "time",
6
+ "state", "initial", "any_state", "pass", "if", "elif", "else", "while", "goto_state", "raise_event", "start", "every",
7
+ "stop", "return", "and", "or", "xor", "not", "true", "false",
8
+ }
9
+
10
+ TYPE_KEYWORDS = ("int", "float", "str", "time")
11
+
12
+ TIME_UNITS = {"ms": 1, "s": 1000, "min": 60000, "h": 3600000}
13
+
14
+ # longest first
15
+ OPERATORS = ["->", "<<", ">>", "==", "!=", "<=", ">=", "+", "-", "*", "/", "%", "&", "|",
16
+ "^", "~", "<", ">", "=", "(", ")", ",", ":"]
17
+
18
+
19
+ class FlowError(Exception):
20
+ def __init__(self, msg, line=0, col=0):
21
+ super().__init__(msg)
22
+ self.msg = msg
23
+ self.line = line
24
+ self.col = col
25
+
26
+ def __str__(self):
27
+ if self.line:
28
+ return "line %d:%d: %s" % (self.line, self.col, self.msg)
29
+ return self.msg
30
+
31
+ def to_dict(self):
32
+ return {"msg": self.msg, "line": self.line, "col": self.col}
33
+
34
+
35
+ class Token:
36
+ __slots__ = ("kind", "value", "line", "col", "text")
37
+
38
+ def __init__(self, kind, value, line, col, text=None):
39
+ self.kind = kind # NAME KW NUM FLOAT STR OP NEWLINE COMMENT INDENT DEDENT EOF
40
+ self.value = value
41
+ self.line = line
42
+ self.col = col
43
+ self.text = text # original spelling (numbers)
44
+
45
+ def __repr__(self):
46
+ return "Token(%s,%r,%d:%d)" % (self.kind, self.value, self.line, self.col)
47
+
48
+
49
+ def tokenize(src):
50
+ """Tokens including INDENT/DEDENT (blocks are delimited by indentation)."""
51
+ return _layout(*_raw_tokens(src))
52
+
53
+
54
+ def _raw_tokens(src):
55
+ toks = []
56
+ indents = {} # line -> indentation width of that line
57
+ i = 0
58
+ line = 1
59
+ col = 1
60
+ n = len(src)
61
+
62
+ def add(kind, value, l, c, text=None):
63
+ toks.append(Token(kind, value, l, c, text))
64
+
65
+ while i < n:
66
+ ch = src[i]
67
+ if col == 1:
68
+ j = i
69
+ while j < n and src[j] in " \t\r":
70
+ j += 1
71
+ if j < n and src[j] != "\n":
72
+ if "\t" in src[i:j]:
73
+ raise FlowError("tab used for indentation; indent with spaces", line, 1)
74
+ indents[line] = j - i
75
+ if ch == "\n":
76
+ add("NEWLINE", None, line, col)
77
+ i += 1
78
+ line += 1
79
+ col = 1
80
+ continue
81
+ if ch in " \t\r":
82
+ i += 1
83
+ col += 1
84
+ continue
85
+ if ch == "#":
86
+ j = src.find("\n", i)
87
+ if j < 0:
88
+ j = n
89
+ text = src[i + 1:j]
90
+ if text.startswith(" "):
91
+ text = text[1:]
92
+ add("COMMENT", text.rstrip(), line, col)
93
+ col += j - i
94
+ i = j
95
+ continue
96
+ if ch.isdigit():
97
+ j = i
98
+ if src.startswith(("0x", "0X"), i):
99
+ j = i + 2
100
+ while j < n and (src[j] in "0123456789abcdefABCDEF_"):
101
+ j += 1
102
+ base = 16
103
+ digits = src[i + 2:j]
104
+ elif src.startswith(("0b", "0B"), i):
105
+ j = i + 2
106
+ while j < n and src[j] in "01_":
107
+ j += 1
108
+ base = 2
109
+ digits = src[i + 2:j]
110
+ else:
111
+ while j < n and (src[j].isdigit() or src[j] == "_"):
112
+ j += 1
113
+ base = 10
114
+ digits = src[i:j]
115
+ # float literal: 1.5, 2.0e-3, 1e6
116
+ k = j
117
+ if k + 1 < n and src[k] == "." and src[k + 1].isdigit():
118
+ k += 1
119
+ while k < n and (src[k].isdigit() or src[k] == "_"):
120
+ k += 1
121
+ if k < n and src[k] in "eE":
122
+ m = k + 1
123
+ if m < n and src[m] in "+-":
124
+ m += 1
125
+ if m < n and src[m].isdigit():
126
+ k = m
127
+ while k < n and src[k].isdigit():
128
+ k += 1
129
+ if k > j:
130
+ if k < n and (src[k].isalpha() or src[k] == "_"):
131
+ raise FlowError("time units need an integer: write %s as a whole number of ms"
132
+ % src[i:k], line, col)
133
+ text = src[i:k]
134
+ add("FLOAT", float(text.replace("_", "")), line, col, text)
135
+ col += k - i
136
+ i = k
137
+ continue
138
+ digits = digits.replace("_", "")
139
+ if not digits:
140
+ raise FlowError("malformed number", line, col)
141
+ value = int(digits, base)
142
+ # time unit suffix
143
+ k = j
144
+ while k < n and (src[k].isalpha() or src[k] == "_" or src[k].isdigit()):
145
+ k += 1
146
+ unit = src[j:k]
147
+ if unit:
148
+ if base != 10 or unit not in TIME_UNITS:
149
+ raise FlowError("bad number suffix '%s' (allowed: ms, s, min, h)" % unit, line, col)
150
+ value *= TIME_UNITS[unit]
151
+ text = src[i:k]
152
+ add("NUM", value, line, col, text)
153
+ col += k - i
154
+ i = k
155
+ continue
156
+ if ch.isalpha() or ch == "_":
157
+ j = i
158
+ while j < n and (src[j].isalnum() or src[j] == "_"):
159
+ j += 1
160
+ word = src[i:j]
161
+ add("KW" if word in KEYWORDS else "NAME", word, line, col)
162
+ col += j - i
163
+ i = j
164
+ continue
165
+ if ch == '"':
166
+ j = i + 1
167
+ out = []
168
+ while True:
169
+ if j >= n or src[j] == "\n":
170
+ raise FlowError("unterminated string", line, col)
171
+ c = src[j]
172
+ if c == '"':
173
+ break
174
+ if c == "\\":
175
+ j += 1
176
+ esc = src[j] if j < n else ""
177
+ out.append({"n": "\n", "t": "\t", '"': '"', "\\": "\\"}.get(esc, esc))
178
+ else:
179
+ out.append(c)
180
+ j += 1
181
+ add("STR", "".join(out), line, col)
182
+ col += j + 1 - i
183
+ i = j + 1
184
+ continue
185
+ for op in OPERATORS:
186
+ if src.startswith(op, i):
187
+ add("OP", op, line, col)
188
+ i += len(op)
189
+ col += len(op)
190
+ break
191
+ else:
192
+ raise FlowError("unexpected character %r" % ch, line, col)
193
+ add("NEWLINE", None, line, col)
194
+ return toks, indents, line, col
195
+
196
+
197
+ def _layout(toks, indents, last_line, last_col):
198
+ """Insert INDENT/DEDENT tokens, Python style.
199
+
200
+ Blank and comment-only lines do not open or close blocks. A comment group directly
201
+ above a code line is placed after that line's INDENT/DEDENT tokens (it becomes the
202
+ line's note). Other comment groups stay in the innermost block that their own
203
+ indentation allows.
204
+ """
205
+ # split into physical lines (each ends with NEWLINE)
206
+ lines = []
207
+ cur = []
208
+ for t in toks:
209
+ cur.append(t)
210
+ if t.kind == "NEWLINE":
211
+ lines.append(cur)
212
+ cur = []
213
+ out = []
214
+ stack = [0]
215
+ pending = []
216
+
217
+ def dedent_to(width, tok):
218
+ while stack[-1] > width:
219
+ stack.pop()
220
+ out.append(Token("DEDENT", None, tok.line, 1))
221
+
222
+ def flush_standalone(group, floor):
223
+ for ln in group:
224
+ if ln[0].kind == "COMMENT":
225
+ dedent_to(max(indents.get(ln[0].line, 0), floor), ln[0])
226
+ out.extend(ln)
227
+
228
+ for ln in lines:
229
+ first = ln[0]
230
+ if first.kind in ("NEWLINE", "COMMENT"):
231
+ pending.append(ln)
232
+ continue
233
+ width = indents.get(first.line, 0)
234
+ # the comment group directly above this line (no blank line between) is its note
235
+ k = len(pending)
236
+ while k > 0 and pending[k - 1][0].kind == "COMMENT":
237
+ k -= 1
238
+ standalone, note = pending[:k], pending[k:]
239
+ continuation = first.kind == "KW" and first.value in ("elif", "else")
240
+ if continuation:
241
+ standalone, note = pending, []
242
+ floor = width
243
+ if continuation:
244
+ deeper = [w for w in stack if w > width]
245
+ floor = min(deeper) if deeper else width
246
+ pending = []
247
+ if width > stack[-1]:
248
+ # blank lines / comments between a header and its block belong to the block
249
+ stack.append(width)
250
+ out.append(Token("INDENT", None, first.line, 1))
251
+ flush_standalone(standalone, width)
252
+ else:
253
+ flush_standalone(standalone, floor)
254
+ if width < stack[-1]:
255
+ dedent_to(width, first)
256
+ if stack[-1] != width:
257
+ raise FlowError("indentation does not match any outer block", first.line, width + 1)
258
+ for nl in note:
259
+ out.extend(nl)
260
+ out.extend(ln)
261
+ end = Token("EOF", None, last_line, last_col)
262
+ flush_standalone(pending, 0)
263
+ dedent_to(0, end)
264
+ out.append(end)
265
+ return out
flowscript/mermaid.py ADDED
@@ -0,0 +1,109 @@
1
+ """Export the visual model as a Mermaid flowchart (for docs, wikis, PRs)."""
2
+
3
+ from .printer import expr_to_str
4
+
5
+
6
+ def _q(s):
7
+ return '"' + s.replace('"', "#quot;") + '"'
8
+
9
+
10
+ class _M:
11
+ def __init__(self):
12
+ self.lines = ["flowchart TD"]
13
+ self.n = 0
14
+
15
+ def node(self, shape, text):
16
+ self.n += 1
17
+ nid = "n%d" % self.n
18
+ l, r = {"proc": ("[", "]"), "io": ("[/", "/]"), "sub": ("[[", "]]"), "dec": ("{", "}"),
19
+ "term": ("([", "])"), "ev": ("{{", "}}"), "goto": ("((", "))"), "emit": (">", "]")}[shape]
20
+ self.lines.append(" %s%s%s%s" % (nid, l, _q(text), r))
21
+ return nid
22
+
23
+ def edge(self, a, b, label=None):
24
+ self.lines.append(" %s -->%s %s" % (a, ("|%s|" % label) if label else "", b))
25
+
26
+ def block(self, stmts, prev, funcs, label=None):
27
+ """Returns list of (node, label) dangling exits."""
28
+ exits = [(prev, label)]
29
+ for s in stmts:
30
+ if s["type"] == "comment":
31
+ continue
32
+ entry, outs = self.stmt(s, funcs)
33
+ for p, lb in exits:
34
+ self.edge(p, entry, lb)
35
+ exits = outs
36
+ return exits
37
+
38
+ def stmt(self, s, funcs):
39
+ t = s["type"]
40
+ if t == "if":
41
+ first = None
42
+ exits = []
43
+ prev_no = None
44
+ for br in s["branches"]:
45
+ d = self.node("dec", expr_to_str(br["cond"]))
46
+ if prev_no:
47
+ self.edge(prev_no, d, "no")
48
+ first = first or d
49
+ exits += self.block(br["body"], d, funcs, "yes")
50
+ prev_no = d
51
+ if s.get("else_body"):
52
+ exits += self.block(s["else_body"], prev_no, funcs, "no")
53
+ else:
54
+ exits.append((prev_no, "no"))
55
+ return first, exits
56
+ if t == "while":
57
+ d = self.node("dec", "while " + expr_to_str(s["cond"]))
58
+ for p, lb in self.block(s["body"], d, funcs, "yes"):
59
+ self.edge(p, d, lb)
60
+ return d, [(d, "no")]
61
+ if t == "goto":
62
+ return self.node("goto", "goto_state " + s["state"]), []
63
+ if t == "return":
64
+ return self.node("term", "return" + (" " + expr_to_str(s["value"]) if s.get("value") else "")), []
65
+ if t == "assign":
66
+ n = self.node("proc", "%s = %s" % (s["target"], expr_to_str(s["value"])))
67
+ elif t == "local":
68
+ n = self.node("proc", "%s %s" % (s.get("vtype") or "int", s["name"]) + (" = " + expr_to_str(s["value"]) if s.get("value") else ""))
69
+ elif t == "call":
70
+ n = self.node("sub" if s["name"] in funcs else "io", expr_to_str(s))
71
+ elif t == "emit":
72
+ n = self.node("emit", "raise_event " + s["event"])
73
+ elif t == "start":
74
+ n = self.node("proc", "start %s %s%s" % (s["timer"], "every " if s.get("every") else "", expr_to_str(s["ms"])))
75
+ else:
76
+ n = self.node("proc", "stop " + s["timer"])
77
+ return n, [(n, None)]
78
+
79
+ def routine(self, r, funcs, title):
80
+ head = self.node("ev" if r["type"] == "on" else "sub", title)
81
+ exits = self.block(r["body"], head, funcs)
82
+ if exits:
83
+ end = self.node("term", "end")
84
+ for p, lb in exits:
85
+ self.edge(p, end, lb)
86
+
87
+
88
+ def to_mermaid(ast):
89
+ m = _M()
90
+ funcs = {i["name"] for i in ast["items"] if i["type"] == "func"}
91
+ for it in ast["items"]:
92
+ if it["type"] == "func":
93
+ params = ", ".join("%s %s" % (t, p) for t, p in zip(it.get("ptypes") or ["int"] * len(it["params"]), it["params"]))
94
+ m.routine(it, funcs, "func %s(%s)%s" % (it["name"], params, (" -> " + it["returns"]) if it.get("returns") else ""))
95
+ elif it["type"] == "on":
96
+ m.routine(it, funcs, "on " + it["event"])
97
+ elif it["type"] == "any_state":
98
+ m.lines.append(" subgraph any_state [any_state]")
99
+ for h in it["handlers"]:
100
+ if h["type"] == "on":
101
+ m.routine(h, funcs, "on " + h["event"])
102
+ m.lines.append(" end")
103
+ elif it["type"] == "state":
104
+ m.lines.append(" subgraph %s [state %s]" % ("s_" + it["name"], it["name"]))
105
+ for h in it["handlers"]:
106
+ if h["type"] == "on":
107
+ m.routine(h, funcs, "%s: on %s" % (it["name"], h["event"]))
108
+ m.lines.append(" end")
109
+ return "\n".join(m.lines) + "\n"