Validation¶
longeron.validate() walks a model and returns
Diagnostic records. The same checks run on
the command line as longeron lint. This guide
documents every diagnostic code, how name resolution works, and the two
strict modes.
Validation never mutates the model. The standard library, when
consulted, is only visible to the resolver. library packages inside
the model itself (including a merged-in standard library) are treated
the same way: they are resolution context, never the subject of
diagnostics.
Scope: these checks are a curated set aimed at real modeling mistakes. They now include a corpus-calibrated selection of the clause-8.3 well-formedness constraints (the kind-level checks below), but they are not an implementation of the OCL constraints embedded in the OMG spec metamodel, and conformance claims keep those axes separate (the corpus badge measures parsing conformance). Rationale in the design doc: The OCL stance.
Reading a diagnostic¶
Each diagnostic prints as file:line:column: severity[code] element: message:
demo.sysml:3:5: error[duplicate-name] Demo::Wheel: name 'Wheel' is already used by another member of Demo
demo.sysml:5:9: warning[unresolved-reference] Demo::Vehicle::mass: typed by 'Reall' does not resolve
element is the qualified name of the subject element. The position
prefix is the subject’s declaration site, stamped by the builder while
parsing: the location field of Diagnostic
is a SourceLocation (line and column are
1-based) or None. Elements built programmatically or rebuilt from
JSON – including model cache hits –
carry no position, and their diagnostics print without the prefix
(longeron lint --no-cache re-parses to get positions back).
validate() returns diagnostics sorted errors-first, then by element
and code.
Severities draw one line: structural problems that make the model
self-contradictory are errors, and references that merely fail to
resolve are warnings. An unresolved reference is a warning because the
missing target may live in a file you did not load. When that excuse
does not apply – the model is complete and typos must not ship –
strict mode (strict=True / --strict)
promotes exactly the resolution-failure codes to errors.
The diagnostic codes¶
Code |
Severity |
Fires when |
|---|---|---|
|
error |
Two members of one namespace share a name or short name. |
|
error |
An element’s specialization hierarchy ( |
|
error |
A transition names a source or target that is not a state of its machine. |
|
warning |
A declared reference does not resolve: |
|
warning |
A succession end in an action body does not resolve: |
|
warning |
An |
|
warning |
A |
|
warning |
A flow’s declared payload typing has no specialization relationship – in either direction – with the target end’s declared typing: |
|
warning |
The leading name of an expression does not resolve. Locals, loop variables, accept payloads, builtin functions, and inherited members are recognized first. Qualified chains through packages and definitions are checked step-wise too ( |
|
warning |
A state machine declares states but no |
|
warning |
A calc has no result expression and no |
|
warning |
A |
|
warning |
|
|
error |
|
|
warning |
|
|
warning |
A scoreboard-convention |
|
warning |
Only under |
|
warning |
A |
|
warning |
Only under |
|
warning |
Only under |
|
error |
A declared type resolves to a definition of a conflicting kind, or to a package: |
|
error |
A metadata annotation ( |
|
error |
A composite occurrence feature (part, state, action, port, …) owned by an attribute definition or usage: |
|
error |
A composite (non- |
|
error |
An interface definition end typed by a non-port definition ( |
|
error |
A connector, interface, or binding end resolves to a definition or package rather than a feature: |
|
error |
A subsetting target resolves to a package or a definition: |
|
error |
A redefinition targets a sibling feature (same featuring type) or a package-level feature: |
|
error |
An attribute or enum definition specializes an occurrence definition: |
|
error |
A behavior-family definition (action, calc, state, constraint, requirement, case, …) specializes a structure-family or data definition: |
|
error |
A structure-family definition (part, item, port, connection, …) specializes a behavior-family or data definition. |
|
error |
A |
|
error |
A non-variant usage owned by a |
|
error |
More than one |
|
error |
More than one |
|
error |
More than one |
|
error |
A usage typed by more than one |
|
error |
An attribute typed by an enumeration definition carries more than one declared type: |
|
error |
An anonymous, unrouted |
|
error |
An |
|
error |
A |
|
error |
A literal multiplicity bound that is not a natural number: |
|
error |
A literal range whose lower bound exceeds its upper: |
Kind-level well-formedness¶
The error rows above from usage-type through multiplicity-bound-order
implement a corpus-calibrated selection of the SysML v2 clause-8.3
constraints (spec validate* names) and the pilot implementation’s
validator rules (SysMLValidator.xtend / KerMLValidator.xtend), under
one contract inherited from the dimensional lint: only speak when two
known things conflict. A check fires only when a reference resolves
and the resolved element’s kind is known to conflict – a
resolved-but-wrong-kind target is a structural self-contradiction and
therefore an error, while an unresolved reference stays a warning
(unresolved-reference), because the target may live in a file you did
not load. Kinds outside the check’s vocabulary – language-extension
definitions, keyword-less feature/ref usages – are bottom: no
guessing.
Every check was calibrated against the 309-file OMG corpus, and four deliberate deviations from the literal rule text keep the spec’s own models clean:
Items in attribute bodies are not judged. The rule says all features of an attribute definition must be non-composite, but the spec’s training models nest composite items there (
attribute def Show { item picture : Picture; }).Directed features are never composite-checked and there is no directed-parameter-placement check at all: the pilot’s corpus places directed features in part definitions and usages (
in item scene;), so SysML textual direction does not map to KerML ParameterMembership.Reference usages may name definitions.
satisfy R1 by x;names a requirement definition; the pilot mints a usage typed by it.Vendored-library kinds are bottom. The KerML libraries project
datatypeonto the item kind (Collections::Array), so kind judgments skip targets inside library packages.
One divergence runs the other way – longeron is deliberately
stricter than the reference, per a decision recorded in
the conformance design’s Decisions
section: multiplicity-bound-order rejects [3..1] as an error although
the pilot has no lower-vs-upper rule at all (its validator carries only
bound-type rules), so the pilot accepts the unsatisfiable range. The
divergence is intentional and pinned by the rejection suite, so a
future pilot differential adjudicates it as ours-only-by-design rather
than a bug.
Names resolve against the standard library¶
Unless disabled, unresolved names get a second chance against the
vendored standard library. A bare Real, with
no import at all, validates silently, because KerML grants standard
library packages implicit visibility from every namespace. A misspelled
Reall still warns.
The stdlib parameter (and --no-stdlib on the CLI) controls this
fallback:
Value |
Behavior |
|---|---|
|
Attach the library to the resolver when it loads. If it cannot load, degrade to resolution without it. |
|
Force the library. Raise if it cannot load. |
|
Never consult the library. Every library reference then warns. |
Implied specializations resolve inherited names¶
The SysML v2 specification requires every definition kind to specialize
a base element of the Systems Model Library, even when the model text
declares no specialization. The resolver honors these implied
specializations, so library members inherited through them resolve in
expressions. This is why start and done resolve inside a plain
action def: the action implicitly specializes Actions::Action, which
owns them.
The full map is
longeron.interpreter.IMPLIED_SPECIALIZATIONS. Representative
entries:
Declared kind |
Implied definition base |
Implied usage subsetting |
|---|---|---|
|
|
|
|
|
|
|
|
— |
|
|
|
|
|
|
The two strict modes¶
The CLI exposes two independent tightening flags; each is a parameter
of validate() in the Python API:
Flag |
API |
Effect |
|---|---|---|
|
|
Resolution failures become errors, and a bare |
|
|
Emit |
Strict mode promotes exactly the resolution-failure family – the
warning codes that report a reference which failed to resolve
(longeron.validation.RESOLUTION_CODES):
unresolved-reference, unresolved-name, unresolved-unit,
dangling-expose, dangling-flow, dangling-succession
Every other diagnostic keeps its severity: stdlib-implicit-name
reports a successful (implicit) resolution, and
flow-payload-mismatch and the dimensional-lint warnings report
conflicts between typings that did resolve – none are resolution
failures, so none promote. The longeron lint exit code is driven by
error count alone, in both modes: --strict fails exactly when a
resolution failure (or any default-mode error) exists, and
bare-import warns without failing.
What --strict means against OMG’s own files, measured on the pinned
309-file release corpus (each file loaded independently with the
vendored standard library attached – the same per-file protocol as
the corpus badge): 142 of 309 files carry at least one strict-mode
error (5 of them already carry default-mode errors), almost entirely
unresolved-reference from cross-file references a per-file load
cannot see; 0 of 309 files use a bare import – the pinned
release writes a visibility prefix on every import. Strict mode is a
single-model discipline: expect it to fail on files that reference
siblings you did not load.
Flow connectivity¶
flow of Payload from a.out to b.in stores its ends and payload as
plain text that nothing resolves at parse time, so a typo’d end or a
wrong payload type is invisible until something downstream silently
ignores the flow. Two diagnostics close that gap:
plant.sysml:11:9: warning[dangling-flow] P::Plant: flow source 'tank.nope' does not resolve
plant.sysml:12:9: warning[flow-payload-mismatch] P::Plant: payload 'Water' is incompatible with flow target 'engine.fuelIn' (accepts 'Fuel')
What carries the typing. The payload’s declared type (flow of Diesel ..., flow of x : Diesel ...) is checked against the target
end’s declared type: the target usage’s typed by, or – for messages
to a named accept action (action receiveIt accept hit : Pong;) –
the accept’s payload typing.
What counts as incompatible. Only provably unrelated types warn:
the payload and target types must have no specialization relationship
in either direction. A Diesel payload flowing into a Fuel port is
fine (Diesel :> Fuel); a declared Fuel payload into a Diesel port
is also silent, because the feature may well hold a conforming value at
runtime. Where typing is absent on either side – an untyped target
feature, a payload feature with no declared type, a payload that does
not resolve – the check stays silent rather than guess. Like the
dimensional lint, it only speaks when two known typings conflict.
The dimensional lint¶
The five unit diagnostics above implement the core tier of the
units design. The interpreter deliberately
evaluates 5.0 [SI::kg] + 30.0 [SI::min] to 35.0 – units are
annotations, and evaluation sees only floats – so the lint is where
that bug class gets caught:
warning[dimension-mismatch] P::Drone::nonsense: operands of '+' have different dimensions: 'kg' [kg] vs 'min' [s]
Where dimensions come from. longeron.units derives an
exponent-vector table from the vendored quantities library’s own
definitional algebra: the SystemOfUnits declaration seeds the basis
(m, kg, s, A, K, mol, cd), derived units evaluate their definitional
expressions (newton = kg*m/s^2) in unit space,
ConversionByPrefix / ConversionByConvention members inherit their
reference unit’s vector, and IntervalScale / the dB family seed the
scale tags. A user package shaped like the standard library – typed
units with definitional expressions or conversions – derives the same
way with no mapping table; longeron.units.register_unit() covers
anything derivation cannot reach.
What carries a dimension. A [unit] annotation on a value; an
attribute whose value expression has one (transitively, through
redefinition and subsetting chains); and quantity subsetting or typing
(attribute mass :> ISQ::mass, attribute d : LengthValue). Unknown
dimensions are bottom and propagate silently – a bare 35.0 could be
anything, so the lint only speaks when two known vectors conflict.
Scaling by a bare numeric literal keeps the known operand’s dimension
(2.0 * mass is still a mass).
Scales outrank dimensions. Every unit carries a scale tag:
linear (almost everything), offset (interval scales – °C, and
any unit an interval scale displays through), log (dB and
anything spelled dB..., oct, dec, Np). Mixing scales under
+/- is an error where a dimension mismatch is only a warning:
25 [°C] + 298.15 [K] is wrong by 273.15 no matter how temperature-
shaped both sides are. Convert explicitly (the [units] extra’s
longeron.units.convert()) or model in kelvin.
Validation stays static and float-free: the lint reads annotations and
declarations, never evaluates, and adds one pass over the same owned
expressions the name checks already walk. With stdlib=False the
derived table is limited to units the model itself declares, and
library references warn as unresolved-unit like every other library
name.
What validation does not do¶
Validation is static. It never evaluates expressions, so a constraint
that always fails validates cleanly. To evaluate constraints against an
instance, use
Interpreter.check or
longeron check. Type checking of expression
operands is also out of scope.