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