Skip to content

Commit 3686009

Browse files
Merge pull request #125 from Open-MBEE/feature/scheduling-policy
* feat(runtime): select the scheduling policy a run resolves its choice points under A run follows a named scheduling policy wherever the library orders nothing: `reverse` (the default, unchanged: reverse token order, first holding guard, first enabled transition), `declared` (spawn and declaration order) or `seed:<n>` (a PCG sequence the seed fixes, reproducible on every platform). Every choice point is still reported and each `took ...` is what the policy took. `explore` is reserved and refused by name until it exists. The same spelling is taken by `sysml -schedule`, `%schedule` in the REPL (a debugging session under way keeps its own), a `schedule` field on ExecuteActionRequest, ExecuteStateRequest and RunAnalysisRequest advertised as the `schedule` capability (a spelling naming no policy is INVALID_ARGUMENT), the Go and Python clients, and a `schedule` pin on a conformance case. The conformance suite runs whole under `declared` and `seed:1` as well, requiring every case that pins no policy and lists no outcomes to produce its default outputs; policy trace goldens pin every outcomes case. The two cases whose default result is one linearization of several pin `reverse`. Co-Authored-By: jason.han <hanhuijun@gmail.com> * docs(proto): describe the schedule capability in ServerInfoResponse Co-Authored-By: jason.han <hanhuijun@gmail.com> * chore(runtime): mark the seeded scheduler's generator as deliberately non-cryptographic Co-Authored-By: jason.han <hanhuijun@gmail.com> * docs(runtime): state that a calc body has no choice points and that policies may reach different ones Co-Authored-By: jason.han <hanhuijun@gmail.com> * test(runtime): admit both pairings of two accepts racing for two sends Restate action_choice_shared_message_accept as an outcomes case derived in the semantic oracle, with a .trace.order and declared/seed-1 trace goldens, in place of its reverse pin. send_identity_same_named_ports stays pinned: the via-less accept over-matches a port-addressed transfer, fixed separately. Co-Authored-By: jason.han <hanhuijun@gmail.com> * fix(runtime): read the picked guard for real and keep a driven run's scheduler A decision branch the policy picks past the first was read only in a probe the context undoes, so its evaluation left nothing behind. The run now reads the picked guard once more before taking the branch, as fireTransition reads a transition's guard again before it fires; the seed:1 trace golden of action_choice_decision_overlapping_guards gains that reading. An executor driven call by call (the REPL debuggers) keeps the scheduler its run started with: another run driven to completion while it is paused no longer replaces the sequence its remaining choices draw from. Co-Authored-By: jason.han <hanhuijun@gmail.com> * refactor(runtime): tighten the beginExecutorRun doc comment Co-Authored-By: jason.han <hanhuijun@gmail.com> * fix(runtime): ask a shared ancestor state for its transition once per dispatch selectCandidates remembers per source state which transition it offers, so a composite state reached from every leaf of its orthogonal regions is asked once per event dispatch or change poll: the choice among its enabled transitions draws once, yields one candidate and one choice note, and the run's next choice takes the seed's next draw. Nested transitions keep their priority. The state_choice_shared_ancestor_regions trace goldens lose the second leaf's redundant re-reading of the first guard. Co-Authored-By: jason.han <hanhuijun@gmail.com> * fix(runtime): pick a state's transition only once it survives conflict resolution An enclosing state's enabled transitions were chosen, and the seeded draw spent, before losesToNestedTransition dropped the candidate to a nested transition, so a choice that never fired and was never reported shifted every later seeded choice. Candidate discovery now returns the enabled positions and chooseTransition picks among them after conflict resolution, on both the dispatch and the change-poll path; a shared ancestor still draws once. Co-Authored-By: jason.han <hanhuijun@gmail.com> * docs(wire): carry the choice-point code in the scheduled ExecuteAction example Co-Authored-By: jason.han <hanhuijun@gmail.com> * fix(runtime): preview a machine's decision from its own run's scheduler Decide read the scheduler the context happened to hold, so after another run had used the context it could name a transition other than the one ProcessNextEvent then fired. It now previews under the executor's own scheduler, drawing nothing from it. Co-Authored-By: jason.han <hanhuijun@gmail.com> * fix(runtime): draw a seeded token order only among the tokens able to act A token parked at a join its other branches have not reached, or at an accept no message in flight answers, keeps its place in the step's order, so steps taken while tokens wait do not shift the choices the run makes later. Co-Authored-By: jason.han <hanhuijun@gmail.com> * fix(grpc): read RunAnalysis's schedule before requiring verification A malformed spelling is INVALID_ARGUMENT whether or not the service offers verification, as it is on ExecuteAction and ExecuteState. Co-Authored-By: jason.han <hanhuijun@gmail.com> * docs(runtime): record that send_identity_same_named_ports runs unpinned With a via-less state accept routed by the receiver a transfer reaches, the case has one enabled transition under every policy and needs no schedule pin. Co-Authored-By: jason.han <hanhuijun@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: jason.han <hanhuijun@gmail.com>
2 parents 79c2de9 + 098d5b8 commit 3686009

92 files changed

Lines changed: 4147 additions & 712 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

api/proto/sysml.pb.go

Lines changed: 53 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/proto/sysml.proto

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ message RunAnalysisRequest {
262262
repeated Value arguments = 4;
263263
// Arguments bound to input parameters by name.
264264
map<string, Value> named_arguments = 5;
265+
// Scheduling policy the run resolves its choice points under: "declared",
266+
// "reverse" or "seed:<n>". Empty is the default, "reverse"; any other
267+
// spelling is INVALID_ARGUMENT.
268+
string schedule = 6;
265269
}
266270

267271
// RunAnalysisResponse carries what the case computed and decided.
@@ -427,6 +431,10 @@ message ExecuteActionRequest {
427431
string model_hash = 1;
428432
string action_symbol_id = 2; // FQN of action def
429433
map<string, Value> inputs = 3; // parameter name → value
434+
// Scheduling policy the run resolves its choice points under: "declared",
435+
// "reverse" or "seed:<n>". Empty is the default, "reverse"; any other
436+
// spelling is INVALID_ARGUMENT.
437+
string schedule = 4;
430438
}
431439

432440
// ExecuteActionResponse contains action execution results
@@ -441,6 +449,10 @@ message ExecuteStateRequest {
441449
string model_hash = 1;
442450
string state_machine_symbol_id = 2;
443451
repeated string events = 3; // sequence of event names to process
452+
// Scheduling policy the run resolves its choice points under: "declared",
453+
// "reverse" or "seed:<n>". Empty is the default, "reverse"; any other
454+
// spelling is INVALID_ARGUMENT.
455+
string schedule = 4;
444456
}
445457

446458
// ExecuteStateResponse contains state machine execution trace
@@ -916,6 +928,11 @@ message ServerInfoResponse {
916928
// Markdown.
917929
// "diagnostic_codes" - Diagnostic.code is populated, so an empty code is a
918930
// finding none was assigned; without it every code is empty.
931+
// "schedule" - ExecuteActionRequest, ExecuteStateRequest and
932+
// RunAnalysisRequest take a schedule, the scheduling policy
933+
// the run resolves its choice points under; without it a
934+
// service drops the field and runs under the default, so a
935+
// client must not send one.
919936
repeated string capabilities = 2;
920937
}
921938

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
- **The scheduling policy a run resolves its choice points under is selectable.** Where the
2+
library orders nothing — several steppable tokens in one step, several holding guards at a
3+
decision, several enabled transitions out of one state for one event, and so whose same-step
4+
write to one feature stands — the executors follow a named policy: `reverse` (the default:
5+
reverse token order, first holding guard, first enabled transition, so every existing result and
6+
trace is unchanged), `declared` (tokens in spawn order, guards and transitions in declaration
7+
order) or `seed:<n>` (a pseudo-random order the seed fixes, so one seed replays one run on every
8+
platform and two seeds may take two linearizations). The spelling is the same everywhere: `sysml
9+
-schedule <policy>` for `-action`, `-state` and `-analysis` (a calc's body performs nothing, so
10+
`-calc` has no choice to make); `%schedule [<policy>]` in the REPL, shown with no argument and
11+
applied to the runs started after it while a debugging session under way keeps its own; a
12+
`schedule` field on `ExecuteActionRequest`, `ExecuteStateRequest` and `RunAnalysisRequest`,
13+
empty for the default and advertised as the `schedule` capability, with the Go and Python
14+
clients taking it as an option (`opensysml.WithSchedule`, `opensysml.Schedule`, `schedule=`);
15+
and a `schedule` pin on a conformance case, which the harness runs under. A policy changes only
16+
which alternative each choice takes: every choice point a run reaches is reported and each `took
17+
…` is what the policy took, though another linearization may reach other choice points. A
18+
spelling naming no policy — an unknown name, `seed` or `seed:` without a number, `seed:-1`,
19+
`seed:abc` — is refused before anything runs, as `INVALID_ARGUMENT` on the wire. `explore`, the
20+
bounded exhaustive replay, is reserved and refused by name until it exists. The conformance
21+
suite also runs whole under `declared` and `seed:1`, requiring every case that pins no policy
22+
and lists no `outcomes` to produce its default outputs. Two accepts racing for two sends now
23+
list both pairings as `outcomes`, with the derivation in the semantic oracle; a send to a
24+
same-named port pins `reverse` until the via-less accept that over-matches it is fixed.

client/opensysml/analysis.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type analysisOptions struct {
5252
subjectSymbolID string
5353
positional []Value
5454
named []namedArgument
55+
schedule string
5556
}
5657

5758
type namedArgument struct {
@@ -77,6 +78,13 @@ func Argument(name string, value Value) AnalysisOption {
7778
return func(o *analysisOptions) { o.named = append(o.named, namedArgument{name, value}) }
7879
}
7980

81+
// Schedule names the scheduling policy the actions the case performs resolve
82+
// their choice points under, as WithSchedule does for ExecuteAction. Requires
83+
// the schedule capability.
84+
func Schedule(policy string) AnalysisOption {
85+
return func(o *analysisOptions) { o.schedule = policy }
86+
}
87+
8088
func (c *client) RunAnalysis(
8189
ctx context.Context,
8290
model *Model,
@@ -98,10 +106,14 @@ func (c *client) RunAnalysis(
98106
if err := c.requireValueCapabilities(ctx, values...); err != nil {
99107
return nil, err
100108
}
109+
if err := c.requireSchedule(ctx, options.schedule); err != nil {
110+
return nil, err
111+
}
101112
req := &pb.RunAnalysisRequest{
102113
ModelHash: hash,
103114
SymbolId: symbolID,
104115
SubjectSymbolId: options.subjectSymbolID,
116+
Schedule: options.schedule,
105117
}
106118
for _, argument := range options.positional {
107119
sent, err := valueToProto(argument)

client/opensysml/client.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,15 @@ type Client interface {
6464
// requires the complex_values capability, an Array, Vector or
6565
// VectorQuantity input the structured_values one, a MeasurementRef input
6666
// the measurement_refs one and a Function input the function_values one,
67-
// checked before anything is sent.
68-
ExecuteAction(ctx context.Context, model *Model, actionSymbolID string, inputs map[string]Value) (*ActionRun, error)
67+
// checked before anything is sent. WithSchedule selects the scheduling
68+
// policy, which requires the schedule capability, checked the same way.
69+
ExecuteAction(ctx context.Context, model *Model, actionSymbolID string, inputs map[string]Value, opts ...ExecuteOption) (*ActionRun, error)
6970

7071
// ExecuteState runs the named state machine, feeding it the events in
7172
// order, and reports the states visited and the context left behind.
72-
ExecuteState(ctx context.Context, model *Model, stateMachineSymbolID string, events []string) (*StateRun, error)
73+
// WithSchedule selects the scheduling policy, which requires the schedule
74+
// capability, checked before anything is sent.
75+
ExecuteState(ctx context.Context, model *Model, stateMachineSymbolID string, events []string, opts ...ExecuteOption) (*StateRun, error)
7376

7477
// VerifyConstraint evaluates the named constraint, optionally Against a
7578
// part to instantiate and check. Requires the verification capability.
@@ -96,9 +99,10 @@ type Client interface {
9699
// reports its outputs with the verdict of its objective and of each
97100
// assertion in its body. Positional arguments bind its inputs in
98101
// declaration order; Against names its subject and Binding a parameter by
99-
// name. Requires the verification capability, and the complex_values or
100-
// structured_values capability for a Complex or a structured argument,
101-
// checked before anything is sent.
102+
// name; Schedule selects the scheduling policy. Requires the verification
103+
// capability, the complex_values or structured_values capability for a
104+
// Complex or a structured argument and the schedule capability for a
105+
// policy, checked before anything is sent.
102106
RunAnalysis(ctx context.Context, model *Model, symbolID string, opts ...AnalysisOption) (*Analysis, error)
103107

104108
// Query selects the model's elements the query matches, in declaration

client/opensysml/execute.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package opensysml
22

33
import (
44
"context"
5+
"fmt"
56
"maps"
67
"slices"
78

@@ -27,17 +28,59 @@ type StateRun struct {
2728
Diagnostics []Diagnostic
2829
}
2930

31+
// ExecuteOption configures ExecuteAction and ExecuteState.
32+
type ExecuteOption func(*executeOptions)
33+
34+
type executeOptions struct {
35+
schedule string
36+
}
37+
38+
// WithSchedule names the scheduling policy the run resolves its choice points
39+
// under — "declared", "reverse" (the default) or "seed:<n>" — spelled as the
40+
// sysml -schedule flag spells it. Requires the schedule capability; a spelling
41+
// naming no policy is refused with CodeInvalidArgument.
42+
func WithSchedule(policy string) ExecuteOption {
43+
return func(o *executeOptions) { o.schedule = policy }
44+
}
45+
46+
// requireSchedule refuses to send a policy to a service without the schedule
47+
// capability, which would run under the default rather than refuse it.
48+
func (c *client) requireSchedule(ctx context.Context, policy string) error {
49+
if policy == "" {
50+
return nil
51+
}
52+
info, err := c.serverInfo(ctx)
53+
if err != nil {
54+
return err
55+
}
56+
if !info.Has(CapabilitySchedule) {
57+
return &StatusError{
58+
Code: CodeUnimplemented,
59+
Message: fmt.Sprintf("capability %q is unavailable", CapabilitySchedule),
60+
}
61+
}
62+
return nil
63+
}
64+
3065
func (c *client) ExecuteAction(
3166
ctx context.Context,
3267
model *Model,
3368
actionSymbolID string,
3469
inputs map[string]Value,
70+
opts ...ExecuteOption,
3571
) (*ActionRun, error) {
72+
var options executeOptions
73+
for _, opt := range opts {
74+
opt(&options)
75+
}
3676
hash, err := c.call(model)
3777
if err != nil {
3878
return nil, err
3979
}
40-
req := &pb.ExecuteActionRequest{ModelHash: hash, ActionSymbolId: actionSymbolID}
80+
if err := c.requireSchedule(ctx, options.schedule); err != nil {
81+
return nil, err
82+
}
83+
req := &pb.ExecuteActionRequest{ModelHash: hash, ActionSymbolId: actionSymbolID, Schedule: options.schedule}
4184
if len(inputs) > 0 {
4285
if err := c.requireValueCapabilities(ctx, slices.Collect(maps.Values(inputs))...); err != nil {
4386
return nil, err
@@ -67,15 +110,24 @@ func (c *client) ExecuteState(
67110
model *Model,
68111
stateMachineSymbolID string,
69112
events []string,
113+
opts ...ExecuteOption,
70114
) (*StateRun, error) {
115+
var options executeOptions
116+
for _, opt := range opts {
117+
opt(&options)
118+
}
71119
hash, err := c.call(model)
72120
if err != nil {
73121
return nil, err
74122
}
123+
if err := c.requireSchedule(ctx, options.schedule); err != nil {
124+
return nil, err
125+
}
75126
resp, err := c.caller.executeState(ctx, &pb.ExecuteStateRequest{
76127
ModelHash: hash,
77128
StateMachineSymbolId: stateMachineSymbolID,
78129
Events: append([]string(nil), events...),
130+
Schedule: options.schedule,
79131
})
80132
if err != nil {
81133
return nil, err

0 commit comments

Comments
 (0)