This chapter describes what the runtime evaluates and how each kind of check reports its result.
A constraint declared on a definition is checked against the object that carries it, so
instantiate the definition first if you want a verdict about a concrete value rather than a
default. When several objects the session holds carry it — two %instantiates of one name leave
the first object reachable as #<id>, and a multi-valued part holds one carrier per element — the
check names them (car.wheels[1], car.wheels[2], #1.wheels[1], …) and asks you to pick one,
with %eval in car.wheels[2] : ... or %eval in #1 : ....
Literals:
attribute x = 42; // Integer
attribute y = 3.14; // Real
attribute flag = true; // Boolean
attribute name = "System"; // String
Operators:
attribute sum = 10 + 5; // Arithmetic
attribute product = 3 * 7;
attribute comparison = x > 10; // Relational
attribute logic = flag and true; // Boolean
Feature References:
part def Wheel {
attribute diameter = 16.0;
}
part Vehicle {
part wheel : Wheel;
attribute wheelDiameter = wheel.diameter; // Feature chain
}
private import ScalarValues::*;
part def Engine {
attribute power : Real default = 200.0;
}
part def Car {
part engine : Engine {
:>> power = 250.0; // Redefine nested feature
}
}
Instantiate and inspect:
sysml> %instantiate Car
✓ Created instance of Car
ID: 1
Use %features Car to inspect
sysml> %features Car
Instance: Car (ID: 1)
Features:
engine = Instance(ID: 2)
power = 250.0
ownedPorts = []
…
ownedPorts = []
…
(The … stand for the library features every part carries, listed after the model's own;
see your first model.) The nested engine is an object of its
own. Reach it by a path from the object that holds it, or by the id it was given, and read a
value from it the same way:
sysml> %features Car.engine
Instance: Car.engine (ID: 2)
Features:
power = 250.0
…
sysml> %eval in #2 : power * 2
✓ power * 2 (on #2 ID: 2)
= 500.0
An element of a multi-valued part is picked by index counted from 1, System.wheels[3]; see
addressing an object.
part System {
part sensors : Sensor[0..10]; // 0 to 10 sensors
part wheels : Wheel[4]; // Exactly 4 wheels
}
A multi-valued feature is unique unless it says nonunique: no two of its values may be
equal, whatever its order. Repeats need the keyword.
package Readings {
private import ScalarValues::*;
attribute channels : Integer[*] ordered = (3, 1, 2); // fine, order kept
attribute levels : Integer[*] = (1, 1, 2); // error
attribute samples : Real[*] nonunique = (0.5, 0.5, 0.7); // fine, three values
}
readings.sysml:4:41: error: 1 (an Integer) is written at positions 1 and 2 of a unique feature
The check reports a repeat it can decide from the text; one that only appears when the model runs
— a value reaching a feature through another feature, an assign, a calc's argument or result —
is refused at that write with the same wording (uniqueness violation), and the feature keeps the
value it had. Nothing is dropped silently: a (1, 1, 2) is not quietly read as (1, 2). A
Collections::Set or Map is the exception, since throwing away repeats is what a set does.
Casts: x as T selects rather than converts. It yields x where T classifies the value x
is, and the empty sequence where it does not; a sequence is cast element by element, keeping the
elements T classifies in their order. An object is kept by every classifier it is an instance of,
its type's generalizations included.
A scalar is of the type it is written or computed in and of that type's supertypes, never of a
narrower one by the number it happens to hold: 4 is an Integer, so also a Rational, a Real
and a Number; 4.0 is a Rational and not an Integer, and so is 6 / 3, since dividing two
integers yields a Rational. x istype T, x hastype T, x @ T, x as T and a feature's type
all judge a value by that one rule, so 4.0 istype Integer is false, 4.0 as Integer is empty,
and attribute whole : Integer = 4 / 2 is refused (by the checker, where the value is a quotient;
at evaluation, where a whole Real reaches an Integer feature). hastype names the value's own
type alone: 4 hastype Rational is false while 4 istype Rational is true.
sysml> package Payload {
...> private import ScalarValues::*;
...> part def Instrument;
...> part def Camera :> Instrument;
...> part navCam : Camera;
...> part probe : Instrument;
...> ref part cameras : Camera[0..*] = (navCam, probe) as Camera;
...> attribute whole : Integer[0..*] = (1.0, 2, 3.0) as Integer;
...> attribute reals : Real[0..*] = (1.0, 2, 3.0) as Real;
...> }
✓ package Payload
sysml> %eval Payload::whole
✓ Payload::whole
= [2]
sysml> %eval Payload::reals
✓ Payload::reals
= [1.0, 2, 3.0]
sysml> %eval Payload::cameras
✓ Payload::cameras
= [Instance(ID: 1)]
sysml> %eval 2.5 as ScalarValues::Integer
✓ 2.5 as ScalarValues::Integer
= []
Declare a feature that holds a cast result [0..1] or [0..*]: a cast that selects nothing is
empty, which a feature of multiplicity [1] cannot hold. The checker warns where a cast can only
be empty because the operand's type and the target are unrelated.
Converting a number: the conversions are library functions, not casts. RationalFunctions::ToInteger
and RealFunctions::ToInteger truncate toward zero, so pair them with RealFunctions::round, floor
or ceiling where rounding is meant; IntegerFunctions::ToNatural refuses a negative integer.
Declare the feature Rational or Real instead where the quotient itself is the value wanted.
sysml> package Counts {
...> private import ScalarValues::*;
...> attribute quotient : Rational = 6 / 3;
...> attribute converted : Integer = RationalFunctions::ToInteger(6 / 3);
...> attribute rounded : Integer = RealFunctions::ToInteger(RealFunctions::round(7 / 2));
...> attribute count : Natural = IntegerFunctions::ToNatural(RationalFunctions::ToInteger(6 / 3));
...> }
✓ package Counts
sysml> %eval Counts::quotient
✓ Counts::quotient
= 2.0
sysml> %eval Counts::converted
✓ Counts::converted
= 2
sysml> %eval Counts::rounded
✓ Counts::rounded
= 4
A user-declared scalar type (attribute def Even :> Integer;) marks no evaluated value, so a bare
5 as Even cannot be decided by the value and is reported rather than answered empty; a value read
from a feature declared Even is kept, the declaration being what states it is one. Natural and
Positive are read the same way: their bounds refuse -1 and 0, and 7 as Natural on a bare
integer is reported, while n as Natural on attribute n : Natural = 7 keeps 7.
The unbounded value: * is a value of its own, not a large number. It exceeds every finite
number, equals itself and prints as *; arithmetic over it is refused with an error naming the
operator.
sysml> package Budget {
...> private import ScalarValues::*;
...> attribute passLimit : Natural = *;
...> attribute withinLimit : Boolean = 40 < passLimit;
...> }
✓ package Budget
sysml> %eval Budget::withinLimit
✓ Budget::withinLimit
= true
sysml> %eval * + 1
error: evaluation failed: type mismatch: operator '+' is not defined for the unbounded value '*': * + 1
Metadata: elem.metadata is the sequence of metadata annotating elem, one object per
annotation in the order written — an inline @ annotation and a metadata … about elem
usage declared elsewhere take their places by source position, across files in document
order — each carrying the values its body binds over the defaults its
metadata def declares — followed by one reflective metaobject of the element's own
metaclass (KerML §8.3.4.8.15), so the metadata of an element nothing annotates is that one
metaobject, not the empty sequence. The library types the sequence as Metaobject, so cast an
annotation to its metadata def before reading the values it binds; x meta T is the shorthand
for x.metadata as T, and the metaclass's own features (name, qualifiedName, owner,
isAbstract, …) read off the metaobject through ordinary member access.
sysml> package Provenance {
...> private import ScalarValues::*;
...> metadata def Heritage { attribute mission : String; attribute flown : Boolean default true; }
...> part def Camera;
...> part navCam : Camera { @Heritage { mission = "Cassini"; } }
...> part sciCam : Camera;
...> }
✓ package Provenance
sysml> %eval (Provenance::navCam.metadata#(1) as Provenance::Heritage).mission
✓ (Provenance::navCam.metadata#(1) as Provenance::Heritage).mission
= "Cassini"
sysml> %eval (Provenance::navCam.metadata#(1) as Provenance::Heritage).flown
✓ (Provenance::navCam.metadata#(1) as Provenance::Heritage).flown
= true
sysml> %eval Provenance::sciCam.metadata
✓ Provenance::sciCam.metadata
= [meta(Provenance::sciCam : SysML::Systems::PartUsage)]
sysml> %eval Provenance::sciCam.metadata#(1).name
✓ Provenance::sciCam.metadata#(1).name
= "sciCam"
Extents: all T (KerML's extent operator) is the ordered sequence of the instances of T
the run has — every object the definition classifies, nested usages included, in declaration
order; for an enumeration, its literals; for a variation, the variants it declares. It is the
run's extent, so a scalar or structured data type (all Integer, all Point) is refused, since
a run creates no data values to enumerate, and a filter or metadata value built on all T is
diagnosed rather than evaluated.
sysml> package Fleet {
...> private import ScalarValues::*;
...> part def Car { attribute seats : Integer; }
...> part sedan : Car { :>> seats = 5; }
...> part coupe : Car { :>> seats = 2; }
...> enum def Color { red; green; blue; }
...> }
✓ package Fleet
sysml> %eval (all Fleet::Car).seats
✓ (all Fleet::Car).seats
= [5, 2]
sysml> %eval all Fleet::Color
✓ all Fleet::Color
= [Color::red, Color::green, Color::blue]
sysml> %eval all Integer
error: evaluation failed: unbounded extent: Integer is a data type, whose values are not enumerated (only an enumeration's literals are)
A quantity is a magnitude with a unit, 3 [SI::km], and arithmetic over quantities composes
units: a product multiplies them, a quotient divides them, a power raises them. The result is
reported in the coherent unit the library declares for its dimension rather than the expression
it was composed by, and a prefix on an input folds into the magnitude — 3 [km] / 2 [s] is
1500.0 [SI::'m/s'], not 1.5 [km/s]. The reduction reads the library's MeasurementReferences
data and nothing else: a unit's unitPowerFactors, its unitConversion, and the prefixes of
SIPrefixes, so a DerivedUnit a model declares reduces the same way as one of SI's.
sysml> package Orbit {
...> private import SI::*;
...> private import ISQ::*;
...> private import MeasurementReferences::*;
...> attribute def GravParam :> Quantities::ScalarQuantityValue;
...> attribute 'm³⋅s⁻²' : DerivedUnit {
...> private attribute m3 : UnitPowerFactor[1] { :>> unit = SI::m; :>> exponent = 3; }
...> private attribute s_2 : UnitPowerFactor[1] { :>> unit = SI::s; :>> exponent = -2; }
...> attribute :>> unitPowerFactors = (m3, s_2);
...> }
...> calc def velocity { in a :> ISQ::acceleration; in t :> ISQ::time; return v :> ISQ::speed = a * t; }
...> calc def orbital { in mu : GravParam; in r :> ISQ::length; return v :> ISQ::speed = (mu / r)^(1/2); }
...> calc def perMass { in f :> ISQ::force; in m :> ISQ::mass; return a :> ISQ::acceleration = f / m; }
...> }
✓ package Orbit
sysml> %calc Orbit::velocity(9.80665 [SI::'m⋅s⁻²'], 311 [SI::s])
✓ Orbit::velocity(9.80665 [SI::'m⋅s⁻²'], 311 [SI::s])
= 3049.86815 [SI::'m/s']
standing: value (observed: 1 run under reverse)
sysml> %calc Orbit::orbital(3.986E14 [Orbit::'m³⋅s⁻²'], 6563 [SI::km])
✓ Orbit::orbital(3.986E14 [Orbit::'m³⋅s⁻²'], 6563 [SI::km])
= 7793.229127559948 [SI::'m/s']
standing: value (observed: 1 run under reverse)
sysml> %calc Orbit::perMass(10 [SI::N], 2 [SI::kg])
✓ Orbit::perMass(10 [SI::N], 2 [SI::kg])
= 5.0 [SI::'m⋅s⁻²']
standing: value (observed: 1 run under reverse)
Every verdict a check produces — a %calc value, a %constraint or %requirement pass — ends
with a standing: line naming the engine that answered and how strong the evidence is: here one
execution under the default reverse scheduling policy (Analysis
engines).
The unit chosen is the one the library declares for the dimension, SI::'m/s' over a synonym a
model declares. Where the library declares several units of one dimension that measure
different kinds of quantity — J for energy and N⋅m for torque — the type of the feature
the value is bound to decides: attribute work : EnergyValue = 3 [N] * 2 [m] reads
6 [SI::J], attribute torque : TorqueValue = 3 [N] * 2 [m] reads 6 [SI::'N⋅m'], and the
same product bound to no quantity kind reads over the base units, 6 [SI::'kg⋅m²⋅s⁻²']. A
value that reduces to dimension one is a plain number (6 [km] / 3 [km] is 2.0); a product
the library declares no unit for stays over its base units (2 [kg] * 3 [K] is 6 [K*kg]); one
over a dimension-one unit such as rad keeps that unit, since the reduction would lose it; and a
unit written as one name (3 [km], 400 [cm]) is kept as written. The magnitude changes only
by the exact scale factor of the reduction — never by the choice of spelling.
Writing a quantity to a feature typed by a quantity kind checks its reduced dimension, not its
spelling: 10 [N] / 2 [kg] is admitted to an AccelerationValue, and an L·T^-1 value written
to one is refused as a type mismatch naming both dimensions. See
Behavior for the same rule over assign. An argument is bound to the
parameter it fills, and that binding is judged like an explicit bind: sum(robots.mass)
handing MassValues to RealFunctions::sum, declared over Real, draws the validation warning
Bound features should have conforming types at the argument, as the SysML v2 pilot reports it.
Sets: the library declares the elements of a Collections::Set unique and unordered, so a
Set holds a set: the elements it was given with every repeat dropped and no order of its own.
Two sets holding the same elements are equal however they were written, and a set prints as
Set{…} in a canonical order. Bag and OrderedSet keep their repeats or their order, and are
sequences.
sysml> package Bands {
...> private import ScalarValues::*;
...> private import Collections::*;
...> private import CollectionFunctions::*;
...> attribute requested : Set { :>> elements = ("X", "Ka", "X", "S"); }
...> attribute licensed : Set { :>> elements = ("S", "X", "Ka"); }
...> attribute same : Boolean = requested == licensed;
...> }
✓ package Bands
sysml> %instantiate Bands::requested
✓ Created instance of Bands::requested
ID: 1
Use %features Bands::requested to inspect
sysml> %features Bands::requested
Instance: Bands::requested (ID: 1)
Features:
elements = Set{"Ka", "S", "X"}
sysml> %eval Bands::same
✓ Bands::same
= true
sysml> %eval CollectionFunctions::size(Bands::requested)
✓ CollectionFunctions::size(Bands::requested)
= 3
Tensors: a TensorQuantityValue of any rank is built by TensorCalculations::'[' from a flat
sequence of numbers and a TensorMeasurementReference whose dimensions give the shape, in
row-major order with the last index varying fastest. # takes one index per dimension, counted
from 1, and refuses an index outside the shape or the wrong number of them; +, - and the
scalar multiplications keep the shape component by component.
sysml> package Stress {
...> private import ScalarValues::*;
...> private import ISQ::*;
...> private import SI::*;
...> private import Quantities::*;
...> private import MeasurementReferences::*;
...> private import TensorCalculations::*;
...> attribute ref3 : TensorMeasurementReference {
...> :>> dimensions = (2, 2, 2);
...> :>> mRefs = (Pa, Pa, Pa, Pa, Pa, Pa, Pa, Pa);
...> }
...> attribute field : TensorQuantityValue = '['((1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0), ref3);
...> attribute cell = field#(2, 1, 1);
...> attribute rank = field.order;
...> }
✓ package Stress
sysml> %eval Stress::field
✓ Stress::field
= Tensor(2, 2, 2)[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0] [Pa]
sysml> %eval Stress::cell
✓ Stress::cell
= 5.0 [Pa]
sysml> %eval Stress::rank
✓ Stress::rank
= 3
Calculations:
sysml> calc distance {
...> in x;
...> in y;
...> x * x + y * y
...> }
✓ calc distance
sysml> %calc distance 3 4
✓ distance(3, 4)
= 25
standing: value (observed: 1 run under reverse)
Library functions:
The KerML function libraries (RealFunctions::sqrt, SequenceFunctions::size,
NumericalFunctions::sum, …) are ordinary library packages, and an expression reaches one of
their functions by the same rule the checker applies to every name: the qualified name resolves
anywhere, and the bare name resolves only where the model imports the package that declares it.
Evaluation follows the checker, so a call the checker reports as an unresolved reference does not
evaluate either; the error names the qualified spellings the call may have meant, and importing
one of those packages makes it resolve.
sysml> package Demo {
...> attribute wheels : ScalarValues::Integer[*] = (1, 2, 3, 4);
...> attribute wheelCount = wheels->size();
...> }
3:36: error: unresolved reference: size — did you mean SequenceFunctions::size or CollectionFunctions::size?
attribute wheelCount = wheels->size();
^~~~
sysml> %eval Demo::wheelCount
error: evaluation failed: unresolved reference: size — did you mean SequenceFunctions::size or CollectionFunctions::size?
sysml> %eval SequenceFunctions::size(Demo::wheels)
✓ SequenceFunctions::size(Demo::wheels)
= 4
sysml> package Demo {
...> private import SequenceFunctions::*;
...> attribute wheels : ScalarValues::Integer[*] = (1, 2, 3, 4);
...> attribute wheelCount = wheels->size();
...> }
✓ package Demo
note: added to the existing package Demo, replacing attribute wheels, attribute wheelCount
sysml> %eval Demo::wheelCount
✓ Demo::wheelCount
= 4
A calc the model declares under a library function's name is what a call resolves to, even where
the library is also imported. %builtins lists every function the build evaluates, each with the
package an import must name for its bare name to resolve.
Calculations as values:
A calc def, a calc usage or an in calc parameter named where a value is expected is a
function value: the calculation, together with whatever it closes over. It is passed as an
argument, held in a feature, compared with ==, and invoked by the parameter that receives it;
reading it on its own answers the function, named by its declaration.
sysml> package Gains {
...> private import ScalarValues::*;
...> calc def Square { in v : Real; return : Real = v * v; }
...> calc def Apply { in calc f { in v : Real; return : Real; } in a : Real; return : Real = f(a); }
...> }
✓ package Gains
sysml> %calc Gains::Apply(Gains::Square, 3.0)
✓ Gains::Apply(Gains::Square, 3.0)
= 9.0
standing: value (observed: 1 run under reverse)
sysml> %eval Gains::Square
✓ Gains::Square
= Gains::Square
A calc def is a definition, not a feature, so it is passed as an argument or referenced through
a calc usage rather than bound directly as a feature's value. A nested calc closes over the
features around it, and SampledFunctions::Sample from the analysis library takes a function value
and tabulates it over a domain.
Collection bodies:
collect, select and reduce (ControlFunctions) take a body whose parameter is bound to each
element in turn. A collect is typed by what its body returns, not by the element type of the
collection it ran over, so its result can be declared with the body's type and a mismatch is
reported before anything runs. A body parameter that declares no type ({ in i; i.mass }) takes
the type of the collection's elements, so a member it does not have is reported as an
unresolved member at validation, not left for the run to discover.
sysml> package Rollup {
...> private import ScalarValues::*;
...> private import ISQ::*;
...> private import SI::*;
...> private import ControlFunctions::*;
...> part def Instrument { attribute mass : MassValue; }
...> part navCam : Instrument { :>> mass = 4.0 [kg]; }
...> part spectrometer : Instrument { :>> mass = 12.0 [kg]; }
...> attribute masses : MassValue[0..*] = (navCam, spectrometer)->collect { in i : Instrument; i.mass };
...> attribute total : MassValue = masses->reduce { in a : MassValue; in b : MassValue; a + b };
...> }
✓ package Rollup
sysml> %eval Rollup::total
✓ Rollup::total
= 16.0 [kg]
sysml> package Rollup {
...> attribute names : String[0..*] = (navCam, spectrometer)->collect { in i : Instrument; i.mass };
...> }
1:35: error: cannot bind a value of type MassValue to a feature typed by String
attribute names : String[0..*] = (navCam, spectrometer)->collect { in i : Instrument; i.mass };
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
note: added to the existing package Rollup (its other members are kept)
Constraints:
sysml> constraint ValidSpeed {
...> 65 > 0 and 65 <= 120
...> }
✓ constraint ValidSpeed
sysml> %constraint ValidSpeed
✓ Constraint ValidSpeed passed
standing: holds (observed: 1 run under reverse)
Requirements:
sysml> requirement SafetyReq {
...> assume constraint { 65 > 0 }
...> require constraint { 100 > 50 }
...> }
✓ requirement SafetyReq
sysml> %requirement SafetyReq
✓ Requirement SafetyReq satisfied
standing: holds (observed: 1 run under reverse)
An object as a whole:
%constraint and %requirement answer one named condition. %validate <object> answers every
assertion about an object the session holds and the objects it holds in turn — each assert constraint the carrier's type declares or inherits, each requirement usage it carries, and each
satisfy assertion whose subject is in the tree — one verdict per assertion per object, then
one about the object itself. The object is an object reference:
the name it was instantiated under, an id, or a path into what it holds.
sysml> package Fleet {
...> part def Wheel {
...> attribute pressure default = 32.0;
...> assert constraint pressureOk { pressure >= 30.0 }
...> }
...> part def Car {
...> attribute mass = 1500.0;
...> part wheels : Wheel[2] {
...> attribute :>> pressure = 20.0;
...> }
...> assert constraint massOk { mass < 2000.0 }
...> requirement light { require constraint { mass < 1000.0 } }
...> }
...> part car : Car;
...> }
✓ package Fleet
sysml> %instantiate Fleet::car
✓ Created instance of Fleet::car
ID: 1
Use %features Fleet::car to inspect
sysml> %validate car
✓ assert constraint massOk holds (on Fleet::car ID: 1)
✗ requirement light fails (on Fleet::car ID: 1)
Required condition evaluated to false: mass < 1000.0
✗ assert constraint pressureOk fails (on Fleet::car.wheels[1] ID: 2)
Assertion evaluated to false: pressure >= 30.0
✗ assert constraint pressureOk fails (on Fleet::car.wheels[2] ID: 3)
Assertion evaluated to false: pressure >= 30.0
✗ Fleet::car is not valid: 3 of 4 assertions fail
standing: violated (witnessed: 1 run under reverse)
sysml> %validate car.wheels[1]
✗ assert constraint pressureOk fails (on Fleet::car.wheels[1] ID: 2)
Assertion evaluated to false: pressure >= 30.0
✗ Fleet::car.wheels[1] is not valid: 1 of 1 assertion fails
standing: violated (witnessed: 1 run under reverse)
The verdicts come root first, then each held object as the walk reaches it, and each names the
object it is about by the path from the one validated, a collection element by its position
(wheels[2], counted from 1). The object is valid only when every assertion holds and every
object it holds was reached: an assertion that could not be evaluated — a feature no value
reaches — is reported as undecided with the reason, not as false, and leaves the object undecided
rather than valid; so does a walk cut short by an object graph that goes on without end. An
object no assertion is about decides nothing — ? Demo::crate states no assertion to validate —
and is not shown valid either. A constraint declared without assert is a definition to check by
name, not an assertion about the object, and is not swept. The command line makes the same check with
-validate=<object>, and a script with the
ValidateInstance RPC (from Python or Go).
For more examples, see
examples/repl-behavioral-demo.sysml, and the
expressions demo for casts, *, .metadata, function
values, sets, tensors and collection bodies worked through one model.