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,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Interpreter
|
|
5
|
+
# The pseudocode's three `computeEntrySet` out-parameters, one value:
|
|
6
|
+
# `states_to_enter`; `states_for_default_entry`, the compound states
|
|
7
|
+
# among them entered by default rather than as a target; and
|
|
8
|
+
# `default_history_content`, by a history's parent, the `t_index` of the
|
|
9
|
+
# history's default transition, whose content runs because the history
|
|
10
|
+
# had recorded nothing. The two sets are frozen ascending Arrays and the
|
|
11
|
+
# map a frozen Hash.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
EntrySet = ::Data.define(:states_to_enter, :states_for_default_entry, :default_history_content) do
|
|
15
|
+
# The entry set with `index` added to `states_to_enter`.
|
|
16
|
+
def add(index) = with(states_to_enter: put(states_to_enter, index))
|
|
17
|
+
|
|
18
|
+
# The entry set with `index` added to `states_for_default_entry`.
|
|
19
|
+
def flag_default_entry(index) = with(states_for_default_entry: put(states_for_default_entry, index))
|
|
20
|
+
|
|
21
|
+
# The entry set with `t_index` registered as `parent`'s default
|
|
22
|
+
# history content.
|
|
23
|
+
def register_history_content(parent, t_index)
|
|
24
|
+
with(default_history_content: default_history_content.merge(parent => t_index).freeze)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def put(set, index) = set.include?(index) ? set : (set + [index]).sort.freeze
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class EntrySet
|
|
33
|
+
# An entry set with nothing in it.
|
|
34
|
+
EMPTY = new(states_to_enter: [].freeze, states_for_default_entry: [].freeze,
|
|
35
|
+
default_history_content: {}.freeze)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The entry set, ported function for function from the pseudocode as
|
|
39
|
+
# the reference ports it: what a set of transitions enters. Each
|
|
40
|
+
# function threads an {EntrySet} through and answers the one it leaves,
|
|
41
|
+
# where the pseudocode writes into its out-parameters.
|
|
42
|
+
#
|
|
43
|
+
# @api private
|
|
44
|
+
module Entering
|
|
45
|
+
module_function
|
|
46
|
+
|
|
47
|
+
# `computeEntrySet`: what `transitions` enter. For each, every written
|
|
48
|
+
# target enters with its descendants, then every effective target (a
|
|
49
|
+
# history resolved to what it stands for) enters its ancestors up to
|
|
50
|
+
# the transition's domain.
|
|
51
|
+
#
|
|
52
|
+
# @return [EntrySet]
|
|
53
|
+
def compute_entry_set(snapshot, transitions)
|
|
54
|
+
transitions.reduce(EntrySet::EMPTY) do |entry_set, transition|
|
|
55
|
+
entry_set = transition.targets.reduce(entry_set) do |set, target|
|
|
56
|
+
add_descendant_states_to_enter(snapshot, target, set)
|
|
57
|
+
end
|
|
58
|
+
domain = Selection.get_transition_domain(snapshot, transition)
|
|
59
|
+
Selection.get_effective_target_states(snapshot, transition).reduce(entry_set) do |set, target|
|
|
60
|
+
add_ancestor_states_to_enter(snapshot, target, domain, set)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# `addDescendantStatesToEnter`: `index` and what enters with it, added
|
|
66
|
+
# to `entry_set`. A history restores its recorded value or, having
|
|
67
|
+
# recorded nothing, follows its default transition and registers that
|
|
68
|
+
# transition's content on its parent. A compound state is marked as
|
|
69
|
+
# entered by default and enters its initial targets. A parallel enters
|
|
70
|
+
# every region no state already in the set lies inside. Any other state
|
|
71
|
+
# enters alone.
|
|
72
|
+
#
|
|
73
|
+
# @return [EntrySet]
|
|
74
|
+
def add_descendant_states_to_enter(snapshot, index, entry_set)
|
|
75
|
+
machine = snapshot.machine
|
|
76
|
+
return enter_history_target(snapshot, index, entry_set) if machine.history?(index)
|
|
77
|
+
|
|
78
|
+
entry_set = entry_set.add(index)
|
|
79
|
+
if machine.compound?(index)
|
|
80
|
+
enter_targets(snapshot, initial_targets(machine, index), index, entry_set.flag_default_entry(index))
|
|
81
|
+
elsif machine.parallel?(index)
|
|
82
|
+
enter_uncovered_regions(snapshot, index, entry_set)
|
|
83
|
+
else
|
|
84
|
+
entry_set
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# `addAncestorStatesToEnter`: every proper ancestor of `index` up to
|
|
89
|
+
# but not including `ancestor` enters, each parallel among them with
|
|
90
|
+
# its regions not yet covered. A nil `ancestor` bounds nothing.
|
|
91
|
+
#
|
|
92
|
+
# @return [EntrySet]
|
|
93
|
+
def add_ancestor_states_to_enter(snapshot, index, ancestor, entry_set)
|
|
94
|
+
machine = snapshot.machine
|
|
95
|
+
machine.proper_ancestors(index).take_while { _1 != ancestor }.reduce(entry_set) do |set, anc|
|
|
96
|
+
set = set.add(anc)
|
|
97
|
+
machine.parallel?(anc) ? enter_uncovered_regions(snapshot, anc, set) : set
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# The history arm: the recorded value when there is one, else the
|
|
102
|
+
# default transition's targets with its content registered on the
|
|
103
|
+
# history's parent; either way the targets enter up to that parent.
|
|
104
|
+
def enter_history_target(snapshot, history_index, entry_set)
|
|
105
|
+
history = snapshot.machine.state(history_index)
|
|
106
|
+
recorded = snapshot.history[history_index]
|
|
107
|
+
return enter_targets(snapshot, recorded, history.parent, entry_set) unless recorded.nil?
|
|
108
|
+
|
|
109
|
+
default = history.history_default
|
|
110
|
+
targets = snapshot.machine.transitions.fetch(default).targets
|
|
111
|
+
enter_targets(snapshot, targets, history.parent, entry_set.register_history_content(history.parent, default))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Every target's descendants, then every target's ancestors up to
|
|
115
|
+
# `ancestor`.
|
|
116
|
+
def enter_targets(snapshot, targets, ancestor, entry_set)
|
|
117
|
+
entry_set = targets.reduce(entry_set) { |set, target| add_descendant_states_to_enter(snapshot, target, set) }
|
|
118
|
+
targets.reduce(entry_set) { |set, target| add_ancestor_states_to_enter(snapshot, target, ancestor, set) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# A compound state's default-entry targets: its `<initial>`
|
|
122
|
+
# transition's when the document wrote one, else the default the
|
|
123
|
+
# compiler resolved from the `initial` attribute or the first child.
|
|
124
|
+
def initial_targets(machine, index)
|
|
125
|
+
t_index = machine.state(index).initial_transition
|
|
126
|
+
t_index.nil? ? machine.state(index).initial : machine.transitions.fetch(t_index).targets
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Every region of the parallel `index` that no state already in the
|
|
130
|
+
# set lies inside enters with its descendants. A history child is
|
|
131
|
+
# never a region.
|
|
132
|
+
def enter_uncovered_regions(snapshot, index, entry_set)
|
|
133
|
+
machine = snapshot.machine
|
|
134
|
+
machine.child_states(index).reduce(entry_set) do |set, region|
|
|
135
|
+
covered = set.states_to_enter.any? { machine.descendant?(_1, region) }
|
|
136
|
+
covered ? set : add_descendant_states_to_enter(snapshot, region, set)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Interpreter
|
|
5
|
+
# Exit and entry, ported function for function from the pseudocode as
|
|
6
|
+
# the reference ports it; each function names the one it ports. Which
|
|
7
|
+
# transitions fire and which states each one leaves is {Selection}'s,
|
|
8
|
+
# and the entry set is {Entering}'s; this module answers, for a set of
|
|
9
|
+
# transitions that fire, what leaving and entering states means: the
|
|
10
|
+
# states to exit in exit order with the history recorded before any of
|
|
11
|
+
# them leaves, and the states to enter in entry order with, for each,
|
|
12
|
+
# the default-entry and history content to run and the completion events
|
|
13
|
+
# its entry raises.
|
|
14
|
+
#
|
|
15
|
+
# It runs no executable content. The pseudocode's `exitStates` and
|
|
16
|
+
# `enterStates` interleave the configuration's moves with content; here
|
|
17
|
+
# {exit_states} and {enter_states} answer the moves as values ({Exit},
|
|
18
|
+
# {Entry}), and the step that calls them runs each state's `<onexit>`,
|
|
19
|
+
# `<onentry>` and transition content, evaluates a final state's
|
|
20
|
+
# `<donedata>` and queues the completion events, state by state, in the
|
|
21
|
+
# order given. No content moves the configuration, so the answers do not
|
|
22
|
+
# depend on what the content does.
|
|
23
|
+
#
|
|
24
|
+
# Exit order is descending index and entry order ascending, which is
|
|
25
|
+
# document order: a descendant leaves before its ancestors and enters
|
|
26
|
+
# after them, and a parallel's regions leave in reverse document order
|
|
27
|
+
# and enter in document order.
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
module ExitEntry
|
|
31
|
+
# What {exit_states} answered: `states`, the states to exit in exit
|
|
32
|
+
# order, each to run its `<onexit>` blocks and leave; `history`, the
|
|
33
|
+
# recorded history with every history child of an exiting state
|
|
34
|
+
# recorded from the configuration as it stood before any state left;
|
|
35
|
+
# and `configuration`, the configuration once they have all left.
|
|
36
|
+
Exit = ::Data.define(:states, :history, :configuration)
|
|
37
|
+
|
|
38
|
+
# A completion event an entry raises: `name`, `done.state.` and the
|
|
39
|
+
# completed state's id; `final`, the index of the final state whose
|
|
40
|
+
# entry raised it (the event's origin); and `donedata`, true when the
|
|
41
|
+
# event carries that final state's `<donedata>` (the parent's event)
|
|
42
|
+
# and false when it carries no data (a parallel's).
|
|
43
|
+
Completion = ::Data.define(:name, :final, :donedata)
|
|
44
|
+
|
|
45
|
+
# One state's entry, as {enter_states} answers it: `index`, the state;
|
|
46
|
+
# `initial_content`, the `t_index` of its `<initial>` transition when
|
|
47
|
+
# it was entered by default and its document wrote one, else nil;
|
|
48
|
+
# `history_content`, the `t_index` of the default transition a history
|
|
49
|
+
# child registered on it, else nil; `completions`, the {Completion}s
|
|
50
|
+
# its entry raises, in the order they are raised; and `stops`, true
|
|
51
|
+
# for a final state whose parent is the root, whose entry ends the
|
|
52
|
+
# execution and raises nothing.
|
|
53
|
+
Arrival = ::Data.define(:index, :initial_content, :history_content, :completions, :stops)
|
|
54
|
+
|
|
55
|
+
# What {enter_states} answered: `states`, an {Arrival} per state to
|
|
56
|
+
# enter in entry order, each to join the configuration and run, in
|
|
57
|
+
# order, its `<onentry>` blocks, its initial content, its history
|
|
58
|
+
# content and then its completions; and `configuration`, the
|
|
59
|
+
# configuration once they have all entered.
|
|
60
|
+
Entry = ::Data.define(:states, :configuration)
|
|
61
|
+
|
|
62
|
+
module_function
|
|
63
|
+
|
|
64
|
+
# `exitStates`, its moves: the exit set ({Selection.compute_exit_set})
|
|
65
|
+
# in exit order, and the history recorded over it.
|
|
66
|
+
#
|
|
67
|
+
# @return [Exit]
|
|
68
|
+
def exit_states(snapshot, transitions)
|
|
69
|
+
exit_set = Selection.compute_exit_set(snapshot, transitions)
|
|
70
|
+
states = exit_set.reverse.freeze
|
|
71
|
+
Exit.new(states:, history: record_history_values(snapshot, states),
|
|
72
|
+
configuration: (snapshot.configuration - exit_set).freeze)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# `exitStates`' history-recording loop: for every history child of
|
|
76
|
+
# every state in `states_to_exit`, in that order, its value, each
|
|
77
|
+
# overwriting what the history had recorded. A shallow history records
|
|
78
|
+
# the state's active children, a deep one its active atomic
|
|
79
|
+
# descendants, both read from the snapshot's configuration.
|
|
80
|
+
def record_history_values(snapshot, states_to_exit)
|
|
81
|
+
machine = snapshot.machine
|
|
82
|
+
recorded = states_to_exit.each_with_object(snapshot.history.dup) do |index, history|
|
|
83
|
+
machine.state(index).history_children.each { history[_1] = recorded_value(snapshot, index, _1) }
|
|
84
|
+
end
|
|
85
|
+
recorded.freeze
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The value one history child `history_index` of `index` records.
|
|
89
|
+
def recorded_value(snapshot, index, history_index)
|
|
90
|
+
machine = snapshot.machine
|
|
91
|
+
deep = machine.state(history_index).history_type == :deep
|
|
92
|
+
snapshot.configuration.select do |active|
|
|
93
|
+
deep ? machine.atomic?(active) && machine.descendant?(active, index) : machine.state(active).parent == index
|
|
94
|
+
end.freeze
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# `enterStates`, its moves: the entry set
|
|
98
|
+
# ({Entering.compute_entry_set}) in entry order, each state with the
|
|
99
|
+
# content its entry runs and the completion events it raises, read
|
|
100
|
+
# against the configuration as the states before it have joined it.
|
|
101
|
+
# `snapshot` is the one the exit left: its configuration and history
|
|
102
|
+
# after {exit_states}.
|
|
103
|
+
#
|
|
104
|
+
# @return [Entry]
|
|
105
|
+
def enter_states(snapshot, transitions)
|
|
106
|
+
entry_set = Entering.compute_entry_set(snapshot, transitions)
|
|
107
|
+
arrivals = []
|
|
108
|
+
configuration = entry_set.states_to_enter.reduce(snapshot.configuration) do |config, index|
|
|
109
|
+
entered = snapshot.with(configuration: (config | [index]).sort.freeze)
|
|
110
|
+
arrivals << arrival(entered, index, entry_set)
|
|
111
|
+
entered.configuration
|
|
112
|
+
end
|
|
113
|
+
Entry.new(states: arrivals.freeze, configuration:)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# One state's entry, read with `index` in the configuration.
|
|
117
|
+
def arrival(snapshot, index, entry_set)
|
|
118
|
+
machine = snapshot.machine
|
|
119
|
+
default_entry = entry_set.states_for_default_entry.include?(index)
|
|
120
|
+
stops = machine.final?(index) && machine.state(index).parent.zero?
|
|
121
|
+
Arrival.new(index:, initial_content: default_entry ? machine.state(index).initial_transition : nil,
|
|
122
|
+
history_content: entry_set.default_history_content[index],
|
|
123
|
+
completions: stops ? [].freeze : completion_events(snapshot, index), stops:)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# A final state's completion events: `done.state.` and its parent's
|
|
127
|
+
# id, carrying its `<donedata>`, then, when the parent's own parent is
|
|
128
|
+
# a parallel every region of which is now in a final state,
|
|
129
|
+
# `done.state.` and that parallel's id, carrying no data. A parent with
|
|
130
|
+
# no id raises nothing, its parallel's event included; a parallel with
|
|
131
|
+
# no id raises nothing for itself.
|
|
132
|
+
def completion_events(snapshot, index)
|
|
133
|
+
machine = snapshot.machine
|
|
134
|
+
return [].freeze unless machine.final?(index)
|
|
135
|
+
|
|
136
|
+
parent = machine.state(index).parent
|
|
137
|
+
return [].freeze unless named?(machine, parent)
|
|
138
|
+
|
|
139
|
+
parent_event = Completion.new(name: "done.state.#{machine.state(parent).id}", final: index, donedata: true)
|
|
140
|
+
[parent_event, *parallel_completion(snapshot, index, machine.state(parent).parent)].freeze
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The parallel's event, when `grandparent` is a parallel with an id
|
|
144
|
+
# every region of which is in a final state.
|
|
145
|
+
def parallel_completion(snapshot, index, grandparent)
|
|
146
|
+
machine = snapshot.machine
|
|
147
|
+
return [] unless !grandparent.nil? && machine.parallel?(grandparent) && named?(machine, grandparent)
|
|
148
|
+
return [] unless machine.child_states(grandparent).all? { in_final_state?(snapshot, _1) }
|
|
149
|
+
|
|
150
|
+
[Completion.new(name: "done.state.#{machine.state(grandparent).id}", final: index, donedata: false)]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Whether the state has a non-empty id.
|
|
154
|
+
def named?(machine, index)
|
|
155
|
+
id = machine.state(index).id
|
|
156
|
+
!id.nil? && !id.empty?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# `isInFinalState`: a compound state is in a final state when an
|
|
160
|
+
# active child is a final; a parallel when every region is,
|
|
161
|
+
# recursively. Anything else, a final itself included, is not.
|
|
162
|
+
def in_final_state?(snapshot, index)
|
|
163
|
+
machine = snapshot.machine
|
|
164
|
+
if machine.compound?(index)
|
|
165
|
+
machine.child_states(index).any? { snapshot.active?(_1) && machine.final?(_1) }
|
|
166
|
+
elsif machine.parallel?(index)
|
|
167
|
+
machine.child_states(index).all? { in_final_state?(snapshot, _1) }
|
|
168
|
+
else
|
|
169
|
+
false
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Interpreter
|
|
5
|
+
# `<foreach>`, as the reference's foreach node runs it
|
|
6
|
+
# (`Statifier.Machine.Content.Foreach` in statifier-ex), for
|
|
7
|
+
# {Content}'s block runner: the names are checked before anything else,
|
|
8
|
+
# and a failure there, or of `array`, keeps nothing.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module Foreach
|
|
12
|
+
# A `<foreach>` `item` that is not a bare variable name.
|
|
13
|
+
IllegalItemName = ::Data.define(:name)
|
|
14
|
+
|
|
15
|
+
# A `<foreach>` `index` that is not a bare variable name.
|
|
16
|
+
IllegalIndexName = ::Data.define(:name)
|
|
17
|
+
|
|
18
|
+
# A `<foreach>` `array` that evaluated to something other than a list.
|
|
19
|
+
NotIterable = ::Data.define(:value)
|
|
20
|
+
|
|
21
|
+
# A bare variable name, the one shape a `<foreach>` `item` and `index`
|
|
22
|
+
# take.
|
|
23
|
+
NAME = /\A[A-Za-z_][A-Za-z0-9_]*\z/
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# `<foreach>`: the names checked, `array` evaluated once (the list is
|
|
28
|
+
# a value, so the body cannot change what is iterated), the item and
|
|
29
|
+
# index declared undefined when the datamodel does not hold them, and
|
|
30
|
+
# the body run once per element with the item and the zero-based index
|
|
31
|
+
# bound. Unlike an `<assign>`, it may bind a name the datamodel does not
|
|
32
|
+
# yet hold. A body failure keeps what earlier iterations wrote.
|
|
33
|
+
def execute(run, node, stamp)
|
|
34
|
+
collection = iterated(run.context, node)
|
|
35
|
+
return [run, collection] unless collection.is_a?(Array)
|
|
36
|
+
|
|
37
|
+
collection.each_with_index.reduce([run.with(context: declare(run.context, node)), nil]) do |(at, _), (value, i)|
|
|
38
|
+
after, reason = Content.nested(at.with(context: bind_iteration(at.context, node, value, i)), node.content,
|
|
39
|
+
stamp)
|
|
40
|
+
break [after, reason] unless reason.nil?
|
|
41
|
+
|
|
42
|
+
[after, nil]
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The context with one iteration's item and index bound.
|
|
47
|
+
def bind_iteration(context, node, value, position)
|
|
48
|
+
context = context.bind(node.item, value)
|
|
49
|
+
node.index.nil? ? context : context.bind(node.index, position)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The list a `<foreach>` iterates, or the reason it cannot: an illegal
|
|
53
|
+
# item, then an illegal index, then a failed `array`, then a value that
|
|
54
|
+
# is not a list.
|
|
55
|
+
def iterated(context, node)
|
|
56
|
+
illegal = illegal_name(node.item, IllegalItemName) || (node.index && illegal_name(node.index, IllegalIndexName))
|
|
57
|
+
return illegal if illegal
|
|
58
|
+
|
|
59
|
+
evaluated = Datamodel.evaluate(context, node.array)
|
|
60
|
+
return evaluated.error unless evaluated.ok?
|
|
61
|
+
|
|
62
|
+
evaluated.value.is_a?(Array) ? evaluated.value : NotIterable.new(value: evaluated.value)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Why `name` cannot be bound, or nil: a system variable's root, or not a
|
|
66
|
+
# bare name.
|
|
67
|
+
def illegal_name(name, illegal)
|
|
68
|
+
return Datamodel::SystemVariable.new(root: name) if name.start_with?("_")
|
|
69
|
+
|
|
70
|
+
NAME.match?(name) ? nil : illegal.new(name:)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The context with the item and the index (when written) each declared
|
|
74
|
+
# undefined unless the datamodel holds it, so both are bound even when
|
|
75
|
+
# the list is empty.
|
|
76
|
+
def declare(context, node)
|
|
77
|
+
[node.item, node.index].compact.reduce(context) do |declared, name|
|
|
78
|
+
declared.data.key?(name) ? declared : declared.bind(name, Predicator::UNDEFINED)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Interpreter
|
|
5
|
+
module Invoke
|
|
6
|
+
# An `<invoke>`'s arguments resolved against one context, in the
|
|
7
|
+
# reference's order (`invoke_one`'s `with` in `Statifier.Interpreter`,
|
|
8
|
+
# statifier-ex): the type, the source, the `namelist` and `<param>`
|
|
9
|
+
# values in document order, then `<content>`. The first failure
|
|
10
|
+
# abandons the whole element, unlike a `<donedata>` param, which fails
|
|
11
|
+
# alone.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
module Arguments
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# The resolved `type:`, `src:`, `params:` and `content:` as a Hash,
|
|
18
|
+
# or the reason the first that failed carries.
|
|
19
|
+
def resolve(context, invoke)
|
|
20
|
+
resolved = {}
|
|
21
|
+
{ type: invoke.type, src: invoke.src }.each do |key, expr|
|
|
22
|
+
evaluated = expr.nil? ? Send::NONE : Datamodel.evaluate(context, expr)
|
|
23
|
+
return evaluated.error unless evaluated.ok?
|
|
24
|
+
|
|
25
|
+
resolved[key] = evaluated.value
|
|
26
|
+
end
|
|
27
|
+
rest(context, invoke, resolved)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The params and the content, after the type and the source.
|
|
31
|
+
def rest(context, invoke, resolved)
|
|
32
|
+
pairs = Send.resolve_params(context, invoke.namelist + invoke.params)
|
|
33
|
+
return pairs.error unless pairs.ok?
|
|
34
|
+
|
|
35
|
+
content = content(context, invoke.content)
|
|
36
|
+
return content.error unless content.ok?
|
|
37
|
+
|
|
38
|
+
resolved.merge(params: Send.params_data(pairs.value), content: content.value)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The `<content>` child's data: nil for none, its body read as a
|
|
42
|
+
# `<send>`'s is ({Send.text_data}), or its `expr`'s value.
|
|
43
|
+
def content(context, content)
|
|
44
|
+
return Send::NONE if content.nil?
|
|
45
|
+
return Send.value(Send.text_data(content.value)) if content.is_a?(Machine::Static)
|
|
46
|
+
|
|
47
|
+
Datamodel.evaluate(context, content)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Interpreter
|
|
5
|
+
module Invoke
|
|
6
|
+
# The finalize and autoforward pass and the cancel on exit, as
|
|
7
|
+
# {Invoke} says.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module Passes
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# `for state in configuration: for inv in state.invoke: if
|
|
14
|
+
# inv.invokeid == externalEvent.invokeid: applyFinalize(inv,
|
|
15
|
+
# externalEvent); if inv.autoforward: send(inv.id, externalEvent)`,
|
|
16
|
+
# run after `_event` is set and before selection, with its
|
|
17
|
+
# {Effect::Trace::FinalizeAutoforward} last. The configuration is
|
|
18
|
+
# walked in document order and each state's invocations in document
|
|
19
|
+
# order; one that is not live is passed over. The two tests are
|
|
20
|
+
# independent: a live invocation that matches and forwards does
|
|
21
|
+
# both, its `<finalize>` first.
|
|
22
|
+
#
|
|
23
|
+
# @return [Array] `[state, effects]`
|
|
24
|
+
def finalize_autoforward(state, event)
|
|
25
|
+
return [state, trace(state, event, [], [])] if state.active_invocations.empty?
|
|
26
|
+
|
|
27
|
+
passed, effects = state.configuration.reduce([state, []]) do |acc, index|
|
|
28
|
+
state.machine.state(index).invoke.reduce(acc) { |inner, invoke| pass_one(inner, index, invoke, event) }
|
|
29
|
+
end
|
|
30
|
+
[passed, effects + trace(passed, event, finalized(state, event), forwarded(effects))]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The live invocations whose id the event carries.
|
|
34
|
+
def finalized(state, event) = state.active_invocations.values.select { _1 == event.invokeid }.uniq
|
|
35
|
+
|
|
36
|
+
# The invocations the pass forwarded the event to, in order.
|
|
37
|
+
def forwarded(effects) = effects.grep(Effect::Autoforward).map(&:invoke_id)
|
|
38
|
+
|
|
39
|
+
# The pass's trace.
|
|
40
|
+
def trace(state, event, finalized, forwarded)
|
|
41
|
+
Blocks.traced(state, Effect::Trace::FinalizeAutoforward, event:, finalized:, forwarded:)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# One `<invoke>`'s body of the pass.
|
|
45
|
+
def pass_one((state, effects), index, invoke, event)
|
|
46
|
+
invoke_id = state.active_invocations[[index, invoke.index]]
|
|
47
|
+
return [state, effects] if invoke_id.nil?
|
|
48
|
+
|
|
49
|
+
stepped = invoke_id == event.invokeid ? finalize(state, index, invoke, event) : none(state)
|
|
50
|
+
[stepped.state, effects + stepped.effects + forward(stepped.state, index, invoke, invoke_id, event)]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A step that changes nothing.
|
|
54
|
+
def none(state) = Stepped.new(state:, effects: [])
|
|
55
|
+
|
|
56
|
+
# The {Effect::Autoforward} of an invocation that sets
|
|
57
|
+
# `autoforward`.
|
|
58
|
+
def forward(state, index, invoke, invoke_id, event)
|
|
59
|
+
return [] unless invoke.autoforward
|
|
60
|
+
|
|
61
|
+
[Effect::Autoforward.new(invoke_id:, state_index: index, event:, **state.counters)]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# `applyFinalize`: no `<finalize>` does nothing; an empty one writes
|
|
65
|
+
# the returned values back ({auto_assign}); any other runs as a block
|
|
66
|
+
# owned by `{kind: :finalize, state_index:, invoke_index:}`.
|
|
67
|
+
#
|
|
68
|
+
# @return [Stepped]
|
|
69
|
+
def finalize(state, index, invoke, event)
|
|
70
|
+
block = invoke.finalize
|
|
71
|
+
return none(state) if block.nil?
|
|
72
|
+
return auto_assign(state, index, invoke, event.data) if block.content.empty?
|
|
73
|
+
|
|
74
|
+
Blocks.run_block(state, { kind: :finalize, state_index: index, invoke_index: invoke.index }, block.content)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# An empty `<finalize>`: each `namelist` entry and each `<param
|
|
78
|
+
# location>` whose name the event's data, a map, carries is written
|
|
79
|
+
# as by `<assign>` with that value, each write answering an
|
|
80
|
+
# {Effect::DatamodelChange} in the order written. A write that fails
|
|
81
|
+
# raises `error.execution` (origin `{kind: :finalize, state_index:,
|
|
82
|
+
# invoke_index:}`) and leaves the other writes standing.
|
|
83
|
+
#
|
|
84
|
+
# @return [Stepped]
|
|
85
|
+
def auto_assign(state, index, invoke, data)
|
|
86
|
+
return none(state) unless data.is_a?(Hash)
|
|
87
|
+
|
|
88
|
+
owner = { kind: :finalize, state_index: index, invoke_index: invoke.index }
|
|
89
|
+
targets(invoke).select { data.key?(_1.name) }.reduce(none(state)) do |acc, param|
|
|
90
|
+
acc.then_step(write(acc.state, param.expr.source, data[param.name], owner))
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The `namelist` entries and `<param location>`s an empty
|
|
95
|
+
# `<finalize>` may write, the ones that compiled.
|
|
96
|
+
def targets(invoke)
|
|
97
|
+
(invoke.namelist + invoke.params).select { _1.kind == :location && _1.expr.is_a?(Machine::Compiled) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# One write of an empty `<finalize>`.
|
|
101
|
+
#
|
|
102
|
+
# @return [Stepped]
|
|
103
|
+
def write(state, source, value, owner)
|
|
104
|
+
written = Datamodel::Location.write(state.context, source, value)
|
|
105
|
+
return none(state.raise_failure(owner, written.error)) unless written.ok?
|
|
106
|
+
|
|
107
|
+
after = state.with(datamodel: Ractor.make_shareable(written.context.data))
|
|
108
|
+
Stepped.new(state: after, effects: [Invoke.change(after, written, source, value, owner)])
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# `for inv in s.invoke: cancelInvoke(inv)`: an {Effect::CancelInvoke}
|
|
112
|
+
# per live invocation of the state at `index`, in document order,
|
|
113
|
+
# each no longer live after it. Run after the state's `<onexit>`
|
|
114
|
+
# blocks and before it leaves the configuration.
|
|
115
|
+
#
|
|
116
|
+
# @return [Stepped]
|
|
117
|
+
def cancel_invocations(state, index)
|
|
118
|
+
state.machine.state(index).invoke.reduce(none(state)) do |acc, invoke|
|
|
119
|
+
acc.then_step(cancel_one(acc.state, index, invoke.index))
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# One invocation's `cancelInvoke(inv)`, nothing when it is not live.
|
|
124
|
+
#
|
|
125
|
+
# @return [Stepped]
|
|
126
|
+
def cancel_one(state, index, invoke_index)
|
|
127
|
+
key = [index, invoke_index]
|
|
128
|
+
invoke_id = state.active_invocations[key]
|
|
129
|
+
return none(state) if invoke_id.nil?
|
|
130
|
+
|
|
131
|
+
after = state.with(active_invocations: state.active_invocations.except(key).freeze)
|
|
132
|
+
Stepped.new(state: after, effects: [cancel_effect(after, invoke_id, index)])
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The {Effect::CancelInvoke}.
|
|
136
|
+
def cancel_effect(state, invoke_id, index)
|
|
137
|
+
Frozen.copy(Effect::CancelInvoke.new(invoke_id:, state_index: index, **state.counters,
|
|
138
|
+
caller_context: state.caller_context))
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|