Constraints
Constraints in a definition validate several values together, also across configurations and definitions.
A parameter’s min, max and pattern check one value on its own.
Many real constraints validate multiple values.
An FD controller needs a data bitrate, the tasks of a core must not overload it, two memory regions must not overlap.
Without constraints such a validation lives in a generator, a script or a review checklist, and a configuration breaking it passes the editor to fail late or not at all. A constraint moves the validation into the definition. The editor marks the violation while the configuration is written, the CLI rejects it, and every product using the definition checks the same rule.
Each constraint is a boolean expression written in a subset of JSONata (see Differences to JSONata), typed against the definition. A typo, a unit mismatch or a constraint that would run in quadratic time is a definition error, never a slow or silent configuration check. You do not need prior JSONata knowledge, Reading an Expression covers what most constraints use.
The examples in this chapter come from one sample definition, Mcu.
A board with CAN controllers, memory regions, ports, cores and tasks.
Why This Language
-
Always terminates: no loops, no recursion, lambdas only as function arguments.
-
Side-effect free: always reproducible, no external state, the result depends on the configuration alone.
-
Compiled with the definition: parsing, typing and cost analysis run once.
-
Statically typed: every path resolves to a definition element, a typo is a definition error.
-
Strongly typed: no implicit conversions, only booleans are conditions and lists stay lists.
-
Errors fail the constraint: a division by zero or an overflow is a violation, never
Infinityornull. -
Familiar syntax: JSONata paths and predicates with C operators (
&&,||,!,==).
Unique to Confµgami constraints:
-
Static cost limits: Confµgami rejects slow constraints in the definition. Constraints need to be
< O(N^2)(see Cost). -
Units in the type: values convert between units, mixing dimensions is a definition error.
-
Checked absence & null safety: reading a possibly absent value requires a check (see Absent Values).
-
Configuration semantics: defaults used, automatic reference resolution, merged model.
-
Exact integers:
u64stays exact, an overflow is a violation. -
Explained violations: a diagnostic lists the values the evaluation read.
-
Editor support: code completion, hover, go to definition and highlighting inside the expression.
Writing a Constraint
constraints can be added to a definition element, like multiplicity:
canControllers: {
typeName: "CanController",
multiplicity: "0..*",
constraints: [
{
name: "FdNeedsDataBitrate",
assert: "mode != 'FD' || $exists(dataBitrate)",
message: "FD controller '{name}' needs a data bitrate.",
},
],
| Key | Meaning |
|---|---|
|
Required, UpperCamelCase.
Unique among all constraints on a type.
Names the constraint in its diagnostics, |
|
Optional, a short title in Markdown.
The hover of an element lists it for the constraint, in place of the |
|
Optional, a longer text in Markdown, shown with the |
|
Required, the boolean expression that has to hold for each instance in a JSONata subset. Evaluating to |
|
Required, the diagnostic text, ending in a A template: A placeholder renders:
|
|
|
The sample configuration satisfies the constraint:
canControllers: [
{
name: "Can0",
mode: "FD",
nominalBitrate: "500 kHz",
dataBitrate: "2 MHz",
rxBuffer: {
size: "128 B"
},
},
],
Removing the dataBitrate line breaks it.
The diagnostic reports at the controller. Its related information lists the values the failing evaluation read:
Mcu.cfg.json5:21:23-29: error: FD controller 'Can0' needs a data bitrate. [FdNeedsDataBitrate]
|
21 | name: "Can0",
| ^^^^^^ FD controller 'Can0' needs a data bitrate. [FdNeedsDataBitrate]
| ------ dataBitrate (absent)
22 | mode: "FD",
| ---- mode = FD
...
Mcu.def.json5:
74 | name: "FdNeedsDataBitrate",
| -------------------- Constraint FdNeedsDataBitrate declared here.
Inside a constraint expression string, the editor provides code completion.
It offers hover, go to definition and semantic highlighting (see Editor Features).
In a configuration file, the hover of a violation traces the violated assert.
An AI agent connected to the Confµgami MCP server helps to write and debug constraints.
A long constraint continues on the next line when its line ends with a \ (a line continuation), the indentation of the next line is whitespace inside the constraint:
{
name: "CoreLoad",
assert: "$sum($referencedBy($instance, 'Mcu::Board::Task::CoreRef').(runtime / period)) \
<= 1",
message: "The tasks of core '{name}' overload it.",
},
End the line with a bare \, not with the \n\ of a multiline description: a constraint needs no line break.
Compatibility
Adding or tightening an Error constraint needs a new major definitionVersion, as with tightening min or max.
Existing configurations can become invalid.
A Warning or Info constraint needs none, since it fails no configuration, but changing one to Error does.
To give the configuration authors time to adapt, ship a new constraint with severity: "Warning" first and raise it to Error with a later major version.
Placing a Constraint
The definition element writing a constraint decides which configuration instances run it and where a violation reports:
-
On an entity, a struct or a parameter, inline or in
types, it is a type constraint. Every instance of the type runs it, wherever the type is used, and a type inheriting from it inherits the constraint.canControllersdeclares its entity inline, soFdNeedsDataBitrateis a type constraint ofMcu::Board::CanController, not a use-site one. It runs for every controller,AboveNominalfor every data bitrate:canControllers: { typeName: "CanController", multiplicity: "0..*", constraints: [ { name: "FdNeedsDataBitrate", assert: "mode != 'FD' || $exists(dataBitrate)", message: "FD controller '{name}' needs a data bitrate.", }, ], entity: { dataBitrate: { u32: { unit: "Hz", }, constraints: [ { name: "AboveNominal", assert: "$instance >= %.nominalBitrate", message: "The data bitrate is below {%.nominalBitrate}.", }, ], }, mode: { multiplicity: "1..1", enum: { literals: [ "CLASSIC", "FD" ], }, }, nominalBitrate: { multiplicity: "1..1", u32: { unit: "Hz", }, }, frameSize: { u16: { unit: "Bytes", default: 64, }, }, -
On a
typeRefor aunionrelation it runs at that use site only.rxBufferandtxBuffershare the typeBuffer, only the rx buffer has to hold a frame:rxBuffer: { multiplicity: "1..1", typeRef: "Mcu::Buffer", constraints: [ { name: "RxBufferFitsFrame", assert: "size >= %.frameSize", message: "The rx buffer holds {size}, a frame needs {%.frameSize}.", }, ], }, txBuffer: { typeRef: "Mcu::Buffer", }, -
At the top level of the definition, beside
definition, it is a constraint of the definition root. It runs once over the whole configuration and reports at the$definitionof the configuration:constraints: [ { name: "NoRegionOverlap", assert: "$noOverlap(board.memoryRegions, $ => start, $ => start + size)", message: "Two memory regions overlap.", }, ],
A constraint on a 1..1 top-level entity such as board runs once as well, with the entity as its context:
board: {
multiplicity: "1..1",
constraints: [
{
name: "CpuLoad",
assert: "$sum(tasks.(runtime / period)) <= 1",
message: "The tasks load the CPU to {$sum(tasks.(runtime / period))}.",
severity: "Warning",
},
{
name: "UniquePriorities",
assert: "$unique(tasks.priority)",
message: "Two tasks share a priority: {tasks.priority}.",
},
],
A violation marks the instance the constraint ran for, so write a constraint on the element it should report on.
PriorityLimit on tasks reports at each offending task, a $every(tasks, ...) on the board would report once, at the board.
A constraint on a parameter runs while the parameter has a value, set in the configuration or by its default, and once per value of a list parameter.
An inherited constraint cannot be overridden or switched off.
See Reusable Types and Inheritance for types, typeRef and inherits.
Reading an Expression
An expression is evaluated for one configuration element at a time, its instance ($instance):
-
A simple name reads a property of the instance: on
canControllers,modeis the mode of the controller. -
.steps to a child. A step over a list visits every element and yields a list. Onboard,tasks.priorityis the priority of each task,tasks.subtasks.namethe name of every subtask of every task. -
[ ]after a step filters its elements by a condition,tasks[priority > 5], or picks one by position,canControllers[0]. Inside the brackets a bare name reads the element tested. -
.( )computes a value per element:tasks.(runtime / period)is the CPU share of each task. -
A name starting with
$is a function,$sum(...), or a variable, such as$instance. -
Comparisons and logic read as in C:
==,!=,<,>=,&&,||,!. -
A string, and a number with a unit, is a quoted literal:
'FD','500 kHz'.
Read from the inside out, CpuLoad on board checks the load of the CPU:
| Part | Value in the sample configuration |
|---|---|
|
The list of tasks, |
|
The share of each task, |
|
The sum of the shares, |
|
|
A rule "if A then B" is written !A || B: it holds when A is false, and otherwise needs B.
FdNeedsDataBitrate reads "the controller is not in FD mode, or it has a data bitrate": mode != 'FD' || $exists(dataBitrate).
An optional property needs such a guard, see Absent Values.
Common Expressions lists the expressions of typical rules and Expression Language the whole language.
Common Expressions
Each expression holds for the sample configuration, on the element in the first column. See Expression Language for syntax explanations.
| Constraint on | Expression | Validates |
|---|---|---|
|
|
A value is required when another has a given value. |
|
|
An optional value, when present, against another one. |
|
|
The same from the optional parameter itself, which runs only while it has a value. |
|
|
Two values of one entity. |
|
|
Two values in different units, |
|
|
A computed end within a bound. |
|
|
An alignment. |
|
|
A value from a fixed set. |
|
|
Values unique across a list. |
|
|
At most two elements of a kind. |
|
|
A specific entity exists. |
|
|
At least one element of a kind. |
|
|
A text length within a bound. |
|
|
An optional value with a fallback when absent. |
|
|
A sum within a budget. |
|
|
Every core is referenced by a task. |
|
|
At most four tasks per core. |
definition root |
|
No two address ranges overlap. |
Expression Language
Context
| Name | Meaning |
|---|---|
|
The instance the constraint runs for: an entity, a struct, or the value of a parameter. The same everywhere in the constraint. |
|
The current context, rebound in every path step and predicate.
A bare name is |
|
The root of the configuration of the definition declaring the constraint, which holds its top-level entities.
In a constraint of a |
|
The parent of what comes before it, see The Parent |
Entity Name and Id
name and id of an entity read like properties.
id has a value only where the configuration writes one or the definition derives it with idSource, so a decision reading it needs a guard (see Absent Values).
An expression has no absolute entity paths such as /Mcu/Board/Core0, since entity names exist only in configurations.
A predicate finds a specific entity: cores[name == 'Core0'].
Annotations
A configuration annotation is a path step, _safety.asil on tasks, typed by its annotation type (see Annotations).
One the definition writes is read through $def, $def($instance)._limit.max on priority.
The Parent %
% reads the parent of an element, the entity or struct holding it:
-
At the start of a path, the parent of the current context
$. Outside a predicate or lambda that is the parent of$instance:%.nominalBitrateondataBitratereads its controller. Inside a predicate it is the parent of the element tested:tasks[priority <= %.maxPriority]onboardreads the board holding each task. -
After a path step or a function, the parent of what it yields:
$referencedBy($instance, 'Mcu::Board::Task::CoreRef').%oncoresis the board holding the tasks. A step through arefreads its target, socoreRef.%ontasksis the board holding the core. -
The parent of a top-level entity such as
boardis the definition root,$$.
The parent type has to be known from the definition.
It is known at a use site and for a type used in one place only.
Buffer is used by the CAN controllers and by the cores, so a constraint on the type Buffer reading % is an error:
The parent of 'Mcu::Buffer' is not unique, it is used in
'Mcu::Board::CanController', 'Mcu::Board::Core'.
Write such a constraint at the use site instead, as RxBufferFitsFrame does.
Operators
-
Paths and predicates:
tasks.priority,tasks[priority > 10], the indexesx[0]andx[-1]. An index binds to the last step of a path, so it applies per element of the step before:tasks.subtasks[0]is the first subtask of every task.tasks.name[0]is an error, since each task has one name. A parenthesized list takes an index:(tasks.subtasks)[0]is the first subtask of all of them. -
Comparison
==(also spelled=),!=,<,<=,>,>=. Membershipintests against a list,['FD', 'CLASSIC']ortasks.priority. -
Arithmetic
+,-,*,/,%./yields a float, so a ratio of integers is never truncated.%between two values is the modulo,start % 4, in front of a path step the parent,%.period. -
Logic
!,&&,||, with the JSONata spellingsandandoraccepted as well. -
c ? a : b, anda ?? b, which takesawhen it has a value, elseb. -
a ?: btakesaunless it is absent,false, zero or'', elseb, andahas to be a boolean, a number or a string.(deadline ?: '10 ms') > '5 ms'ontaskstreats a zero deadline as unset. -
[a, b], which joins a list operand into the list,[tasks.subtasks.name, cores.name]onboard, ranges with literal bounds[1..4], and( $x := expr; ... )blocks binding variables. -
:=is a variable assignment, it binds everything up to the next;of its block.
Precedence
From the tightest to the loosest binding, the operators of one level group from left to right, ??, ?: and ? : from right to left.
A float result rounds after each operation in that grouping, so a + b + c rounds a + b first.
| Operators | Example |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Lambdas
A lambda is a literal argument of $every, $some, $single, $filter and $noOverlap, with exactly one parameter, and its body runs to the end of that argument.
$ => body binds the element as the context, so its bare names read the element: $every(tasks, $ => priority > 15) reads like the predicate tasks[priority > 15].
A named parameter, $t => ..., keeps the outer context for bare names, write one only where the body needs both.
The JSONata form function($t){ body } works as well.
Literals
-
Numbers: integers
42, hex0x7FF, decimals1.5and exponents1e3, a negative number is the unary-,-1 -
Strings (
'FD','500 kHz') -
Booleans:
true,false
A string takes single or double quotes, single quotes need no escape inside the double-quoted JSON5 string of the constraint. Each escape is written with a doubled backslash in the JSON5 string:
| Escape | In the JSON5 string | Meaning |
|---|---|---|
|
|
A backslash |
|
|
A single quote |
|
|
A double quote |
|
|
A newline, a tab, a carriage return |
|
|
A slash |
|
|
A UTF-16 code unit, a surrogate pair as two of them |
Type Paths
A string argument names a type by its type path, 'Mcu::Board::Task'.
It names a property by the type path of its parent, a . and the property name: 'Mcu::Board::Task.coreRef'.
$referencedBy also takes the ref parameter’s own type path, 'Mcu::Board::Task::CoreRef', whose last name is the propertyName with its first letter uppercased unless the parameter has a typeName.
:: joins type names only, 'Mcu::Board::Task::coreRef' is an error.
Types and Units
Every path resolves to definition elements during compilation.
An unknown property, an unknown enum literal or a property no refTargetTypes entry has is a definition error.
So is comparing with == against a string the parameter can never hold, because it is outside its literalsExhaustive or fails its pattern: arch == 'CortexM9'.
An ordering bound such as arch < 'D' need not be a value the parameter holds.
-
Strings order by their Unicode code points, so
'B' < 'a'. An enum has no order, compare it with==or!=. -
Booleans are strict:
!,&&,||and a? :condition take only booleans,$count(tasks) > 0instead of$count(tasks). -
A list relation types as a list whatever its size, a
0..1relation as an optional value, a1..1relation as a value. An empty list is a list, so$countof it is 0, and$exists,??and?:on a list are errors. -
Integers are exact,
u64included, an overflow is a violation. An integer compared with a float keeps every digit.$uniqueand$distincttreat3.0and3as equal. -
A value carries its unit.
deadline <= periodcompares milliseconds with microseconds converted,Hzagainstsis a definition error. A unit literal is a string,'500 kHz', a bare number is in the unit of the other operand. Arithmetic keeps the dimensions apart:-
+and-take one dimension on both sides,runtime + deadline. -
*and/take a unitless factor,period * 2. -
/of two values of one dimension yields a unitless ratio,runtime / period.
-
-
A
refdereferences in a path step,coreRef.arch, and compares by target with==.
Absent Values
A decision that could read an absent value is a definition error until the expression states what absence means.
Decisions are the assert, the operands of !, && and ||, a ? : condition, a predicate and a lambda body.
Absent may be:
-
an optional parameter without
default, an optional entity or struct, an annotation, anid, -
a property only some
refTargetTypeshave, -
$max,$minor$averageof a list that may be empty, -
$cfgByDefinition, and$$in a constraint of apublictype, since another definition may use the type.
dataBitrate >= nominalBitrate on canControllers, in a constraint named DataBitrateAboveNominal, reports:
Constraint DataBitrateAboveNominal may be undefined when 'dataBitrate' is absent.
State what absence means: '!$exists(dataBitrate) || ...' holds without it,
'$exists(dataBitrate) && ...' requires it, '?? <default>' substitutes one.
The checker follows the guards: the right side of $exists(x) && ... and of !$exists(x) || ..., the branches of $exists(x) ? ... : ... and the right side of ?? and ?: know x has a value.
$isType(x, 'Def::Type') narrows the same way.
The right side of $isType(x, 'Def::Type') && ... and of !$isType(x, 'Def::Type') || ..., and the first branch of $isType(x, 'Def::Type') ? ... : ... read x as that type.
Only these forms guard: $count(x) > 0 && $max(x) > 3 is still an error, write ($max(x) ?? 0) > 3.
A comparison or an arithmetic operation reading an absent value has no result, it is neither true nor false.
?? chooses one for that case: (txBuffer.size != 128) ?? true holds for a controller without a tx buffer.
A constraint on an optional parameter runs only while it has a value, so AboveNominal needs no guard.
A message placeholder renders an absent value as an empty text.
Built-in Functions
Built-in Functions lists every function with its argument and result types.
Evaluation
A constraint sees the whole configuration of its definition in the CfgSpace, however the file mode splits it into files:
-
SingleFile: the one configuration file. -
MultipleFiles: the root combines the top-level entities of every file, so a root constraint,$$and a%up to the root see every file. An entity below the top level lives in one file together with its children. -
MultipleFilesEntitiesMergeableandMultipleFilesEntitiesMergeableWithPriority: the merged model, each element combining every file writing it.
Evaluation is short-circuit and left to right: && and || read their right side only when the left does not decide, ? : reads one branch.
A constraint does not run while a configuration file it reads or the definition declaring it has errors.
A division by zero, an overflow or a non-finite float is a violation at the constraint’s severity, with the cause in the message.
Cost
Every constraint runs on each configuration change in the editor, so an unchecked cost would slow the editor down. The cost checker therefore calculates the static cost of a constraint at compile time. From the multiplicities it computes how often a constraint reads each element.
A constraint above linear in the configuration size is a definition error:
| Constraint | Finding |
|---|---|
|
|
|
|
|
|
|
Linear: every task takes the length of the cores once and reads no core. |
|
Linear, one length per task, no subtask is read. |
|
Linear: the sum reads nothing of the task, so it is computed once and every task reads the result. |
|
|
A part that depends neither on the instance nor on the element of an enclosing lambda or predicate is the same for every instance. It is computed once per configuration and counts once, a violation still relates the values it read.
$count of a list takes its length once per parent and reads no element.
$count(tasks[priority > 5]) > 0 stops testing at the first match, a count of a filter compared with a number stops one match past it wherever reaching that match alone makes the constraint hold.
in against a list of integers and ranges, priority in [1, 5..9], tests their bounds at a fixed cost.
A range anywhere else, $sum([1..1000]), costs a step per number.
$referencedBy answers from an index of the references in the configuration, which makes the backward direction linear.
CoreLoad, the line continuation example in Writing a Constraint, finds the tasks of a core without scanning all tasks.
A constraint on a type is checked at each place the type is used, since the number of instances differs per place.
Limits
Linear alone does not make a constraint cheap, so all constraints of a definition share a budget, checked when the definition is compiled. A step is one operator, function call or path step, or one element a path step visits. A step repeated for every element of a list is a read of that element.
| Limit | Value | What it prevents |
|---|---|---|
Growth |
Linear |
The most important limit. A constraint comparing every task with every other task takes ten billion steps on 100000 tasks, half an hour instead of milliseconds. |
Reads per element |
25 |
Constraints that stay linear but run many times slower than the rest of the validation. |
Fixed steps |
20000 |
Work that does not depend on the configuration, like |
Runtime steps |
2 x cost + 1000 |
A constraint the cost analysis underestimates running without end. An editor stops it and warns that its result is incomplete, the CLI completes it and warns, so a CLI validation never passes on a cut-off result. |
Above a limit the definition names the three costliest constraints. Later versions may raise the limits.