statifier 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +601 -16
- data/lib/statifier/accepts.rb +167 -0
- data/lib/statifier/basichttp/decode.rb +142 -0
- data/lib/statifier/basichttp/encoding.rb +224 -0
- data/lib/statifier/basichttp/net_http_transport.rb +157 -0
- data/lib/statifier/basichttp/outbound.rb +87 -0
- data/lib/statifier/basichttp/processor.rb +170 -0
- data/lib/statifier/basichttp/request.rb +94 -0
- data/lib/statifier/basichttp/response.rb +34 -0
- data/lib/statifier/basichttp.rb +47 -0
- data/lib/statifier/compiler/content.rb +131 -0
- data/lib/statifier/compiler/error.rb +30 -0
- data/lib/statifier/compiler/expressions.rb +64 -0
- data/lib/statifier/compiler/numbering.rb +145 -0
- data/lib/statifier/compiler/parts.rb +144 -0
- data/lib/statifier/compiler/walk.rb +61 -0
- data/lib/statifier/compiler.rb +82 -0
- data/lib/statifier/datamodel/data_binding.rb +114 -0
- data/lib/statifier/datamodel/delay.rb +54 -0
- data/lib/statifier/datamodel/location.rb +145 -0
- data/lib/statifier/datamodel/program.rb +82 -0
- data/lib/statifier/datamodel/reason_value.rb +42 -0
- data/lib/statifier/datamodel/reasons.rb +88 -0
- data/lib/statifier/datamodel/stores.rb +169 -0
- data/lib/statifier/datamodel/system_variables.rb +69 -0
- data/lib/statifier/datamodel.rb +120 -0
- data/lib/statifier/document/data.rb +36 -0
- data/lib/statifier/document/executable.rb +69 -0
- data/lib/statifier/document/invoke.rb +17 -0
- data/lib/statifier/document/states.rb +40 -0
- data/lib/statifier/document.rb +51 -0
- data/lib/statifier/driver/calls.rb +183 -0
- data/lib/statifier/driver/checks.rb +110 -0
- data/lib/statifier/driver/delivery.rb +45 -0
- data/lib/statifier/driver/invocations.rb +129 -0
- data/lib/statifier/driver/mailbox.rb +120 -0
- data/lib/statifier/driver/routing.rb +193 -0
- data/lib/statifier/driver/session_id.rb +40 -0
- data/lib/statifier/driver/work.rb +196 -0
- data/lib/statifier/driver.rb +205 -0
- data/lib/statifier/effect.rb +208 -0
- data/lib/statifier/event.rb +67 -0
- data/lib/statifier/frozen.rb +22 -0
- data/lib/statifier/interpreter/blocks.rb +142 -0
- data/lib/statifier/interpreter/content.rb +228 -0
- data/lib/statifier/interpreter/entering.rb +141 -0
- data/lib/statifier/interpreter/exit_entry.rb +174 -0
- data/lib/statifier/interpreter/foreach.rb +83 -0
- data/lib/statifier/interpreter/invoke/arguments.rb +52 -0
- data/lib/statifier/interpreter/invoke/passes.rb +143 -0
- data/lib/statifier/interpreter/invoke.rb +145 -0
- data/lib/statifier/interpreter/loop.rb +170 -0
- data/lib/statifier/interpreter/machine_state.rb +152 -0
- data/lib/statifier/interpreter/macrostep.rb +185 -0
- data/lib/statifier/interpreter/name_match.rb +43 -0
- data/lib/statifier/interpreter/selection.rb +221 -0
- data/lib/statifier/interpreter/send.rb +223 -0
- data/lib/statifier/interpreter/step.rb +138 -0
- data/lib/statifier/interpreter/stop.rb +66 -0
- data/lib/statifier/interpreter/targets.rb +107 -0
- data/lib/statifier/interpreter.rb +57 -0
- data/lib/statifier/lowering/branches.rb +84 -0
- data/lib/statifier/lowering/data.rb +102 -0
- data/lib/statifier/lowering/error.rb +102 -0
- data/lib/statifier/lowering/executable.rb +106 -0
- data/lib/statifier/lowering/placement.rb +65 -0
- data/lib/statifier/lowering/reading.rb +53 -0
- data/lib/statifier/lowering/states.rb +111 -0
- data/lib/statifier/lowering.rb +129 -0
- data/lib/statifier/machine/content.rb +81 -0
- data/lib/statifier/machine/data.rb +42 -0
- data/lib/statifier/machine/states.rb +56 -0
- data/lib/statifier/machine/values.rb +37 -0
- data/lib/statifier/machine.rb +103 -0
- data/lib/statifier/position/rebuild.rb +84 -0
- data/lib/statifier/position/shape.rb +73 -0
- data/lib/statifier/position/text.rb +43 -0
- data/lib/statifier/position.rb +206 -0
- data/lib/statifier/validator/attributes.rb +35 -0
- data/lib/statifier/validator/context.rb +74 -0
- data/lib/statifier/validator/data.rb +136 -0
- data/lib/statifier/validator/error.rb +90 -0
- data/lib/statifier/validator/executable.rb +139 -0
- data/lib/statifier/validator/history.rb +81 -0
- data/lib/statifier/validator/messages.rb +142 -0
- data/lib/statifier/validator/states.rb +161 -0
- data/lib/statifier/validator.rb +94 -0
- data/lib/statifier/version.rb +4 -1
- data/lib/statifier/xml/character_data.rb +142 -0
- data/lib/statifier/xml/cursor.rb +131 -0
- data/lib/statifier/xml/markup.rb +153 -0
- data/lib/statifier/xml/nodes.rb +50 -0
- data/lib/statifier/xml/parser.rb +121 -0
- data/lib/statifier/xml/scope.rb +46 -0
- data/lib/statifier/xml/tags.rb +112 -0
- data/lib/statifier/xml.rb +85 -0
- data/lib/statifier.rb +386 -2
- metadata +129 -20
- data/.gitignore +0 -8
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -11
- data/Gemfile.lock +0 -33
- data/Rakefile +0 -10
- data/bin/console +0 -8
- data/bin/setup +0 -6
- data/statifier.gemspec +0 -34
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Driver
|
|
5
|
+
# The bodies of the driver's calls, which {Statifier.start},
|
|
6
|
+
# {Statifier.step}, {Statifier.advance}, {Statifier.report_send_failed},
|
|
7
|
+
# {Statifier.configuration} and {Statifier.done?} answer through, and
|
|
8
|
+
# the checks on what a host passes them.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module Calls
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# {Statifier.start}'s body.
|
|
15
|
+
#
|
|
16
|
+
# @return [Result]
|
|
17
|
+
def start(machine, options)
|
|
18
|
+
options = Checks.start_options(options)
|
|
19
|
+
types, session_id = options.values_at(:types, :session_id)
|
|
20
|
+
started = Interpreter::Loop.initialize(machine, **options.slice(:session_id, :clock, :datamodel, :trace,
|
|
21
|
+
:max_macrostep_rounds), **seams(options))
|
|
22
|
+
work = Run.begin_call(fresh(started.state, session_id, types), options[:processors], options[:clock])
|
|
23
|
+
ok(Run.drain(Run.perform(work, started.effects)).moved)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# What the driver declares to the interpreter as the chart starts: the
|
|
27
|
+
# registered types, their `_ioprocessors` entries and the routes.
|
|
28
|
+
def seams(options)
|
|
29
|
+
types, session_id = options.values_at(:types, :session_id)
|
|
30
|
+
{ send_types: (types unless types.empty?), ioprocessors: entries(options[:processors], types, session_id),
|
|
31
|
+
routes: own_routes(session_id) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The driver's state over a machine state that has just started.
|
|
35
|
+
def fresh(machine_state, session_id, types)
|
|
36
|
+
State.new(machine_state:, session_id:, now_ms: 0, timers: [].freeze, timer_sequence: 0,
|
|
37
|
+
external_queue: [].freeze, held_sends: {}.freeze, halted: nil, done: nil, send_types: types,
|
|
38
|
+
invocations: {}.freeze, mailbox: [].freeze, invoked_as: nil, cancelled: false)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# {Statifier.step}'s body.
|
|
42
|
+
#
|
|
43
|
+
# @return [Result]
|
|
44
|
+
def step(state, name, data:, processors:, clock:)
|
|
45
|
+
work = Run.begin_call(Checks.state(state, processors), processors, clock)
|
|
46
|
+
raise TypeError, "an event name is a String, got #{name.class}" unless name.is_a?(String)
|
|
47
|
+
return refused(:not_running) unless state.machine_state.running
|
|
48
|
+
|
|
49
|
+
ok(Run.drain(Routing.enqueue(work, Event.external(name, data:), counted: false).stamped).moved)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# {Statifier.report_send_failed}'s body: the failed send's
|
|
53
|
+
# `error.communication` delivered onto the internal queue and the chart
|
|
54
|
+
# run to a stable configuration, then the external queue taken, as a
|
|
55
|
+
# step takes it. Refused once the chart has stopped, and while a
|
|
56
|
+
# macrostep's round budget has halted it, as the reference's session
|
|
57
|
+
# drops a report then.
|
|
58
|
+
#
|
|
59
|
+
# @return [Result]
|
|
60
|
+
def report_send_failed(machine, state, send, options)
|
|
61
|
+
processors, clock = Checks.report(machine, state, send, options).values_at(:processors, :clock)
|
|
62
|
+
work = Run.begin_call(Checks.state(state, processors), processors, clock)
|
|
63
|
+
return refused(:not_running) unless state.machine_state.running
|
|
64
|
+
return refused(:halted) unless state.halted.nil?
|
|
65
|
+
|
|
66
|
+
work, deferred = Delivery.failed(work.stamped, send)
|
|
67
|
+
ok(Run.drain(Run.perform(work, deferred)).moved)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# {Statifier.advance}'s body: the clock moved `milliseconds` forward, every
|
|
71
|
+
# timer due by then, anywhere in the tree of sessions, fired in
|
|
72
|
+
# due-then-scheduled order, each run to completion before the next: the
|
|
73
|
+
# session it fired in, then each session above it.
|
|
74
|
+
#
|
|
75
|
+
# @return [Result]
|
|
76
|
+
def advance(state, milliseconds, processors:, clock:)
|
|
77
|
+
work = Run.begin_call(Checks.state(state, processors), processors, clock)
|
|
78
|
+
return refused(:invalid_duration) unless duration?(milliseconds)
|
|
79
|
+
|
|
80
|
+
until_ms = state.now_ms + milliseconds
|
|
81
|
+
while (found = next_due(work.state, until_ms))
|
|
82
|
+
path, timer = found
|
|
83
|
+
work = fire_at(work.with_state(now_ms: timer.due_ms), path, timer)
|
|
84
|
+
end
|
|
85
|
+
ok(work.with_state(now_ms: until_ms).moved)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The timer fired in the session `path` (the invoke ids from this
|
|
89
|
+
# session down) names, each session on the way drained after it.
|
|
90
|
+
def fire_at(work, path, timer)
|
|
91
|
+
return fire(due(work, timer), timer.delayed) if path.empty?
|
|
92
|
+
|
|
93
|
+
Run.drain(Mailbox.within(work, path.first) { fire_at(_1, path.drop(1), timer) })
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Whether `value` is a duration the clock can move by: a real number,
|
|
97
|
+
# finite and not negative.
|
|
98
|
+
def duration?(value) = value.is_a?(Numeric) && value.real? && value.finite? && value >= 0
|
|
99
|
+
|
|
100
|
+
# The work with `timer` taken off the pending timers and the clock at
|
|
101
|
+
# its due time.
|
|
102
|
+
def due(work, timer)
|
|
103
|
+
work.with_state(timers: work.state.timers.reject { _1.equal?(timer) }.freeze, now_ms: timer.due_ms)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# The earliest pending timer due by `until_ms` in the tree, the first
|
|
107
|
+
# scheduled among equals, as `[path, timer]`, or nil.
|
|
108
|
+
def next_due(state, until_ms)
|
|
109
|
+
pending(state, []).select { _2.due_ms <= until_ms }.min_by { [_2.due_ms, _2.sequence] }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Every pending timer of the tree under `state`, each with the path to
|
|
113
|
+
# its session.
|
|
114
|
+
def pending(state, path)
|
|
115
|
+
state.timers.map { [path, _1] } +
|
|
116
|
+
state.invocations.flat_map { |id, invocation| pending(invocation.child, path + [id]) }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# A fired timer's send, routed as an immediate one is but never
|
|
120
|
+
# counted, then the external queue taken. While a macrostep's budget
|
|
121
|
+
# has halted the session, a send to the session itself still joins
|
|
122
|
+
# its external queue, and any other is dropped, as the reference drops
|
|
123
|
+
# it.
|
|
124
|
+
def fire(work, send)
|
|
125
|
+
unless work.state.halted.nil?
|
|
126
|
+
return work unless Routing.destination(work, send) == :external
|
|
127
|
+
|
|
128
|
+
return Routing.enqueue(work, Routing.delivered_event(send, work.state.session_id), counted: false)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
work, deferred = Routing.route(work.stamped, send, undelayed: false)
|
|
132
|
+
Run.drain(Run.perform(work, deferred))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# {Statifier.configuration}'s body: the active states' ids, the root
|
|
136
|
+
# left out, sorted. A state the document gave no id is named `#` and
|
|
137
|
+
# its index, a spelling no XML id can take.
|
|
138
|
+
#
|
|
139
|
+
# @return [Result]
|
|
140
|
+
def configuration(state)
|
|
141
|
+
Checks.state(state, nil)
|
|
142
|
+
ok(names(state.machine_state.machine, state.machine_state.configuration - [0]))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# {Statifier.done?}'s body.
|
|
146
|
+
#
|
|
147
|
+
# @return [Result]
|
|
148
|
+
def done?(state)
|
|
149
|
+
Checks.state(state, nil)
|
|
150
|
+
done = state.done
|
|
151
|
+
return ok(DoneStatus.new(done: false, donedata: nil, configuration: nil)) if done.nil?
|
|
152
|
+
|
|
153
|
+
ok(DoneStatus.new(done: true, donedata: done.donedata,
|
|
154
|
+
configuration: names(state.machine_state.machine, done.configuration - [0])))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# The states at `indexes` by name, sorted, a frozen list.
|
|
158
|
+
def names(machine, indexes) = indexes.map { machine.state(_1).id || "##{_1}" }.sort.freeze
|
|
159
|
+
|
|
160
|
+
# A successful result.
|
|
161
|
+
def ok(value) = Result.new(value:, error: nil)
|
|
162
|
+
|
|
163
|
+
# A refused call.
|
|
164
|
+
def refused(reason) = Result.new(value: nil, error: reason)
|
|
165
|
+
|
|
166
|
+
# The routes a session the driver runs can reach: itself, its parent
|
|
167
|
+
# when it has one, and its live invocations, by id.
|
|
168
|
+
def own_routes(session_id, parent: false, invokes: [].freeze)
|
|
169
|
+
Interpreter::Targets::Routes.new(sessions: [session_id].freeze, parent?: parent, invokes: invokes.freeze)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Each registered type's `_ioprocessors` entry, asked of its processor
|
|
173
|
+
# once as the chart starts; empty when it answers none.
|
|
174
|
+
def entries(processors, types, session_id)
|
|
175
|
+
context = Context.new(session_id:)
|
|
176
|
+
types.to_h do |type|
|
|
177
|
+
processor = processors[type]
|
|
178
|
+
[type, processor.respond_to?(:ioprocessors_entry) ? processor.ioprocessors_entry(type, context) : {}]
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Driver
|
|
5
|
+
# The checks on what a host passes the driver's calls.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module Checks
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# The options {Statifier.start} takes, as they stand when the host
|
|
12
|
+
# names none.
|
|
13
|
+
START_OPTIONS = {
|
|
14
|
+
session_id: nil, datamodel: {}.freeze, processors: {}.freeze, max_macrostep_rounds: MAX_MACROSTEP_ROUNDS,
|
|
15
|
+
trace: false, clock: nil
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
# {Statifier.start}'s options, after checking them, with the
|
|
19
|
+
# registered `types` and the `session_id`, minted when the host named
|
|
20
|
+
# none.
|
|
21
|
+
#
|
|
22
|
+
# @raise [ArgumentError] for an option {START_OPTIONS} does not name,
|
|
23
|
+
# or a round budget that is neither a positive Integer nor
|
|
24
|
+
# `:infinity`
|
|
25
|
+
def start_options(options)
|
|
26
|
+
options = known(options, START_OPTIONS)
|
|
27
|
+
budget(options[:max_macrostep_rounds])
|
|
28
|
+
options.merge(types: registered_types(options[:processors]), session_id: session_id(options[:session_id]))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The registered types, sorted, after checking the processors.
|
|
32
|
+
#
|
|
33
|
+
# @raise [TypeError] when `processors` is not a Hash of String types to
|
|
34
|
+
# objects answering `deliver`
|
|
35
|
+
def registered_types(processors)
|
|
36
|
+
raise TypeError, "processors is a Hash of send types to processors" unless processors.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
processors.each do |type, processor|
|
|
39
|
+
raise TypeError, "a send type is a String, got #{type.inspect}" unless type.is_a?(String)
|
|
40
|
+
raise TypeError, "the processor for #{type.inspect} does not answer deliver" \
|
|
41
|
+
unless processor.respond_to?(:deliver)
|
|
42
|
+
end
|
|
43
|
+
processors.keys.sort.freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# `state`, after checking it, and, unless `processors` is nil, that
|
|
47
|
+
# they register the types the chart was started with.
|
|
48
|
+
#
|
|
49
|
+
# @raise [TypeError] when `state` is not a driver state
|
|
50
|
+
# @raise [ArgumentError] when the processors name other types
|
|
51
|
+
def state(state, processors)
|
|
52
|
+
raise TypeError, "expected a driver state a call answered, got #{state.class}" unless state.is_a?(State)
|
|
53
|
+
return state if processors.nil? || registered_types(processors) == state.send_types
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, "processors must register the types the chart started with: #{state.send_types}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The options {Statifier.report_send_failed} takes, as they stand when
|
|
59
|
+
# the host names none.
|
|
60
|
+
REPORT_OPTIONS = { reason: nil, processors: {}.freeze, clock: nil }.freeze
|
|
61
|
+
|
|
62
|
+
# A failed-send report's options, after checking them and the chart and
|
|
63
|
+
# the send it names against `state`.
|
|
64
|
+
#
|
|
65
|
+
# @raise [TypeError] when `state` is not a driver state, `machine` is
|
|
66
|
+
# not a compiled machine or `send` is not a send a processor is handed
|
|
67
|
+
# @raise [ArgumentError] for an option {REPORT_OPTIONS} does not name, or
|
|
68
|
+
# when `state` is not an execution of `machine`
|
|
69
|
+
def report(machine, state, send, options)
|
|
70
|
+
options = known(options, REPORT_OPTIONS)
|
|
71
|
+
raise TypeError, "expected a compiled machine, got #{machine.class}" unless machine.is_a?(Machine)
|
|
72
|
+
raise TypeError, "expected a Statifier::Effect::Send or SendDelayed, got #{send.class}" \
|
|
73
|
+
unless send.is_a?(Effect::Send) || send.is_a?(Effect::SendDelayed)
|
|
74
|
+
return options if state(state, nil).machine_state.machine == machine
|
|
75
|
+
|
|
76
|
+
raise ArgumentError, "the state is not an execution of the machine named"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# `options` over `defaults`, after checking that `defaults` names
|
|
80
|
+
# every one.
|
|
81
|
+
#
|
|
82
|
+
# @raise [ArgumentError] for an option `defaults` does not name
|
|
83
|
+
def known(options, defaults)
|
|
84
|
+
unknown = options.keys - defaults.keys
|
|
85
|
+
raise ArgumentError, "unknown options: #{unknown.inspect}" unless unknown.empty?
|
|
86
|
+
|
|
87
|
+
defaults.merge(options)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The session id the host named, or a fresh one, frozen: a host's
|
|
91
|
+
# string is copied first, so the state holds nothing the host can
|
|
92
|
+
# change and the host's string is never frozen in place.
|
|
93
|
+
#
|
|
94
|
+
# @raise [TypeError] when it is neither nil nor a String
|
|
95
|
+
def session_id(session_id)
|
|
96
|
+
return -SessionId.mint if session_id.nil?
|
|
97
|
+
return -session_id if session_id.is_a?(String)
|
|
98
|
+
|
|
99
|
+
raise TypeError, "a session id is a String, got #{session_id.class}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The round budget, after checking it.
|
|
103
|
+
def budget(budget)
|
|
104
|
+
return budget if budget == :infinity || (budget.is_a?(Integer) && budget.positive?)
|
|
105
|
+
|
|
106
|
+
raise ArgumentError, "max_macrostep_rounds is a positive Integer or :infinity, got #{budget.inspect}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Driver
|
|
5
|
+
# A send of a registered type handed to the host's processor, and a send
|
|
6
|
+
# that processor could not deliver, as the reference's session hands a
|
|
7
|
+
# send to its processor's `deliver/3` and writes a failed send in
|
|
8
|
+
# `Statifier.Session.failed_send/3`. Each function answers
|
|
9
|
+
# `[work, deferred effects]`, as {Routing}'s do.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module Delivery
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# A registered type's send handed to its processor with the event a
|
|
16
|
+
# delivery would carry; a delayed one is held under its send id, so a
|
|
17
|
+
# `<cancel>` of that id reaches the processor. A {DeliveryFailure} the
|
|
18
|
+
# processor answers fails the send here, at its place in the call, as
|
|
19
|
+
# the reference raises the failure its processor plans in `deliver/3`.
|
|
20
|
+
# The failure counts against the call's budget as an undelayed send
|
|
21
|
+
# onto the internal queue does, so a chart that sends again on every
|
|
22
|
+
# failure halts rather than running on within the call.
|
|
23
|
+
def hand_off(work, send)
|
|
24
|
+
work = hold(work, send) if send.is_a?(Effect::SendDelayed)
|
|
25
|
+
event = Routing.delivered_event(send, work.state.session_id)
|
|
26
|
+
answered = work.processors.fetch(send.type).deliver(send, event, work.context)
|
|
27
|
+
return [work, []] unless answered.is_a?(DeliveryFailure)
|
|
28
|
+
|
|
29
|
+
Routing.counted(work) { failed(_1, send) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The work with the send's type holding a delayed send under its id.
|
|
33
|
+
def hold(work, send)
|
|
34
|
+
types = (work.state.held_sends.fetch(send.send_id, []) | [send.type]).sort.freeze
|
|
35
|
+
work.with_state(held_sends: work.state.held_sends.merge(send.send_id => types).freeze)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A send its processor could not deliver, as the reference's
|
|
39
|
+
# `Statifier.Session.failed_send/3` writes it: `error.communication`
|
|
40
|
+
# on the internal queue, its origin the send's content and its
|
|
41
|
+
# `sendid` the send's id whether or not its author named it.
|
|
42
|
+
def failed(work, send) = Routing.communication_error(work, send, sendid: send.send_id)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Driver
|
|
5
|
+
# The invocations a session holds and the child sessions behind them, as
|
|
6
|
+
# the reference's session runs a built-in `scxml` invocation
|
|
7
|
+
# (`Statifier.Session`, `Statifier.Session.Invocations` and
|
|
8
|
+
# `Statifier.Invoke.Source` in statifier-ex), with a child's {State}
|
|
9
|
+
# held in its parent's {Invocation} in place of a process. {Driver} says
|
|
10
|
+
# what each does; what passes between a parent and its child is
|
|
11
|
+
# {Mailbox}'s.
|
|
12
|
+
#
|
|
13
|
+
# A child runs as a work of its own ({Mailbox.within}): it starts on its
|
|
14
|
+
# parent's clock and timer sequence, and its state, the timer sequence
|
|
15
|
+
# it leaves, its effects and what it sent its parent go back to the
|
|
16
|
+
# parent once it has run.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
module Invocations
|
|
20
|
+
# The fields of an error an invocation raises: no data and no send id.
|
|
21
|
+
NO_FIELDS = { data: Predicator::UNDEFINED, sendid: nil }.freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# One invocation effect acted on: `[work, deferred effects]`.
|
|
26
|
+
def perform(work, effect)
|
|
27
|
+
case effect
|
|
28
|
+
when Effect::Invoke then start(work, effect)
|
|
29
|
+
when Effect::CancelInvoke then [cancel(work, effect.invoke_id), []]
|
|
30
|
+
when Effect::Autoforward then [Mailbox.deliver(work, effect.invoke_id, effect.event), []]
|
|
31
|
+
else [work, []]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# An {Effect::Invoke}: a type other than the SCXML type raises
|
|
36
|
+
# `error.execution`; content that is not markup, or does not compile,
|
|
37
|
+
# raises `error.communication`; otherwise the child is started.
|
|
38
|
+
# `[work, deferred effects]`.
|
|
39
|
+
def start(work, effect)
|
|
40
|
+
origin = { kind: :invoke, state_index: effect.state_index, invoke_index: effect.invoke_index }
|
|
41
|
+
unless Interpreter::Invoke.built_in_type?(effect.type)
|
|
42
|
+
return Routing.deliver_internal(work, :platform, "error.execution", origin, NO_FIELDS)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
machine = effect.content.is_a?(String) ? compile(effect.content) : nil
|
|
46
|
+
return Routing.deliver_internal(work, :platform, "error.communication", origin, NO_FIELDS) if machine.nil?
|
|
47
|
+
|
|
48
|
+
[launch(work, effect, machine), []]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Inline content markup compiled as the reference's
|
|
52
|
+
# `Statifier.Invoke.Source` compiles it, a root with no namespace
|
|
53
|
+
# accepted; nil when any stage refuses it.
|
|
54
|
+
def compile(source)
|
|
55
|
+
parsed = XML.parse(source)
|
|
56
|
+
return nil unless parsed.ok?
|
|
57
|
+
|
|
58
|
+
lowered = Lowering.lower(parsed.value, source)
|
|
59
|
+
return nil unless lowered.ok?
|
|
60
|
+
|
|
61
|
+
validated = Validator.validate(lowered.value, source, invoke_content_markup: true)
|
|
62
|
+
return nil unless validated.ok?
|
|
63
|
+
|
|
64
|
+
compiled = Compiler.compile(validated.value, warnings: validated.warnings)
|
|
65
|
+
compiled.ok? ? compiled.value : nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The child started, recorded live under its invoke id, and run to a
|
|
69
|
+
# stable configuration.
|
|
70
|
+
def launch(work, effect, machine)
|
|
71
|
+
child = child_start(work, effect, machine)
|
|
72
|
+
work = work.put_invocation(Invocation.new(invoke_id: effect.invoke_id, autoforward: effect.autoforward == true,
|
|
73
|
+
completed: false, caller_context: effect.caller_context,
|
|
74
|
+
child: child.state))
|
|
75
|
+
Mailbox.within(work, effect.invoke_id) { Run.drain(Run.perform(_1, child.effects)) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The child's start: its {State} and the effects its start answered,
|
|
79
|
+
# as a {Interpreter::Stepped}. Its session id is its parent's, a dot
|
|
80
|
+
# and the invoke id.
|
|
81
|
+
def child_start(work, effect, machine)
|
|
82
|
+
session_id = -"#{work.state.session_id}.#{effect.invoke_id}"
|
|
83
|
+
started = Interpreter::Loop.initialize(machine, **start_options(work, session_id, effect.params, machine))
|
|
84
|
+
state = Calls.fresh(started.state, session_id, work.state.send_types).with(invoked_as: effect.invoke_id)
|
|
85
|
+
Interpreter::Stepped.new(state:, effects: started.effects)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# What the child starts with: its parent's registered types, their
|
|
89
|
+
# `_ioprocessors` entries asked for its own session id, its parent's
|
|
90
|
+
# trace flag, and its datamodel seeded ({seed}).
|
|
91
|
+
def start_options(work, session_id, params, machine)
|
|
92
|
+
types = work.state.send_types
|
|
93
|
+
{ session_id:, clock: work.clock, datamodel: seed(params, machine), trace: work.machine_state.trace,
|
|
94
|
+
send_types: (types unless types.empty?), ioprocessors: Calls.entries(work.processors, types, session_id),
|
|
95
|
+
routes: Calls.own_routes(session_id, parent: true) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The reference's `Invocations.seed_datamodel/2`: the params a root
|
|
99
|
+
# `<data>` of the child names, the rest dropped.
|
|
100
|
+
def seed(params, machine)
|
|
101
|
+
return {} unless params.is_a?(Hash)
|
|
102
|
+
|
|
103
|
+
params.slice(*machine.state(0).data.map { machine.data_elements.fetch(_1).id })
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# An {Effect::CancelInvoke}: the invocation retired, then its child
|
|
107
|
+
# stopped, its active states' `<onexit>` handlers run, its effects
|
|
108
|
+
# reported and what it sent its parent discarded. An invocation whose
|
|
109
|
+
# child already said it completed is left for its done event, and an
|
|
110
|
+
# id that names nothing live is a no-op.
|
|
111
|
+
def cancel(work, invoke_id)
|
|
112
|
+
invocation = work.state.invocations[invoke_id]
|
|
113
|
+
return work if invocation.nil? || invocation.completed
|
|
114
|
+
|
|
115
|
+
work = work.drop_invocation(invoke_id)
|
|
116
|
+
return work unless invocation.child.machine_state.running
|
|
117
|
+
|
|
118
|
+
Mailbox.absorb(work, stop(work, work.synced(invocation.child).with(cancelled: true)))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The cancelled child's run: the reference's `Interpreter.cancel/1`
|
|
122
|
+
# and the effects it answered acted on.
|
|
123
|
+
def stop(work, child)
|
|
124
|
+
handled = Interpreter::Loop.cancel(child.machine_state)
|
|
125
|
+
Run.perform(Run.begin_call(child, work.processors, work.clock).with_machine(handled.state), handled.effects)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Driver
|
|
5
|
+
# What passes between a session and its invoked children, as the
|
|
6
|
+
# reference's session passes it (`Statifier.Session`, its
|
|
7
|
+
# `Statifier.Session.Invocations` table and `Statifier.Invoke.Answer` in
|
|
8
|
+
# statifier-ex): a child run inside its parent's work ({within}), an
|
|
9
|
+
# event delivered to a child, and what a child sends its parent - an
|
|
10
|
+
# event to `#_parent`, the notice that it completed and its donedata -
|
|
11
|
+
# waiting in the parent's mailbox until the parent takes it.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module Mailbox
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# The child under `invoke_id` run by the block, which takes and
|
|
18
|
+
# answers the child's work, then its state, timer sequence, effects
|
|
19
|
+
# and mail taken back.
|
|
20
|
+
#
|
|
21
|
+
# @return [Work]
|
|
22
|
+
def within(work, invoke_id)
|
|
23
|
+
invocation = work.state.invocations.fetch(invoke_id)
|
|
24
|
+
ran = yield Run.begin_call(work.synced(invocation.child), work.processors, work.clock)
|
|
25
|
+
receive(absorb(work.put_invocation(invocation.with(child: ran.state)), ran), ran.outbox)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The work with what a child's run left it: the timer sequence and the
|
|
29
|
+
# effects.
|
|
30
|
+
def absorb(work, ran) = work.with_state(timer_sequence: ran.state.timer_sequence).report_all(ran.effects)
|
|
31
|
+
|
|
32
|
+
# The work with a child's mail in its mailbox, an undelayed send's
|
|
33
|
+
# counted.
|
|
34
|
+
def receive(work, outbox)
|
|
35
|
+
outbox.reduce(work) do |acc, (mail, counted)|
|
|
36
|
+
acc = acc.with_state(mailbox: (acc.state.mailbox + [mail]).freeze)
|
|
37
|
+
counted ? acc.with(counted: (acc.counted + [mail]).freeze) : acc
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A send to `#_parent`: the event, stamped with the invoke id, into
|
|
42
|
+
# the parent's mailbox.
|
|
43
|
+
def to_parent(work, event, counted:)
|
|
44
|
+
event = Frozen.copy(event.with(invokeid: work.state.invoked_as))
|
|
45
|
+
work.post(Mail.new(kind: :event, invoke_id: work.state.invoked_as, payload: event), counted:)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# An event for the live child under `invoke_id`: it joins the child's
|
|
49
|
+
# external queue and the child runs to a stable configuration. Nothing
|
|
50
|
+
# reaches an invocation that is not live, or a child that has stopped.
|
|
51
|
+
def deliver(work, invoke_id, event)
|
|
52
|
+
invocation = work.state.invocations[invoke_id]
|
|
53
|
+
return work if invocation.nil? || !invocation.child.machine_state.running
|
|
54
|
+
|
|
55
|
+
within(work, invoke_id) { Run.drain(Routing.enqueue(_1, event, counted: false).stamped) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# A child whose batch stops it tells its parent it completed, ahead of
|
|
59
|
+
# everything the batch sends; a cancelled child does not.
|
|
60
|
+
#
|
|
61
|
+
# @return [Work]
|
|
62
|
+
def announce(work, batch)
|
|
63
|
+
state = work.state
|
|
64
|
+
return work if state.invoked_as.nil? || state.cancelled || batch.none?(Effect::Done)
|
|
65
|
+
|
|
66
|
+
work.post(Mail.new(kind: :completed, invoke_id: state.invoked_as, payload: nil))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# A child that stopped on its own sends its parent its donedata; one
|
|
70
|
+
# that was cancelled sends nothing.
|
|
71
|
+
#
|
|
72
|
+
# @return [Work]
|
|
73
|
+
def return_done(work, done)
|
|
74
|
+
state = work.state
|
|
75
|
+
return work if state.invoked_as.nil? || state.cancelled
|
|
76
|
+
|
|
77
|
+
work.post(Mail.new(kind: :done, invoke_id: state.invoked_as, payload: done.donedata))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# One mailbox entry. One that carries an event declares the routes as
|
|
81
|
+
# it arrives; an entry whose invocation is no longer live is
|
|
82
|
+
# discarded.
|
|
83
|
+
#
|
|
84
|
+
# @return [Work]
|
|
85
|
+
def take_mail(work, mail)
|
|
86
|
+
work = work.stamped unless mail.kind == :completed
|
|
87
|
+
invocation = work.state.invocations[mail.invoke_id]
|
|
88
|
+
invocation.nil? ? work : open_mail(work, mail, invocation)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# A live invocation's entry: a completion notice marks it completed;
|
|
92
|
+
# an event is taken as an external event; and the done event, built
|
|
93
|
+
# here, retires it once taken.
|
|
94
|
+
#
|
|
95
|
+
# @return [Work]
|
|
96
|
+
def open_mail(work, mail, invocation)
|
|
97
|
+
case mail.kind
|
|
98
|
+
when :completed then work.put_invocation(invocation.with(completed: true))
|
|
99
|
+
when :event then Run.take(work, mail.payload)
|
|
100
|
+
else Run.take(work, done_event(work.state.session_id, invocation, mail.payload)).drop_invocation(mail.invoke_id)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The reference's `Invoke.Answer.done/4`: `done.invoke.` and the
|
|
105
|
+
# invoke id, its data the child's donedata, from the parent's own
|
|
106
|
+
# address, carrying the invocation's `caller_context`.
|
|
107
|
+
#
|
|
108
|
+
# @return [Event]
|
|
109
|
+
def done_event(session_id, invocation, donedata)
|
|
110
|
+
Frozen.copy(Event.external(
|
|
111
|
+
"done.invoke.#{invocation.invoke_id}",
|
|
112
|
+
data: donedata, invokeid: invocation.invoke_id,
|
|
113
|
+
origin: Datamodel::SystemVariables.scxml_location(session_id),
|
|
114
|
+
origintype: Datamodel::SystemVariables::SCXML_EVENT_PROCESSOR,
|
|
115
|
+
caller_context: invocation.caller_context
|
|
116
|
+
))
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|