Documents and Arche › Design notes

Documents and Arche

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, type or port remain 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 a block 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, expect and setup in test plans, do in stimuli and a transition's / action take 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. When MAX-1 names nothing but MAX is a constant, the linter reports hyphen-minus with the fix MAX - 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 and docs/patterns.md all 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: bus is not a port type (a bus is a component with kind bus); process is an OS process in a deployment, a workflow is a workflow; mode is an operating-mode, not a state. A pseudo-state is pseudo choice (kind is the catalog); a requirement's provenance is origin (source is where code lives). Fields are attr key = value, as prop key = value; a private function is private fn; extends is 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). processor and device are component: kind processor (or cores) is what makes a block hardware. Older spellings are still read and build the same project: each is an alias-keyword hint, and archec fmt --simplify rewrites 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; defaults were 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.js compares 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=150 runs 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 by archec check and 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 check on 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.