MN 113-7: Primmel Forms and Subforms

#<Coradoc::CoreModel::CommentLine:0x00007fd9672993a0> #<Coradoc::CoreModel::CommentLine:0x00007fd967298e50> #<Coradoc::CoreModel::CommentLine:0x00007fd967298bd0> #<Coradoc::CoreModel::CommentLine:0x00007fd967298810> #<Coradoc::CoreModel::CommentLine:0x00007fd967298450> #<Coradoc::CoreModel::CommentLine:0x00007fd967297eb0>

1. Forms overview

1.1. General

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:

  • the shape of one filled-in instance (fields, types, units)
  • the computation rules that derive some fields from others (via calculation references — see mn-113-9)
  • the evaluation rules that decide pass/fail at the field and form level (via OCL expressions)
  • the applicability conditions under which the form is required (dimension-based, e.g., "applies when accuracy_class is C or D")
  • the composition via subform references that allow reuse of common row shapes (see subforms)

1.2. Relationship to other Primmel constructs

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.

1.3. Why forms are a first-class construct

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:

  • Form schemas participate in cross-model mapping ( map_profile)
  • Form instances are exportable and importable across tools
  • Form applicability is checkable at parse time
  • Calculation bindings are type-checked against the calculation registry

2. Form syntax

2.1. General

A form declaration uses the form keyword followed by an identifier and a brace-delimited body.

2.2. Syntax

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{...}"
  }
}
  1. Display name. Required.
  2. Free-text description. Optional.
  3. Links this form to a class#data; instances persist as records of that class. Optional.
  4. Shared header form (e.g., metadata block common to many forms). Optional.
  5. The process this form fills in when executed. Optional.
  6. Dimension-based applicability. Optional.
  7. Inline field declaration (see field-types). Zero or more.
  8. Reference to a subform with parameter bindings (see subforms). Zero or more.
  9. Form-level pass/fail rule. Optional.

2.3. Properties

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.

2.4. The `pass_fail` block

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.

2.5. Example: a minimal form

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
  }
}

3. Field types

3.1. General

Form fields share their grammar with class fields (see classes ) and add form-specific properties for measurement method, calculation bindings, and evaluation rules.

3.2. Syntax

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>
}
  1. Display label for UI rendering.
  2. Free-text definition.
  3. Unit annotation (e.g., ”kg“, ”counts/v“, ”v“).
  4. Whether the field must be filled in.
  5. How the field's value is obtained. See measurement-methods.
  6. Named calculation to invoke. See mn-113-9.
  7. Input bindings for the calculation. See calculation-bindings.
  8. Field-level pass/fail rule. See evaluation-rules.
  9. Normative reference for traceability.
  10. Allowed values for enum-typed fields.
  11. Default value if not explicitly set.

3.3. Field types

The <type> placeholder can be any of:

  • Primitives: string, text, number, integer, boolean, date, datetime
  • Enumerations: enum (with values block)
  • Objects: object (with nested fields block)
  • Arrays: array or array<ElementType> (with items block)
  • References: a class name (e.g., Address) for embedded helper-class values

3.4. Measurement methods

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`).

3.5. Calculation bindings

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".

3.6. Evaluation rules

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.

3.7. Default values

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.

4. Applicability

4.1. General

A form's applicability block declares under what conditions the form is required. Conditions are expressed as dimension-value constraints.

4.2. Syntax

form <FormID> {
  applicability {
    <dimension>: [<values>]
    <dimension>: [<values>]
  }
}
  1. Single-dimension constraint: form applies when the dimension's value is in the listed set.
  2. Multi-dimension constraint: ALL dimension constraints must hold (AND semantics).

4.3. Semantics

  • An empty applicability block (or no block at all) means the form always applies.
  • Multiple dimensions are AND-ed: all must hold for the form to apply.
  • Within a dimension, the values are OR-ed: the dimension's actual value must be in the listed set.

4.4. Example: humidity form applies only for CH class

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.

4.5. Example: load test applies for all accuracy classes

form LoadTest {
  name "Load test data"
  applicability {
    accuracy_class: [A, B, C, D]
  }
  // ...
}

4.6. Call-site override

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.

4.7. Parameterized 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.

4.8. Relationship to conformance tests

A form's applicability should match the applicability of its conformance_process . The parser emits a warning if they diverge.

5. Subforms

5.1. General

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.

5.2. Why subforms exist

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.

5.3. Syntax

subform <SubformID> {
  parameters {
    <name> : <type> {
      description "..."
      default <value>
      mapping { <key>: <value> }
    }
  }
  type object | array
  description "..."
  field <FieldName> : <type> { ... }
}
  1. Declared parameters. Each can have a description, default value, and dimension-to-value mapping. Zero or more parameters.
  2. Shape: object for a single record, array for a list of records.
  3. Free-text description.
  4. Field declarations using the same grammar as form fields. Zero or more.

5.4. Parameter declaration

A parameter has a name, a type, and optionally:

  • description — what the parameter represents
  • default — value used if not provided at the call site
  • mapping — dimension-keyed resolution table
parameters {
  n_runs : integer {
    description "Number of runs per test_load. Resolved from accuracy_class."
    mapping {
      A: 5
      B: 5
      C: 3
      D: 3
    }
  }
}

5.5. `${{ param }}` placeholders

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
}

5.6. Composition via `subform_ref`

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:

  • parameters { ... } — literal parameter values (highest precedence)
  • applicability { ... } — call-site dimension constraints, intersected with the parent's applicability

The subform's content (with parameters substituted) becomes the items shape of the parent's array field.

5.7. Recursive composition

A subform can reference another subform via subform_ref . The parser recursively composes, depth-first. Cycles are detected and rejected.

5.8. Idempotency

Composing the same subform twice produces two independent inlined copies. They do not share state.

5.9. Example: shared row shape across forms

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<Run> {
    min_items "${{ n_runs }}"
    max_items "${{ n_runs }}"
  }
  field average_indication : number {
    measurement_method derived
    derivation "ocl{mean(runs.indication)}"
  }
}
form LoadTest {
  applicability { accuracy_class: [A, B, C, D] }
  field test_loads : array {
    subform_ref LoadTestRow { }
  }
}
form HumidityTest {
  applicability { humidity_class: [CH] }
  field load_test_before : array {
    subform_ref LoadTestRow { applicability { phase: before } }
  }
  field load_test_after : array {
    subform_ref LoadTestRow { applicability { phase: after } }
  }
}

The LoadTestRow subform is composed into both LoadTest and HumidityTest , parameterized differently by each parent.

6. Parameter substitution

6.1. General

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.

6.2. Placeholder syntax

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.

6.3. Whole-string vs inline substitution

The substituter distinguishes two cases:

6.3.1. Whole-string substitution

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")

6.3.2. Inline substitution

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"

6.4. Missing parameters

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.

6.5. Parameter resolution order

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

6.5.1. Example: all three modes

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
      }
    }
  }
}

6.6. Recursive substitution

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 }}"
  }
}

6.7. Substitution scope

Substitution applies to:

  • Field property values (strings, numbers, booleans)
  • Nested object and array property values
  • Calculation binding values (if they reference parameters)

Substitution does NOT apply to:

  • Comments
  • Identifiers (field names, form IDs)
  • Keyword arguments outside of values

6.8. Build-time guarantee

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.

7. Composition rules

7.1. General

When a form references a subform via subform_ref , the subform is inlined into the parent. This section defines the rules that govern composition.

7.2. Order of operations

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.).

7.3. Override precedence

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:

  • applicability — intersection of parent and call-site values
  • name, label, description — call-site value if provided, else subform value

7.4. Cycle detection

If a subform references itself (directly or transitively), the parser rejects the model with an error:

subform-resolver: cycle detected: A → B → C → A

7.5. Idempotency

Composing the same subform into multiple parents produces independent inlined copies. They do not share state.

7.6. Type invariance

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.

7.7. Field-name uniqueness

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.

7.8. Reference resolution after composition

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.

7.9. Example: full composition

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.

8. Formal grammar

8.1. General

This section provides the formal grammar for forms, subforms, and related constructs using Extended Backus-Naur Form (EBNF).

8.2. Lexical conventions

  • <ID> — an identifier matching [a-zA-Z_][a-zA-Z0-9_]*
  • <STRING> — a double-quoted string literal
  • <NUMBER> — a numeric literal (integer or decimal)
  • { ... } — brace-delimited block
  • [ ... ] — optional element
  • * — zero or more repetitions
  • | — alternation

8.3. Top-level declarations

ModelElement = FormDecl | SubformDecl | SymbolDecl
              | CalculationDecl | StateMachineDecl
              | /* existing MMEL 0.1 constructs */ ;
FormDecl     = "form" ID "{" FormBody "}" ;
SubformDecl  = "subform" ID "{" SubformBody "}" ;

8.4. Form body

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 "}" ;

8.5. Field declaration

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 } ;

8.6. Subform

SubformBody  = { SubformProperty } ;
SubformProperty = "parameters" "{" { ParameterDecl } "}"
                 | "type" ( "object" | "array" )
                 | "description" STRING
                 | FieldDecl
                 | SubformRef ;
ParameterDecl = ID ":" TypeSpec "{" { ParameterProperty } "}" ;
ParameterProperty = "description" STRING
                   | "default" Value
                   | "mapping" "{" { ID ":" Value } "}" ;

8.7. Subform reference

SubformRef   = "subform_ref" ID "{" { SubformRefProperty } "}" ;
SubformRefProperty = "parameters" "{" { ID ":" Value } "}"
                   | "applicability" "{" { ApplicabilityEntry } "}" ;

8.8. Values

Value        = STRING | NUMBER | "true" | "false" | ID ;

8.9. Placeholder syntax (within STRING values)

Within any STRING value, the token ${{ ID }} is a parameter placeholder. The substitution pass replaces these before further processing.

Placeholder = "${{" ID "}}" ;
NOTE
the tokenizer treats ${{ ... }} as part of the enclosing string literal; the placeholder is recognized by the substitution pass, not by the tokenizer.