${{ ... }}
as part of the enclosing string
literal; the placeholder is recognized by the substitution pass, not by
the tokenizer.Forms are declarative schemas for data-capture artifacts — audit checklists, test reports, inspection records, quality control sheets. Every compliance system has them; Primmel provides a first-class construct so they need not be invented in YAML by each adopter.
A form describes:
| Construct | Relationship |
|---|---|
class#data
(
mn-113-2
) |
A form instance persists as a record of the linked `data_class`. The form describes the *shape of capture*; the data class describes the *shape of storage*. |
process
(
mn-113-3
) |
A form is filled in when a `process` executes. The link is via `conformance_process` on the form. |
measurement
(
mn-113-5
) |
Form fields can reference measurements. Symbol-typed fields resolve to the symbol registry (<<mn-113-8>>). |
calculation
(
mn-113-9
) |
Form fields can invoke calculations by name via the `calculation` property. The calculation's inputs are bound to form fields or sub-paths. |
provision
(
mn-113-4
) |
A form's
pass_fail
block typically validates one or more provisions. |
Without a form construct, every compliance system must invent one. The alternatives — embedding form schemas in code, or using ad-hoc YAML — violate the single-notation integration principle (see principles-and-requirements ).
By making forms first-class:
A form declaration uses the
form
keyword followed by an identifier
and a brace-delimited body.
form <FormID> {
name "Display name"
description "..."
data_class <DataClassID>
header <HeaderFormID>
conformance_process <ProcessID>
applicability {
<dimension>: [<values>]
}
field <FieldName> : <type> { ... }
subform_ref <SubformID> {
parameters { ... }
applicability { ... }
}
pass_fail {
criteria "..."
pass_if "ocl{...}"
}
}
| Property | Required | Description |
|---|---|---|
name |
yes | Display name of the form. |
description |
no | Free-text description. |
data_class |
no | Links the form to a `class#data`. When set, instances of the form are persisted as records of the named data class. |
header |
no | A shared header form. The header's fields are prepended to this form's fields when rendered. |
conformance_process |
no | The
process
that fills in this form when executed. |
applicability |
no | Dimension-based applicability constraints. See applicability . |
field |
no | Inline field declaration. Zero or more. See field-types . |
subform_ref |
no | Composition reference to a
subform
. Zero or more. See
subforms
. |
pass_fail |
no | Form-level pass/fail rule. See below. |
The
pass_fail
block declares the form-level pass/fail rule. It is
optional; if absent, the form has no aggregate evaluation and only
field-level
evaluation
rules apply.
pass_fail {
criteria "Human-readable description of what constitutes a pass"
pass_if "ocl{<expression>}"
}
The
criteria
is a human-readable string used in documentation and UI
rendering. The
pass_if
is an OCL expression that must evaluate to
true
for the form to pass.
form LoadCellInfo {
name "Load cell information"
description "Identification and parameters of the load cell under test."
data_class LoadCellInfo#data
header FormHeaderA
field manufacturer {
label "Manufacturer"
type string
required true
measurement_method declared
}
field type_designation {
label "Type designation"
type string
required true
measurement_method declared
}
field emax {
label "Maximum capacity (Emax)"
type number
unit "kg"
required true
measurement_method declared
}
field accuracy_class {
label "Accuracy class"
type enum
values [A, B, C, D]
required true
measurement_method declared
}
}
Form fields share their grammar with
class
fields (see
classes
)
and add form-specific properties for measurement method, calculation
bindings, and evaluation rules.
field <FieldName> : <type> {
label "..."
definition "..."
unit "..."
required true | false
measurement_method direct | computed | derived | declared | lookup | evaluated
calculation <CalculationID>
calculation_bindings {
<param>: <field-or-path>
}
evaluation {
rule "ocl{...}"
condition "..."
}
reference { <ReferenceID> }
values [<value>, ...]
default <value>
}
The
<type>
placeholder can be any of:
| Method | Description |
|---|---|
direct |
The value is observed directly from the instrument under test (e.g., a voltage reading). |
computed |
The value is calculated from other fields via a
calculation
reference. |
derived |
The value is computed from other fields via an inline OCL expression in `derivation`. |
declared |
The value is declared by the manufacturer or applicant (e.g., Emax, accuracy class). |
lookup |
The value is looked up from a
table
based on other field values. |
evaluated |
The value is the result of evaluating a pass/fail rule (e.g., `within_mpe`, `overall_result`). |
When a field has a
calculation
reference, its
calculation_bindings
block maps the calculation's named inputs to form fields or sub-paths:
field error_EL : number {
unit "v"
measurement_method computed
calculation loadCellError
calculation_bindings {
avgIndication: average_indication
referenceIndication: reference_indication
conversionFactor: f
}
}
The parser:
1. Looks up the calculation
loadCellError
in the calculation registry.
2. Validates that the bindings match the calculation's declared inputs
by name.
3. Validates that each bound field's type and unit are compatible with
the calculation input's declared type and unit.
4. The field's effective type and unit become the calculation's output
type and unit.
Sub-paths are allowed:
runs.indication
means "the indication field of
each item in the runs array".
A field's
evaluation
block declares a pass/fail rule:
field within_mpe : enum {
values [yes, no]
measurement_method evaluated
evaluation {
rule "ocl{abs(error_EL) <= abs(mpe)}"
condition "|E_L| <= |MPE|"
reference { R60-1-5-3-1 }
}
}
The
rule
is an OCL expression. The
condition
is a human-readable
summary. The optional
reference
links to the normative source of the
rule.
A field may declare a default value:
field plc : number {
default 0.7
measurement_method declared
}
If the field is not explicitly set when the form is filled in, the default applies.
A form's
applicability
block declares under what conditions the form
is required. Conditions are expressed as dimension-value constraints.
form <FormID> {
applicability {
<dimension>: [<values>]
<dimension>: [<values>]
}
}
form HumidityCH {
name "Humidity test for CH/unmarked load cells"
applicability {
humidity_class: [CH]
}
// ...
}
This form applies only when the instrument under test has
humidity_class = CH
. Instruments with
SH
or
NH
are exempt.
form LoadTest {
name "Load test data"
applicability {
accuracy_class: [A, B, C, D]
}
// ...
}
When a form references a subform via
subform_ref
, the call site can
further narrow the subform's applicability:
field load_test_before : array {
subform_ref LoadTestRow {
parameters { n_runs: 3 }
applicability { phase: before }
}
}
The composed field's applicability is the intersection of the parent form's applicability and the call-site applicability.
A form's
applicability
block can declare parameter mappings that
resolve at form-materialization time:
form LoadTest {
applicability {
accuracy_class: [A, B, C, D]
n_runs: {
A: 5
B: 5
C: 3
D: 3
}
}
// ...
}
The form materializer resolves
n_runs
from the active
accuracy_class
when instantiating the form for a specific instrument.
A form's
applicability
should match the applicability of its
conformance_process
. The parser emits a warning if they diverge.
Subforms are reusable, parameterizable row shapes that can be composed into multiple parent forms. They eliminate duplication when the same row structure appears in multiple forms with minor variations.
Without subforms, every variant of a repeated structure requires a
separate
form
declaration. For example, a load test with 3 runs
(classes C/D) and 5 runs (classes A/B) would be two separate forms,
each ~95 lines, with 90% duplicated content.
Subforms collapse this duplication: one parameterized subform is composed into multiple parent forms with different parameter values.
subform <SubformID> {
parameters {
<name> : <type> {
description "..."
default <value>
mapping { <key>: <value> }
}
}
type object | array
description "..."
field <FieldName> : <type> { ... }
}
A parameter has a name, a type, and optionally:
parameters {
n_runs : integer {
description "Number of runs per test_load. Resolved from accuracy_class."
mapping {
A: 5
B: 5
C: 3
D: 3
}
}
}
Field properties inside a subform may contain
${{ name }}
placeholders.
These are substituted at composition time using the parameter resolution
rules (see
parameter-substitution
).
field runs : array<Run> {
min_items "${{ n_runs }}"
max_items "${{ n_runs }}"
}
If
n_runs = 3
resolves at the call site, the inlined field becomes:
field runs : array<Run> {
min_items 3
max_items 3
}
A form (or another subform) references a subform via the
subform_ref
block:
form HumidityTest {
field load_test_before : array {
subform_ref LoadTestRow {
parameters { n_runs: 3 }
applicability { phase: before }
}
}
field load_test_after : array {
subform_ref LoadTestRow {
parameters { n_runs: 3 }
applicability { phase: after }
}
}
}
The
subform_ref
block:
The subform's content (with parameters substituted) becomes the items
shape of the parent's
array
field.
A subform can reference another subform via
subform_ref
. The parser
recursively composes, depth-first. Cycles are detected and rejected.
Composing the same subform twice produces two independent inlined copies. They do not share state.
Parameter substitution replaces
${{ name }}
placeholders inside
subform field properties with values resolved from the call site. The
result is a fully-expanded form tree with no placeholders visible to
consumers.
Substitution happens at PARSE TIME (build time). The runtime sees fully-resolved values. This keeps the runtime simple and produces audit-friendly output.
A placeholder is the token
${{
followed by whitespace, a parameter
name, whitespace, and
}}
:
min_items "${{ n_runs }}"
Within a quoted string, the placeholder is preserved by the tokenizer. The substitution pass walks the parsed tree, finds placeholders, and replaces them.
The substituter distinguishes two cases:
If the entire string value (after trimming quotes) is a single placeholder, the substituted value retains its native type:
min_items "${{ n_runs }}" // ${n_runs = 3} → min_items 3 (integer, not "3")
If the placeholder appears within a longer string, the result is a
string with the placeholder replaced by
String(value)
:
label "Run ${{ n_runs }} of ${{ total }}" // → label "Run 3 of 5"
If a placeholder references a parameter that is not in scope, the build FAILS with an error:
subform-resolver: missing parameter "n_runs" while substituting
(known: phase, humidity_class)
No silent
${{ }}
tokens are left in the output. This prevents
runtime surprises.
Parameters are resolved in this precedence (highest first):
1.
Call-site literal
: explicit value in
subform_ref.parameters
2.
Mapping-derived
: value looked up from
mapping
table based on a
call-site dimension value
3.
Subform default
: value from the parameter's
default
declaration
4.
Error
: parameter required, no resolution found
subform Example {
parameters {
a : integer { default 10 }
b : integer { mapping { X: 1, Y: 2 } }
c : integer { } // no default, no mapping — required
}
field value_a : integer { default "${{ a }}" }
field value_b : integer { default "${{ b }}" }
field value_c : integer { default "${{ c }}" }
}
form Caller {
field x : array {
subform_ref Example {
parameters {
c: 99 // explicit literal
// b resolves from mapping if parent's dimension value is X or Y
// a falls back to default 10
}
}
}
}
Substitution happens BEFORE recursive composition. If a subform references another subform, the outer parameters propagate inward:
subform Outer {
parameters { n : integer }
field inner : array {
subform_ref Inner {
parameters { count: "${{ n }}" } // outer parameter passed to inner
}
}
}
subform Inner {
parameters { count : integer }
field items : array {
min_items "${{ count }}"
}
}
Substitution applies to:
Substitution does NOT apply to:
After the substitution pass, the parsed model contains NO
${{ }}
tokens. The
dump
serializer emits the fully-resolved form. The
runtime consumes only resolved values.
This makes the runtime trivial: it never needs to know about parameters or substitution.
When a form references a subform via
subform_ref
, the subform is
inlined
into the parent. This section defines the rules that govern
composition.
When parsing a model with
subform_ref
directives, the parser performs
these steps in order:
1.
Tokenize
— split the source into tokens.
2.
Parse
— build a structured representation with
subform_ref
directives preserved.
3.
Resolve parameter values
— for each
subform_ref
, build the
parameter map (literal > mapping-derived > default > error).
4.
Substitute placeholders
— replace
${{ name }}
tokens in the
subform's content using the parameter map.
5.
Inline
— replace the
subform_ref
block with the substituted
subform content.
6.
Recurse
— if the inlined content contains further
subform_ref
directives, repeat from step 3.
7.
Resolve cross-references
— resolve named references to other
model elements (Process, Calculation, etc.).
When the parent form and the subform both declare the same property, the call-site (parent) value wins. This allows the parent to customize the inlined content.
Properties that follow override precedence:
If a subform references itself (directly or transitively), the parser rejects the model with an error:
subform-resolver: cycle detected: A → B → C → A
Composing the same subform into multiple parents produces independent inlined copies. They do not share state.
The inlined content's shape (object vs array) is determined by the
subform's
type
declaration, not by the call site. A subform declared
as
type object
composes as an object; declared as
type array
,
composes as an array.
After composition, field names within a single form must be unique. The parser rejects duplicate field names with an error.
If two subform_refs would produce the same field name (e.g., both
inlining a
test_load
field), the call site must rename one via the
as
keyword (a future extension) or the parent must wrap each
subform_ref in a distinct object field.
Once composition completes, named references (to Process, Calculation, Symbol, etc.) are resolved as if the inlined content had been written directly in the parent. References in the subform that point to model elements continue to resolve correctly.
Given:
subform LoadTestRow {
parameters { n_runs : integer { mapping { A: 5, B: 5, C: 3, D: 3 } } }
type object
field test_load : number { unit "kg" }
field runs : array {
min_items "${{ n_runs }}"
max_items "${{ n_runs }}"
}
}
form LoadTest {
applicability { accuracy_class: [C, D] }
field test_loads : array {
subform_ref LoadTestRow { }
}
}
After composition (assuming
accuracy_class = C
resolves
n_runs = 3
):
form LoadTest {
applicability { accuracy_class: [C, D] }
field test_loads : array {
items {
type object
field test_load : number { unit "kg" }
field runs : array {
min_items 3
max_items 3
}
}
}
}
The composed form is semantically equivalent to writing the inlined
form directly. The source
.mmel
retains the
subform_ref
for
authoring convenience and DRY; the runtime sees only the composed form.
This section provides the formal grammar for forms, subforms, and related constructs using Extended Backus-Naur Form (EBNF).
ModelElement = FormDecl | SubformDecl | SymbolDecl
| CalculationDecl | StateMachineDecl
| /* existing MMEL 0.1 constructs */ ;
FormDecl = "form" ID "{" FormBody "}" ;
SubformDecl = "subform" ID "{" SubformBody "}" ;
FormBody = { FormProperty } ;
FormProperty = "name" STRING
| "description" STRING
| "data_class" ID
| "header" ID
| "conformance_process" ID
| "applicability" "{" { ApplicabilityEntry } "}"
| FieldDecl
| SubformRef
| PassFail ;
ApplicabilityEntry = ID ":" "[" ID { "," ID } "]"
| ID ":" "{" { ID ":" Value } "}" ;
PassFail = "pass_fail" "{" "criteria" STRING "pass_if" STRING "}" ;
FieldDecl = "field" ID [ ":" TypeSpec ] "{" { FieldProperty } "}" ;
TypeSpec = "string" | "text" | "number" | "integer"
| "boolean" | "date" | "datetime"
| "enum" | "object" | "array" | "array" "<" TypeSpec ">"
| ID ; (* class reference *)
FieldProperty = "label" STRING
| "definition" STRING
| "unit" STRING
| "required" ( "true" | "false" )
| "measurement_method" MeasurementMethod
| "calculation" ID
| "calculation_bindings" "{" { BindingEntry } "}"
| "evaluation" "{" "rule" STRING "condition" STRING [ "reference" "{" ID { ID } "}" ] "}"
| "reference" "{" { ID } "}"
| "values" "[" Value { "," Value } "]"
| "default" Value
| "derivation" STRING
| "items" "{" { FieldProperty } "}"
| "fields" "{" { FieldDecl } "}" ;
MeasurementMethod = "direct" | "computed" | "derived"
| "declared" | "lookup" | "evaluated" ;
BindingEntry = ID ":" PathExpr ;
PathExpr = ID { "." ID } ;
SubformBody = { SubformProperty } ;
SubformProperty = "parameters" "{" { ParameterDecl } "}"
| "type" ( "object" | "array" )
| "description" STRING
| FieldDecl
| SubformRef ;
ParameterDecl = ID ":" TypeSpec "{" { ParameterProperty } "}" ;
ParameterProperty = "description" STRING
| "default" Value
| "mapping" "{" { ID ":" Value } "}" ;
SubformRef = "subform_ref" ID "{" { SubformRefProperty } "}" ;
SubformRefProperty = "parameters" "{" { ID ":" Value } "}"
| "applicability" "{" { ApplicabilityEntry } "}" ;
Value = STRING | NUMBER | "true" | "false" | ID ;
Within any
STRING
value, the token
${{
ID
}}
is a parameter
placeholder. The substitution pass replaces these before further
processing.
Placeholder = "${{" ID "}}" ;
${{ ... }}
as part of the enclosing string
literal; the placeholder is recognized by the substitution pass, not by
the tokenizer.