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,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