From 3a9c595e4767cf42032190dc097264ec228b764e Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 20 Sep 2026 17:02:23 +0000
Subject: [PATCH 1/5] feat(runtime): invoke an operation with a positional
argument list
InvokeOperationWith takes OperationArguments, positional or named, binding a
positional list to the operation's effective input parameters in signature
order (semantics.Model.SignatureParametersOf) and selecting among same-named
members through SelectAmongArguments. A mixed list is ErrMixedArguments and a
surplus ErrOperationArity. %invoke accepts bare expressions beside its
= pairs.
Co-Authored-By: jason.han
---
.../positional-invoke-arguments.added.md | 1 +
docs/project/roadmap.md | 52 +++++++--
docs/project/spec-compliance.md | 5 +-
.../exec/runtime/classifier_behavior_test.go | 102 +++++++++++++++++
internal/exec/runtime/errors.go | 8 ++
internal/exec/runtime/invoke_operation.go | 88 +++++++++++----
.../robustness_positional_invoke_test.go | 106 ++++++++++++++++++
.../frontend/repl/classifier_behavior_test.go | 21 +++-
internal/frontend/repl/meta.go | 77 ++++++++++---
internal/frontend/repl/object_args_test.go | 3 +-
internal/semantic/semantics/binding.go | 20 ++++
11 files changed, 431 insertions(+), 52 deletions(-)
create mode 100644 changes/unreleased/positional-invoke-arguments.added.md
create mode 100644 internal/exec/runtime/robustness_positional_invoke_test.go
diff --git a/changes/unreleased/positional-invoke-arguments.added.md b/changes/unreleased/positional-invoke-arguments.added.md
new file mode 100644
index 000000000..14a5cc7f0
--- /dev/null
+++ b/changes/unreleased/positional-invoke-arguments.added.md
@@ -0,0 +1 @@
+- **An operation is invoked with a positional argument list.** `Context.InvokeOperationWith` takes `OperationArguments`, positional or named, and `%invoke ` accepts bare expressions (`%invoke rover drive 10 20`) beside its `=` pairs. Positionals bind the operation's `in`/`inout` parameters in signature order, a trailing defaulted parameter may be omitted, and among same-named operations the one the arguments fit is selected as an invocation expression would. A list mixing the two forms is refused (`ErrMixedArguments`), as is a surplus argument (`ErrOperationArity`).
diff --git a/docs/project/roadmap.md b/docs/project/roadmap.md
index 7b7eea43f..60c54cd85 100644
--- a/docs/project/roadmap.md
+++ b/docs/project/roadmap.md
@@ -1396,9 +1396,28 @@ No IR change; proof fixtures written in the record; two routing points to settle
**Prioritize when** a model relies on an exhibited machine to refuse an arrival, or on a port
definition's machine at all.
-## E6 — operation invocation with positional arguments
-
-**Today.** `Context.InvokeOperation(inst, name, args map[string]Value)`
+## E6 — operation invocation with positional arguments (landed)
+
+**Landed.** `Context.InvokeOperationWith(inst, name, OperationArguments{Positional, Named})`
+(`runtime/invoke_operation.go`) takes the ordered list; `InvokeOperation` keeps the named map and
+delegates to it. A positional list binds to the operation's effective input parameters —
+`semantics.Model.SignatureParametersOf`, the `in`/`inout` parameters `signatureOf` gives an
+invocation expression in signature order, `out` and result excluded — so a trailing defaulted
+parameter may be omitted, an `inout` parameter takes a position and comes back as a result, and an
+`out` parameter takes none. Among same-named members, `operationOf` selects through
+`semantics.Model.SelectAmongArguments`, the overload selection the expression evaluator uses, so
+two calcs of one name are told apart by arity and a list neither takes is refused. A surplus is
+`ErrOperationArity` (`operation … takes N input parameter(s), got M argument(s)`), a list mixing
+the two forms is `ErrMixedArguments`, and a required parameter left unbound is still
+`ErrUnboundParameter`. The REPL's `%invoke ` takes bare expressions or `=`
+pairs (`repl/meta.go` `parseInvokeArguments`), refusing a mixed list and a parameter named twice
+before the object is reached. Proof: `runtime/classifier_behavior_test.go`
+`TestInvokeOperationWithPositionalArguments`, `runtime/robustness_positional_invoke_test.go`,
+`repl/classifier_behavior_test.go` `TestInvokeBindsPositionalArguments` and
+`TestInvokeReportsItsFailureModes`; the bullet left the compliance list. The gRPC surface exposes
+no operation invocation, so nothing there changed.
+
+**Before it landed.** `Context.InvokeOperation(inst, name, args map[string]Value)`
(`runtime/invoke_operation.go`) runs a member of an object's type with the object as performer,
whichever behavior the member is — an action through `ExecuteActionPerformedBy`, a calc through
the calc invocation with the object as its featuring object, a constraint through condition
@@ -2987,9 +3006,11 @@ carried more than that list. By track, with the pull requests the tracks cite:
document's several views (#349) and opens on demand (#348), writes layout into the document
that declares the element across the workspace (#307), and reparents by drag (#305).
- **Track E** — E1 landed (`terminate` runs in every position, the PSSM `terminate-gap` bucket
- retired); E9 and E10 landed as conformance findings; E8's refusal landed (#229), the item
- itself is open; E3 closed by its design record ([expansion-regions.md](expansion-regions.md):
- the iterative form is `for`, the parallel form is not SysML v2), no executor work following.
+ retired); E6 landed (a positional argument list on `InvokeOperationWith` and `%invoke`, bound
+ to the effective signature an invocation expression binds to); E9 and E10 landed as conformance
+ findings; E8's refusal landed (#229), the item itself is open; E3 closed by its design record
+ ([expansion-regions.md](expansion-regions.md): the iterative form is `for`, the parallel form
+ is not SysML v2), no executor work following.
- **Track D** — D12 (the standard library's normative element ids) is done.
- **Release follow-through** — R4's Windows installer is published by `v0.7.0` and `v0.8.0`
alike; the release procedure runs git-flow (#151); `opensysml` 0.5.0 is on PyPI; the
@@ -3001,9 +3022,17 @@ The open items, by track, with the item that gates each where one does. Everythi
is landed or is a track the previous baseline left as it stands (D, N, M, I, V, B, R2–R5);
Tracks F, S, L and A are closed.
+<<<<<<< HEAD
- **Track E** — eligible and first: E2, then E4 (E1 landed), then E6 on request, E3 closed by
its design record, E5 closed by its record (an optional follow-up waits on a model that needs
it), E7 behind its object-model item, E8 behind a model that needs it. The
+||||||| parent of cab80e653 (feat(runtime): invoke an operation with a positional argument list)
+- **Track E** — eligible and first: E2, then E4 (E1 landed), then E6 on request, E3/E5 behind
+ their design records, E7 behind its object-model item, E8 behind a model that needs it. The
+=======
+- **Track E** — eligible and first: E2, then E4 (E1 and E6 landed), E3/E5 behind
+ their design records, E7 behind its object-model item, E8 behind a model that needs it. The
+>>>>>>> cab80e653 (feat(runtime): invoke an operation with a positional argument list)
PSSM referee's 17 `fail` tests are the state side's measurement, every one attributed (#326):
eleven wait on the region-order choice point whose design record #342 wrote and left at two
maintainer decisions — the nine the record names to move `fail` → `pass`, plus *Terminate 001*
@@ -3116,12 +3145,11 @@ an empty action end its performance. The decision is the release checklist's, re
- **Track S.** Landed in the order agreed: S1 (#110), S2 (#123), S3 (#125), S4 (#134); #141 added
the region-order choice point afterwards. Nothing remains in the track.
- **Track E.** Eligible — step 1 above. **E1** (termination of an ongoing performance, which
- **E2** and **E4** build on) is landed; the order is **E2**, then **E4**; **E6** whenever asked,
- being a day's work; **E3**'s record is landed and closes the item; **E5**'s record is landed and
- closes the item, its optional follow-up waiting on a model that needs it; **E7** after the
- object-model item it depends on; **E8** when a model redefines run-to-completion, its refusal
- (#229) standing until then; **E1**, **E9** and **E10** are landed; **E3** is closed by its
- record, no work following.
+ **E2** and **E4** build on) is landed; the order is **E2**, then **E4**; **E6** is landed;
+ **E3**'s record is landed and closes the item; **E5**'s record is landed and closes the item,
+ its optional follow-up waiting on a model that needs it; **E7** after the object-model item it
+ depends on; **E8** when a model redefines run-to-completion, its refusal (#229) standing until
+ then; **E1**, **E9** and **E10** are landed; no work follows E3.
- **Track X.** X2, X3, X4, X5, X6, X7's values and X8's typing landed (#164, #115, #113, #211,
#122, #121, #112). What is left, in order: X8's harness halves (normalization and adjudication
in the pilot differential, a standalone RDF expression-tree round trip) so every later X item is
diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md
index 830334e91..a69dfd185 100644
--- a/docs/project/spec-compliance.md
+++ b/docs/project/spec-compliance.md
@@ -853,7 +853,7 @@ checked after the result is bound is not a form the runtime offers, and none is
| An object's parameter space is its own: an action's `out` parameter answers the caller even where the performing object declares a feature of that name | `runtime/action_statements.go` `assignOuter`; `runtime/action_executor.go` `declaresParameter` | `runtime/classifier_behavior_test.go:TestOperationOutputNamedLikeAFeatureAnswersTheCaller` | ✅ Faithful |
| A failed materialization leaves no behavior of the object attached or queued, and an edited model drops an object whose behavior body changed rather than resuming it on the values the old body wrote | `runtime/classifier_behavior.go` `startClassifierBehaviors`/`forgetBehaviorsFrom`; `runtime/adopt.go` `writeBoundBehaviors` | `runtime/classifier_behavior_test.go:TestFailedMaterializationLeavesNoBehaviorBehind`, `repl/classifier_behavior_test.go:TestRewritingTheExhibitedMachineDropsTheObject`, `:TestObjectMachineSurvivesAnUnrelatedDeclaration` | ⚠️ Approximate (tool-defined: the spec describes one fixed model, so what a live execution does when the model is edited is a REPL policy) |
| A second materialization of one name is a second object, with its own identity and its own behaviors; `occurrenceOf` remains the reuse path for a named occurrence | `runtime/instance.go` `instantiateOwnedBy`; `repl/query.go` `instantiateNamed` (which object the name now denotes) | `robustness_test.go:second_instantiation_of_one_type`, `repl/classifier_behavior_test.go:TestSecondInstantiateIsAnotherObject` | ⚠️ Approximate (tool-defined; the spec leaves object creation semantics open) |
-| Invoking an operation of an object's type runs it with that object as performer, whichever behavior the member is — an action, a calc or a constraint — binding named arguments as the call machinery binds them | `runtime/invoke_operation.go` `InvokeOperation` (`operationOf` settles which behavior the member is; an action runs through `ExecuteActionPerformedBy`, a calc through the existing calc invocation with the object as its featuring object, a constraint through the existing condition evaluation against the object); `runtime/eval.go` `evalInvocation` preserves that performer for nested calc invocation expressions; `repl/meta.go` `%invoke` | `runtime/classifier_behavior_test.go:TestInvokeOperationPerformedByTheObject`, `:TestCalcInvocationExpressionSeesPerformingObject` (an action, direct and nested calcs reading a mutated feature, an anonymous result, and a constraint), `:TestInvokeOperationFailureModes`, `robustness_test.go:operation_invoked_with_unbound_parameters`, `:operation_constraint_body_cannot_be_evaluated`, `repl/classifier_behavior_test.go:TestInvokeRunsAnOperationOnTheObject`, `:TestInvokeReportsItsFailureModes` | ✅ Faithful, **self-assessed** (the pinned reference cannot invoke an operation on an object, so nothing external adjudicates this). A calc's value comes back under its result parameter's name, or `result` where the result is anonymous, and a constraint's verdict comes back under `result` — a `false` verdict is an answer, not `ErrViolated`, since the invocation asked for the value. A body that cannot be evaluated is still an error. A state member is refused with `ErrUnsupportedClassifierBehavior`; its exhibited state machine runs during materialization instead. A member that is no behavior at all is `ErrNotABehavior`. Arguments bind by name because no invocation surface expresses positional operation arguments — an operation *call written in a model* is an InvocationExpression and is bound by the expression machinery, not here |
+| Invoking an operation of an object's type runs it with that object as performer, whichever behavior the member is — an action, a calc or a constraint — binding named arguments as the call machinery binds them, or a positional list to the effective input parameters in signature order (KerML §8.2.5.8.3: an `ArgumentList` is positional or named, never a mix; §8.4.4.9.5: a positional list binds parameters in declaration order) | `runtime/invoke_operation.go` `InvokeOperation` (named) and `InvokeOperationWith` (`OperationArguments`, positional or named; `operationOf` settles which behavior the member is and, among same-named members, selects one through `semantics.Model.SelectAmongArguments` — the overload selection an invocation expression uses; `operationInputs` binds a positional list to `semantics.Model.SignatureParametersOf` — `signatureOf`'s `in`/`inout` parameters in signature order, `out` and result excluded — refusing a surplus with `ErrOperationArity` and a mixed list with `ErrMixedArguments`; an action runs through `ExecuteActionPerformedBy`, a calc through the existing calc invocation with the object as its featuring object, a constraint through the existing condition evaluation against the object); `runtime/eval.go` `evalInvocation` preserves that performer for nested calc invocation expressions; `repl/meta.go` `%invoke` (`parseInvokeArguments`: bare expressions or `=` pairs, a mixed list and a parameter named twice refused before the object is reached) | `runtime/classifier_behavior_test.go:TestInvokeOperationPerformedByTheObject`, `:TestInvokeOperationWithPositionalArguments` (a defaulted trailing parameter omitted, an `inout` read and written, an `out` taking no position, two same-named calcs told apart by arity, fewer, surplus and mixed lists), `robustness_positional_invoke_test.go:TestRuntimeRobustnessPositionalInvoke`, `repl/classifier_behavior_test.go:TestInvokeBindsPositionalArguments`, `:TestCalcInvocationExpressionSeesPerformingObject` (an action, direct and nested calcs reading a mutated feature, an anonymous result, and a constraint), `:TestInvokeOperationFailureModes`, `robustness_test.go:operation_invoked_with_unbound_parameters`, `:operation_constraint_body_cannot_be_evaluated`, `repl/classifier_behavior_test.go:TestInvokeRunsAnOperationOnTheObject`, `:TestInvokeReportsItsFailureModes` | ✅ Faithful, **self-assessed** (the pinned reference cannot invoke an operation on an object, so nothing external adjudicates this). A calc's value comes back under its result parameter's name, or `result` where the result is anonymous, and a constraint's verdict comes back under `result` — a `false` verdict is an answer, not `ErrViolated`, since the invocation asked for the value. A body that cannot be evaluated is still an error. A state member is refused with `ErrUnsupportedClassifierBehavior`; its exhibited state machine runs during materialization instead. A member that is no behavior at all is `ErrNotABehavior`. Arguments bind by name or by position on the Go API and the REPL alike; the gRPC surface exposes no operation invocation. A positional list is bound to the same effective signature an invocation expression is bound to, so the two surfaces cannot drift |
| An object typed by a behavior — a performance occurrence, the `action def` a v1 activity becomes when a part performs it — runs no classifier behaviors of its own: its `perform`/`exhibit` members are steps of the performance that runs it, not behaviors bound to the occurrence, so a body's `action call : Sub;` runs once as a step and `this` in it is the performer; a reference that is no action is `ErrNotABehavior` | `runtime/classifier_behavior.go` `classifierBehaviorsOf` (a behavior type binds none); `runtime/signal.go` `isBehaviorType`; `runtime/invoke_action.go` `actionCandidates` (`ErrNotABehavior`) | conformance `performed_action_def_perform_members_are_steps` + `.expected.json`; `migrate/opaque_migration_test.go:TestSwimlaneBodiesAndGuardsRunAgainstTheRepresentedPart` (a migrated workflow whose action body performs a sub-activity through its performer) | ✅ Faithful, **self-assessed** (KerML §8.4.4.3: `performances` are bound to the objects of a `Class` that is not itself a `Behavior`; a `Behavior`'s steps are its own `subperformances`) |
| A `perform action x ::> part.action;` usage (an action usage referencing a feature chain) performs the chain's last action with the object the chain reaches as its performer, so `this` in the performed body is that object — `tel.point` moves the telescope, not the station; an action usage referencing or typed by a qualified name still runs on the caller's own performer. The chain is evaluated in the caller's context when the node fires: a chain holding no object is `ErrPerformerNotObject`, one holding several objects is refused, one whose last segment is not an action is `ErrPerformerNotObject` too, and one reaching an object that was destroyed is `ErrOccurrenceDestroyed` naming when it was destroyed | `parser/defusage.go` (a `::>` reference to a feature chain on an action usage); `lower/action_graph.go` `performsAction` (`RelReferences` to a `FeatureChainExpr`); `runtime/invoke_action.go` `actionInvocation` (target or chain), `performerOf` (the chain evaluated over the caller's `self`, resolved to an instance through `Context.Instance`), `beginCallee`; `runtime/action_executor.go` `stepNestedAction` | `parse/perform_action_on_part.golden`; conformance `perform_action_on_part` + trace golden; `robustness_perform_on_part_test.go:performer_holds_no_object`, `:performer_holds_several_objects`, `:chain_ends_in_no_action` (`ErrPerformerNotObject`), `:performer_was_destroyed` (`ErrOccurrenceDestroyed`); `migrate/behavior_test.go:TestActivityMigratesToAnExecutableActionDef` (a migrated v1 `CallOperationAction` on a target pin runs through this form) | ✅ Faithful, **self-assessed** (SysML v2 §7.16: an action usage that references another performs it as a `subperformance` of the referenced feature's featuring occurrence; the pinned reference cannot execute one) |
| A typed usage that also references a feature chain, `perform action spin : Motor::Spin ::> drive.motor.spin { in rpm = 30.0; }`, performs the chain's last action on the object the chain reaches, as the untyped form does, and its body binds the callee's `in` parameters by name: the reference names both the action and its performer, so it settles what is performed where the typing only restates the action's definition; an unqualified name the body reads that no argument binds is read from the caller's scope when the callee is performed. A chain holding no object or ending in no action is refused with `ErrPerformerNotObject` | `parser/defusage.go` (a typing followed by a `::>` reference to a feature chain); `runtime/invoke_action.go` `nestedInvocation` (the reference subsetting wins over the typing), `referencedInvocation`; `runtime/action_frame.go` (invocation inputs bound from the body's `in p = …` arguments, then from the enclosing scope for an unqualified callee) | `parse/perform_typed_subsetting.golden`; conformance `perform_typed_action_on_nested_part`, `accept_payload_bound_into_typed_action` + trace goldens; `robustness_perform_typed_on_part_test.go:reference_names_the_performer`, `:performer_holds_no_object`, `:chain_ends_in_no_action`; `migrate/interaction_test.go`, `migrate/call_port_test.go`, `migrate/reception_test.go` (a migrated v1 call message, a `CallOperationAction` routed over a port and a reception's method run through this form) | ✅ Faithful, **self-assessed** (SysML v2 §7.16: an action usage typed by a definition and referencing a feature performs the referenced feature; the pinned reference cannot execute one) |
@@ -2366,7 +2366,7 @@ are tracked here):
### Major Features Not Implemented (UML-referenced; no SysML v2 notation or KerML performance)
-The behavior-execution entries below — interruptible regions, expansion regions, streaming pins, positional operation arguments, routing to a second object — each have a stated scope, dependency order and acceptance gate as [Track E of the roadmap](roadmap.md#track-e--behavior-execution); none is near-term.
+The behavior-execution entries below — interruptible regions, expansion regions, streaming pins, routing to a second object — each have a stated scope, dependency order and acceptance gate as [Track E of the roadmap](roadmap.md#track-e--behavior-execution); none is near-term.
**Actions (Advanced):**
- Interruptible regions
@@ -2392,7 +2392,6 @@ The behavior-execution entries below — interruptible regions, expansion region
**Object Model:**
- Dynamic object creation/destruction (an object is materialized once, and nothing destroys it)
-- Operation invocation with positional arguments (an operation with named arguments runs, whether it is an action, a `calc` or a `constraint` — see the Classifier Behaviors map)
- Routing to a second object of one usage (a `via` send follows the connections and an addressed send resolves an object, but the object reached is the one this context holds as its target's occurrence — see Known Limitations)
**Type System:**
diff --git a/internal/exec/runtime/classifier_behavior_test.go b/internal/exec/runtime/classifier_behavior_test.go
index a4c9cae54..58c73beed 100644
--- a/internal/exec/runtime/classifier_behavior_test.go
+++ b/internal/exec/runtime/classifier_behavior_test.go
@@ -516,6 +516,108 @@ func TestInvokeOperationPerformedByTheObject(t *testing.T) {
}
}
+// positionalInvokeFixture owns operations a positional argument list exercises:
+// a defaulted trailing parameter, an `out` and an `inout` parameter, and two
+// calcs of one name told apart by arity.
+const positionalInvokeFixture = `
+ package test {
+ private import ScalarValues::*;
+ part def Tank {
+ attribute level : Integer = 2;
+ action fillBy { in n : Integer; in times : Integer = 1; out filled : Integer;
+ first apply; action apply { assign level := level + n * times; assign filled := level; } }
+ action drainInto { in n : Integer; inout sink : Integer; out drained : Integer;
+ first apply; action apply { assign level := level - n; assign sink := sink + n; assign drained := n; } }
+ calc scaled { in factor : Integer; return : Integer = level * factor; }
+ calc scaled { in factor : Integer; in offset : Integer; return : Integer = level * factor + offset; }
+ }
+ }
+`
+
+// A positional argument list binds the operation's `in` and `inout` parameters in
+// declaration order, leaves a trailing defaulted parameter to its default, skips an
+// `out` parameter, and selects among same-named operations by arity.
+func TestInvokeOperationWithPositionalArguments(t *testing.T) {
+ idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, positionalInvokeFixture))
+ tank := findSymbolByName(idx.DocumentRoot(""), "Tank", ast.DefPart)
+ if tank == nil {
+ t.Fatal("Tank not found")
+ }
+ inst, err := ctx.Instantiate(tank)
+ if err != nil {
+ t.Fatalf("Instantiate: %v", err)
+ }
+ positional := func(values ...Value) OperationArguments {
+ return OperationArguments{Positional: values}
+ }
+
+ results, err := ctx.InvokeOperationWith(inst, "fillBy", positional(intArgument(3)))
+ if err != nil {
+ t.Fatalf("fillBy(3): %v", err)
+ }
+ wantResult(t, results, "filled", 5)
+ results, err = ctx.InvokeOperationWith(inst, "fillBy", positional(intArgument(3), intArgument(2)))
+ if err != nil {
+ t.Fatalf("fillBy(3, 2): %v", err)
+ }
+ wantResult(t, results, "filled", 11)
+
+ results, err = ctx.InvokeOperationWith(inst, "drainInto", positional(intArgument(4), intArgument(10)))
+ if err != nil {
+ t.Fatalf("drainInto(4, 10): %v", err)
+ }
+ wantResult(t, results, "sink", 14)
+ wantResult(t, results, "drained", 4)
+ if fv, err := inst.GetFeatureValue(ctx, "level"); err != nil || fv.HeldValue().Const.Int != 7 {
+ t.Errorf("level = %v, %v, want 7", fv, err)
+ }
+
+ results, err = ctx.InvokeOperationWith(inst, "scaled", positional(intArgument(2)))
+ if err != nil {
+ t.Fatalf("scaled(2): %v", err)
+ }
+ wantResult(t, results, "result", 14)
+ results, err = ctx.InvokeOperationWith(inst, "scaled", positional(intArgument(2), intArgument(1)))
+ if err != nil {
+ t.Fatalf("scaled(2, 1): %v", err)
+ }
+ wantResult(t, results, "result", 15)
+ results, err = ctx.InvokeOperationWith(inst, "scaled", OperationArguments{Named: map[string]Value{"factor": intArgument(3), "offset": intArgument(1)}})
+ if err != nil {
+ t.Fatalf("scaled(factor=3, offset=1): %v", err)
+ }
+ wantResult(t, results, "result", 22)
+
+ for _, tc := range []struct {
+ name string
+ op string
+ args OperationArguments
+ want error
+ }{
+ {"fewer than required", "fillBy", positional(), ErrUnboundParameter},
+ {"more than declared", "fillBy", positional(intArgument(1), intArgument(2), intArgument(3)), ErrOperationArity},
+ {"out parameter counted", "drainInto", positional(intArgument(1), intArgument(2), intArgument(3)), ErrOperationArity},
+ {"positional and named mixed", "fillBy", OperationArguments{Positional: []Value{intArgument(1)}, Named: map[string]Value{"n": intArgument(1)}}, ErrMixedArguments},
+ {"out parameter named", "drainInto", OperationArguments{Named: map[string]Value{"n": intArgument(1), "sink": intArgument(1), "drained": intArgument(1)}}, ErrUnboundParameter},
+ {"no arity fits", "scaled", positional(intArgument(1), intArgument(2), intArgument(3)), ErrOperationArity},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ if _, err := ctx.InvokeOperationWith(inst, tc.op, tc.args); !errors.Is(err, tc.want) {
+ t.Fatalf("error = %v, want %v", err, tc.want)
+ }
+ })
+ }
+}
+
+// wantResult checks that an operation answered name with the integer want.
+func wantResult(t *testing.T, results map[string]Value, name string, want int64) {
+ t.Helper()
+ got, ok := results[name]
+ if !ok || got.Kind != ValConst || got.Const.Int != want {
+ t.Errorf("%s = %v, want %d", name, results, want)
+ }
+}
+
// An operation invocation counts its own calc or constraint cost against the
// step budget, while separate invocations receive separate budgets.
func TestInvokeOperationCountsItsOwnCostAgainstBudget(t *testing.T) {
diff --git a/internal/exec/runtime/errors.go b/internal/exec/runtime/errors.go
index 546691fc3..290f5d12e 100644
--- a/internal/exec/runtime/errors.go
+++ b/internal/exec/runtime/errors.go
@@ -273,6 +273,14 @@ var (
// parameter twice: by two named arguments, or by a positional and a named one.
ErrDuplicateArgument = errors.New("argument bound more than once")
+ // ErrOperationArity is returned when an operation invocation passes more
+ // positional arguments than the operation declares input parameters.
+ ErrOperationArity = errors.New("operation argument count mismatch")
+
+ // ErrMixedArguments is returned when an operation invocation gives positional and
+ // named arguments together: an argument list is one form or the other (KerML 8.2.5.8.3).
+ ErrMixedArguments = errors.New("positional and named arguments mixed")
+
// ErrNodeNotPerformed is returned when a pin of an action node is read before
// any performance of the node has started.
ErrNodeNotPerformed = errors.New("action node read before it is performed")
diff --git a/internal/exec/runtime/invoke_operation.go b/internal/exec/runtime/invoke_operation.go
index 79616a4d6..4c220c234 100644
--- a/internal/exec/runtime/invoke_operation.go
+++ b/internal/exec/runtime/invoke_operation.go
@@ -3,16 +3,32 @@ package runtime
import (
"errors"
"fmt"
+ "maps"
+ "slices"
+ "github.com/Open-MBEE/OpenSysML/internal/semantic/semantics"
"github.com/Open-MBEE/OpenSysML/internal/semantic/symbols"
"github.com/Open-MBEE/OpenSysML/internal/syntax/ast"
)
+// OperationArguments is an invocation's argument list (KerML 8.2.5.8.3): either
+// positional, bound to the operation's `in` and `inout` parameters in declaration
+// order, or named. A list giving both is refused.
+type OperationArguments struct {
+ Positional []Value
+ Named map[string]Value
+}
+
// InvokeOperation runs a behavior the object's type owns with the object as the
// performer: what the body reads and writes is that object's feature values, and
// what it sends and accepts carries that object's identity. Arguments bind to the
// operation's `in` and `inout` parameters by name.
func (ctx *Context) InvokeOperation(inst *Instance, name string, args map[string]Value) (map[string]Value, error) {
+ return ctx.InvokeOperationWith(inst, name, OperationArguments{Named: args})
+}
+
+// InvokeOperationWith is InvokeOperation taking either argument list form.
+func (ctx *Context) InvokeOperationWith(inst *Instance, name string, args OperationArguments) (map[string]Value, error) {
defer ctx.beginRun()()
if inst == nil {
@@ -21,11 +37,15 @@ func (ctx *Context) InvokeOperation(inst *Instance, name string, args map[string
if err := ctx.checkPerformer(inst); err != nil {
return nil, fmt.Errorf("invoke %s on object #%d: %w", name, inst.ID, err)
}
- sym, err := ctx.operationOf(inst, name)
+ if len(args.Positional) > 0 && len(args.Named) > 0 {
+ return nil, fmt.Errorf("%w: operation %s is invoked with %d positional and %d named argument(s)",
+ ErrMixedArguments, name, len(args.Positional), len(args.Named))
+ }
+ sym, err := ctx.operationOf(inst, name, args)
if err != nil {
return nil, err
}
- inputs, err := operationInputs(ctx.actionParametersOf(sym), name, args)
+ inputs, err := operationInputs(ctx.model.semantics.SignatureParametersOf(sym), name, args)
if err != nil {
return nil, err
}
@@ -71,20 +91,37 @@ func (ctx *Context) InvokeOperation(inst *Instance, name string, args map[string
return nil, fmt.Errorf("%w: %s of %s", ErrNotABehavior, name, symbolText(inst.Type))
}
-// operationOf resolves the member of the object's type that name invokes, and
-// reports a member that states no executable behavior.
-func (ctx *Context) operationOf(inst *Instance, name string) (*symbols.Symbol, error) {
- var member *symbols.Symbol
+// operationOf resolves the member of the object's type that name invokes — among
+// several so named, the one the arguments' values select as a call in the model
+// would — and reports a member that states no executable behavior.
+func (ctx *Context) operationOf(inst *Instance, name string, args OperationArguments) (*symbols.Symbol, error) {
+ var candidates []*symbols.Symbol
for _, candidate := range ctx.model.semantics.MembersOf(inst.Type) {
if candidate.Name == name {
- member = candidate
- break
+ candidates = append(candidates, candidate)
}
}
- if member == nil {
+ if len(candidates) == 0 {
return nil, fmt.Errorf("%w: %s of object #%d (type %s)",
ErrNoSuchBehavior, name, inst.ID, symbolText(inst.Type))
}
+ member := candidates[0]
+ if len(candidates) > 1 {
+ scope := DeclScope(inst.Type)
+ ec := NewEvalContextIn(ctx, scope, inst)
+ typed := make([]semantics.Argument, 0, len(args.Positional)+len(args.Named))
+ for _, value := range args.Positional {
+ typed = append(typed, ec.valueArgument(value, nil))
+ }
+ for _, param := range slices.Sorted(maps.Keys(args.Named)) {
+ typed = append(typed, ec.valueArgument(args.Named[param], ast.QualifiedNameOf(param)))
+ }
+ sel := ctx.model.semantics.SelectAmongArguments(scope, candidates, typed, semantics.PerformsBehavior)
+ if sel.Ambiguous || sel.Called() == nil {
+ return nil, ambiguousInvocationError(name, sel.Tied)
+ }
+ member = sel.Called()
+ }
switch member.Kind {
case symbols.SymbolActionDef, symbols.SymbolActionUsage:
return member, nil
@@ -138,16 +175,25 @@ func (ctx *Context) evaluateConstraintInvocation(sym *symbols.Symbol, scope *sym
return holds, err
}
-// operationInputs binds arguments to the operation's input parameters, reporting
-// an argument naming no parameter and a parameter left with no value: either
-// would otherwise run the body against values the invocation never stated.
-func operationInputs(params []actionParameter, name string, args map[string]Value) (map[string]Value, error) {
- inputs := make(map[string]Value, len(args))
- for _, param := range params {
- if param.Direction == ast.DirOut {
- continue
+// operationInputs binds arguments to the operation's input parameters — a positional
+// list in signature order, a named one by name — reporting a surplus positional, an
+// argument naming no parameter and a parameter left with no value: any would
+// otherwise run the body against values the invocation never stated.
+func operationInputs(params []semantics.SignatureParameter, name string, args OperationArguments) (map[string]Value, error) {
+ named := args.Named
+ if len(args.Positional) > 0 {
+ if len(args.Positional) > len(params) {
+ return nil, fmt.Errorf("%w: operation %s takes %d input parameter(s), got %d argument(s)",
+ ErrOperationArity, name, len(params), len(args.Positional))
}
- value, bound := args[param.Name]
+ named = make(map[string]Value, len(args.Positional))
+ for i, value := range args.Positional {
+ named[params[i].Name] = value
+ }
+ }
+ inputs := make(map[string]Value, len(named))
+ for _, param := range params {
+ value, bound := named[param.Name]
switch {
case bound:
inputs[param.Name] = value
@@ -157,7 +203,7 @@ func operationInputs(params []actionParameter, name string, args map[string]Valu
ErrUnboundParameter, param.Name, name)
}
}
- for arg := range args {
+ for arg := range named {
if !bindsParameter(params, arg) {
return nil, fmt.Errorf("%w: %s is no input parameter of operation %s",
ErrUnboundParameter, arg, name)
@@ -167,9 +213,9 @@ func operationInputs(params []actionParameter, name string, args map[string]Valu
}
// bindsParameter reports whether name is an input parameter an invocation binds.
-func bindsParameter(params []actionParameter, name string) bool {
+func bindsParameter(params []semantics.SignatureParameter, name string) bool {
for _, param := range params {
- if param.Name == name && param.Direction != ast.DirOut {
+ if param.Name == name {
return true
}
}
diff --git a/internal/exec/runtime/robustness_positional_invoke_test.go b/internal/exec/runtime/robustness_positional_invoke_test.go
new file mode 100644
index 000000000..4c5038eaa
--- /dev/null
+++ b/internal/exec/runtime/robustness_positional_invoke_test.go
@@ -0,0 +1,106 @@
+package runtime
+
+import (
+ "errors"
+ "testing"
+)
+
+// TestRuntimeRobustnessPositionalInvoke exercises the failure modes of an operation
+// invoked with a positional argument list: a surplus argument, a list mixing the
+// positional and the named form, and a list no same-named operation takes.
+func TestRuntimeRobustnessPositionalInvoke(t *testing.T) {
+ t.Run("surplus_positional_argument", testPositionalInvokeSurplus)
+ t.Run("surplus_over_an_out_parameter", testPositionalInvokeSurplusOverOut)
+ t.Run("positional_and_named_mixed", testPositionalInvokeMixed)
+ t.Run("required_parameter_left_unbound", testPositionalInvokeTooFew)
+ t.Run("no_overload_of_that_arity", testPositionalInvokeNoOverload)
+}
+
+// positionalInvokeSource declares operations with a defaulted, an `out` and an
+// `inout` parameter, and two calcs of one name that differ in arity.
+const positionalInvokeSource = `
+ package test {
+ private import ScalarValues::*;
+ part def Adder {
+ attribute total : Integer = 0;
+ action add { in addend : Integer; in times : Integer = 1; out sum : Integer;
+ first apply; action apply { assign total := total + addend * times; assign sum := total; } }
+ action moveTo { in amount : Integer; inout sink : Integer;
+ first apply; action apply { assign sink := sink + amount; } }
+ calc scaled { in factor : Integer; return : Integer = total * factor; }
+ calc scaled { in factor : Integer; in offset : Integer; return : Integer = total * factor + offset; }
+ }
+ }`
+
+func positionalInvokeObject(t *testing.T) (*Context, *Instance) {
+ t.Helper()
+ ctx, inst, err := instantiateWithLibraries(t, positionalInvokeSource, "test::Adder")
+ if err != nil {
+ t.Fatalf("Instantiate: %v", err)
+ }
+ return ctx, inst
+}
+
+// testPositionalInvokeSurplus: a third argument to an operation of two input
+// parameters is an arity error, not a value bound nowhere.
+func testPositionalInvokeSurplus(t *testing.T) {
+ ctx, inst := positionalInvokeObject(t)
+ _, err := ctx.InvokeOperationWith(inst, "add", OperationArguments{
+ Positional: []Value{intArgument(1), intArgument(2), intArgument(3)},
+ })
+ if !errors.Is(err, ErrOperationArity) {
+ t.Fatalf("add(1, 2, 3): %v, want ErrOperationArity", err)
+ }
+}
+
+// testPositionalInvokeSurplusOverOut: an `out` parameter takes no position, so an
+// argument aimed at it is a surplus.
+func testPositionalInvokeSurplusOverOut(t *testing.T) {
+ ctx, inst := positionalInvokeObject(t)
+ _, err := ctx.InvokeOperationWith(inst, "moveTo", OperationArguments{
+ Positional: []Value{intArgument(1), intArgument(2), intArgument(3)},
+ })
+ if !errors.Is(err, ErrOperationArity) {
+ t.Fatalf("moveTo(1, 2, 3): %v, want ErrOperationArity", err)
+ }
+}
+
+// testPositionalInvokeMixed: an argument list is positional or named, never both,
+// even when the two would bind different parameters.
+func testPositionalInvokeMixed(t *testing.T) {
+ ctx, inst := positionalInvokeObject(t)
+ _, err := ctx.InvokeOperationWith(inst, "add", OperationArguments{
+ Positional: []Value{intArgument(1)},
+ Named: map[string]Value{"times": intArgument(2)},
+ })
+ if !errors.Is(err, ErrMixedArguments) {
+ t.Fatalf("add(1, times=2): %v, want ErrMixedArguments", err)
+ }
+ if fv, err := inst.GetFeatureValue(ctx, "total"); err != nil || fv.HeldValue().Const.Int != 0 {
+ t.Fatalf("total after a refused invocation = %v, %v, want 0", fv, err)
+ }
+}
+
+// testPositionalInvokeTooFew: an empty positional list leaves the default-less
+// first parameter unbound; only the trailing defaulted one may be omitted.
+func testPositionalInvokeTooFew(t *testing.T) {
+ ctx, inst := positionalInvokeObject(t)
+ _, err := ctx.InvokeOperationWith(inst, "moveTo", OperationArguments{
+ Positional: []Value{intArgument(1)},
+ })
+ if !errors.Is(err, ErrUnboundParameter) {
+ t.Fatalf("moveTo(1): %v, want ErrUnboundParameter", err)
+ }
+}
+
+// testPositionalInvokeNoOverload: with two same-named calcs taking one and two
+// arguments, three arguments select neither and report the arity.
+func testPositionalInvokeNoOverload(t *testing.T) {
+ ctx, inst := positionalInvokeObject(t)
+ _, err := ctx.InvokeOperationWith(inst, "scaled", OperationArguments{
+ Positional: []Value{intArgument(1), intArgument(2), intArgument(3)},
+ })
+ if !errors.Is(err, ErrOperationArity) {
+ t.Fatalf("scaled(1, 2, 3): %v, want ErrOperationArity", err)
+ }
+}
diff --git a/internal/frontend/repl/classifier_behavior_test.go b/internal/frontend/repl/classifier_behavior_test.go
index f7032fdd2..5b7c3c02a 100644
--- a/internal/frontend/repl/classifier_behavior_test.go
+++ b/internal/frontend/repl/classifier_behavior_test.go
@@ -72,8 +72,21 @@ func TestInvokeRunsAnOperationOnTheObject(t *testing.T) {
wants(t, run(t, s, "%features Obj::Monitor"), "count = 5")
}
+// A positional argument list binds the operation's input parameters in declaration
+// order, an expression with spaces included.
+func TestInvokeBindsPositionalArguments(t *testing.T) {
+ s := loadFixture(t, "testdata/exhibited_machine.sysml")
+ run(t, s, "%instantiate Obj::Monitor")
+
+ wants(t, run(t, s, "%invoke Obj::Monitor bumpBy 4"), "Invoked bumpBy on object #")
+ wants(t, run(t, s, "%features Obj::Monitor"), "count = 5")
+ wants(t, run(t, s, "%invoke Obj::Monitor bumpBy 2 + 3"), "Invoked bumpBy on object #")
+ wants(t, run(t, s, "%features Obj::Monitor"), "count = 10")
+}
+
// %invoke reports its usage, an operation the type does not own, an argument
-// naming no parameter and a parameter left unbound.
+// naming no parameter, a parameter left unbound, a list mixing the positional and
+// the named form, a surplus positional argument and one bound twice.
func TestInvokeReportsItsFailureModes(t *testing.T) {
s := loadFixture(t, "testdata/exhibited_machine.sysml")
run(t, s, "%instantiate Obj::Monitor")
@@ -82,7 +95,11 @@ func TestInvokeReportsItsFailureModes(t *testing.T) {
wants(t, run(t, s, "%invoke Obj::Monitor missing"), "error:", "missing")
wants(t, run(t, s, "%invoke Obj::Monitor bumpBy"), "error:", "unbound parameter")
wants(t, run(t, s, "%invoke Obj::Monitor bumpBy other=1"), "error:", "unbound parameter")
- wants(t, run(t, s, "%invoke Obj::Monitor bumpBy n"), "error:", "=")
+ wants(t, run(t, s, "%invoke Obj::Monitor bumpBy n"), "error:", "unresolved reference")
+ wants(t, run(t, s, "%invoke Obj::Monitor bumpBy 1 n=2"), "error:", "positional and named arguments mixed")
+ wants(t, run(t, s, "%invoke Obj::Monitor bumpBy 1 2"), "error:", "takes 1 input parameter(s), got 2 argument(s)")
+ wants(t, run(t, s, "%invoke Obj::Monitor bumpBy n=1 n=2"), "error:", "parameter n is given more than one argument")
+ wants(t, run(t, s, "%features Obj::Monitor"), "count = 1")
}
// An unrelated declaration submitted while an object's machine is being debugged
diff --git a/internal/frontend/repl/meta.go b/internal/frontend/repl/meta.go
index 1385aabe5..589c12a5b 100644
--- a/internal/frontend/repl/meta.go
+++ b/internal/frontend/repl/meta.go
@@ -192,7 +192,7 @@ var metaCommandTable = []metaCommand{
{name: "%eval", group: groupRuntime, args: "[in ||# :] ", desc: "evaluate an expression, in the named element or object when one is named"},
{name: "%features", group: groupRuntime, args: " [all|depth ] [json]", desc: "show an object's feature values and what its behaviors are doing, bounded unless all or a depth is asked for; json writes the object graph as the API does; an object is named, #, or a path such as car.fl or #1.wheels[2]"},
{name: "%instances", group: groupRuntime, desc: "list all instantiated objects"},
- {name: "%invoke", group: groupRuntime, args: " [=]", desc: "invoke an operation of an object's type, performed by that object; an object is named, #, or a path such as car.fl"},
+ {name: "%invoke", group: groupRuntime, args: " [... | =...]", desc: "invoke an operation of an object's type, performed by that object, with arguments by position or by name; an object is named, #, or a path such as car.fl"},
{name: "%calc", group: groupBehavioral, args: " ", desc: "invoke a calculation with arguments"},
{name: cmdAnalysis, group: groupBehavioral, args: "[()] []", desc: "run an analysis case and report its outputs and the verdict of its objective; arguments bind its inputs and an object is its subject"},
@@ -595,9 +595,9 @@ func (s *Session) metaDebugCommand(fields []string, line string) (metaResult, bo
return metaOut(s.doStateMachine(fields[1], fields[2:])), true
case "%invoke":
if len(fields) < 3 {
- return metaOut([]string{"usage: %invoke [= ...]"}, false, nil), true
+ return metaOut([]string{"usage: %invoke [ ... | = ...]"}, false, nil), true
}
- return metaOut(s.doInvoke(fields[1], fields[2], fields[3:])), true
+ return metaOut(s.doInvoke(fields[1], fields[2], strings.Join(fields[3:], " "))), true
case cmdQuery:
if len(fields) < 2 {
return metaOut([]string{"usage: %query "}, false, nil), true
@@ -2024,11 +2024,14 @@ func splitArgs(text string) []string {
// continuesExpr reports whether frag continues the expression buf holds rather
// than starting the next argument: a unit or index bracket does, as does a
// fragment that is no expression on its own or that follows an unfinished one
-// (`5 - 3` is one argument, `5 -3` is two).
+// (`5 - 3` is one argument, `5 -3` is two). A named argument always starts one.
func continuesExpr(buf, frag string) bool {
if strings.HasPrefix(frag, "[") || strings.HasPrefix(frag, "#") {
return true
}
+ if isNamedArgument(frag) {
+ return false
+ }
if _, err := parseWholeExpr(frag); err != nil {
return true
}
@@ -3105,7 +3108,7 @@ func (s *Session) stateStep(exec *runtime.StateExecutor) (string, error) {
}
// doInvoke invokes an operation on an object, with the object as its performer.
-func (s *Session) doInvoke(name, operation string, args []string) ([]string, bool, error) {
+func (s *Session) doInvoke(name, operation, args string) ([]string, bool, error) {
lines, err := s.invokeOperation(name, operation, args)
if err != nil {
if errors.Is(err, errRuntimeInit) {
@@ -3117,11 +3120,11 @@ func (s *Session) doInvoke(name, operation string, args []string) ([]string, boo
return lines, false, nil
}
-// invokeOperation binds the arguments written as `name=` and runs the
-// operation the object's type owns, performed by that object; the arguments are
-// parsed before the object is reached.
-func (s *Session) invokeOperation(name, operation string, args []string) ([]string, error) {
- parsed, err := parseArguments(args)
+// invokeOperation binds the arguments, written positionally or as `name=`,
+// and runs the operation the object's type owns, performed by that object; the
+// arguments are parsed before the object is reached.
+func (s *Session) invokeOperation(name, operation, args string) ([]string, error) {
+ parsed, err := parseInvokeArguments(args)
if err != nil {
return nil, err
}
@@ -3133,11 +3136,18 @@ func (s *Session) invokeOperation(name, operation string, args []string) ([]stri
if rerr != nil {
return nil, rerr
}
- bound, err := s.evalArguments(ctx, parsed)
- if err != nil {
+ var bound runtime.OperationArguments
+ if bound.Named, err = s.evalArguments(ctx, parsed.named); err != nil {
return nil, err
}
- results, err := ctx.InvokeOperation(inst, operation, bound)
+ for i, arg := range parsed.positional {
+ value, err := ctx.EvalWithScope(arg.expr, s.promptScope())
+ if err != nil {
+ return nil, fmt.Errorf("argument %d (%s): %w", i+1, arg.text, err)
+ }
+ bound.Positional = append(bound.Positional, value)
+ }
+ results, err := ctx.InvokeOperationWith(inst, operation, bound)
if err != nil {
return nil, err
}
@@ -3158,6 +3168,47 @@ type argument struct {
node ast.Node
}
+// invokeArguments are %invoke's arguments parsed and not yet evaluated: the list is
+// positional or named, never both.
+type invokeArguments struct {
+ positional []argExpr
+ named []argument
+}
+
+// parseInvokeArguments parses %invoke's argument list, each argument either a bare
+// expression or `=`. A list mixing the two forms or naming
+// a parameter twice is refused here, before the object is reached.
+func parseInvokeArguments(text string) (invokeArguments, error) {
+ var args invokeArguments
+ for _, arg := range splitArgs(text) {
+ if isNamedArgument(arg) {
+ named, err := parseArguments([]string{arg})
+ if err != nil {
+ return invokeArguments{}, err
+ }
+ args.named = append(args.named, named...)
+ continue
+ }
+ expr, err := parseWholeExpr(arg)
+ if err != nil {
+ return invokeArguments{}, err
+ }
+ args.positional = append(args.positional, argExpr{expr: expr, text: arg})
+ }
+ if len(args.positional) > 0 && len(args.named) > 0 {
+ return invokeArguments{}, fmt.Errorf("%w: %d positional and %d named argument(s)",
+ runtime.ErrMixedArguments, len(args.positional), len(args.named))
+ }
+ for i, a := range args.named {
+ for _, b := range args.named[:i] {
+ if a.param == b.param {
+ return invokeArguments{}, fmt.Errorf("parameter %s is given more than one argument", a.param)
+ }
+ }
+ }
+ return args, nil
+}
+
// parseArguments takes apart and parses `name=` arguments.
func parseArguments(args []string) ([]argument, error) {
parsed := make([]argument, 0, len(args))
diff --git a/internal/frontend/repl/object_args_test.go b/internal/frontend/repl/object_args_test.go
index 67ffe931b..8cad29149 100644
--- a/internal/frontend/repl/object_args_test.go
+++ b/internal/frontend/repl/object_args_test.go
@@ -361,7 +361,8 @@ func TestMalformedExpressionsMaterializeNothing(t *testing.T) {
for _, tc := range []struct{ line, reject string }{
{"%eval in car.fl : radius *", "car.fl"},
{"%invoke car.fl spin turns=1 +", "car.fl"},
- {"%invoke car.fl spin 1", "car.fl"},
+ {"%invoke car.fl spin 1 +", "car.fl"},
+ {"%invoke car.fl spin 1 turns=2", "car.fl"},
{"%send Spin(turns=1 +) to car.fl", "car.fl"},
{"%send Spin(1) to car.fl", "car.fl"},
{"%send Spin(turns=1, turns=2) to car.fl", "car.fl"},
diff --git a/internal/semantic/semantics/binding.go b/internal/semantic/semantics/binding.go
index 633fe345c..cbc413396 100644
--- a/internal/semantic/semantics/binding.go
+++ b/internal/semantic/semantics/binding.go
@@ -30,6 +30,26 @@ func (m *Model) LookupBinding(scope *symbols.Scope, typ *symbols.Symbol, name *a
return found, true
}
+// SignatureParameter is one input parameter of a callee as a call binds it.
+type SignatureParameter struct {
+ Name string // the name a runtime keys the binding by
+ Optional bool // may go without an argument: a default, or a multiplicity admitting none
+}
+
+// SignatureParametersOf lists callee's effective input parameters in signature order —
+// what a positional argument list binds to, one parameter per position.
+func (m *Model) SignatureParametersOf(callee *symbols.Symbol) []SignatureParameter {
+ if m == nil || callee == nil {
+ return nil
+ }
+ sig := m.signatureOf(callee)
+ params := make([]SignatureParameter, len(sig.params))
+ for i, p := range sig.params {
+ params[i] = SignatureParameter{Name: p.name, Optional: p.optional}
+ }
+ return params
+}
+
// BoundParameter is the input parameter of callee a named argument binds, by the name
// callee's signature gives it — what a runtime keys its bindings by; false when none.
func (m *Model) BoundParameter(scope *symbols.Scope, callee *symbols.Symbol, name *ast.QualifiedName) (string, bool) {
From b838de2ef9c97ce0698b9de8bc49054ebbeca2cb Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 20 Sep 2026 17:37:21 +0000
Subject: [PATCH 2/5] fix(repl): pass %invoke's argument text to the parser as
written
The field splitter drops string delimiters, so a positional or named
string literal reached the argument parser unquoted. %invoke now cuts the
object and operation off the raw line and parses the rest verbatim.
Co-Authored-By: jason.han
---
.../frontend/repl/classifier_behavior_test.go | 15 +++++++++++++++
internal/frontend/repl/meta.go | 18 +++++++++++++++++-
.../repl/testdata/exhibited_machine.sysml | 9 +++++++++
3 files changed, 41 insertions(+), 1 deletion(-)
diff --git a/internal/frontend/repl/classifier_behavior_test.go b/internal/frontend/repl/classifier_behavior_test.go
index 5b7c3c02a..f77557fe2 100644
--- a/internal/frontend/repl/classifier_behavior_test.go
+++ b/internal/frontend/repl/classifier_behavior_test.go
@@ -84,6 +84,21 @@ func TestInvokeBindsPositionalArguments(t *testing.T) {
wants(t, run(t, s, "%features Obj::Monitor"), "count = 10")
}
+// A string literal argument reaches the operation as written, its quotes and the
+// spaces inside it included, positionally and by name.
+func TestInvokeKeepsStringLiteralArguments(t *testing.T) {
+ s := loadFixture(t, "testdata/exhibited_machine.sysml")
+ run(t, s, "%instantiate Obj::Monitor")
+
+ wants(t, run(t, s, `%invoke Obj::Monitor setLabel "ready now"`), "Invoked setLabel on object #")
+ wants(t, run(t, s, "%features Obj::Monitor"), `label = "ready now"`)
+ wants(t, run(t, s, `%invoke Obj::Monitor setLabel text="by name"`), "Invoked setLabel on object #")
+ wants(t, run(t, s, "%features Obj::Monitor"), `label = "by name"`)
+ wants(t, run(t, s, `%invoke Obj::Monitor setLabel ""`), "Invoked setLabel on object #")
+ wants(t, run(t, s, "%features Obj::Monitor"), `label = ""`)
+ wants(t, run(t, s, `%invoke Obj::Monitor setLabel "a" "b"`), "error:", "takes 1 input parameter(s), got 2 argument(s)")
+}
+
// %invoke reports its usage, an operation the type does not own, an argument
// naming no parameter, a parameter left unbound, a list mixing the positional and
// the named form, a surplus positional argument and one bound twice.
diff --git a/internal/frontend/repl/meta.go b/internal/frontend/repl/meta.go
index 589c12a5b..8c0decb53 100644
--- a/internal/frontend/repl/meta.go
+++ b/internal/frontend/repl/meta.go
@@ -597,7 +597,8 @@ func (s *Session) metaDebugCommand(fields []string, line string) (metaResult, bo
if len(fields) < 3 {
return metaOut([]string{"usage: %invoke [ ... | = ...]"}, false, nil), true
}
- return metaOut(s.doInvoke(fields[1], fields[2], strings.Join(fields[3:], " "))), true
+ object, operation, args := splitInvokeLine(strings.TrimPrefix(strings.TrimSpace(line), "%invoke"))
+ return metaOut(s.doInvoke(object, operation, args)), true
case cmdQuery:
if len(fields) < 2 {
return metaOut([]string{"usage: %query "}, false, nil), true
@@ -3209,6 +3210,21 @@ func parseInvokeArguments(text string) (invokeArguments, error) {
return args, nil
}
+// splitInvokeLine cuts `%invoke`'s tail into the object, the operation and the
+// argument text as written, so a string literal keeps its quotes and spaces.
+func splitInvokeLine(tail string) (object, operation, args string) {
+ tail = strings.TrimSpace(tail)
+ cut := indexOutsideName(tail, " \t")
+ if cut < 0 {
+ return tail, "", ""
+ }
+ object, tail = tail[:cut], strings.TrimSpace(tail[cut:])
+ if cut = indexOutsideName(tail, " \t"); cut < 0 {
+ return object, tail, ""
+ }
+ return object, tail[:cut], strings.TrimSpace(tail[cut:])
+}
+
// parseArguments takes apart and parses `name=` arguments.
func parseArguments(args []string) ([]argument, error) {
parsed := make([]argument, 0, len(args))
diff --git a/internal/frontend/repl/testdata/exhibited_machine.sysml b/internal/frontend/repl/testdata/exhibited_machine.sysml
index 97775cd4f..1870afd73 100644
--- a/internal/frontend/repl/testdata/exhibited_machine.sysml
+++ b/internal/frontend/repl/testdata/exhibited_machine.sysml
@@ -26,5 +26,14 @@ package Obj {
assign count := count + n;
}
}
+
+ attribute label = "";
+ action setLabel {
+ in text;
+ first apply;
+ action apply {
+ assign label := text;
+ }
+ }
}
}
From 52b53f551d8933e13cc1500a5670c235a7bb7449 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 20 Sep 2026 19:54:40 +0000
Subject: [PATCH 3/5] docs(project): resolve the roadmap's Track E status after
rebasing on develop
Co-Authored-By: jason.han
---
docs/project/roadmap.md | 14 +++-----------
1 file changed, 3 insertions(+), 11 deletions(-)
diff --git a/docs/project/roadmap.md b/docs/project/roadmap.md
index 60c54cd85..ee15a313d 100644
--- a/docs/project/roadmap.md
+++ b/docs/project/roadmap.md
@@ -3022,17 +3022,9 @@ The open items, by track, with the item that gates each where one does. Everythi
is landed or is a track the previous baseline left as it stands (D, N, M, I, V, B, R2–R5);
Tracks F, S, L and A are closed.
-<<<<<<< HEAD
-- **Track E** — eligible and first: E2, then E4 (E1 landed), then E6 on request, E3 closed by
- its design record, E5 closed by its record (an optional follow-up waits on a model that needs
- it), E7 behind its object-model item, E8 behind a model that needs it. The
-||||||| parent of cab80e653 (feat(runtime): invoke an operation with a positional argument list)
-- **Track E** — eligible and first: E2, then E4 (E1 landed), then E6 on request, E3/E5 behind
- their design records, E7 behind its object-model item, E8 behind a model that needs it. The
-=======
-- **Track E** — eligible and first: E2, then E4 (E1 and E6 landed), E3/E5 behind
- their design records, E7 behind its object-model item, E8 behind a model that needs it. The
->>>>>>> cab80e653 (feat(runtime): invoke an operation with a positional argument list)
+- **Track E** — eligible and first: E2, then E4 (E1 and E6 landed), E3 closed by its design
+ record, E5 closed by its record (an optional follow-up waits on a model that needs it), E7
+ behind its object-model item, E8 behind a model that needs it. The
PSSM referee's 17 `fail` tests are the state side's measurement, every one attributed (#326):
eleven wait on the region-order choice point whose design record #342 wrote and left at two
maintainer decisions — the nine the record names to move `fail` → `pass`, plus *Terminate 001*
From 9c108760dbee506a60d00f8220403d3ff6408cc6 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Sun, 20 Sep 2026 20:03:56 +0000
Subject: [PATCH 4/5] fix(runtime): select an invoked operation among actions
and calcs alike
An expression's overload selection sets actions aside whenever a calc of the
name exists, since only a calc yields the value the expression needs. An
operation invocation runs either, so it selects in a mode of its own,
PerformsOperation, where the arguments alone decide between them.
Co-Authored-By: jason.han
---
docs/project/roadmap.md | 6 +++--
docs/project/spec-compliance.md | 2 +-
.../exec/runtime/classifier_behavior_test.go | 24 ++++++++++++++++++-
internal/exec/runtime/invoke_operation.go | 2 +-
internal/semantic/semantics/invocation.go | 3 +++
5 files changed, 32 insertions(+), 5 deletions(-)
diff --git a/docs/project/roadmap.md b/docs/project/roadmap.md
index ee15a313d..d48f5b4cc 100644
--- a/docs/project/roadmap.md
+++ b/docs/project/roadmap.md
@@ -1405,8 +1405,10 @@ delegates to it. A positional list binds to the operation's effective input para
invocation expression in signature order, `out` and result excluded — so a trailing defaulted
parameter may be omitted, an `inout` parameter takes a position and comes back as a result, and an
`out` parameter takes none. Among same-named members, `operationOf` selects through
-`semantics.Model.SelectAmongArguments`, the overload selection the expression evaluator uses, so
-two calcs of one name are told apart by arity and a list neither takes is refused. A surplus is
+`semantics.Model.SelectAmongArguments`, the overload selection the expression evaluator uses, in
+its `PerformsOperation` mode — every behavior admitted alike, an expression's preference for a
+calc set aside — so two calcs of one name are told apart by arity, an action and a calc of one
+name by the arguments' types, and a list none takes is refused. A surplus is
`ErrOperationArity` (`operation … takes N input parameter(s), got M argument(s)`), a list mixing
the two forms is `ErrMixedArguments`, and a required parameter left unbound is still
`ErrUnboundParameter`. The REPL's `%invoke ` takes bare expressions or `=`
diff --git a/docs/project/spec-compliance.md b/docs/project/spec-compliance.md
index a69dfd185..08292e7fe 100644
--- a/docs/project/spec-compliance.md
+++ b/docs/project/spec-compliance.md
@@ -853,7 +853,7 @@ checked after the result is bound is not a form the runtime offers, and none is
| An object's parameter space is its own: an action's `out` parameter answers the caller even where the performing object declares a feature of that name | `runtime/action_statements.go` `assignOuter`; `runtime/action_executor.go` `declaresParameter` | `runtime/classifier_behavior_test.go:TestOperationOutputNamedLikeAFeatureAnswersTheCaller` | ✅ Faithful |
| A failed materialization leaves no behavior of the object attached or queued, and an edited model drops an object whose behavior body changed rather than resuming it on the values the old body wrote | `runtime/classifier_behavior.go` `startClassifierBehaviors`/`forgetBehaviorsFrom`; `runtime/adopt.go` `writeBoundBehaviors` | `runtime/classifier_behavior_test.go:TestFailedMaterializationLeavesNoBehaviorBehind`, `repl/classifier_behavior_test.go:TestRewritingTheExhibitedMachineDropsTheObject`, `:TestObjectMachineSurvivesAnUnrelatedDeclaration` | ⚠️ Approximate (tool-defined: the spec describes one fixed model, so what a live execution does when the model is edited is a REPL policy) |
| A second materialization of one name is a second object, with its own identity and its own behaviors; `occurrenceOf` remains the reuse path for a named occurrence | `runtime/instance.go` `instantiateOwnedBy`; `repl/query.go` `instantiateNamed` (which object the name now denotes) | `robustness_test.go:second_instantiation_of_one_type`, `repl/classifier_behavior_test.go:TestSecondInstantiateIsAnotherObject` | ⚠️ Approximate (tool-defined; the spec leaves object creation semantics open) |
-| Invoking an operation of an object's type runs it with that object as performer, whichever behavior the member is — an action, a calc or a constraint — binding named arguments as the call machinery binds them, or a positional list to the effective input parameters in signature order (KerML §8.2.5.8.3: an `ArgumentList` is positional or named, never a mix; §8.4.4.9.5: a positional list binds parameters in declaration order) | `runtime/invoke_operation.go` `InvokeOperation` (named) and `InvokeOperationWith` (`OperationArguments`, positional or named; `operationOf` settles which behavior the member is and, among same-named members, selects one through `semantics.Model.SelectAmongArguments` — the overload selection an invocation expression uses; `operationInputs` binds a positional list to `semantics.Model.SignatureParametersOf` — `signatureOf`'s `in`/`inout` parameters in signature order, `out` and result excluded — refusing a surplus with `ErrOperationArity` and a mixed list with `ErrMixedArguments`; an action runs through `ExecuteActionPerformedBy`, a calc through the existing calc invocation with the object as its featuring object, a constraint through the existing condition evaluation against the object); `runtime/eval.go` `evalInvocation` preserves that performer for nested calc invocation expressions; `repl/meta.go` `%invoke` (`parseInvokeArguments`: bare expressions or `=` pairs, a mixed list and a parameter named twice refused before the object is reached) | `runtime/classifier_behavior_test.go:TestInvokeOperationPerformedByTheObject`, `:TestInvokeOperationWithPositionalArguments` (a defaulted trailing parameter omitted, an `inout` read and written, an `out` taking no position, two same-named calcs told apart by arity, fewer, surplus and mixed lists), `robustness_positional_invoke_test.go:TestRuntimeRobustnessPositionalInvoke`, `repl/classifier_behavior_test.go:TestInvokeBindsPositionalArguments`, `:TestCalcInvocationExpressionSeesPerformingObject` (an action, direct and nested calcs reading a mutated feature, an anonymous result, and a constraint), `:TestInvokeOperationFailureModes`, `robustness_test.go:operation_invoked_with_unbound_parameters`, `:operation_constraint_body_cannot_be_evaluated`, `repl/classifier_behavior_test.go:TestInvokeRunsAnOperationOnTheObject`, `:TestInvokeReportsItsFailureModes` | ✅ Faithful, **self-assessed** (the pinned reference cannot invoke an operation on an object, so nothing external adjudicates this). A calc's value comes back under its result parameter's name, or `result` where the result is anonymous, and a constraint's verdict comes back under `result` — a `false` verdict is an answer, not `ErrViolated`, since the invocation asked for the value. A body that cannot be evaluated is still an error. A state member is refused with `ErrUnsupportedClassifierBehavior`; its exhibited state machine runs during materialization instead. A member that is no behavior at all is `ErrNotABehavior`. Arguments bind by name or by position on the Go API and the REPL alike; the gRPC surface exposes no operation invocation. A positional list is bound to the same effective signature an invocation expression is bound to, so the two surfaces cannot drift |
+| Invoking an operation of an object's type runs it with that object as performer, whichever behavior the member is — an action, a calc or a constraint — binding named arguments as the call machinery binds them, or a positional list to the effective input parameters in signature order (KerML §8.2.5.8.3: an `ArgumentList` is positional or named, never a mix; §8.4.4.9.5: a positional list binds parameters in declaration order) | `runtime/invoke_operation.go` `InvokeOperation` (named) and `InvokeOperationWith` (`OperationArguments`, positional or named; `operationOf` settles which behavior the member is and, among same-named members, selects one through `semantics.Model.SelectAmongArguments` in its `PerformsOperation` mode — the overload selection an invocation expression uses, without the expression's preference for a calc, so an action and a calc of one name are told apart by the arguments' types; `operationInputs` binds a positional list to `semantics.Model.SignatureParametersOf` — `signatureOf`'s `in`/`inout` parameters in signature order, `out` and result excluded — refusing a surplus with `ErrOperationArity` and a mixed list with `ErrMixedArguments`; an action runs through `ExecuteActionPerformedBy`, a calc through the existing calc invocation with the object as its featuring object, a constraint through the existing condition evaluation against the object); `runtime/eval.go` `evalInvocation` preserves that performer for nested calc invocation expressions; `repl/meta.go` `%invoke` (`parseInvokeArguments`: bare expressions or `=` pairs, a mixed list and a parameter named twice refused before the object is reached) | `runtime/classifier_behavior_test.go:TestInvokeOperationPerformedByTheObject`, `:TestInvokeOperationWithPositionalArguments` (a defaulted trailing parameter omitted, an `inout` read and written, an `out` taking no position, two same-named calcs told apart by arity, a same-named action and calc by argument type, fewer, surplus and mixed lists), `robustness_positional_invoke_test.go:TestRuntimeRobustnessPositionalInvoke`, `repl/classifier_behavior_test.go:TestInvokeBindsPositionalArguments`, `:TestCalcInvocationExpressionSeesPerformingObject` (an action, direct and nested calcs reading a mutated feature, an anonymous result, and a constraint), `:TestInvokeOperationFailureModes`, `robustness_test.go:operation_invoked_with_unbound_parameters`, `:operation_constraint_body_cannot_be_evaluated`, `repl/classifier_behavior_test.go:TestInvokeRunsAnOperationOnTheObject`, `:TestInvokeReportsItsFailureModes` | ✅ Faithful, **self-assessed** (the pinned reference cannot invoke an operation on an object, so nothing external adjudicates this). A calc's value comes back under its result parameter's name, or `result` where the result is anonymous, and a constraint's verdict comes back under `result` — a `false` verdict is an answer, not `ErrViolated`, since the invocation asked for the value. A body that cannot be evaluated is still an error. A state member is refused with `ErrUnsupportedClassifierBehavior`; its exhibited state machine runs during materialization instead. A member that is no behavior at all is `ErrNotABehavior`. Arguments bind by name or by position on the Go API and the REPL alike; the gRPC surface exposes no operation invocation. A positional list is bound to the same effective signature an invocation expression is bound to, so the two surfaces cannot drift |
| An object typed by a behavior — a performance occurrence, the `action def` a v1 activity becomes when a part performs it — runs no classifier behaviors of its own: its `perform`/`exhibit` members are steps of the performance that runs it, not behaviors bound to the occurrence, so a body's `action call : Sub;` runs once as a step and `this` in it is the performer; a reference that is no action is `ErrNotABehavior` | `runtime/classifier_behavior.go` `classifierBehaviorsOf` (a behavior type binds none); `runtime/signal.go` `isBehaviorType`; `runtime/invoke_action.go` `actionCandidates` (`ErrNotABehavior`) | conformance `performed_action_def_perform_members_are_steps` + `.expected.json`; `migrate/opaque_migration_test.go:TestSwimlaneBodiesAndGuardsRunAgainstTheRepresentedPart` (a migrated workflow whose action body performs a sub-activity through its performer) | ✅ Faithful, **self-assessed** (KerML §8.4.4.3: `performances` are bound to the objects of a `Class` that is not itself a `Behavior`; a `Behavior`'s steps are its own `subperformances`) |
| A `perform action x ::> part.action;` usage (an action usage referencing a feature chain) performs the chain's last action with the object the chain reaches as its performer, so `this` in the performed body is that object — `tel.point` moves the telescope, not the station; an action usage referencing or typed by a qualified name still runs on the caller's own performer. The chain is evaluated in the caller's context when the node fires: a chain holding no object is `ErrPerformerNotObject`, one holding several objects is refused, one whose last segment is not an action is `ErrPerformerNotObject` too, and one reaching an object that was destroyed is `ErrOccurrenceDestroyed` naming when it was destroyed | `parser/defusage.go` (a `::>` reference to a feature chain on an action usage); `lower/action_graph.go` `performsAction` (`RelReferences` to a `FeatureChainExpr`); `runtime/invoke_action.go` `actionInvocation` (target or chain), `performerOf` (the chain evaluated over the caller's `self`, resolved to an instance through `Context.Instance`), `beginCallee`; `runtime/action_executor.go` `stepNestedAction` | `parse/perform_action_on_part.golden`; conformance `perform_action_on_part` + trace golden; `robustness_perform_on_part_test.go:performer_holds_no_object`, `:performer_holds_several_objects`, `:chain_ends_in_no_action` (`ErrPerformerNotObject`), `:performer_was_destroyed` (`ErrOccurrenceDestroyed`); `migrate/behavior_test.go:TestActivityMigratesToAnExecutableActionDef` (a migrated v1 `CallOperationAction` on a target pin runs through this form) | ✅ Faithful, **self-assessed** (SysML v2 §7.16: an action usage that references another performs it as a `subperformance` of the referenced feature's featuring occurrence; the pinned reference cannot execute one) |
| A typed usage that also references a feature chain, `perform action spin : Motor::Spin ::> drive.motor.spin { in rpm = 30.0; }`, performs the chain's last action on the object the chain reaches, as the untyped form does, and its body binds the callee's `in` parameters by name: the reference names both the action and its performer, so it settles what is performed where the typing only restates the action's definition; an unqualified name the body reads that no argument binds is read from the caller's scope when the callee is performed. A chain holding no object or ending in no action is refused with `ErrPerformerNotObject` | `parser/defusage.go` (a typing followed by a `::>` reference to a feature chain); `runtime/invoke_action.go` `nestedInvocation` (the reference subsetting wins over the typing), `referencedInvocation`; `runtime/action_frame.go` (invocation inputs bound from the body's `in p = …` arguments, then from the enclosing scope for an unqualified callee) | `parse/perform_typed_subsetting.golden`; conformance `perform_typed_action_on_nested_part`, `accept_payload_bound_into_typed_action` + trace goldens; `robustness_perform_typed_on_part_test.go:reference_names_the_performer`, `:performer_holds_no_object`, `:chain_ends_in_no_action`; `migrate/interaction_test.go`, `migrate/call_port_test.go`, `migrate/reception_test.go` (a migrated v1 call message, a `CallOperationAction` routed over a port and a reception's method run through this form) | ✅ Faithful, **self-assessed** (SysML v2 §7.16: an action usage typed by a definition and referencing a feature performs the referenced feature; the pinned reference cannot execute one) |
diff --git a/internal/exec/runtime/classifier_behavior_test.go b/internal/exec/runtime/classifier_behavior_test.go
index 58c73beed..c366692f7 100644
--- a/internal/exec/runtime/classifier_behavior_test.go
+++ b/internal/exec/runtime/classifier_behavior_test.go
@@ -530,13 +530,17 @@ const positionalInvokeFixture = `
first apply; action apply { assign level := level - n; assign sink := sink + n; assign drained := n; } }
calc scaled { in factor : Integer; return : Integer = level * factor; }
calc scaled { in factor : Integer; in offset : Integer; return : Integer = level * factor + offset; }
+ attribute label : String = "";
+ action run { in x : String; first apply; action apply { assign label := x; } }
+ calc run { in x : Integer; return : Integer = level + x; }
}
}
`
// A positional argument list binds the operation's `in` and `inout` parameters in
// declaration order, leaves a trailing defaulted parameter to its default, skips an
-// `out` parameter, and selects among same-named operations by arity.
+// `out` parameter, and selects among same-named operations by arity and by type —
+// an action and a calc of one name each answering the arguments that fit it.
func TestInvokeOperationWithPositionalArguments(t *testing.T) {
idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, positionalInvokeFixture))
tank := findSymbolByName(idx.DocumentRoot(""), "Tank", ast.DefPart)
@@ -588,6 +592,24 @@ func TestInvokeOperationWithPositionalArguments(t *testing.T) {
}
wantResult(t, results, "result", 22)
+ results, err = ctx.InvokeOperationWith(inst, "run", positional(intArgument(3)))
+ if err != nil {
+ t.Fatalf("run(3): %v", err)
+ }
+ wantResult(t, results, "result", 10)
+ if _, err = ctx.InvokeOperationWith(inst, "run", positional(NewStringValue("go"))); err != nil {
+ t.Fatalf(`run("go"): %v`, err)
+ }
+ if fv, err := inst.GetFeatureValue(ctx, "label"); err != nil || fv.HeldValue().Str() != "go" {
+ t.Errorf("label = %v, %v, want go", fv, err)
+ }
+ if _, err = ctx.InvokeOperationWith(inst, "run", OperationArguments{Named: map[string]Value{"x": NewStringValue("named")}}); err != nil {
+ t.Fatalf(`run(x="named"): %v`, err)
+ }
+ if fv, err := inst.GetFeatureValue(ctx, "label"); err != nil || fv.HeldValue().Str() != "named" {
+ t.Errorf("label = %v, %v, want named", fv, err)
+ }
+
for _, tc := range []struct {
name string
op string
diff --git a/internal/exec/runtime/invoke_operation.go b/internal/exec/runtime/invoke_operation.go
index 4c220c234..4ed601c2d 100644
--- a/internal/exec/runtime/invoke_operation.go
+++ b/internal/exec/runtime/invoke_operation.go
@@ -116,7 +116,7 @@ func (ctx *Context) operationOf(inst *Instance, name string, args OperationArgum
for _, param := range slices.Sorted(maps.Keys(args.Named)) {
typed = append(typed, ec.valueArgument(args.Named[param], ast.QualifiedNameOf(param)))
}
- sel := ctx.model.semantics.SelectAmongArguments(scope, candidates, typed, semantics.PerformsBehavior)
+ sel := ctx.model.semantics.SelectAmongArguments(scope, candidates, typed, semantics.PerformsOperation)
if sel.Ambiguous || sel.Called() == nil {
return nil, ambiguousInvocationError(name, sel.Tied)
}
diff --git a/internal/semantic/semantics/invocation.go b/internal/semantic/semantics/invocation.go
index 3339250db..aa6feabee 100644
--- a/internal/semantic/semantics/invocation.go
+++ b/internal/semantic/semantics/invocation.go
@@ -157,6 +157,9 @@ const (
PerformsBehavior Performs = iota
// PerformsAction runs an action, as `action a = tag(x);` does: only actions answer.
PerformsAction
+ // PerformsOperation invokes a behavior by name on an object: every behavior
+ // answers alike, the arguments alone selecting among calcs and actions.
+ PerformsOperation
)
// CallSite is the kind of call site a reference is: an action performance when
From c1468600107e858a894a72e0d861ce6baf299dc2 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Mon, 21 Sep 2026 01:42:55 +0000
Subject: [PATCH 5/5] test(runtime): cover an unnamed inout redefinition
invoked as an operation
Co-Authored-By: jason.han
---
.../exec/runtime/classifier_behavior_test.go | 33 +++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/internal/exec/runtime/classifier_behavior_test.go b/internal/exec/runtime/classifier_behavior_test.go
index c366692f7..ca4dd7c85 100644
--- a/internal/exec/runtime/classifier_behavior_test.go
+++ b/internal/exec/runtime/classifier_behavior_test.go
@@ -533,7 +533,10 @@ const positionalInvokeFixture = `
attribute label : String = "";
action run { in x : String; first apply; action apply { assign label := x; } }
calc run { in x : Integer; return : Integer = level + x; }
+ action countUp : Count { inout redefines tally;
+ first apply; action apply { assign tally := tally + by; assign level := level + by; } }
}
+ action def Count { inout tally : Integer; in by : Integer = 1; }
}
`
@@ -631,6 +634,36 @@ func TestInvokeOperationWithPositionalArguments(t *testing.T) {
}
}
+// An unnamed `inout redefines` parameter is bound and returned under the name it
+// inherits, positionally or by name, the defaulted trailing input left out.
+func TestInvokeOperationReturnsRedefinedInout(t *testing.T) {
+ idx, _, ctx := buildRuntimeWithLibraries(t, "", parseAndBuild(t, positionalInvokeFixture))
+ tank := findSymbolByName(idx.DocumentRoot(""), "Tank", ast.DefPart)
+ if tank == nil {
+ t.Fatal("Tank not found")
+ }
+ inst, err := ctx.Instantiate(tank)
+ if err != nil {
+ t.Fatalf("Instantiate: %v", err)
+ }
+ results, err := ctx.InvokeOperationWith(inst, "countUp", OperationArguments{Positional: []Value{intArgument(10), intArgument(4)}})
+ if err != nil {
+ t.Fatalf("countUp(10, 4): %v", err)
+ }
+ wantResult(t, results, "tally", 14)
+ results, err = ctx.InvokeOperationWith(inst, "countUp", OperationArguments{Named: map[string]Value{"tally": intArgument(10)}})
+ if err != nil {
+ t.Fatalf("countUp(tally=10): %v", err)
+ }
+ wantResult(t, results, "tally", 11)
+ if len(results) != 1 {
+ t.Errorf("results = %v, want tally alone", results)
+ }
+ if fv, err := inst.GetFeatureValue(ctx, "level"); err != nil || fv.HeldValue().Const.Int != 7 {
+ t.Errorf("level = %v, %v, want 7", fv, err)
+ }
+}
+
// wantResult checks that an operation answered name with the integer want.
func wantResult(t *testing.T, results map[string]Value, name string, want int64) {
t.Helper()