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,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Interpreter
|
|
5
|
+
# The event-name matcher of the specification's section 3.13 (Appendix
|
|
6
|
+
# D's `nameMatch`), as the reference matches.
|
|
7
|
+
#
|
|
8
|
+
# A descriptor matches an event name when its tokens are a prefix of the
|
|
9
|
+
# event's tokens on token boundaries: `error` matches `error.execution`
|
|
10
|
+
# and not `errors.execution`. A trailing `*` token, or the empty token a
|
|
11
|
+
# trailing dot leaves, is dropped before the prefix test, so `*` matches
|
|
12
|
+
# every event, `foo.*` matches `foo` and `foo.bar`, and `foo.` matches as
|
|
13
|
+
# `foo` does. A `*` anywhere else is matched as written.
|
|
14
|
+
#
|
|
15
|
+
# A transition's descriptors arrive split on their dots by the compiler
|
|
16
|
+
# (`Machine::Transition#events`), so both sides here are token lists; an
|
|
17
|
+
# event name is split once per selection round by {tokenize}.
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
module NameMatch
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# Whether any of `descriptors` (a list of token lists, one per
|
|
24
|
+
# descriptor of a transition's `event` attribute) matches the event
|
|
25
|
+
# whose name is `event_tokens`.
|
|
26
|
+
def name_match?(descriptors, event_tokens) = descriptors.any? { descriptor_match?(_1, event_tokens) }
|
|
27
|
+
|
|
28
|
+
# Whether one descriptor's tokens, normalized, are a prefix of
|
|
29
|
+
# `event_tokens`.
|
|
30
|
+
def descriptor_match?(descriptor, event_tokens)
|
|
31
|
+
normalized = normalize(descriptor)
|
|
32
|
+
event_tokens.take(normalized.size) == normalized
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The descriptor with a trailing `*` or empty token dropped.
|
|
36
|
+
def normalize(descriptor) = ["*", ""].include?(descriptor.last) ? descriptor[0...-1] : descriptor
|
|
37
|
+
|
|
38
|
+
# An event name split on its dots, every empty part kept: the empty
|
|
39
|
+
# name is one empty token, as the reference splits it.
|
|
40
|
+
def tokenize(name) = name.empty? ? [""] : name.split(".", -1)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Interpreter
|
|
5
|
+
# Transition selection, ported function for function from the
|
|
6
|
+
# pseudocode as the reference ports it; each function names the one it
|
|
7
|
+
# ports.
|
|
8
|
+
#
|
|
9
|
+
# The domain half ({find_lcca}, {get_effective_target_states},
|
|
10
|
+
# {get_transition_domain} and {compute_exit_set}) answers which states a
|
|
11
|
+
# transition leaves; {ExitEntry} uses it too. The selection half
|
|
12
|
+
# ({condition_match}, {select_transitions},
|
|
13
|
+
# {select_eventless_transitions} and {remove_conflicting_transitions})
|
|
14
|
+
# answers which transitions fire.
|
|
15
|
+
#
|
|
16
|
+
# The two selection walks evaluate conditions against one context built
|
|
17
|
+
# per round from the datamodel the caller passes and the snapshot's
|
|
18
|
+
# configuration. A condition that fails, or answers something other than
|
|
19
|
+
# true or false, leaves its transition unselected: the walks answer,
|
|
20
|
+
# beside the transitions, every written condition they evaluated and
|
|
21
|
+
# how it came out ({Selected}), in walk order, and the caller raises an
|
|
22
|
+
# `error.execution` for each that failed.
|
|
23
|
+
#
|
|
24
|
+
# @api private
|
|
25
|
+
module Selection
|
|
26
|
+
# What a selection round answered: `transitions`, the transitions that
|
|
27
|
+
# fire, in the order the round took them; and `evaluations`, an
|
|
28
|
+
# {Evaluation} per written condition the round evaluated, in walk
|
|
29
|
+
# order.
|
|
30
|
+
Selected = ::Data.define(:transitions, :evaluations)
|
|
31
|
+
|
|
32
|
+
# One written condition evaluated: the transition's `t_index`; its
|
|
33
|
+
# `outcome`, `:enabled`, `:disabled` or `:error`; and `reason`, the
|
|
34
|
+
# failure an `:error` carries, else nil. A transition with no
|
|
35
|
+
# condition is never recorded.
|
|
36
|
+
Evaluation = ::Data.define(:t_index, :outcome, :reason)
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# `findLCCA`: the least common compound ancestor of `indexes`
|
|
41
|
+
# ({Machine#lcca}). A parallel is never one.
|
|
42
|
+
def find_lcca(machine, indexes) = machine.lcca(indexes)
|
|
43
|
+
|
|
44
|
+
# `getEffectiveTargetStates`: the transition's targets, each history
|
|
45
|
+
# target resolved to its recorded value when it has one, else to its
|
|
46
|
+
# default transition's own effective targets, as a frozen Array. The
|
|
47
|
+
# list is not deduplicated; no caller's answer changes on a repeat.
|
|
48
|
+
def get_effective_target_states(snapshot, transition)
|
|
49
|
+
transition.targets.flat_map { effective_target_states(snapshot, _1) }.freeze
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# One target's effective states.
|
|
53
|
+
def effective_target_states(snapshot, target)
|
|
54
|
+
machine = snapshot.machine
|
|
55
|
+
return [target] unless machine.history?(target)
|
|
56
|
+
return snapshot.history.fetch(target) if snapshot.history.key?(target)
|
|
57
|
+
|
|
58
|
+
default = machine.transitions.fetch(machine.state(target).history_default)
|
|
59
|
+
get_effective_target_states(snapshot, default)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# `getTransitionDomain`: the state whose active descendants the
|
|
63
|
+
# transition exits, or nil when its effective targets are empty. An
|
|
64
|
+
# internal transition whose source is compound and whose every
|
|
65
|
+
# effective target is a proper descendant of it has the source as its
|
|
66
|
+
# domain; any other has the least common compound ancestor of its
|
|
67
|
+
# source and its effective targets.
|
|
68
|
+
def get_transition_domain(snapshot, transition)
|
|
69
|
+
targets = get_effective_target_states(snapshot, transition)
|
|
70
|
+
return nil if targets.empty?
|
|
71
|
+
|
|
72
|
+
machine = snapshot.machine
|
|
73
|
+
source = transition.source
|
|
74
|
+
return source if internal_domain?(machine, transition, targets)
|
|
75
|
+
|
|
76
|
+
find_lcca(machine, [source, *targets])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Whether an internal transition's domain is its own source.
|
|
80
|
+
def internal_domain?(machine, transition, targets)
|
|
81
|
+
source = transition.source
|
|
82
|
+
transition.type == :internal && machine.compound?(source) &&
|
|
83
|
+
targets.all? { machine.descendant?(_1, source) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# `computeExitSet`: every active state that is a proper descendant of
|
|
87
|
+
# some transition's domain, across `transitions`, as a frozen Array in
|
|
88
|
+
# document order (exit order is its reverse). A transition with no
|
|
89
|
+
# written target contributes nothing, and neither does one whose
|
|
90
|
+
# written targets resolve to no domain.
|
|
91
|
+
def compute_exit_set(snapshot, transitions)
|
|
92
|
+
exit_set = transitions.reject { _1.targets.empty? }.flat_map do |transition|
|
|
93
|
+
domain = get_transition_domain(snapshot, transition)
|
|
94
|
+
domain.nil? ? [] : snapshot.configuration.select { snapshot.machine.descendant?(_1, domain) }
|
|
95
|
+
end
|
|
96
|
+
exit_set.uniq.sort.freeze
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# `conditionMatch`: the transition's condition evaluated against `data`
|
|
100
|
+
# and the snapshot's configuration (for `In()`); no condition is true.
|
|
101
|
+
#
|
|
102
|
+
# @return [Datamodel::Evaluated] true or false, or the reason it failed
|
|
103
|
+
def condition_match(snapshot, transition, data, clock: nil)
|
|
104
|
+
Datamodel.evaluate_cond(context(snapshot, data, clock), transition.cond)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# `selectTransitions`: the transitions the event named `name` enables,
|
|
108
|
+
# at most one per active atomic state, the states taken in document
|
|
109
|
+
# order. Each state is searched, then each proper ancestor outward, for
|
|
110
|
+
# the first transition in document order whose descriptors match the
|
|
111
|
+
# name and whose condition holds, so a descendant's transition preempts
|
|
112
|
+
# an ancestor's. The first occurrence of each transition is kept, and
|
|
113
|
+
# the list passes through {remove_conflicting_transitions}.
|
|
114
|
+
#
|
|
115
|
+
# @return [Selected]
|
|
116
|
+
def select_transitions(snapshot, name, data, clock: nil)
|
|
117
|
+
round(snapshot, context(snapshot, data, clock), NameMatch.tokenize(name))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# `selectEventlessTransitions`: {select_transitions}'s walk over the
|
|
121
|
+
# transitions that name no event. A transition naming one is never
|
|
122
|
+
# taken here, and an eventless one never by {select_transitions}.
|
|
123
|
+
#
|
|
124
|
+
# @return [Selected]
|
|
125
|
+
def select_eventless_transitions(snapshot, data, clock: nil)
|
|
126
|
+
round(snapshot, context(snapshot, data, clock), nil)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `removeConflictingTransitions`: the enabled transitions, filtered to
|
|
130
|
+
# a set no two of which exit a common state. Taken in the order given,
|
|
131
|
+
# each is checked against those kept so far: on a conflict, one whose
|
|
132
|
+
# source is a descendant of the kept one's source preempts it and keeps
|
|
133
|
+
# checking; otherwise the later one is dropped and the check stops. A
|
|
134
|
+
# surviving transition is appended after every kept one it preempted is
|
|
135
|
+
# removed.
|
|
136
|
+
def remove_conflicting_transitions(snapshot, enabled)
|
|
137
|
+
enabled.reduce([]) do |filtered, candidate|
|
|
138
|
+
preempted = resolve(snapshot, candidate, filtered)
|
|
139
|
+
preempted.nil? ? filtered : (filtered - preempted) + [candidate]
|
|
140
|
+
end.freeze
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The kept transitions `candidate` preempts, or nil when a kept one
|
|
144
|
+
# preempts `candidate`.
|
|
145
|
+
def resolve(snapshot, candidate, filtered)
|
|
146
|
+
filtered.each_with_object([]) do |kept, preempted|
|
|
147
|
+
next unless conflict?(snapshot, candidate, kept)
|
|
148
|
+
return nil unless snapshot.machine.descendant?(candidate.source, kept.source)
|
|
149
|
+
|
|
150
|
+
preempted << kept
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Whether two transitions' exit sets share a state.
|
|
155
|
+
def conflict?(snapshot, one, other)
|
|
156
|
+
compute_exit_set(snapshot, [one]).intersect?(compute_exit_set(snapshot, [other]))
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# The round both selection walks share: every active atomic state in
|
|
160
|
+
# document order, the written conditions recorded in walk order, the
|
|
161
|
+
# first occurrence of each transition kept and the conflicts removed.
|
|
162
|
+
# `tokens` is nil for the eventless walk.
|
|
163
|
+
def round(snapshot, context, tokens)
|
|
164
|
+
evaluations = []
|
|
165
|
+
enabled = snapshot.configuration.select { snapshot.machine.atomic?(_1) }.sort.filter_map do |atomic|
|
|
166
|
+
selected_for_atomic_state(snapshot.machine, context, atomic, tokens, evaluations)
|
|
167
|
+
end
|
|
168
|
+
transitions = remove_conflicting_transitions(snapshot, enabled.uniq(&:t_index))
|
|
169
|
+
Selected.new(transitions:, evaluations: evaluations.freeze)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# The labelled `break loop` of one atomic state's search: the state,
|
|
173
|
+
# then each proper ancestor outward, stopping at the first that has an
|
|
174
|
+
# enabled transition.
|
|
175
|
+
def selected_for_atomic_state(machine, context, atomic, tokens, evaluations)
|
|
176
|
+
[atomic, *machine.proper_ancestors(atomic)].each do |index|
|
|
177
|
+
found = first_matching_transition(machine, context, index, tokens, evaluations)
|
|
178
|
+
return found unless found.nil?
|
|
179
|
+
end
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# A state's own transitions in document order, stopping at the first
|
|
184
|
+
# enabled one, so a later sibling's condition is never evaluated once
|
|
185
|
+
# an earlier one is taken.
|
|
186
|
+
def first_matching_transition(machine, context, index, tokens, evaluations)
|
|
187
|
+
machine.state(index).transitions.each do |t_index|
|
|
188
|
+
transition = machine.transitions.fetch(t_index)
|
|
189
|
+
return transition if enabled?(context, transition, tokens, evaluations)
|
|
190
|
+
end
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# The eventless predicate when `tokens` is nil, the event-matched one
|
|
195
|
+
# otherwise; both end in the transition's condition, which, when
|
|
196
|
+
# written, is recorded in `evaluations`.
|
|
197
|
+
def enabled?(context, transition, tokens, evaluations)
|
|
198
|
+
eventless = transition.events.empty?
|
|
199
|
+
return false unless tokens.nil? ? eventless : !eventless && NameMatch.name_match?(transition.events, tokens)
|
|
200
|
+
return true if transition.cond.nil?
|
|
201
|
+
|
|
202
|
+
evaluated = Datamodel.evaluate_cond(context, transition.cond)
|
|
203
|
+
evaluations << evaluation(transition, evaluated)
|
|
204
|
+
evaluated.ok? && evaluated.value
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# How one written condition came out.
|
|
208
|
+
def evaluation(transition, evaluated)
|
|
209
|
+
t_index = transition.t_index
|
|
210
|
+
return Evaluation.new(t_index:, outcome: :error, reason: evaluated.error) unless evaluated.ok?
|
|
211
|
+
|
|
212
|
+
Evaluation.new(t_index:, outcome: evaluated.value ? :enabled : :disabled, reason: nil)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# The context one round evaluates against.
|
|
216
|
+
def context(snapshot, data, clock)
|
|
217
|
+
Datamodel.context(data, snapshot.machine, snapshot.configuration, clock:)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Interpreter
|
|
5
|
+
# `<send>` and `<cancel>`, as the reference's send and cancel nodes run
|
|
6
|
+
# them (`Statifier.Machine.Content.Send` and `.Cancel` in statifier-ex),
|
|
7
|
+
# with the event data its `Statifier.EventData` makes.
|
|
8
|
+
#
|
|
9
|
+
# The core delivers nothing and schedules nothing. A `<send>` answers
|
|
10
|
+
# one effect describing the message, {Effect::Send} when it goes now and
|
|
11
|
+
# {Effect::SendDelayed} with its delay when it waits, whatever its
|
|
12
|
+
# target, `#_internal` included; the driver delivers it. A `<cancel>`
|
|
13
|
+
# answers an {Effect::Cancel}.
|
|
14
|
+
#
|
|
15
|
+
# A `<send>` resolves its arguments in the reference's order: the event,
|
|
16
|
+
# the target, the type, the delay, the `namelist` and `<param>` values in
|
|
17
|
+
# document order, then `<content>`. The first that fails discards the
|
|
18
|
+
# send: no id is minted, nothing is written, and the failure is the
|
|
19
|
+
# reason the block's `error.execution` carries. Then the send id is
|
|
20
|
+
# minted (or the author's `id` read) and `idlocation` written; a write
|
|
21
|
+
# that fails discards the send the same way. Only then are the type and
|
|
22
|
+
# the target judged ({Targets.reject_reason}): a refused send keeps its
|
|
23
|
+
# minted id and its `idlocation` write, answers no effect, and fails with
|
|
24
|
+
# a {SendRejected} naming the id and the event it raises.
|
|
25
|
+
#
|
|
26
|
+
# @api private
|
|
27
|
+
module Send
|
|
28
|
+
# What a session carries for its sends from block to block, never a
|
|
29
|
+
# module variable: `send_counter`, the sequence a generated id is
|
|
30
|
+
# minted from (the next id is `send_` and the counter plus one);
|
|
31
|
+
# `timer_counter`, the ordinal sequence every delayed send, every
|
|
32
|
+
# cancel and every immediate send of a registered type advances;
|
|
33
|
+
# `send_types`, the types the host registered a processor for, or nil
|
|
34
|
+
# for none; and `routes`, the {Targets::Routes} the driver last
|
|
35
|
+
# declared, or nil when it declared none.
|
|
36
|
+
SendState = ::Data.define(:send_counter, :timer_counter, :send_types, :routes) do
|
|
37
|
+
# The state with the next generated id minted.
|
|
38
|
+
def mint = with(send_counter: send_counter + 1)
|
|
39
|
+
|
|
40
|
+
# The state with the timer counter advanced.
|
|
41
|
+
def tick = with(timer_counter: timer_counter + 1)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class SendState
|
|
45
|
+
# The send state before the first send.
|
|
46
|
+
INITIAL = new(send_counter: 0, timer_counter: 0, send_types: nil, routes: nil)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# A `<send>` refused after its id was minted: `send_id`; `kind`,
|
|
50
|
+
# `:execution` or `:communication`, the `error.*` event it raises; and
|
|
51
|
+
# `reason`, the refusal the event's data carries.
|
|
52
|
+
SendRejected = ::Data.define(:send_id, :kind, :reason)
|
|
53
|
+
|
|
54
|
+
# What executing a `<send>` or `<cancel>` answered: the `context` and
|
|
55
|
+
# `sends` the block keeps, the `effects` in order, and `error`, the
|
|
56
|
+
# reason it failed, nil when it did not. A failure answers no effect,
|
|
57
|
+
# and an argument failure keeps the context and send state it started
|
|
58
|
+
# with.
|
|
59
|
+
Sent = ::Data.define(:context, :sends, :effects, :error) do
|
|
60
|
+
# @return [Boolean] whether the element succeeded
|
|
61
|
+
def ok? = error.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The whitespace a text payload is normalized over, the ASCII set.
|
|
65
|
+
RUNS_OF_SPACE = /[ \t\n\v\f\r]+/
|
|
66
|
+
|
|
67
|
+
# A successful evaluation of nil, for an attribute not written.
|
|
68
|
+
NONE = Datamodel::Evaluated.new(value: nil, error: nil)
|
|
69
|
+
|
|
70
|
+
module_function
|
|
71
|
+
|
|
72
|
+
# Executes the {Machine::Send} `node` against `context` and `sends`;
|
|
73
|
+
# `stamp` ({Content::Stamp}) names the block and the counters.
|
|
74
|
+
#
|
|
75
|
+
# @return [Sent]
|
|
76
|
+
def execute_send(context, node, sends, stamp)
|
|
77
|
+
resolved = resolve_arguments(context, node)
|
|
78
|
+
return failed(context, sends, resolved) unless resolved.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
send_id, minted = mint(node, sends)
|
|
81
|
+
write = node.idlocation && Datamodel::Location.write(context, node.idlocation, send_id)
|
|
82
|
+
return failed(context, sends, write.error) if write && !write.ok?
|
|
83
|
+
|
|
84
|
+
judged(node, resolved.merge(send_id:, write:), minted, stamp, write ? write.context : context)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The send id and the send state after it: the author's `id`, which
|
|
88
|
+
# consumes no counter, else the next generated id.
|
|
89
|
+
def mint(node, sends) = node.id.nil? ? ["send_#{sends.mint.send_counter}", sends.mint] : [node.id, sends]
|
|
90
|
+
|
|
91
|
+
# A send whose id is minted and whose `idlocation` is written, judged
|
|
92
|
+
# ({Targets.reject_reason}): refused, keeping both, or accepted.
|
|
93
|
+
def judged(node, resolved, sends, stamp, context)
|
|
94
|
+
kind, reason = Targets.reject_reason(*resolved.values_at(:target, :type, :delay_ms), sends.send_types,
|
|
95
|
+
sends.routes)
|
|
96
|
+
return failed(context, sends, SendRejected.new(send_id: resolved[:send_id], kind:, reason:)) if kind
|
|
97
|
+
|
|
98
|
+
accepted(node, resolved, sends, stamp, context)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Executes the {Machine::Cancel} `node`: its send id resolved and the
|
|
102
|
+
# effect naming it. An id that names nothing pending is the host's
|
|
103
|
+
# no-op, not a failure.
|
|
104
|
+
#
|
|
105
|
+
# @return [Sent]
|
|
106
|
+
def execute_cancel(context, node, sends, stamp)
|
|
107
|
+
evaluated = Datamodel.evaluate(context, node.sendid)
|
|
108
|
+
return failed(context, sends, evaluated.error) unless evaluated.ok?
|
|
109
|
+
|
|
110
|
+
sends = sends.tick
|
|
111
|
+
effect = Effect::Cancel.new(send_id: evaluated.value, c_index: node.c_index, **stamp.fields,
|
|
112
|
+
ordinal: sends.timer_counter, caller_context: stamp.caller_context)
|
|
113
|
+
Sent.new(context:, sends:, effects: [effect].freeze, error: nil)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# A `<content>` body as data, as the reference's event data reads one:
|
|
117
|
+
# blank text is no data; text that predicator reads as a literal (a
|
|
118
|
+
# number, a string, a list, a map) is that value; anything else is the
|
|
119
|
+
# trimmed text with each run of whitespace one space.
|
|
120
|
+
def text_data(text)
|
|
121
|
+
trimmed = text.nil? ? "" : Lowering::Errors.trim(text)
|
|
122
|
+
return Predicator::UNDEFINED if trimmed.empty?
|
|
123
|
+
|
|
124
|
+
compiled = Predicator.compile(trimmed)
|
|
125
|
+
evaluated = compiled.ok? ? Predicator.evaluate(compiled.value, {}, on_unbound: :error) : compiled
|
|
126
|
+
evaluated.ok? ? evaluated.value : trimmed.gsub(RUNS_OF_SPACE, " ")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# `namelist` and `<param>` values (name and value pairs) as data: a
|
|
130
|
+
# Hash, the last of a repeated name winning, or no data when there are
|
|
131
|
+
# none.
|
|
132
|
+
def params_data(pairs) = pairs.empty? ? Predicator::UNDEFINED : pairs.to_h.freeze
|
|
133
|
+
|
|
134
|
+
# Every argument resolved, as a Hash, or the reason the first failed.
|
|
135
|
+
def resolve_arguments(context, node)
|
|
136
|
+
resolved = {}
|
|
137
|
+
{ event: node.event, target: node.target, type: node.type }.each do |key, expr|
|
|
138
|
+
evaluated = expr.nil? ? NONE : Datamodel.evaluate(context, expr)
|
|
139
|
+
return evaluated.error unless evaluated.ok?
|
|
140
|
+
|
|
141
|
+
resolved[key] = evaluated.value
|
|
142
|
+
end
|
|
143
|
+
resolve_rest(context, node, resolved)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# The delay and the payload, after the first three.
|
|
147
|
+
def resolve_rest(context, node, resolved)
|
|
148
|
+
delay = resolve_delay(context, node.delay)
|
|
149
|
+
return delay.error unless delay.ok?
|
|
150
|
+
|
|
151
|
+
data = resolve_data(context, node)
|
|
152
|
+
data.ok? ? resolved.merge(delay_ms: delay.value, data: data.value) : data.error
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The delay in whole milliseconds ({Datamodel::Delay}), nil for a send
|
|
156
|
+
# with none.
|
|
157
|
+
def resolve_delay(context, delay)
|
|
158
|
+
return NONE if delay.nil?
|
|
159
|
+
|
|
160
|
+
evaluated = Datamodel.evaluate(context, delay)
|
|
161
|
+
evaluated.ok? ? Datamodel::Delay.milliseconds(evaluated.value) : evaluated
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# The payload: the `namelist` and `<param>` values, stopping at the
|
|
165
|
+
# first that fails, then, when the node has a `<content>` child, that
|
|
166
|
+
# child's data in their place.
|
|
167
|
+
def resolve_data(context, node)
|
|
168
|
+
pairs = resolve_params(context, node.namelist + node.params)
|
|
169
|
+
return pairs unless pairs.ok?
|
|
170
|
+
|
|
171
|
+
content = node.content
|
|
172
|
+
return value(params_data(pairs.value)) if content.nil?
|
|
173
|
+
|
|
174
|
+
content.is_a?(Machine::Static) ? value(text_data(content.value)) : Datamodel.evaluate(context, content)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# A successful evaluation of `value`.
|
|
178
|
+
def value(value) = Datamodel::Evaluated.new(value:, error: nil)
|
|
179
|
+
|
|
180
|
+
# Each param's name and value, in order, or the first failure.
|
|
181
|
+
def resolve_params(context, params)
|
|
182
|
+
pairs = params.map do |param|
|
|
183
|
+
evaluated = Datamodel.evaluate(context, param.expr)
|
|
184
|
+
return evaluated unless evaluated.ok?
|
|
185
|
+
|
|
186
|
+
[param.name, evaluated.value]
|
|
187
|
+
end
|
|
188
|
+
value(pairs)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# A failure: no effect, and the context and send state the block keeps.
|
|
192
|
+
def failed(context, sends, error) = Sent.new(context:, sends:, effects: [].freeze, error:)
|
|
193
|
+
|
|
194
|
+
# A send that is not refused: the timer counter advanced for a delay
|
|
195
|
+
# or a registered type, its `idlocation` write's change, then its own
|
|
196
|
+
# effect. `resolved` carries the send id and the {Datamodel::Written}
|
|
197
|
+
# of its `idlocation`, nil when it has none.
|
|
198
|
+
def accepted(node, resolved, sends, stamp, context)
|
|
199
|
+
stamps = stamps?(resolved, sends)
|
|
200
|
+
sends = sends.tick if stamps
|
|
201
|
+
write = resolved[:write]
|
|
202
|
+
changes = write ? [Content.change(write, node.idlocation, resolved[:send_id], node.c_index, stamp)] : []
|
|
203
|
+
effect = send_effect(node, resolved, stamp, (sends.timer_counter if stamps))
|
|
204
|
+
Sent.new(context:, sends:, effects: (changes << effect).freeze, error: nil)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Whether the send stamps an ordinal: a delay, or a registered type.
|
|
208
|
+
def stamps?(resolved, sends)
|
|
209
|
+
!resolved[:delay_ms].nil? || Targets.classify(sends.send_types, resolved[:type]) == :registered
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# The effect itself, immediate or delayed.
|
|
213
|
+
def send_effect(node, resolved, stamp, ordinal)
|
|
214
|
+
fields = resolved.slice(:event, :target, :type, :data, :send_id)
|
|
215
|
+
.merge(c_index: node.c_index, **stamp.fields, ordinal:,
|
|
216
|
+
id_from_author?: !node.id.nil? || !node.idlocation.nil?)
|
|
217
|
+
return Effect::Send.new(**fields) if resolved[:delay_ms].nil?
|
|
218
|
+
|
|
219
|
+
Effect::SendDelayed.new(**fields, delay_ms: resolved[:delay_ms], caller_context: stamp.caller_context)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Interpreter
|
|
5
|
+
# The specification's `microstep`, `exitStates` and `enterStates` with
|
|
6
|
+
# their content run, as the reference runs them
|
|
7
|
+
# (`Statifier.Interpreter.microstep/2` and
|
|
8
|
+
# `Statifier.Interpreter.ExitEntry` in statifier-ex). Which states leave
|
|
9
|
+
# and enter, in which order, is {ExitEntry}'s; this module walks those
|
|
10
|
+
# moves state by state against a {MachineState}: each exiting state runs
|
|
11
|
+
# its `<onexit>` blocks while it is still active and then leaves, and
|
|
12
|
+
# each entering state joins the configuration, binds its own `<data>`
|
|
13
|
+
# on its first entry under late binding, runs its `<onentry>` blocks,
|
|
14
|
+
# its initial and default-history content, and raises its completion
|
|
15
|
+
# events, its parent's carrying the final state's `<donedata>`. The
|
|
16
|
+
# blocks run through {Blocks}.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
module Step
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
# `microstep`: the states the transitions leave exit, each
|
|
23
|
+
# transition's content runs in the order given, and the states they
|
|
24
|
+
# reach enter.
|
|
25
|
+
#
|
|
26
|
+
# @return [Stepped]
|
|
27
|
+
def microstep(state, transitions)
|
|
28
|
+
exited = exit_states(state, transitions)
|
|
29
|
+
executed = execute_transition_content(exited.state, transitions)
|
|
30
|
+
exited.then_step(executed).then_step(enter_states(executed.state, transitions))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# `exitStates`: the exit set leaves the states to invoke, the history
|
|
34
|
+
# is recorded from the configuration before anything leaves, and each
|
|
35
|
+
# state in exit order runs its `<onexit>` blocks and leaves. The
|
|
36
|
+
# {Effect::Trace::ExitSet} comes first, stamped before the exits.
|
|
37
|
+
#
|
|
38
|
+
# @return [Stepped]
|
|
39
|
+
def exit_states(state, transitions)
|
|
40
|
+
moves = ExitEntry.exit_states(state.snapshot, transitions)
|
|
41
|
+
state = state.with(states_to_invoke: (state.states_to_invoke - moves.states).freeze)
|
|
42
|
+
trace = Blocks.traced(state, Effect::Trace::ExitSet, indexes: moves.states, configuration: moves.configuration)
|
|
43
|
+
departures(Stepped.new(state: state.with(history: moves.history), effects: trace), moves.states)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Each of `indexes` leaving in turn, after `stepped`.
|
|
47
|
+
#
|
|
48
|
+
# @return [Stepped]
|
|
49
|
+
def departures(stepped, indexes)
|
|
50
|
+
indexes.reduce(stepped) { |acc, index| acc.then_step(depart(acc.state, index)) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# One state leaving: its `<onexit>` blocks, the cancel of its live
|
|
54
|
+
# invocations ({Invoke::Passes.cancel_invocations}), then out of the
|
|
55
|
+
# configuration.
|
|
56
|
+
#
|
|
57
|
+
# @return [Stepped]
|
|
58
|
+
def depart(state, index)
|
|
59
|
+
left = Blocks.run_blocks(state, :onexit, index)
|
|
60
|
+
left = left.then_step(Invoke::Passes.cancel_invocations(left.state, index))
|
|
61
|
+
left.with(state: left.state.with(configuration: (left.state.configuration - [index]).freeze))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# `executeTransitionContent`: each transition's content, in the order
|
|
65
|
+
# given.
|
|
66
|
+
#
|
|
67
|
+
# @return [Stepped]
|
|
68
|
+
def execute_transition_content(state, transitions)
|
|
69
|
+
transitions.reduce(Stepped.new(state:, effects: [])) do |acc, transition|
|
|
70
|
+
acc.then_step(Blocks.run_transition_content(acc.state, transition.t_index))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# `enterStates`: each state of the entry set in entry order, with the
|
|
75
|
+
# {Effect::Trace::EntrySet} first, stamped before any entry.
|
|
76
|
+
#
|
|
77
|
+
# @return [Stepped]
|
|
78
|
+
def enter_states(state, transitions)
|
|
79
|
+
moves = ExitEntry.enter_states(state.snapshot, transitions)
|
|
80
|
+
trace = Blocks.traced(state, Effect::Trace::EntrySet, indexes: moves.states.map(&:index).freeze,
|
|
81
|
+
configuration: moves.configuration)
|
|
82
|
+
moves.states.reduce(Stepped.new(state:, effects: trace)) do |acc, arrival|
|
|
83
|
+
acc.then_step(arrive(acc.state, arrival))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# One {ExitEntry::Arrival}: the state joins the configuration and the
|
|
88
|
+
# states to invoke, binds its own `<data>` on its first entry, runs
|
|
89
|
+
# its `<onentry>` blocks, its initial content and its default-history
|
|
90
|
+
# content, and then raises its completions; a top-level final clears
|
|
91
|
+
# `running` instead.
|
|
92
|
+
#
|
|
93
|
+
# @return [Stepped]
|
|
94
|
+
def arrive(state, arrival)
|
|
95
|
+
index = arrival.index
|
|
96
|
+
stepped = enter(state, index)
|
|
97
|
+
stepped = stepped.then_step(Blocks.run_blocks(stepped.state, :onentry, index))
|
|
98
|
+
stepped = [arrival.initial_content, arrival.history_content].reduce(stepped) do |acc, t_index|
|
|
99
|
+
acc.then_step(Blocks.run_transition_content(acc.state, t_index))
|
|
100
|
+
end
|
|
101
|
+
stepped.with(state: complete(stepped.state, arrival))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The state with `index` active, among the states to invoke and
|
|
105
|
+
# recorded as entered.
|
|
106
|
+
def join(state, index)
|
|
107
|
+
state.with(configuration: (state.configuration | [index]).sort.freeze,
|
|
108
|
+
states_to_invoke: (state.states_to_invoke | [index]).sort.freeze,
|
|
109
|
+
entered_states: (state.entered_states | [index]).sort.freeze)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The state at `index` joined ({join}), with its own `<data>` bound
|
|
113
|
+
# when this is its first entry ({Datamodel::DataBinding.enter_state}).
|
|
114
|
+
#
|
|
115
|
+
# @return [Stepped]
|
|
116
|
+
def enter(state, index)
|
|
117
|
+
joined = join(state, index)
|
|
118
|
+
return Stepped.new(state: joined, effects: []) if state.entered_states.include?(index)
|
|
119
|
+
|
|
120
|
+
Blocks.bound(joined, Datamodel::DataBinding.enter_state(joined.context, index))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The completions an arrival raises (origin `{kind: :state,
|
|
124
|
+
# state_index:}`, the final state's), or `running` cleared.
|
|
125
|
+
def complete(state, arrival)
|
|
126
|
+
return state.with(running: false) if arrival.stops
|
|
127
|
+
|
|
128
|
+
arrival.completions.reduce(state) do |acc, completion|
|
|
129
|
+
origin = { kind: :state, state_index: completion.final }
|
|
130
|
+
next acc.raise_platform(completion.name, origin) unless completion.donedata
|
|
131
|
+
|
|
132
|
+
acc, data = Blocks.donedata(acc, completion.final)
|
|
133
|
+
acc.raise_platform(completion.name, origin, data:)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|