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,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ # The effects executable content answers, ported field for field from the
5
+ # reference's effect vocabulary (`Statifier.Effect.*` in statifier-ex):
6
+ # plain data values a host reads, each a `Data` the block runner answers
7
+ # as a deeply frozen copy, the values it carries included, except the
8
+ # host's own `caller_context`, which it carries untouched. The reference tags
9
+ # every effect with an atom beside its struct; here the class is the tag.
10
+ #
11
+ # Every effect carries the counters as they stood when it was made
12
+ # (`macrostep`, `microstep`, `round`), and every effect content makes
13
+ # carries the content node's `c_index` and its `owner`, the block it ran
14
+ # in: `{kind: :onentry, state_index:, ordinal:}`,
15
+ # `{kind: :onexit, state_index:, ordinal:}`, `{kind: :transition,
16
+ # t_index:}` or `{kind: :finalize, state_index:, invoke_index:}`.
17
+ #
18
+ # A value is a predicator value: `nil` is null and `Predicator::UNDEFINED`
19
+ # is no value at all.
20
+ #
21
+ # These are the effects the driver's calls answer a host
22
+ # ({Driver::Moved}'s `effects`), and what a host's processor is handed.
23
+ # Beside content's effects are the ones the interpreter's loop answers:
24
+ # {DatamodelInit}, {BudgetExhausted}, {Done}, the invocation effects
25
+ # ({Invoke}, {CancelInvoke} and {Autoforward}) and the {Trace} effects.
26
+ # The driver adds one of its own, {Child}, which carries an effect an
27
+ # invoked child's run answered.
28
+ #
29
+ # Every effect a call's runs answered is among the call's effects, in the
30
+ # order made, whatever the driver did with it:
31
+ #
32
+ # - A {Send} or a {SendDelayed}, by its type. A type the host registered
33
+ # is handed to that type's processor (`deliver`), delayed or not. The
34
+ # SCXML type, or none, is routed by its target, and a {SendDelayed} of it
35
+ # waits as a pending timer until {Statifier.advance} reaches it. A send
36
+ # of any other type is refused inside the chart with `error.execution`
37
+ # and answers no effect.
38
+ # - A {Cancel} drops every pending timer under its send id and tells a
39
+ # processor holding a delayed send under it, once.
40
+ # - An {Invoke} of the SCXML type whose content is markup starts a child
41
+ # session in process (no content, or content that is not markup or does
42
+ # not compile, raises `error.communication`); any other type raises
43
+ # `error.execution` inside the chart. A {CancelInvoke} stops the child,
44
+ # and an {Autoforward} hands it the event.
45
+ # - A {Log} is the host's to record; the driver does nothing with it.
46
+ # - A {Done} says the chart stopped; its pending timers and queued external
47
+ # events are discarded.
48
+ # - An error is never an effect: `error.execution` and
49
+ # `error.communication` are events the chart takes from its internal
50
+ # queue, seen by a host only in what the chart does next, or, for a
51
+ # chart started with `trace: true`, as a {Trace::EventDequeued}. The
52
+ # one failure answered as an effect is a {BudgetExhausted}: a macrostep
53
+ # spent its round budget, and the driver then queues external events
54
+ # without taking them.
55
+ # - A {Child} carries an effect an invoked child answered, tagged with the
56
+ # child's session id; a child's own child's effect is tagged with its
57
+ # own.
58
+ module Effect
59
+ # A `<log>`: `label` as written, nil when it was not, and `value`, the
60
+ # evaluated `expr`, nil for a `<log>` without one.
61
+ Log = ::Data.define(:label, :value, :c_index, :owner, :macrostep, :microstep, :round)
62
+
63
+ # A `<send>` with no delay, for the host to deliver now. `event`,
64
+ # `target` and `type` are the resolved values, nil when the element
65
+ # wrote neither attribute of the pair; `data` is the payload;
66
+ # `send_id` is always set, generated when the author named none, and
67
+ # `id_from_author?` is true when the element wrote `id` or
68
+ # `idlocation`. `ordinal` is set only for a type the host registered: the
69
+ # timer counter as this send advanced it.
70
+ Send = ::Data.define(
71
+ :event, :target, :type, :data, :send_id, :c_index, :owner, :macrostep, :microstep, :round, :ordinal,
72
+ :id_from_author?
73
+ )
74
+
75
+ # A `<send>` with a delay, for the host to schedule `delay_ms`
76
+ # milliseconds from now: every field of {Send}, the delay, an `ordinal`
77
+ # that is always set, and `caller_context`, the opaque value the host
78
+ # attached to the step, the same object it attached (never copied or
79
+ # frozen), nil when it attached none.
80
+ SendDelayed = ::Data.define(
81
+ :event, :target, :type, :data, :send_id, :delay_ms, :c_index, :owner, :macrostep, :microstep, :round,
82
+ :ordinal, :id_from_author?, :caller_context
83
+ )
84
+
85
+ # A `<cancel>`: the host drops the pending delayed send `send_id` names,
86
+ # if any. `ordinal` and `caller_context` as {SendDelayed}'s.
87
+ Cancel = ::Data.define(:send_id, :c_index, :owner, :macrostep, :microstep, :round, :ordinal, :caller_context)
88
+
89
+ # One write to the datamodel: `location_path` the path written (a String
90
+ # key or an Integer index per segment, the root first),
91
+ # `location_source` the location as written, `new_value` and
92
+ # `prior_value` the values at the path after and before. A write by
93
+ # content carries its `c_index` and `owner` and a nil `d_index`; a
94
+ # `<data>` that binds carries its `d_index`, its id as the one-segment
95
+ # path and the source, and a nil `c_index` and `owner`. The two writes
96
+ # no content node makes carry a nil `c_index` and `d_index`: an
97
+ # `<invoke>`'s `idlocation` write, owned by `{kind: :invoke,
98
+ # state_index:, invoke_index:}`, and each write an empty `<finalize>`
99
+ # makes, owned by `{kind: :finalize, state_index:, invoke_index:}`.
100
+ DatamodelChange = ::Data.define(
101
+ :location_path, :location_source, :new_value, :prior_value, :d_index, :c_index, :owner, :macrostep,
102
+ :microstep, :round
103
+ )
104
+
105
+ # An `<invoke>` started, for the driver to start its service. `type`
106
+ # and `src` are the resolved values, nil when the element wrote neither
107
+ # attribute of the pair; `params` is the `namelist` and `<param>` values
108
+ # as data (`Predicator::UNDEFINED` for none); `content` the `<content>`
109
+ # child's data, nil when it has none; `autoforward` the attribute;
110
+ # `invoke_id` always set, generated when the author named none;
111
+ # `state_index` the invoking state and `invoke_index` the element's
112
+ # position among that state's `<invoke>`s; and `caller_context` as
113
+ # {SendDelayed}'s.
114
+ Invoke = ::Data.define(
115
+ :invoke_id, :type, :src, :params, :content, :autoforward, :state_index, :invoke_index, :macrostep,
116
+ :microstep, :round, :caller_context
117
+ )
118
+
119
+ # A live invocation is cancelled: its invoking state, `state_index`,
120
+ # exits (or the chart stops) before it answered. `caller_context` as
121
+ # {SendDelayed}'s.
122
+ CancelInvoke = ::Data.define(:invoke_id, :state_index, :macrostep, :microstep, :round, :caller_context)
123
+
124
+ # An external event forwarded, unchanged, to the live invocation
125
+ # `invoke_id` names, whose `<invoke>` sets `autoforward`; `state_index`
126
+ # is its invoking state and `event` the {Event} whole.
127
+ Autoforward = ::Data.define(:invoke_id, :state_index, :event, :macrostep, :microstep, :round)
128
+
129
+ # An effect an invoked child's run answered, as the driver reports it
130
+ # among a call's effects: `session_id`, the child's session id (its
131
+ # parent's, a dot and the invoke id), and `effect`, the effect itself. A
132
+ # child's own child's effect is reported the same way, under its own
133
+ # session id.
134
+ Child = ::Data.define(:session_id, :effect)
135
+
136
+ # The datamodel as the chart starts, before any `<data>` binds: the
137
+ # host's values, the system variables over them and every `<data>` id
138
+ # the host did not supply, declared undefined. Answered first when a
139
+ # chart starts.
140
+ DatamodelInit = ::Data.define(:datamodel, :macrostep, :microstep, :round)
141
+
142
+ # A macrostep spent its round budget without reaching a stable
143
+ # configuration: `configuration`, the state indexes active where the
144
+ # last round left them, in document order; `budget`, the budget spent (a
145
+ # count, or `:infinity`); and `pending_internal_events`, the {Event}s
146
+ # still queued, oldest first. The chart keeps running.
147
+ BudgetExhausted = ::Data.define(:configuration, :budget, :pending_internal_events, :macrostep, :microstep,
148
+ :round)
149
+
150
+ # The chart stopped. `donedata` is the top-level final's `<donedata>`
151
+ # (`Predicator::UNDEFINED` when it has none, or it failed), nil when no
152
+ # top-level final was active; `donedata_error`, the data of the first
153
+ # `error.execution` that folding it raised, else nil; and
154
+ # `configuration`, the state indexes active when it stopped, in document
155
+ # order. Answered last.
156
+ Done = ::Data.define(:donedata, :donedata_error, :configuration, :macrostep, :microstep, :round)
157
+
158
+ # The trace effects, ported field for field from the reference's
159
+ # (`Statifier.Effect.Trace.*`): diagnostics the loop answers only for a
160
+ # state started with `trace: true`, each stamped with the counters as
161
+ # they stood at the phase it reports. Configurations and index lists
162
+ # are frozen Arrays in the order the reference gives.
163
+ module Trace
164
+ # One selection round's written conditions, in walk order: each a
165
+ # `{t_index:, outcome:, reason:}` Hash, `outcome` `:enabled`,
166
+ # `:disabled` or `:error` (with its `reason`). Answered only when the
167
+ # round evaluated one.
168
+ CondsEvaluated = ::Data.define(:evaluations, :macrostep, :microstep, :round)
169
+
170
+ # A block of content ran: its `owner` (a content owner, or
171
+ # `{kind: :global_script, index:}` for a `<script>` child of
172
+ # `<scxml>`) and `c_indexes`, the nodes that ran, the failing one
173
+ # included.
174
+ ContentExecuted = ::Data.define(:owner, :c_indexes, :macrostep, :microstep, :round)
175
+
176
+ # The chart stopped: {Effect::Done}'s fields, just before it.
177
+ Done = ::Data.define(:donedata, :donedata_error, :configuration, :macrostep, :microstep, :round)
178
+
179
+ # The states a microstep enters, in entry order (`indexes`), and the
180
+ # `configuration` once they have entered.
181
+ EntrySet = ::Data.define(:indexes, :configuration, :macrostep, :microstep, :round)
182
+
183
+ # An {Event} was taken, `from` `:external` or `:internal`.
184
+ EventDequeued = ::Data.define(:event, :from, :macrostep, :microstep, :round)
185
+
186
+ # The states a microstep exits (or the chart's stop leaves), in exit
187
+ # order (`indexes`), and the `configuration` once they have left.
188
+ ExitSet = ::Data.define(:indexes, :configuration, :macrostep, :microstep, :round)
189
+
190
+ # The pass over live invocations an external event makes before
191
+ # selection: the `event`, the invoke ids `finalized` and those
192
+ # `forwarded` to.
193
+ FinalizeAutoforward = ::Data.define(:event, :finalized, :forwarded, :macrostep, :microstep, :round)
194
+
195
+ # The invoke pass at the end of a stable macrostep: the states it
196
+ # walked (`state_indexes`, entry order) and the `invoke_ids` it
197
+ # started.
198
+ InvokePass = ::Data.define(:state_indexes, :invoke_ids, :macrostep, :microstep, :round)
199
+
200
+ # A macrostep reached a stable configuration with the chart running.
201
+ MacrostepStable = ::Data.define(:configuration, :macrostep, :microstep, :round)
202
+
203
+ # A selection: the `t_indexes` it took (empty included) and the
204
+ # `event` it selected on, nil for an eventless round.
205
+ TransitionsSelected = ::Data.define(:t_indexes, :event, :macrostep, :microstep, :round)
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ # An event, as the reference carries one (`Statifier.Event` in
5
+ # statifier-ex): the one value every queue holds, whoever raised it.
6
+ #
7
+ # `name`; `type`, `:external`, `:internal` or `:platform` (provenance,
8
+ # not routing: an internal and a platform event ride the same internal
9
+ # queue); `data`, `Predicator::UNDEFINED` for an event with none (nil is
10
+ # predicator's null); `cause`, the {Cause} of an event the chart raised,
11
+ # nil for one the host sent; `invokeid`, `origin`, `origintype` and
12
+ # `sendid`, the specification's `_event` fields, nil when the event
13
+ # carries none; and `caller_context`, the opaque value a host attached to
14
+ # an external event, carried as the same object and never read.
15
+ #
16
+ # A host meets one in the effects the driver answers and as the event a
17
+ # processor is handed beside a send.
18
+ Event = ::Data.define(:name, :type, :data, :cause, :invokeid, :origin, :origintype, :sendid, :caller_context)
19
+
20
+ # The event's {Cause} and its constructors.
21
+ class Event
22
+ # Why the chart raised an event: `origin`, what raised it, and the
23
+ # counters as they stood when it was raised. An origin is one of
24
+ # `{kind: :content, c_index:, owner:}` (a content node, `owner` its
25
+ # block), `{kind: :state, state_index:}` (a completion, or a failed
26
+ # `<donedata>` `<content>`), `{kind: :transition, t_index:}` (a failed
27
+ # condition), `{kind: :data, d_index:}` (a failed `<data>`),
28
+ # `{kind: :donedata_param, state_index:, param_index:}`,
29
+ # `{kind: :global_script, index:}`, `{kind: :invoke, state_index:,
30
+ # invoke_index:}` (an `<invoke>` that failed to start) and
31
+ # `{kind: :finalize, state_index:, invoke_index:}` (a write of an empty
32
+ # `<finalize>` that failed).
33
+ Cause = ::Data.define(:origin, :macrostep, :microstep, :round)
34
+
35
+ # The fields an event the chart raised leaves blank.
36
+ BLANK = { invokeid: nil, origin: nil, origintype: nil, caller_context: nil }.freeze
37
+
38
+ # The optional fields of an event the host sends, as they stand when it
39
+ # names none of them.
40
+ EXTERNAL = { data: Predicator::UNDEFINED, sendid: nil, **BLANK }.freeze
41
+
42
+ # An event the host sent: no cause, and each of {EXTERNAL}'s fields as
43
+ # `fields` gives it.
44
+ #
45
+ # @return [Event]
46
+ # @raise [ArgumentError] for a field an event does not have
47
+ def self.external(name, **fields) = new(name:, type: :external, cause: nil, **EXTERNAL, **fields)
48
+
49
+ # An event raised by a `<raise>`, or by a send to the internal queue:
50
+ # `origin` and `origintype` are blank, as the specification requires of
51
+ # an internal event.
52
+ #
53
+ # @return [Event]
54
+ def self.internal(name, cause, data: Predicator::UNDEFINED, sendid: nil)
55
+ new(name:, type: :internal, data:, cause:, sendid:, **BLANK)
56
+ end
57
+
58
+ # An event the platform raised: an `error.*` or a `done.state.*`.
59
+ #
60
+ # @return [Event]
61
+ def self.platform(name, cause, data: Predicator::UNDEFINED, sendid: nil)
62
+ new(name:, type: :platform, data:, cause:, sendid:, **BLANK)
63
+ end
64
+
65
+ private_constant :BLANK, :EXTERNAL
66
+ end
67
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ # How the package freezes the values it builds and answers.
5
+ #
6
+ # @api private
7
+ module Frozen
8
+ module_function
9
+
10
+ # `value`, an effect or an event, as a deeply frozen copy: whatever it
11
+ # carries that is not already shareable is copied before it is frozen,
12
+ # so no object the caller passed in (a datamodel value, the stamp's
13
+ # owner) is frozen in place. The one field that is not copied or frozen
14
+ # is `caller_context`, the host's own opaque value, which the effect
15
+ # carries as the same object, whatever it is.
16
+ def copy(value)
17
+ return Ractor.make_shareable(value, copy: true) unless value.respond_to?(:caller_context)
18
+
19
+ Ractor.make_shareable(value.with(caller_context: nil), copy: true).with(caller_context: value.caller_context)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module Interpreter
5
+ # What a loop function answered: the {MachineState} it leaves and its
6
+ # effects, in the order they were made.
7
+ #
8
+ # @api private
9
+ Stepped = ::Data.define(:state, :effects) do
10
+ # This step followed by `later`: `later`'s state, and both effect
11
+ # lists in order.
12
+ def then_step(later) = Stepped.new(state: later.state, effects: effects + later.effects)
13
+ end
14
+
15
+ # What a step runs against a {MachineState}, as the reference runs it
16
+ # (`Statifier.Interpreter.Content.execute_block/3`, the binding of
17
+ # `Statifier.Interpreter.Datamodel` and `ExitEntry.donedata/2` in
18
+ # statifier-ex): a block of content, a `<data>` binding pass and a
19
+ # final state's `<donedata>`, each read against the state as it stands
20
+ # and each answering the state it leaves.
21
+ #
22
+ # A block runs through {Content.execute_block}: the datamodel it leaves,
23
+ # its send state and the events it raised are kept, and with tracing on
24
+ # a {Effect::Trace::ContentExecuted} follows its effects.
25
+ #
26
+ # @api private
27
+ module Blocks
28
+ module_function
29
+
30
+ # One block run, owned by `owner`.
31
+ #
32
+ # @return [Stepped]
33
+ def run_block(state, owner, content)
34
+ stamp = Content::Stamp.new(owner:, **state.counters, caller_context: state.caller_context)
35
+ outcome = Content.execute_block(state.context, content, stamp, state.sends)
36
+ after = kept(state, outcome)
37
+ trace = traced(after, Effect::Trace::ContentExecuted, owner:, c_indexes: outcome.executed)
38
+ Stepped.new(state: after, effects: outcome.effects + trace)
39
+ end
40
+
41
+ # The state with what a block left kept: its datamodel, deeply frozen,
42
+ # its send state and the events it raised, queued.
43
+ def kept(state, outcome)
44
+ state.with(datamodel: Ractor.make_shareable(outcome.context.data), sends: outcome.sends)
45
+ .enqueue(*outcome.raised)
46
+ end
47
+
48
+ # Each of a state's `<onentry>` or `<onexit>` blocks (`kind`), in
49
+ # document order, owned by its ordinal.
50
+ #
51
+ # @return [Stepped]
52
+ def run_blocks(state, kind, index)
53
+ state.machine.state(index).public_send(kind).each_with_index
54
+ .reduce(Stepped.new(state:, effects: [])) do |acc, (block, ordinal)|
55
+ acc.then_step(run_block(acc.state, { kind:, state_index: index, ordinal: }, block.content))
56
+ end
57
+ end
58
+
59
+ # The content of the transition at `t_index`; nothing when it is nil.
60
+ #
61
+ # @return [Stepped]
62
+ def run_transition_content(state, t_index)
63
+ return Stepped.new(state:, effects: []) if t_index.nil?
64
+
65
+ run_block(state, { kind: :transition, t_index: }, state.machine.transitions.fetch(t_index).content)
66
+ end
67
+
68
+ # What a `<data>` binding pass answered ({Datamodel::DataBinding}),
69
+ # kept: each binding a {Effect::DatamodelChange} (its id the one-segment
70
+ # path), each failure an `error.execution` (origin `{kind: :data,
71
+ # d_index:}`).
72
+ #
73
+ # @return [Stepped]
74
+ def bound(state, bound)
75
+ after = bound.failures.reduce(state.with(datamodel: Ractor.make_shareable(bound.data))) do |acc, failure|
76
+ acc.raise_failure({ kind: :data, d_index: failure.d_index }, failure.reason)
77
+ end
78
+ Stepped.new(state: after, effects: bound.changes.map { binding_change(state, _1) })
79
+ end
80
+
81
+ # The effect one binding answers.
82
+ def binding_change(state, change)
83
+ Frozen.copy(Effect::DatamodelChange.new(
84
+ location_path: [change.id], location_source: change.id, new_value: change.new_value,
85
+ prior_value: change.prior_value, d_index: change.d_index, c_index: nil, owner: nil,
86
+ **state.counters
87
+ ))
88
+ end
89
+
90
+ # `returnDoneEvent`'s data: the final state's `<donedata>` folded to
91
+ # the value its `done.*` event carries, as `[state, data]`. None, or
92
+ # an empty one, is no data; a `<content>` body is read as a `<send>`'s
93
+ # is ({Send.text_data}); a `<content>` `expr` is evaluated, and a
94
+ # failure raises `error.execution` (origin `{kind: :state,
95
+ # state_index:}`) and is no data; `<param>`s are evaluated in order
96
+ # against one context, each failure raising its own `error.execution`
97
+ # (origin `{kind: :donedata_param, state_index:, param_index:}`) and
98
+ # dropping that param.
99
+ def donedata(state, index)
100
+ donedata = state.machine.state(index).donedata
101
+ return [state, Predicator::UNDEFINED] if donedata.nil? || (donedata.expr.nil? && donedata.params.empty?)
102
+ return donedata_params(state, index, donedata.params) if donedata.expr.nil?
103
+
104
+ donedata_content(state, index, donedata.expr)
105
+ end
106
+
107
+ # A `<donedata>` `<content>`, as {donedata}.
108
+ def donedata_content(state, index, expr)
109
+ return [state, Send.text_data(expr.value)] if expr.is_a?(Machine::Static)
110
+
111
+ evaluated = Datamodel.evaluate(state.context, expr)
112
+ return [state, evaluated.value] if evaluated.ok?
113
+
114
+ [state.raise_failure({ kind: :state, state_index: index }, evaluated.error), Predicator::UNDEFINED]
115
+ end
116
+
117
+ # The `<donedata>` `<param>`s, as {donedata}.
118
+ def donedata_params(state, index, params)
119
+ context = state.context
120
+ pairs = []
121
+ params.each_with_index do |param, param_index|
122
+ evaluated = Datamodel.evaluate(context, param.expr)
123
+ next pairs << [param.name, evaluated.value] if evaluated.ok?
124
+
125
+ state = state.raise_failure({ kind: :donedata_param, state_index: index, param_index: }, evaluated.error)
126
+ end
127
+ [state, Send.params_data(pairs)]
128
+ end
129
+
130
+ # The trace effect `klass` with `fields`, stamped with `state`'s
131
+ # counters, as a one-element list; empty, and nothing built, when the
132
+ # state does not trace. Every field is deeply frozen except an {Event},
133
+ # already a frozen copy, whose host `caller_context` stays the host's
134
+ # object.
135
+ def traced(state, klass, **fields)
136
+ return [] unless state.trace
137
+
138
+ [klass.new(**fields.transform_values { _1.is_a?(Event) ? _1 : Ractor.make_shareable(_1) }, **state.counters)]
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module Interpreter
5
+ # The block runner and the content nodes it runs, as the reference runs
6
+ # them (`Statifier.Interpreter.Content` and `Statifier.Machine.Content.*`
7
+ # in statifier-ex): the specification's `executeContent`.
8
+ #
9
+ # A block runs its nodes in document order against one
10
+ # {Datamodel::Context}: every write a node makes (an `<assign>`, a
11
+ # `<foreach>`'s item and index, a `<script>`'s program, a `<send>`'s
12
+ # `idlocation`) is seen by every node after it. The first node that
13
+ # fails stops the block. The nodes that ran before it keep what they did;
14
+ # the failing node answers no effect, and its failure becomes one
15
+ # `error.*` platform event whose origin is that node, the top-level node
16
+ # of this block when the failure was inside an `<if>` or a `<foreach>`
17
+ # ({NestedContent} then names the inner node). Other blocks are not
18
+ # affected: each call is independent.
19
+ #
20
+ # What a failing node keeps is the reference's: a leaf that fails (a
21
+ # `<log>`, an `<assign>`, a `<cancel>`, a `<send>` whose argument
22
+ # failed, a `<script>` that never compiled) keeps nothing; an `<if>` or
23
+ # a `<foreach>` keeps what its nodes wrote and raised, though not their
24
+ # effects; a `<script>` keeps the writes its program made before it
25
+ # stopped; a refused `<send>` keeps its minted id and its `idlocation`
26
+ # write.
27
+ #
28
+ # A condition of an `<if>` that fails, or answers something other than
29
+ # true or false, does not stop the block: its branch counts as not
30
+ # taken, and its `error.execution` is raised once the top-level node that
31
+ # holds it finishes, after any event the taken branch raised, the order
32
+ # the reference takes.
33
+ #
34
+ # Nothing here moves a queue. The runner answers the events the block
35
+ # raised, in the order they join the internal queue, beside its effects,
36
+ # and the step that called it queues them and stamps their counters.
37
+ #
38
+ # @api private
39
+ module Content
40
+ # Where a block runs: `owner`, the block (`{kind: :onentry,
41
+ # state_index:, ordinal:}`, `{kind: :onexit, state_index:, ordinal:}`,
42
+ # `{kind: :transition, t_index:}` or `{kind: :finalize, state_index:,
43
+ # invoke_index:}`); the counters as they stand while it runs, which no
44
+ # node moves; and `caller_context`, the opaque value the host attached
45
+ # to the step (any object, never copied or frozen here), nil for none.
46
+ Stamp = ::Data.define(:owner, :macrostep, :microstep, :round, :caller_context) do
47
+ # The fields every effect content makes carries: the owner and the
48
+ # counters.
49
+ def fields = { owner:, macrostep:, microstep:, round: }
50
+
51
+ # The origin of an event the content node at `c_index` raises.
52
+ def origin(c_index) = { kind: :content, c_index:, owner: }
53
+
54
+ # The {Event::Cause} of an event the content node at `c_index`
55
+ # raises: its origin and the counters.
56
+ def cause(c_index) = Event::Cause.new(origin: origin(c_index), macrostep:, microstep:, round:)
57
+ end
58
+
59
+ # What a block answered: the `context` it leaves; `sends`, the
60
+ # {Send::SendState} it leaves; `effects` in the order the nodes made
61
+ # them; `raised`, the {Event}s it raised in queue order (an internal
62
+ # event per `<raise>`, a platform `error.execution` per failure and an
63
+ # `error.execution` or `error.communication` per refused `<send>`,
64
+ # each with its {Event::Cause}); and `executed`, the `c_index` of every
65
+ # top-level node that ran, the failing one included.
66
+ Outcome = ::Data.define(:context, :sends, :effects, :raised, :executed)
67
+
68
+ # A node inside an `<if>` or a `<foreach>` failed: `c_index` names it
69
+ # and `reason` is its failure.
70
+ NestedContent = ::Data.define(:c_index, :reason)
71
+
72
+ # What a block threads from node to node.
73
+ Run = ::Data.define(:context, :sends, :effects, :raised, :pending) do
74
+ # The run with `effect` appended, as a deeply frozen copy
75
+ # ({Frozen.copy}).
76
+ def effect(effect) = with(effects: (effects + [Frozen.copy(effect)]).freeze)
77
+
78
+ # The run with `events` raised, each a deeply frozen copy.
79
+ def raise_events(*events) = with(raised: (raised + events.map { Frozen.copy(_1) }).freeze)
80
+
81
+ # The run with a condition's failure recorded.
82
+ def defer(reason) = with(pending: (pending + [reason]).freeze)
83
+ end
84
+
85
+ module_function
86
+
87
+ # The function each kind of node runs through.
88
+ RUNNERS = {
89
+ Machine::Raise => :raise_event, Machine::Log => :log, Machine::Assign => :assign, Machine::If => :if_node,
90
+ Machine::Foreach => :foreach, Machine::Script => :script, Machine::Send => :send_node,
91
+ Machine::Cancel => :cancel
92
+ }.freeze
93
+
94
+ # Runs `content`, a block's nodes, in document order against
95
+ # `context`, stopping at the first that fails. `sends` is the send
96
+ # state the block starts with and `stamp` the {Stamp} its effects and
97
+ # events carry.
98
+ #
99
+ # @return [Outcome]
100
+ def execute_block(context, content, stamp, sends = Send::SendState::INITIAL)
101
+ run = Run.new(context:, sends:, effects: [].freeze, raised: [].freeze, pending: [].freeze)
102
+ executed = content.each_with_object([]) do |node, ran|
103
+ ran << node.c_index
104
+ run, failed = top_level(run, node, stamp)
105
+ break ran if failed
106
+ end
107
+ Outcome.new(**run.to_h.except(:pending), executed: executed.freeze)
108
+ end
109
+
110
+ # One top-level node run, its recorded condition failures raised
111
+ # against it, then its own failure: `[run, true]` when it failed.
112
+ def top_level(run, node, stamp)
113
+ cause = stamp.cause(node.c_index)
114
+ run, reason = execute_node(run, node, stamp)
115
+ run = drain(run, cause)
116
+ reason.nil? ? [run, false] : [run.raise_events(error_event(reason, cause)), true]
117
+ end
118
+
119
+ # One node run: `[run, nil]` on success, else `[run, reason]` with the
120
+ # run the block keeps, which never holds the failing node's effects.
121
+ def execute_node(run, node, stamp)
122
+ after, reason = __send__(RUNNERS.fetch(node.class), run, node, stamp)
123
+ reason.nil? ? [after, nil] : [after.with(effects: run.effects), reason]
124
+ end
125
+
126
+ # The pending condition failures raised with `cause`, the top-level
127
+ # node's, in the order they were recorded.
128
+ def drain(run, cause)
129
+ return run if run.pending.empty?
130
+
131
+ run.raise_events(*run.pending.map { error_event(_1, cause) }).with(pending: [].freeze)
132
+ end
133
+
134
+ # The event a failure raises: a refused `<send>` raises the event its
135
+ # kind names, carrying its id; anything else `error.execution`. The
136
+ # failure, as a predicator value ({Datamodel::ReasonValue.of}), is the
137
+ # event's data.
138
+ def error_event(reason, cause)
139
+ unless reason.is_a?(Send::SendRejected)
140
+ return Event.platform("error.execution", cause, data: Datamodel::ReasonValue.of(reason))
141
+ end
142
+
143
+ Event.platform("error.#{reason.kind}", cause, data: Datamodel::ReasonValue.of(reason.reason),
144
+ sendid: reason.send_id)
145
+ end
146
+
147
+ # `<raise>`: one internal event named as written.
148
+ def raise_event(run, node, stamp)
149
+ [run.raise_events(Event.internal(node.event, stamp.cause(node.c_index))), nil]
150
+ end
151
+
152
+ # `<log>`: the label and the evaluated `expr`, nil without one, as an
153
+ # effect.
154
+ def log(run, node, stamp)
155
+ evaluated = node.expr.nil? ? Send::NONE : Datamodel.evaluate(run.context, node.expr)
156
+ return [run, evaluated.error] unless evaluated.ok?
157
+
158
+ [run.effect(Effect::Log.new(label: node.label, value: evaluated.value, c_index: node.c_index, **stamp.fields)),
159
+ nil]
160
+ end
161
+
162
+ # `<assign>`: the value evaluated, then written at the location
163
+ # ({Datamodel::Location.write}), which never declares a root. A write
164
+ # that lands answers its change.
165
+ def assign(run, node, stamp)
166
+ context = run.context
167
+ value = Datamodel.evaluate(context, node.value)
168
+ written = value.ok? ? Datamodel::Location.write(context, node.location, value.value) : value
169
+ return [run, written.error] unless written.ok?
170
+
171
+ [run.with(context: written.context).effect(change(written, node.location, value.value, node.c_index, stamp)),
172
+ nil]
173
+ end
174
+
175
+ # The change a content node's write made: `written` the
176
+ # {Datamodel::Written}, `source` the location as written.
177
+ def change(written, source, new_value, c_index, stamp)
178
+ Effect::DatamodelChange.new(location_path: written.path, location_source: source, new_value:,
179
+ prior_value: written.prior_value, d_index: nil, c_index:, **stamp.fields)
180
+ end
181
+
182
+ # `<if>`: the first branch whose condition is true runs, an `<else>`
183
+ # always; a condition that fails or is not a boolean is recorded and
184
+ # its branch passed over.
185
+ def if_node(run, node, stamp)
186
+ node.branches.each do |branch|
187
+ evaluated = Datamodel.evaluate_cond(run.context, branch.cond)
188
+ return nested(run, branch.content, stamp) if evaluated.ok? && evaluated.value
189
+
190
+ run = run.defer(evaluated.error) unless evaluated.ok?
191
+ end
192
+ [run, nil]
193
+ end
194
+
195
+ # A partition or a loop body: each node in order, the first failure
196
+ # wrapped as a {NestedContent} naming the inner node.
197
+ def nested(run, content, stamp)
198
+ content.each do |node|
199
+ run, reason = execute_node(run, node, stamp)
200
+ return [run, NestedContent.new(c_index: node.c_index, reason:)] unless reason.nil?
201
+ end
202
+ [run, nil]
203
+ end
204
+
205
+ # `<foreach>`, through {Foreach.execute}.
206
+ def foreach(run, node, stamp) = Foreach.execute(run, node, stamp)
207
+
208
+ # `<script>`: the program run through {Datamodel.execute}; its writes
209
+ # before a failure are kept.
210
+ def script(run, node, _stamp)
211
+ executed = Datamodel.execute(run.context, node.program)
212
+ [run.with(context: run.context.with(data: executed.data)), executed.error]
213
+ end
214
+
215
+ # `<send>`, through {Send.execute_send}.
216
+ def send_node(run, node, stamp) = sent(run, Send.execute_send(run.context, node, run.sends, stamp))
217
+
218
+ # `<cancel>`, through {Send.execute_cancel}.
219
+ def cancel(run, node, stamp) = sent(run, Send.execute_cancel(run.context, node, run.sends, stamp))
220
+
221
+ # What a `<send>` or `<cancel>` answered, kept.
222
+ def sent(run, sent)
223
+ kept = run.with(context: sent.context, sends: sent.sends)
224
+ [sent.effects.reduce(kept) { |acc, effect| acc.effect(effect) }, sent.error]
225
+ end
226
+ end
227
+ end
228
+ end