Synoema

IoT Rules DSL

Strict Synoema subset for LLM-generated rules — runs on Linux/sysfs and bare-MCU WASM

The Rules DSL is not a separate language. It is Synoema with a narrower grammar: same parser, same type checker, same code generator. The narrower grammar gives two guarantees:

  1. Portability. A rule that parses under the DSL grammar runs unchanged on a Linux IoT board (sysfs GPIO) and on an STM32 / ESP32 / nRF MCU (compiled to WASM, executed by wasm3).
  2. LLM safety. The corresponding GBNF (synoema-iot-rules.gbnf) is fed to constrained decoding. The model literally cannot sample a non-conformant token — http_get, spawn, 3.14 are all unreachable at sampling time.

Rule form — prefix convention

A rule is an ordinary top-level function whose name starts with rule_. No new keyword, no annotation. Tooling enumerates rules by walking top-level bindings and grepping for the prefix.

rule_temp_fan temp =
  ? temp > 25 -> gpio_write 17 1 : gpio_write 17 0

Allowed constructs

CategoryAllowed
LiteralsInt, Nat (10n), Bool, String, Char
Operators+ - * / %, == != < > <= >=, && ||, ++, |>, >>, ->
Conditional? cond -> then : else — ternary, chainable
Functionsf x = expr, multi-arg, pattern-match heads
Lambdas\x -> expr
Lists[1 2 3], comprehensions [x | x <- xs, x > 0], ranges [1 .. 10]
Tuples / Records(a, b); { k = v, ... }
ADTstype Status = Ok | Err Int
Pattern matchingfunction heads, let-like bindings, case via cond chain
GPIO builtinsgpio_read : Int -> Int, gpio_write : Int -> Int -> (), gpio_mode : Int -> String -> ()
Pure helpersmax, min, arithmetic, comparisons, list HOFs (map, filter, fold)
Sleepsleep : Int -> () — milliseconds

Forbidden constructs (with reasons)

CategoryForbiddenWhy
Networkinghttp_get, http_post, tcp_*No TCP/IP stack on bare MCU
Filesystemfile_read, file_write, fd_popenNo POSIX host on MCU target
Environmentenv_get, argsSame
Concurrencyspawn, pmap, scope, channel_*wasm3 is single-threaded
Floats3.14, any Float bindingWASM v2 has no float lowering yet
Rationals(1 / 3) rational literalSame
Bytes / multiline stringsb"...", """..."""Surface kept small for the LLM's benefit
Modulesimport, mod, use, trait, implRule files are single-file by design

Worked example — temperature-driven fan

-- Rule: fan on at >25 °C, off otherwise.
rule_temp_fan temp =
  ? temp > 25 -> gpio_write 17 1 : gpio_write 17 0

main =
  m = gpio_mode 17 "out"
  m |> \_ -> rule_temp_fan 28

Verifier lang/tools/constrained/verify_iot_rules_gbnf.sh applies four grep-based checks:

  1. At least one top-level rule_* binding
  2. No forbidden identifier appears
  3. No float literal pattern ([0-9]+\.[0-9]+)
  4. No rational literal ((N / M) form)

Naming patterns — tactical hints for the LLM

  • Single-threshold: rule_<sensor>_<actuator> — e.g. rule_temp_fan, rule_pressure_pump
  • Pattern-match on digital input: rule_<what>_<alarm> — e.g. rule_water_leak, rule_door_open
  • Hysteresis (two thresholds + previous state): signature rule_xxx temp lo hi prev
  • Moving-average / duty-cycle: precompute avg separately; the rule takes a scalar input plus a trigger

Cross-references

  • Full grammar: lang/tools/constrained/synoema.gbnf
  • Rule subset GBNF: lang/tools/constrained/synoema-iot-rules.gbnf
  • Bare-MCU runtime subset spec: openspec/specs/bare-mcu-subset/spec.md
  • Linux/sysfs IoT capability spec: openspec/specs/iot-runtime-linux/spec.md
  • 5 reference rule files: lang/examples/iot/rules/
  • IoT architecture: /iot — tier model + verticals
  • LLM tooling: /llm — MCP, RAG, ReAct