The action and activity executor has, until now, had no external referee: the PSSM referee covers state machines, the pilot execution referee covers expressions, and actions were checked only by this repository's own conformance fixtures. This record describes the referee that closes that gap: the activity tests of ModelDriven's fUML Reference Implementation, translated by rule into SysML v2 textual notation and executed by this runtime, with the reference implementation's own outputs as the oracle. Like the PSSM referee it is advisory and opt-in: CI compares committed bucket counts, never a pass/fail verdict, and a movement in any count is adjudicated in the change that moves it.
What a pass means. The reference implementation executes UML activities under the fUML semantics; SysML v2 actions are a different language with a different (though closely related) semantics, and where the two disagree this runtime follows SysML v2 and the Kernel Semantic Library. A pass therefore says that, for an activity with a defensible SysML v2 mapping, this runtime computes the output values the reference implementation computes. It is a second opinion on the action rows of the precise-semantics alignment note and a regression oracle for the constructs both languages define alike; it is never a conformance statement about SysML v2, and it is not a fUML conformance statement about this runtime either.
| Source | ModelDriven's fUML Reference Implementation, https://github.com/ModelDriven/fUML-Reference-Implementation, release v1.5.0a at commit 45e506336d4cd56965d4ad3b684149245f899f3a |
| Test model | fUML-Tests.uml, the Eclipse UML2 XMI model behind the implementation's JUnit activity tests: 43 activities, 42 of them packaged directly in the model and run one by one, one (ActiveClassBehavior) the classifier behavior of an active class that runs only when that class is instantiated |
| Exception model | fUML-Exception-Tests.uml, 12 activities exercising the RaiseExceptionAction and exception handlers that fUML 1.4 added: eight tests, a called behavior, and three behaviors owned by a class. SysML v2 has no exception handler; these are fetched for completeness and classified not-expressible, never translated |
| Library | fUML_Library.xmi, the foundational model library the tests call into (WriteLine, the primitive functions); the implementation itself loads the copy inside its jar, whose digest the record carries as libraryDigest |
| Executable | fuml-1.5.0a.jar, the implementation, and its fifteen runtime dependencies from Maven Central |
| SHA-256 | c7d54bf2…e0f0 (test model), 4e831580…2d74b (exception model), 7e8bae51…e2da8b7 (library), 4e78a194…de1f06e (jar); in full in scripts/fuml-pin.sh |
| Pin | scripts/fuml-pin.sh (FUML_RI_TAG, FUML_RI_COMMIT, the four *_SHA256 variables, FUML_DEPS) |
| Download | ./scripts/download-fuml-suite.sh, into the git-ignored build/fuml/, idempotently; a file already there with its pinned digest is kept (so a suite placed by hand, without a stamp, is verified and stamped rather than fetched), one whose digest is not the pinned one is discarded, and the referee refuses to read a suite whose digest is not the pinned one |
The tag names the release; the commit is what every fetch reads from, because a tag is a mutable ref; the checksums are what the fetch verifies, because the file behind a URL can change. The three are changed together.
Runtime dependencies. The jar does not bundle its dependencies. They are the runtime scope
of the implementation's pom.xml at the pinned commit, as mvn dependency:build-classpath -DincludeScope=runtime resolves it — log4j 1.2, commons-logging, commons-collections,
commons-lang, Xerces, Xalan and its serializer, xml-apis, the StAX API with SJSXP and
stax-utils, and JAXB 3 with its API, core and activation — pinned by Maven Central path and
SHA-256 in FUML_DEPS, so a fetch needs neither Maven nor the pom and always builds the same
classpath. FUML_MAVEN_REPO names the repository (Maven Central by default; a mirror serves
the same bytes, and the checksums say so). Any file already installed with its pinned digest,
dependency or model or jar, is kept rather than fetched again; --force fetches everything.
Licence and attribution. The reference implementation, its test models and its library are
copyright Lockheed Martin Corporation and Model Driven Solutions (formerly Data Access
Technologies, Inc.) and are licensed under the Academic Free License version 3.0
(https://opensource.org/licenses/AFL-3.0), as the repository's Licensing-Information.txt
states; the bundled third-party programs it lists carry their own licences (CDDL 1.0 for SJSXP
and JAXB, Apache 2.0 for log4j, commons-logging and Xerces). This repository proceeds as it does
for the PSSM suite: nothing is vendored. The models, jar and dependencies are downloaded at a
pinned commit and checksum into an ignored directory at build time, the models are translated
in memory, and only the implementation's computed outputs and event trace (the
expected-record below), bucket counts and per-activity verdicts are committed. No XMI, no
excerpt and no derived .sysml model of a fUML test is in the repository; -keep <dir> writes
the translated models for debugging only, into a directory the caller names.
The referee never runs Java. What the reference implementation computes is recorded once, in
docs/project/fuml-referee-expected.json, and committed; the Go tests and
tools/cmd/fuml-referee -check read that record and nothing else. It is regenerated by
make fuml-expected # needs a JDK (javac and java on PATH, or JAVA_HOME)which fetches the suite if it is absent, compiles the driver in scripts/fuml-driver/
against the pinned jar and dependencies, and runs it over both models. The committed record
is replaced only when every executed activity completes: if one throws or exceeds the
per-activity timeout, the target exits nonzero, leaves the record as it was, and writes the
partial output beside it as fuml-referee-expected.json.failed (git-ignored) for diagnosis,
with the failing activities' error fields filled in. The record is
byte-stable across runs on one pin: the only nondeterministic content the implementation
emits — object identifiers, which are Java hash codes — is aliased per activity in order of
first appearance (obj1, obj2, …).
The driver (scripts/fuml-driver/io/opensysml/fuml/FumlExpected.java) uses the implementation as a library:
it loads each model with Fuml.load, enumerates the uml:Activity elements the model file
declares, and for each one packaged directly in the model (the ones the JUnit suite runs)
selects it with Environment.findElementById and executes it with
ExecutionEnvironment.execute. Selection is by XMI id, never by name: the implementation's
name lookup ranges over every named element, and ForkMerge resolves to a parameter of
TestSimpleActivities before it resolves to the activity. Activities owned by a class or an
action (ActiveClassBehavior, the exception model's C$Impl, C_Factory,
raiseException$Impl) run only as part of their owner and are recorded as skipped with that
reason, as are cross-references into the library, which are not declarations. The
implementation reports what it does through fuml.Debug as [event] lines; the driver
attaches a log4j appender to that logger and captures the lines as structured events instead of
scraping console output. The locus's extent is cleared between activities, as the JUnit suite's
set-up does, so an object created by one test is not read by the next.
Its shape. provenance names the release tag, the commit, the jar's digest, the
foundational library the jar carries and its digest, and each model with its namespace URI and
digest; tools/referee/fuml refuses a record whose provenance is not the current pin's, so a moved
pin cannot be compared against old truth. Each entry of activities is one declared activity:
{
"model": "fUML-Tests.uml",
"id": "_15_5_1_1a900482_1225499009421_139168_1510",
"name": "DecisionJoin",
"executed": true,
"parameters": [{"name": "output", "direction": "out", "type": "Integer",
"lower": 1, "upper": "*", "isOrdered": true, "isUnique": true}],
"outputs": [{"parameter": "output",
"values": [{"kind": "Integer", "value": 0}, {"kind": "Integer", "value": 1}]}],
"events": [{"kind": "Execute", "activity": "DecisionJoin", "id": "_15_5_1_1a900482_1225499009421_139168_1510"},
{"kind": "Fire", "activity": "DecisionJoin", "action": "Value(0)", "id": "_15_5_1_…_1531"},
{"kind": "Fire", "activity": "DecisionJoin", "action": "Action_A", "id": "_15_5_1_…_1611"},
{"kind": "Execute", "activity": "Copier", "id": "_15_5_1_…_826"},
{"kind": "Output", "activity": "Copier", "parameter": "output", "value": "0"},
{"kind": "Complete", "activity": "Copier", "id": "_15_5_1_…_826"},
"…",
{"kind": "Complete", "activity": "DecisionJoin", "id": "_15_5_1_1a900482_1225499009421_139168_1510"}]
}parameters are the activity's parameters as the implementation loaded them (upper is a
natural number or *). outputs hold, per out and inout parameter, the values the
execution left in it, in the implementation's order — primitives as JSON numbers, booleans or
strings (Real and UnlimitedNatural as strings, so * and the implementation's own
spelling of a real survive), references as the object they point at with its types and feature
values to a bounded depth. events are the implementation's trace in order: Execute when
an activity (the one under test or one it calls) starts and Complete when that execution
ends, Fire when an action runs, Output when an output parameter receives a value (and
Post where the implementation reports one posted to a parameter node; none of the pinned
activities does). Execute and Complete nest, so the trace shows which execution each
Fire belongs to even when an activity calls itself. The implementation reports elements by
name only; the driver adds the XMI id of the activity (Execute, Complete) or the action
node (Fire) where the name identifies exactly one element of the loaded models, and omits it
where it does not — a node name an activity uses twice (the exception model's Test001 reads this at two nodes), or an
action of a library activity such as WriteLine. The Fire sequence is one legal schedule —
the implementation's, which is sequential — and is compared advisorily only; the outputs
are the oracle.
The record also carries the one construct the classifier reads from the trace rather than
from the model. ExpectedActivity.Refired names every action the implementation fired more
than once within one execution of its activity — the activity under test or one it calls —
with the activity it belongs to, both XMI ids and the number of fires (DecisionJoin's
Action_A twice, ForkMergeData's Action_B twice, ForkMerge's Value(0) twice,
TestBooleanFunctions' Call(And) four times). The count is per node id and per
Execute…Complete span: two nodes sharing a name are never mistaken for one node firing
twice (a Fire without an id counts as nothing), an activity a test calls twice re-fires
nothing by being called twice, and a callee execution — a recursive one included — neither
inherits nor resets its caller's counts. Whether such a re-firing is a design difference is
then the classifier's decision, below.
tools/referee/fuml reads the two test models and the library with the XMI element walker shared
with the PSSM referee (internal/translate/xmi). The walker accepts every OMG XMI namespace version:
the models are 20131001, the library 20110701, and the two are cross-referenced. The
reader (ReadModelFile, ReadLibraryFile) accepts a uml:Model root or one wrapped in
xmi:XMI, and builds an immutable Model:
| fUML | Model |
|---|---|
Activity, its parameters (direction, type, multiplicity, ordering, uniqueness) and its owner when a class or an action owns it |
Activity, Parameter, Multiplicity; Activity.Owner, Class.ClassifierBehavior |
Every ActivityNode kind fUML defines — control nodes, parameter nodes, buffers, actions, structured nodes — with their InputPins and OutputPins and the nodes a structured node owns |
Node (NodeKind), Node.Pins, Node.Nodes; Activity.AllNodes walks the structured nodes too |
ControlFlow and ObjectFlow with guard (a literal or an instance value naming the enumeration literal) and weight |
Edge (EdgeKind), Edge.Guard, Edge.Weight; Node.Incoming/Outgoing index them on the node or pin they touch |
The behavior a CallBehaviorAction calls, the operation a CallOperationAction calls or a CallEvent names, the signal a SendSignalAction/SignalEvent names, the feature a structural-feature action reads or writes, the classifier a create/read-extent/reclassify action names |
resolved TypeRefs: a local element by id, or an external reference into the library by href, kept with its qualified name (PrimitiveBehaviors::IntegerFunctions::+) |
Class with properties, generalizations, operations (and their methods), Signal, Association and its member ends |
Class, Property, Operation, Signal, Association |
ExceptionHandler, Trigger |
Node.Handlers, Node.Triggers |
Every reference the model makes to something it does not declare is a diagnostic on the
Model, never a silent gap; the pinned models produce none. The reader is exercised against
the pinned corpus (43 activities with 95 control and 345 object flows, seven classes, three
signals and two associations in the test model; 12 activities in the exception model) and
against a synthetic wrapped model that covers the 20110701 namespace, an external library
reference, guards, weights, an association, an operation call and its accepter.
Forty-three, not forty-four. The test model contains 44 uml:Activity elements, but one
is the library's WriteLine referenced by href from HelloWorld, not a declaration; the
JUnit suite runs 42 activities directly and ActiveClassBehavior through its class. The
reader, the expected-record and the checklist below all count 43.
Classify(activity, expected) files each activity into one of three classes from the
constructs it uses and the implementation's trace, before any translation exists, with a
reason that names the construct, why SysML v2 has no spelling for it, and where in the
activity it occurs (ReadExtentAction (SysML v2 has no classifier extent): ReadExtent(TestClass)).
The classes decide two of the referee's four buckets outright; the third leaves the verdict to
a run.
| Class | Bucket | Decided by |
|---|---|---|
| not-expressible | not-expressible |
any construct in the construct map's not-expressible rows — CallOperationAction, AcceptCallAction, ReplyAction, ReadExtentAction, ReadIsClassifiedObjectAction, TestIdentityAction, ReclassifyObjectAction, UnmarshallAction, ReadLinkAction and the structural-feature actions on an association end, CentralBufferNode, DataStoreNode, DestroyObjectAction, RaiseExceptionAction and ExceptionHandler; a library function without a Kernel Function Library counterpart (BasicInputOutput::WriteLine, the UnlimitedNaturalFunctions) or a parameter or pin typed UnlimitedNatural, which ScalarValues lacks; the exception model as a whole; and, transitively, a call of or a classifier behavior starting a not-expressible activity, reported as the dependency and its decisive constructs |
| differs-by-design | differs-by-design |
an action the trace fired more than once within one execution of its activity and that an object flow feeds — fUML fires it once per token offered to a multiplicity-1 pin; SysML v2 performs the node once with every delivery (action_node_concurrent_performances). A merge that delivers two control tokens re-fires its successor in SysML v2 too, so ForkMerge's Value(0) stays expressible |
| expressible | pass or fail, by the run |
everything else: control and object flow, fork, join, merge and decision with guards, value specifications, calls of activities and of library functions with a counterpart, multi-valued outputs, object creation and feature reads and writes on a class, signals, active classes, ReadSelfAction, StructuredActivityNode |
A differs-by-design classification is the one an alignment row documents; the classifier
finds four such activities in the corpus and lists each re-fired action with its count.
LibraryCounterpart maps each of the 46 library functions the test model calls either to its
Kernel Function Library spelling or to a reason; the test pins the seven without one
(WriteLine and the six UnlimitedNaturalFunctions).
The checklist. TestSuiteClassification in tools/referee/fuml/classify_test.go is the
per-activity checklist: every activity of both models with the class it must receive, the
counts pinned, every non-expressible reason required to name a construct and a location, and
every exception-model activity required to be filed as such. Over the test model:
| Class | Count | Activities |
|---|---|---|
| expressible | 24 | Copier, CopierCaller, SimpleDecision, ForkJoin, ForkMerge, NodeEnabler, TestNodeEnabler, TestIntegerFunctions, TestIntegerComparisonFunctions, TestRealFunctions, TestRealComparisonFunctions, TestStringFunctions, GenerateBooleanTestData, GenerateListTestData, TestListFunctions, TestGeneralizationAssembly, TestClassObjectCreator, TestClassWriterReader, TestClassAttributeWriter, TestClassAttributeValueRemover, ActiveClassBehavior, ActiveClassBehaviorSender, TestSignalReceiver, TestSpecializedSignalSend |
| differs-by-design | 4 | DecisionJoin (Action_A ×2), ForkMergeData (Action_B ×2), TestSimpleActivities (through both), TestBooleanFunctions (Call(Not) ×2, Call(And), Call(Or), Call(Implies), Call(Xor) ×4) |
| not-expressible | 15 | HelloWorld (WriteLine), TestUnlimitedNaturalFunctions, TestClassIdentityTester, TestClassExtentReader, TestClassObjectDestroyer, TestCompositeObjectDestroyer, TestClassReclassifier, TestClassUnmarshaller, SelfReader (ReadIsClassifiedObjectAction), TestAssociationEndWriterReader, TestCentralBuffer, TestDataStore, TestCallAccepter, TestCallSender, TestCallSend |
and all 12 activities of the exception model are not-expressible. TestBooleanFunctions
is in the second class, not the first, because GenerateBooleanTestData hands each function
a four-token truth table through multiplicity-1 pins, and Not reads Value(true) and
Value(false) through one pin.
The reader and classifier tests (TestSuite*) run only when the suite is present in
build/fuml/ (./scripts/download-fuml-suite.sh). CI downloads and caches it as it does
the PSSM suite, sets OPENSYSML_REQUIRE_FUML_SUITE=1 so an absent suite fails rather than
skips, and re-runs the suite gates on their own so a skip cannot hide behind a green run. The
expected-record tests need no suite.
Emit(activity) translates an expressible activity, with every activity it transitively
calls, into one model in SysML v2 textual notation: a package fuml importing ScalarValues,
SequenceFunctions and ControlFunctions, holding one part def per class the activities
touch and one action def per activity. The translation is by rule and in memory; nothing
of it is committed, and -keep <dir> writes it out for reading. The rules, found by hand
translation of the pilot activities and then generalized:
| fUML | SysML v2 |
|---|---|
Parameter |
in/out/inout parameter of the definition, with its type from ScalarValues and its multiplicity; a multi-valued one [0..*] nonunique whatever its fUML bounds, ordered where the parameter is — a SysML v2 multiplicity holds in every state of the feature, where a fUML parameter's holds only once the activity completes (GenerateBooleanTestData's [4..4] outputs fill one token at a time and would not validate), so the record's value count checks the bound instead; an out with lower bound 0 is initialized = () so an activity that leaves it empty leaves it empty here. A called activity's parameter that shares its name with another definition's is spelled <Activity>_<name> (Copier_output), because a SysML v2 nested action returns its outputs to same-named features of the actions around it and reads an unbound input from them, where a fUML activity's parameters are its own; the translated activity's parameters keep their names, which the record compares by |
ActivityParameterNode |
of an input, an action whose one output pin reads the parameter; of an output, a collector action whose one input pin is assigned to the parameter (assign output := v for a scalar, assign output := (output, v) for a list). A collector performs once per succession into it, so each delivery is appended as it arrives |
ValueSpecificationAction |
an action whose result pin is the literal; the literal's kind types the pin, as the implementation puts the evaluated literal on the pin whatever the pin says |
CallBehaviorAction of an activity |
action <name> : fuml::<Activity>; — a nested action typed by the callee's definition, its pins the callee's parameters by position |
CallBehaviorAction of a library function |
an action whose result pin is the Kernel Function Library counterpart applied to the argument pins, as LibraryCounterpart maps it; Div is ToInteger(x / y), Inv is 1.0 / x, Implies is ControlFunctions::'implies', ListConcat is SequenceFunctions::union. An untyped pin (the list functions') takes its type from the flow into it |
ObjectFlow |
flow <src>.<pin> to <tgt>.<pin>, traced end to end: from each pin or parameter node that produces a value, through the control nodes routing its token, to each pin it can reach, one flow per route. Beside every flow between two actions an enabling succession is added, because a SysML v2 flow delivers a value but does not enable its target as fUML's object-flow does |
ControlFlow |
succession first <src> then <tgt> |
InitialNode, ForkNode |
fork; the initial node is a fork so several first nodes can follow it. Nodes with nothing coming in are enabled from start, through a fork when there are several |
JoinNode, MergeNode, DecisionNode |
join, merge, decide; a decision's guards are tests of the pin it decides on (succession first D if A.result == 0 then …), found back through its decision-input flow or its one incoming object flow; a join has one outgoing succession, so a decision after it is spelled after it |
| An action with several outgoing control flows, several edges into a collector or into the final node | an implicit fork before, an implicit merge after, since a plain action node has one successor |
StructuredActivityNode without pins |
a nested action owning its contents' flow; an object flow across its boundary becomes a parameter of it, with a reader or collector inside. A control flow across the boundary, a guarded or weighted crossing, or a crossing of more than one boundary is refused |
ActivityFinalNode |
done; a FlowFinalNode ends the edge into it |
Class with ownedAttributes and generalizations |
part def <Class> :> <General> { attribute <p> : <T> [l..u] ordered nonunique; … } — a part definition, not an item definition, because a fUML object is an occurrence that may perform behaviors (an active class's classifier behavior, alignment row SM43) and a SysML v2 part is the item that performs actions; the attributes keep their declared multiplicity exactly, UML's default [1..1] unwritten, since a class's feature is a store the actions write within its bounds, where a parameter's fills one token at a time. An attribute typed by a class is a part (composite) or ref part. Every class the activity's closure names — as a parameter's, pin's or attribute's type, as the classifier created, as the owner of a feature touched — is declared once, generals first; a class is found by its XMI id and then by its name, since a type reference may carry either |
CreateObjectAction |
action <name> { out result : <Class> = new <Class>(); } — a new occurrence on the result pin. Creation starts no behavior; that is StartObjectBehaviorAction's (SM43) |
ReadSelfAction |
action <name> { out result : <Owner> = this; } inside a class's owned behavior, this being the object performing it — the behavior's definition is nested in the owner's part def, so the name resolves lexically to the enclosing object; in an activity performed on its own, self is the performance and not an object, and the action is refused |
Class with ownedBehaviors and a classifierBehavior |
inside the part def, action def <Behavior> { … } per owned behavior the closure spells, the activity's body translated by the same rules as a top-level one, and action classifierBehavior : <Behavior>; for the classifier behavior — an action usage the part's objects may perform, declared without perform, so an object is created performing nothing (SM43); the class's members share one namespace, so an attribute named as a behavior, as a parameter of one or as classifierBehavior is refused, as is a CallBehaviorAction of an owned behavior (fUML calls it in the caller's context, which no SysML v2 nested action does) |
Activity used as a class — instantiated by CreateObjectAction or owning attributes |
part def <Activity> { attribute …; action def 'behavior' { … } action classifierBehavior : 'behavior'; } — a UML behavior is a class, and the suite creates objects of one and starts them; the body is its own classifier behavior under the name behavior, since the part definition took the activity's. An activity both instantiated and called as an action would be two things and is refused |
StartObjectBehaviorAction |
action <name> { in object : <Class>; perform object.classifierBehavior.start; } — the start of the object's classifier behavior, own or inherited, performed: the runtime lowers a perform of a behavior member's start to an explicit start effect that runs the behavior as the object's own execution, asynchronously, the starter completing at once as fUML's start does (SM43); a second start of a running or completed behavior starts nothing. A start with a result pin, without an object pin, passing arguments, or of an object whose class has no classifier behavior is refused |
ReadStructuralFeatureAction |
action <name> { in object : <Class>; out result : <T> [m] = object.<f>; } at the feature's multiplicity; the object arrives at the object pin as fUML's does |
Signal with ownedAttributes and generalizations |
attribute def <Signal> :> <General> { attribute <p> : <T> [l..u]; … } — an attribute definition, since a fUML signal instance is a value carried by a message and read by attribute, never an occurrence that performs anything; a signal with nothing to declare is attribute def <Signal>;. Every signal the closure names — sent, accepted, typing a parameter, a pin or an attribute of a class or of another signal — is declared once, generals first, before the classes and the activities, and a signal is found by id and then by name as a class is |
SendSignalAction |
action <name> { in target : <Class>; in <arg> : <T>; … send new <Signal>(<attr> = <arg>, …) to target; } — the target object and one argument pin per attribute of the signal, inherited ones included, in the signal's attribute order; the body sends a new instance to the target and completes without waiting, as fUML's send does. A send with a result pin, without a target pin or with an argument count other than the signal's attribute count is refused |
AcceptEventAction of one SignalEvent |
action <name> accept <pin> : <Signal>; — an accept node whose result pin is the instance accepted; without a result pin, action <name> accept <Signal>;. The runtime matches by conformance, so an instance of a specialized signal satisfies an accept of its general as fUML's SignalEvent matching does. An accept with several triggers, of a CallEvent, with an input pin or isUnmarshall (one pin per attribute) is refused |
AddStructuralFeatureValueAction, RemoveStructuralFeatureValueAction, ClearStructuralFeatureAction |
action <name> { in object : <Class>; in value : <T>; in insertAt : Integer; out result : <Class> = object; assign object.<f> := <held after>; } — the result pin hands the original object on, as fUML §8.10.2's structural feature actions do, and the feature is assigned what the reference implementation computes it to hold afterwards: an add with isReplaceAll, or to a single-valued feature, the value; otherwise a unique feature dropping its old copy first, the value inserted at insertAt (includingAt, one-based, so the runtime rejects a position of 0, which the reference implementation hands to its choice strategy; *, spelled -1 since no position is negative, appending) or, with no position, first — the reference's FirstChoiceStrategy chooses position 1; a remove dropping every copy (isRemoveDuplicates, excluding), the copy at removeAt when there is one (excludingAt), or the first copy (select over the positions), a single-valued feature emptied when it holds the value; a clear emptying. The insertAt/removeAt pins are fed by LiteralUnlimitedNatural value specifications, which the emitter spells as Integer positions — the one place an unlimited natural has a spelling |
The runtime executes a class as its object model executes any part: new materializes an
instance whose features hold their declared multiplicity, assign object.f := … writes a
feature through the same statement a user's action body writes with, and a violation of the
feature's bounds is the runtime's typed error, so a translated write the bounds refuse is a
fail naming it. A translated send posts its message through the runtime's signal routing,
and an accept parks its node until a matching message arrives; the value accepted is bound
to the node's result pin, from where the object flows carry it on
(accept_payload_flows_from_pin in the execution-conformance fixtures pins that a
Calibrated accepted as a Reading reaches the consumer with its attribute readable). An
accept that nothing can ever satisfy is the runtime's typed accept deadlock, so an activity
that waits for a signal no node of it sends is a fail naming the wait — where the reference
implementation's execute() returns with the accepter registered and the outputs empty.
A started classifier behavior is the object's own execution: this in it is the object, its
writes land on the object's features, and an accept in it parks the behavior — not the
starter — until a message for the object arrives, which the runtime delivers once the
enclosing run has posted it (the object's behaviors are drained after the top-level
performance ends as well as at the start). The behavior completing leaves the object, which
the activity may still hand out through a parameter (SM44); a start that fails is undone
whole, so the object performs nothing and keeps no write of the failed behavior. A behavior's
body nested in the part def is refereed through the activities that start an object of its
owner (below), since the record runs it only that way: the activity holding the start and every
activity calling it, transitively, of which those the record executes carry the row — fail when
one fails, else pass when one passes — each reason naming the starter and, when the start is
reached through a call, the activity holding it.
Everything else the classifier calls expressible — ReadSelfAction in an activity performed
on its own (its self is the performance, which the suite's TestSignalReceiver writes an
attribute of as fUML treats an execution as an object), a start passing arguments, an edge
weight other than 1, an object-flow cycle through control nodes — is a TranslateError
naming the activity, the node or edge, and the construct: TestSignalReceiver: ReadSelf: reads self in an activity performed on its own, where self is the performance and not an object. The classifier decides expressibility; the emitter
decides what it can translate; a TranslateError on an expressible activity files the row
not-expressible by the emitter, its reason not yet translated: and the construct, the
row's class still expressible so the two judgments stay apart. The two edge rules are
refusals no row of the pinned suite reaches: every weight in both models is the literal
1, and no expressible activity routes an object flow back through a control node, so a
weighted edge (SysML v2 has no per-firing token count on a flow) and such a cycle stay
TranslateErrors proven only by TestEmitRefusesWeightsAndCycles, and a model of the
suite that grew one would file not-expressible by the emitter, never pass by accident.
Every translated model is
checked (Validate) through the parser's diagnostics and the lowering to an action graph
before it is run, so a translation the runtime would reject fails as a translation, with the
diagnostic.
Running. Execute parses the model, resolves the definition, and performs it with the
runtime's ExploreWith: every linearization of the concurrent nodes within the exploration
budget (1024 runs of depth 64 by default), the runs of one activity spread over -jobs
workers, the report the same for any job count because the outcomes are collected as a set.
Every in parameter is given the value the implementation's ExecutionEnvironment.execute
gives it — 0, "", false, 0.0; for a class, a signal or an activity objects are created of,
one instance with every attribute, inherited ones included, defaulted the same way
(Environment.makeValue); for an untyped parameter a String, as the implementation defaults
it — since the JUnit suite passes none. The oracle is the output parameters: each run's
out and inout values are rendered and compared with the record's, a scalar by value, an
ordered multi-valued parameter in order, an unordered one as a multiset (both sides sorted), an
absent optional output as - against a present one, and an object by its classifier and
features — TestClass#1{x = 7; y = 5, 3}, every attribute of the definition the emitted model
declares under that name in name order, an unset one -, an object mentioned again #n — the
objects numbered canonically on each side, since the record's identifiers are aliases and the
runtime's are its own: each side's objects are refined by their type, their features' values
and every parameter or feature holding them until the classes settle, then numbered in class
order, so equal object graphs number alike whichever order either side met them (two objects
alike in every feature, one of which a second parameter also holds, are told apart by that
holder), and objects the refinement cannot tell apart number in order of first mention. A row
passes when every run within the budget agrees with the record and none ends in a runtime
error; the runs need not exhaust the schedules, and status says whether they did
(complete (972 runs), incomplete: runs budget 1024 hit after 1024 runs), so an agreement
over a sample is reported as one. A run that ends in a typed runtime error — a deadlock, a
multiplicity violation, a budget — is a fail with the error as its reason. The
implementation's Fire sequence is compared advisorily: the actions the runs' outputs
show fired here against the record's Fire events, the difference reported as fired and
never a bucket.
The buckets. A not-expressible or differs-by-design classification files the activity
there before any translation, and a differs-by-design row is still translated and run, so
the difference is recorded rather than presumed. An expressible activity the emitter
translates is pass or fail by its run, a run error being a failure; one it refuses is
not-expressible with the construct named, so the count says what is not yet checked.
./scripts/download-fuml-suite.sh # once
go run -C tools ./cmd/fuml-referee # the summary: counts, then every non-pass row with its reasons
go run -C tools ./cmd/fuml-referee -json # the full report, byte-stable
go run -C tools ./cmd/fuml-referee -filter Decision -keep /tmp/fuml # a few activities, their models written out
go run -C tools ./cmd/fuml-referee -jobs 8 -check # reproduce the committed counts
go run -C tools ./cmd/fuml-referee -update # after adjudicating a movement-check and -update refuse -filter, since the counts are the whole suite's; -jobs
below 1 is refused; an absent suite is reported and exits 0 unless
OPENSYSML_REQUIRE_FUML_SUITE is set, as CI sets it. The baseline
docs/project/fuml-referee-baseline.json carries the provenance (tag, commit, the four
digests of the suite files — the two models, the downloaded library the reader resolves
against, and the jar — the activity count, the date and develop commit -update recorded), the bucket
counts and every row with its bucket, reasons, expected and reached outputs, run count and
status. -check compares the provenance first — a moved pin is a different question, never a
moved count — then the four counts, and names the rows that moved between buckets when a
count differs. CI runs it in the fUML suite gate after the reader and classifier tests; the
jar never runs there.
The committed baseline over the 55 activities of both models:
| Bucket | Count | Activities |
|---|---|---|
pass |
23 | Copier, CopierCaller, SimpleDecision, ForkJoin, ForkMerge, NodeEnabler, TestNodeEnabler, TestIntegerFunctions, TestIntegerComparisonFunctions, TestRealFunctions, TestRealComparisonFunctions, TestStringFunctions, GenerateBooleanTestData, GenerateListTestData, TestListFunctions, TestGeneralizationAssembly, TestClassObjectCreator, TestClassWriterReader, TestClassAttributeWriter, TestClassAttributeValueRemover, TestSpecializedSignalSend, ActiveClassBehaviorSender, ActiveClassBehavior |
fail |
0 | |
differs-by-design |
4 | DecisionJoin, ForkMergeData, TestSimpleActivities, TestBooleanFunctions |
not-expressible |
28 | the 15 of the test model and the 12 of the exception model listed above, by the classifier; and by the emitter, TestSignalReceiver (ReadSelfAction in an activity performed on its own) |
Every pilot activity the scope named runs: the eight control- and object-flow activities and
the primitive-function tests pass with every linearization agreeing, seven of them with the
schedules exhausted (ForkMerge 20 runs, TestIntegerComparisonFunctions 972) and the
function tests, whose nodes are all concurrent, over the 1024-run sample; DecisionJoin,
ForkMergeData and TestSimpleActivities are differs-by-design as the scope expected.
The five object activities pass. TestClassObjectCreator creates a TestClass and
returns it, its x unset and y empty; TestClassAttributeWriter and
TestClassAttributeValueRemover take a defaulted TestClass and add to and remove from its
y; TestClassWriterReader creates, writes x and reads it back;
TestGeneralizationAssembly creates a Specific whose General's attribute is reachable
through :> and returns it through an inout parameter. Each agrees with the record over
every linearization, the schedules exhausted (the remover in 18 runs, the writer in 6, the
others in 1 or 2), and the object comparison is by class and feature values, never by identity.
The three active-object activities pass. ActiveClassBehaviorSender creates an
ActiveClass, starts it and sends it a TestSignal and an OtherSignal; its classifier
behavior ActiveClassBehavior — nested in the part def, refereed through its starter since
the record runs it only as part of its owner — accepts both, reads self and writes each signal
into the object's attribute, and the run ends with the starter complete and the object's
behavior done, over all 6 linearizations; the record has no output parameter to hold
against, so the pass is the run's completion, as the reference's own is.
TestSpecializedSignalSend creates an object of the activity TestSignalReceiver, starts it
and sends it a SpecializedSignal; the behavior accepts it as a TestSignal, writes it
through ReadSelf into the object's signal and into its own testSignalOut, and the
starter, which has no output, agrees with the record over all 5 linearizations. Both sides
hold the created object alive after its behavior completes, and neither runs a behavior at
creation.
No row fails, and one is not-expressible by the emitter rather than the classifier,
adjudicated as such: a TranslateError on a construct the emitter does not spell, where the
classifier holds the activity expressible, so the row keeps class: expressible and names
the construct after not yet translated:, told apart from the classifier's 27 in the report.
TestSignalReceiver is the same body the pass above runs as an object's behavior, executed
here on its own as the JUnit suite also does: its ReadSelf then yields the activity
execution, which fUML treats as an object with the activity's attribute signal, and the
emitter has no spelling for a performance written to as an object. The record of that run
is empty — the reference registers the accept on an execution that is no active object, so
nothing ever fires and testSignalOut stays empty — where a SysML v2 performance parked at
the accept would be the runtime's typed accept deadlock, so translating the row would move
it to fail on a difference of the accept model rather than a finding about the executor.
It stays untranslated, its reason naming the construct, and moves only if the emitter grows
a rule for self as a performance or the row is reclassified with that argument.
Eight rows moved since the previous baseline (15 pass, 36 not-expressible), in three
steps of the emitter, each with a re-run of the suite: the rules for classes, object
creation and the structural feature actions moved five (20 pass, 31 not-expressible);
the rules for signals, SendSignalAction and AcceptEventAction moved none on their own,
since the two signal activities of the suite are also the two that use an activity as a
class, so their reasons sharpened and their bucket did not; the rules for a class's owned
behaviors, an activity as a class, ReadSelfAction in an owned behavior and
StartObjectBehaviorAction moved three (23 pass, 28 not-expressible). The referee
gained class-typed default inputs, the object rendering and the refereeing of an owned
behavior through its starter. No other row's bucket changed; the classifier's 27 rows and
the four differs-by-design rows are byte-identical to the previous baseline's, and the one
emitter row that stays not-expressible names the construct it still meets. The signal
rules are proven over synthetic models as well (TestEmitSignals, TestEmitSendSignal,
TestEmitAcceptEvent, TestExecuteSignals), where a Pong :> Ping sent with its inherited
attribute reaches an accept of Ping, and an accept nothing sends to is the runtime's typed
accept deadlock, a fail and never a not-expressible.
| Activity | Movement | Adjudication |
|---|---|---|
TestClassObjectCreator |
not-expressible (emitter) → pass |
Expected. Create(TestClass) is new TestClass(), and the record's object is a TestClass with x unset and y empty, which is what an occurrence whose features nobody wrote holds; the runtime's unset scalar renders - as the record's absent value does |
TestClassAttributeWriter |
not-expressible (emitter) → pass |
Expected. The activity is given a defaulted TestClass — x = 0 and y holding one 0, since the implementation gives every attribute one default value whatever its multiplicity — and the input value = 0, and adds the value to y at position *, which appends (including); the record's y = 0, 0 is what the runtime leaves. Exhaustive over 6 runs |
TestClassAttributeValueRemover |
not-expressible (emitter) → pass |
Expected. Four adds without a position put their values first, as the reference's FirstChoiceStrategy inserts them; a remove without a position drops the first copy of its value and a remove with isRemoveDuplicates drops every copy (excluding); the record's y = 2, 0 is what the runtime leaves. Exhaustive over 18 runs |
TestClassWriterReader |
not-expressible (emitter) → pass |
Expected. Creates a TestClass, writes 999 to x (single-valued: the value replaces), reads x back onto the result pin and returns it; the read is object.x at the feature's multiplicity. Exhaustive over 2 runs |
TestGeneralizationAssembly |
not-expressible (emitter) → pass |
Expected. Specific :> General; the created Specific reaches the inout result parameter through its output node — the node with an incoming edge, told apart from the same parameter's input node — and renders with General's attribute among its own, as the record's does |
ActiveClassBehaviorSender |
not-expressible (emitter) → pass |
Expected. Creates an ActiveClass, starts it (perform object.classifierBehavior.start, the behavior running asynchronously as the object's own execution) and sends it a TestSignal then an OtherSignal; the record has no parameter, so the verdict is the run completing with nothing to compare, as the reference's is. The started behavior is not what the record holds against, but it runs: after the run the ActiveClass performs one completed classifierBehavior with signal1 and signal2 holding the two instances sent, the accepts having fired in whichever order the schedule delivered the signals. Exhaustive over 6 runs |
ActiveClassBehavior |
not-expressible (emitter) → pass |
Expected, by its starter. The record does not execute the owned behavior on its own (skipped: ownedBehavior of ActiveClass: runs only as part of its owner), so the row's bucket is the bucket of the activity that starts an object of ActiveClass — ActiveClassBehaviorSender, pass — and its reason names that starter. Its body is the action def ActiveClassBehavior nested in part def ActiveClass, translated by the same rules as a top-level one; were the starter to fail, this row would fail with it |
TestSpecializedSignalSend |
not-expressible (emitter) → pass |
Expected. Creates an object of the activity TestSignalReceiver (a part def whose classifier behavior 'behavior' is the activity's body), starts it and sends it a SpecializedSignal; the behavior's accept of TestSignal takes the specialized instance, and ReadSelf yields the object, so Write(signal) lands on the object's attribute and testSignalOut on the behavior's own parameter. The record has no parameter; the verdict is the run completing with nothing to compare, as the reference's is. Exhaustive over 5 runs |
TestSignalReceiver |
stays not-expressible (emitter) |
The same body, executed on its own; its ReadSelf is the performance, not an object, and the emitter refuses it (ReadSelf: reads self in an activity performed on its own, where self is the performance and not an object). Adjudicated above: the record's testSignalOut is empty because the reference's accept on a non-object execution never fires, where the runtime would report a typed accept deadlock — a difference of the accept model, not an executor finding |
The four design differences run as the alignment row predicts. DecisionJoin offers
Action_A two tokens through a multiplicity-1 pin; the implementation fires it twice and the
decision routes one result each way, so both branches reach the join. Here Action_A
performs once, one guard holds, and the join waits for the other branch: action deadlock: 1 token(s) stuck. TestBooleanFunctions delivers a four-row truth table to each function's
multiplicity-1 pin: four firings there, multiplicity violation: 4 value(s) bound to a feature with multiplicity upper bound 1 here. TestSimpleActivities calls DecisionJoin and
ForkMergeData and inherits the deadlock. Each row carries its runtime error as a reason so
the difference is visible, but the bucket is the classifier's, not the run's.
One translation rule the pilot forced, no runtime finding. With the nested Copier's
output parameter and ForkMergeData's both spelled output, the model reached two outcomes
over its 20 linearizations, output = 0, 0 (the record's) and output = 0, 0, 0: when
Action_B's second performance ended after the collector had appended the first value, the
runtime returned the nested output to the enclosing output — the same-named enclosing
feature — as SysML v2 has it do (action_invoked_node_body_writes_output pins that), and the
collector then appended to a list that already held it. fUML gives each activity its own
parameters, so the emitter spells a called activity's shared parameter names apart
(Copier_output), and ForkMergeData now reaches one outcome, exhaustively, agreeing with
the record. The runtime behaved as specified; the referee row is differs-by-design for the
re-firing regardless, so no count depends on it.
TestEmit*, TestValidate, TestExecute* and TestReferee* in tools/referee/fuml are the
translation's permanent tests over synthetic models: parameter directions and
multiplicities, the enabling succession beside a flow, fork, join, merge and guarded
decision, a nested call and its qualified name, the typed refusal, the parser-and-lowerer
check, the four comparison rules and the advisory firing difference, the budget sample and the
run error, the buckets' precedence, determinism across job counts, and the baseline's
provenance-then-counts comparison with its moved-row diagnostic; and over an object model, the
part definitions with their generalization and every multiplicity shape, creation, the
replacing, first-position and indexed adds, the indexed remove, the unique feature's one copy,
the read and the clear, the object handed on through result pins, the top-level ReadSelf
refused, and the executed object compared against the record's shape, in order and out of it;
and over an active-object model, the owned behavior nested in its owner's part def with the
classifierBehavior usage, ReadSelf bound to the owner, an activity as a part def whose
body is its 'behavior', the mixed activity refused, the created object handed out unchanged
after its started behavior ran (TestExecuteStartedObject), and the owned behavior's row
taking its starter's bucket (TestRefereeOwnedBehavior). The runtime side — creation performing
nothing, the start running the behavior as the object with this bound, a later message waking
it, a second start running nothing more, an inherited behavior started on the specialized
object, the typed refusals of a start on no object or of no behavior, a failing start undone
whole, and the trace — is TestRuntimeRobustnessClassifierBehaviorStart in
internal/exec/runtime/robustness_classifier_behavior_test.go, with
TestStartedActionAwaitingAMessageIsWokenByASibling beside the materialization tests in
classifier_behavior_test.go.