ECS for Control: The ECS File
Reference for the ECS file, the program's definition extracted from its sources: the kind declaration every module carries, the ROLENAMES and MODULES sections and their kinds, the DECLARATIONS, the HEADER of a file written for generation, and the ECS map file beside it.
Overview
An ECS file is a text file beside a program's sources that states the program's definition in the terms of the architecture: which modules exist, of which kind, under which names they play their kind's roles, and where they belong; which tokens, surfaces, manifests, library imports, adapter procedures, periods, resources, schedules and crossings the sources establish. The file is extracted from the sources: every module declares its kind in its own text, and the extractor reads the declarations and the Oberon text and writes the file. Nothing in it is inferred.
One term recurs throughout and is worth having in hand from the start: a schedule is one run of the kernel's loop over its own set of partitions, and a program has one or more of them. Each schedule runs on its own core: a program with several schedules is a multi-core program, one binary with one schedule per core. Where two schedules exchange data, the file declares the crossing, and the program with one schedule is simply the case where nothing crosses.
Three things to know before the terms:
-
The file is not required to implement the architecture. A program written to the architecture's discipline is complete without it; what the discipline requires is the kinds' constructs, and what the tools require is the kind declaration in each module. The file is what makes the program's structure statable in one place and comparable between versions: the extractor writes it from the sources, the checker verifies the program against what its modules declare, and the generator writes a skeleton from a file written for that purpose.
-
The kinds are the architecture's module taxonomy. Module names are the programmer's free choice; the kind names are fixed, and each names exactly one kind of module with one placement. The documents of this set use the kind names as their vocabulary for that reason: "a
SYSTEMmodule of aPARTITION" says what no module name can. -
The file has two roles under one grammar. The extracted file is the record of a program: its head and allow-lists lines,
ROLENAMES,MODULESandDECLARATIONS, written by extract-ecs, regenerated whenever the sources change, never edited. A file written for generation is authored:ROLENAMES,MODULESand theHEADERthe generated modules carry, and noDECLARATIONS, since there are no sources yet. A file whose content disagrees with the sources is a finding, not a decision. -
The program is a built program. The extractor reads a program that compiles and links, verifies that by the build's own products, the code, symbol and binary files and their dates, and never invokes the compiler. The linker's map file names every linked module with its location, and that list is the program: a module in the tree that did not link is not in it, and a library transport an adapter names is found there by name, not along a search path. Every finding the extractor reports is therefore a finding about the architecture, never about the Oberon.
The grammar has one writer, extract-ecs (its manual page), and one reader, the generator that writes a program skeleton from an authored file; check-ecs reads the sources only and never the file. The grammar is strict: an unknown section, kind or fact, or a reference that resolves to nothing, is an error, never skipped. Beside the ECS file the extractor writes the ECS map file, the program's definition in the forms a reviewer reads (The ECS Map File, below).
The Kind Declaration
Every module of a program declares its kind for the tools in a comment of its own before the MODULE line, among any other comments there; a program reviewed by hand alone needs no declaration:
(* ECSKIND: SYSTEM Manifest Name Run *)
MODULE BlinkSystem;
The comment holds the tag ECSKIND:, the kind, and the kind's parameters: the module's names for the kind's roles, in the order the parameters table gives (The Kinds, below), variables before procedures. A kind's roles are fixed; the names are the module's own, so S and Build play the STATE kind's two roles in one program and Store and Create may play them in another. A parameter names a role's item if the module has one: a LOCKED module without an interface record still declares LOCKED L I, and the declaration says what the record would be called, not that it exists. An ADAPTER has no roles and one parameter, the transport it serves. A kind without roles is declared bare, (* ECSKIND: PERIODS *).
The comment is the declaration, and its formatting inside is free, as whitespace is free in Oberon: the comment's text, with whitespace and asterisks stripped from both ends, is the tag, an optional space, the colon, the kind and the parameters, separated by whitespace of any kind, newlines included, and nothing else. (**** ECSKIND:STATE S Build ****) and a declaration spread over three lines are the same declaration. A comment whose stripped text does not start with the tag is not a declaration, whatever it mentions; one that starts with the tag and does not match the form is a finding, never skipped.
Exactly one kind declaration per module, read before anything else: a module without one, with two, with an unknown kind, or with the wrong number of parameters is a finding that stops the extraction and the check. The declaration is the module's statement of what it is, and every check reads the module in that light: as each variable has a type, each module has a kind.
The Sections
An ECS file has a head line, an allow-lists line, and up to four sections, in this order:
| Section | In the extracted file | In a file for generation | Content |
|---|---|---|---|
| head line | written | optional | the label and the extraction's date and time |
| allow-lists line | written | absent | the allow-lists file the extraction used, by that file's head line |
ROLENAMES |
written | authored | the program's names for the kinds' roles, once per kind |
MODULES |
written | authored | the module inventory: groups, entries, kinds and parameters |
DECLARATIONS |
written | absent | the facts the sources establish, per module, inside the frame of their group |
HEADER |
absent | authored | the header block the generated modules carry, verbatim until the end of the file |
The head line is the label, --, the word extracted, the date and time to the minute, and the extractor's name and version: EcsControlSchedule-v29 -- extracted 2026-10-12 14:05 extract-ecs 1.0. It is the first line of the file when present, and it takes no part in the comparison of two files.
The allow-lists line is the keyword ALLOWLISTS and the head line of the allow-lists file the extraction read, verbatim. The allow-lists file is the tools' data for the kinds' import lists, versioned with the library; a project may run the tools on a copy of it, and the line records under which file the facts were extracted. It follows the head line, and it takes part in the comparison.
A section starts with its keyword on a line of its own. Comments are Oberon comments, (* ... *), and may appear anywhere outside the HEADER section. No separators are written: one item per line is the house style, and the next token decides every boundary.
ROLENAMES
The ROLENAMES section states the program's names for the roles, one line per kind, in the kind table's order:
ROLENAMES
PARTITION Create AddSystems
SYSTEM Manifest Name Run
STATE S Build
LOCKED L I
TOKENS L I Names
CONS Assert
BOUNDARY Crossings
A kind's line is the default for its entries: an entry carries no parameters of its own unless its module deviates, and then it carries all of them, OddSystem: SYSTEM Manifest Name Step. A kind absent from ROLENAMES has its parameters on every entry of that kind. The extractor writes as a kind's line the names most modules of the kind use, and writes no line, every entry explicit, when no names have a majority; a file for generation may use the section as it likes. Every spelling of the same names means the same, and the comparison of two files reads the meaning, not the text.
MODULES
The MODULES section is the program's inventory. It consists of groups, and every group lists entries.
Groups
A group is written as a name, =, a group kind, its entries, and END. There are four group kinds:
| Group kind | Names | Lists |
|---|---|---|
PROGRAM |
the program, by its label | the program-level modules: one SCHEDULE entry per schedule, the program-level definition modules, the boundary, and the OTHER and transport modules of the program level |
SCHEDULE |
a schedule's module | the partitions the schedule creates, one PARTITION entry each, in the order of their creation |
PARTITION |
a partition | the partition's modules |
LIBRARY |
a library, by a free name | the library transports the program's adapters serve |
A file has exactly one PROGRAM group, one SCHEDULE group per schedule, one PARTITION group per partition, and any number of LIBRARY groups. A partition belongs to exactly one schedule. The PROGRAM group is a group, never a module: no module carries the program's label.
The PROGRAM group of a one-schedule program, from EcsControlSchedule-v26:
EcsControlSchedule-v26 = PROGRAM
EcsControlScheduleV26: SCHEDULE
P: PERIODS
R: RESOURCES
T: TOKENBASES
N: TOKENNAMES
C: CONS
MemMap: OTHER
END
The entry's parameter, the gate, comes from the ROLENAMES line, SCHEDULE run here: schedule 0's gate is a local procedure, called from the module body and from nowhere else, while every other schedule's gate is exported and started by a deployment call. A two-schedule program has two SCHEDULE entries in its PROGRAM group and a BOUNDARY entry beside them; each schedule group then lists its own partitions:
EcsControlScheduleS0v29 = SCHEDULE
BlinkPartition: PARTITION
CoordPartition: PARTITION
MonPartition: PARTITION
PrintPartition: PARTITION
END
EcsControlScheduleS1v29 = SCHEDULE
AdaptPartition: PARTITION
END
The one-schedule program is the case of one SCHEDULE group: there is one notation, not two.
Entries
An entry is written as a module name, :, a kind, and the kind's parameters when the entry carries its own (ROLENAMES, above): BlinkSystem: SYSTEM under a ROLENAMES line for SYSTEM, OddSystem: SYSTEM Manifest Name Step when the module's names deviate. The kind and the parameters are those of the module's kind declaration, verbatim. Several modules of one kind may share a line, comma-separated, when none carries parameters. The = of a group defines an entity; the : of an entry places a module under its kind.
An ADAPTER entry always carries its one parameter, the transport it serves: X: ADAPTER Y, where Y is a transport declared in a partition or in a LIBRARY group.
A PARTITION group, from the same program:
CoordPartition = PARTITION
PeriodSystem: SYSTEM
CScoord: STATE
CLcoord: LOCKED
Tcoord: TOKENS
Wcoord: WIRING
Vcoord: VALUES
PeriodPort: SPORT
PeriodPortAdapt: ADAPTER PeriodPort
END
A LIBRARY group declares the transports a program takes from a library rather than from its own tree; its members are transport kinds only. The extractor finds them through the adapters' parameters, locates them by name in the linker's map file, and reads their kind declarations where they lie:
Lib = LIBRARY
ObservationPorts: SPORT
END
(ecs-file-kinds)=
The Kinds
Every kind names one kind of module, has one placement, and has its roles: the exported items a module of the kind provides, each a named slot, <storevar>, that the module's kind declaration fills with the module's own identifier. The placement is where the kind's entries may appear: in the PROGRAM group, in a SCHEDULE group, in a PARTITION group, or in a LIBRARY group. The roles columns below name the slots; the parameters table at the end of this section gives their order per kind and the identifiers the examples use.
Program-level kinds, members of the PROGRAM group:
| Kind | The module | Roles |
|---|---|---|
SCHEDULE |
a schedule's module: creates its partitions, sets the boundary, registers their Systems, plans, commits, and runs the schedule; schedule 0's module is the program's main module, its gate local and called from the module body; every other gate exported and started by a deployment call | <gateproc>, the gate |
PERIODS |
the periods: one tick period per schedule, and the Systems' periods in ticks of their schedule | none |
RESOURCES |
the resource choices: library handles, chip units and pins, exception priorities, each an authored choice in the owning module's vocabulary | none |
TOKENBASES |
the program-wide token allocation: each partition's token count, its base, and the total against the kernel's capacity | none |
TOKENNAMES |
the program-wide token name table, gathered from the partitions' TOKENS modules, for the plan report |
<namesvar> |
CONS |
the consistency module: the assertions that hold across partitions' definitions, the first station of schedule 0's sequence; one per program | <assertproc> |
BOUNDARY |
the schedule boundary: the crossings between schedules, declared once for the whole program | <crossingsvar> |
OTHER |
a module the definition places but says nothing about, tracked by existence only | none |
The partition kind, member of a SCHEDULE group:
| Kind | The module | Roles |
|---|---|---|
PARTITION |
the partition module: creates the partition's world and registers its Systems | <createproc> <addsystemsproc> |
Partition kinds, members of a PARTITION group:
| Kind | The module | Roles |
|---|---|---|
SYSTEM |
one System: its manifest, its name, its run procedure | <manifestvar> <namevar> <runproc> |
STATE |
the partition's variable Component state, the store record, and its build procedure | <storevar> <buildproc> |
LOCKED |
the partition's locked Component state: written once at load, read-only thereafter, with its interface record for other partitions | <lockedvar> <interfacevar> |
TOKENS |
the partition's tokens, one per store variable, its surfaces, and its token names | <tokensvar> <surfacesvar> <namesvar> |
WIRING |
the partition's structure: cardinalities and role-binding indices | none |
VALUES |
the partition's tuning values | none |
ADAPTER |
the store access procedures over one transport, store-handle-indexed where the store path is an array, and the procedure types that carry their direction | none; its parameter is <transport>, the module it serves |
BINDING |
a device seam of the partition: operates interface hardware for the partition's Systems, configured at the partition's creation, called by its Systems | none |
OTHER |
as above | none |
Transport kinds, members of a PARTITION group when the transport lives in the program's tree and one partition's store holds its instances, of the PROGRAM group when the stores of several partitions do, or of a LIBRARY group when it comes from a library:
| Kind | The transport |
|---|---|
SPORT |
a sampling port: the latest value, one writer, one reader, wait-free; a closed API of put, get and init |
QPORT |
a queueing port: messages in sending order, one producer, one consumer; send and receive under preconditions |
RBUFFER |
a ring buffer: elements in order, one producer, one consumer; put and get, single and multiple, under preconditions |
BUFFER |
a buffer whose organisation the definition leaves unstated; its adapter is hand-written |
SPORT, QPORT and RBUFFER are canonical kinds: a module of that kind is identical in its interface to the framework's reference implementation of that kind. That is what lets the generator write a working adapter from the kind alone, and what a LIBRARY entry promises about a library module. A module whose interface differs is a BUFFER. Transport kinds have no roles: the canonical API is the kind.
The parameters, kind by kind. A declaration's parameters are the kind's roles in this order, one identifier per role; the right column shows the declarations the examples use:
| Kind | Parameters | In the examples |
|---|---|---|
SCHEDULE |
<gateproc> |
(* ECSKIND: SCHEDULE Run *) |
PERIODS |
none | (* ECSKIND: PERIODS *) |
RESOURCES |
none | (* ECSKIND: RESOURCES *) |
TOKENBASES |
none | (* ECSKIND: TOKENBASES *) |
TOKENNAMES |
<namesvar> |
(* ECSKIND: TOKENNAMES Names *) |
CONS |
<assertproc> |
(* ECSKIND: CONS Assert *) |
BOUNDARY |
<crossingsvar> |
(* ECSKIND: BOUNDARY Crossings *) |
OTHER |
none | (* ECSKIND: OTHER *) |
PARTITION |
<createproc> <addsystemsproc> |
(* ECSKIND: PARTITION Create AddSystems *) |
SYSTEM |
<manifestvar> <namevar> <runproc> |
(* ECSKIND: SYSTEM Manifest Name Run *) |
STATE |
<storevar> <buildproc> |
(* ECSKIND: STATE S Build *) |
LOCKED |
<lockedvar> <interfacevar> |
(* ECSKIND: LOCKED L I *) |
TOKENS |
<tokensvar> <surfacesvar> <namesvar> |
(* ECSKIND: TOKENS L I Names *) |
WIRING |
none | (* ECSKIND: WIRING *) |
VALUES |
none | (* ECSKIND: VALUES *) |
BINDING |
none | (* ECSKIND: BINDING *) |
ADAPTER |
<transport>, the module it serves |
(* ECSKIND: ADAPTER ObservationPorts *) |
SPORT, QPORT, RBUFFER, BUFFER |
none | (* ECSKIND: SPORT *) |
A role name is the slot's name, in angle brackets wherever a document or a tool speaks of it; the identifier in the declaration is chosen by the program. A finding names both: STATE storevar = Store: not exported.
Two kinds carry something beyond their roles: an ADAPTER names its transport as its parameter, and a SCHEDULE has its partitions in a group of its own. The transport kinds are the only kinds with more than one placement; every other kind has exactly one.
DECLARATIONS
The DECLARATIONS section holds the facts the sources establish about the declared modules. It is extracted from the sources by extract-ecs and never authored, and a file for generation has none: the inventory says which modules exist, the declarations say what they establish.
The section is closed under reference: every dotted path in it resolves to a line in the file. A fact that would name something the file does not declare is not written; the extractor reports it instead.
Frames
Facts are grouped in frames, one per PARTITION and one for the PROGRAM, written as the group's name, :, the group kind, the frame's entries, and END. Frames do not nest. Inside a frame, each entry names a module of the group with its kind, followed by its facts and END.
Every module may carry one fact, the library modules it imports; ten kinds have facts of their own beside it, and a module with no fact gets no entry:
| Kind | Facts |
|---|---|
| any kind | the library modules the module imports |
TOKENS |
the tokens with their partition-local ordinals; the surfaces |
SYSTEM |
the System's name; its period; its manifest, one line per non-empty set; the VALUES constants it names |
ADAPTER |
per exported procedure, its direction and the store variables it reaches |
SCHEDULE |
schedule 0 its number; every other schedule its deployment call |
BOUNDARY |
the crossings, per pair of schedules |
PERIODS, RESOURCES, TOKENBASES, WIRING, VALUES |
the module's exported constants, each with its expression as the source writes it |
Facts
A fact is written as a name, =, and a value. A value is a number, a string in double quotes, a set expression, a list of names, or Oberon text as the source writes it: a constant expression, or a deployment call.
Imports. Any entry declares the library modules its module imports, by name, in the order of the module's import list:
UARTbinding: BINDING
library = GPIO, UART, Clocks
END
The list holds what the kind's import list does not cover: a program module, and a framework module the allow-lists name by name, is checked against the list and not recorded; every other import is a library the module draws on, recorded for the reviewer and nothing more. A module that imports no such module carries no library fact.
Tokens. A TOKENS entry declares each token under the module's token record and each surface under its surface record:
Tcoord: TOKENS
L.mission.periodStep = 0
L.periodPort.ports = 1
I.readPeriods = {L.periodPort.ports}
END
A token's name is the store variable's path below the record, verbatim: mission.periodStep names the column periodStep of the table mission. The ordinal is partition-local; the program-wide value is the ordinal plus the partition's base from TOKENBASES. A surface is a named set of the partition's own tokens, published for other partitions: a manifest, an adapter or a crossing refers to a surface by name and never to a foreign partition's tokens one by one. The record names, L and I here, are the module's own; the file declares them and the checker verifies them.
Systems. A SYSTEM entry declares the System's name, its period as the registration call states it, and its manifest:
BlinkSystem: SYSTEM
name = "Blink"
period = P.BlinkPeriod
manifest.C = Tcoord.I.readPeriods
manifest.O = {Tblink.L.blinker.tickers}
manifest.P = {Tblink.L.ledLeader.setpoints}
END
The manifest has six sets: C and Cp for consumption, this pass's value and the previous pass's value; O and P for owned and produced writes; Px and Cx for deposits into a shared facility and their collection. A set absent from the entry is empty. A System that names constants of a VALUES module declares them, module-qualified, each once: values = Vmon.OpsStep_None, Vmon.TestSeq_None.
Adapters. An ADAPTER entry declares, per exported procedure, its direction and the store variables it reaches:
PeriodPortAdapt: ADAPTER
get.GetVal = {Tcoord.L.periodPort.ports}
put.PutVal = {Tcoord.L.periodPort.ports}
END
The directions are put, get and read: a write into the transport, a consuming read, and a read that consumes nothing, such as a predicate. They are read from the procedure's declared parameter modes: a value parameter carries data in, a VAR parameter carries it out, a function result reads without consuming. A procedure that reaches a second store variable beside the transport, an observation kept next to it, lists both.
Schedules. Schedule 0's entry declares its number; every other schedule's entry declares the deployment call that starts its gate, as the source writes it:
EcsControlScheduleS0v30: SCHEDULE
schedule = 0
END
EcsControlScheduleS1v30: SCHEDULE
deployment = Cores.StartCoreOne(Core1Prog.Run, NIL)
END
Schedule 0 is the one whose module is the main module, its gate local and called from the module body. The core every other schedule runs on lives in its deployment call alone, in the deployment procedure's name or arguments, and the tools do not know the deployment procedures: the file states the call, and the reviewer reads the core off it. The call is written as the source writes it, import aliases included, on one line. The schedules are n in all, and the crossing indices of the boundary range over 0…n-1 (The boundary, below).
The boundary. A BOUNDARY entry declares the crossings as two matrices over schedule pairs, flow for dataflow and deposit for deposits, each indexed from schedule to schedule:
B: BOUNDARY
Crossings.flow[0, 1] = Tprint.I.clipsObs
Crossings.flow[1, 0] = Tadapt.I.drainCmd
END
A crossing names surfaces only: what a partition publishes is what may cross a schedule boundary, exactly as it may cross a partition boundary. The variable's name, Crossings here, is the module's own, declared as the kind's parameter; the matrix indices are the schedule numbers as the source states them, constants evaluated; 0 is schedule 0, and which index is which other schedule is the programmer's statement, read against the deployment calls.
Constants. A definition module's entry lists its exported constants with their expressions as the source writes them:
P: PERIODS
TickPeriodS0 = 50
EachTickPeriod = 0
BlinkPeriod = EachTickPeriod
PeriodPeriod = 5000 DIV TickPeriodS0
END
The expression is not evaluated here: the file states what the module states. The ECS map file carries the evaluated values where a reviewer needs them.
References and Set Expressions
A reference is module-qualified: Tblink.L.blinker.tickers names a token by its TOKENS module, its record and its path, Tcoord.I.readPeriods names a surface by its module, its record and its name. References never go through import aliases; the file speaks in module names.
A set expression is written as in Oberon: braced lists of references, {a, b}, and bare surface references, joined by +. A surface is a set and therefore stands bare, never inside braces; a token stands inside braces, never bare. Tadapt.I.drainCmd + Tcoord.I.readPeriods is the union of two surfaces; {Tblink.L.ledLeader.setpoints, Tblink.L.ledFollower.setpoints} is a set of two tokens.
HEADER
The HEADER section belongs to a file written for generation: the comment block the generated modules carry, taken verbatim from the section's keyword to the end of the file. An extracted file has none; the modules' headers are the modules' own. Two conventions apply to its lines:
-
A line beginning with
!is mandatory: every module the generator writes carries it. The!itself is not part of the line. -
The line
<<DESCRIPTION>>is the description slot: where the generator writes a module's description, taken from the kind's reference implementation, when it creates the module. A header without the slot gets no description.
The header of EcsControlSchedule-v26:
HEADER
!(**
! Oberon RTK Framework v3.2
! --
! Example/test program for ECS architecture
! EcsControlSchedule-v26
! --
<<DESCRIPTION>>
! --
! MCU: RP2350
! Board: Pico2
! --
Copyright (c) 2026 Gray, gray@grayraven.org
! https://oberon-rtk.org/licences/
!**)
The ECS Map File
The extractor writes a second file beside the ECS file, with the same stem and the extension .ecsm: the ECS map file, the program's definition in the forms a reviewer reads, named after the linker's map file, which it resembles in purpose. It is text for people, read by no tool, and it carries its own head line: the label, the extraction's date and time, the ECS file it belongs to, and the extractor's name and version. Its sections, in this order:
| Section | Content |
|---|---|
SCHEDULES |
per schedule: its gate and the call that starts it, schedule 0 marked, its tick period, its partitions in creation order |
ADAPTERS |
per adapter and access procedure: the direction, the store tokens reached as program-wide values, the Systems calling it |
TOKENMAP |
per TOKENS module: its base and count as stated and as evaluated, each token's program-wide value; then the ranges, gaps and overlaps across the program |
MANIFESTS |
per schedule, a matrix: one row per System, one column per token by program-wide value, the set's letter in the cell, the partition index over each range |
Where the ECS file states an expression as written, the ECS map file states the evaluated value. The two files are written by one run and compared as a pair: the ECS file by its meaning, the ECS map file as text below its head line.
The Label
The label is the program's name in the file: the name of the PROGRAM group and of the PROGRAM frame. It is a name with letters, digits and hyphens, never dotted, and no module carries it.
In the extracted file the label is the program directory's name. In a file for generation it is authored, and the generator takes it as given.
The Directories
A program's sources lie in one directory per group: the program-level modules in the program directory, each partition's modules in a directory of their own below it, named as the partition module, and a library's transports wherever the library is. The extractor places a module by the first directory below the program directory in its linked path: the program directory itself is the PROGRAM group, a partition directory is that partition's group, and anything deeper is the programmer's own organisation and reads the same. One directory per partition, always: a one-partition program has one partition directory, as a one-schedule program has one SCHEDULE group. A partition kind in the program directory, or a program-level kind in a partition directory, is a placement error.
The generator writes by the same rule and creates nothing else. The file carries no directory data beyond the rule, and a LIBRARY entry names a module that exists.
Grammar
The grammar in one place, in the notation of the Oberon report. Keywords and the kind names are written in capitals; the kind sets are the ones of The Kinds above.
ECSFile = [HeadLine] [AllowListsLine] [RolenamesSection] ModulesSection [DeclarationsSection] [HeaderSection].
HeadLine = label "--" "extracted" date time ident version.
AllowListsLine = "ALLOWLISTS" line.
RolenamesSection = "ROLENAMES" {kind {ident}}.
ModulesSection = "MODULES" {Group}.
Group = ProgramGroup | ScheduleGroup | PartitionGroup | LibraryGroup.
ProgramGroup = label "=" "PROGRAM" {Entry} "END".
ScheduleGroup = ident "=" "SCHEDULE" {Entry} "END".
PartitionGroup = ident "=" "PARTITION" {Entry} "END".
LibraryGroup = ident "=" "LIBRARY" {Entry} "END".
Entry = ident {"," ident} ":" kind
| ident ":" kind ident {ident}.
DeclarationsSection = "DECLARATIONS" {Frame}.
Frame = label ":" "PROGRAM" {DeclEntry} "END"
| ident ":" "PARTITION" {DeclEntry} "END".
DeclEntry = ident ":" kind {Fact} "END".
Fact = factName "=" Value.
factName = ident {"." ident} ["[" number "," number "]"].
Value = number | string | SetExpression | NameList | SourceText | "UNREAD".
NameList = Name {"," Name}.
Name = ident ["." ident].
SourceText = Oberon text as the source writes it, to the end of the line: a constant expression, or a deployment call.
SetExpression = Term {"+" Term}.
Term = "{" [Reference {"," Reference}] "}" | Reference.
Reference = ident "." ident "." ident {"." ident}.
HeaderSection = "HEADER" {line}.
label = letter {letter | digit | "-"}.
ident = letter {letter | digit}.
number = digit {digit}.
string = '"' {character} '"'.
Comments are Oberon comments, (* ... *), admitted anywhere before the HEADER keyword; the header's lines and the allow-lists line are taken verbatim to the end of their line or of the file. There are no separators: the next token decides every boundary.
The kind a kind stands for depends on the group: the program-level kinds and the transport kinds in a PROGRAM group, PARTITION in a SCHEDULE group, the partition kinds and the transport kinds in a PARTITION group, the transport kinds in a LIBRARY group. The second form of Entry carries the entry's parameters, an adapter's transport among them; the comma form admits several modules of one kind on a line when none carries parameters. A DeclEntry has the kind of its module, and the form of a Value is decided by the fact's name, the table below: a number alone is a number, a bare name is a constant expression under a constant's name and a module under library. The value UNREAD stands where the extractor met a construct it does not read, the discipline prescribing an effect and not a form: the fact's name is written, its value is not, the reader takes the fact as absent, and the reviewer reads the source.
The facts a DeclEntry admits depend on its kind:
| Kind | Fact | Value |
|---|---|---|
| any kind | library |
a list of module names: the library modules the module imports |
TOKENS |
<record>.<path> |
a number: the token's partition-local ordinal |
TOKENS |
<record>.<name> |
a braced set of the module's own tokens: a surface |
SYSTEM |
name |
a string: the System's name |
SYSTEM |
period |
the registration call's period argument, as written |
SYSTEM |
manifest.C, manifest.Cp, manifest.O, manifest.P, manifest.Px, manifest.Cx |
a set expression |
SYSTEM |
values |
a list of module-qualified names: the VALUES constants the System names |
ADAPTER |
put.<procedure>, get.<procedure>, read.<procedure> |
a braced set of the tokens the procedure reaches |
SCHEDULE |
schedule |
the number 0, on schedule 0's entry alone |
SCHEDULE |
deployment |
the deployment call as the source writes it, on every other schedule's entry |
BOUNDARY |
<variable>.flow[i, j], <variable>.deposit[i, j] |
a set expression over surfaces, from schedule i to schedule j |
PERIODS, RESOURCES, TOKENBASES, WIRING, VALUES |
<constant> |
the constant's expression as the source writes it |
Rules the Grammar Does Not Express
The reader enforces these beyond the syntax:
-
One
PROGRAMgroup, oneSCHEDULEgroup per schedule, onePARTITIONgroup per partition; everySCHEDULEentry of thePROGRAMgroup has its group, everyPARTITIONentry of aSCHEDULEgroup has its group, and a partition belongs to exactly one schedule. -
Parameters match the kind. An entry's parameters, its own or its kind's
ROLENAMESline, are as many as the kind has roles; an entry of a kind absent fromROLENAMEScarries its own; an entry carries all of its kind's parameters or none. -
Kinds by placement. An entry's kind must be admissible in its group; a
TOKENSentry in thePROGRAMgroup or aTOKENBASESentry in aPARTITIONgroup is an error. -
An adapter's transport is declared: in a
PARTITIONgroup under a transport kind, or in aLIBRARYgroup. -
Frames follow the groups. Every
DeclEntrysits in the frame of the group that declares its module; frames do not nest; an entry outside a frame is an error. ALIBRARYgroup has no frame: a library module is read for its kind declaration alone. -
Closed under reference. Every
Referenceresolves to a declared module, one of its two records, and a token or surface declared there. A token stands inside braces, a surface stands bare; a surface's members are tokens of the same module; a manifest or crossing names a foreign module's surfaces, never its tokens one by one. -
One record name each. A
TOKENSentry declares all tokens under one record name and all surfaces under another; the two differ. Ordinals are not repeated within a module. -
No duplicate facts. A fact name appears once per entry; a reference appears once per set.
-
Schedule 0 is numbered, the others are deployed. Exactly one
SCHEDULEentry carriesschedule = 0, every other carriesdeployment; the matrix indices of a crossing lie in 0…n-1 for n schedules, and a schedule does not cross to itself. -
UNREADis written by the extractor alone. A file for generation has noDECLARATIONS, so it never carries the value; the reader takes such a fact as absent. -
The
HEADERblock is one balanced Oberon comment, once the!marks and the description slot line are taken as the conventions they are: it is emitted as a module's leading comment, verbatim.
A file that breaks a rule is refused as a whole; nothing is read past the error.
See Also
- ECS for Control: Concepts
- ECS for Control: Implementation
- ECS for Control: Review Guide
- ECS for Control: check-ecs
- ECS for Control: extract-ecs
Last updated: 2 November 2026