Skip to content

feat(fuml): translate classes, structural features, signals and active-class behavior in the referee's emitter - #413

Merged
HuiJun merged 43 commits into
developfrom
feature/fuml-emitter-objects-and-signals
Sep 22, 2026
Merged

HuiJun merged 43 commits into
developfrom
feature/fuml-emitter-objects-and-signals

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

What and why

The fUML referee's emitter (tools/referee/fuml/emit.go) translated only the primitive-valued activities of the reference implementation's test models; nine expressible activities stayed not-expressible with a not yet translated: reason because they create objects, write structural features, accept signals or run a class's behavior. This PR grows the emitter over those constructs — classes and structural features, signals, active classes — one rule per construct with its SysML v2 spelling recorded in docs/project/fuml-referee.md, extends the referee so object-valued inputs and outputs execute and compare, and gives the runtime what the rules need: an accepted signal flowing on from the accept's result pin, and a declared behavior started on an explicit perform.

Classes, object creation, structural features

  • fUML Classpart def, generals as :> supertypes, each attribute at its exact declared multiplicity ([0..*] ordered nonunique, [0..3], …; the UML default [1..1] unwritten). A redefining property replaces the one it redefines; classifiers resolve by ID first and only then by name, and external type references never resolve to local classifiers.
  • CreateObjectActionaction N { out result : T = new T(); }. Creation starts no behavior; StartObjectBehaviorAction does (below).
  • ReadStructuralFeatureActionout result … = object.f; at the feature's multiplicity.
  • AddStructuralFeatureValueAction / RemoveStructuralFeatureValueAction / ClearStructuralFeatureActionassign object.f := …; with the reference implementation's semantics: a replacing add or a single-valued feature takes the value; a unique feature drops its old copy; an unpositioned add inserts first (the reference's FirstChoiceStrategy), insertAt inserts at that one-based position with * appending and 0 out of range; a remove drops every copy (isRemoveDuplicates), the copy at removeAt, or the first copy; clear empties. The object flows on through the result pin.
  • Untyped parameters (the reference runs them as String) are declared without a type; an inout parameter's input and output nodes are told apart by their edges.
  • The referee (tools/referee/fuml/run.go, render.go) materializes class-typed inputs the way the reference's Environment.makeValue does (an object per class, features defaulted recursively) and renders object outputs by class and feature values, numbered by graph refinement rather than arrival order so identical objects compare by reference structure; a feature read that fails is a run error, not text.

Signals

  • fUML Signalattribute def with its attributes, specializing its generals, declared before the classes and activities of the closure; a class-typed signal attribute references its class.
  • SendSignalAction → an action sending new <Signal>(…) to the object at its target pin, one argument pin per attribute, inherited ones included.
  • AcceptEventAction of one SignalEvent → an accept node whose result pin is the instance received; an untyped result pin takes the trigger's signal type; a specialized signal satisfies an accept of its general through the runtime's conformance matching.
  • Runtime: the lowering (internal/ir/lower/action_graph.go) records an accept's payload as the node's output feature, and the executor (internal/exec/runtime/action_executor.go) binds the accepted value on the accept's own performance, so flow receiver.msg to consumer.value carries the instance on. An accept whose trigger is a when/at/after condition has no payload and declares no output.

Active classes

  • A class's owned behavior → an action def nested in its part def; its classifier behavior → the member action classifierBehavior : <Behavior>;, a declaration no creation starts. ReadSelfAction in such a behavior → this; at the top level of an activity performed on its own it stays a typed TranslateError (self there is the performance, not an object).
  • An activity instantiated as an object (TestSpecializedSignalSend creates and starts TestSignalReceiver) → a part def of the activity's name around an action def of its body, when the body's shape is representable; a call of a class's owned behavior stays a typed TranslateError.
  • StartObjectBehaviorActionaction N { in object : T; perform object.classifierBehavior.start; }, the classifier behavior found on the object's class or inherited from a general; a start passing arguments, of an object of a class without classifier behavior, or without an object pin is a typed TranslateError.
  • An owned behavior's row is refereed through the top-level activities whose run starts an object of its owner — the activity holding the start and every activity calling it, transitively, of which those the record executes carry the row (ActiveClassBehavior through ActiveClassBehaviorSender) — 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.
  • Runtime: perform obj.beh.start; lowers to a start effect (lower.EffectStart) only where the operand, resolved through the scope tree, is a behavior the object holds or a feature declaring no start of its own — an action a part def declares as action start : Launch; is performed as any other feature — and the effect is what internal/exec/runtime/start_behavior.go runs as the object's own execution — this the object, its writes on the object's features, a message sent afterwards waking an accept it parks at, the object outliving the behavior's completion. Object behaviors are drained after the enclosing top-level action completes, so a sender's signals reach a behavior it started in the same run. A second start of a running behavior starts nothing more; a start on no one object or of a member that is no behavior of the object is a typed error; a start that fails is undone whole — its own work: an older parked behavior a message of the started one wakes is drained only once the start is kept, within a run boundary as a store's, so its move is never inside the start's undo; the trace records the start as the object's execution. A performed action that names no separate body (perform action a { … }) keeps resolving to itself.

Runtime fixes the referee surfaced

  • Two flows out of one pin deliver twice. A value a streaming flow carries to a target not yet under way waits at the target's pin, keyed until now by source performance and pin alone, so the two flow Action_A.result to Action_B.input; routes of a fUML fork duplicating a token collapsed into one delivery and the second performance of the nested Copier ran with its required input unbound. Each flow declaration is a transfer of its own: internal/exec/runtime/action_frame.go stage keys the waiting place by source performance, pin and flow declaration (held images carry the flow), and internal/exec/smt/encode.go stagedSlot keys the encoding's slot the same way, so a later write along the same flow still replaces its earlier one while two flows each stage the write. action_flow_streaming_aliased_source_pin (a pin named by its inherited and its redefining name, two flows) states the contract and expects two deliveries; action_flow_streaming_two_flows_one_pin_to_call and stream_stage_test.go lock the fork shape. ForkMergeData reaches its recorded 0, 0 again; it stays differs-by-design for per-token re-firing alone. Fragment changes/unreleased/streaming-flow-per-declaration-transfer.fixed.md; the streaming-flow row of spec-compliance.md carries the rule.
  • Object-typed parameters are referential; a port's directed feature is not. Materializing an occurrence gives a behavior's object-typed parameter no object of its own (an object flows into it), which is what lets a started behavior's in parameter bind to the object it is performed on. semantics.IsBehaviorParameter restricts this to parameters a behavior or step owns, so a port definition's in item cmd : Cmd; keeps its composite value and an exhibited state machine's parameter binds from engineControl.cmd (exhibited_state_self_target).

Not translated

An edge weight other than 1 and an object-flow cycle through control nodes remain typed translation refusals naming the edge; no activity of the pinned suite has either (all 439 test-model and 93 exception-model weights are 1), so no row depends on them. TestSignalReceiver stays not-expressible: it reads self as a top-level activity performed on its own.

Referee

./scripts/download-fuml-suite.sh && go run -C tools ./cmd/fuml-referee -jobs 8:

bucket before after
pass 15 23
fail 0 0
not-expressible 36 28
differs-by-design 4 4

Moved to pass: TestGeneralizationAssembly, TestClassObjectCreator, TestClassWriterReader, TestClassAttributeWriter, TestClassAttributeValueRemover (objects and structural features); TestSpecializedSignalSend, ActiveClassBehaviorSender, ActiveClassBehavior (signals and active classes). Every moved row is adjudicated in docs/project/fuml-referee.md; the classifier's 27 rows did not move; the fixed expected record for ActiveClassBehavior (skipped, runs only as part of its owner) is unchanged. The baseline's pin — tag v1.5.0a, commit 45e50633…, the four digests — is unchanged; only its develop provenance advanced with the merge of develop.

Specification basis

fUML 1.5 (formal/2021-07-01) § 8.6 Actions — CreateObjectAction, ReadSelfAction, the structural feature actions, SendSignalAction, AcceptEventAction, StartObjectBehaviorAction — and § 8.8 object-behavior startup (creation starts nothing; an explicit start does), as the pinned reference implementation (v1.5.0a) executes them. docs/project/spec-compliance.md: the fUML referee row carries the counts above, and a new row records the declared-behavior start (passive construction, explicit perform obj.beh.start;, this, message wake-up, owner preservation, duplicate start, inherited behavior, typed failures, rollback, trace). docs/internals/design/precise-semantics-alignment.md states the same lifecycle.

How it was verified

  • tools/referee/fuml/emit_test.go: TestEmitClasses, TestEmitObjectCreationAndFeatureWrites, TestEmitFeatureReads, TestEmitReadSelf, TestEmitActivityAsObject, TestExecuteStartedObject, TestExecuteObjects, TestExecuteUnorderedObjects, TestExecuteIdenticalObjectsNumberByReference, TestEmitSignals, TestEmitRedefinedProperties, TestEmitSignalTypedAttribute, TestEmitClassTypedSignalAttribute, TestEmitPrimitiveNamesakeAndPositionedScalarRemove, TestEmitExternalTypesNeverLocalClassifiers, TestEmitIDReferencesNeverResolveByName, TestExecuteInsertAtZeroIsOutOfRange, TestEmitSendSignal, TestEmitAcceptEvent, TestRenderSignalValues, TestRenderNumbersObjectsByStructureNotArrival, TestExecuteSignals, TestEmitRefusesWeightsAndCycles on hand-built models; every emitted model passes Validate. referee_test.go: TestRefereeOwnedBehavior, TestRefereeOwnedBehaviorStartedThroughACall.
  • internal/ir/lower: TestPerformOfStartLowersToStartEffect, TestPerformOfBehaviorStaysPerform, TestPerformOfDeclaredStartActionStaysPerform, TestAcceptNodePayloadIsItsOutputPin.
  • internal/exec/runtime: TestRuntimeRobustnessClassifierBehaviorStart (passive construction, start as the object, wake by a later message, second start, inherited behavior, start on no object, start of no behavior, an action declared as start performed rather than started, failing start undone, trace), TestStartedActionAwaitingAMessageIsWokenByASibling, TestRuntimeRobustnessAcceptPayload, conformance pairs accept_payload_flows_from_pin, action_flow_streaming_two_flows_one_pin_to_call, action_flow_streaming_aliased_source_pin (two deliveries), stream_stage_test.go:TestStagedStreams (same-flow replacement, distinct flows side by side); internal/semantic/semantics/shape_test.go:TestIsBehaviorParameter.
  • go build ./..., go vet ./..., gofmt -l . (empty), go test ./... (root and tools/), make lint, make docs-check, python3 scripts/changelog.py check, mkdocs build --strict — all clean on the merged head.
  • OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 go test -count=1 ./tests/corpus -run 'TestTrainingExamples|TestPilotCorpora' clean.
  • go run -C tools ./cmd/fuml-referee -jobs 8 -check clean; -json twice byte-identical.

Checklist

  • make test and make lint pass locally
  • Tests added or updated for the change
  • Documentation extended where it already covers the surface (see CONTRIBUTING.md)
  • Changelog entry added as changes/unreleased/<slug>.<section>.md, not as an edit to CHANGELOG.md
  • baselines regenerated and make docs-counts run if a gate count moved (compliance rows need nothing: the census is counted at docs build)
  • No internal work-item labels (waves, slices, F4, K5) in the body, docs, or changelog

… actions

A fUML Class becomes a part def with its generals as supertypes and every
attribute at its exact multiplicity; CreateObjectAction is `new T()` on the
result pin and starts no behavior; Read/Add/Remove/ClearStructuralFeatureAction
read or assign the object's feature as the reference implementation computes
it (replacing add, first-position insert, indexed insert and remove, unique
copies dropped, first or every copy removed) and hand the object on through
the result pin. Untyped parameters stay untyped; an inout parameter's two
nodes are told apart by their edges.

The referee materializes class-typed inputs as the reference does (an object
per class with defaulted features) and renders objects by type and feature,
numbered by first mention, so object outputs compare.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

devin-ai-integration Bot and others added 5 commits September 18, 2026 19:14
The referee baseline records 20 pass, 0 fail, 31 not-expressible and 4
differs-by-design: TestGeneralizationAssembly, TestClassObjectCreator,
TestClassWriterReader, TestClassAttributeWriter and
TestClassAttributeValueRemover move from not-expressible to pass, each
adjudicated in docs/project/fuml-referee.md with its construct map row.
A CreateObjectAction whose classifier is an activity is refused naming
the activity rather than reported as an unknown class.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
… in the referee's emitter

A fUML Signal with its attributes and generalizations is an `attribute def` specializing its generals, declared before the classes and activities of the closure; a SendSignalAction is an action sending `new <Signal>(...)` to the object at its target pin with one argument pin per attribute, inherited ones included; an AcceptEventAction of one SignalEvent is an `accept` node whose result pin is the instance received. A specialized signal satisfies an accept of its general through the runtime's conformance matching.

The lowering now records an accept's payload parameter as the node's output feature and the executor binds the accepted value on the accept's own performance, so an object flow out of the accept pin carries the instance on.

The suite's signal activities also use an activity as a class, so their reasons sharpen and their buckets stay; the referee record and the baseline's reasons are updated, the counts unchanged.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…er-objects-and-signals

Co-Authored-By: jason.han <hanhuijun@gmail.com>

# Conflicts:
#	internal/exec/runtime/robustness_accept_payload_test.go
#	internal/exec/runtime/testdata/conformance/accept_payload_flows_from_pin.expected.json
#	internal/exec/runtime/testdata/conformance/accept_payload_flows_from_pin.sysml
@HuiJun
HuiJun marked this pull request as ready for review September 19, 2026 22:51
…er-objects-and-signals

Co-Authored-By: jason.han <hanhuijun@gmail.com>

# Conflicts:
#	tools/referee/fuml/emit.go
devin-ai-integration[bot]

This comment was marked as resolved.

…d signal attributes, lower accept triggers without a payload pin

An unordered output holding objects is spelled in an order its values fix,
not the order they arrived in, so equal multisets compare equal whichever
object reached the parameter first.

A signal attribute typed by a class is a reference to an object of that
class, spelled as a class attribute is, and the class closure follows a
signal's attributes so the referenced class is declared.

An accept whose trigger is a condition or a time (accept when/at/after)
produces no value, so it declares no output feature; only a message
payload is the accept's output pin.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

… removeAt on scalars

A class or signal of the model named Integer, Boolean, String or Real is that
classifier wherever the model references it; the primitive is then spelled
ScalarValues::<name> so the package-local part def does not shadow it. A
RemoveStructuralFeatureValueAction with a removeAt pin on a single-valued
feature empties it at position 1 regardless of the value pin, as the
reference implementation removes by position.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

A class's or signal's effective attributes held both an inherited property
and the one redefining it, so a specialized signal with one logical attribute
expected two argument pins and a redefined class attribute was declared twice.
The reader keeps each property's redefinedProperty references, AllAttributes
drops what another effective property redefines, and the emitter spells the
redefinition as `:>>`.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…t includingAt validate every insertion position

An external type reference (an href fragment from the UML primitive types or
the fUML library) names no class or signal of the model, whatever XMI id a
local classifier carries; ClassOf, SignalOf, primitive and the classifier's
behavior lookup all return none for it. A positioned add hands every position
but `*` to includingAt, so a position of 0 is the runtime's out-of-range
error rather than an insertion first.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…mention

Two recorded objects alike in every feature but held by different parameters
numbered by the order the unordered parameter met them, so an equivalent
object graph could render with swapped aliases on the two sides. The renderer
now builds each side's object graph, refines the objects by type, feature
values and holders until the classes settle, and numbers them in class order,
falling back to first mention only for objects nothing tells apart.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

A fUML signal instance is a value and the reference implementation's record
gives it no identity, so a signal one run instance delivers to two outputs
must spell whole in both, not as an alias of its first mention. The run-side
renderer keys objects by instance and signals by mention, re-entering an
instance already being read by identity so a cyclic reference stays bounded.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 6 commits September 20, 2026 22:48
…nd SignalOf

A reference carrying an XMI id names that element and no other: a miss on
the class map no longer falls back to a class sharing the name of the
signal the id names, and symmetrically for signals. Name lookup serves
only references without an id.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…zing occurrences

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 5 commits September 21, 2026 14:57
…ect numbering on the partition

A run object whose declared feature cannot be read no longer spells as
comparison text: renderOutputs returns the runtime's error and compare
files it with the run's errors, so the row reads as a run error rather
than as outputs differing.

Canonical numbering refines from each object's class so far, making each
round a split of the one before; a stable class count is then a settled
partition, and objects the same graph mentions in different orders
number alike.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…xplicit perform

A class's owned behavior is emitted as an action def nested in its part def and its classifier behavior as a declared action member no creation starts; an activity instantiated as an object becomes such a part def around its own body, ReadSelfAction in an owned behavior binds `this` to the owner and StartObjectBehaviorAction becomes `perform object.classifierBehavior.start;`. An owned behavior's referee row inherits the verdict of the activity that starts an object of its owner.

The lowering gives a perform naming a behavior's `start` its own effect kind and the runtime starts the behavior as the object's own execution: journaled and rolled back whole on failure, a second start of a running behavior a no-op, an inherited member found on the specialized object, and the object's behaviors drained again once the enclosing top-level performance ends so a message sent after the start wakes an accept the behavior parked at. A weight other than 1 and an object-flow cycle through control nodes stay typed translation refusals.

ActiveClassBehaviorSender, ActiveClassBehavior and TestSpecializedSignalSend move to pass: 23 pass, 0 fail, 28 not-expressible, 4 differs-by-design.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
ActiveClassBehaviorSender, ActiveClassBehavior and TestSpecializedSignalSend move from not-expressible to pass: 23 pass, 0 fail, 28 not-expressible, 4 differs-by-design, the pin and digests unchanged. A behavior a type declares for its objects to start carries whether it names the element holding its body, as an exhibited or performed one does.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

An activity the model instantiates as an object is its part definition wherever a reference names it by id, even when it bears a ScalarValues name; the scalar is then spelled qualified beside it, as it is beside a class or signal of that name. The renderer reads the features of an object of an activity classifier from the activity's attributes instead of rendering it unknown.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 2 commits September 21, 2026 21:57
…, not the model's names

The renderer and the input defaults looked a runtime type name up among the model's classes, then signals, then activities, so an object of an activity that shares its name with a class or a signal took the namesake's attributes, and an activity-typed input parameter had no default at all. Both now resolve through the translation's closure, whose package declares one definition per name: the renderer reads the attributes of the part or attribute definition the emitted model declares under the type's name, and an input typed by any classifier the package declares — a class, an instantiated activity or a signal — defaults to a fresh instance whose attributes hold their types' defaults, as Environment.makeValue does.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

…avior through callers of its starter

A `perform` chain ending in `start` is lowered to a start effect only where the `start` is the shot every behavior has: an action a part def declares under that name is performed as any other feature. Lowering resolves the operand through the scope tree; a path it cannot follow is left as written, so a start of a non-behavior still reaches the runtime's typed refusal.

The fUML referee files a class's owned behavior by the activities whose run starts an object of the class: the activity holding the start and every activity calling it, transitively, of which those the record executes carry the row.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration Bot and others added 3 commits September 21, 2026 23:15
A value a streaming flow carries to a target not yet under way waited under the source performance and pin alone, so two flow declarations out of one pin into one target pin collapsed into one delivery, and a second performance of a nested action definition with a required input ran with it unbound. Each flow declaration is a transfer of its own: the runtime, the held image and the smt encoder key the staged place by source performance, pin and flow. Two flows naming one pin by its inherited and redefining name now deliver twice; ForkMergeData on the fUML suite reaches its recorded output again.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
…zing objects

Only a behavior's or step's parameter is referential; a port definition's `in item` is a flow item the port holds, so an exhibited state machine's parameter can bind from it.

Co-Authored-By: jason.han <hanhuijun@gmail.com>
devin-ai-integration[bot]

This comment was marked as resolved.

A message the started behavior sends could wake an older parked behavior
inside the start's journal; a failure then rolled back the writes and the
bus but left that behavior past its accept. The start now drains within a
run boundary, as a store does, and the woken behaviors run after commit.

Co-Authored-By: jason.han <hanhuijun@gmail.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Devin Review

Comment thread internal/exec/runtime/action_executor.go
@HuiJun
HuiJun merged commit dcb58a8 into develop Sep 22, 2026
15 checks passed
@HuiJun
HuiJun deleted the feature/fuml-emitter-objects-and-signals branch September 22, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant