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.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/{LICENSE.txt → LICENSE} +1 -1
  4. data/README.md +601 -16
  5. data/lib/statifier/accepts.rb +167 -0
  6. data/lib/statifier/basichttp/decode.rb +142 -0
  7. data/lib/statifier/basichttp/encoding.rb +224 -0
  8. data/lib/statifier/basichttp/net_http_transport.rb +157 -0
  9. data/lib/statifier/basichttp/outbound.rb +87 -0
  10. data/lib/statifier/basichttp/processor.rb +170 -0
  11. data/lib/statifier/basichttp/request.rb +94 -0
  12. data/lib/statifier/basichttp/response.rb +34 -0
  13. data/lib/statifier/basichttp.rb +47 -0
  14. data/lib/statifier/compiler/content.rb +131 -0
  15. data/lib/statifier/compiler/error.rb +30 -0
  16. data/lib/statifier/compiler/expressions.rb +64 -0
  17. data/lib/statifier/compiler/numbering.rb +145 -0
  18. data/lib/statifier/compiler/parts.rb +144 -0
  19. data/lib/statifier/compiler/walk.rb +61 -0
  20. data/lib/statifier/compiler.rb +82 -0
  21. data/lib/statifier/datamodel/data_binding.rb +114 -0
  22. data/lib/statifier/datamodel/delay.rb +54 -0
  23. data/lib/statifier/datamodel/location.rb +145 -0
  24. data/lib/statifier/datamodel/program.rb +82 -0
  25. data/lib/statifier/datamodel/reason_value.rb +42 -0
  26. data/lib/statifier/datamodel/reasons.rb +88 -0
  27. data/lib/statifier/datamodel/stores.rb +169 -0
  28. data/lib/statifier/datamodel/system_variables.rb +69 -0
  29. data/lib/statifier/datamodel.rb +120 -0
  30. data/lib/statifier/document/data.rb +36 -0
  31. data/lib/statifier/document/executable.rb +69 -0
  32. data/lib/statifier/document/invoke.rb +17 -0
  33. data/lib/statifier/document/states.rb +40 -0
  34. data/lib/statifier/document.rb +51 -0
  35. data/lib/statifier/driver/calls.rb +183 -0
  36. data/lib/statifier/driver/checks.rb +110 -0
  37. data/lib/statifier/driver/delivery.rb +45 -0
  38. data/lib/statifier/driver/invocations.rb +129 -0
  39. data/lib/statifier/driver/mailbox.rb +120 -0
  40. data/lib/statifier/driver/routing.rb +193 -0
  41. data/lib/statifier/driver/session_id.rb +40 -0
  42. data/lib/statifier/driver/work.rb +196 -0
  43. data/lib/statifier/driver.rb +205 -0
  44. data/lib/statifier/effect.rb +208 -0
  45. data/lib/statifier/event.rb +67 -0
  46. data/lib/statifier/frozen.rb +22 -0
  47. data/lib/statifier/interpreter/blocks.rb +142 -0
  48. data/lib/statifier/interpreter/content.rb +228 -0
  49. data/lib/statifier/interpreter/entering.rb +141 -0
  50. data/lib/statifier/interpreter/exit_entry.rb +174 -0
  51. data/lib/statifier/interpreter/foreach.rb +83 -0
  52. data/lib/statifier/interpreter/invoke/arguments.rb +52 -0
  53. data/lib/statifier/interpreter/invoke/passes.rb +143 -0
  54. data/lib/statifier/interpreter/invoke.rb +145 -0
  55. data/lib/statifier/interpreter/loop.rb +170 -0
  56. data/lib/statifier/interpreter/machine_state.rb +152 -0
  57. data/lib/statifier/interpreter/macrostep.rb +185 -0
  58. data/lib/statifier/interpreter/name_match.rb +43 -0
  59. data/lib/statifier/interpreter/selection.rb +221 -0
  60. data/lib/statifier/interpreter/send.rb +223 -0
  61. data/lib/statifier/interpreter/step.rb +138 -0
  62. data/lib/statifier/interpreter/stop.rb +66 -0
  63. data/lib/statifier/interpreter/targets.rb +107 -0
  64. data/lib/statifier/interpreter.rb +57 -0
  65. data/lib/statifier/lowering/branches.rb +84 -0
  66. data/lib/statifier/lowering/data.rb +102 -0
  67. data/lib/statifier/lowering/error.rb +102 -0
  68. data/lib/statifier/lowering/executable.rb +106 -0
  69. data/lib/statifier/lowering/placement.rb +65 -0
  70. data/lib/statifier/lowering/reading.rb +53 -0
  71. data/lib/statifier/lowering/states.rb +111 -0
  72. data/lib/statifier/lowering.rb +129 -0
  73. data/lib/statifier/machine/content.rb +81 -0
  74. data/lib/statifier/machine/data.rb +42 -0
  75. data/lib/statifier/machine/states.rb +56 -0
  76. data/lib/statifier/machine/values.rb +37 -0
  77. data/lib/statifier/machine.rb +103 -0
  78. data/lib/statifier/position/rebuild.rb +84 -0
  79. data/lib/statifier/position/shape.rb +73 -0
  80. data/lib/statifier/position/text.rb +43 -0
  81. data/lib/statifier/position.rb +206 -0
  82. data/lib/statifier/validator/attributes.rb +35 -0
  83. data/lib/statifier/validator/context.rb +74 -0
  84. data/lib/statifier/validator/data.rb +136 -0
  85. data/lib/statifier/validator/error.rb +90 -0
  86. data/lib/statifier/validator/executable.rb +139 -0
  87. data/lib/statifier/validator/history.rb +81 -0
  88. data/lib/statifier/validator/messages.rb +142 -0
  89. data/lib/statifier/validator/states.rb +161 -0
  90. data/lib/statifier/validator.rb +94 -0
  91. data/lib/statifier/version.rb +4 -1
  92. data/lib/statifier/xml/character_data.rb +142 -0
  93. data/lib/statifier/xml/cursor.rb +131 -0
  94. data/lib/statifier/xml/markup.rb +153 -0
  95. data/lib/statifier/xml/nodes.rb +50 -0
  96. data/lib/statifier/xml/parser.rb +121 -0
  97. data/lib/statifier/xml/scope.rb +46 -0
  98. data/lib/statifier/xml/tags.rb +112 -0
  99. data/lib/statifier/xml.rb +85 -0
  100. data/lib/statifier.rb +386 -2
  101. metadata +129 -20
  102. data/.gitignore +0 -8
  103. data/CODE_OF_CONDUCT.md +0 -74
  104. data/Gemfile +0 -11
  105. data/Gemfile.lock +0 -33
  106. data/Rakefile +0 -10
  107. data/bin/console +0 -8
  108. data/bin/setup +0 -6
  109. 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