Handlers: TypeScript, Python, the action language
A model's behaviour — a function's body, a port's on-receive, a state's
on-enter — is code. It is written in TypeScript (what archec new writes),
in Python, or in the action language (```arche), which archec
translates. This page is the contract between archec and Arche's simulator:
both sides do what it says. Anything it does not say, do what the Python runtime
does (Arche's arche_runtime.py), with the same names.
1. Which language a piece of code is in#
In arche-lang:
- The project says it:
project { handlers typescript }— orpython, orarche. - A component may say its own:
component Legacy { handlers python }. Without one, a component takes its definition's, the one around it, itscodefiles' (code "ctrl.ts"is TypeScript,code "ctrl.py"Python), else the project's. - A block says its own:
```ts,```python,```arche. A bare```is the component's language. - A project without
handlerskeeps what its code is: Python somewhere (a.pyfile, a```pythonor bare block) makes itpython; otherwise it istypescript. archec newwriteshandlers typescript(archec new --handlers python|archefor the others).
In the Arche project:
simulation.yamlsays the project's language:behavioral: { language: typescript | python }. Absent ispython, so every existing project keeps working. archec writes it when the project sayshandlers, or when it has code and that code is TypeScript.- A model file's code file decides for that file's code:
code: "rfic.ts"is TypeScript,code: "rfic.py"Python. - Inline code — activation bodies, handlers, state actions, guards written as code — follows the component's code file when it has one, else the project's language.
The action language is translated: to the project's language for Arche
(TypeScript, or Python in a handlers python project; handlers arche gives
TypeScript), and to C, Rust or Go for firmware, as before. A call to another
part, brake.apply(2.5) through a port that requires an interface with that
operation, becomes what a handler writes for it: await component("brk").apply(2.5)
for the part at the other end of the link (the specification).
archec copies every body into the Arche project (a code "x.ts" function becomes
the inline body it would be written as), so Arche runs one language for the
whole project. A block in the other language is reported (handlers-language):
archec sim runs it, Arche would not.
2. What TypeScript code looks like#
The source is TypeScript. Both sides strip the types and run the JavaScript: archec blanks them out itself (src/tscode.ts — nothing to install), Arche with a small in-page transpiler. Nothing is type-checked to run.
A code file mirrors a .py file one to one, with the same function names:
import { send, reply, goto, wait, ms, warn, u8 } from "arche" // optional: the names are global anyway
import type { Rfic, SPI_FRAME } from "./arche" // archec stubs: the model's types
export async function rfic_on_receive_spi(self: Rfic, msg: Rfic.Msg) {
if (msg.name !== "SPI_FRAME" || (msg.cs !== self.cs_index && msg.cs !== RFIC_BROADCAST)) return
self.frames += 1
if (msg.cmd === 0) reply(SPI_FRAME({ cs: msg.cs, cmd: 0, addr: msg.addr, data: await self.readRegister(msg.addr) }))
else await self.writeRegister(msg.addr, msg.data)
}
- The first parameter is
self, as in Python.self.xreads and writes a variable,self.STATUS.statea port's state,self.mem[...]memory,await self.f(a)a function of the component. Only the owner writes its variables (the same rule as Python). - A received message is a value, not bare names. Python puts a message's
fields in scope as bare names and passes its name as
message; TypeScript passes one argument,msg, an object{ name: "SPI_FRAME", cs, cmd, addr, data }— the fields by name, andname. In inline code,msgis in scope, andmessageis its name (a string), for code moved from Python. - Inline code is the body of an
asyncfunction withself(andmsg, when a message triggered it) in scope:self.count += 1; await goto("Run"). - Message constructors take one object:
SPI_FRAME({ cs: 1, cmd: 0 })(Python's keyword arguments). Enum literals and constants are global names, as in Python.
In a model, a code "ctrl.ts" file binds as a .py file does: fn main takes
main, a state On takes on_enter_On and on_exit_On, a port rx takes
on_receive_rx, a memory M takes on_write_M; on-enter name takes the
function named. A function's parameters are self and the fn's arguments, or
for a message's hook self, msg (py-params otherwise). The constants, types and
helper functions at the top of the file are copied into each body that uses
them; imports are the editor's and are dropped (only "arche" and "./arche").
3. The API#
Global names, the same as arche_runtime.py's.
Waits are awaited, as in Python: wait(d), wait_until(test, timeout?),
clock(n?), recv(port, timeout?), select(...ports) /
select([ports], timeout), call(port, msg, to?, timeout?), goto(state),
put/get/take(port, …), lock(port, timeout?),
wait_bits(port, bits, { all, clear, timeout }), stream_write/stream_read,
compute(d).
Immediate: send(port, msg, to?), reply(msg), give, unlock, set_bits,
clear_bits, queued, start_timer, stop_timer, pulse(signal, width?),
at(address), message(name), component(name), log(...a), warn(...a),
print(...a) (to the trace), fail(msg?), now(), rand(),
randint(lo, hi), randn(mu?, sigma?) (seeded by the engine — never
Math.random, never Date), ns/us/ms/sec(x), to_ms/to_us/to_s(t),
fmt(t), bits(v, hi, lo?), bit(v, n), setbits(v, hi, lo, x),
u8/u16/u32/u64/i8/i16/i32/i64(n).
Python's with locked(p): is await lock(p); try { … } finally { unlock(p) }.
Math.* is allowed, except Math.random.
The same determinism rule as Python: stepping back runs the code again and must give the same result. The same runaway-loop guard: loop turns are counted, and past the same limit it is an error.
4. Where it runs, and types#
- Arche: a
JsRuntime(Lang = 'python' | 'typescript') in a Web Worker (no DOM, no network); the engine owns time and state through the same host-call protocol the Python bridge uses. Pyodide is loaded only when the project has Python code. archec sim(src/websim.ts, src/websimrt.ts) runs the JavaScript directly with the same API — in node, in the pagearchec sim --htmlwrites, in the live page and VS Code's ▶ panel.archec stubswritesarche.d.tsbeside the model (as it writesarche.py): the API's signatures, an interface per message (withname), a type per component (its variables, ports, functions, and its states as a string union) with aMsgunion of what it receives, the constants and the enums.
5. Test plans and guards#
Arche runs a test plan's code — a step's do and expect, a setup, a stimulus —
in the project's language. In arche-lang it is written as Python, as before; in a
TypeScript project archec writes it to Arche as TypeScript (src/testcode.ts):
| Written | Arche runs |
|---|---|
abs(c.x) < 0.1 and not c.done |
Math.abs(c.x) < 0.1 && !(c.done) |
send(rx.frames, RC_FRAME(throttle=1200, arm=True)) |
send(rx.frames, RC_FRAME({ throttle: 1200, arm: true })) |
x is None, min, max, round, int, len(xs) |
x === null, Math.min, Math.max, Math.round, Math.trunc, (xs).length |
pass, # note |
nothing, // note |
if c: / elif / else: / while c: / for i in range(n): / for x in xs: |
the same in braces |
The prefix of an expectation (within 5ms:, never:, always:) stays as it is. What
reads as TypeScript already (a && b, GO({ n: 1 })) is left as it is, and a
```ts block is written as it is. What has no simple TypeScript — a if c else b, in, //, a def, with, try, an f-string, a lambda — is reported
(test-typescript): write it so, or in a ```ts block.
A transition's guard and action are written the same way in a TypeScript project:
[abs(angle) > FALLEN] reaches Arche as Math.abs(angle) > FALLEN (the trigger's
fields are in scope by name, in either language).
In archec#
| What | How |
|---|---|
| Write the language | project { handlers typescript }, component c { handlers python } |
| Move blocks to a file | archec fmt --extract-code=behaviour.ts --write main.arche (or --extract-code alone: one file per component, in its language) |
| The editor's types | archec stubs main.arche → arche.d.ts (and arche.py when there is Python) |
| Run it | archec sim main.arche (--html, --plant, --batch, --test), archec difftest |
| Check it | archec check: each TypeScript syntax error on its line (ts) |
| Change the language | archec convert-handlers main.arche --to typescript|python|arche [--component C] [--dry-run] |
What archec sim runs of the API: the waits wait, compute, goto and the
part's own functions; send, reply (on the port the message came in by), log,
print, warn, fail, now, the seeded rand, randint, randn, the
durations, fmt, the bit helpers and the integer casts, message(name).
The rest — recv, select, call, put/get/take, lock, the bits,
streams and timers, wait_until, clock, pulse, at, component — stops the
body with an error that says Arche's simulator runs it. Math.random and Date
are errors too: a run must replay. A port handle has no state in archec sim.
What does not read: an enum or a namespace (no JavaScript of their own: use a
union of strings, or the model's enum), a constructor's public x parameter, an
async arrow function (archec sim; write an async function). A message with no
fields (message GO: bool) is { name, value }.
Portable TypeScript#
The simulators run any TypeScript. The generators (archec gen --target c|embassy|go,
and archec difftest) translate what the action language says, as they translate
```arche: a TypeScript body that keeps to that is read into the action
language's tree, and the C, Rust or Go is written from it. The subset:
| TypeScript | The action language |
|---|---|
let x = e, const x = e (a value given) |
var x = e |
self.x = e, +=, -=, *=, /=, self.n++, --k |
x = e, n += 1, k -= 1 |
if (c) … else if (c) … else …, while (c) { } |
if, while |
for (let i = a; i < b; i++) (or i <= b, i += 1) |
for i in a..b |
send(self.p, MSG({ k: v })), MSG(), { k } |
send p MSG { k: v } |
await goto("Run"), await wait(ms(2)), wait(ms(self.t)) |
goto Run, wait 2ms, wait t * 1ms |
log(…), print(…) |
log … |
await self.f(a) |
f(a), the part's function |
msg.distance, msg.name |
distance, message (the received message's fields) |
self.state === "Idle" |
state == Idle |
=== !== < <= > >= + - * / % && || !, x ** 2 |
== != … and or not, x * x |
Math.abs min max sqrt sin cos atan2, Math.trunc(x), Number(x), Math.PI |
abs …, int(x), float(x), a literal |
now(), to_s(t), to_ms(t), ns/us/ms/sec(x) |
the same |
return, break, continue, numbers, strings, true, false, constants, Color.Red |
the same |
Then the action language's check: a name it does not know, a type that does not fit, or a division of two integers (TypeScript divides as floats) keeps the body out too.
Outside — an array or an index, a ternary, a template string, a method of a value
(list.push), a helper function, reply, recv, a bit operator, switch, try —
the body runs in the simulators as it is, and the generators write a TODO with the
TypeScript in its place, as for Python they cannot translate. archec check says
which construct (ts-portable): info in a project that only simulates, a warning when
a deployment generates code for the component.
A port's on-receive in TypeScript that reads its message (msg.distance) gets it in
C: <part>_on_receive_<port>(self, const void *arche_msg), its fields by name, as a
transition's guards and actions get theirs. A guard written in TypeScript
([msg.force > 0]) is translated the same way. So archec difftest compares the
TypeScript the simulator runs with the C it becomes (examples/ts-handlers agrees).
Converting#
archec convert-handlers main.arche --to typescript|python|arche rewrites the model's
handlers in another language, in place: each block (its fence and indentation kept), each `code` file (`code "ctrl.py"` becomes `"ctrl.ts"`; the old file is kept, remove it when done; to `arche`, each function becomes a ````arche ```` block on its
element), the transitions' guards, and handlers on the project (on the component,
with --component Ctrl).
- From
```arche: exact, by the translators Arche's copy is made with. - From Python: statement by statement, through what the action language says (the same reading the generators use for Python).
- From TypeScript: statement by statement, through the portable subset above.
- A received message: Python's bare field names (
distance) are TypeScript'smsg.distance, Python'smessageismsg.name. The action language has no message in a body: there, a statement that reads one stays a TODO. - A statement that does not translate stays as a comment with the original,
// TODO(convert): <python>(# TODO(convert): <ts>in Python): the command lists each with why, andarchec checkreports it (convert-todo) until it is written. Comments between statements are kept; a comment at the end of a line, and one inside a nested block, are not. - Nothing is written when the result has more errors than the model (
--dry-run: only say what would change).
Converting is meaning for meaning, not text for text: clamp(x, a, b) comes back from
TypeScript as min(max(x, a), b), a - b + (c - d) as a - b + c - d.
Diagnostics#
| Code | |
|---|---|
handlers |
handlers names no handler language |
handlers-language |
code in another language than the one Arche runs the project in |
code-language |
a ``` block of a language that is not a handler language |
ts |
TypeScript that does not read, has no JavaScript, imports a module, or keeps state at the top of a file |
ts-portable |
a TypeScript body outside the portable subset: the construct; the generators write a TODO |
convert-todo |
a statement archec convert-handlers left as a TODO(convert): comment |
py-params, py-not-found, py-unused, code-not-found |
as for .py files |
test-typescript |
test code in a TypeScript project that archec cannot write as TypeScript (§5) |