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,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module Datamodel
5
+ # Reads the stores of a compiled statement program and rewrites them,
6
+ # the mechanism {Program} uses to give a `<script>` the reference's
7
+ # outcome on a published predicator that offers no protected roots and
8
+ # answers no context for a run that stops.
9
+ #
10
+ # The instruction list is predicator's public, versioned shape (the
11
+ # instruction set `Predicator.isa_version` names). A `store n` pops a
12
+ # value and a path of `n` segments, and the path's first segment, its
13
+ # root, is pushed by a `lit` of the root's name, since an assignment's
14
+ # target begins with a name. The stack depth before every instruction is
15
+ # computed by following every jump, so the instruction that pushed a
16
+ # store's root is the last one before the store at the depth the root
17
+ # sits at: everything between keeps the root on the stack.
18
+ #
19
+ # @api private
20
+ module Stores
21
+ # The host function a store into a protected root becomes: it refuses,
22
+ # so the run stops at the attempt. No source can call it: a name in
23
+ # source never holds a colon.
24
+ GUARD = "statifier:protected"
25
+
26
+ # The host function each other store reports its root's new value to.
27
+ NOTE = "statifier:wrote"
28
+
29
+ # The host function a reachable store whose root cannot be read
30
+ # becomes: it refuses, so such a store never writes unguarded. None of
31
+ # predicator's compiled programs has one.
32
+ UNREAD = "statifier:unread"
33
+
34
+ # The jumps, and whether each counts backward.
35
+ JUMPS = { "jump" => false, "pop_jump_if_falsy" => false, "jump_if_falsy_or_pop" => false,
36
+ "jump_if_true_or_pop" => false, "jump_backward" => true }.freeze
37
+
38
+ # The instruction-set version the tables here were written for. On any
39
+ # other version no store is read: every store becomes {UNREAD}.
40
+ ISA_VERSION = 6
41
+
42
+ # The stack effect of each instruction of that version whose effect is
43
+ # fixed, the ones that leave the depth as it is listed too.
44
+ EFFECTS = {
45
+ "lit" => 1, "load" => 1, "object_new" => 1, "duration" => 1, "pop" => -1,
46
+ "compare" => -1, "add" => -1, "subtract" => -1, "multiply" => -1, "divide" => -1, "modulo" => -1,
47
+ "bracket_access" => -1, "in" => -1, "contains" => -1, "object_set" => -1,
48
+ "not" => 0, "unary_bang" => 0, "unary_minus" => 0, "access" => 0, "relative_date" => 0, "cast" => 0
49
+ }.freeze
50
+
51
+ # The instructions whose effect is read from an operand.
52
+ COUNTED = %w[make_list call store].freeze
53
+
54
+ # Every instruction of that version the depths are computed over. A
55
+ # program holding any other (a retired one or a later version's) has no
56
+ # store read: every store becomes {UNREAD}.
57
+ OPCODES = (EFFECTS.keys + COUNTED + JUMPS.keys).freeze
58
+
59
+ module_function
60
+
61
+ # `instructions` with every store into one of `protected_roots` made a
62
+ # call of {GUARD} with its root, a store whose root cannot be read a
63
+ # call of {UNREAD}, and every other store followed by a call of {NOTE}
64
+ # with its root's name and value; every jump moved so it lands where it
65
+ # landed before.
66
+ def guarded(instructions, protected_roots)
67
+ roots = roots(instructions)
68
+ pieces = instructions.each_with_index.map { |instruction, at| piece(instruction, at, roots, protected_roots) }
69
+ starts = pieces.each_with_object([0]) { |piece, acc| acc << (acc.last + piece.size) }
70
+ pieces.each_with_index.flat_map { |piece, at| relocated(piece, at, starts) }
71
+ end
72
+
73
+ # The root each reachable store writes, by the store's index: the
74
+ # name, or nil for a store whose root is not a named `lit` (none of
75
+ # predicator's compiled programs has one), and nil for every store of
76
+ # a program whose stores cannot be read ({readable?}).
77
+ def roots(instructions)
78
+ return unread(instructions) unless readable?(instructions)
79
+
80
+ depths = depths(instructions)
81
+ instructions.each_with_index.filter_map do |(opcode, count), at|
82
+ next unless opcode == "store" && depths[at]
83
+
84
+ [at, root_name(instructions, pusher(depths, at, depths[at] - count - 1))]
85
+ end.to_h
86
+ end
87
+
88
+ # Whether the stores of `instructions` can be read: the instruction set
89
+ # is the version the tables were written for and every instruction is
90
+ # one of {OPCODES}.
91
+ def readable?(instructions)
92
+ Predicator.isa_version == ISA_VERSION && instructions.all? { OPCODES.include?(_1.first) }
93
+ end
94
+
95
+ # Every store of `instructions`, none read.
96
+ def unread(instructions)
97
+ instructions.each_index.select { instructions[_1].first == "store" }.to_h { [_1, nil] }
98
+ end
99
+
100
+ # The last instruction before `at` that a path reaches at `depth` or
101
+ # below: the one that pushed the slot at `depth`.
102
+ def pusher(depths, at, depth) = (at - 1).downto(0).find { depths[_1] && depths[_1] <= depth }
103
+
104
+ # The name the `lit` at `at` pushes, or nil.
105
+ def root_name(instructions, at)
106
+ opcode, name = at && instructions[at]
107
+ name if opcode == "lit" && name.is_a?(String)
108
+ end
109
+
110
+ # The stack depth before each instruction, nil for one no path reaches.
111
+ def depths(instructions)
112
+ depths = Array.new(instructions.size)
113
+ pending = [[0, 0]]
114
+ until pending.empty?
115
+ at, depth = pending.pop
116
+ next if at.negative? || at >= instructions.size || depths[at]
117
+
118
+ depths[at] = depth
119
+ pending.concat(successors(instructions[at], at, depth))
120
+ end
121
+ depths
122
+ end
123
+
124
+ # Where control goes after the instruction at `at`, each with the depth
125
+ # it arrives at.
126
+ def successors((opcode, operand, count), at, depth)
127
+ case opcode
128
+ when "jump" then [[at + operand, depth]]
129
+ when "jump_backward" then [[at - operand, depth]]
130
+ when "pop_jump_if_falsy" then [[at + 1, depth - 1], [at + operand, depth - 1]]
131
+ when "jump_if_falsy_or_pop", "jump_if_true_or_pop" then [[at + 1, depth - 1], [at + operand, depth]]
132
+ else [[at + 1, depth + effect(opcode, operand, count)]]
133
+ end
134
+ end
135
+
136
+ # How an instruction moves the depth.
137
+ def effect(opcode, operand, count)
138
+ case opcode
139
+ when "make_list" then 1 - operand
140
+ when "call" then 1 - count
141
+ when "store" then -1 - operand
142
+ else EFFECTS.fetch(opcode)
143
+ end
144
+ end
145
+
146
+ # What the instruction at `at` becomes, `roots` the stores read.
147
+ def piece(instruction, at, roots, protected_roots)
148
+ return [instruction] unless roots.key?(at)
149
+
150
+ root = roots[at]
151
+ return [["call", UNREAD, 0]] if root.nil?
152
+ return [["lit", root], ["call", GUARD, 1]] if protected_roots.include?(root)
153
+
154
+ [instruction, ["lit", root], ["load", root], ["call", NOTE, 2], ["pop"]]
155
+ end
156
+
157
+ # A piece with its jump moved to the new index of its old target; a
158
+ # target past the end stays past the end.
159
+ def relocated(piece, at, starts)
160
+ opcode, offset = piece.first
161
+ return piece unless JUMPS.key?(opcode)
162
+
163
+ backward = JUMPS.fetch(opcode)
164
+ target = (backward ? at - offset : at + offset).clamp(0, starts.size - 1)
165
+ [[opcode, backward ? starts[at] - starts[target] : starts[target] - starts[at]]]
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module Datamodel
5
+ # The specification's system variables (section 5.10) as datamodel
6
+ # values, as the reference writes them. A field with no value is
7
+ # `Predicator::UNDEFINED`, never nil, which is predicator's null.
8
+ #
9
+ # @api private
10
+ module SystemVariables
11
+ # The SCXML Event I/O Processor's type URI, the key of its
12
+ # `_ioprocessors` entry.
13
+ SCXML_EVENT_PROCESSOR = "http://www.w3.org/TR/scxml/#SCXMLEventProcessor"
14
+
15
+ module_function
16
+
17
+ # The address a session is reached at through the SCXML Event I/O
18
+ # Processor.
19
+ def scxml_location(session_id) = "#_scxml_#{session_id}"
20
+
21
+ # The four system variables before any event. `_sessionid` is the id
22
+ # the host minted; `_name` the chart's name, undefined when it has
23
+ # none; `_event` declared with no value, so a read before the first
24
+ # event answers undefined rather than failing; `_ioprocessors` an entry
25
+ # per registered send type (`entries`, by type) and the SCXML Event I/O
26
+ # Processor's, which a registered type of the same spelling never
27
+ # replaces.
28
+ def initial(session_id:, name:, entries: {})
29
+ {
30
+ "_sessionid" => session_id,
31
+ "_name" => name.nil? ? Predicator::UNDEFINED : name,
32
+ "_event" => Predicator::UNDEFINED,
33
+ "_ioprocessors" => entries.merge(SCXML_EVENT_PROCESSOR => { "location" => scxml_location(session_id) })
34
+ }.freeze
35
+ end
36
+
37
+ # A session's starting datamodel: the host's roots with the system
38
+ # variables over them, so a host value under a system variable's name
39
+ # never survives.
40
+ def seed(host, session_id:, name:, entries: {}) = host.merge(initial(session_id:, name:, entries:)).freeze
41
+
42
+ # The fields of `_event` an event may not carry.
43
+ OPTIONAL_FIELDS = %i[sendid origin origintype invokeid].freeze
44
+
45
+ # `_event`'s value for an event: its name, its type (`:internal`,
46
+ # `:external` or `:platform`) as text, each of {OPTIONAL_FIELDS}
47
+ # (keywords in `optional`) undefined when the event has none, and its
48
+ # data as it is.
49
+ #
50
+ # @raise [ArgumentError] for a keyword that names no field of `_event`
51
+ def event(name:, type:, data:, **optional)
52
+ unknown = optional.keys - OPTIONAL_FIELDS
53
+ raise ArgumentError, "unknown _event fields: #{unknown.inspect}" unless unknown.empty?
54
+
55
+ fields = OPTIONAL_FIELDS.to_h { [_1.to_s, absent(optional[_1])] }
56
+ { "name" => name, "type" => type.to_s, **fields, "data" => data }.freeze
57
+ end
58
+
59
+ # `_event`'s value for `event`, a {Statifier::Event} ({event}).
60
+ def of_event(event) = event(**event.to_h.slice(:name, :type, :data, *OPTIONAL_FIELDS))
61
+
62
+ # The datamodel with `_event` set to `event_value` ({event}).
63
+ def put_event(data, event_value) = data.merge("_event" => event_value).freeze
64
+
65
+ # nil, a field the event does not carry, as undefined.
66
+ def absent(value) = value.nil? ? Predicator::UNDEFINED : value
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "predicator"
4
+
5
+ require_relative "machine"
6
+ require_relative "datamodel/reasons"
7
+ require_relative "datamodel/reason_value"
8
+ require_relative "datamodel/system_variables"
9
+ require_relative "datamodel/stores"
10
+ require_relative "datamodel/program"
11
+ require_relative "datamodel/location"
12
+ require_relative "datamodel/data_binding"
13
+ require_relative "datamodel/delay"
14
+
15
+ module Statifier
16
+ # The datamodel binding: how a chart's expressions reach predicator, the
17
+ # one expression language, as they reach it in the reference.
18
+ #
19
+ # The datamodel is a Hash of String root to predicator value. Every
20
+ # evaluation runs through `Predicator.evaluate` (a condition, a value) or
21
+ # `Predicator.execute` (a `<script>` program) against a {Context}: the
22
+ # datamodel, `In()` as a host function over the configuration the context
23
+ # was built with, the unbound policy {UNBOUND} and the clock the host
24
+ # passed for the call. A context is built per evaluation site and is never
25
+ # kept: `In()` reads a configuration that moves at every microstep, and
26
+ # `_event` is rewritten for every event.
27
+ #
28
+ # Nothing here raises on a failed evaluation. A failure is answered as a
29
+ # value carrying its reason, and the interpreter turns it into the
30
+ # `error.execution` platform event the reference raises (an {Event});
31
+ # the `<data>` binding ({DataBinding}) answers its failures the same way.
32
+ #
33
+ # @api private
34
+ module Datamodel
35
+ # The unbound policy every evaluation passes: a load of a root the
36
+ # datamodel does not hold fails at the load, as the reference evaluates.
37
+ # A declared root with no value holds `Predicator::UNDEFINED` instead,
38
+ # and reads as undefined.
39
+ UNBOUND = :error
40
+
41
+ # One evaluation site's view: `data`, the datamodel; `machine` and
42
+ # `configuration` (the indexes of the active states), which `In()`
43
+ # reads; and `clock`, the host's clock for the call, nil for
44
+ # predicator's own.
45
+ Context = ::Data.define(:data, :machine, :configuration, :clock) do
46
+ # The context with `root` bound to `value`, everything else carried
47
+ # over: safe only inside the evaluation site that built this context.
48
+ def bind(root, value) = with(data: data.merge(root => value).freeze)
49
+
50
+ # The options an evaluation of this context passes to predicator.
51
+ def options = { functions: { "In" => Datamodel.in_state(machine, configuration) }, on_unbound: UNBOUND, clock: }
52
+ end
53
+
54
+ module_function
55
+
56
+ # The context one evaluation site runs against.
57
+ def context(data, machine, configuration, clock: nil) = Context.new(data:, machine:, configuration:, clock:)
58
+
59
+ # `In(stateId)` over `configuration`: true when the id names a state of
60
+ # `machine` that is active, false for an inactive state and for an id
61
+ # the chart never declared, as the reference answers. It takes exactly
62
+ # one argument.
63
+ def in_state(machine, configuration)
64
+ lambda do |state_id|
65
+ index = machine.id_to_index[state_id]
66
+ !index.nil? && configuration.include?(index)
67
+ end
68
+ end
69
+
70
+ # Evaluates `expr` (a {Machine::Compiled}, a {Machine::Static} or a
71
+ # {Machine::Invalid}) against `context`. A static value comes back
72
+ # untouched; a compiled one runs through `Predicator.evaluate`; an
73
+ # invalid one answers the compile refusal it carries, the deferral the
74
+ # reference raises when the node runs.
75
+ #
76
+ # @return [Evaluated] the value, or the reason it failed
77
+ def evaluate(context, expr)
78
+ case expr
79
+ when Machine::Static then Evaluated.new(value: expr.value, error: nil)
80
+ when Machine::Invalid then Evaluated.new(value: nil, error: expr.error)
81
+ else
82
+ result = Predicator.evaluate(expr.instructions, context.data, **context.options)
83
+ error = EvaluatorError.new(source: expr.source, error: result.error) unless result.ok?
84
+ Evaluated.new(value: result.value, error:)
85
+ end
86
+ end
87
+
88
+ # Evaluates a transition's condition: nil (none written) is true. A
89
+ # condition that fails, or answers anything but true or false, answers
90
+ # the failure; the specification treats both as false and requires
91
+ # `error.execution` for both.
92
+ #
93
+ # @return [Evaluated] true or false, or the reason it failed
94
+ def evaluate_cond(context, cond)
95
+ return Evaluated.new(value: true, error: nil) if cond.nil?
96
+
97
+ evaluated = evaluate(context, cond)
98
+ return evaluated if !evaluated.ok? || [true, false].include?(evaluated.value)
99
+
100
+ Evaluated.new(value: nil, error: NonBooleanCond.new(value: evaluated.value))
101
+ end
102
+
103
+ # Runs a `<script>` program against `context` ({Program.run}); an
104
+ # {Machine::Invalid} program answers its compile refusal and writes
105
+ # nothing.
106
+ #
107
+ # @return [Executed] the datamodel the program leaves, and the reason it
108
+ # failed, if it did
109
+ def execute(context, program)
110
+ return Executed.new(data: context.data, error: program.error) if program.is_a?(Machine::Invalid)
111
+
112
+ Program.run(context, program)
113
+ end
114
+
115
+ # Whether `root` is a system variable's: the rule is the underscore
116
+ # prefix, as the reference's, so it covers the four seeded variables and
117
+ # any other root spelled that way.
118
+ def system_root?(root) = root.is_a?(String) && root.start_with?("_")
119
+ end
120
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ class Document
5
+ # A `<datamodel>`: its `<data>` children.
6
+ #
7
+ # @api private
8
+ Datamodel = ::Data.define(:location, :data)
9
+
10
+ # A `<data>`: `id`, `expr` and `src` as written (`expr` and `src` nil
11
+ # when absent), and `text`, the element's own character data as
12
+ # written.
13
+ #
14
+ # @api private
15
+ Data = ::Data.define(:location, :id, :expr, :src, :text, :attribute_locations)
16
+
17
+ # A `<donedata>`: its `<content>` child or nil, and its `<param>`
18
+ # children.
19
+ #
20
+ # @api private
21
+ Donedata = ::Data.define(:location, :content, :params)
22
+
23
+ # A `<param>`: `name` as written, `expr` or nil, and `param_location`,
24
+ # its `location` attribute or nil.
25
+ #
26
+ # @api private
27
+ Param = ::Data.define(:location, :name, :expr, :param_location, :attribute_locations)
28
+
29
+ # A `<content>`: `expr` or nil; `text`, the element's own character data
30
+ # as written; and `markup` and `markup_location`, as an {Assign} has
31
+ # them.
32
+ #
33
+ # @api private
34
+ Content = ::Data.define(:location, :expr, :text, :markup, :markup_location, :attribute_locations)
35
+ end
36
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ class Document
5
+ # A `<raise>`: `event` is the attribute as written, one string, not
6
+ # split.
7
+ #
8
+ # @api private
9
+ Raise = ::Data.define(:location, :event, :attribute_locations)
10
+
11
+ # A `<log>`: `label` and `expr` as written, or nil.
12
+ #
13
+ # @api private
14
+ Log = ::Data.define(:location, :label, :expr, :attribute_locations)
15
+
16
+ # An `<assign>`. `location` is its `location` attribute, the path
17
+ # assigned to; the element's span is `node_location`. `expr` is the
18
+ # attribute or nil. `text` is the element's own character data, as
19
+ # written. `markup` is the source of its element children, from the
20
+ # first child that is not blank text to the last, sliced from the
21
+ # document with its line breaks as they are there, and
22
+ # `markup_location` its span; both are nil when it has no element child.
23
+ #
24
+ # @api private
25
+ Assign = ::Data.define(:location, :node_location, :expr, :text, :markup, :markup_location, :attribute_locations)
26
+
27
+ # An `<if>`: one {Branch} for the `<if>` itself, then one per `<elseif>`
28
+ # and `<else>`, each holding the executable content up to the next.
29
+ #
30
+ # @api private
31
+ If = ::Data.define(:location, :branches)
32
+
33
+ class If
34
+ # One partition of an `<if>`. `location` is the span of the element
35
+ # that opened it (the `<if>`, an `<elseif>` or an `<else>`); `cond` is
36
+ # that element's condition source, nil for an `<else>`.
37
+ #
38
+ # @api private
39
+ Branch = ::Data.define(:location, :cond, :content, :attribute_locations)
40
+ end
41
+
42
+ # A `<foreach>`: `array`, `item` and `index` as written (`index` nil
43
+ # when absent), and its executable content.
44
+ #
45
+ # @api private
46
+ Foreach = ::Data.define(:location, :array, :item, :index, :content, :attribute_locations)
47
+
48
+ # A `<script>`: `text` is its program, the element's own character data
49
+ # as written. A script with `src` does not lower, so `src` is always nil
50
+ # and `attribute_locations` empty.
51
+ #
52
+ # @api private
53
+ Script = ::Data.define(:location, :text, :src, :attribute_locations)
54
+
55
+ # A `<send>`: each attribute as written, or nil, except `namelist`,
56
+ # split on whitespace; its `<param>` children and its `<content>` child.
57
+ #
58
+ # @api private
59
+ Send = ::Data.define(
60
+ :location, :event, :eventexpr, :target, :targetexpr, :type, :typeexpr, :id,
61
+ :idlocation, :delay, :delayexpr, :namelist, :params, :content, :attribute_locations
62
+ )
63
+
64
+ # A `<cancel>`: `sendid` and `sendidexpr` as written, or nil.
65
+ #
66
+ # @api private
67
+ Cancel = ::Data.define(:location, :sendid, :sendidexpr, :attribute_locations)
68
+ end
69
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ class Document
5
+ # An `<invoke>`: each attribute as written, or nil, except `namelist`,
6
+ # split on whitespace, and `autoforward`, true only when written
7
+ # `"true"`; its `<param>` children, its `<content>` child or nil, and its
8
+ # `<finalize>` block or nil (a written empty `<finalize/>` is an empty
9
+ # {Block}, so absent and empty differ).
10
+ #
11
+ # @api private
12
+ Invoke = ::Data.define(
13
+ :location, :type, :typeexpr, :src, :srcexpr, :id, :idlocation, :namelist,
14
+ :autoforward, :params, :content, :finalize, :attribute_locations
15
+ )
16
+ end
17
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ class Document
5
+ # A `<state>`, `<parallel>`, `<final>` or `<history>`: one node for the
6
+ # four, `kind` naming which (`:state`, `:parallel`, `:final`,
7
+ # `:history`), the slots a kind does not use left empty. `id` is the
8
+ # attribute or nil; `initial` is the attribute split on whitespace;
9
+ # `history_type` is `:shallow` or `:deep` on a history (`:shallow` when
10
+ # absent or neither) and nil on every other kind. `onentry` and `onexit`
11
+ # hold one {Block} per element, never flattened.
12
+ #
13
+ # @api private
14
+ State = ::Data.define(
15
+ :kind, :location, :id, :initial, :initial_element, :states, :transitions,
16
+ :onentry, :onexit, :history_type, :donedata, :datamodel_element, :invoke,
17
+ :attribute_locations
18
+ )
19
+
20
+ # An `<initial>`: however many transitions were written, none and two
21
+ # included.
22
+ #
23
+ # @api private
24
+ Initial = ::Data.define(:location, :transitions)
25
+
26
+ # A `<transition>`. `event` and `target` are the attributes split on
27
+ # whitespace, `cond` the condition's source or nil, `type` `:external`
28
+ # or `:internal` (`:external` when absent or neither). `content` holds
29
+ # its executable content directly: a transition has no block element.
30
+ #
31
+ # @api private
32
+ Transition = ::Data.define(:location, :event, :target, :cond, :type, :content, :attribute_locations)
33
+
34
+ # The executable content of one `<onentry>`, `<onexit>` or `<finalize>`
35
+ # element. A written empty element is an empty block.
36
+ #
37
+ # @api private
38
+ Block = ::Data.define(:location, :content)
39
+ end
40
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ # The typed chart: what lowering makes of the reader's element tree, one
5
+ # immutable node per SCXML element, and what the compiler reads. The node
6
+ # for `<scxml>` itself is this class; the nodes for the elements under it
7
+ # are the classes nested in it.
8
+ #
9
+ # Three things hold for every node:
10
+ #
11
+ # - Expressions are source. `cond`, `expr`, `array`, `location` and every
12
+ # other attribute a datamodel evaluates are the text the author wrote,
13
+ # uncompiled.
14
+ # - The tree is not yet validated. It holds what was written, including
15
+ # shapes the specification forbids (a `<send>` with both `event` and
16
+ # `eventexpr`, an `<initial>` with two transitions), for a later check to
17
+ # report.
18
+ # - `attribute_locations` records only what was written: a frozen Hash
19
+ # from an attribute's name, as a Symbol, to the span of its value (the
20
+ # text inside the quotes), holding a key exactly when the author wrote
21
+ # that attribute. A field's value alone cannot say whether it was
22
+ # written, because lowering applies the defaults (`binding` is `:early`,
23
+ # a transition's `type` is `:external`, a history's `type` is
24
+ # `:shallow`).
25
+ #
26
+ # Every node carries the span of the element it was made from
27
+ # (`location`, or `node_location` on an {Assign}, whose `location` is its
28
+ # attribute) as an {XML::Location}. Every list is frozen and in document
29
+ # order. A field for one child element (`datamodel_element`,
30
+ # `initial_element`, `donedata`, `content`, `finalize`) holds the last such
31
+ # child written.
32
+ #
33
+ # The root fields: `name`, `version`, `datamodel` and `xmlns` are the
34
+ # attributes as written, or nil; `namespace` is the namespace the root's
35
+ # name resolves to (it differs from `xmlns` for a prefixed root); `binding`
36
+ # is `:early` or `:late`, `:early` when absent or neither; `initial` is the
37
+ # attribute split on whitespace; `states` holds the `<state>`,
38
+ # `<parallel>` and `<final>` children, `datamodel_element` the
39
+ # `<datamodel>` child or nil, and `scripts` every `<script>` child.
40
+ #
41
+ # @api private
42
+ Document = ::Data.define(
43
+ :location, :name, :version, :xmlns, :namespace, :datamodel, :binding,
44
+ :initial, :states, :datamodel_element, :scripts, :attribute_locations
45
+ )
46
+ end
47
+
48
+ require_relative "document/states"
49
+ require_relative "document/executable"
50
+ require_relative "document/data"
51
+ require_relative "document/invoke"