Design notes
Why the language is as it is, its pointers, and how fast archec is.
One spelling for each thing#
There is one spelling for each thing. The older ones (port x "N": t out, out x "N": t,
body ```…``` , [A, B], initial X, : type { carries [M] }, main "…", do "…",
two properties on one line without ;) still parse, but the linter marks each one as a
long-form / same-line hint with a quick fix, and archec fmt --simplify --write *.arche
rewrites a file in the short forms — the generated Arche project stays byte-for-byte the same.
Pointers#
*T (or C-style T*), **T, *void and arrays of pointers *T[4] work wherever a type
does: variables, struct and message fields, function arguments and results, constants,
memory blocks. A pointer is pointer-size bytes (project { pointer-size 8 }, default 4)
and holds an address — an integer, a constant or nil; anything else is an error
(pointer-value). Arche has no pointer type, so each one becomes a catalog entry in
data-types.yaml — ptr_Node, named *Node, an unsigned integer as wide as an address,
unit address — and a pointer field to a structure is an association in the class
diagram. A pointer in a message payload is flagged (pointer-on-wire).
Choices in the language#
- Contextual keywords. The lexer only produces identifiers, so
state,typeorportremain usable as names; meaning comes from position. - Line-sensitive properties. A property's values start on its own line (
priority high); after a comma they may continue. This lets properties sit one per line with no separators (on one line,;separates them), and lets the formatter only re-indent — it never moves a token. The one exception it makes: the body of ablock opened at the end of a header (`fn fpython`) is indented one level under that header (a block's common indentation is removed when it is read, so the code is the same). - Python where Python goes.
do,expectandsetupin test plans,doin stimuli and a transition's/ actiontake Python as it is, to the end of the line (brackets balanced; a;or the block's}ends it). No quotes, no\"escapes, and the linter can read the names. - Identifiers may contain
-(REQ-BRK-001,can-fd), as Arche ids do. Write subtraction with spaces:a - b. WhenMAX-1names nothing butMAXis a constant, the linter reportshyphen-minuswith the fixMAX - 1. - Structure apart from layout.
canvas System { … }holds the canvas positions, so the system reads as what is connected to what, and moving a block on the canvas does not touch it. - One table describes the language (
src/spec.ts): the parser's contexts, the linter's property checks, completion, hover anddocs/patterns.mdall come from it. - One way to write each thing (
src/canonical.ts). Every declaration has one standard keyword, and multi-word keywords take a hyphen (port-type,test-plan,use-case,failure-mode). A word means one thing:busis not a port type (a bus is a component withkind bus);processis an OS process in a deployment, a workflow is aworkflow;modeis anoperating-mode, not a state. A pseudo-state ispseudo choice(kindis the catalog); a requirement's provenance isorigin(sourceis where code lives). Fields areattr key = value, asprop key = value; a private function isprivate fn;extendsis in the header; lists have no brackets; values are lower case (complexity medium); a unit is written as it is (unit m/s²,unit °C; quoted only with a space).processoranddevicearecomponent:kind processor(orcores) is what makes a block hardware. Older spellings are still read and build the same project: each is analias-keywordhint, andarchec fmt --simplifyrewrites it.
Performance#
node tools/bench/bench.js times archec on kacao (69 files, 1.7 MB) and on a model ten
times its size made from it by tools/bench/synth.js (kacao and nine copies with every
declaration renamed, 681 files, 17 MB): load, check and emit in the process, archec check and archec build as commands, and the language server over stdio. In ms, on an
Apple M4 Max shared with other work (the numbers move by 10–20 % from one run to the next),
before this work (76ea8f2) and after:
| kacao before | kacao after | 10× before | 10× after | |
|---|---|---|---|---|
| load + check, warm | 230 | 122 | 3024 | 1248 |
| one-file edit, check again (end of the file / its middle) | 230 | 55 / 70 | 3133 | 541 / 664 |
| whole compile (emit, schema, rules), warm | 401 | 277 | 7762 | 5651 |
archec check |
750 | 621 | 8407 | 6204 |
archec build |
1316 | 759 | 12495 | 7136 |
| editor: open → first diagnostics | 725 | 302 | 8124 | 1733 |
| editor: keystroke → diagnostics | 449 | 174 | 3197 | 831 |
What made the difference:
What grew faster than the model. The ports providing an interface were looked for with a walk of the whole model per interface; the long forms parsed every file a second time;
defaultswere looked through for every node; the lexer tested each character with a regular expression. Check is 2.5× faster on the 10× model, load 2×.An incremental compile (
src/incremental.ts,Session). The check runs in pieces — each top-level declaration, each file's long forms — and what each piece reads is written down: the names it looked up and what they gave, the declarations it reached, the files it looked into, the namespaces it listed. After an edit, a piece whose own declaration and everything it read are unchanged is not run: its diagnostics and references are taken again, in their place. An edit that changes what loading does across files (the includes, a package, the project, translations, means, generics, a .py file) checks everything again. The tokens of unchanged files are kept too.test/incremental.test.jscompares it with a compile from scratch after random single-file edits of every example — the same diagnostics in the same order and the same project, byte for byte (ARCHE_INC_SEEDS=150runs 22 000 edits).The language server compiles each project with a Session, reads a file from disk again only when its size or time changes, answers what is here / references / rename / symbols / semantic colours from an index of each program, and runs the full compile (emit, schema, rules) in a worker thread: a keystroke's check never waits for it. The pause before a keystroke's check follows how long the last one took (50–200 ms).
Arche's rule engine (bundled in
vendor/arche.js, run byarchec checkand the full compile) copied every project constant into each subject's scope for each rule, and counted each element's traces by going through all of them. It now looks the constants up and counts the traces once (arche/frontend/src/rules.ts):archec checkon the 10× model goes from 6.7 s to 3.4 s, with the same output byte for byte.
What is left: loading (≈ 250 ms on the 10× model) and the whole-project checks (≈ 150 ms) still run whole on every edit. The CLI has no disk cache: its time is mostly the emit's, which an edit changes anyway.