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,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Lowering
|
|
5
|
+
# Placing a container's lowered children into its fields. A builder
|
|
6
|
+
# answers its node tagged with the kind of place it goes in (`:state` for
|
|
7
|
+
# the four state kinds, `:content_node` for executable content, the
|
|
8
|
+
# element's own name for the rest); a container names the tags it has a
|
|
9
|
+
# place for, and every other child is misplaced. Placement never looks at
|
|
10
|
+
# the parent's element name except to word that error.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
module Placement
|
|
14
|
+
# The element name a misplaced executable node is reported under.
|
|
15
|
+
NAMES = {
|
|
16
|
+
Document::Raise => "raise", Document::Log => "log", Document::Assign => "assign",
|
|
17
|
+
Document::If => "if", Document::Foreach => "foreach", Document::Script => "script",
|
|
18
|
+
Document::Send => "send", Document::Cancel => "cancel"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# The places of a container whose one place is executable content.
|
|
22
|
+
CONTENT = { content_node: %i[content many] }.freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# Folds `results` (the tagged children, nil for one that could not be
|
|
27
|
+
# made) into the places `slots` names, `{tag => [field, :many | :one]}`;
|
|
28
|
+
# a node class as a key places executable content of that class alone.
|
|
29
|
+
# A `:many` field collects in document order, a `:one` field keeps the
|
|
30
|
+
# last. Answers the filled fields and the misplaced errors, newest
|
|
31
|
+
# first, as the reference accumulates them.
|
|
32
|
+
def fold(results, slots, parent_name)
|
|
33
|
+
filled = {}
|
|
34
|
+
errors = []
|
|
35
|
+
results.each do |result|
|
|
36
|
+
next if result.nil?
|
|
37
|
+
|
|
38
|
+
field, arity = slot_for(slots, *result)
|
|
39
|
+
next errors.unshift(misplaced(*result, parent_name)) if field.nil?
|
|
40
|
+
|
|
41
|
+
arity == :many ? (filled[field] ||= []) << result.last : filled[field] = result.last
|
|
42
|
+
end
|
|
43
|
+
[filled, errors]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A `:many` field's nodes, frozen.
|
|
47
|
+
def many(filled, field) = (filled[field] || Reading::EMPTY).freeze
|
|
48
|
+
|
|
49
|
+
# The field and arity `slots` names for a tagged node, or nil.
|
|
50
|
+
def slot_for(slots, tag, node)
|
|
51
|
+
slots.fetch(tag) { tag == :content_node ? slots[node.class] : nil }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The misplaced error for a tagged node its parent has no place for.
|
|
55
|
+
def misplaced(tag, node, parent_name)
|
|
56
|
+
return Errors.misplaced(NAMES.fetch(node.class), parent_name, span(node)) if tag == :content_node
|
|
57
|
+
|
|
58
|
+
Errors.misplaced(tag.to_s, parent_name, node.location)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# An assign's `location` is its attribute; its span is `node_location`.
|
|
62
|
+
def span(node) = node.is_a?(Document::Assign) ? node.node_location : node.location
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Lowering
|
|
5
|
+
# Reading an element's attributes and text as the builders need them.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module Reading
|
|
9
|
+
# What a list attribute splits on: Unicode white space except the
|
|
10
|
+
# no-break spaces, as the reference splits it.
|
|
11
|
+
SPLIT = /[\t\n\v\f\r \u0085\u1680\u2000-\u2006\u2008-\u200a\u2028\u2029\u205f\u3000]+/
|
|
12
|
+
BLANK = /\A[#{Errors::TRIM}]*\z/
|
|
13
|
+
EMPTY = [].freeze
|
|
14
|
+
NO_LOCATIONS = {}.freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# The value of the first attribute written with `name`, or nil.
|
|
19
|
+
def value(element, name) = attribute(element, name)&.value
|
|
20
|
+
|
|
21
|
+
# An attribute split on white space: absent and empty both answer an
|
|
22
|
+
# empty list, told apart by the attribute's location.
|
|
23
|
+
def list(element, name)
|
|
24
|
+
raw = value(element, name)
|
|
25
|
+
raw.nil? ? EMPTY : raw.split(SPLIT).reject(&:empty?).map(&:freeze).freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# An attribute mapped through `mapping`, or `default` when it is absent
|
|
29
|
+
# or not a key of `mapping`.
|
|
30
|
+
def choice(element, name, mapping, default) = mapping.fetch(value(element, name), default)
|
|
31
|
+
|
|
32
|
+
# The value spans of the named attributes that were written, keyed by
|
|
33
|
+
# name as a Symbol.
|
|
34
|
+
def locations(element, names)
|
|
35
|
+
names.each_with_object({}) do |name, spans|
|
|
36
|
+
found = attribute(element, name)
|
|
37
|
+
spans[name.to_sym] = found.value_location if found&.value_location
|
|
38
|
+
end.freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The element's own character data, its text children joined.
|
|
42
|
+
def text(element)
|
|
43
|
+
element.children.grep(XML::Text).map(&:value).join.freeze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Whether trimming Unicode white space from `text` leaves nothing.
|
|
47
|
+
def blank?(text) = BLANK.match?(text)
|
|
48
|
+
|
|
49
|
+
# The first attribute written with `name`, or nil.
|
|
50
|
+
def attribute(element, name) = element.attributes.find { |candidate| candidate.name == name }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Lowering
|
|
5
|
+
# The builders of `<scxml>`, the state family, `<initial>`,
|
|
6
|
+
# `<transition>` and the blocks. Each takes an element and the source,
|
|
7
|
+
# and answers its node tagged with its place (the root's untagged) and
|
|
8
|
+
# its errors.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module States
|
|
12
|
+
ROOT_SLOTS = {
|
|
13
|
+
state: %i[states many], datamodel: %i[datamodel_element one], Document::Script => %i[scripts many]
|
|
14
|
+
}.freeze
|
|
15
|
+
STATE_SLOTS = {
|
|
16
|
+
state: %i[states many], transition: %i[transitions many], initial: %i[initial_element one],
|
|
17
|
+
onentry: %i[onentry many], onexit: %i[onexit many], donedata: %i[donedata one],
|
|
18
|
+
datamodel: %i[datamodel_element one], invoke: %i[invoke many]
|
|
19
|
+
}.freeze
|
|
20
|
+
INITIAL_SLOTS = { transition: %i[transitions many] }.freeze
|
|
21
|
+
ROOT_ATTRIBUTES = %w[initial name datamodel binding version xmlns].freeze
|
|
22
|
+
BINDINGS = { "early" => :early, "late" => :late }.freeze
|
|
23
|
+
HISTORY_TYPES = { "shallow" => :shallow, "deep" => :deep }.freeze
|
|
24
|
+
TRANSITION_TYPES = { "internal" => :internal, "external" => :external }.freeze
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
# `<scxml>`: answers the {Document} and its errors.
|
|
29
|
+
def scxml(element, source)
|
|
30
|
+
filled, errors = lower_children(element, source, ROOT_SLOTS)
|
|
31
|
+
document = Document.new(
|
|
32
|
+
location: element.location, **%i[name version xmlns datamodel].to_h { [_1, Reading.value(element, _1.to_s)] },
|
|
33
|
+
namespace: element.namespace, binding: Reading.choice(element, "binding", BINDINGS, :early),
|
|
34
|
+
initial: Reading.list(element, "initial"), states: Placement.many(filled, :states),
|
|
35
|
+
datamodel_element: filled[:datamodel_element], scripts: Placement.many(filled, :scripts),
|
|
36
|
+
attribute_locations: Reading.locations(element, ROOT_ATTRIBUTES)
|
|
37
|
+
)
|
|
38
|
+
[document, errors]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The four state kinds share one builder; each answers a
|
|
42
|
+
# {Document::State} tagged `:state`.
|
|
43
|
+
def state(element, source) = state_like(element, source, :state)
|
|
44
|
+
def parallel(element, source) = state_like(element, source, :parallel)
|
|
45
|
+
def final(element, source) = state_like(element, source, :final)
|
|
46
|
+
|
|
47
|
+
# A history's `type` lowers to `:shallow` when it is neither value, and
|
|
48
|
+
# its location is kept so that a check can point at it.
|
|
49
|
+
def history(element, source)
|
|
50
|
+
(tag, state), errors = state_like(element, source, :history)
|
|
51
|
+
locations = state.attribute_locations.merge(Reading.locations(element, %w[type])).freeze
|
|
52
|
+
history_type = Reading.choice(element, "type", HISTORY_TYPES, :shallow)
|
|
53
|
+
[[tag, state.with(history_type:, attribute_locations: locations)], errors]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# An `<initial>`: every `<transition>` child it holds.
|
|
57
|
+
def initial(element, source)
|
|
58
|
+
filled, errors = lower_children(element, source, INITIAL_SLOTS)
|
|
59
|
+
[[:initial, Document::Initial.new(location: element.location,
|
|
60
|
+
transitions: Placement.many(filled, :transitions))], errors]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# A `<transition>`: its attributes and its executable content.
|
|
64
|
+
def transition(element, source)
|
|
65
|
+
filled, errors = lower_children(element, source, Placement::CONTENT)
|
|
66
|
+
node = Document::Transition.new(
|
|
67
|
+
location: element.location, event: Reading.list(element, "event"),
|
|
68
|
+
target: Reading.list(element, "target"), cond: Reading.value(element, "cond"),
|
|
69
|
+
type: Reading.choice(element, "type", TRANSITION_TYPES, :external),
|
|
70
|
+
content: Placement.many(filled, :content),
|
|
71
|
+
attribute_locations: Reading.locations(element, %w[event target cond type])
|
|
72
|
+
)
|
|
73
|
+
[[:transition, node], errors]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# `<onentry>`, `<onexit>` and `<finalize>` each answer one block,
|
|
77
|
+
# tagged with the element's name.
|
|
78
|
+
def onentry(element, source) = block(element, source, :onentry)
|
|
79
|
+
def onexit(element, source) = block(element, source, :onexit)
|
|
80
|
+
def finalize(element, source) = block(element, source, :finalize)
|
|
81
|
+
|
|
82
|
+
# One {Document::Block} per element, never flattened.
|
|
83
|
+
def block(element, source, tag)
|
|
84
|
+
filled, errors = lower_children(element, source, Placement::CONTENT)
|
|
85
|
+
[[tag, Document::Block.new(location: element.location, content: Placement.many(filled, :content))], errors]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A state of `kind`: its attributes and its children in their fields.
|
|
89
|
+
def state_like(element, source, kind)
|
|
90
|
+
filled, errors = lower_children(element, source, STATE_SLOTS)
|
|
91
|
+
node = Document::State.new(
|
|
92
|
+
kind:, location: element.location, id: Reading.value(element, "id"),
|
|
93
|
+
initial: Reading.list(element, "initial"), initial_element: filled[:initial_element],
|
|
94
|
+
states: Placement.many(filled, :states), transitions: Placement.many(filled, :transitions),
|
|
95
|
+
onentry: Placement.many(filled, :onentry), onexit: Placement.many(filled, :onexit),
|
|
96
|
+
history_type: nil, donedata: filled[:donedata], datamodel_element: filled[:datamodel_element],
|
|
97
|
+
invoke: Placement.many(filled, :invoke), attribute_locations: Reading.locations(element, %w[id initial])
|
|
98
|
+
)
|
|
99
|
+
[[:state, node], errors]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Walks an element's children and places them: answers the filled
|
|
103
|
+
# fields and the walk's errors followed by the misplaced ones.
|
|
104
|
+
def lower_children(element, source, slots)
|
|
105
|
+
results, errors = Lowering.walk_children(element, source)
|
|
106
|
+
filled, misplaced = Placement.fold(results, slots, element.name)
|
|
107
|
+
[filled, errors + misplaced]
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "document"
|
|
4
|
+
require_relative "lowering/error"
|
|
5
|
+
require_relative "lowering/reading"
|
|
6
|
+
require_relative "lowering/placement"
|
|
7
|
+
require_relative "lowering/states"
|
|
8
|
+
require_relative "lowering/executable"
|
|
9
|
+
require_relative "lowering/data"
|
|
10
|
+
|
|
11
|
+
module Statifier
|
|
12
|
+
# The reader's element tree in, the typed chart ({Document}) out: the
|
|
13
|
+
# second step of reading a chart, a port of the reference's lowering.
|
|
14
|
+
#
|
|
15
|
+
# One walk. Each element is lowered by the builder its local name selects;
|
|
16
|
+
# a builder lowers its children first, then places each into a field of
|
|
17
|
+
# its own node, and a child its node has no place for is misplaced. An
|
|
18
|
+
# element is SCXML's own when its name resolves to the SCXML namespace or
|
|
19
|
+
# to none, so a chart that declares no namespace lowers as its declared
|
|
20
|
+
# twin does; reporting a missing declaration is a later check's.
|
|
21
|
+
# `<content>`, `<assign>`, `<data>` and `<script>` hold text as their
|
|
22
|
+
# payload and are exempt from the stray-text rule; the element children of
|
|
23
|
+
# `<content>` and `<assign>` are kept as a slice of the source, never
|
|
24
|
+
# lowered.
|
|
25
|
+
#
|
|
26
|
+
# The walk does not stop at the first refusal: it collects every one, and
|
|
27
|
+
# the chart is answered only when there are none, never a partial one.
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
module Lowering
|
|
31
|
+
# The SCXML namespace.
|
|
32
|
+
SCXML_NAMESPACE = "http://www.w3.org/2005/07/scxml"
|
|
33
|
+
|
|
34
|
+
# The builder of each element that may appear below the root. `<scxml>`
|
|
35
|
+
# is not one: it is only ever the root.
|
|
36
|
+
BUILDERS = {
|
|
37
|
+
"state" => [States, :state], "parallel" => [States, :parallel], "final" => [States, :final],
|
|
38
|
+
"history" => [States, :history], "initial" => [States, :initial], "transition" => [States, :transition],
|
|
39
|
+
"onentry" => [States, :onentry], "onexit" => [States, :onexit], "finalize" => [States, :finalize],
|
|
40
|
+
"raise" => [Executable, :raise_node], "log" => [Executable, :log], "assign" => [Executable, :assign],
|
|
41
|
+
"if" => [Executable, :if_node], "elseif" => [Executable, :elseif], "else" => [Executable, :else_node],
|
|
42
|
+
"foreach" => [Executable, :foreach], "script" => [Executable, :script], "send" => [Executable, :send_node],
|
|
43
|
+
"cancel" => [Executable, :cancel], "datamodel" => [DataModel, :datamodel], "data" => [DataModel, :data],
|
|
44
|
+
"donedata" => [DataModel, :donedata], "param" => [DataModel, :param], "content" => [DataModel, :content],
|
|
45
|
+
"invoke" => [DataModel, :invoke]
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
# The encodings whose strings the reader reads from their bytes.
|
|
49
|
+
READ_AS_BYTES = [Encoding::BINARY, Encoding::US_ASCII].freeze
|
|
50
|
+
|
|
51
|
+
# What {Lowering.lower} answers: the chart as `value`, or every refusal,
|
|
52
|
+
# in document order, as `errors`.
|
|
53
|
+
Result = ::Data.define(:value, :errors) do
|
|
54
|
+
# @return [Boolean] true when the tree lowered
|
|
55
|
+
def ok? = errors.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Lowers a parsed `<scxml>` element tree into its typed chart.
|
|
59
|
+
#
|
|
60
|
+
# @param root [XML::Element] the root element {XML.parse} answered
|
|
61
|
+
# @param source [String] the document it was parsed from; `<content>` and
|
|
62
|
+
# `<assign>` slice their markup out of it
|
|
63
|
+
# @return [Result] the {Document} as `value`, or the {Error}s as `errors`
|
|
64
|
+
def self.lower(root, source)
|
|
65
|
+
refusal = root_refusal(root)
|
|
66
|
+
return refuse([refusal]) if refusal
|
|
67
|
+
|
|
68
|
+
document, errors = States.scxml(root, utf8(source))
|
|
69
|
+
errors.empty? ? Result.new(value: document, errors: Reading::EMPTY) : refuse(errors)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Why a root does not lower: it is foreign, or it is not `<scxml>`.
|
|
73
|
+
def self.root_refusal(root)
|
|
74
|
+
return Errors.foreign_element(root.name, root.namespace, root.location) unless scxml?(root.namespace)
|
|
75
|
+
|
|
76
|
+
name = local_name(root.name)
|
|
77
|
+
Errors.unexpected_root(name, root.location) unless name == "scxml"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Lowers an element's children: each child element by its builder, each
|
|
81
|
+
# text run checked against the stray-text rule. Answers the tagged
|
|
82
|
+
# results in document order (nil for an element that made no node) and
|
|
83
|
+
# the errors newest first, as the reference accumulates them.
|
|
84
|
+
def self.walk_children(element, source)
|
|
85
|
+
element.children.each_with_object([[], []]) do |child, (results, errors)|
|
|
86
|
+
result, child_errors = child.is_a?(XML::Text) ? [nil, walk_text(child)] : walk_element(child, source)
|
|
87
|
+
results << result
|
|
88
|
+
errors.unshift(*child_errors.reverse)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.walk_text(text)
|
|
93
|
+
Reading.blank?(text.value) ? Reading::EMPTY : [Errors.stray_text(text.value, text.location)]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A child element lowered by its builder, or refused when it is foreign
|
|
97
|
+
# or no builder reads it.
|
|
98
|
+
def self.walk_element(element, source)
|
|
99
|
+
return [nil, [Errors.foreign_element(element.name, element.namespace, element.location)]] \
|
|
100
|
+
unless scxml?(element.namespace)
|
|
101
|
+
|
|
102
|
+
builder, method = BUILDERS[local_name(element.name)]
|
|
103
|
+
return [nil, [Errors.unsupported(element.name, element.location)]] if builder.nil?
|
|
104
|
+
|
|
105
|
+
builder.public_send(method, element, source)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.scxml?(uri) = uri.nil? || uri == SCXML_NAMESPACE
|
|
109
|
+
|
|
110
|
+
def self.local_name(name) = name.split(":", 2).last
|
|
111
|
+
|
|
112
|
+
# Every error in document order: by start offset, found order kept at
|
|
113
|
+
# one offset.
|
|
114
|
+
def self.refuse(errors)
|
|
115
|
+
sorted = errors.each_with_index.sort_by { |error, index| [error.location.start_offset, index] }.map(&:first)
|
|
116
|
+
Result.new(value: nil, errors: sorted.freeze)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# The source as the reader read it, so that its byte offsets slice it.
|
|
120
|
+
def self.utf8(source)
|
|
121
|
+
return source if source.encoding == Encoding::UTF_8
|
|
122
|
+
return source.dup.force_encoding(Encoding::UTF_8) if READ_AS_BYTES.include?(source.encoding)
|
|
123
|
+
|
|
124
|
+
source.encode(Encoding::UTF_8)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private_class_method :root_refusal, :walk_text, :walk_element, :scxml?, :local_name, :refuse, :utf8
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
class Machine
|
|
5
|
+
# The compiled executable content. Every node carries its `c_index`, its
|
|
6
|
+
# place in one dense index space over the whole chart in document order
|
|
7
|
+
# (a container before its children), the index an execution error names.
|
|
8
|
+
# An expression is a {Compiled} or a {Static}; one whose failure is
|
|
9
|
+
# deferred to when it runs is an {Invalid}.
|
|
10
|
+
|
|
11
|
+
# A `<raise>`: `event` as written.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
Raise = ::Data.define(:c_index, :event, :location)
|
|
15
|
+
|
|
16
|
+
# A `<log>`: `label` as written, `expr` a {Compiled} or nil, and
|
|
17
|
+
# `expr_location` where it was written, nil when it was not.
|
|
18
|
+
#
|
|
19
|
+
# @api private
|
|
20
|
+
Log = ::Data.define(:c_index, :label, :expr, :location, :expr_location)
|
|
21
|
+
|
|
22
|
+
# An `<assign>`. `location` is the path assigned to, as written, and
|
|
23
|
+
# `location_location` its span; `node_location` is the element's span.
|
|
24
|
+
# `value` is its `expr` (a {Compiled}, or an {Invalid} when it did not
|
|
25
|
+
# compile), else its markup children as a {Static} string, else its text
|
|
26
|
+
# folded to a {Static} value; `expr_location` is where `expr` was
|
|
27
|
+
# written, nil when it was not.
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
Assign = ::Data.define(:c_index, :location, :node_location, :value, :location_location, :expr_location)
|
|
31
|
+
|
|
32
|
+
# An `<if>`: one {Branch} for the `<if>` and one per `<elseif>` and
|
|
33
|
+
# `<else>`.
|
|
34
|
+
#
|
|
35
|
+
# @api private
|
|
36
|
+
If = ::Data.define(:c_index, :location, :branches)
|
|
37
|
+
|
|
38
|
+
class If
|
|
39
|
+
# One branch: `cond` a {Compiled}, nil for an `<else>`, with
|
|
40
|
+
# `cond_location`; `content` the content up to the next branch.
|
|
41
|
+
#
|
|
42
|
+
# @api private
|
|
43
|
+
Branch = ::Data.define(:cond, :cond_location, :content)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# A `<foreach>`: `array` a {Compiled}, `item` and `index` as written
|
|
47
|
+
# (`index` nil when absent), each with the span of its attribute
|
|
48
|
+
# (`index_location` nil when `index` is), and its content.
|
|
49
|
+
#
|
|
50
|
+
# @api private
|
|
51
|
+
Foreach = ::Data.define(
|
|
52
|
+
:c_index, :location, :array, :array_location, :item, :item_location, :index, :index_location, :content
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# A `<script>`: `program` a {Program}, or an {Invalid} when its body did
|
|
56
|
+
# not compile; `node_location` the element's span.
|
|
57
|
+
#
|
|
58
|
+
# @api private
|
|
59
|
+
Script = ::Data.define(:c_index, :program, :node_location)
|
|
60
|
+
|
|
61
|
+
# A `<send>`. `event`, `target`, `type` and `delay` each fold a literal
|
|
62
|
+
# attribute and its `expr` twin into one expression: a {Static} for the
|
|
63
|
+
# literal, a {Compiled} for the twin, nil when neither was written.
|
|
64
|
+
# `id` and `idlocation` are as written; `namelist` holds a {Param} per
|
|
65
|
+
# entry (a location read by its own name, an {Invalid} when it did not
|
|
66
|
+
# compile), `params` a {Param} per `<param>`, and `content` the
|
|
67
|
+
# `<content>` child's value, or nil.
|
|
68
|
+
#
|
|
69
|
+
# @api private
|
|
70
|
+
Send = ::Data.define(
|
|
71
|
+
:c_index, :location, :event, :target, :type, :id, :idlocation, :delay, :namelist, :params, :content,
|
|
72
|
+
:attribute_locations
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# A `<cancel>`: `sendid` a {Static} for the literal attribute, else a
|
|
76
|
+
# {Compiled} for `sendidexpr`.
|
|
77
|
+
#
|
|
78
|
+
# @api private
|
|
79
|
+
Cancel = ::Data.define(:c_index, :location, :sendid, :attribute_locations)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
class Machine
|
|
5
|
+
# A compiled `<data>`. `value` is, in the specification's precedence, its
|
|
6
|
+
# `expr` (a {Compiled}, or an {Invalid} when it did not compile, which
|
|
7
|
+
# fails when the datamodel binds it), its `src` (a {Src}), its text
|
|
8
|
+
# folded to a {Static} value, or a {Static} nil. `value_location` is
|
|
9
|
+
# where the value was written: the attribute when one was, else the
|
|
10
|
+
# element.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
Data = ::Data.define(:d_index, :id, :value, :location, :value_location)
|
|
14
|
+
|
|
15
|
+
# A compiled `<param>`, or one `namelist` entry. `kind` is `:expr` when
|
|
16
|
+
# the value is an `expr` and `:location` when it is a location read;
|
|
17
|
+
# `expr` is a {Compiled} (a `namelist` entry's an {Invalid} when it did
|
|
18
|
+
# not compile); `expr_location` is where it was written.
|
|
19
|
+
#
|
|
20
|
+
# @api private
|
|
21
|
+
Param = ::Data.define(:name, :kind, :expr, :expr_location, :location)
|
|
22
|
+
|
|
23
|
+
# A final state's compiled `<donedata>`: its `<content>`'s value as
|
|
24
|
+
# `expr` with `expr_location`, or its {Param}s.
|
|
25
|
+
#
|
|
26
|
+
# @api private
|
|
27
|
+
Donedata = ::Data.define(:location, :expr, :expr_location, :params)
|
|
28
|
+
|
|
29
|
+
# A compiled `<invoke>`. `index` is its position among its state's
|
|
30
|
+
# `<invoke>` elements; `type` and `src` fold a literal attribute and its
|
|
31
|
+
# `expr` twin as a `<send>`'s do; `id` and `idlocation` are as written;
|
|
32
|
+
# `namelist` and `params` hold {Param}s, as a `<send>`'s do; `content` is
|
|
33
|
+
# the `<content>` child's value or nil; `finalize` its {Block}, or nil
|
|
34
|
+
# when none was written.
|
|
35
|
+
#
|
|
36
|
+
# @api private
|
|
37
|
+
Invoke = ::Data.define(
|
|
38
|
+
:index, :location, :type, :src, :id, :idlocation, :namelist, :params, :content, :autoforward,
|
|
39
|
+
:finalize, :attribute_locations
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
class Machine
|
|
5
|
+
# A compiled state. The root at index 0 has the kind `:scxml`, no id and
|
|
6
|
+
# no parent; every other kind is the document's (`:state`, `:parallel`,
|
|
7
|
+
# `:final`, `:history`).
|
|
8
|
+
#
|
|
9
|
+
# - `parent`: the parent's index, nil only for the root;
|
|
10
|
+
# - `last`: the highest index in the state's subtree, so its
|
|
11
|
+
# descendants are `index + 1` to `last`;
|
|
12
|
+
# - `children`: the direct children's indexes in document order, history
|
|
13
|
+
# states included, and `history_children` those that are histories;
|
|
14
|
+
# - `initial`: the states a default entry enters, by the specification's
|
|
15
|
+
# precedence (the `initial` attribute, else the `<initial>` element's
|
|
16
|
+
# one transition's targets, else the first child); empty for every
|
|
17
|
+
# kind but a `<state>` and the root;
|
|
18
|
+
# - `history_type`: `:shallow` or `:deep` on a history, nil otherwise;
|
|
19
|
+
# - `transitions`: the selectable transitions' `t_index`es in document
|
|
20
|
+
# order, empty for a history;
|
|
21
|
+
# - `initial_transition`: the `t_index` of the `<initial>` element's
|
|
22
|
+
# transition, and `history_default` that of a history's default, each
|
|
23
|
+
# nil when absent;
|
|
24
|
+
# - `onentry` and `onexit`: one {Block} per element;
|
|
25
|
+
# - `donedata`: a final state's {Donedata}, or nil;
|
|
26
|
+
# - `data`: the `d_index`es of the `<data>` its own `<datamodel>`
|
|
27
|
+
# declares;
|
|
28
|
+
# - `invoke`: its {Invoke}s in document order.
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
State = ::Data.define(
|
|
32
|
+
:index, :id, :kind, :parent, :last, :children, :initial, :history_type, :history_children,
|
|
33
|
+
:transitions, :onentry, :onexit, :initial_transition, :history_default, :donedata, :data,
|
|
34
|
+
:invoke, :location, :attribute_locations
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# A compiled transition. `source` is the index of the state that holds
|
|
38
|
+
# it; `targets` the target states' indexes; `events` each event
|
|
39
|
+
# descriptor split on its dots (`a.b c` is `[["a", "b"], ["c"]]`); `cond`
|
|
40
|
+
# the {Compiled} condition, or nil when none was written, and
|
|
41
|
+
# `cond_location` where it was written, nil exactly when `cond` is;
|
|
42
|
+
# `type` `:external` or `:internal`; `content` its executable content.
|
|
43
|
+
#
|
|
44
|
+
# @api private
|
|
45
|
+
Transition = ::Data.define(
|
|
46
|
+
:t_index, :source, :targets, :events, :cond, :type, :content, :location, :cond_location,
|
|
47
|
+
:attribute_locations
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# One `<onentry>`, `<onexit>` or `<finalize>` block: its executable
|
|
51
|
+
# content.
|
|
52
|
+
#
|
|
53
|
+
# @api private
|
|
54
|
+
Block = ::Data.define(:location, :content)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
class Machine
|
|
5
|
+
# An expression compiled by `Predicator.compile`: its `instructions`, the
|
|
6
|
+
# list predicator evaluates, and its `source`, as written.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
Compiled = ::Data.define(:instructions, :source)
|
|
10
|
+
|
|
11
|
+
# A statement program (a `<script>` body) compiled by
|
|
12
|
+
# `Predicator.compile_program`: its `instructions`, the list predicator
|
|
13
|
+
# executes, and its `source`, as written.
|
|
14
|
+
#
|
|
15
|
+
# @api private
|
|
16
|
+
Program = ::Data.define(:instructions, :source)
|
|
17
|
+
|
|
18
|
+
# A value fixed when the chart compiled, with nothing to evaluate: a
|
|
19
|
+
# literal attribute, a `<content>` body, or an in-line `<data>` or
|
|
20
|
+
# `<assign>` value.
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
Static = ::Data.define(:value)
|
|
24
|
+
|
|
25
|
+
# An expression or program that did not compile, where the failure is
|
|
26
|
+
# deferred to when it runs: `error` is the {Compiler::Error} the compile
|
|
27
|
+
# answered.
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
Invalid = ::Data.define(:error)
|
|
31
|
+
|
|
32
|
+
# A `<data>` value the host resolves: `src` as written.
|
|
33
|
+
#
|
|
34
|
+
# @api private
|
|
35
|
+
Src = ::Data.define(:src)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
# The compiled chart the interpreter steps: what {Compiler.compile} makes of
|
|
5
|
+
# a validated {Document}, and the `value` of a successful {Statifier.compile}.
|
|
6
|
+
# It is a frozen value, deeply: every node, list and compiled expression in
|
|
7
|
+
# it is frozen, so it may be shared between executions and Ractors.
|
|
8
|
+
#
|
|
9
|
+
# Every state is interned to an index in document order. Index 0 is the
|
|
10
|
+
# `<scxml>` root, made from the document's own fields: the specification
|
|
11
|
+
# treats `<scxml>` as a state when it computes a transition's domain, and
|
|
12
|
+
# giving it an index removes the special case. A state's descendants are
|
|
13
|
+
# exactly the indexes after its own up to and including its `last`, so
|
|
14
|
+
# ancestry and document order are integer comparisons.
|
|
15
|
+
#
|
|
16
|
+
# Transitions and `<data>` elements have their own dense index spaces,
|
|
17
|
+
# `t_index` and `d_index`, assigned in document order. Executable content
|
|
18
|
+
# carries its `c_index` on each node and sits inline in the block,
|
|
19
|
+
# transition, branch or loop body that holds it.
|
|
20
|
+
#
|
|
21
|
+
# The fields: `states`, every {State} by index; `id_to_index`, the index of
|
|
22
|
+
# every state written with a non-empty id; `transitions`, every
|
|
23
|
+
# {Transition} by `t_index`; `data_elements`, every {Data} by `d_index`,
|
|
24
|
+
# the root's own first; `global_scripts`, the `<script>` children of
|
|
25
|
+
# `<scxml>` (each a {Program} or an {Invalid}), run once when the chart
|
|
26
|
+
# starts; `name`, `datamodel` and `binding`, the root's attributes;
|
|
27
|
+
# `location`, the root's span; and `warnings`, the
|
|
28
|
+
# {Validator::Warning}s the chart was compiled with.
|
|
29
|
+
#
|
|
30
|
+
# Its shape is internal and may change in any release: a host passes a
|
|
31
|
+
# machine on and reads one of its members only, `warnings`, the documented
|
|
32
|
+
# exception ({Statifier.compile} says what it holds). Every other member
|
|
33
|
+
# is private.
|
|
34
|
+
#
|
|
35
|
+
# @api private
|
|
36
|
+
Machine = ::Data.define(
|
|
37
|
+
:states, :id_to_index, :transitions, :data_elements, :global_scripts,
|
|
38
|
+
:name, :datamodel, :binding, :location, :warnings
|
|
39
|
+
) do
|
|
40
|
+
# @return [State] the state at `index`
|
|
41
|
+
# @raise [IndexError] when the machine holds no such state
|
|
42
|
+
def state(index) = states.fetch(index)
|
|
43
|
+
|
|
44
|
+
# Whether `descendant` sits inside `ancestor`. Proper: a state is not its
|
|
45
|
+
# own descendant.
|
|
46
|
+
def descendant?(descendant, ancestor) = ancestor < descendant && descendant <= state(ancestor).last
|
|
47
|
+
|
|
48
|
+
# Whether the state has no child states. A final state is atomic.
|
|
49
|
+
def atomic?(index) = state(index).children.empty?
|
|
50
|
+
|
|
51
|
+
# Whether the state is compound: a `<state>` or the root with at least
|
|
52
|
+
# one child. A parallel is never compound.
|
|
53
|
+
def compound?(index)
|
|
54
|
+
node = state(index)
|
|
55
|
+
%i[state scxml].include?(node.kind) && !node.children.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Whether the state is a `<parallel>`.
|
|
59
|
+
def parallel?(index) = state(index).kind == :parallel
|
|
60
|
+
|
|
61
|
+
# Whether the state is a `<final>`.
|
|
62
|
+
def final?(index) = state(index).kind == :final
|
|
63
|
+
|
|
64
|
+
# Whether the state is a `<history>` pseudo-state.
|
|
65
|
+
def history?(index) = state(index).kind == :history
|
|
66
|
+
|
|
67
|
+
# The state's child states in document order, history pseudo-states left
|
|
68
|
+
# out (the specification's `getChildStates`).
|
|
69
|
+
def child_states(index)
|
|
70
|
+
node = state(index)
|
|
71
|
+
node.children - node.history_children
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The state's proper ancestors, nearest first and the root last (the
|
|
75
|
+
# specification's `getProperAncestors` with no bound).
|
|
76
|
+
def proper_ancestors(index)
|
|
77
|
+
ancestors = []
|
|
78
|
+
parent = state(index).parent
|
|
79
|
+
until parent.nil?
|
|
80
|
+
ancestors << parent
|
|
81
|
+
parent = state(parent).parent
|
|
82
|
+
end
|
|
83
|
+
ancestors
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The least common compound ancestor of `indexes` (the specification's
|
|
87
|
+
# `findLCCA`): the nearest proper ancestor of the first index that is
|
|
88
|
+
# compound and holds every index as a descendant, or nil when none does.
|
|
89
|
+
# A parallel never qualifies.
|
|
90
|
+
def lcca(indexes)
|
|
91
|
+
return nil if indexes.empty?
|
|
92
|
+
|
|
93
|
+
proper_ancestors(indexes.first).find do |candidate|
|
|
94
|
+
compound?(candidate) && indexes.all? { descendant?(_1, candidate) }
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
require_relative "machine/values"
|
|
101
|
+
require_relative "machine/states"
|
|
102
|
+
require_relative "machine/content"
|
|
103
|
+
require_relative "machine/data"
|