Oberon RTK

ECS for Control — Round 2 Lessons Learned

What building EcsControlStore taught: how the round's results were reached, and what generalises about reaching them.

Overview

The round's descriptive documents state its results as settled claims. This document records the working-level findings behind them – the method that produced the dissolutions, the trades the domain offered, the cost accounting, and the handful of implementation findings that shaped the code in ways no requirement predicted. It continues the round-1 lessons at the same two levels: concept first, implementation after.

Concept-Level Lessons

Structure Replaces Checking – and Finding the Structure Is a Procedure

The round's central experience, repeated often enough to state as method: when a proposed mechanism carries machinery – a registry, a mask, a run-time predicate, a checker – challenge it until the invariant surfaces that makes the machinery redundant. Every time the challenge was pressed, an invariant was there, and the mechanism collapsed into a name, a layout, or a declaration.

The pattern behind the collapses is uniform: each proposed mechanism was a reasonable, general-purpose answer, and it dissolved because the closed world of the control problem had not yet been thought through to the bottom. Pressing on the closed-world facts – the population is known, the members have names, the structure never changes – and following their consequences until they were fully exploited cut degree of freedom after degree of freedom, until what remained was the essence, ready to be implemented directly, with no residual question left for machinery to answer at run-time.

The capability query is the public specimen (the retrospective tracks it), and the same happened, at smaller scale, several times inside the round. The procedure generalises: a mechanism that seems necessary marks a spot where the closed world and the invariants had not yet been exploited to the bottom – press their facts and their consequences until the question the mechanism would have answered no longer exists.

The result of applying it consistently is visible in what the program does not contain: no query, no capability masks, no registry of kinds, no run-time membership predicate, no global enumeration of Systems. Where such machinery would stand, there are names (I), layouts (the split arrays), and declarations (the manifests).

The Domain Supplies the Invariants

The dissolutions were not clever tricks; each was paid for by an invariant the control domain supplies for free. Three trades carried the round:

  • boundedness – every population is known, so the length of an array is its population: LEN replaces size constants, fill counters, and sentinels;

  • run-to-completion on one schedule – no two Systems ever run at once, so the schedule is the mutual exclusion: derived once, it replaces every lock;

  • frozen structure – nothing changes shape after creation, so declarations checked once are trustworthy forever: the construction checks replace run-time defence.

The general reading: before building a guarantee, look for the domain property that already implies it. A control system's domain is unusually rich in such properties – that is arguably why the architecture fits it.

Cross-Cutting Concerns Cost Nothing at Run-Time – the Ledger

The round's headline claim deserves its accounting, because the claim is easy to disbelieve. To gain declared access roles, a derived schedule, checked construction, shared facilities with any number of participants, and tick monitoring, the round added: the arrays split at writer boundaries (a storage decision, not a mechanism), four identity constants, two token classes in the manifest record, eight construction checks of essentially one set expression each, and two development-only reporting modules. Steady state gained no mechanism at all: the tick loop runs Systems in a precomputed order, exactly as before.

What the same period removed or never built is the machinery listed in the previous lesson. The net direction is the finding: capability grew while run-time mechanism shrank. When an architecture claims a feature will cost mechanism, the claim deserves this kind of ledger before it is believed.

Change Has an Address

Round 1 established the orthogonality of change by perturbation: renaming a System touched one module; adding one touched the two designated growth points and nothing else. Round 2 preserves that result and clarifies it: the store's module structure – CS, CL, W, I, V – gives every kind of change a named address.

Adding a System now decomposes mechanically by role: its own module (the law and its manifest); CS, if it brings new Components (types, fields, a token); W, if it claims a new resource or extends a population; I, if it participates in a shared facility; CL, one wiring line per pairing; the main program, one registration line and – if a device is new – its construction; TokenNames, one entry. Removal is the same list backwards.

Close to mechanical is not the same as easy: which Components, which token granularity, which facility – that is still design, and the design is still engineering. What the module roles fix is what and where: no change hunts for its place, and no place accepts a change of the wrong kind.

The construction checks add what round 1 could not: a missed edit – a product nobody consumes, a deposit nobody collects, a consumer without its producer – is caught at the next start-up, before the first tick. The growth-point checklist is verified, not remembered.

Implementation-Level Lessons

Spend Enforcement Where It Buys Enforcement

Two scarcity facts shaped the access API more than any principle: an exported variable is read-only to every importer – the language has no finer grant – and procedure parameters are a bounded resource (an open array costs two words, a record likewise).

The settlement: the compiler's enforcement went where it is total and free (the locked half, read-only by the export rule; read parameters, read-only by mode), the signature's parameter slots went where the modes carry meaning (the store's variable half), and locked configuration is read ambiently – a parameter for it would duplicate a guarantee the compiler already gives, at real cost.

The lesson generalises past Oberon: enforcement mechanisms are a budget, not a blanket. Placing them by what they add – rather than uniformly, by symmetry – is itself a design activity, and the result reads better, not worse: the seam where enforcement is thinnest (the Run argument list) is exactly where review knows to look.

Token Granularity and Storage Layout Are Independent

Entering the round, one decision looked like one axis: how the Components are stored. It proved to be two.

  • Tokens follow writers – the unit of declaration and checking is the writer's footprint.

  • Arrays follow layout – how many arrays back a token is a storage decision with its own criteria.

One writer covering two arrays keeps one token; one record type written by two Systems splits into two tokens over the same declaration. The single-writer invariant then holds by construction exactly where the two axes were separated correctly.

The independence was discovered, not designed – the split-array form appeared as the answer to a checking problem and only afterwards revealed that it had also settled the storage question. Worth knowing in advance next time: when granularity questions tangle, look for two axes.

Review Surfaces Are Checker Rules in Waiting

Review – the third of the three defence levels the implementation document sets out – turned out to ask only for mechanical correspondences: the import matrix (a System module never imports W or V); runSystem's parameter modes against the manifest; Run's argument list against the store's field attributions; an I reference wherever a slot is written, and nowhere else; loop bounds taken from the array actually indexed. None of these needs judgement – they need looking.

The round accumulated them as a rule ledger for a future checker tool: what review does by eye today, a tool can do exhaustively, over the same surfaces, with no change to the program. That the surfaces were shaped for review first is what makes them tool-checkable at all – the dependency runs in that direction, not the other way.

Consistent idioms serve the same end. Expressing the same construct in the same way everywhere – within a program, and across programs – gives both the eye and a tool a stable shape to recognise: wherever a Run touches the store, it is the same two lines, VAR S: CS.Store and S := CS.S; the store modules carry the same names, CS, CL, W, I, V, in every program built this way. Neither is prescribed by the architecture or the method – the local copy could be called anything, and the modules too – and that is the point: the particular choice does not matter, the consistent use does. Against a uniform background, a deviation carries information.

From Lessons to the Next Round

The accounting of this round against the previous one's plan is the retrospective's business; the residuals it carries forward, and the shape of the next round, belong to the outlook document. The lessons above are what travels regardless of either: a procedure for dissolving machinery, the habit of asking the domain first, a ledger discipline for cost claims, and change with a named address.

See Also

Last updated: 30 July 2026