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/__init__.py +35 -0
- flowscript/__main__.py +4 -0
- flowscript/api.py +286 -0
- flowscript/bytecode.py +372 -0
- flowscript/cli.py +126 -0
- flowscript/compiler.py +1019 -0
- flowscript/debugger/__init__.py +0 -0
- flowscript/debugger/server.py +505 -0
- flowscript/debugger/static/app.js +626 -0
- flowscript/debugger/static/chart.js +620 -0
- flowscript/debugger/static/edit.js +513 -0
- flowscript/debugger/static/index.html +115 -0
- flowscript/debugger/static/style.css +216 -0
- flowscript/intops.py +63 -0
- flowscript/lexer.py +265 -0
- flowscript/mermaid.py +109 -0
- flowscript/parser.py +578 -0
- flowscript/printer.py +225 -0
- flowscript/runner.py +164 -0
- flowscript/sizes.py +512 -0
- flowscript/svg.py +766 -0
- flowscript/values.py +724 -0
- flowscript/vm.py +577 -0
- flowscript_vm-0.1.0.dist-info/METADATA +400 -0
- flowscript_vm-0.1.0.dist-info/RECORD +29 -0
- flowscript_vm-0.1.0.dist-info/WHEEL +5 -0
- flowscript_vm-0.1.0.dist-info/entry_points.txt +2 -0
- flowscript_vm-0.1.0.dist-info/licenses/LICENSE +21 -0
- flowscript_vm-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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"
|