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,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Driver
|
|
5
|
+
# Where each `<send>` the core answers goes, as the reference's planner
|
|
6
|
+
# (`Statifier.Session.Effects`) and its session's `deliver/5` route it:
|
|
7
|
+
# to a host's processor ({Delivery}), onto the external queue, onto the
|
|
8
|
+
# internal queue through the interpreter's internal delivery, onto a
|
|
9
|
+
# pending timer, or nowhere, with `error.communication`. Each function answers
|
|
10
|
+
# `[work, deferred effects]`, or the work alone where nothing is
|
|
11
|
+
# deferred.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module Routing
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# An immediate send: handed to its processor, or routed now.
|
|
18
|
+
def send_now(work, send)
|
|
19
|
+
return Delivery.hand_off(work, send) if registered?(work, send)
|
|
20
|
+
|
|
21
|
+
route(work, send, undelayed: true)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# A delayed send: handed to its processor, which holds it, or a
|
|
25
|
+
# pending timer due its delay from now.
|
|
26
|
+
def send_later(work, send)
|
|
27
|
+
return Delivery.hand_off(work, send) if registered?(work, send)
|
|
28
|
+
|
|
29
|
+
[schedule(work, send), []]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The work with a pending timer for the delayed send.
|
|
33
|
+
def schedule(work, send)
|
|
34
|
+
state = work.state
|
|
35
|
+
timer = Timer.new(send_id: send.send_id, due_ms: state.now_ms + send.delay_ms, sequence: state.timer_sequence,
|
|
36
|
+
delayed: send)
|
|
37
|
+
work.with_state(timers: (state.timers + [timer]).freeze, timer_sequence: timer.sequence + 1)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Whether the send's type is one the host registered.
|
|
41
|
+
def registered?(work, send)
|
|
42
|
+
Interpreter::Targets.classify(work.machine_state.sends.send_types, send.type) == :registered
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# A `<cancel>`: every pending timer under its id goes, and each
|
|
46
|
+
# processor holding a delayed send under it is told once.
|
|
47
|
+
#
|
|
48
|
+
# @return [Work]
|
|
49
|
+
def cancel(work, cancel)
|
|
50
|
+
id = cancel.send_id
|
|
51
|
+
work = work.with_state(timers: work.state.timers.reject { _1.send_id == id }.freeze)
|
|
52
|
+
held = work.state.held_sends.fetch(id, nil)
|
|
53
|
+
return work if held.nil?
|
|
54
|
+
|
|
55
|
+
held.each { tell_cancel(work, _1, cancel) }
|
|
56
|
+
release(work, id)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The work with no processor holding a delayed send under `id`.
|
|
60
|
+
def release(work, id) = work.with_state(held_sends: work.state.held_sends.except(id).freeze)
|
|
61
|
+
|
|
62
|
+
# The processor for `type` told of the `<cancel>`, when it takes one.
|
|
63
|
+
def tell_cancel(work, type, cancel)
|
|
64
|
+
processor = work.processors.fetch(type)
|
|
65
|
+
processor.cancel(cancel, work.context) if processor.respond_to?(:cancel)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# A built-in send routed: to the session itself (no target, or its own
|
|
69
|
+
# `#_scxml_` address) onto the external queue, counted when the send
|
|
70
|
+
# was undelayed; to `#_internal` onto the internal queue; to
|
|
71
|
+
# `#_parent` into the parent's mailbox, counted there when undelayed;
|
|
72
|
+
# to `#_<invokeid>` onto that live child's external queue; anything
|
|
73
|
+
# else reaches nothing.
|
|
74
|
+
def route(work, send, undelayed:)
|
|
75
|
+
event = -> { delivered_event(send, work.state.session_id) }
|
|
76
|
+
case destination(work, send)
|
|
77
|
+
when :external then [enqueue(work, event.call, counted: undelayed), []]
|
|
78
|
+
when :internal then internal(work, send, undelayed)
|
|
79
|
+
when :parent then [Mailbox.to_parent(work, event.call, counted: undelayed), []]
|
|
80
|
+
when :invoke then [Mailbox.deliver(work, Interpreter::Targets.parse_target(send.target).id, event.call), []]
|
|
81
|
+
else communication_error(work, send)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Where a built-in send's target leads: `:external`, `:internal`,
|
|
86
|
+
# `:parent` (a session that has one), `:invoke` (a live invocation) or
|
|
87
|
+
# `:nowhere`.
|
|
88
|
+
def destination(work, send)
|
|
89
|
+
route = Interpreter::Targets.parse_target(send.target)
|
|
90
|
+
work.reaches?(route) ? DESTINATIONS.fetch(route.kind, :nowhere) : :nowhere
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Where each kind of target leads when the session reaches it.
|
|
94
|
+
DESTINATIONS = { self: :external, session: :external, internal: :internal, parent: :parent,
|
|
95
|
+
invoke: :invoke }.freeze
|
|
96
|
+
|
|
97
|
+
# `event` joined to the external queue, marked as queued by an
|
|
98
|
+
# undelayed send when `counted`.
|
|
99
|
+
#
|
|
100
|
+
# @return [Work]
|
|
101
|
+
def enqueue(work, event, counted:)
|
|
102
|
+
event = Frozen.copy(event)
|
|
103
|
+
work = work.with_state(external_queue: (work.state.external_queue + [event]).freeze)
|
|
104
|
+
counted ? work.with(counted: (work.counted + [event]).freeze) : work
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# A send to `#_internal`. An undelayed one is counted against this
|
|
108
|
+
# call's budget; past it, the event waits on the internal queue unrun
|
|
109
|
+
# and the session halts.
|
|
110
|
+
def internal(work, send, undelayed)
|
|
111
|
+
deliver = ->(acc) { deliver_internal(acc, :internal, name(send), origin(send), fields(send, send.data)) }
|
|
112
|
+
undelayed ? counted(work, &deliver) : deliver.call(work)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A delivery onto the internal queue counted against this call's
|
|
116
|
+
# budget with the undelayed sends made there: made by `deliver` within
|
|
117
|
+
# the budget, and past it made with the chart not run and the session
|
|
118
|
+
# halted.
|
|
119
|
+
def counted(work, &deliver)
|
|
120
|
+
work = work.with(sent_internal: work.sent_internal + 1)
|
|
121
|
+
work.within?(work.sent_internal) ? deliver.call(work) : past_budget(work, deliver)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# An internal delivery past this call's budget: made with the chart
|
|
125
|
+
# not run, and the session halted, with the budget's effect when it
|
|
126
|
+
# was not halted already.
|
|
127
|
+
def past_budget(work, deliver)
|
|
128
|
+
return [work, []] unless work.machine_state.running
|
|
129
|
+
|
|
130
|
+
halted = !work.state.halted.nil?
|
|
131
|
+
work, = deliver.call(work.with(spent: true))
|
|
132
|
+
return [work, []] if halted
|
|
133
|
+
|
|
134
|
+
[work.with_state(halted: :budget_exhausted), Interpreter::Macrostep.exhausted(work.machine_state)]
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# A send to something the driver cannot reach: `error.communication`
|
|
138
|
+
# on the internal queue, carrying the send id the delivered event
|
|
139
|
+
# would have.
|
|
140
|
+
def communication_error(work, send, sendid: author_id(send))
|
|
141
|
+
deliver_internal(work, :platform, "error.communication", origin(send), { data: Predicator::UNDEFINED, sendid: })
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# The interpreter's internal delivery: the event joins the internal
|
|
145
|
+
# queue and the chart runs to a stable configuration, its effects
|
|
146
|
+
# deferred. A stopped chart takes nothing; once the call has halted
|
|
147
|
+
# the session for its budget, the event waits there unrun.
|
|
148
|
+
# `fields` are the event's `data:` and `sendid:`.
|
|
149
|
+
def deliver_internal(work, kind, name, origin, fields)
|
|
150
|
+
state = work.machine_state
|
|
151
|
+
return [work, []] unless state.running
|
|
152
|
+
return [work.with_machine(queued(state, kind, name, origin, fields)), []] if work.spent
|
|
153
|
+
|
|
154
|
+
handled = Interpreter::Loop.deliver_internal(work.stamped.machine_state, kind, name, origin, **fields,
|
|
155
|
+
clock: work.clock)
|
|
156
|
+
[work.with_machine(handled.state), handled.effects]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# The machine state with the event queued and not run.
|
|
160
|
+
def queued(state, kind, name, origin, fields)
|
|
161
|
+
state.public_send(kind == :internal ? :raise_internal : :raise_platform, name, origin, **fields)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# An event's `data:` and `sendid:` about the send: `data`, and the
|
|
165
|
+
# send's id when its author named it.
|
|
166
|
+
def fields(send, data) = { data:, sendid: author_id(send) }
|
|
167
|
+
|
|
168
|
+
# The event a delivered send carries, as the reference's
|
|
169
|
+
# `Statifier.Send.Event.build/3` makes it: from this session's own
|
|
170
|
+
# address through the SCXML Event I/O Processor, its send id only when
|
|
171
|
+
# the author named one, and a delayed send's `caller_context`.
|
|
172
|
+
#
|
|
173
|
+
# @return [Event]
|
|
174
|
+
def delivered_event(send, session_id)
|
|
175
|
+
Event.external(name(send), data: send.data,
|
|
176
|
+
origin: Datamodel::SystemVariables.scxml_location(session_id),
|
|
177
|
+
origintype: Datamodel::SystemVariables::SCXML_EVENT_PROCESSOR,
|
|
178
|
+
sendid: author_id(send),
|
|
179
|
+
caller_context: send.is_a?(Effect::SendDelayed) ? send.caller_context : nil)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# The send's event name: the empty name for a send whose event is not
|
|
183
|
+
# text.
|
|
184
|
+
def name(send) = send.event.is_a?(String) ? send.event : ""
|
|
185
|
+
|
|
186
|
+
# The send's id when its author named it, else nil.
|
|
187
|
+
def author_id(send) = send.id_from_author? ? send.send_id : nil
|
|
188
|
+
|
|
189
|
+
# The cause of an event about the send: its content node.
|
|
190
|
+
def origin(send) = { kind: :content, c_index: send.c_index, owner: send.owner }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Driver
|
|
5
|
+
# The session id the driver mints when the host names none, as the
|
|
6
|
+
# reference mints one (`Statifier.MachineState.generate_session_id/0`
|
|
7
|
+
# in statifier-ex): `sess_`, then a 48-bit millisecond timestamp and 80
|
|
8
|
+
# random bits written in lower-case Crockford base32, so ids sort by the
|
|
9
|
+
# millisecond they were minted in.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module SessionId
|
|
13
|
+
# Crockford base32's alphabet, in symbol order.
|
|
14
|
+
ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz"
|
|
15
|
+
|
|
16
|
+
# The timestamp's width in bits.
|
|
17
|
+
TIME_BITS = 48
|
|
18
|
+
|
|
19
|
+
# The random part's width in bytes.
|
|
20
|
+
RANDOM_BYTES = 10
|
|
21
|
+
|
|
22
|
+
module_function
|
|
23
|
+
|
|
24
|
+
# A fresh session id.
|
|
25
|
+
#
|
|
26
|
+
# @return [String]
|
|
27
|
+
def mint
|
|
28
|
+
millis = Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond) & ((1 << TIME_BITS) - 1)
|
|
29
|
+
encode((millis << (RANDOM_BYTES * 8)) | Random.urandom(RANDOM_BYTES).unpack1("H*").to_i(16))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The 128 bits of `value` in base32, the last symbol padded with two
|
|
33
|
+
# zero bits, as an unpadded base32 encoding of 16 bytes writes them.
|
|
34
|
+
def encode(value)
|
|
35
|
+
padded = value << 2
|
|
36
|
+
"sess_#{25.downto(0).map { |digit| ALPHABET[(padded >> (5 * digit)) & 31] }.join}"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Driver
|
|
5
|
+
# One call's work in progress, a value threaded through the driver's
|
|
6
|
+
# functions as the interpreter's loop threads its machine state: the
|
|
7
|
+
# {State} as it stands, the `effects` answered so far, the call's
|
|
8
|
+
# `processors` and `clock`, and the call's own budget counts: `counted`,
|
|
9
|
+
# the external events an undelayed send queued for the session in this
|
|
10
|
+
# call; `taken`, how many of those the call has taken; `sent_internal`,
|
|
11
|
+
# the undelayed sends it delivered onto its own internal queue; and
|
|
12
|
+
# `spent`, set once the call has halted the session for its budget; and
|
|
13
|
+
# `outbox`, what an invoked child's run sent its parent, each a
|
|
14
|
+
# `[Mail, counted]` pair, `counted` true for an undelayed send's event.
|
|
15
|
+
# A run of an invoked child is a work of its own, its counts its own.
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
Work = ::Data.define(:state, :effects, :processors, :clock, :counted, :taken, :sent_internal, :spent,
|
|
19
|
+
:outbox) do
|
|
20
|
+
# The interpreter's position.
|
|
21
|
+
def machine_state = state.machine_state
|
|
22
|
+
|
|
23
|
+
# The work with the interpreter's position replaced.
|
|
24
|
+
def with_machine(machine_state) = with(state: state.with(machine_state:))
|
|
25
|
+
|
|
26
|
+
# The work with the driver's own fields of the state replaced.
|
|
27
|
+
def with_state(**fields) = with(state: state.with(**fields))
|
|
28
|
+
|
|
29
|
+
# The round budget the chart was started with.
|
|
30
|
+
def budget = machine_state.max_macrostep_rounds
|
|
31
|
+
|
|
32
|
+
# Whether `count` is within the round budget.
|
|
33
|
+
def within?(count) = budget == :infinity || count <= budget
|
|
34
|
+
|
|
35
|
+
# The {Context} a processor is handed.
|
|
36
|
+
def context = Context.new(session_id: state.session_id)
|
|
37
|
+
|
|
38
|
+
# Whether `event` was queued by an undelayed send in this call.
|
|
39
|
+
def counted?(event) = counted.any? { _1.equal?(event) }
|
|
40
|
+
|
|
41
|
+
# What the call answers.
|
|
42
|
+
def moved = Moved.new(state:, effects: effects.freeze)
|
|
43
|
+
|
|
44
|
+
# The work with `effects` answered, in order.
|
|
45
|
+
def report_all(effects) = with(effects: (self.effects + effects).freeze)
|
|
46
|
+
|
|
47
|
+
# The work with `invocation` live under its id.
|
|
48
|
+
def put_invocation(invocation)
|
|
49
|
+
with_state(invocations: state.invocations.merge(invocation.invoke_id => invocation).freeze)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The work with the invocation under `invoke_id` no longer live.
|
|
53
|
+
def drop_invocation(invoke_id) = with_state(invocations: state.invocations.except(invoke_id).freeze)
|
|
54
|
+
|
|
55
|
+
# A child's state on this session's clock and timer sequence.
|
|
56
|
+
def synced(child) = child.with(now_ms: state.now_ms, timer_sequence: state.timer_sequence)
|
|
57
|
+
|
|
58
|
+
# The work with the routes its session can reach declared to the core:
|
|
59
|
+
# its own id, whether it has a parent and its live invocations.
|
|
60
|
+
def stamped = with_machine(machine_state.with(sends: machine_state.sends.with(routes:)))
|
|
61
|
+
|
|
62
|
+
# The {Interpreter::Targets::Routes} the session reaches now.
|
|
63
|
+
def routes
|
|
64
|
+
Calls.own_routes(state.session_id, parent: !state.invoked_as.nil?, invokes: state.invocations.keys.sort)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Whether the session reaches `route` (a {Interpreter::Targets::Route})
|
|
68
|
+
# now: its own `#_scxml_` address, its parent when it has one, and a
|
|
69
|
+
# live invocation; the targets that need no route are always reached.
|
|
70
|
+
def reaches?(route)
|
|
71
|
+
case route.kind
|
|
72
|
+
when :session then route.id == state.session_id
|
|
73
|
+
when :parent then !state.invoked_as.nil?
|
|
74
|
+
when :invoke then state.invocations.key?(route.id)
|
|
75
|
+
else true
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The work with `mail` sent to the parent.
|
|
80
|
+
def post(mail, counted: false) = with(outbox: (outbox + [[mail, counted]]).freeze)
|
|
81
|
+
|
|
82
|
+
# `effect` as the call reports it: an invoked child's wrapped in an
|
|
83
|
+
# {Effect::Child} naming the child.
|
|
84
|
+
def report(effect)
|
|
85
|
+
state.invoked_as.nil? ? effect : Effect::Child.new(session_id: state.session_id, effect:)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# How a call runs a chart: acting on the effects the core answers, in
|
|
90
|
+
# order, and taking the external queue one event per macrostep.
|
|
91
|
+
#
|
|
92
|
+
# @api private
|
|
93
|
+
module Run
|
|
94
|
+
module_function
|
|
95
|
+
|
|
96
|
+
# A call's work, before anything ran, over `state`.
|
|
97
|
+
#
|
|
98
|
+
# @return [Work]
|
|
99
|
+
def begin_call(state, processors, clock)
|
|
100
|
+
Work.new(state:, effects: [].freeze, processors:, clock:, counted: [].freeze, taken: 0, sent_internal: 0,
|
|
101
|
+
spent: false, outbox: [].freeze)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Acts on a batch of effects in order. A delivery onto the internal
|
|
105
|
+
# queue runs the chart at once, but its own effects are acted on after
|
|
106
|
+
# the batch, as the reference defers them; each is answered as it is
|
|
107
|
+
# acted on, so what a child it starts or reaches answers follows it. A
|
|
108
|
+
# child whose batch stops it tells its parent first
|
|
109
|
+
# ({Mailbox.announce}).
|
|
110
|
+
#
|
|
111
|
+
# @return [Work]
|
|
112
|
+
def perform(work, effects)
|
|
113
|
+
batch = effects
|
|
114
|
+
until batch.empty?
|
|
115
|
+
work = Mailbox.announce(work, batch)
|
|
116
|
+
work, batch = batch.reduce([work, []]) do |(acc, deferred), effect|
|
|
117
|
+
acc, more = perform_one(acc.with(effects: (acc.effects + [acc.report(effect)]).freeze), effect)
|
|
118
|
+
[acc, deferred + more]
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
work
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# One effect acted on: `[work, deferred effects]`.
|
|
125
|
+
def perform_one(work, effect)
|
|
126
|
+
case effect
|
|
127
|
+
when Effect::Send then Routing.send_now(work, effect)
|
|
128
|
+
when Effect::SendDelayed then Routing.send_later(work, effect)
|
|
129
|
+
when Effect::Cancel then [Routing.cancel(work, effect), []]
|
|
130
|
+
when Effect::Done then [stopped(work, effect), []]
|
|
131
|
+
when Effect::BudgetExhausted then [work.with_state(halted: :budget_exhausted), []]
|
|
132
|
+
else Invocations.perform(work, effect)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# The chart stopped: the {Effect::Done} is kept, and the pending timers,
|
|
137
|
+
# the queued external events, the untaken mail and the invocations
|
|
138
|
+
# its stop left (the completed ones) are discarded. A child that
|
|
139
|
+
# stopped on its own sends its parent its donedata.
|
|
140
|
+
def stopped(work, done)
|
|
141
|
+
Mailbox.return_done(work.with_state(done:, timers: [].freeze, external_queue: [].freeze, mailbox: [].freeze,
|
|
142
|
+
invocations: {}.freeze), done)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Takes the external queue one event per macrostep, while the chart
|
|
146
|
+
# runs and nothing has halted it, and, once it is empty, the mailbox
|
|
147
|
+
# one entry at a time ({Mailbox.take_mail}). A counted entry the
|
|
148
|
+
# session would take past its round budget in this call is left
|
|
149
|
+
# queued, and the session halts.
|
|
150
|
+
#
|
|
151
|
+
# @return [Work]
|
|
152
|
+
def drain(work)
|
|
153
|
+
loop do
|
|
154
|
+
entry, from = next_entry(work)
|
|
155
|
+
return work if entry.nil?
|
|
156
|
+
|
|
157
|
+
work = work.with(taken: work.taken + 1) if work.counted?(entry)
|
|
158
|
+
return halt(work) unless work.within?(work.taken)
|
|
159
|
+
|
|
160
|
+
work = take_entry(work, entry, from)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# The entry the drain takes next and where it waits (`:external` or
|
|
165
|
+
# `:mailbox`): none once the chart has stopped or a budget has halted
|
|
166
|
+
# it.
|
|
167
|
+
def next_entry(work)
|
|
168
|
+
state = work.state
|
|
169
|
+
return nil unless state.halted.nil? && work.machine_state.running
|
|
170
|
+
return [state.external_queue.first, :external] unless state.external_queue.empty?
|
|
171
|
+
|
|
172
|
+
[state.mailbox.first, :mailbox] unless state.mailbox.empty?
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# One entry taken off where it waits.
|
|
176
|
+
def take_entry(work, entry, from)
|
|
177
|
+
state = work.state
|
|
178
|
+
return take(work.with_state(external_queue: state.external_queue.drop(1).freeze), entry) if from == :external
|
|
179
|
+
|
|
180
|
+
Mailbox.take_mail(work.with_state(mailbox: state.mailbox.drop(1).freeze), entry)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# One external event, one macrostep.
|
|
184
|
+
def take(work, event)
|
|
185
|
+
handled = Interpreter::Loop.handle_event(work.machine_state, event, clock: work.clock)
|
|
186
|
+
return work unless handled.ok?
|
|
187
|
+
|
|
188
|
+
perform(work.with_machine(handled.state), handled.effects)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# The session halted for this call's round budget, with the effect a
|
|
192
|
+
# macrostep that spends its rounds answers.
|
|
193
|
+
def halt(work) = perform(work.with(spent: true), Interpreter::Macrostep.exhausted(work.machine_state))
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "interpreter"
|
|
4
|
+
|
|
5
|
+
module Statifier
|
|
6
|
+
# The in-memory driver: the small loop around the interpreter that a host
|
|
7
|
+
# steps a chart with, as the reference's session is (`Statifier.Session`
|
|
8
|
+
# and `Statifier.Session.Effects` in statifier-ex), with no process, no
|
|
9
|
+
# real clock and no I/O. {Statifier.start}, {Statifier.step} and
|
|
10
|
+
# {Statifier.advance} are its calls; this module holds what they answer
|
|
11
|
+
# and the work behind them.
|
|
12
|
+
#
|
|
13
|
+
# The driver keeps the external queue and the delayed sends as pending
|
|
14
|
+
# timers on a virtual clock, routes every `<send>` the core answers, and
|
|
15
|
+
# hands a send of a registered type to the host's processor for that
|
|
16
|
+
# type. Every call takes a {State} and answers a new one, never changing
|
|
17
|
+
# the one it was given; nothing is kept between calls but what the
|
|
18
|
+
# answered state holds.
|
|
19
|
+
#
|
|
20
|
+
# Routing, as the reference's planner and session route a send. A send
|
|
21
|
+
# with no target, or to this session's own `#_scxml_` address, joins the
|
|
22
|
+
# external queue. A send to `#_internal` is delivered onto the internal
|
|
23
|
+
# queue through the interpreter's internal delivery
|
|
24
|
+
# ({Interpreter::Loop.deliver_internal}), and the chart runs to a stable
|
|
25
|
+
# configuration again; the effects of that run are acted on after the
|
|
26
|
+
# batch that sent it, as the reference defers them. `#_parent` from an
|
|
27
|
+
# invoked child, and `#_<invokeid>` naming a live invocation, deliver as
|
|
28
|
+
# external events (below). Another session's address, a parent a session
|
|
29
|
+
# does not have and an invocation that is not live reach nothing: the core
|
|
30
|
+
# refuses such an immediate send itself (the driver declares the routes a
|
|
31
|
+
# session can reach, its own id, whether it has a parent and the ids of
|
|
32
|
+
# its live invocations, when it starts, when an event reaches it from
|
|
33
|
+
# outside, when a timer fires in it and before an internal delivery), a
|
|
34
|
+
# send the declaration reaches that names nothing live by the time it is
|
|
35
|
+
# routed fails here, and a delayed one is judged when its timer fires,
|
|
36
|
+
# each with `error.communication` on the internal queue.
|
|
37
|
+
#
|
|
38
|
+
# Invocations, as the reference's session and its built-in `scxml`
|
|
39
|
+
# handler run them (`Statifier.Session`, `Statifier.Session.Invocations`
|
|
40
|
+
# and `Statifier.Invoke.Source` in statifier-ex), with one tree of
|
|
41
|
+
# session states in place of processes. An {Effect::Invoke} of the SCXML
|
|
42
|
+
# type whose content is markup compiles it, with the namespace relaxed as
|
|
43
|
+
# inline content is placed, and starts it as a child session, its session
|
|
44
|
+
# id its parent's, a dot and the invoke id, its datamodel seeded with the
|
|
45
|
+
# params a root `<data>` of the child names, the rest dropped. A content
|
|
46
|
+
# that is not markup, or does not compile, and an invocation with no
|
|
47
|
+
# content (`src` is never fetched) raise `error.communication` and record
|
|
48
|
+
# nothing; any other type raises `error.execution`. A child is stepped by
|
|
49
|
+
# this driver on its parent's virtual clock: starting it, an event sent to
|
|
50
|
+
# it and an autoforwarded event each run it to a stable configuration at
|
|
51
|
+
# once, and its timers fire with its parent's, in due-then-scheduled order
|
|
52
|
+
# across the tree. A child inherits its parent's registered send types
|
|
53
|
+
# (its processors are the call's, handed the child's own session id) and
|
|
54
|
+
# its trace flag, and every effect its runs answer is among the call's
|
|
55
|
+
# effects, in the order made, as an {Effect::Child} naming its session id.
|
|
56
|
+
#
|
|
57
|
+
# What a child sends its parent - an event to `#_parent`, stamped with the
|
|
58
|
+
# invoke id; a notice that it reached a top-level final, ahead of the
|
|
59
|
+
# batch that stops it; and its `done.invoke.<id>` with its donedata -
|
|
60
|
+
# waits in the parent's mailbox, taken one entry at a time once the
|
|
61
|
+
# parent's external queue is empty. An entry whose invocation is no longer
|
|
62
|
+
# live is discarded; the done event retires its invocation once taken. An
|
|
63
|
+
# {Effect::CancelInvoke} retires the invocation and stops its child,
|
|
64
|
+
# running its `<onexit>` handlers, unless the child already said it
|
|
65
|
+
# completed; a cancelled child returns no done event. An
|
|
66
|
+
# {Effect::Autoforward} delivers the event, unchanged, to a live child.
|
|
67
|
+
#
|
|
68
|
+
# The virtual clock. A delayed send of a built-in type becomes a pending
|
|
69
|
+
# timer due at the clock's time plus its delay. {Statifier.advance} moves
|
|
70
|
+
# the clock forward and fires every timer due by the new time, earliest
|
|
71
|
+
# first and, at one due time, in the order they were scheduled; each
|
|
72
|
+
# fired send is routed then, and a self-addressed one is taken as an
|
|
73
|
+
# external event before the next timer fires. A `<cancel>` removes every
|
|
74
|
+
# pending timer under its send id. When the chart stops, its pending
|
|
75
|
+
# timers and its queued external events are discarded.
|
|
76
|
+
#
|
|
77
|
+
# Registered send types. The processors a host registers are passed with
|
|
78
|
+
# every call, by type, and name the same types for the whole life of a
|
|
79
|
+
# chart: the types are fixed when the chart starts, as the reference fixes
|
|
80
|
+
# `_ioprocessors` then. A send of a registered type is handed to its
|
|
81
|
+
# processor's `deliver` with the event a delivery would carry and a
|
|
82
|
+
# {Context}, and is never routed or scheduled, delayed or not: the
|
|
83
|
+
# processor holds a delayed one, and a `<cancel>` of its send id reaches
|
|
84
|
+
# that processor's `cancel` once. What `deliver` answers is not read,
|
|
85
|
+
# unless it is a {DeliveryFailure}.
|
|
86
|
+
#
|
|
87
|
+
# A failed send. A send a processor could not deliver comes back to the
|
|
88
|
+
# chart as `error.communication` on its internal queue, its origin the
|
|
89
|
+
# send's content and its `sendid` the send's id whether or not its author
|
|
90
|
+
# named it, as the reference's `Statifier.Session.failed_send/3` writes it.
|
|
91
|
+
# A failure the processor knows of while it is handed the send is answered
|
|
92
|
+
# from `deliver` as a {DeliveryFailure}, and the event is raised then, at
|
|
93
|
+
# the send's place in the call, as the reference raises the failure its
|
|
94
|
+
# processor plans in `deliver/3`. A failure the host learns of later is
|
|
95
|
+
# reported through {Statifier.report_send_failed}, with the send it was
|
|
96
|
+
# handed: the driver keeps no table of handed sends. The reason the host
|
|
97
|
+
# gives is not read, and the chart is not handed it.
|
|
98
|
+
#
|
|
99
|
+
# A macrostep that spends its round budget halts the driver as the
|
|
100
|
+
# reference halts its session: further external events are queued and not
|
|
101
|
+
# taken. A fired timer to the chart itself joins its external queue and
|
|
102
|
+
# any other is dropped. One call also spends that budget
|
|
103
|
+
# twice more, so that it always returns where the reference's session
|
|
104
|
+
# would keep running: against the events it takes that an undelayed send
|
|
105
|
+
# queued for the session in this call, and against the undelayed sends it
|
|
106
|
+
# delivers onto its own internal queue. A fired timer's event is never
|
|
107
|
+
# counted, and an `:infinity` budget bounds neither.
|
|
108
|
+
module Driver
|
|
109
|
+
# A chart's execution between two calls: an immutable value a host keeps
|
|
110
|
+
# and hands back, reading it only through {Statifier.configuration} and
|
|
111
|
+
# {Statifier.done?}. Its members are internal and may change in any
|
|
112
|
+
# release.
|
|
113
|
+
#
|
|
114
|
+
# @api private
|
|
115
|
+
State = ::Data.define(
|
|
116
|
+
:machine_state, :session_id, :now_ms, :timers, :timer_sequence, :external_queue, :held_sends, :halted,
|
|
117
|
+
:done, :send_types, :invocations, :mailbox, :invoked_as, :cancelled
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# A live invocation of a child session, as the parent holds it: its
|
|
121
|
+
# `invoke_id`; `autoforward`; `completed`, set once the child said it
|
|
122
|
+
# reached a top-level final; `caller_context`, the {Effect::Invoke}'s,
|
|
123
|
+
# which its done event carries; and `child`, the child's {State}, whose
|
|
124
|
+
# `now_ms` and `timer_sequence` are its parent's as they stood when the
|
|
125
|
+
# child last ran.
|
|
126
|
+
#
|
|
127
|
+
# @api private
|
|
128
|
+
Invocation = ::Data.define(:invoke_id, :autoforward, :completed, :caller_context, :child)
|
|
129
|
+
|
|
130
|
+
# What a child sent its parent and the parent has not taken: `kind`
|
|
131
|
+
# `:completed` (no `payload`), `:event` (the {Event}) or `:done` (the
|
|
132
|
+
# child's donedata), and the `invoke_id` it came from.
|
|
133
|
+
#
|
|
134
|
+
# @api private
|
|
135
|
+
Mail = ::Data.define(:kind, :invoke_id, :payload)
|
|
136
|
+
|
|
137
|
+
# A delayed send waiting on the virtual clock: its send id, the virtual
|
|
138
|
+
# time it is due at, the order it was scheduled in, and the
|
|
139
|
+
# {Effect::SendDelayed} itself, `delayed`.
|
|
140
|
+
#
|
|
141
|
+
# @api private
|
|
142
|
+
Timer = ::Data.define(:send_id, :due_ms, :sequence, :delayed)
|
|
143
|
+
|
|
144
|
+
# What a call that moves a chart answers as its result's `value`.
|
|
145
|
+
#
|
|
146
|
+
# @!attribute [r] state
|
|
147
|
+
# @return [State] the chart's execution after the call
|
|
148
|
+
# @!attribute [r] effects
|
|
149
|
+
# @return [Array] every effect the call's runs answered, in the order
|
|
150
|
+
# they were made, a frozen list of {Statifier::Effect} values, an
|
|
151
|
+
# invoked child's each wrapped in an {Effect::Child}
|
|
152
|
+
Moved = ::Data.define(:state, :effects)
|
|
153
|
+
|
|
154
|
+
# What {Statifier.done?} answers as its result's `value`.
|
|
155
|
+
#
|
|
156
|
+
# @!attribute [r] done
|
|
157
|
+
# @return [Boolean] whether the chart has stopped
|
|
158
|
+
# @!attribute [r] donedata
|
|
159
|
+
# @return [Object, nil] the top-level final's `<donedata>` as the
|
|
160
|
+
# {Effect::Done} carried it (`Predicator::UNDEFINED` when it has
|
|
161
|
+
# none); nil while the chart runs
|
|
162
|
+
# @!attribute [r] configuration
|
|
163
|
+
# @return [Array<String>, nil] the states active when the chart
|
|
164
|
+
# stopped, named as {Statifier.configuration} names them; nil while
|
|
165
|
+
# the chart runs
|
|
166
|
+
DoneStatus = ::Data.define(:done, :donedata, :configuration) do
|
|
167
|
+
# @return [Boolean] whether the chart has stopped
|
|
168
|
+
def done? = done
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# What the driver tells a processor about the session it is called
|
|
172
|
+
# for.
|
|
173
|
+
#
|
|
174
|
+
# @!attribute [r] session_id
|
|
175
|
+
# @return [String] the session's id, the one `_sessionid` reads
|
|
176
|
+
Context = ::Data.define(:session_id)
|
|
177
|
+
|
|
178
|
+
# What a processor's `deliver` answers for a send it could not deliver:
|
|
179
|
+
# the send then fails within the call that handed it, as
|
|
180
|
+
# {Statifier.report_send_failed} fails a send reported later.
|
|
181
|
+
#
|
|
182
|
+
# @!attribute [r] reason
|
|
183
|
+
# @return [Object, nil] the host's own account of the failure; the
|
|
184
|
+
# driver does not read it and the chart is not handed it
|
|
185
|
+
DeliveryFailure = ::Data.define(:reason) do
|
|
186
|
+
# A failure with no reason given.
|
|
187
|
+
def initialize(reason: nil) = super
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# The round budget when the host sets none.
|
|
191
|
+
MAX_MACROSTEP_ROUNDS = Interpreter::MachineState::MAX_MACROSTEP_ROUNDS
|
|
192
|
+
|
|
193
|
+
require_relative "driver/session_id"
|
|
194
|
+
require_relative "driver/work"
|
|
195
|
+
require_relative "driver/routing"
|
|
196
|
+
require_relative "driver/delivery"
|
|
197
|
+
require_relative "driver/checks"
|
|
198
|
+
require_relative "driver/invocations"
|
|
199
|
+
require_relative "driver/mailbox"
|
|
200
|
+
require_relative "driver/calls"
|
|
201
|
+
|
|
202
|
+
private_constant :Calls, :Checks, :Delivery, :Invocation, :Invocations, :Mail, :Mailbox, :MAX_MACROSTEP_ROUNDS,
|
|
203
|
+
:Routing, :Run, :SessionId, :State, :Timer, :Work
|
|
204
|
+
end
|
|
205
|
+
end
|