flowscript-vm 0.1.0__tar.gz

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.
Files changed (86) hide show
  1. flowscript_vm-0.1.0/LICENSE +21 -0
  2. flowscript_vm-0.1.0/MANIFEST.in +6 -0
  3. flowscript_vm-0.1.0/PKG-INFO +400 -0
  4. flowscript_vm-0.1.0/README.md +372 -0
  5. flowscript_vm-0.1.0/SPEC.md +556 -0
  6. flowscript_vm-0.1.0/c/Makefile +72 -0
  7. flowscript_vm-0.1.0/c/codesize.py +101 -0
  8. flowscript_vm-0.1.0/c/flowscript.c +2086 -0
  9. flowscript_vm-0.1.0/c/flowscript.h +448 -0
  10. flowscript_vm-0.1.0/c/fsrun.c +527 -0
  11. flowscript_vm-0.1.0/c/test_api.c +270 -0
  12. flowscript_vm-0.1.0/examples/gps_tracker.fs +122 -0
  13. flowscript_vm-0.1.0/examples/gps_tracker.scenario +21 -0
  14. flowscript_vm-0.1.0/examples/pct_pid.fs +137 -0
  15. flowscript_vm-0.1.0/examples/pct_pid.scenario +15 -0
  16. flowscript_vm-0.1.0/flowscript/__init__.py +35 -0
  17. flowscript_vm-0.1.0/flowscript/__main__.py +4 -0
  18. flowscript_vm-0.1.0/flowscript/api.py +286 -0
  19. flowscript_vm-0.1.0/flowscript/bytecode.py +372 -0
  20. flowscript_vm-0.1.0/flowscript/cli.py +126 -0
  21. flowscript_vm-0.1.0/flowscript/compiler.py +1019 -0
  22. flowscript_vm-0.1.0/flowscript/debugger/__init__.py +0 -0
  23. flowscript_vm-0.1.0/flowscript/debugger/server.py +505 -0
  24. flowscript_vm-0.1.0/flowscript/debugger/static/app.js +626 -0
  25. flowscript_vm-0.1.0/flowscript/debugger/static/chart.js +620 -0
  26. flowscript_vm-0.1.0/flowscript/debugger/static/edit.js +513 -0
  27. flowscript_vm-0.1.0/flowscript/debugger/static/index.html +115 -0
  28. flowscript_vm-0.1.0/flowscript/debugger/static/style.css +216 -0
  29. flowscript_vm-0.1.0/flowscript/intops.py +63 -0
  30. flowscript_vm-0.1.0/flowscript/lexer.py +265 -0
  31. flowscript_vm-0.1.0/flowscript/mermaid.py +109 -0
  32. flowscript_vm-0.1.0/flowscript/parser.py +578 -0
  33. flowscript_vm-0.1.0/flowscript/printer.py +225 -0
  34. flowscript_vm-0.1.0/flowscript/runner.py +164 -0
  35. flowscript_vm-0.1.0/flowscript/sizes.py +512 -0
  36. flowscript_vm-0.1.0/flowscript/svg.py +766 -0
  37. flowscript_vm-0.1.0/flowscript/values.py +724 -0
  38. flowscript_vm-0.1.0/flowscript/vm.py +577 -0
  39. flowscript_vm-0.1.0/flowscript_vm.egg-info/PKG-INFO +400 -0
  40. flowscript_vm-0.1.0/flowscript_vm.egg-info/SOURCES.txt +84 -0
  41. flowscript_vm-0.1.0/flowscript_vm.egg-info/dependency_links.txt +1 -0
  42. flowscript_vm-0.1.0/flowscript_vm.egg-info/entry_points.txt +2 -0
  43. flowscript_vm-0.1.0/flowscript_vm.egg-info/top_level.txt +1 -0
  44. flowscript_vm-0.1.0/pyproject.toml +47 -0
  45. flowscript_vm-0.1.0/setup.cfg +4 -0
  46. flowscript_vm-0.1.0/tests/api/api.fs +36 -0
  47. flowscript_vm-0.1.0/tests/conformance.py +123 -0
  48. flowscript_vm-0.1.0/tests/fuzz_c.py +116 -0
  49. flowscript_vm-0.1.0/tests/programs/arith.fs +40 -0
  50. flowscript_vm-0.1.0/tests/programs/arith.scenario +31 -0
  51. flowscript_vm-0.1.0/tests/programs/bootfault.fs +25 -0
  52. flowscript_vm-0.1.0/tests/programs/bootfault.scenario +3 -0
  53. flowscript_vm-0.1.0/tests/programs/events.fs +49 -0
  54. flowscript_vm-0.1.0/tests/programs/events.scenario +16 -0
  55. flowscript_vm-0.1.0/tests/programs/faults.fs +107 -0
  56. flowscript_vm-0.1.0/tests/programs/faults.scenario +14 -0
  57. flowscript_vm-0.1.0/tests/programs/floats.fs +55 -0
  58. flowscript_vm-0.1.0/tests/programs/floats.scenario +7 -0
  59. flowscript_vm-0.1.0/tests/programs/hosttimers.fs +43 -0
  60. flowscript_vm-0.1.0/tests/programs/hosttimers.scenario +12 -0
  61. flowscript_vm-0.1.0/tests/programs/livelock.fs +42 -0
  62. flowscript_vm-0.1.0/tests/programs/livelock.scenario +7 -0
  63. flowscript_vm-0.1.0/tests/programs/logic.fs +37 -0
  64. flowscript_vm-0.1.0/tests/programs/logic.scenario +14 -0
  65. flowscript_vm-0.1.0/tests/programs/nostates.fs +9 -0
  66. flowscript_vm-0.1.0/tests/programs/statetimers.fs +65 -0
  67. flowscript_vm-0.1.0/tests/programs/statetimers.scenario +9 -0
  68. flowscript_vm-0.1.0/tests/programs/statevars.fs +33 -0
  69. flowscript_vm-0.1.0/tests/programs/statevars.scenario +9 -0
  70. flowscript_vm-0.1.0/tests/programs/strings.fs +14 -0
  71. flowscript_vm-0.1.0/tests/programs/strings.scenario +1 -0
  72. flowscript_vm-0.1.0/tests/programs/strs.fs +75 -0
  73. flowscript_vm-0.1.0/tests/programs/strs.scenario +18 -0
  74. flowscript_vm-0.1.0/tests/programs/timers.fs +51 -0
  75. flowscript_vm-0.1.0/tests/programs/timers.scenario +47 -0
  76. flowscript_vm-0.1.0/tests/programs/times.fs +42 -0
  77. flowscript_vm-0.1.0/tests/programs/times.scenario +17 -0
  78. flowscript_vm-0.1.0/tests/programs/transitions.fs +78 -0
  79. flowscript_vm-0.1.0/tests/programs/transitions.scenario +10 -0
  80. flowscript_vm-0.1.0/tests/programs/types.fs +39 -0
  81. flowscript_vm-0.1.0/tests/programs/types.scenario +11 -0
  82. flowscript_vm-0.1.0/tests/reject/bighandler.fs +248 -0
  83. flowscript_vm-0.1.0/tests/test_compiler_vm.py +394 -0
  84. flowscript_vm-0.1.0/tests/test_host_api.py +229 -0
  85. flowscript_vm-0.1.0/tests/test_roundtrip.py +151 -0
  86. flowscript_vm-0.1.0/tests/test_sizes.py +107 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruben Nørgaard
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,6 @@
1
+ include README.md SPEC.md LICENSE
2
+ recursive-include flowscript/debugger/static *
3
+ recursive-include examples *.fs *.scenario
4
+ recursive-include tests *.py *.fs *.scenario
5
+ include c/*.c c/*.h c/Makefile c/*.py
6
+ global-exclude *.pyc __pycache__ *.fsb
@@ -0,0 +1,400 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowscript-vm
3
+ Version: 0.1.0
4
+ Summary: An event-driven scripting language whose programs are flowcharts: compiler, bytecode VM and SVG flowcharts
5
+ Author: Ruben Nørgaard
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/RubenNorgaard/flowscript
8
+ Project-URL: Specification, https://github.com/RubenNorgaard/flowscript/blob/main/SPEC.md
9
+ Keywords: scripting,state machine,flowchart,bytecode,virtual machine,embedded
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Compilers
21
+ Classifier: Topic :: Software Development :: Interpreters
22
+ Classifier: Topic :: Software Development :: Embedded Systems
23
+ Classifier: Operating System :: OS Independent
24
+ Requires-Python: >=3.9
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Dynamic: license-file
28
+
29
+ # FlowScript
30
+
31
+ FlowScript is a small event-driven language for the **flow control** of a bigger system.
32
+ The system software (C firmware, a Python service, …) stays fixed and stable; the logic
33
+ is written in FlowScript, compiled to compact bytecode and downloaded to the device.
34
+
35
+ The language is designed so that **every statement is one flowchart node** and **every
36
+ condition is a logic-gate circuit**. Script and flowchart are two views of the same
37
+ model, so you can edit either one.
38
+
39
+ ```
40
+ ┌────────── source (.fs) ──────────┐ ┌───────── flowchart ─────────┐
41
+ │ state driving: │ │ ⬡ on report │
42
+ │ on report: │ parse │ │ │
43
+ │ if speed > LIMIT and not low:│ ────▶ │ [speed>LIMIT]─┐ │
44
+ │ send_position(2) │ ◀──── │ [low]─NOT─────AND──▶ ◇ yes │
45
+ │ │ format│ ▱ send_position(2)│
46
+ └──────────────────────────────────┘ └──────────────────────────────┘
47
+ │ compile
48
+ ▼
49
+ bytecode (.fsb) ──▶ C VM (firmware) flowscript.c, no malloc, ~16 KB Thumb-2
50
+ └─▶ Python VM reference implementation + debugger
51
+ ```
52
+
53
+ ## Quick start
54
+
55
+ ```sh
56
+ python3 -m flowscript debug examples/gps_tracker.fs # 3-view debugger in the browser
57
+ python3 -m flowscript compile examples/gps_tracker.fs # -> examples/gps_tracker.fsb
58
+ python3 -m flowscript disasm examples/gps_tracker.fsb
59
+ python3 -m flowscript run examples/gps_tracker.fs examples/gps_tracker.scenario
60
+ python3 -m flowscript fmt examples/gps_tracker.fs # canonical formatting
61
+ python3 -m flowscript mermaid examples/gps_tracker.fs # Mermaid flowchart export
62
+ python3 -m flowscript size examples/gps_tracker.fs # where the bytes go + RAM needed
63
+ python3 -m flowscript ast examples/gps_tracker.fs # the visual model as JSON
64
+
65
+ cd c && make && ./fsrun ../examples/gps_tracker.fsb ../examples/gps_tracker.scenario
66
+ ```
67
+
68
+ Requirements: Python 3.9+ (stdlib only), any C99 compiler. No other dependencies.
69
+
70
+ ## Using FlowScript from Python
71
+
72
+ ```sh
73
+ pip install flowscript-vm # imports as `flowscript`; adds the `flowscript` command
74
+ ```
75
+
76
+ A Python service can compile scripts, run them (from source or bytecode) and draw them:
77
+
78
+ ```python
79
+ import time
80
+ import flowscript
81
+
82
+ program = flowscript.compile(source) # flowscript.CompileError(.line, .col) on errors
83
+ for w in program.warnings: # compiles, but probably not as intended
84
+ print(w.line, w.message)
85
+
86
+ machine = flowscript.Machine(
87
+ program, # or bytecode: flowscript.load(path_or_bytes)
88
+ externs={"param": settings.__getitem__, # one Python callable per `extern`
89
+ "diag": diagnostics.__setitem__},
90
+ clock=time.time, # what now() returns (seconds, or a datetime)
91
+ on_event=lambda name, args: ..., # every raise_event
92
+ on_fault=lambda fault: log.warning(fault), # also kept in machine.faults
93
+ )
94
+ machine.start() # on boot, then enter the initial state
95
+ machine.send("tick") # post an event and run until idle
96
+ machine.advance(1000) # let timers run for 1 s (or run them yourself:
97
+ # on_timer=... and machine.timer_expired(name))
98
+ machine.state # "trading"
99
+ machine["aggression"], machine.globals # variables by name
100
+
101
+ data = program.to_bytes() # the .fsb image the C VM runs; program.save(path)
102
+ ```
103
+
104
+ Values cross the boundary as plain Python: `int`, `float`, `str` and timezone-aware
105
+ `datetime` for FlowScript's `time`. The Python VM is the reference implementation: a
106
+ program behaves exactly as on the C VM, including faults (a faulting handler is aborted
107
+ and reported; the machine carries on).
108
+
109
+ ### Flowcharts as SVG
110
+
111
+ ```python
112
+ svg = flowscript.to_svg(program) # the flowchart
113
+ svg = flowscript.to_svg(program, view="states") # the state diagram
114
+ svg = flowscript.to_svg(program, state=machine.state, # highlight the live state
115
+ width="100%") # scale to the container
116
+ ```
117
+
118
+ The result is a self-contained `<svg>` string: inline it in a page (in a Django template:
119
+ `{{ svg|safe }}`) or save it as a file. It has no scripts and no external references.
120
+ Its colours are CSS variables (`--fs-bg`, `--fs-fg`, `--fs-accent`, ... see
121
+ `flowscript.svg.THEMES`) that the page may override, and `theme="auto"` (the default)
122
+ follows `prefers-color-scheme` and a `data-theme="light"`/`"dark"` attribute on an
123
+ ancestor such as `<html>`; `theme="dark"`/`"light"` fix it. Nodes carry `data-line` (the
124
+ source line) and states `data-state`, for pages that add their own interaction.
125
+
126
+ ## The language in one page
127
+
128
+ ```flowscript
129
+ flow tracker version 3 # name + version are stored in the bytecode
130
+
131
+ extern gps_speed() -> int # functions the host provides (bound by name at load)
132
+ extern send_position(int reason, str note) # no '-> type': returns nothing
133
+ event ignition(int active) # events the host raises (up to 8 typed args)
134
+ timer report # VM-managed timer; also an event called "report"
135
+ const LIMIT = 90 # compile-time constant (any type)
136
+ int reports = 0 # global variables: int, float, str, time
137
+ time since = time(2026, 1, 1)
138
+
139
+ func notify(int reason, int count) -> int: # sub-process: sees params, consts, externs, not globals
140
+ send_position(reason, "report " + str(count))
141
+ return count + 1 # the result goes back to the handler
142
+
143
+ any_state: # handlers that apply in every state
144
+ on boot: # runs once at start
145
+ start report every 30s # time units: ms, s, min, h
146
+ since = now() # the host's clock
147
+
148
+ initial state parked: # the program starts here (required with 2+ states)
149
+ on ignition(active):
150
+ if active:
151
+ goto_state driving # ends the handler; runs exit/enter handlers
152
+
153
+ state driving:
154
+ int slow_reports = 0 # state variable: only visible here, reset on each entry
155
+
156
+ on report: # replaces an any_state 'on report' here
157
+ float kmh = gps_speed() * 1.0 # local variable; ints promote to float
158
+ if kmh > LIMIT and not reports > 100:
159
+ reports = notify(2, reports)
160
+ elif kmh == 0 and now() - since > 10min:
161
+ raise_event ignition(0) # queued for the script + host notified
162
+ else:
163
+ slow_reports = slow_reports + 1
164
+ reports = notify(1, reports)
165
+ ```
166
+
167
+ Blocks are defined by indentation, as in Python: a header line ends with `:` and its
168
+ body is indented below it (the formatter uses 2 spaces; tabs are rejected). `pass`
169
+ marks an empty block. One statement per line, so every line is one flowchart node.
170
+
171
+ Four types — `int` (int32), `float` (double), `str`, `time` (UTC, ms) — checked at compile
172
+ time, with int → float promotion and nothing else implicit; built-in conversions
173
+ (`int() float() str() time()`), string (`len substr find`), time (`now date year … weekday`)
174
+ and math functions (`abs min max floor ceil round sqrt exp log pow`). Statements:
175
+ `if/elif/else`, `while`, `goto_state`, `raise_event`, `start/stop`, `return`, functions,
176
+ and the operators `or xor and not == != < <= > >= | ^ & << >> + - * / % ~`. That is the
177
+ whole language. See [SPEC.md](https://github.com/RubenNorgaard/flowscript/blob/main/SPEC.md) for the exact semantics and the bytecode format.
178
+
179
+ Strings made at run time live in a fixed pool of slots inside the VM (`FS_STR_SLOTS` ×
180
+ `FS_STR_LEN` bytes, default 16 × 64), reclaimed by a mark-and-sweep pass when the pool is
181
+ full: no heap, and running out is a fault like any other. `examples/pct_pid.fs` is a
182
+ larger program (a PID-steered market-making controller) using floats and times.
183
+
184
+ | Statement | Flowchart symbol |
185
+ |---|---|
186
+ | `on EVENT` | event (hexagon) |
187
+ | `state S` / `any_state` | state container / dashed container around all states |
188
+ | `x = …`, `var x = …` | process (rectangle) |
189
+ | state `var` | "state variables" card inside the state container |
190
+ | `extern_fn(…)` | I/O (parallelogram) |
191
+ | `func_name(…)` | predefined process (double-sided rectangle) |
192
+ | `if` / `elif` / `else` | decision (diamond) fed by a logic-gate tree |
193
+ | `while` | decision with a back edge |
194
+ | `and` `or` `xor` `not` | AND / OR / XOR / NOT gates |
195
+ | `goto_state S` | state transition (red terminator) |
196
+ | `raise_event E` | signal (flag) |
197
+ | `start` / `stop` | timer (hourglass) |
198
+ | `return` | terminator |
199
+ | `# comment` | annotation (attached to the node below it) |
200
+
201
+ ## The debugger (3 views)
202
+
203
+ `python3 -m flowscript debug FILE.fs` opens a browser page with
204
+
205
+ 1. **FlowScript source** – editable, syntax highlighted, click the gutter for breakpoints.
206
+ 2. **Bytecode** – disassembly with the current instruction and the instructions of the
207
+ current statement highlighted.
208
+ 3. **Flowchart** – the executing node is highlighted; conditions are drawn as logic
209
+ gates that turn green/red as their inputs are evaluated. A *States* tab shows the
210
+ state machine derived from the `goto_state`s.
211
+
212
+ Hovering in any view highlights the corresponding parts of the other two.
213
+ Stepping: *Step instr* (F7), *Step node* (F11), *Step over* (F10), *Step out*
214
+ (Shift+F11), *Finish* handler, *Run* to idle/breakpoint (F5). Post events with
215
+ arguments, advance time with *Tick*, set the return values of the host functions
216
+ (“host stubs”), and edit globals live.
217
+
218
+ **Visual editing:** tick *edit* in the flowchart pane. Click any node to edit it in the
219
+ inspector, use the `+` handles on the edges to insert nodes, wrap any condition input in
220
+ `NOT` / `AND` / `OR` / `XOR`, add handlers, functions, states and declarations. Every
221
+ change regenerates the script (and recompiles), so the two views never diverge.
222
+
223
+ ## Embedding the C VM
224
+
225
+ ```c
226
+ #include "flowscript.h"
227
+
228
+ static fs_vm vm; /* static, no heap */
229
+
230
+ static fs_value x_gps_speed(fs_vm *vm, const fs_value *a, int n, void *u) { return fs_int(gps_speed_kmh()); }
231
+ static fs_value x_send(fs_vm *vm, const fs_value *a, int n, void *u) {
232
+ modem_send(a[0].as.i, fs_str(vm, a[1], NULL)); /* a str argument: valid during the call */
233
+ return fs_int(0); /* ignored: send_position returns nothing */
234
+ }
235
+ static int64_t utc_ms(fs_vm *vm, void *u) { return rtc_unix_ms(); } /* now() */
236
+
237
+ void logic_init(const uint8_t *image, size_t len) {
238
+ if (fs_load(&vm, image, len) != FS_OK) { /* reject download, keep old logic */ }
239
+ fs_bind(&vm, "gps_speed", x_gps_speed);
240
+ fs_bind(&vm, "send_position", x_send);
241
+ fs_set_clock(&vm, utc_ms);
242
+ /* ... bind every extern; fs_unbound(&vm) names any you forgot ... */
243
+ fs_start(&vm);
244
+ fs_run(&vm, 100000);
245
+ }
246
+
247
+ void on_ignition(int on) { /* firmware event -> script */
248
+ fs_value arg = fs_int(on); /* fs_float(), fs_time(), fs_cstr("text") */
249
+ fs_post(&vm, fs_event_id(&vm, "ignition"), &arg, 1);
250
+ fs_run(&vm, 100000);
251
+ }
252
+
253
+ void every_10ms(void) { fs_tick(&vm, 10); fs_run(&vm, 100000); }
254
+ ```
255
+
256
+ `fs_tick` is the simple option. For low-power firmware, let the hardware run the
257
+ timers instead, so the CPU sleeps until something is actually due:
258
+
259
+ ```c
260
+ static void on_timer(fs_vm *vm, int timer, int start, int32_t ms, int periodic, void *u) {
261
+ if (start) hw_timer_arm(timer, ms, periodic); /* RTC alarm, LPTIM, RTOS timer, ... */
262
+ else hw_timer_cancel(timer);
263
+ }
264
+
265
+ void main_loop(void) { /* or an RTOS task */
266
+ for (;;) {
267
+ int timer;
268
+ sleep_until_interrupt(); /* timer ISRs only record what fired */
269
+ while (hw_timer_pop_expired(&timer))
270
+ fs_timer_expired(&vm, timer); /* ignored if the script stopped it */
271
+ fs_run(&vm, 100000);
272
+ }
273
+ }
274
+
275
+ /* in logic_init, before fs_start(): */
276
+ fs_set_timer_handler(&vm, on_timer);
277
+ ```
278
+
279
+ Like every VM call, `fs_timer_expired` and `fs_run` must not interrupt a running
280
+ `fs_run`, so call them from one task or the main loop, not directly from an ISR.
281
+ A timer belongs to the state whose handler started it and stops automatically when
282
+ that state is left; in this mode the VM then calls `on_timer(..., start = 0, ...)`.
283
+
284
+ See [`c/flowscript.h`](https://github.com/RubenNorgaard/flowscript/blob/main/c/flowscript.h) for the full API and `c/fsrun.c` for a complete
285
+ host. The loader validates the whole image and every run-time operand, faults abort only
286
+ the current handler, each handler has an instruction budget, and a script that keeps
287
+ itself busy forever (e.g. two states whose `enter` handlers `goto_state` each other) is stopped
288
+ after 256 handler runs in a row. A bad download can therefore never take down the
289
+ firmware.
290
+
291
+ ## Repository layout
292
+
293
+ ```
294
+ SPEC.md language + bytecode specification (normative)
295
+ pyproject.toml the Python package (pip install flowscript-vm)
296
+ flowscript/ Python toolchain
297
+ api.py host API: compile(), load(), Machine
298
+ svg.py SVG flowcharts and state diagrams (to_svg)
299
+ lexer.py parser.py source -> AST (= visual model, JSON)
300
+ printer.py AST -> canonical source
301
+ compiler.py AST -> bytecode + debug info
302
+ bytecode.py .fsb format, opcodes, disassembler
303
+ vm.py reference VM
304
+ runner.py scenario runner (canonical trace format)
305
+ mermaid.py Mermaid export
306
+ debugger/ 3-view web debugger (server + static UI)
307
+ c/ C VM (flowscript.c/.h), fsrun CLI, Makefile
308
+ examples/ example programs + scenarios
309
+ tests/ unit tests, round-trip tests, C/Python conformance, fuzzing
310
+ ```
311
+
312
+ ## Tests
313
+
314
+ ```sh
315
+ python3 -m unittest discover -s tests # compiler, VM, round trip
316
+ cd c && make test # C API test + C/Python conformance (normal, ASan/UBSan,
317
+ # and the reduced FS_NO_* builds)
318
+ cd c && make fuzz # corrupt-image fuzzing against the ASan build
319
+ python3 tests/fuzz_c.py --runner fsrun-min # ... or against a reduced build
320
+ python3 c/codesize.py # VM code size for every switch combination
321
+ cd c && make size # code size of the VM
322
+ ```
323
+
324
+ C VM footprint (default sizes): **about 16 KB** of code for Cortex-M4 (`-Os`, Thumb-2,
325
+ what a `--gc-sections` link keeps, measured with `c/codesize.py`; v1, ints only, was
326
+ 6.6 KB), plus the C library's double-precision routines (soft-float on a Cortex-M4) and
327
+ `floor ceil sqrt exp log pow fmod`; no data/bss of its own; a `fs_vm` struct of 6,080
328
+ bytes RAM on a 32-bit target with the default `FS_*` macros in `flowscript.h`. Most of
329
+ that is headroom: see below. Build with `-ffp-contract=off` (fused multiply-add would
330
+ change float results) and link `-lm`.
331
+
332
+ Inside the VM a value is stored as a payload cell (8 bytes: a double or an int64 time
333
+ fits) plus a 1-byte type tag kept in a separate array, so each global, stack slot and
334
+ queued event argument costs 9 bytes rather than the 16 a tagged struct would take after
335
+ padding. Hosts never see this: the API passes `fs_value` structs.
336
+
337
+ ### Feature switches
338
+
339
+ Define these when building the C VM to leave out what your programs never use:
340
+
341
+ | Switch | Leaves out | Still works |
342
+ |---|---|---|
343
+ | `FS_NO_FLOAT` | `float` values, the float built-ins, soft-float and libm | everything else; no `-lm` needed |
344
+ | `FS_NO_TIME` | `time` values, `now()` and the date built-ins | `start t every 30s` (timers are not times) |
345
+ | `FS_NO_STR` | strings made at run time: `+` on strings, `str()`, `substr()`, `str` results and event arguments from the host, the string slots | string literals, e.g. `log("started", 3)`, `len()` and `find()` on them |
346
+ | `FS_NO_TEXT` | text conversion of floats and times in scripts: `str(float)`, `str(time)`, `float("...")`, `time("...")` | both types and all their arithmetic; `int` to and from text |
347
+
348
+ The compiler records in the image which of these a program needs, and `fs_load()`
349
+ refuses an image that needs a left-out feature with `FS_ERR_FEATURE`, so a mismatch
350
+ is caught at download, not in the field. `flowscript size` lists the switches a
351
+ program allows and includes them in its suggested build flags.
352
+
353
+ `fs_format()`, `fs_parse_float()` and `fs_parse_time()` stay available to the host in
354
+ every build (the test runner prints its traces with them). A device that never calls
355
+ them should link with `-ffunction-sections -Wl,--gc-sections`, so they and, with
356
+ `FS_NO_TEXT`, all the text code are dropped; the figures below assume that.
357
+
358
+ | Switches | VM code (Cortex-M4 `-Os`) | RAM per stored value |
359
+ |---|---|---|
360
+ | none | 15.7 KB + soft-float and libm | 9 bytes |
361
+ | `FS_NO_STR` | 13.5 KB + soft-float and libm | 9 bytes |
362
+ | `FS_NO_TEXT` | 12.5 KB + soft-float and libm | 9 bytes |
363
+ | `FS_NO_STR FS_NO_TEXT` (e.g. `pct_pid`) | 11.5 KB + soft-float and libm | 9 bytes |
364
+ | `FS_NO_TIME` | 12.5 KB + soft-float and libm | 9 bytes |
365
+ | `FS_NO_FLOAT` | 13.2 KB | 9 bytes |
366
+ | `FS_NO_FLOAT FS_NO_TIME` | 9.2 KB | 5 bytes |
367
+ | `FS_NO_FLOAT FS_NO_TIME FS_NO_STR` (e.g. the GPS tracker) | **8.2 KB**, needing only `memcpy memset memcmp strlen` | 5 bytes |
368
+
369
+ `python3 c/codesize.py` prints all 16 combinations. Every combination compiles
370
+ warning-free (`make switches`), and the conformance test runs each program on four
371
+ reduced builds, checking identical traces where the program is allowed and a refusal
372
+ where it is not.
373
+
374
+ ## Optimizing size
375
+
376
+ `python3 -m flowscript size FILE.fs` (or `compile --size`, or the **Size** tab in the
377
+ debugger's bytecode pane) shows the three budgets separately:
378
+
379
+ * **Image** (the `.fsb`): bytes per section, code per handler/function, and every
380
+ string with what it is used for. The C VM reads the image in place, so it costs flash
381
+ (or wherever the host keeps the download), not RAM.
382
+ * **RAM** (`sizeof(fs_vm)`): computed for the defaults and for the smallest `FS_*`
383
+ settings this program runs with. Stack and call depth come from a worst-case analysis of
384
+ the bytecode; the report prints ready-to-use `-D` build flags, including the feature
385
+ switches. For the GPS example that is 384 bytes instead of 6,080: it uses no floats,
386
+ times or run-time strings, so those switches apply, a stored value is 5 bytes and
387
+ there are no string slots. These settings are also load-time limits, so size them
388
+ for the largest program you plan to download. Recursion has no static bound and keeps
389
+ the defaults.
390
+ * **C VM code**: independent of the program, but set by the feature switches: about
391
+ 16 KB with everything, 8.2 KB with the smallest set (see the table above).
392
+
393
+ It also lists concrete savings: unused externs, functions, events, timers and globals,
394
+ globals that could be constants, the cost of names, and the debug section (`-g`).
395
+ The struct layout is checked against the C compiler and the minimum settings against
396
+ every test program (`tests/test_sizes.py`).
397
+
398
+ ## License
399
+
400
+ MIT, see [LICENSE](https://github.com/RubenNorgaard/flowscript/blob/main/LICENSE).