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
flowscript/sizes.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"""Size report: where the bytes of a compiled program go, and how much RAM the C VM needs.
|
|
2
|
+
|
|
3
|
+
Three separate budgets matter on a device:
|
|
4
|
+
|
|
5
|
+
* the **image** (.fsb) – the C VM uses it in place (never copied), so it lives wherever the
|
|
6
|
+
host keeps the download, typically flash;
|
|
7
|
+
* the **RAM** of the ``fs_vm`` struct – fixed by the ``FS_*`` macros the firmware is built
|
|
8
|
+
with, so the report computes the smallest settings this program can run with;
|
|
9
|
+
* the **C interpreter code** (flowscript.c) – fixed, independent of the program.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .bytecode import Program, decode, BUILTIN_EVENTS, features, FEATURES, FLAG_STR
|
|
13
|
+
from .values import BUILTINS
|
|
14
|
+
|
|
15
|
+
# C VM defaults (c/flowscript.h)
|
|
16
|
+
DEFAULTS = {
|
|
17
|
+
"FS_STACK_SIZE": 64, "FS_MAX_FRAMES": 16, "FS_QUEUE_SIZE": 16, "FS_MAX_EXTERNS": 64,
|
|
18
|
+
"FS_MAX_GLOBALS": 256, "FS_MAX_TIMERS": 16, "FS_MAX_EVENT_ARGS": 8,
|
|
19
|
+
"FS_STR_SLOTS": 16, "FS_STR_LEN": 64,
|
|
20
|
+
}
|
|
21
|
+
SWITCHES = ("FS_NO_FLOAT", "FS_NO_TIME", "FS_NO_STR", "FS_NO_TEXT") # c/flowscript.h switches
|
|
22
|
+
# VM code + constants in bytes kept by a Cortex-M4 link (clang -Os, --gc-sections; the host
|
|
23
|
+
# does not call fs_format/fs_parse_*), by the switches that are set. Measured with
|
|
24
|
+
# c/codesize.py. Builds with floats also link soft-float and libm routines.
|
|
25
|
+
VM_CODE_BYTES = {
|
|
26
|
+
(): 16087,
|
|
27
|
+
("FS_NO_FLOAT",): 13495,
|
|
28
|
+
("FS_NO_TIME",): 12801,
|
|
29
|
+
("FS_NO_STR",): 13841,
|
|
30
|
+
("FS_NO_TEXT",): 12817,
|
|
31
|
+
("FS_NO_FLOAT", "FS_NO_TIME"): 9421,
|
|
32
|
+
("FS_NO_FLOAT", "FS_NO_STR"): 11877,
|
|
33
|
+
("FS_NO_FLOAT", "FS_NO_TEXT"): 11955,
|
|
34
|
+
("FS_NO_TIME", "FS_NO_STR"): 10979,
|
|
35
|
+
("FS_NO_TIME", "FS_NO_TEXT"): 11083,
|
|
36
|
+
("FS_NO_STR", "FS_NO_TEXT"): 11811,
|
|
37
|
+
("FS_NO_FLOAT", "FS_NO_TIME", "FS_NO_STR"): 8383,
|
|
38
|
+
("FS_NO_FLOAT", "FS_NO_TIME", "FS_NO_TEXT"): 9421,
|
|
39
|
+
("FS_NO_FLOAT", "FS_NO_STR", "FS_NO_TEXT"): 10747,
|
|
40
|
+
("FS_NO_TIME", "FS_NO_STR", "FS_NO_TEXT"): 10065,
|
|
41
|
+
("FS_NO_FLOAT", "FS_NO_TIME", "FS_NO_STR", "FS_NO_TEXT"): 8383,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def cell_layout(cfg):
|
|
46
|
+
"""(sizeof, alignof) of fs_cell, the payload the VM stores (its type tag is a separate byte)."""
|
|
47
|
+
if not cfg.get("FS_NO_FLOAT") or not cfg.get("FS_NO_TIME"):
|
|
48
|
+
return (8, 8) # double f / int64 t
|
|
49
|
+
return (4, 4) # int32 i, s
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def stored_value_bytes(cfg):
|
|
53
|
+
"""RAM per value held by the VM (globals, stack, queued event arguments): cell + tag."""
|
|
54
|
+
return cell_layout(cfg)[0] + 1
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def queue_entry_layout(cfg):
|
|
58
|
+
n = cfg["FS_MAX_EVENT_ARGS"]
|
|
59
|
+
csize, calign = cell_layout(cfg)
|
|
60
|
+
return _layout([(csize * n, calign), (2, 2), (n, 1)]) # args, event, types
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def vm_code_bytes(cfg):
|
|
64
|
+
return VM_CODE_BYTES[tuple(k for k in SWITCHES if cfg.get(k))]
|
|
65
|
+
|
|
66
|
+
# net stack effect of instructions that just fall through (CALL/XCALL/EMIT/RET handled apart)
|
|
67
|
+
_EFFECT = {
|
|
68
|
+
"NOP": 0, "PUSH": 1, "PUSH8": 1, "PUSHS": 1, "PUSHF": 1, "PUSHT": 1, "POP": -1, "DUP": 1,
|
|
69
|
+
"LOADG": 1, "STOREG": -1, "LOADL": 1, "STOREL": -1,
|
|
70
|
+
"NEG": 0, "BNOT": 0, "NOT": 0, "BOOL": 0, "TSTART": -1, "TEVERY": -1, "TSTOP": 0,
|
|
71
|
+
"JMP": 0, "JZ": -1, "JNZ": -1,
|
|
72
|
+
}
|
|
73
|
+
for _op in ("ADD", "CONCAT", "SUB", "MUL", "DIV", "MOD", "BAND", "BOR", "BXOR", "SHL", "SHR",
|
|
74
|
+
"EQ", "NE", "LT", "LE", "GT", "GE"):
|
|
75
|
+
_EFFECT[_op] = -1
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------------------------------------------------------------------------------------
|
|
79
|
+
# C struct layout (mirrors struct fs_vm in c/flowscript.h)
|
|
80
|
+
|
|
81
|
+
def _layout(fields):
|
|
82
|
+
"""fields: (size, align) in declaration order -> (sizeof, alignof) with C padding."""
|
|
83
|
+
off = 0
|
|
84
|
+
max_align = 1
|
|
85
|
+
for size, align in fields:
|
|
86
|
+
off = (off + align - 1) // align * align
|
|
87
|
+
off += size
|
|
88
|
+
max_align = max(max_align, align)
|
|
89
|
+
return (off + max_align - 1) // max_align * max_align, max_align
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def fs_vm_size(cfg, ptr=4):
|
|
93
|
+
"""sizeof(fs_vm) for the given FS_* settings; ptr = pointer size (4 on Cortex-M)."""
|
|
94
|
+
P = (ptr, ptr)
|
|
95
|
+
i32 = (4, 4)
|
|
96
|
+
u16 = (2, 2)
|
|
97
|
+
u8 = (1, 1)
|
|
98
|
+
csize, calign = cell_layout(cfg)
|
|
99
|
+
qentry, qalign = queue_entry_layout(cfg)
|
|
100
|
+
timer, talign = _layout([u8, u8, (2, 2), i32, (8, 8)])
|
|
101
|
+
strslot, salign = _layout([u16, u8, u8, (cfg["FS_STR_LEN"] + 1, 1)])
|
|
102
|
+
fields = [P, P] + [P] * 9 + [i32] + [u16] * 10 + [u8]
|
|
103
|
+
fields += [(ptr * cfg["FS_MAX_EXTERNS"], ptr), P, P, P] # bound, on_emit/fault/timer
|
|
104
|
+
fields += [] if cfg.get("FS_NO_TIME") else [P] # clock
|
|
105
|
+
fields += [P] # user
|
|
106
|
+
fields += [(csize * cfg["FS_MAX_GLOBALS"], calign), (csize * cfg["FS_STACK_SIZE"], calign), i32,
|
|
107
|
+
(8 * cfg["FS_MAX_FRAMES"], 4)]
|
|
108
|
+
fields += [i32] * 7 + [u8, u8] + [i32] * 4
|
|
109
|
+
fields += [(qentry * cfg["FS_QUEUE_SIZE"], qalign), i32, i32,
|
|
110
|
+
(timer * cfg["FS_MAX_TIMERS"], talign),
|
|
111
|
+
(cfg["FS_MAX_GLOBALS"], 1), (cfg["FS_STACK_SIZE"], 1)] # gtypes, stypes
|
|
112
|
+
if not cfg.get("FS_NO_STR"):
|
|
113
|
+
fields.append((strslot * cfg["FS_STR_SLOTS"], salign))
|
|
114
|
+
return _layout(fields)[0]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def makes_strings(prog):
|
|
118
|
+
"""Can this program create run-time strings (so it needs the string slots)?
|
|
119
|
+
Yes if it joins strings (CONCAT), calls str() / substr(), has an extern returning
|
|
120
|
+
a str or an event with a str parameter (the host's strings are copied in)."""
|
|
121
|
+
return bool(features(prog) & FLAG_STR)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _pcs(prog):
|
|
125
|
+
pc = 0
|
|
126
|
+
while pc < len(prog.code):
|
|
127
|
+
yield pc
|
|
128
|
+
pc += decode(prog.code, pc)[2]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------------------
|
|
132
|
+
# bytecode analysis
|
|
133
|
+
|
|
134
|
+
def _routines(prog):
|
|
135
|
+
"""[(kind, name, argc, nlocals, start, end)] in code order."""
|
|
136
|
+
out = []
|
|
137
|
+
for name, argc, nlocals, off in prog.funcs:
|
|
138
|
+
out.append(("func", name, argc, nlocals, off))
|
|
139
|
+
for st, ev, argc, nlocals, off in prog.handlers:
|
|
140
|
+
evn = prog.events[ev][0]
|
|
141
|
+
name = ("on %s" % evn) if st is None else ("%s: on %s" % (prog.states[st], evn))
|
|
142
|
+
out.append(("handler", name, argc, nlocals, off))
|
|
143
|
+
out.sort(key=lambda r: r[4])
|
|
144
|
+
res = []
|
|
145
|
+
for i, r in enumerate(out):
|
|
146
|
+
end = out[i + 1][4] if i + 1 < len(out) else len(prog.code)
|
|
147
|
+
res.append(r + (max(end, r[4]),))
|
|
148
|
+
return res
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _local_peak(prog, start, nlocals):
|
|
152
|
+
"""Walk one routine. Returns (peak depth relative to its base, call sites).
|
|
153
|
+
|
|
154
|
+
call sites: [(depth_before_call, func_index)].
|
|
155
|
+
"""
|
|
156
|
+
code = prog.code
|
|
157
|
+
seen = {}
|
|
158
|
+
work = [(start, nlocals)]
|
|
159
|
+
peak = nlocals
|
|
160
|
+
calls = []
|
|
161
|
+
while work:
|
|
162
|
+
pc, d = work.pop()
|
|
163
|
+
while 0 <= pc < len(code):
|
|
164
|
+
if seen.get(pc, -1) >= d:
|
|
165
|
+
break
|
|
166
|
+
seen[pc] = d
|
|
167
|
+
name, arg, size = decode(code, pc)
|
|
168
|
+
npc = pc + size
|
|
169
|
+
if name in ("RET", "RET0", "GOTO", "???"):
|
|
170
|
+
break
|
|
171
|
+
if name == "CALL":
|
|
172
|
+
if arg >= len(prog.funcs):
|
|
173
|
+
break
|
|
174
|
+
calls.append((d, arg))
|
|
175
|
+
d = d - prog.funcs[arg][1] + 1
|
|
176
|
+
elif name == "XCALL":
|
|
177
|
+
if arg >= len(prog.externs):
|
|
178
|
+
break
|
|
179
|
+
d = d - prog.externs[arg][1] + 1
|
|
180
|
+
elif name == "BUILTIN":
|
|
181
|
+
if arg >= len(BUILTINS):
|
|
182
|
+
break
|
|
183
|
+
d = d - len(BUILTINS[arg][1]) + 1
|
|
184
|
+
elif name == "EMIT":
|
|
185
|
+
if arg >= len(prog.events):
|
|
186
|
+
break
|
|
187
|
+
d -= prog.events[arg][1]
|
|
188
|
+
else:
|
|
189
|
+
d += _EFFECT.get(name, 0)
|
|
190
|
+
peak = max(peak, d)
|
|
191
|
+
if name in ("JZ", "JNZ"):
|
|
192
|
+
work.append((arg, d))
|
|
193
|
+
elif name == "JMP":
|
|
194
|
+
npc = arg
|
|
195
|
+
pc = npc
|
|
196
|
+
return peak, calls
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def stack_analysis(prog):
|
|
200
|
+
"""Worst-case stack slots and call frames per routine (None = unbounded: recursion)."""
|
|
201
|
+
funcs = {}
|
|
202
|
+
for i, (name, argc, nlocals, off) in enumerate(prog.funcs):
|
|
203
|
+
funcs[i] = _local_peak(prog, off, nlocals)
|
|
204
|
+
memo = {}
|
|
205
|
+
|
|
206
|
+
def total(i, path=()):
|
|
207
|
+
"""(stack peak relative to the func's base, frames incl. its own) or None."""
|
|
208
|
+
if i in path:
|
|
209
|
+
return None
|
|
210
|
+
if i in memo:
|
|
211
|
+
return memo[i]
|
|
212
|
+
peak, calls = funcs[i]
|
|
213
|
+
frames = 1
|
|
214
|
+
for d, callee in calls:
|
|
215
|
+
t = total(callee, path + (i,))
|
|
216
|
+
if t is None:
|
|
217
|
+
memo[i] = None
|
|
218
|
+
return None
|
|
219
|
+
peak = max(peak, d - prog.funcs[callee][1] + t[0])
|
|
220
|
+
frames = max(frames, 1 + t[1])
|
|
221
|
+
memo[i] = (peak, frames)
|
|
222
|
+
return memo[i]
|
|
223
|
+
|
|
224
|
+
result = []
|
|
225
|
+
for kind, name, argc, nlocals, start, end in _routines(prog):
|
|
226
|
+
if kind == "func":
|
|
227
|
+
idx = [i for i, f in enumerate(prog.funcs) if f[3] == start and f[0] == name][0]
|
|
228
|
+
t = total(idx)
|
|
229
|
+
else:
|
|
230
|
+
peak, calls = _local_peak(prog, start, nlocals)
|
|
231
|
+
frames = 1
|
|
232
|
+
t = (peak, frames)
|
|
233
|
+
for d, callee in calls:
|
|
234
|
+
c = total(callee)
|
|
235
|
+
if c is None:
|
|
236
|
+
t = None
|
|
237
|
+
break
|
|
238
|
+
t = (max(t[0], d - prog.funcs[callee][1] + c[0]), max(t[1], 1 + c[1]))
|
|
239
|
+
result.append({"kind": kind, "name": name, "stack": t[0] if t else None,
|
|
240
|
+
"frames": t[1] if t else None})
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _usage(prog):
|
|
245
|
+
used = {"XCALL": set(), "CALL": set(), "GOTO": set(), "EMIT": set(), "PUSHS": set(),
|
|
246
|
+
"LOADG": set(), "STOREG": set(), "TIMER": set()}
|
|
247
|
+
pc = 0
|
|
248
|
+
while pc < len(prog.code):
|
|
249
|
+
name, arg, size = decode(prog.code, pc)
|
|
250
|
+
if name in used:
|
|
251
|
+
used[name].add(arg)
|
|
252
|
+
elif name in ("TSTART", "TEVERY"):
|
|
253
|
+
used["TIMER"].add(arg)
|
|
254
|
+
pc += size
|
|
255
|
+
return used
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
def _sbytes(s):
|
|
261
|
+
return 2 + len(s.encode("utf-8", "surrogateescape")) + 1
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def size_report(prog, debug=False, image=None):
|
|
265
|
+
"""Everything as a JSON-ready dict (used by the CLI and the debugger).
|
|
266
|
+
|
|
267
|
+
image: the original .fsb bytes when reporting on an existing file (prog loaded from it).
|
|
268
|
+
"""
|
|
269
|
+
if image is not None:
|
|
270
|
+
debug = bool(Program.from_bytes(image).lines) or debug
|
|
271
|
+
else:
|
|
272
|
+
image = prog.to_bytes(debug=debug)
|
|
273
|
+
full = Program.from_bytes(image) # string table as it is really written
|
|
274
|
+
strings = full.strings
|
|
275
|
+
|
|
276
|
+
sections = [("header", 32, "magic, version, counts")]
|
|
277
|
+
sections.append(("strings", sum(_sbytes(s) for s in strings),
|
|
278
|
+
"%d strings (2-byte length + text + NUL each)" % len(strings)))
|
|
279
|
+
sections.append(("globals", 9 * len(prog.globals), "%d initial values x 9 (type + 8 bytes)" % len(prog.globals)))
|
|
280
|
+
sections.append(("externs", 8 * len(prog.externs), "%d x 8 (name, argc, return and parameter types)" % len(prog.externs)))
|
|
281
|
+
sections.append(("events", 6 * len(prog.events),
|
|
282
|
+
"%d x 6 (incl. built-in boot/enter/exit)" % len(prog.events)))
|
|
283
|
+
sections.append(("states", 2 * len(prog.states), "%d x 2" % len(prog.states)))
|
|
284
|
+
sections.append(("timers", 2 * len(prog.timers), "%d x 2" % len(prog.timers)))
|
|
285
|
+
sections.append(("funcs", 6 * len(prog.funcs), "%d x 6" % len(prog.funcs)))
|
|
286
|
+
sections.append(("handlers", 8 * len(prog.handlers), "%d x 8" % len(prog.handlers)))
|
|
287
|
+
sections.append(("code", len(prog.code), "bytecode"))
|
|
288
|
+
dbg_bytes = (len(prog.to_bytes(debug=True)) - len(prog.to_bytes(debug=False))) if prog.lines else None
|
|
289
|
+
if debug:
|
|
290
|
+
sections.append(("debug", len(image) - sum(s[1] for s in sections),
|
|
291
|
+
"line table + global-name indices (the names are in strings)"))
|
|
292
|
+
assert sum(s[1] for s in sections) == len(image), "size report out of sync with to_bytes"
|
|
293
|
+
|
|
294
|
+
# what each string is used for
|
|
295
|
+
roles = {i: [] for i in range(len(strings))}
|
|
296
|
+
idx = {s: i for i, s in enumerate(strings)}
|
|
297
|
+
if prog.name:
|
|
298
|
+
roles[idx[prog.name]].append("program name")
|
|
299
|
+
for n, *_rest in prog.externs:
|
|
300
|
+
roles[idx[n]].append("extern")
|
|
301
|
+
for i, (n, *_rest) in enumerate(prog.events):
|
|
302
|
+
roles[idx[n]].append("built-in event" if i < len(BUILTIN_EVENTS) else "event")
|
|
303
|
+
for n in prog.states:
|
|
304
|
+
roles[idx[n]].append("state")
|
|
305
|
+
for f in prog.funcs:
|
|
306
|
+
roles[idx[f[0]]].append("func")
|
|
307
|
+
used = _usage(prog)
|
|
308
|
+
for i, s in enumerate(prog.strings):
|
|
309
|
+
if i in used["PUSHS"]:
|
|
310
|
+
roles[idx[s]].append("literal")
|
|
311
|
+
if debug:
|
|
312
|
+
for n in prog.global_names:
|
|
313
|
+
if n in idx:
|
|
314
|
+
roles[idx[n]].append("global name (debug)")
|
|
315
|
+
string_rows = sorted(({"text": s, "bytes": _sbytes(s), "roles": roles[i]}
|
|
316
|
+
for i, s in enumerate(strings)), key=lambda r: -r["bytes"])
|
|
317
|
+
by_role = {}
|
|
318
|
+
for r in string_rows:
|
|
319
|
+
key = r["roles"][0] if r["roles"] else "unused"
|
|
320
|
+
by_role[key] = by_role.get(key, 0) + r["bytes"]
|
|
321
|
+
|
|
322
|
+
routines = [{"kind": k, "name": n, "start": s, "bytes": e - s}
|
|
323
|
+
for k, n, _a, _l, s, e in _routines(prog)]
|
|
324
|
+
stacks = {r["name"]: r for r in stack_analysis(prog)}
|
|
325
|
+
for r in routines:
|
|
326
|
+
r["stack"] = stacks[r["name"]]["stack"]
|
|
327
|
+
r["frames"] = stacks[r["name"]]["frames"]
|
|
328
|
+
|
|
329
|
+
# RAM: smallest FS_* settings that run this program
|
|
330
|
+
handler_rows = [r for r in routines if r["kind"] == "handler"]
|
|
331
|
+
unbounded = [r["name"] for r in handler_rows if r["stack"] is None]
|
|
332
|
+
deep_stack = max(handler_rows, key=lambda r: r["stack"] or 0) if handler_rows else None
|
|
333
|
+
deep_frames = max(handler_rows, key=lambda r: r["frames"] or 0) if handler_rows else None
|
|
334
|
+
minimum = dict(DEFAULTS)
|
|
335
|
+
why = {}
|
|
336
|
+
shown = dict(minimum)
|
|
337
|
+
shown["FS_QUEUE_SIZE"] = None
|
|
338
|
+
if not unbounded and handler_rows:
|
|
339
|
+
minimum["FS_STACK_SIZE"] = max(1, deep_stack["stack"])
|
|
340
|
+
why["FS_STACK_SIZE"] = "deepest: %s" % deep_stack["name"]
|
|
341
|
+
minimum["FS_MAX_FRAMES"] = max(1, deep_frames["frames"])
|
|
342
|
+
why["FS_MAX_FRAMES"] = "deepest call chain: %s" % deep_frames["name"]
|
|
343
|
+
elif unbounded:
|
|
344
|
+
why["FS_STACK_SIZE"] = why["FS_MAX_FRAMES"] = "recursion (%s): no static bound" % unbounded[0]
|
|
345
|
+
shown["FS_STACK_SIZE"] = shown["FS_MAX_FRAMES"] = None
|
|
346
|
+
minimum["FS_MAX_GLOBALS"] = max(1, len(prog.globals))
|
|
347
|
+
minimum["FS_MAX_EXTERNS"] = max(1, len(prog.externs))
|
|
348
|
+
minimum["FS_MAX_TIMERS"] = max(1, len(prog.timers))
|
|
349
|
+
minimum["FS_MAX_EVENT_ARGS"] = max([1] + [e[1] for e in prog.events])
|
|
350
|
+
# feature switches: leave out what the program never uses
|
|
351
|
+
need = features(prog)
|
|
352
|
+
switches = []
|
|
353
|
+
for flag, what, macro in FEATURES:
|
|
354
|
+
needed = bool(need & flag)
|
|
355
|
+
switches.append({"macro": macro, "allowed": not needed, "feature": what})
|
|
356
|
+
if not needed:
|
|
357
|
+
minimum[macro] = 1
|
|
358
|
+
if makes_strings(prog):
|
|
359
|
+
why["FS_STR_SLOTS"] = why["FS_STR_LEN"] = "the program makes strings: size for its data"
|
|
360
|
+
else:
|
|
361
|
+
minimum["FS_STR_SLOTS"] = minimum["FS_STR_LEN"] = 1
|
|
362
|
+
why["FS_STR_SLOTS"] = why["FS_STR_LEN"] = "left out with FS_NO_STR"
|
|
363
|
+
for k in ("FS_STACK_SIZE", "FS_MAX_FRAMES", "FS_MAX_GLOBALS", "FS_MAX_EXTERNS",
|
|
364
|
+
"FS_MAX_TIMERS", "FS_MAX_EVENT_ARGS", "FS_STR_SLOTS", "FS_STR_LEN"):
|
|
365
|
+
if not (unbounded and k in ("FS_STACK_SIZE", "FS_MAX_FRAMES")):
|
|
366
|
+
shown[k] = minimum[k]
|
|
367
|
+
if minimum.get("FS_NO_STR"): # no slots at all: their sizes do not matter
|
|
368
|
+
shown["FS_STR_SLOTS"] = shown["FS_STR_LEN"] = None
|
|
369
|
+
flags = ["-D%s" % sw["macro"] for sw in switches if sw["allowed"]]
|
|
370
|
+
why["FS_QUEUE_SIZE"] = "depends on how many events the host posts between runs"
|
|
371
|
+
ram = {
|
|
372
|
+
"rows": [{"macro": k, "default": DEFAULTS[k], "minimum": shown[k], "why": why.get(k, "")}
|
|
373
|
+
for k in ("FS_STACK_SIZE", "FS_MAX_FRAMES", "FS_MAX_GLOBALS", "FS_MAX_EXTERNS",
|
|
374
|
+
"FS_MAX_TIMERS", "FS_MAX_EVENT_ARGS", "FS_STR_SLOTS", "FS_STR_LEN",
|
|
375
|
+
"FS_QUEUE_SIZE")],
|
|
376
|
+
"default_bytes": fs_vm_size(DEFAULTS),
|
|
377
|
+
"minimum_bytes": fs_vm_size(minimum),
|
|
378
|
+
"queue_entry_bytes": queue_entry_layout(minimum)[0],
|
|
379
|
+
"value_bytes": stored_value_bytes(DEFAULTS), "minimum_value_bytes": stored_value_bytes(minimum),
|
|
380
|
+
"switches": switches,
|
|
381
|
+
"cflags": " ".join(flags + ["-D%s=%d" % (k, shown[k]) for k in sorted(shown)
|
|
382
|
+
if shown[k] is not None and shown[k] != DEFAULTS[k]]),
|
|
383
|
+
"unbounded": unbounded,
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
# hints
|
|
387
|
+
hints = []
|
|
388
|
+
names = lambda pairs: [n for n, _a in pairs]
|
|
389
|
+
for i, (n, *_rest) in enumerate(prog.externs):
|
|
390
|
+
if i not in used["XCALL"]:
|
|
391
|
+
hints.append("extern '%s' is never called: removing it saves %d bytes "
|
|
392
|
+
"(and the firmware need not bind it)" % (n, 4 + _sbytes(n)))
|
|
393
|
+
for i, f in enumerate(prog.funcs):
|
|
394
|
+
if i not in used["CALL"]:
|
|
395
|
+
r = [x for x in routines if x["kind"] == "func" and x["name"] == f[0]]
|
|
396
|
+
hints.append("func '%s' is never called: removing it saves %d bytes"
|
|
397
|
+
% (f[0], 6 + _sbytes(f[0]) + (r[0]["bytes"] if r else 0)))
|
|
398
|
+
handled = {ev for _s, ev, _a, _l, _o in prog.handlers}
|
|
399
|
+
for i, (n, *_rest) in enumerate(prog.events):
|
|
400
|
+
if i < len(BUILTIN_EVENTS) or i in prog.timers:
|
|
401
|
+
continue
|
|
402
|
+
if i not in handled and i not in used["EMIT"]:
|
|
403
|
+
hints.append("event '%s' is neither handled nor emitted: removing it saves %d bytes"
|
|
404
|
+
% (n, 4 + _sbytes(n)))
|
|
405
|
+
for ti, ev in enumerate(prog.timers):
|
|
406
|
+
if ti not in used["TIMER"]:
|
|
407
|
+
hints.append("timer '%s' is never started" % prog.events[ev][0])
|
|
408
|
+
gnames = prog.global_names or ["#%d" % i for i in range(len(prog.globals))]
|
|
409
|
+
for i, n in enumerate(gnames):
|
|
410
|
+
if i not in used["LOADG"]:
|
|
411
|
+
hints.append("global '%s' is never read: removing it saves 4 bytes of image and RAM, "
|
|
412
|
+
"plus its stores" % n)
|
|
413
|
+
elif i not in used["STOREG"]:
|
|
414
|
+
hints.append("global '%s' is never assigned: a const would save 4 bytes of image and "
|
|
415
|
+
"RAM and 1 byte per use" % n)
|
|
416
|
+
for i, n in enumerate(prog.states):
|
|
417
|
+
if i and i not in used["GOTO"]:
|
|
418
|
+
hints.append("state '%s' is never entered (no goto_state to it)" % n)
|
|
419
|
+
NAME_ROLES = {"extern", "event", "built-in event", "state", "func", "program name"}
|
|
420
|
+
name_bytes = sum(r["bytes"] for r in string_rows if NAME_ROLES & set(r["roles"]))
|
|
421
|
+
if debug:
|
|
422
|
+
dbg_only = sum(r["bytes"] for r in string_rows if r["roles"] == ["global name (debug)"])
|
|
423
|
+
dbg_bytes = [x[1] for x in sections if x[0] == "debug"][0] + dbg_only
|
|
424
|
+
if name_bytes:
|
|
425
|
+
hints.append("names (externs, events, states, funcs) take %d bytes; they are stored for "
|
|
426
|
+
"binding by name and for fs_state_name(), so shorter names give a smaller "
|
|
427
|
+
"image" % name_bytes)
|
|
428
|
+
if debug and dbg_bytes:
|
|
429
|
+
hints.append("the debug section adds %d bytes: compile without -g for the device" % dbg_bytes)
|
|
430
|
+
if unbounded:
|
|
431
|
+
hints.append("recursion in %s: the stack/frame need cannot be computed; a fault "
|
|
432
|
+
"(stack overflow / call depth) stops it at run time" % ", ".join(unbounded))
|
|
433
|
+
hints.append("RAM settings marked 'minimum' are also load-time limits: fs_load rejects later "
|
|
434
|
+
"downloads that need more, so size them for the largest program you plan to ship")
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
"name": prog.name, "image_bytes": len(image), "debug": debug,
|
|
438
|
+
"debug_bytes": dbg_bytes,
|
|
439
|
+
"sections": [{"name": n, "bytes": b, "detail": d} for n, b, d in sections],
|
|
440
|
+
"routines": routines, "strings": string_rows, "strings_by_role": by_role,
|
|
441
|
+
"ram": ram, "vm_code_bytes": vm_code_bytes(DEFAULTS),
|
|
442
|
+
"minimum_vm_code_bytes": vm_code_bytes(minimum), "hints": hints,
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _bar(n, total, width=24):
|
|
447
|
+
if not total:
|
|
448
|
+
return ""
|
|
449
|
+
k = int(round(width * n / float(total)))
|
|
450
|
+
return "#" * k
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def format_report(rep, title=""):
|
|
454
|
+
out = []
|
|
455
|
+
w = out.append
|
|
456
|
+
total = rep["image_bytes"]
|
|
457
|
+
w("FlowScript size report%s" % ((" — " + title) if title else ""))
|
|
458
|
+
w("")
|
|
459
|
+
w("IMAGE %d bytes (.fsb; the C VM reads it in place, e.g. from flash)%s"
|
|
460
|
+
% (total, " [+%d with -g]" % rep["debug_bytes"] if rep["debug_bytes"] and not rep["debug"] else ""))
|
|
461
|
+
for s in rep["sections"]:
|
|
462
|
+
if not s["bytes"]:
|
|
463
|
+
continue
|
|
464
|
+
w(" %-9s %6d %3d%% %-24s %s" % (s["name"], s["bytes"], round(100.0 * s["bytes"] / total),
|
|
465
|
+
_bar(s["bytes"], total), s["detail"]))
|
|
466
|
+
w("")
|
|
467
|
+
code = sum(r["bytes"] for r in rep["routines"])
|
|
468
|
+
w("CODE BY ROUTINE %d bytes" % code)
|
|
469
|
+
w(" %-30s %6s %5s %6s" % ("", "bytes", "stack", "frames"))
|
|
470
|
+
for r in sorted(rep["routines"], key=lambda r: -r["bytes"]):
|
|
471
|
+
st = "-" if r["stack"] is None else str(r["stack"])
|
|
472
|
+
fr = "-" if r["frames"] is None else str(r["frames"])
|
|
473
|
+
w(" %-30s %6d %5s %6s %s" % (("func " if r["kind"] == "func" else "") + r["name"],
|
|
474
|
+
r["bytes"], st, fr, _bar(r["bytes"], code, 20)))
|
|
475
|
+
w("")
|
|
476
|
+
w("STRINGS %d bytes (%s)" % (sum(r["bytes"] for r in rep["strings"]),
|
|
477
|
+
", ".join("%s %d" % kv for kv in sorted(
|
|
478
|
+
rep["strings_by_role"].items(), key=lambda kv: -kv[1]))))
|
|
479
|
+
for r in rep["strings"][:12]:
|
|
480
|
+
w(" %4d %-32s %s" % (r["bytes"], '"%s"' % r["text"], ", ".join(r["roles"])))
|
|
481
|
+
if len(rep["strings"]) > 12:
|
|
482
|
+
w(" ... %d more" % (len(rep["strings"]) - 12))
|
|
483
|
+
w("")
|
|
484
|
+
ram = rep["ram"]
|
|
485
|
+
w("RAM sizeof(fs_vm) on a 32-bit target: %d bytes with the defaults, %d bytes with the "
|
|
486
|
+
"minimum settings" % (ram["default_bytes"], ram["minimum_bytes"]))
|
|
487
|
+
w(" %-18s %8s %8s" % ("", "default", "minimum"))
|
|
488
|
+
for r in ram["rows"]:
|
|
489
|
+
w(" %-18s %8d %8s %s" % (r["macro"], r["default"],
|
|
490
|
+
"-" if r["minimum"] is None else r["minimum"], r["why"]))
|
|
491
|
+
w(" each queue entry costs %d bytes" % ram["queue_entry_bytes"])
|
|
492
|
+
if ram["cflags"]:
|
|
493
|
+
w(" build flags: %s" % ram["cflags"])
|
|
494
|
+
w("")
|
|
495
|
+
w(" values: %d bytes each with the defaults, %d with the switches above"
|
|
496
|
+
% (ram["value_bytes"], ram["minimum_value_bytes"]))
|
|
497
|
+
w("")
|
|
498
|
+
w("FEATURES (C VM switches: leave out what the program never uses)")
|
|
499
|
+
for sw in ram["switches"]:
|
|
500
|
+
w(" %-12s %s" % (sw["macro"], "allowed: the program uses no %s" % sw["feature"]
|
|
501
|
+
if sw["allowed"] else "not allowed: the program uses %s" % sw["feature"]))
|
|
502
|
+
w("")
|
|
503
|
+
w("C VM CODE ~%.1f KB (flowscript.c, Cortex-M4 -Os, linked) with all features, ~%.1f KB with the "
|
|
504
|
+
"switches this program allows" % (rep["vm_code_bytes"] / 1024, rep["minimum_vm_code_bytes"] / 1024))
|
|
505
|
+
if not any(sw["macro"] == "FS_NO_FLOAT" and sw["allowed"] for sw in ram["switches"]):
|
|
506
|
+
w(" plus the C library's soft-float and libm routines")
|
|
507
|
+
if rep["hints"]:
|
|
508
|
+
w("")
|
|
509
|
+
w("HINTS")
|
|
510
|
+
for h in rep["hints"]:
|
|
511
|
+
w(" - " + h)
|
|
512
|
+
return "\n".join(out) + "\n"
|