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 Infinity or null.

  • 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: u64 stays 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

name

Required, UpperCamelCase. Unique among all constraints on a type. Names the constraint in its diagnostics, [FdNeedsDataBitrate].

title

Optional, a short title in Markdown. The hover of an element lists it for the constraint, in place of the assert.

description

Optional, a longer text in Markdown, shown with the title in the hover of the constraint like the description of a definition element.

assert

Required, the boolean expression that has to hold for each instance in a JSONata subset.

Evaluating to true shows no message, evaluating to false shows the message at the severity.

message

Required, the diagnostic text, ending in a ..

A template: {expr} placeholders are expressions evaluated for the failing instance, {{ and }} are literal braces.

A placeholder renders:

  • a parameter as the configuration: 500 kHz

  • a computed number as the editor hover shows it: 3 ms for a $sum(tasks.runtime) of 3000 µs

  • an entity by its name

  • a list joined with commas

severity

Error (the default), Warning or Info.

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. canControllers declares its entity inline, so FdNeedsDataBitrate is a type constraint of Mcu::Board::CanController, not a use-site one. It runs for every controller, AboveNominal for 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 typeRef or a union relation it runs at that use site only. rxBuffer and txBuffer share the type Buffer, 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 $definition of 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, mode is the mode of the controller.

  • . steps to a child. A step over a list visits every element and yields a list. On board, tasks.priority is the priority of each task, tasks.subtasks.name the 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

tasks

The list of tasks, Control.

tasks.(runtime / period)

The share of each task, 0.2 for 200 µs every 1 ms.

$sum(tasks.(runtime / period))

The sum of the shares, 0.2.

$sum(tasks.(runtime / period)) <= 1

true, the constraint holds.

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

canControllers

mode != 'FD' || $exists(dataBitrate)

A value is required when another has a given value.

canControllers

!$exists(dataBitrate) || dataBitrate >= nominalBitrate

An optional value, when present, against another one.

dataBitrate

$instance >= %.nominalBitrate

The same from the optional parameter itself, which runs only while it has a value.

tasks

runtime <= period

Two values of one entity.

deadline

$instance <= %.period

Two values in different units, ms against us, converted.

memoryRegions

start + size <= 0x100000

A computed end within a bound.

memoryRegions

start % 4 == 0

An alignment.

canControllers

nominalBitrate in ['125 kHz', '250 kHz', '500 kHz', '1 MHz']

A value from a fixed set.

board

$unique(tasks.priority)

Values unique across a list.

board

$count(canControllers[mode == 'FD']) <= 2

At most two elements of a kind.

board

$count(cores[name == 'Core0']) == 1

A specific entity exists.

board

$some(cores, $ => arch == 'CortexM7')

At least one element of a kind.

cores

$length(name) <= 8

A text length within a bound.

canControllers

(dataBitrate ?? nominalBitrate) >= nominalBitrate

An optional value with a fallback when absent.

board

$sum(tasks.(runtime / period)) <= 1

A sum within a budget.

cores

$count($referencedBy($instance, 'Mcu::Board::Task::CoreRef')) > 0

Every core is referenced by a task.

cores

$count($referencedBy($instance, 'Mcu::Board::Task::CoreRef')) <= 4

At most four tasks per core.

definition root

$noOverlap(board.memoryRegions, $ => start, $ => start + size)

No two address ranges overlap.

Expression Language

Context

Name Meaning

$instance

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 $.name, so runtime <= period on tasks reads the task’s own parameters.

$$

The root of the configuration of the definition declaring the constraint, which holds its top-level entities. In a constraint of a public type it may be absent. An instance in the configuration of another definition has no configuration of the declaring one.

%

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: %.nominalBitrate on dataBitrate reads its controller. Inside a predicate it is the parent of the element tested: tasks[priority <= %.maxPriority] on board reads the board holding each task.

  • After a path step or a function, the parent of what it yields: $referencedBy($instance, 'Mcu::Board::Task::CoreRef').% on cores is the board holding the tasks. A step through a ref reads its target, so coreRef.% on tasks is the board holding the core.

  • The parent of a top-level entity such as board is 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 indexes x[0] and x[-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 =), !=, <, <=, >, >=. Membership in tests against a list, ['FD', 'CLASSIC'] or tasks.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 spellings and and or accepted as well.

  • c ? a : b, and a ?? b, which takes a when it has a value, else b.

  • a ?: b takes a unless it is absent, false, zero or '', else b, and a has to be a boolean, a number or a string. (deadline ?: '10 ms') > '5 ms' on tasks treats a zero deadline as unset.

  • [a, b], which joins a list operand into the list, [tasks.subtasks.name, cores.name] on board, 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

( ), $f( )

(a || b) && c

., [ ]

-a.b is -(a.b)

!, -

!a == b is (!a) == b, -a * b is (-a) * b

*, /, %

a - b * c is a - (b * c)

+, -

a - b - c is (a - b) - c, a + b + c + d is ((a + b) + c) + d

??, ?:

x ?? 0 > 5 is (x ?? 0) > 5, x ?? y + 1 is x ?? (y + 1), x ?? y ?: z is x ?? (y ?: z)

==, !=, <, <=, >, >=, in

a < b == c is (a < b) == c

&&, and

a || b && c is a || (b && c)

||, or

a || b ? c : d is (a || b) ? c : d

c ? a : b

a ? b : c ? d : e is a ? b : (c ? d : e)

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, hex 0x7FF, decimals 1.5 and exponents 1e3, 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

\n, \t, \r

\\n, \\t, \\r

A newline, a tab, a carriage return

\/

\\/

A slash

\u and 4 hex digits

'caf\\u00e9'

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) > 0 instead of $count(tasks).

  • A list relation types as a list whatever its size, a 0..1 relation as an optional value, a 1..1 relation as a value. An empty list is a list, so $count of it is 0, and $exists, ?? and ?: on a list are errors.

  • Integers are exact, u64 included, an overflow is a violation. An integer compared with a float keeps every digit. $unique and $distinct treat 3.0 and 3 as equal.

  • A value carries its unit. deadline <= period compares milliseconds with microseconds converted, Hz against s is 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 ref dereferences 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, an id,

  • a property only some refTargetTypes have,

  • $max, $min or $average of a list that may be empty,

  • $cfgByDefinition, and $$ in a constraint of a public type, 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.

  • MultipleFilesEntitiesMergeable and MultipleFilesEntitiesMergeableWithPriority: 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

$count($$.board.tasks[priority == $instance.priority]) == 1 on tasks

O(N^2) in 'tasks' x 'tasks': every task scans all tasks. $unique(tasks.priority) on board checks the same once.

$count($$.board.tasks[coreRef == $instance]) <= 4 on cores

O(N^2) in 'cores' x 'tasks': every core scans all tasks. $referencedBy($instance, 'Mcu::Board::Task::CoreRef') finds the tasks of a core from an index.

$sum(%.memoryRegions.size) <= 0x100000 on tasks

O(N^2) in 'memoryRegions' x 'tasks': every task reads all memory regions, write the constraint on board.

$count(%.cores) == 1 on tasks

Linear: every task takes the length of the cores once and reads no core.

$count(subtasks) <= 8 on tasks

Linear, one length per task, no subtask is read.

$sum($$.board.tasks.runtime) <= 1000 on tasks

Linear: the sum reads nothing of the task, so it is computed once and every task reads the result.

$every($$.board.tasks, $t => $t.priority <= priority) on tasks

O(N^2) in 'tasks' x 'tasks': the list is computed once, but every task scans it.

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 $sum([1..1000000000]), which would take minutes on every validation.

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.