Langage › Handlers

Langage

Cette page n’est pas encore traduite : voici la version anglaise.

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 } — or python, or arche.
  • A component may say its own: component Legacy { handlers python }. Without one, a component takes its definition's, the one around it, its code files' (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 handlers keeps what its code is: Python somewhere (a .py file, a ```python or bare block) makes it python; otherwise it is typescript.
  • archec new writes handlers typescript (archec new --handlers python|arche for the others).

In the Arche project:

  • simulation.yaml says the project's language: behavioral: { language: typescript | python }. Absent is python, so every existing project keeps working. archec writes it when the project says handlers, 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.x reads and writes a variable, self.STATUS.state a 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, and name. In inline code, msg is in scope, and message is its name (a string), for code moved from Python.
  • Inline code is the body of an async function with self (and msg, 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 page archec sim --html writes, in the live page and VS Code's ▶ panel.
  • archec stubs writes arche.d.ts beside the model (as it writes arche.py): the API's signatures, an interface per message (with name), a type per component (its variables, ports, functions, and its states as a string union) with a Msg union 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's msg.distance, Python's message is msg.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, and archec check reports 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)