tools/referee/diff uses the pinned OMG pilot as an external referee for parsing, resolution and
validation. This document answers a different question: how far does the pinned pilot's
execution surface reach, and which of the behavior rows in
spec compliance can it adjudicate?
The answer is narrow, and deliberately stated as such. The pinned artifact evaluates expressions over a model's declarations. It does not execute actions, does not run state machines, and has no notion of a step, a token or a trace. Three of the four behavior areas are therefore out of its reach, and no amount of harness work changes that.
Pin: tag 2026-08, artifact jupyter-sysml-kernel 0.62.0 (scripts/pilot-pin.sh). Every
command below was run against the shaded jar that scripts/download-pilot-validator.sh
unpacks, at
build/pilot-validator/target/sysml-download/sysml/jupyter-sysml-kernel-0.62.0-all.jar.
| Behavior area | Verdict | Why |
|---|---|---|
| Expression evaluation | Can adjudicate — for model-level expressions | SysMLInteractive.eval (the %eval magic) evaluates literals, operators, library functions, calc invocations and feature default values, headlessly, deterministically. This is a genuine second opinion, and tools/referee/exec uses it |
| Action / token-flow execution | Cannot speak to it | No interpreter exists in the artifact. %eval refuses an action def or usage as a target |
| State-machine execution | Cannot speak to it | Same: no state execution, no transition firing, no trace output. %eval refuses a state def or an exhibited state |
Classifier behaviors (exhibit/perform) |
Cannot speak to it for execution; can corroborate the declared value of a performed action's out parameter |
%eval reads machine.p.n as the model-level value of the parameter's default expression, which is not an execution: no performer object exists, nothing is stepped |
| (cross-cutting) Scope of an expression in a behavior body | Can corroborate only | The pilot resolves names for a model-level evaluation of a declaration written in a behavior body. It says nothing about frames, shadowing or the values a running body writes — the substance of those rows |
The magics in the pinned jar:
$ unzip -Z1 build/pilot-validator/.../jupyter-sysml-kernel-0.62.0-all.jar \
| grep -i 'jupyter/kernel/magic/[A-Za-z]*\.class'
org/omg/sysml/jupyter/kernel/magic/Load.class
org/omg/sysml/jupyter/kernel/magic/Projects.class
org/omg/sysml/jupyter/kernel/magic/View.class
org/omg/sysml/jupyter/kernel/magic/Viz.class
org/omg/sysml/jupyter/kernel/magic/Show.class
org/omg/sysml/jupyter/kernel/magic/Publish.class
org/omg/sysml/jupyter/kernel/magic/Repo.class
org/omg/sysml/jupyter/kernel/magic/Eval.class
org/omg/sysml/jupyter/kernel/magic/Help.class
org/omg/sysml/jupyter/kernel/magic/MyMagicParser.class
org/omg/sysml/jupyter/kernel/magic/Export.class
org/omg/sysml/jupyter/kernel/magic/Listing.class
Eval is the only one that computes anything: Load/Projects/Repo/Publish talk to a
model repository, Viz/View/Show/Listing/Export render or serialize. The kernel's own
help text for it, verbatim from SysMLInteractiveHelp:
Usage: %eval [--target=<NAME>] <EXPR>
Print the results of evaluating <EXPR> on the target given by <NAME>, which must be fully qualified.
If a target is not given, then evaluate <EXPR> in global scope.
The org/omg/sysml/execution/ package in the jar contains exactly one thing —
execution/expressions/, an ExpressionEvaluator plus library function implementations
(SizeFunction, SelectFunction, SumFunction, the trig functions, …). There is no
interpreter, simulator, scheduler, token or trace class under org/omg/sysml/ at all; a
search for those names in the jar returns only Guava/ICU/Xtext infrastructure. Action and
state semantics are present in the artifact as a metamodel and as adapters
(ActionUsageImpl, StateUsageImpl, TransitionUsageImpl, ExhibitStateUsageAdapter,
plantuml/VStateMachine for drawing a machine) — representation and rendering, not
execution.
scripts/pilot-evaluator/EvalSysML.java drives SysMLInteractive directly — the same class
the kernel drives — with no notebook and no kernel protocol:
SysMLInteractive.createInstance(), loadLibrary(dir), process(modelText) per model, then
eval(expr, target, List.of()) per case. scripts/download-pilot-evaluator.sh compiles it
against the pinned jar and writes the launcher build/pilot-evaluator/eval-sysml. So the
expression surface is usable as a referee exactly the way the two validators are.
Output is plain text, one line per resulting value, each line a metamodel node kind, the value, and the element's UUID:
== case int
LiteralInteger 7 (fb98f88c-9172-45bc-8ed8-b78fe546719b)
== end int
== case seqattr
LiteralInteger 3 (938afbeb-0cea-48ec-9c99-b5eadc1ab9b9)
LiteralInteger 1 (390142ba-ef95-4e34-9f78-8381a269be31)
LiteralInteger 2 (1b8b3471-f721-4eab-a377-6d6da070fb36)
== end seqattr
(== case/== end are our driver's framing; the lines between them are the pilot's own
output, verbatim.) There is no machine-readable protocol for eval — no JSON, no exit
code per case. A sequence is a run of value lines in order; an empty result is zero lines,
which is the same rendering an unevaluable expression gets (1 / 0 produces no lines and no
diagnostic). Diagnostics come back as ERROR:… / WARNING:… lines in the same stream.
An expression the evaluator cannot reduce comes back as the unevaluated node, which is how quantities appear:
== case quant
OperatorExpression [ (3009c3ad-7f3b-4665-a111-d99b4e256305)
== end quant
== case quantcalc
OperatorExpression + (e2642709-e92b-484f-bb0c-03331ad7ca9e)
== end quantcalc
Probe::q is 3.0 [SI::kg] and Probe::Quant() is 3.0 [SI::kg] + 1.0 [SI::kg]; OpenSysML
answers 3.00 [SI::kg] and 4.00 [SI::kg]. The pilot is not disagreeing — it is not
evaluating. Unit-carrying values are therefore out of the referee's reach, and
tools/referee/exec buckets them pilot-unevaluated rather than as a disagreement.
It is deterministic: two runs of the same cases in separate JVMs differ only in the UUIDs.
$ diff <(sed -E 's/\([0-9a-f-]{36}\)//' run1.txt) <(sed -E 's/\([0-9a-f-]{36}\)//' run2.txt)
IDENTICAL
A model is processed as text into an accumulating session, as a notebook cell is. A model
with any error registers nothing: while Probe had one unresolved function name, every
later case failed with Couldn't resolve reference to Element 'Probe::n' — a whole-file
verdict, not a per-case one. The harness reports the model block so this cannot be mistaken
for a semantic disagreement.
A --target must be a namespace. A package works; a part, an action, a state or a feature
does not:
== case action-def (target -, expr Behave::Bump)
ERROR:Must be a valid feature
== case action-usage (target -, expr Behave::Flow::a)
ERROR:Must be an accessible feature (use dot notation for nesting)
== case state-def (target -, expr Behave::Modes)
ERROR:Must be a valid feature
== case exhibit (target -, expr Behave::machine::s)
ERROR:Must be an accessible feature (use dot notation for nesting)
== case perform (target -, expr Behave::machine::p)
ERROR:Must be an accessible feature (use dot notation for nesting)
== case part-attr (target Behave::cc, expr count)
ERROR:Must be an accessible feature (use dot notation for nesting)
Naming a behavior is how one would ask for its execution, and that is precisely what is refused. Nothing in the surface takes a behavior and runs it.
Feature values are readable through dot notation, and this is the corroboration the surface does offer:
== case dot-part-attr (expr Behave::cc.count)
LiteralInteger 0 (d36e4833-0c8d-480c-b7e4-3de1b0f94d6d)
== case dot-perform-out (expr Behave::machine.p.n)
LiteralInteger 1 (1a672f09-0b90-4137-a358-c8f5ea36164d)
Behave::Bump declares out n : Integer = c.count + 1, so 1 is the default expression
evaluated over declarations — no object was materialized and no action ran.
- Action (all rows), State Machine (all rows), Classifier Behaviors (execution rows). Out of reach. The two named deliberate deviations — concurrent fork branches writing a shared feature space in step order, and variant selection not being ordering-sensitive — cannot be adjudicated by the pinned artifact, because ordering is a property of an execution the artifact never performs. Their status stays self-assessed against our golden traces; this document is not evidence for or against them.
- Scope of an expression in a behavior body. Corroboration only, and only for the model-level slice: whether a name written in a behavior body resolves at all, and what a declaration's default expression evaluates to. Frames, shadowing and values written by a running body are unobservable to the pilot.
- Expression Evaluation rows. Genuinely adjudicable, within the limits below; this is what
tools/referee/execcompares.
No compliance row's status flag is changed on the strength of this work.
- Reals are compared to two decimal places, a tolerance of the harness itself (both sides
are rounded in
tools/referee/exec,roundedReal). A divergence below 2dp is invisible to this harness. It is no longer a display limit — we now print1.0 / 3.0as0.3333333333333333, the same digits the pilot reports asLiteralRational, so the tolerance can be tightened on its own once the buckets it moves are adjudicated. - Integer vs Rational is reported, never normalized away.
2 ** 40gives the pilotLiteralRational 1.099511627776E12and gives us1099511627776; the harness buckets thatkind-only, notagree. - Quantities are out of reach (see above), as is anything else the pilot returns as an unevaluated node.
- Silence is its own state, and it is ambiguous. The pilot prints nothing at all both for
an expression that legitimately yields the empty sequence and for one it silently gives up on
(
1 / 0produces no lines and no diagnostic), so it cannot distinguish "no value" from "declined to evaluate". Those cases bucketpilot-silentrather than being read as agreement with our empty sequence or as our error. - An undetermined answer is its own bucket. A model-level read the model leaves open — a
feature with no value, a
[1..*]count — is<undetermined>on our side, a value and not an error; the pilot has no such answer, so those cases bucketours-undeterminedwhatever the pilot printed, and the pilot's line is recorded for adjudication rather than read as a disagreement. - Collection order is compared as order; a same-multiset/different-order result is its own bucket, so a real ordering difference is never hidden by sorting.
- Scalar vs one-element sequence is unobservable. We print
[2]where the pilot prints a single value line, and its rendering has no way to distinguish the two (in the notation every value is a sequence), so single-element sequences are unwrapped on both sides. A genuine scalar/singleton difference, if one exists, would be invisible here. - UUID identity is the only thing normalized away on the pilot side.
Run it with go run -C tools ./cmd/pilot-exec-diff after ./scripts/download-pilot-evaluator.sh; with the
execution artifact absent it prints a provisioning instruction, exits 0 and writes nothing, so
tools/referee/diff and its committed baseline are untouched. The bucket counts below are as measured
when this record was last updated and are not the current baseline — go run -C tools ./cmd/pilot-exec-diff
prints the current ones. State of the 446 committed cases, the original 32, the 62 the
expression round added (one of them, intdiv, since moved to integer_quotient.cases), the 14 of
value_classification.cases, the 3 of contextual_names.cases, the 14 of rational_terms.cases,
the 5 the empty-aggregate and subsetting round added to w6d_expr_depth.cases the 12 of
tensor_quantities.cases, the 9 of coordinate_frames.cases, the 7 of cast_expressions.cases,
the 27 of scalar_classification.cases, the 24 of literal_types.cases, the 23 of
enumeration_classification.cases, the 24 of metadata_access.cases, the 13 of
extent_expressions.cases, the 1 of extent_count_mismatch.cases, the 166 of
undetermined_operands.cases, the 4 of unknown_bounds.cases, the 2 of vast_bounds.cases and
the 4 of body_local_conformance.cases:
agree: 211 · kind-only: 1 · order-only: 0 · disagree: 30
pilot-unevaluated: 124 · pilot-silent: 21 · pilot-error: 9 · ours-error: 10 · ours-undetermined: 28
both-error: 12 · nondeterministic: 0
The first six extent_expressions.cases probe all T (KerML 1.0 §7.4.9.2, §8.2.5.8.1
ExtentExpression, BaseFunctions::'all'), added with the evaluation they were meant to referee
and could not: the pilot does not evaluate the operator, as Table 5 of §8.2.5.8.1 foretells by
marking all not model-level evaluable. The four extents — all Size
over an enumeration, all Wheel and all Car over definitions a package-level part car : Car
instantiates, all Boat over one nothing instantiates — come back as the unevaluated
OperatorExpression all and land in pilot-unevaluated, where we answer the three literals, the
two wheels car holds, car, and the empty sequence. The two counts are the disagree cases
extent-variation-count and extent-uninstantiated-count: size(all Gearbox) over a variation
with two variants and size(all Boat) over a definition with no instance both answer 1 from the
pilot, against our 2 and 0. The 1 is the size of the one unevaluated node size was handed,
not a count of instances — no reading of the extent gives a type with no instances the same size
as one with two — so neither is a verdict against us. The extent is taken over the whole loaded
model — every document's namespace-level object usages, the standard library's included — and
not over the namespaces enclosing the expression, which leaves these six where they were: the
referee's model is one package in one document, the library declares no Wheel, Car or Boat,
and the pilot returns the same unevaluated node whatever the extent's reach. The extent semantics
are self-assessed in the extent row of spec-compliance.md.
The other seven extent_expressions.cases and the one extent_count_mismatch.cases referee what
a namespace-level object usage of several occurrences denotes (KerML 1.0 §7.3.4.3 Multiplicities —
a feature of multiplicity [2] has exactly two values): part rims : Rim[2]; in a package, read
through size(all Rim), size(rims), rims#(1).radius and rims.radius, beside part hubs : Hub[1..*];, part spares : Spare[0..*];, a valued part rods : Rod[2] = (new Rod(), new Rod());
and, in a model of its own, a part uneven : Rod[2] whose value yields three — of its own because
the extent is taken over the whole loaded model, so a usage whose value the runtime refuses to bind
ends every all T that may reach it, size(all Rim) included, with that typed error. We answer 2, 2, 2.0, [2.0, 2.0], 1, 0, 2 and the typed
ErrMultiplicityViolation, the usage's lower bound of objects materialized once for the run, in
declaration order. The pilot materializes no object, so every one of its answers is a fold over
the unevaluated usage element rather than a count of instances, and no bucket here is a verdict:
extent-namespace-collection-count, -collection-read and -lower-bound-zero are disagree
because size over the one unevaluated node is 1 whether the usage is [2] or [0..*] — the
same fold as extent-uninstantiated-count and size-slots — and extent-namespace-lower-bound-one
agrees only because a [1..*] usage's lower bound happens to be that 1; -collection-index
agrees because the pilot folds rims#(1).radius to Rim::radius's default 2.0 as it folds any
chain through a usage, and -collection-chain is disagree because it folds rims.radius to the
same one 2.0 where two rims give two; -valued-collection agrees because a constructor
expression per value is countable without an object (size of the two new Rod() is 2 on both
sides); and -valued-count-mismatch is ours-error, the pilot answering 3 for a [2] feature
given three values where the runtime refuses the binding as the multiplicity violation it is
(§7.3.4.3). Read the eight with the six before them: the pilot evaluates neither all nor a
namespace usage's objects, so the semantics stay self-assessed in the extent row of
spec-compliance.md.
The run is deterministic: two runs into separate output directories differ only in the pilot's element UUIDs, and agree line for line once those are stripped.
The fourteen metadata_access.cases referee x.metadata and the meta cast, added with the
evaluation they referee, and all fourteen agree. They were run before the runtime changed, to
settle what .metadata answers: KerML 1.0 §8.3.4.8.15 states that the result of a
MetadataAccessExpression is the metadata annotations of the referenced element followed by one
reflective metaobject of the element's own metaclass, and the pilot (0.61.0) reads it the same
way — seatBelt.metadata->size() on a part annotated once is 2 and chassis.metadata->size()
on a part nothing annotates is 1, where the runtime then answered 1 and 0 (the two
disagree of that run; every meta case was ours-error, the operator refused). The pilot and
the spec text agree, so the runtime now appends the reflective metaobject and the two committed
.metadata conformance fixtures moved with it: metadata_access_annotations expects the
two annotation objects then meta(test::seatBelt : SysML::Systems::PartUsage) (and the metaobject
alone for the element nothing annotates, where it expected ()), and
metadata_access_textual_order expects its four annotations then the metaobject. The meta
cases fix the cast as x.metadata as T (§7.4.9.2): (seatBelt meta KerML::Feature)->size() is
1 (the metaobject alone — a Safety annotation is not a Feature), (seatBelt meta Safety)->size() 1 with .level 4, (chassis meta SysML::PartDefinition)->size() 0 for a
part usage and (chassis meta SysML::PartUsage)->size() 1; the reflective features read
alike on both sides — .name "seatBelt", .declaredName "chassis", .qualifiedName
"Meta::chassis", .ownedFeature->size() 2 (the nested part and the attribute),
(Vehicle meta SysML::PartDefinition).declaredName "Vehicle", .isAbstract false — and
(chassis meta KerML::Feature) === (chassis meta KerML::Type) is true on both, so a
metaobject's identity is the element's whatever metaclass it is cast to. Each case reads a
model-level attribute bound to the expression because the pilot resolves no library name
(KerML::Feature, SysML::PartUsage) inside a bare %eval.
Five more metadata_access.cases referee Element::documentation, declared
Documentation[0..*] in KerML.kerml, so a documentation comment reads back as a metaobject
whose own Comment::body and Comment::locale are the strings. Three agree:
(Wheel meta KerML::Element).documentation->size() is 1 for a part definition with one
doc comment and 0 for one without, and .documentation.owner.declaredName is "Wheel".
.documentation.body is the one new disagree, and it is a rendering artefact of the pilot,
not a semantic difference: the pilot prints LiteralString Turns. (<uuid>), a body with a
trailing space before its two-space id separator, which normalizes to Turns. against the
runtime's "Turns.": the pilot keeps the blank before */, the runtime reads the body the way
Element::documentation and LSP hover always have (source.CommentBody, delimiters and
margin off), so the runtime's answer stands and the referee's normalizer is left honest rather
than taught to trim. .documentation.qualifiedName is pilot-silent: the pilot prints nothing
for it, and the runtime answers (), which is what Element::qualifiedName derives to for an
element that declares no name (KerML 1.0 §8.3.2.1 Elements — a doc comment names nothing);
the empty pilot line and the empty sequence are the same reading, but the referee cannot tell
the pilot's "no value" from "declined", so the bucket is left as measured.
Five more metadata_access.cases referee the two KerML declarations a SysML model holds that
are neither types nor features — a dependency and a textual representation (rep … language "Java" /* … */) — which .metadata classifies as KerML::Dependency and
KerML::TextualRepresentation. Four agree: relies.metadata->size() is 1 (the metaobject
alone) and (relies meta KerML::Dependency)->size() 1, .client.declaredName is
"seatBelt" (Dependency::client redefines Relationship::source, the from side), and
chassis::asJava.metadata->size() is 1. .representedElement.declaredName is the ninth
disagree, adjudicated ours: KerML.kerml declares TextualRepresentation::representedElement : Element[1..1] subsets owner redefines annotatedElement (KerML 1.0 §8.3.2.2.12, the element the
representation is of, always its owner), so the runtime answers "chassis", the owning part;
the pilot answers "asJava", the representation's own name — it reads representedElement as
the representation itself, while its .owner.declaredName is "chassis" and its
.documentation.annotatedElement prints nothing, so its annotatedElement redefinitions do
not derive to the owner the library says they subset. The case reads .representedElement
rather than .language because language is a keyword to the pilot's expression parser (no viable alternative at input 'language'), which would fail the whole model.
The 166 undetermined_operands.cases probe model-level evaluation over an unbound feature
(attribute u;, no type, no value; beside it s : String, b : Boolean, r : Real,
xs : Real[2..4], ss : String[2..*], bs : Boolean[2] and os : Real[0..4], typed and
valueless) and over usages whose multiplicity leaves the count open (slots[3], gear[1..*],
loose[0..2], lone, many[10001..*], fixed :> gear, vacant[0] and tagged :> gear,
redefining the tag of D its default "d" gives; in store, sub[10001..*] :> base[0..*],
three[3..*] :> cap[0..3], and inner[2] and outer[5..*] both subsetting pool[0..*]), added
with the undetermined result they referee. Of the first 37, fifteen agree: the Boolean forms a constant operand fixes answer on both sides whichever
operand is the constant — false and (u > 3), true or (u == 1) and false implies (u == 1)
never read the second operand, and (u > 3) and false, (u == 1) or true and (u == 1) implies true answer false, true, true on both sides (see Boolean folding on the second operand
below) — as do includes((1, u + 1), 1) true and excludes((1, u + 1), 1) false, decided by
the known element; size(u) 1, size((1, u)) 2 and notEmpty(u) true, decided by the
assumed [1] of an unbound feature (KerML 1.1 §7.3.4.1); size(rack.lone) 1; size(Mode::ON)
1; and notEmpty(rack.gear) true and isEmpty(rack.gear) false, decided by the lower bound
1. Six are pilot-unevaluated: u, u + 5, (u + 5) * 2, u > 3, if u > 0 ? 1 else 2 and
(10, 20, 30)#(u) come back as AttributeUsage u, OperatorExpression +/*/>/if and
IndexExpression # — the pilot does not evaluate a reference to a feature with no value, and
leaves every expression over it unevaluated — where we answer <undetermined>. Three are
pilot-silent: -u, rack.slots#(2) and rack.gear#(7) draw no line at all; we answer
<undetermined>, the instance slots holds at position 2, and <undetermined>. The one
disagree is size(rack.slots): 3 here, since [3] fixes the count, 1 from the pilot,
which counts the one unevaluated PartUsage slots — a count of the operand expression, not of
the feature's values.
Twelve are ours-undetermined, and the pilot's lines for them are not answers but artifacts
of the same non-evaluation: wherever the unevaluated AttributeUsage u reaches an operator that
does evaluate, the pilot compares the usage element — u == 5 is false, not (u == 5)
true, (u > 3) and true false (the unevaluated u > 3 is not the literal true), (u == 1) or false false, (u == 1) implies false true, includes((1, 2), u + 1) false and
excludes((1, 2), u + 1) true; and where a [1..*] or [0..2] usage is counted it counts the
one unevaluated PartUsage — size(rack.gear) 1, size(rack.loose) 1, size((rack.gear, rack.loose)) 2, isEmpty(rack.loose) false, notEmpty(rack.loose) true. The pilot
materializes nothing, so these are evidence that no definite answer exists, not a source for the
numbers: gear[1..*] has at least one value and loose[0..2] may be empty, and the model fixes
nothing further, so each is <undetermined> here (adjudicated in
spec-compliance.md, Expression evaluation, and
docs/reference/repl-commands.md).
The next eight probe what an open collection still decides. many[10001..*] has a lower bound
too large to materialize into objects, which is never attempted at model level: size(rack.many)
is ours-undetermined (the pilot's 1 is again the count of the unevaluated PartUsage) and
notEmpty(rack.many) agrees on true. fixed :> gear is a value gear certainly holds:
includes(rack.gear, rack.fixed) answers true here and is the second disagree, the pilot
answering false by comparing the two unevaluated PartUsage elements, which are distinct
elements whatever values they hold — an artifact of non-evaluation like size(rack.slots), not a
verdict; includes(rack.gear, rack.lone) is ours-undetermined, lone being neither certainly
held nor excluded. The quantifiers decide from the elements a collection certainly holds and the
pilot agrees on all three decided forms: (1, u)->exists{in x; x == 1} is true on the witness
1, (1, u)->forAll{in x; x > 2} is false on the counterexample 1, anyTrue((true, u == 1))
is true; (1, u)->exists{in x; x == 2} is ours-undetermined, since u may be 2.
The next six probe what the elements two sequences certainly hold decide, and the pilot agrees
on every decided form: includes((1), (2, u)) is false, the determined (1) lacking the 2
the second certainly holds, and excludes((1), (1, u)) is false, both certainly holding 1;
select and collect over (1, u) apply their body to the 1 and keep what it yields, so
includes((1, u)->select{in x; x == 1}, 1) and includes((1, u)->collect{in x; x + 1}, 2) are
true, while size((1, u)->select{in x; x == 1}) is ours-undetermined — the selection holds
the 1 and possibly u, one or two values (the pilot's 1 is once more a count over the
unevaluated usage). ()#(u) is pilot-unevaluated (IndexExpression #); here it is the index
error ()#(1) is, since no index reaches into a sequence certainly empty.
The last eight probe the counts an open value still fixes. A mapping counts what its body
yields per element the collection may hold beyond those it certainly does, so a body of fixed
count keeps the collection's own bounds: size((1, u)->collect{in x; x + 1}) is 2 and agrees
(the pilot mapping both elements, the second to an unevaluated u + 1), while size(rack.loose->collect{in x; x}) is ours-undetermined, [0..2] like loose itself. vacant[0] declares a feature that
holds nothing, a count the model fixes: rack.vacant reads as the empty sequence
(pilot-unevaluated, the pilot returning the PartUsage), and size(rack.vacant) 0 and
isEmpty(rack.vacant) true are two more disagree, the pilot's 1 and false counting the
unevaluated usage as before. A conditional over an open test holds as many values as its
branches declare: size(if u > 0 ? 1 else 2) is 1 and agrees, size(if u > 0 ? (1, 2) else (3, 4)) is 2 and the last disagree (the pilot's 1 counts the unevaluated if), and
size(if u > 0 ? (1, 2) else 3) is ours-undetermined, the branches fixing [1..2] between
them.
The last eight probe what a determined operand alone decides. ?? over an operand that may
be empty yields either that operand, then holding at least one value, or the fallback, so its
count covers both: size(u ?? 3) is 1 and notEmpty(rack.loose ?? 3) is true, and both
agree (the pilot's 1 is once more the count of the unevaluated usage, its true the
non-emptiness of that one element). A test that does not depend on the element decides a
quantifier over a collection certainly holding one: rack.gear->exists{in x; true} is true
and rack.gear->forAll{in x; false} is false, and both agree; rack.loose->exists{in x; true} is ours-undetermined, loose possibly holding nothing, where the pilot's true
quantifies over the unevaluated PartUsage. A zero divisor fails a division whatever the open
operand holds: u / 0 and u % 0 are the division by zero error here and
pilot-unevaluated (OperatorExpression /, %), as is u / 2, which stays <undetermined>.
The last twelve probe the positions a library function checks before it reads its open
operand. A determined position that names no place in any value the operand may hold fails
the function: Substring(s, 0, 2), includingAt(xs, 1.0, 0), subsequence(xs, 0, 1) and
excludingAt(xs, 0) are the index error here, as are includingAt(xs, 1.0, 6),
subsequence(xs, 2, 5) and excludingAt(xs, 5), positions past the most xs : Real[2..4]
admits. The pilot leaves the four zero-index forms unevaluated (pilot-unevaluated, the
InvocationExpression or the unevaluated AttributeUsage xs) and throws
IndexOutOfBoundsException for the three past-the-end forms (both-error) — by indexing into
the one unevaluated usage element, so subsequence(xs, 1, 2) throws too (pilot-error,
toIndex = 2 over a one-element list) where it is <undetermined> here, as are
Substring(s, 1, 2) and includingAt(xs, 1.0, 1) (pilot-unevaluated) and
excludingAt(xs, 1) (pilot-silent), positions every value of the operand admits.
Substring(s, 3, 2) selects nothing whatever s holds and is "" (pilot-unevaluated).
The next four probe the domain a scalar numeric function's parameter puts on a determined
argument before the open one is read, all four pilot-unevaluated (the InvocationExpression
itself): IntegerFunctions::max(1.5, u) is the type mismatch a Real is against an Integer
parameter, RationalFunctions::rat(u, 0) the division by zero a zero denominator is
whatever the numerator, RationalFunctions::gcd(1.5, u) the domain error a fraction is; and
IntegerFunctions::max(1, u), whose determined argument conforms, is <undetermined>.
The next five probe a filter whose test does not depend on the element over a collection
the model leaves open: the test decides as a whole for the values the collection may hold beyond
those it certainly does. notEmpty(rack.gear->select{in x; true}) is true and
isEmpty(rack.gear->reject{in x; true}) is true, and both agree (the pilot filtering the one
unevaluated PartUsage); xs->reject{in x; true} is the empty sequence here and
pilot-silent; size(xs->select{in x; true}) is ours-undetermined of [2..4], the count of
xs itself; and xs->select{in x; 1 / 0 > 0} is the division by zero error here, the test
failing on any element, and pilot-silent.
The next nineteen probe an open operand whose feature declares a type: an operator or library
function that admits no value of that type is the type mismatch it is for a determined value of
it, before anything is left open. s - 1, s > 1, if s ? 1 else 2, (10, 20, 30)#(s),
s < r, b - 1, StringFunctions::Length(r) and RealFunctions::'-'(s, 1) are that error
here and pilot-unevaluated (the pilot returning the expression itself), -s is that error
and pilot-silent; not s and s and true are the two ours-error, where the pilot answers
true and false by comparing the unevaluated AttributeUsage s to a Boolean literal — the
artifact of non-evaluation seen above, not a reading of s as a Boolean. Operations the type
admits stay <undetermined> and pilot-unevaluated: s + "a", s < "a", r - 1,
if b ? 1 else 2, (10, 20, 30)#(r), and not b (ours-undetermined, the pilot's true
once more comparing the usage element). The constant operand still folds whatever the first
declares: false and s and b and false are false on both sides.
Thirteen probe a feature chain through a collection the model leaves open, which
reads the members of the values it certainly holds. rack.fixed.tag is "d", the default
D declares, and rack.tagged.tag "x", the redefinition, both agreeing. rack.gear.tag
is <undetermined> of [2..*] here, holding those two and the tag of whatever else gear
holds; the pilot answers the one default "d", reading the declaration's default through the
one unevaluated PartUsage gear as if it were gear's single value, so its every answer over
the chain is that of the sequence ("d"): includes(rack.gear.tag, "d") true and
excludes(rack.gear.tag, "d") false agree, decided here by the default fixed certainly
contributes; includes(rack.gear.tag, "x") is true here on tagged and rack.gear.tag->exists{in x; x == "x"} true on the same witness, two more disagree against the pilot's false — it
never sees the subsetter, as includes(rack.gear, rack.fixed) showed; and
includes(rack.gear.tag, "zz") and size(rack.gear.tag) are ours-undetermined against the
pilot's false and 1, gear possibly holding a D tagged "zz". rack.gear.tag->forAll{in x; x == "d"} is false on the counterexample tagged and agrees. rack.gear.mass and
rack.loose.tag, members no value fixes, are <undetermined> (pilot-unevaluated), and
rack.gear.nope is the unresolved member on both sides (both-error).
The next five probe the named conditional ControlFunctions::'if', which checks that an open
test may be Boolean before it stays open, as the if ? else operator does: 'if'(r, 1, 2)
and 'if'(s, 1, 2) are the type mismatch here, 'if'(bs, 1, 2) the multiplicity violation a
Boolean[2] test is against the one-valued parameter, and 'if'(b, 1, 2) and 'if'(u, 1, 2)
stay <undetermined>; all five are pilot-unevaluated (the InvocationExpression if).
The next twenty probe an open operand that certainly holds several values, which is no scalar:
the operators and functions taking exactly one value refuse it as they refuse a determined
sequence. xs + 1, xs > 1, (10, 20)#(xs), ss + "a" and if bs ? 1 else 2 are the type
mismatch here and pilot-unevaluated, -xs that error and pilot-silent; bs and true and
not bs are two more ours-error, the pilot's false and true comparing the unevaluated
AttributeUsage bs to a literal as with s. The library forms RealFunctions::'+'(xs, 1.0),
RealFunctions::abs(xs), StringFunctions::Length(ss) and StringFunctions::Substring(ss, 1, 1) are the multiplicity violation their one-valued parameter states, all pilot-unevaluated.
What takes a collection or compares whole values still takes it: SequenceFunctions::head(xs)
and xs ?? 3 are <undetermined> and pilot-unevaluated, and xs == 1.0, size(xs) and
includes(xs, 1.0) are ours-undetermined against the pilot's false, 1 and false over
the one unevaluated usage. An operand that may hold a single value is still a scalar the
operation may take: os + 1 and (10, 20)#(os) are <undetermined> and pilot-unevaluated,
-os <undetermined> and pilot-silent.
Ten probe a model-level read of a collection whose subsetters the model leaves open,
which reads them as it reads the collection: an open subsetter is not made up to its lower
bound, and contributes the fewest values it holds rather than made-up members. store.base,
store.sub and store.base.mass are <undetermined> of [10001..*] here without an object
made, and pilot-unevaluated; notEmpty(store.base) is true from that bound and agrees,
size(store.base) is ours-undetermined against the pilot's count of the one unevaluated
usage. three[3..*] fills cap[0..3] to its upper bound, so size(store.cap) is 3 here,
another disagree against the pilot's 1, while store.cap itself stays <undetermined>
(pilot-unevaluated), its members unknown. pool certainly holds the two objects of
inner[2] and at least the five of outer[5..*]: notEmpty(store.pool) is true and agrees,
includes(store.pool, store.inner#(1)) is true here and the last disagree (the pilot's
false compares unevaluated usages, as with rack.fixed), and store.pool#(1) is
<undetermined> and pilot-unevaluated, membership fixing no position.
The 4 unknown_bounds.cases probe usages whose bounds name a feature the model gives no
value: a : Real[n] and an : Real[1..n] over a valueless n : Natural. The pilot rejects
the model itself — Must have a Natural value on both declarations — and then resolves no name
in it, so all four are pilot-error (they are kept to their own model so the rejection reaches
no other case). A bound the model does not evaluate fixes no count either, so a, size(a)
and isEmpty(a) are <undetermined> here, of the bounds the declaration does fix, and
notEmpty(an) is true from the lower bound 1; an object-level read of such a usage is the
unknown multiplicity error it always was.
Of the eleven positional cases, nine agree: a sequence fixes each position up to its first
element of open count, so (10, u, 30)#(1) and #(3) are 10 and 30 on both sides, as are
head, last, last(tail(…)), subsequence((10, u, 30), 2, 3)#(2), excludingAt((10, u, 30), 2)#(2) and includingAt((10, u, 30), 20, 2)#(4) over it, and (10, xs, 30)#(1) is 10 though
xs : Real[2..4] leaves every later position open; the pilot leaves (10, u, 30)#(2)
unevaluated where we answer <undetermined> of [1], and is silent on (10, u, 30)#(4) where
we report the index outside 1..3.
The 2 vast_bounds.cases probe includingAt over a usage whose upper bound is the largest
count the runtime represents, vast : Real[0..9223372036854775807], at that bound and at 1.
The pilot rejects the bound itself and then resolves no name in the model (Couldn't resolve reference to Element 'V'), so both are pilot-error and self-assessed, kept to a model of
their own so the rejection reaches no other case; here both insertions are <undetermined>,
the insertion index checked against the bound plus one without overflowing it.
Boolean folding on the second operand. The conditional and, or and implies live in
ControlFunctions.kerml of the Kernel Function Library (BaseFunctions declares none of them;
BooleanFunctions holds the eager &, | and xor), each as in firstValue : Boolean[1]; in expr secondValue[0..1] { return : Boolean[1]; } — the second operand is an expression the
function evaluates only when the first does not settle the answer (the package's own doc: "one
or more operands are expressions whose evaluation is determined by another operand"), which is
the short-circuit the first three cases pin. The library text says nothing about a first operand
that is not a Boolean value, which is the case here, so the question is what the function's
value is when firstValue is unknown. x and false is false whether x is true (the
second operand is evaluated and is false) or false (the first settles it); x or true is
true either way; x implies true is true either way. A result every assignment of the
unknown yields is determined, so we answer it, and the pilot answers the same three values. The
alternative reading — that the library's laziness makes a determined first operand a
precondition, so every form over an unknown first operand is <undetermined> — is defensible
from the declaration alone and would move and-second-false, or-second-true and
implies-second-true from agree to ours-undetermined; it is not taken. The forms the unknown
decides — and true, or false, implies false — stay <undetermined>.
Open cardinality at model level. A FeatureReferenceExpression evaluates to the values of
the feature on a target (KerML 1.1 §7.4.9), and a multiplicity bounds how many values a feature
has on each instance of its featuring type (§7.3.4.1, §7.4.12); at model level there is no
target, so a valueless gear[1..*] or loose[0..2] states bounds and no count. Three other
readings have each been the behaviour at some point and are not taken: (1) the read is an
error (ErrNoValue) — that is the instance-level contract for a required value that is
missing, and a model that declares loose[0..2] is missing nothing; (2) the minimum is
materialized, so size(rack.gear) is 1 and rack.gear#(7) is out of range — that is what
%instantiate does to build an object, a definite population the model does not assert; (3) a
valueless [0..n] usage reads as the empty sequence — the same minimum-population rule spelled
for the lower bound 0, which makes isEmpty(rack.loose) true where the model allows two
values. Reading (2) and (3) remain the documented object-level contract; only the model-level
read changed. The pilot supports the adjudication by declining to answer any of these rather
than by supplying the numbers.
The twelve tensor_quantities.cases probe TensorCalculations over a 2×2 stress tensor built
by TensorCalculations::'[' on a model-declared TensorMeasurementReference, added with the
tensor quantity value they were meant to referee and could not: the pilot answers every one —
the construction (InvocationExpression [), dimensions and flattenedSize (the Feature dimensions itself), # (IndexExpression #), + (OperatorExpression +), the scalar
multiplications, the zero and unit predicates, tensorTensorMult and VectorCalculations::outer,
qualified at the prompt or as an attribute's value — with the unevaluated node, so all twelve
land in pilot-unevaluated. The semantics are therefore self-assessed against the vendored
declarations, in spec-compliance.md (Structured values) and
omg-issues.md.
The nine coordinate_frames.cases probe a coordinate frame and a measurement scale as
values, added with the implementation they were meant to referee and could not: the pilot
answers the Annex A frame spatialCF with the unevaluated AttributeUsage spatialCF,
spatialCF / s and velocityCF / s with OperatorExpression /, (1.0, 2.0, 3.0) [spatialCF]
with OperatorExpression [, its mRef with AttributeUsage mRef, transform(...) with
InvocationExpression transform, ConvertQuantity to and from SI::'°C_abs' with
InvocationExpression ConvertQuantity and Time::UTC with the usage itself, so all nine land in
pilot-unevaluated and the semantics are self-assessed against the library text
(spec-compliance.md, Structured values; the readings the text leaves open are drafted in
omg-issues.md).
The fourteen rational_terms.cases probe RationalFunctions::rat, numer and denom, added
with the implementation they were meant to referee and could not: the pilot answers every call
— rat(1, 3), rat(1, 0), numer(0.1), denom(1.0 / 3.0), numer(2), qualified at the prompt
or as an attribute's value — with the unevaluated InvocationExpression rat/numer/denom, so
thirteen land in pilot-unevaluated and only quotient-by-operator (6 / 4, 1.5) agrees. The
semantics are therefore self-assessed, in
exact-rational-evaluation.md.
The fourteen value_classification.cases all agree, and they were added with the fixes they
referee: x @ T with a value subject is a classification test like x istype T — a : Integer = 3 answers a @ Integer and a @ Real true and a @ String false, car : Car answers car @ Vehicle true — where before the runtime judged every @ against the subject's metadata
annotations and reported a scalar as classifying no element. The two operators part over a
collection (KerML 1.0 §7.4.9.2): istype holds when every value is classified, @ when at least
one is, so mixed : Real[*] = (1, 2.5, 3) answers mixed istype Integer false and mixed @ Integer true, and () answers @ Integer false (and istype Integer true, the
empty-istype-integer of scalar_classification.cases); the pilot's IsTypeFunction tests every
value and its AtFunction any, and both sides agree on all of them.
A feature declared Integer[0..1] with no value is not probed: the pilot evaluates the bare
feature reference to the feature itself, one element whose type is Integer, and answers none @ Integer true and none @ String false, where the runtime at model level answers none istype Integer true (every value the feature could hold is an Integer) and none @ Integer
<undetermined> (the model fixes no count, so whether any value is present is open), and on an
instantiated object holds the empty collection and answers @ false — pinned by
conformance/value_classification_shared_rule.sysml. x @ Safety with a metadata type keeps the metadata reading, which the pilot does not share (its @
classifies the value alone, so it answers false); no committed case probes it, since the corpus
was written model-level and the annotation forms are pinned by the runtime conformance fixtures
instead.
The seven cast_expressions.cases probe x as T, added with the evaluation they referee. Two
agree: n as Real on n : Integer = 7 answers 7 on both sides, and (1, 2.5, 3) as Integer
answers (1, 3) on both — the cast selects element-wise and converts nothing. Three are
pilot-silent: n as Natural, 2.5 as Integer and 4.0 as Integer draw no output at all from
the pilot, so its reading of a cast is unobservable here; we answer the empty sequence for both
2.5 as Integer and 4.0 as Integer, and the typed ErrUndecidedClassification for n as Natural, because a cast keeps exactly the values istype affirms and the pilot's istype
verdicts below fix those. The two part cases, car as Vehicle and car as Car, land in
pilot-unevaluated: the pilot answers with the unevaluated PartUsage car, which names the same
value we select but is not an evaluation of the cast.
The 27 scalar_classification.cases referee the rule the cast borrows — which ScalarValues types
a scalar value is of — through istype and hastype, which the pilot does evaluate. 26 agree,
and together they fix the rule as the one KerML states: a value is of the type its representation
states and of that type's supertypes, whatever number it holds. An integer is an Integer
(n hastype Rational false, n istype Rational true); a finite real is a Rational, whole or not
(w : Real = 4.0 answers w istype Integer false, w hastype Rational true and w hastype Real
false — KerML 1.0 §8.4.4.9.2: only the rational subset of the reals has a finite literal, so a
LiteralRational's result is classified in Rational); a quotient is what IntegerFunctions::'/'
returns, a Rational (§9.4.11.1), so 6 / 3 istype Integer and (7 / 2) hastype Real are false
and (7 / 2) hastype Rational true; an integer written to a Rational feature stays the Integer
it is (rat : Rational = 4 answers rat hastype Integer true, rat hastype Rational false); *
is a Positive (§8.4.4.9.2) and the empty sequence is of every type. n as Rational keeps the
integer 4 and r as Real the rational 2.5, converting neither. The one disagree is
natural-feature-istype-natural: nat : Natural = 7 answers nat istype Natural true here and
false from the pilot, which reads the literal's type alone — the same evaluator answers nat hastype Integer true. The values of a feature are instances of all its types (§8.3.3.3.4
Feature, type), so the feature's typing is a type its value is of and the verdict stays ours;
no evaluation produces a value whose own type is Natural, which is why hastype Natural is false
on both sides and why a bare 7 istype Natural is false on both (w6d:istype-int-natural).
The 23 enumeration_classification.cases referee classification against an enumeration, added
with the rule they referee: an enumeration's enumerated values are the only instances of it
(SysML v2 §8.3.7 EnumerationDefinition: "An EnumerationDefinition is an AttributeDefinition all
of whose instances are given by an explicit list of enumeratedValues"), so whether a value is of
enum def Level :> Integer { low = 1; high = 3; } is decided by equality with the enumerated
values where the shared classification rule (classifyValue) would otherwise leave a scalar
against a narrower type undecided — 3 istype Level is true, 2 istype Level false, 3 as Level the value Level::high (printed 3), 2 as Level (), and held : Level = three is
admitted while = 2 is the write-conformance refusal. hastype keeps reading the value's own
type alone (KerML 1.0 §7.4.9.2, "directly"): a bare 3 is an Integer and no Level, and a
Level literal — written Level::high, held by lvl : Level, or produced by 3 as Level — is a
Level and not directly an Integer, which is only a supertype. A plain enum def Color { red; green; blue; } classifies by identity with its literals, and 5 as Even on a plain subtype
stays undecided. Twelve agree, eight disagree and three are pilot-silent, per case:
| Case | Pilot | Ours | Read |
|---|---|---|---|
two-istype-level, three-hastype-level, three-hastype-integer, high-istype-integer, high-eq-three, high-plus-one, cast-hastype-level, red-istype-color, red-hastype-color, c-hastype-color, three-istype-color, red-istype-level |
as ours | false, false, true, true, true, 4, true, true, true, true, false, false |
Agree. A bare 3 is directly an Integer and no Level; a Level literal is an Integer by specialization and equals and computes as its value; (3 as Level) hastype Level is true on both sides; a plain enumeration's literal is of its enumeration alone and a scalar is of no plain enumeration |
three-istype-level, three-at-level |
false |
true |
Ours. The pilot's IsTypeFunction/AtFunction compare the literal's type (Integer) against Level by specialization alone and never consult the enumerated values, so they answer false for a value §8.3.7 makes an instance of Level. The same evaluator answers two-istype-level false for the right reason and the wrong one at once; the two verdicts cannot both come from the extent |
high-istype-level, high-hastype-level, high-hastype-integer, lvl-hastype-level, lvl-hastype-integer, held-hastype-level |
false, false, true, false, true, false |
true, true, false, true, false, true |
Ours. The pilot folds a scalar-valued enumeration literal to the LiteralInteger 3 it is assigned and classifies that — so Level::high istype Level is false from the pilot, which no reading of §8.3.7 (the enumerated values are the instances) or of hastype (KerML 1.0 §7.4.9.2) allows, and contradicts its own cast-hastype-level and red-hastype-color answers. The runtime keeps the literal's identity on its scalar value (runtime.Value.EnumerationLiteral), so a literal is directly of its enumeration and only indirectly an Integer |
three-as-level, two-as-level, five-as-even |
no output | 3, (), ErrUndecidedClassification |
Unrefereeable. As for cast_expressions.cases, a cast draws no output from the pilot, so its reading is unobservable; ours follows the istype verdicts above, and 5 as Even keeps the undecided refusal the plain-subtype row pins |
The 24 literal_types.cases all agree, and they pin the rule the runtime's two typing paths now
share: a literal's own type is its ScalarValues definition, found in the library and not by its
simple name in the evaluating scope. scalar_classification.cases could not see the difference
because its model imports ScalarValues::*. In a package that imports nothing from
ScalarValues, 2 istype ScalarValues::Integer and 2.5 istype ScalarValues::Real are true
(the direct-type path once failed both, unable to determine the direct type "Integer"), 2 istype ScalarValues::Real is true and 2 hastype ScalarValues::Real and 2 istype ScalarValues::Natural are false; 2.5 hastype ScalarValues::Rational and not Real, as the
paragraph above states. Beside a model's own attribute def Integer (and Real, Boolean,
String), the written istype Integer still resolves to the type the scope sees, so 2 istype Integer and 2 hastype Integer are false while 2 istype ScalarValues::Integer is true —
where the direct-type path once took the model's Integer for the literal's type.
The three contextual_names.cases all agree, and they were added with the parser fix they
referee: chain is the feature chain modifier only when a name follows it, so attribute chain = 1; declares a feature named chain and chain + 1 reads it — where before the parser took the
word for the modifier, declared an anonymous attribute and left chain unresolved.
A later round of runtime fixes moved five of the six ours-error cases to agree
(dot-perform-out, dot-machine-attr, w6d:inherited-value-no-body,
w6d:inherited-value-template, w6d:vector-elements) and bump-out-target from
both-error to pilot-error: we now answer n = 1 on --target=Behave::Bump, which the pilot
refuses outright, so the remaining error is the pilot's alone.
These counts were lost once and regained. When the expression type checker
(passes/typecheck_expr.go) landed, 20 cases moved from agree (19) and kind-only (1) to
ours-error — agree: 37 · ours-error: 21 — without a single referee case changing. All 20 draw on
expr_values.sysml, and the checker refused that whole model on one declaration, calc def IntDiv { return : Integer = 7 / 2; }, with cannot bind Rational value to a feature typed by Integer. The
first fix was in the checker: a binding was refused only when the value's type and the feature's
were disjoint (String to Integer, Boolean to Real) or when the value was a literal whose
type is exact (2.5 or -3 to Natural), and a quotient, a call or a Real feature bound to an
Integer was left to evaluation, which then judged the value by its magnitude — 4 / 2, evaluating
to 2.0, was held by an Integer feature and 7 / 2 was not. After that fix the count read
agree: 56 · ours-error: 1; each of the 20 returned to the bucket it held before, and intdiv
alone was ours-error, refused at run time.
That by-magnitude reading is superseded. The scalar_classification.cases above fix what a scalar
value is of, and a feature write is the same judgement: a feature value and a calculation argument
are bindings (KerML 1.0 §7.4.9: a BindingConnector requires the same values at both ends), and
the values of a feature must be instances of all its types (§8.3.3.3.4), so a feature holds a value
exactly when istype would affirm the feature's type of it. A quotient is a Rational whatever it
divides (§9.4.11.1), a finite real is a Rational whatever number it holds (§8.4.4.9.2), and neither
is an Integer: attribute whole : Integer = 4 / 2 and an Integer parameter fed the real 2.0
are now the typed ErrTypeMismatch that 7 / 2 and 3.5 already were, and a model that means the
whole number converts with RationalFunctions::ToInteger, RealFunctions::ToInteger or
IntegerFunctions::ToNatural — the library functions that convert — or declares the feature
Rational or Real. One shared classification (runtime/classification.go classifyValue)
answers as, istype, hastype, @ and write_conformance.go valueConforms, so no two of
them can judge the same value and type differently. The static checker follows the same rule
where the value's type is settled at its spelling: a literal's type is exact, and a quotient's is
Rational however whole or signed, so Integer = 7 / 2, Integer = -(4 / 2), Natural = i / 2
and an argument add(4 / 2) to an Integer parameter are refused where they are written
(passes/typecheck_expr.go bindable, isQuotient); a call or a Real feature bound to an
Integer is still left to evaluation, since their static type only bounds their values (a Real
feature may hold an integer, as rat : Rational = 4 shows). Nothing is truncated: a sequence
index that evaluates to 2.0 names the second element and one that evaluates to 1.5 names none,
as before.
The fixtures that relied on the by-magnitude write were re-adjudicated one by one rather than
relaxed: an Integer or Natural feature or parameter that a quotient or a whole real reached
is declared Rational or Real where the model computes such a value (the REPL, gRPC and LSP
fixtures, the compiled-calculation and choice-point fixtures; the disposal-robot demo computes
no such value and runs unchanged), converts with
ToInteger where it means the integer (value_conformance_test.go, the checker's own tests), or
pins the typed error where the write is the point of the fixture (runtime/robustness_test.go, the
coordinate-frame failure modes, calc_cast_scalar_values). Each movement is cited in the
feature-write conformance row of spec-compliance.md.
intdiv is now refused by the checker rather than at run time — cannot bind Rational value to a feature typed by Integer where the pilot answers 3.5 — and is kept in a model of its own,
integer_quotient.cases, so the static refusal leaves the other expr_values probes evaluable.
The pilot's answer is not a reading of the specification we differ on: its evaluator computes the
result expression and does not check what the result parameter is typed by, so it would answer
3.5 for return : String = 7 / 2 too. Reporting the binding is stricter than the reference, not
different from it, and the case stays as written because it probes exactly that.
The one kind-only is 2 ** 40 (above). The pilot-error, pilot-unevaluated and pilot-silent
buckets — 71 cases, a third of the corpus — are the pilot's limits rather than
disagreements, which is the central finding of this page restated as a count.
Of the five disagree outside enumeration_classification.cases and extent_expressions.cases,
the first is w6d:complex-is-zero-qualified: the pilot answers false for
ComplexFunctions::isZero(rect(0.0, 0.0)) where we answer true. It is not a value verdict against
us — the pilot's re/im have no evaluable body, and the same run answers false for
isZero(rect(3.0, 4.0)) too, so its result folds against unevaluated operands rather than deciding
zero. Read it as unrefereeable, and see the ComplexFunctions row of
spec-compliance.md for the adjudication.
The remaining three are the subsetting-membership cases added with the fix they were meant to
referee, and they are unrefereeable in the same way. w6d:subsetting-defaulted-count asks
size(subsystem) of a part subsystem : Sub[*] default null; that part a : Sub :> subsystem;
and part b : Sub :> subsystem; subset: we answer 2, the pilot 0. Its 0 is the folded
default, not a count of members — w6d:subsetting-two-count drops the default and the pilot
answers 1 for the same two parts, and w6d:subsetting-none-count answers 1 for a collection
nothing subsets (we answer 2 and 0). A count that is 1 for two members and for none is the
static evaluator reading the feature as one value, so it decides nothing about which objects a
subsetted collection holds; w6d:subsetting-rollup agrees only because Sub::mass defaults to
1.0 and one part subsets. The two empty-aggregate cases of the same round land in
pilot-unevaluated: sum(qsNone) over an empty MassValue[*] is InvocationExpression sum and
3.0 [SI::kg] + sum(qsNone) is OperatorExpression + where we answer 0 [kg] and 3.0 [SI::kg],
so the kind of an empty quantity sum, like every other unit-carrying value, has no reference
verdict. See the aggregate and subsetting rows of spec-compliance.md.
The dedicated Complex value moved no case between buckets — the counts above are as remeasured
after it — but it changed one answer inside pilot-unevaluated: w6d:complex-mul-re,
re(rect(0.0, 1.0) * rect(0.0, 1.0)), now answers -1.0 where we reported operator '*' is not defined for a sequence and a sequence. A Complex was two Reals, so the unqualified * saw two
sequences; it is one value now (runtime.ValComplex), and an arithmetic operator over a Complex
operand computes as the ComplexFunctions declaration of the same name. The pilot answers no value
for the case, so the bucket does not move. Overload selection by argument type changed two more
answers inside pilot-unevaluated without moving them: w6d:complex-abs answers 5.0 and
w6d:complex-is-zero answers true, where both reported that NumericalFunctions::abs/isZero
require a numeric value — the unqualified name now selects the ComplexFunctions declaration its
argument fits (the ComplexFunctions row of spec-compliance.md). The pilot
answers the unevaluated InvocationExpression for both, so it referees neither.
The five remaining ours-error cases are adjudicated divergences:
| Case | Ours | Read |
|---|---|---|
body-local-level-refused, body-local-count-refused, body-local-repeat-refused |
declaration of l: type mismatch: cannot write 2 (an Integer) to a feature typed by Level, declaration of xs: multiplicity violation: 3 value(s) bound to a feature with multiplicity upper bound 2, declaration of xs: uniqueness violation: 1 (an Integer) is written at positions 1 and 3 of a unique feature |
Deliberately ours. attribute l : Level = n;, attribute xs : Integer[2] = (n, n + 1, n + 2); and attribute xs : Integer[*] = (n, n + 1, n); are declarations local to a calc body, and the values of a feature are instances of its types (KerML 1.0 §7.3.4), within its multiplicity (§7.4.12) and, unless nonunique, without repeats (§7.3.4.2) — the rule a parameter, a return and a namespace-level declaration answer to, now applied to the body-local declaration when its initial value is bound (the feature-write and uniqueness rows of spec-compliance.md). The pilot checks nothing when it binds a body-local value: it answers 2, 3 and 3, the unchecked initializers carried through l + 0 and size(xs), so it neither confirms nor refutes the refusal and only the accepted body-local-level-accepted (BodyOnly(3) = 3) can agree, as it does. |
w6d:held-undeclared-multi |
multiplicity violation: 2 value(s) bound to a feature with multiplicity upper bound 1 |
Deliberately ours. attribute xs = (1.0, 2.0) declares no multiplicity, so the assumed 1..1 makes the default a violation (KerML 1.0 §7.4.5, and the multiplicity row of spec-compliance.md); the pilot returns both values. An adjudicated divergence, not a defect |
intdiv |
integer_quotient.sysml:6:42: error: cannot bind Rational value to a feature typed by Integer |
Deliberately ours. calc def IntDiv { return : Integer = 7 / 2; } binds a Rational — what IntegerFunctions::'/' returns, KerML 1.0 §9.4.11.1 — to an Integer result parameter, and a feature's values must be instances of its types (§8.3.3.3.4, §7.4.9; the feature-write conformance row of spec-compliance.md). The pilot answers 3.5, checking nothing against the parameter's type. Stricter than the reference, not a defect in either |
The cases that were ours-error before that round, and what closed them:
| Case | Was | Closed by |
|---|---|---|
dot-perform-out, dot-machine-attr |
usage machine: performed action p of machine: bind c: unresolved reference: cc |
A name that denotes one object — an occurrence, or a structured attribute usage holding features rather than a value — evaluates to that object (runtime.Context.namesOneObject), so a binding in a performed action's body resolves the package-level sibling it names |
w6d:inherited-value-no-body, w6d:inherited-value-template |
unresolved reference: template, usage W6D::template has no value |
The same classification: template is a structured value, so plain.cost reads its features and template.v reads the redefined one, with the features it does not redefine keeping their inherited values |
w6d:vector-elements |
cannot chain through non-instance member |
A numerical vector is the sequence of its elements, so elements of one is that same sequence (runtime.isNumericVector) rather than an element-wise member lookup |
-
Behave::machine.p.n— aperform actionbinding's scope, closed by those runtime fixes. Both tools validate the model clean and both now answer the same value:$ ./bin/sysml -e "Behave::machine.p.n" behavior.sysml ✓ Behave::machine.p.n = 1against the pilot's
LiteralInteger 1, onperform action p : Bump { in c = cc; }insidepart machine, whereccis a sibling member of the enclosing package. The binding resolves because a name denoting one object evaluates to that object; conformance caseperform_action_binding_package_sibling. -
The pilot's evaluation context is narrower than ours, so the scope rows get no referee. With
--target=Behave::Flowthe pilot cannot see the enclosing package's members (cc.count→Couldn't resolve reference to Element 'cc') where we answer0; and--target=Behave::Bumpwithnis refused outright (Must be an accessible feature) where we answer1. Both are limits of the pilot's%evalcontext rather than statements about scope semantics — which is exactly why the "scope of an expression in a behavior body" rows get corroboration only, and why these two cases are bucketedpilot-errorrather than as our disagreement.
Nothing short of a second executing implementation. Options, in order of cost:
- A pilot version that executes. The OMG pilot's execution work lives outside this artifact; adopting it means moving off the pin, and the pin is what makes the static differential meaningful. It would have to be a second, separately pinned artifact.
- A different tool (e.g. an fUML/Alf-based executor, or SysIDE's runtime if it grows one), which brings its own conformance question: a disagreement then needs adjudication against the specification anyway.
- Specification-derived traces. Hand-adjudicated traces from KerML
Performances/Occurrencesand the Systems Library, reviewed as evidence in their own right. Slower per row, but it is the only route that answers the ordering questions the deviations raise.