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,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module Compiler
5
+ # The executable content half of the {Walk}: each content node compiled,
6
+ # taking the next content index first, so a container's index precedes
7
+ # its children's.
8
+ #
9
+ # A refusal is one of two things, class by class, as in the reference.
10
+ # A transition's `cond`, a `<log>`'s `expr`, an `<if>` or `<elseif>`
11
+ # condition, a `<foreach>`'s `array`, a `<cancel>`'s `sendidexpr`, and a
12
+ # `<send>`'s expressions, `<content>` and `<param>`s are load-time
13
+ # errors: the chart does not compile. An `<assign>`'s `expr` and a
14
+ # `<script>` body are deferred: the node holds a {Machine::Invalid} that
15
+ # fails when it runs.
16
+ #
17
+ # @api private
18
+ module Content
19
+ # The builder of each kind of content node.
20
+ BUILDERS = {
21
+ Document::Raise => :raise_node, Document::Log => :log, Document::Assign => :assign,
22
+ Document::If => :if_node, Document::Foreach => :foreach, Document::Script => :script,
23
+ Document::Send => :send_node, Document::Cancel => :cancel
24
+ }.freeze
25
+
26
+ # A `<send>`'s attribute pairs, in the order they compile.
27
+ SEND_PAIRS = [%i[event eventexpr], %i[target targetexpr], %i[type typeexpr], %i[delay delayexpr]].freeze
28
+
29
+ # The content of a block, a transition, a branch or a loop body.
30
+ def content_list(nodes) = nodes.map { __send__(BUILDERS.fetch(_1.class), _1, next_c_index) }.freeze
31
+
32
+ private
33
+
34
+ def content_site(c_index, element, attribute, location)
35
+ site({ kind: :content, c_index: }, element, attribute, location)
36
+ end
37
+
38
+ def raise_node(node, c_index) = Machine::Raise.new(c_index:, event: node.event, location: node.location)
39
+
40
+ def log(node, c_index)
41
+ expr_location = node.expr && span(node, :expr)
42
+ expr = node.expr && load_time(:content, node.expr, content_site(c_index, "log", "expr", expr_location))
43
+ Machine::Log.new(c_index:, label: node.label, expr:, location: node.location, expr_location:)
44
+ end
45
+
46
+ # An `<assign>`'s value: its `expr`, a refusal deferred; else its
47
+ # markup children as a string; else its text folded as an in-line
48
+ # value; else nil.
49
+ def assign(node, c_index)
50
+ expr_location = node.expr && span(node, :expr, node.node_location)
51
+ Machine::Assign.new(
52
+ c_index:, location: node.location, node_location: node.node_location, value: assign_value(node, c_index),
53
+ location_location: span(node, :location, node.node_location), expr_location:
54
+ )
55
+ end
56
+
57
+ def assign_value(node, c_index)
58
+ return text_value(node.markup, node.text) if node.expr.nil?
59
+
60
+ site = content_site(c_index, "assign", "expr", span(node, :expr, node.node_location))
61
+ Expressions.deferred(Expressions.compile(node.expr, site))
62
+ end
63
+
64
+ # Every branch's condition compiles, each refusal its own error.
65
+ def if_node(node, c_index)
66
+ branches = node.branches.each_with_index.map { |branch, position| branch(branch, position, c_index) }
67
+ Machine::If.new(c_index:, location: node.location, branches: branches.freeze)
68
+ end
69
+
70
+ # One branch: the `<if>` itself at position 0, then each `<elseif>` and
71
+ # `<else>`.
72
+ def branch(branch, position, c_index)
73
+ cond_location = branch.cond && span(branch, :cond)
74
+ site = content_site(c_index, position.zero? ? "if" : "elseif", "cond", cond_location)
75
+ cond = branch.cond && load_time(:content, branch.cond, site)
76
+ Machine::If::Branch.new(cond:, cond_location:, content: content_list(branch.content))
77
+ end
78
+
79
+ def foreach(node, c_index)
80
+ array_location = span(node, :array)
81
+ array = load_time(:content, node.array, content_site(c_index, "foreach", "array", array_location))
82
+ Machine::Foreach.new(
83
+ c_index:, location: node.location, array:, array_location:, item: node.item,
84
+ item_location: span(node, :item), index: node.index, index_location: node.index && span(node, :index),
85
+ content: content_list(node.content)
86
+ )
87
+ end
88
+
89
+ def script(node, c_index)
90
+ program = Expressions.program(node.text, content_site(c_index, "script", nil, node.location))
91
+ Machine::Script.new(c_index:, program:, node_location: node.location)
92
+ end
93
+
94
+ # A `<send>`: its attribute pairs, then its `<content>`, compile in
95
+ # that order and the first refusal stops the rest, its `<param>`s
96
+ # included; the `<param>`s then each answer their own refusal.
97
+ def send_node(node, c_index)
98
+ owner = { kind: :content, c_index: }
99
+ folded, refusal = send_values(node, owner)
100
+ record_all(:content, [refusal]) if refusal
101
+ Machine::Send.new(
102
+ c_index:, location: node.location, **folded, id: node.id, idlocation: node.idlocation,
103
+ namelist: namelist(node, owner, "send"),
104
+ params: refusal ? Numbering::EMPTY : params(:content, node.params, owner),
105
+ attribute_locations: node.attribute_locations
106
+ )
107
+ end
108
+
109
+ # The folded attribute pairs and `<content>` of a `<send>`, and the
110
+ # first refusal among them.
111
+ def send_values(node, owner)
112
+ folded = { event: nil, target: nil, type: nil, delay: nil, content: nil }
113
+ steps = SEND_PAIRS.map { |names| [names.first, -> { pair(node, names, owner, "send") }] }
114
+ steps << [:content, -> { node.content && content_value(node.content, owner) }]
115
+ steps.each do |key, step|
116
+ value = step.call
117
+ return [folded, value] if value.is_a?(Error)
118
+
119
+ folded[key] = value
120
+ end
121
+ [folded, nil]
122
+ end
123
+
124
+ # A `<cancel>`: `sendid` as a literal, else `sendidexpr` compiled.
125
+ def cancel(node, c_index)
126
+ sendid = record_all(:content, [pair(node, %i[sendid sendidexpr], { kind: :content, c_index: }, "cancel")]).first
127
+ Machine::Cancel.new(c_index:, location: node.location, sendid:, attribute_locations: node.attribute_locations)
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module Compiler
5
+ # Why a chart did not compile: a reason symbol, the detail that reason
6
+ # carries (a frozen Hash), a message for a person, and the span reported.
7
+ # The shape of {Lowering::Error} and {Validator::Error}.
8
+ #
9
+ # The one reason is `:expression_compile_error`, an expression or
10
+ # program that predicator refused. Its detail:
11
+ #
12
+ # - `owner`: the node the expression belongs to, a frozen Hash in the
13
+ # machine's index spaces: `{kind: :transition, t_index:}`,
14
+ # `{kind: :content, c_index:}`, `{kind: :donedata, state_index:}`,
15
+ # `{kind: :data, d_index:}`, `{kind: :global_script, index:}` or
16
+ # `{kind: :invoke, state_index:, invoke_index:}`;
17
+ # - `element` and `attribute`: where it was written, as Strings
18
+ # (`"transition"` and `"cond"`); a `<script>` body, written as the
19
+ # element's text, has the element `"script"` and a nil attribute;
20
+ # - `source`: the expression as written;
21
+ # - `error`: predicator's own `Predicator::ParseError`, its position in
22
+ # the expression's own lines and columns.
23
+ #
24
+ # The span is the attribute's value when one was written, and the element
25
+ # otherwise.
26
+ #
27
+ # @api private
28
+ Error = ::Data.define(:reason, :detail, :message, :location)
29
+ end
30
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module Compiler
5
+ # Every expression of a chart compiles here, once, through predicator: a
6
+ # condition or a value through `Predicator.compile`, a `<script>` body
7
+ # through `Predicator.compile_program`, as the reference compiles them.
8
+ #
9
+ # @api private
10
+ module Expressions
11
+ # Where an expression was written: what a refusal names.
12
+ Site = ::Data.define(:owner, :element, :attribute, :location)
13
+
14
+ module_function
15
+
16
+ # The expression `source` compiled: a {Machine::Compiled}, or the
17
+ # {Error} predicator's refusal makes at `site`.
18
+ def compile(source, site)
19
+ result = Predicator.compile(source)
20
+ result.ok? ? Machine::Compiled.new(instructions: result.value, source:) : refusal(source, result.error, site)
21
+ end
22
+
23
+ # The statement program `source` compiled: a {Machine::Program}, or an
24
+ # {Machine::Invalid} carrying the {Error} predicator's refusal makes at
25
+ # `site`. A program's refusal is always deferred to when it runs.
26
+ def program(source, site)
27
+ result = Predicator.compile_program(source)
28
+ return Machine::Program.new(instructions: result.value, source:) if result.ok?
29
+
30
+ Machine::Invalid.new(error: refusal(source, result.error, site))
31
+ end
32
+
33
+ # `compiled`, or an {Machine::Invalid} carrying it when it is an
34
+ # {Error}: the deferral the reference gives `<assign expr>`,
35
+ # `<data expr>` and a `namelist` entry.
36
+ def deferred(compiled) = compiled.is_a?(Error) ? Machine::Invalid.new(error: compiled) : compiled
37
+
38
+ # A literal value.
39
+ def static(value) = Machine::Static.new(value:)
40
+
41
+ # An in-line value (a `<data>` or `<assign>` body) folded to a literal:
42
+ # the trimmed text compiled and evaluated against an empty context that
43
+ # refuses every unbound name, and the trimmed text itself when either
44
+ # step fails. So `[1, 2]` is a list, `3` a number, and `x` the string
45
+ # `"x"` rather than a read of a variable.
46
+ def inline_value(text)
47
+ trimmed = Lowering::Errors.trim(text)
48
+ compiled = Predicator.compile(trimmed)
49
+ evaluated = compiled.ok? ? Predicator.evaluate(compiled.value, {}, on_unbound: :error) : compiled
50
+ static(evaluated.ok? ? evaluated.value : trimmed)
51
+ end
52
+
53
+ # The {Error} for `parse_error` on `source`, with the reference's
54
+ # message.
55
+ def refusal(source, parse_error, site)
56
+ position = parse_error.position
57
+ message = "failed to compile expression #{Lowering::Errors.quote(source)}: #{parse_error.message} " \
58
+ "(predicator line #{position.line}, column #{position.column})"
59
+ detail = { owner: site.owner, element: site.element, attribute: site.attribute, source:, error: parse_error }
60
+ Error.new(reason: :expression_compile_error, detail:, message:, location: site.location)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module Compiler
5
+ # The state half of the {Walk}. A state takes its index when the walk
6
+ # reaches it, before its children; then its own `<onentry>` and
7
+ # `<onexit>` content, its transitions (a history's are its default; any
8
+ # other kind's plain transitions come before its `<initial>` element's)
9
+ # with their content, its `<donedata>`, its `<data>`, and its `<invoke>`
10
+ # elements with their `<finalize>` content are numbered before the walk
11
+ # descends. A transition's targets can name a state the walk has not
12
+ # reached yet, so targets are resolved, and conditions compiled, once the
13
+ # walk is done.
14
+ #
15
+ # @api private
16
+ module Numbering
17
+ EMPTY = [].freeze
18
+
19
+ # The parts the root has none of.
20
+ ROOT_PARTS = {
21
+ transitions: EMPTY, onentry: EMPTY, onexit: EMPTY, initial_transition: nil, history_default: nil,
22
+ donedata: nil, invoke: EMPTY
23
+ }.freeze
24
+
25
+ PendingTransition = ::Data.define(:transition, :source, :t_index, :content)
26
+
27
+ # The fields a compiled state copies from its document node.
28
+ COPIED = %i[id kind history_type location attribute_locations].freeze
29
+
30
+ private
31
+
32
+ # Numbers one group of sibling states and their subtrees, depth first,
33
+ # from `index`; answers the group's indexes and the first index after
34
+ # the whole group.
35
+ def siblings(states, index, parent)
36
+ indexes = states.map do |state|
37
+ own = index
38
+ index = numbered(state, own, parent)
39
+ own
40
+ end
41
+ [indexes.freeze, index]
42
+ end
43
+
44
+ # Numbers `state` at `index` and its subtree; answers the first index
45
+ # after the subtree.
46
+ def numbered(state, index, parent)
47
+ own = own_parts(state, index)
48
+ children, after = siblings(state.states, index + 1, index)
49
+ @states[index] = Machine::State.new(
50
+ index:, parent:, last: after - 1, children:, **own, **state.to_h.slice(*COPIED),
51
+ initial: resolve_initial(state, children), history_children: histories(children)
52
+ )
53
+ @id_to_index[state.id] = index unless state.id.nil?
54
+ after
55
+ end
56
+
57
+ # The parts of a state numbered before its children, in the order the
58
+ # walk numbers them.
59
+ def own_parts(state, index)
60
+ parts = { onentry: blocks(state.onentry), onexit: blocks(state.onexit) }
61
+ parts.merge!(transition_parts(state, index))
62
+ parts.merge!(data_parts(state, index))
63
+ end
64
+
65
+ # A state's `<donedata>`, then its `<data>`, then its `<invoke>`s.
66
+ def data_parts(state, index)
67
+ donedata = state.donedata && build_donedata(state.donedata, index)
68
+ assign_data(state.datamodel_element, index)
69
+ invoke = state.invoke.each_with_index.map { |node, at| build_invoke(node, index, at) }.freeze
70
+ { donedata:, data: owned_data(index), invoke: }
71
+ end
72
+
73
+ # A history's transitions are its default; any other kind's plain
74
+ # transitions are selectable, and its `<initial>` element's follow.
75
+ def transition_parts(state, index)
76
+ plain = numbered_transitions(state.transitions, index)
77
+ return { transitions: EMPTY, initial_transition: nil, history_default: plain.first } if state.kind == :history
78
+
79
+ initial = numbered_transitions(state.initial_element&.transitions || EMPTY, index)
80
+ { transitions: plain, initial_transition: initial.first, history_default: nil }
81
+ end
82
+
83
+ def numbered_transitions(transitions, source)
84
+ transitions.map do |transition|
85
+ content = content_list(transition.content)
86
+ @pending << PendingTransition.new(transition:, source:, t_index: @pending.size, content:)
87
+ @pending.size - 1
88
+ end.freeze
89
+ end
90
+
91
+ def build_transition(pending)
92
+ transition = pending.transition
93
+ fields = pending.to_h.slice(:t_index, :source, :content)
94
+ .merge(transition.to_h.slice(:type, :location, :attribute_locations))
95
+ Machine::Transition.new(
96
+ targets: resolve(transition.target), events: events(transition.event),
97
+ **transition_cond(transition, pending.t_index), **fields
98
+ )
99
+ end
100
+
101
+ # Each event descriptor split on its dots, an empty part kept.
102
+ def events(descriptors) = descriptors.map { _1.split(".", -1).map(&:freeze).freeze }.freeze
103
+
104
+ # A transition's condition compiled, a load-time error, and where it
105
+ # was written; both nil when none was.
106
+ def transition_cond(transition, t_index)
107
+ return { cond: nil, cond_location: nil } if transition.cond.nil?
108
+
109
+ cond_location = span(transition, :cond)
110
+ site = site({ kind: :transition, t_index: }, "transition", "cond", cond_location)
111
+ { cond: load_time(:transitions, transition.cond, site), cond_location: }
112
+ end
113
+
114
+ # A `<state>`'s default entry, in the specification's precedence.
115
+ # Only a `<state>` has one: a parallel enters every child, and a final
116
+ # or history is never entered by default.
117
+ def resolve_initial(state, children)
118
+ return EMPTY unless state.kind == :state
119
+ return resolve(state.initial) unless state.initial.empty?
120
+
121
+ written = state.initial_element&.transitions || EMPTY
122
+ written.size == 1 ? resolve(written.first.target) : children.first(1).freeze
123
+ end
124
+
125
+ # The indexes of `ids`; the validator has resolved every one.
126
+ def resolve(ids) = ids.map { @id_to_index.fetch(_1) }.freeze
127
+
128
+ def histories(children) = children.select { @states.fetch(_1).kind == :history }.freeze
129
+
130
+ def blocks(blocks) = blocks.map { block(_1) }.freeze
131
+
132
+ def block(block) = Machine::Block.new(location: block.location, content: content_list(block.content))
133
+
134
+ def assign_data(datamodel, state_index)
135
+ datamodel&.data&.each do |node|
136
+ d_index = @data.size
137
+ @data << data_element(node, d_index)
138
+ @data_by_state[state_index] << d_index
139
+ end
140
+ end
141
+
142
+ def owned_data(index) = @data_by_state.fetch(index, EMPTY).dup.freeze
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module Compiler
5
+ # The data half of the {Walk}: `<data>`, `<donedata>`, `<param>`,
6
+ # `<content>`, `namelist` and `<invoke>`, and the helpers every half
7
+ # shares. A `<data>`'s `expr` and a `namelist` entry defer their refusal
8
+ # to when they run; every other expression here is a load-time error.
9
+ #
10
+ # @api private
11
+ module Parts
12
+ private
13
+
14
+ def site(owner, element, attribute, location) = Expressions::Site.new(owner:, element:, attribute:, location:)
15
+
16
+ # Where `node` wrote the attribute `key`: its value's span, else
17
+ # `fallback`, the element's span by default.
18
+ def span(node, key, fallback = node.location) = node.attribute_locations[key] || fallback
19
+
20
+ # `source` compiled at `site`, or nil with the refusal recorded in
21
+ # `group`: a load-time error.
22
+ def load_time(group, source, site)
23
+ compiled = Expressions.compile(source, site)
24
+ return compiled unless compiled.is_a?(Error)
25
+
26
+ @groups.fetch(group) << compiled
27
+ nil
28
+ end
29
+
30
+ # The values among `built`, each {Error} among them recorded in
31
+ # `group`.
32
+ def record_all(group, built)
33
+ refused, kept = built.partition { _1.is_a?(Error) }
34
+ @groups.fetch(group).concat(refused)
35
+ kept.freeze
36
+ end
37
+
38
+ # A literal attribute folded with its `expr` twin: a static value for
39
+ # the literal (the validator refuses a chart that writes both), the
40
+ # twin compiled (a compiled value or an {Error}), or nil for neither.
41
+ def pair(node, (literal, twin), owner, element)
42
+ written = node.public_send(literal)
43
+ return Expressions.static(written) unless written.nil?
44
+
45
+ source = node.public_send(twin)
46
+ source && Expressions.compile(source, site(owner, element, twin.to_s, span(node, twin)))
47
+ end
48
+
49
+ # A body with no `expr`: its markup children as a string, else its
50
+ # text folded as an in-line value, else nil.
51
+ def text_value(markup, text)
52
+ return Expressions.static(markup) if markup
53
+ return Expressions.static(nil) if Lowering::Reading.blank?(text)
54
+
55
+ Expressions.inline_value(text)
56
+ end
57
+
58
+ # A `<content>`'s value: its `expr` compiled (a compiled value or an
59
+ # {Error}), else its markup children, else its text, as written.
60
+ def content_value(content, owner)
61
+ return Expressions.static(content.markup || content.text) if content.expr.nil?
62
+
63
+ Expressions.compile(content.expr, site(owner, "content", "expr", span(content, :expr)))
64
+ end
65
+
66
+ # A `<param>`: its `expr` when it has no `location`, else its
67
+ # `location`, compiled; a {Machine::Param} or an {Error}. The
68
+ # validator guarantees one of the two.
69
+ def param(node, owner)
70
+ kind = node.expr && node.param_location.nil? ? :expr : :location
71
+ expr_location = span(node, node.expr.nil? ? :location : :expr)
72
+ compiled = Expressions.compile(kind == :expr ? node.expr : node.param_location,
73
+ site(owner, "param", kind.to_s, expr_location))
74
+ return compiled if compiled.is_a?(Error)
75
+
76
+ Machine::Param.new(name: node.name, kind:, expr: compiled, expr_location:, location: node.location)
77
+ end
78
+
79
+ def params(group, nodes, owner) = record_all(group, nodes.map { param(_1, owner) })
80
+
81
+ # A `namelist`'s entries, each a location read by its own name, its
82
+ # refusal deferred. The whole attribute shares one span.
83
+ def namelist(node, owner, element)
84
+ expr_location = span(node, :namelist)
85
+ node.namelist.map do |name|
86
+ expr = Expressions.deferred(Expressions.compile(name, site(owner, element, "namelist", expr_location)))
87
+ Machine::Param.new(name:, kind: :location, expr:, expr_location:, location: node.location)
88
+ end.freeze
89
+ end
90
+
91
+ # A `<data>`'s value, in the specification's precedence: `expr` (its
92
+ # refusal deferred), then `src`, then its text, then nil.
93
+ def data_element(node, d_index)
94
+ value, value_location = data_value(node, d_index)
95
+ Machine::Data.new(d_index:, id: node.id, value:, location: node.location, value_location:)
96
+ end
97
+
98
+ def data_value(node, d_index)
99
+ if node.expr
100
+ location = span(node, :expr)
101
+ [Expressions.deferred(Expressions.compile(node.expr, site({ kind: :data, d_index: }, "data", "expr",
102
+ location))), location]
103
+ elsif node.src
104
+ [Machine::Src.new(src: node.src), span(node, :src)]
105
+ else
106
+ [text_value(nil, node.text), node.location]
107
+ end
108
+ end
109
+
110
+ # A final state's `<donedata>`: its `<content>`'s value, or its
111
+ # `<param>`s.
112
+ def build_donedata(node, state_index)
113
+ owner = { kind: :donedata, state_index: }
114
+ content = node.content
115
+ if content.nil?
116
+ return Machine::Donedata.new(location: node.location, expr: nil, expr_location: nil,
117
+ params: params(:donedata, node.params, owner))
118
+ end
119
+
120
+ expr = record_all(:donedata, [content_value(content, owner)]).first
121
+ expr_location = content.expr ? span(content, :expr) : content.markup_location || content.location
122
+ Machine::Donedata.new(location: node.location, expr:, expr_location:, params: Numbering::EMPTY)
123
+ end
124
+
125
+ def build_invoke(node, state_index, invoke_index)
126
+ owner = { kind: :invoke, state_index:, invoke_index: }
127
+ Machine::Invoke.new(
128
+ index: invoke_index, **invoke_values(node, owner), namelist: namelist(node, owner, "invoke"),
129
+ params: params(:invoke, node.params, owner), finalize: node.finalize && block(node.finalize),
130
+ **node.to_h.slice(:location, :id, :idlocation, :autoforward, :attribute_locations)
131
+ )
132
+ end
133
+
134
+ # An `<invoke>`'s `type` and `src` pairs and its `<content>`, in that
135
+ # order, each a load-time error.
136
+ def invoke_values(node, owner)
137
+ values = { type: pair(node, %i[type typeexpr], owner, "invoke"),
138
+ src: pair(node, %i[src srcexpr], owner, "invoke") }
139
+ values[:content] = node.content && content_value(node.content, owner)
140
+ values.transform_values { record_all(:invoke, [_1]).first }
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Statifier
4
+ module Compiler
5
+ # The numbering walk over one chart: made by {Compiler.compile}, used
6
+ # once and discarded. It numbers every state, transition, content node
7
+ # and `<data>` in document order and collects the refusals in four
8
+ # groups (the transitions', the content's, the donedata's and the
9
+ # invokes'), which {Compiler.compile} merges.
10
+ #
11
+ # @api private
12
+ class Walk
13
+ include Numbering
14
+ include Content
15
+ include Parts
16
+
17
+ # The pieces the walk has numbered.
18
+ attr_reader :id_to_index, :states, :data
19
+
20
+ # The refusals, by group, in the order the groups merge.
21
+ attr_reader :groups
22
+
23
+ def initialize
24
+ @id_to_index = {}
25
+ @states = []
26
+ @pending = []
27
+ @data = []
28
+ @data_by_state = Hash.new { |hash, key| hash[key] = [] }
29
+ @c_next = 0
30
+ @groups = { transitions: [], content: [], donedata: [], invoke: [] }
31
+ end
32
+
33
+ # Numbers the whole chart, the root at index 0, the root's own `<data>`
34
+ # first.
35
+ def root(document)
36
+ assign_data(document.datamodel_element, 0)
37
+ children, after = siblings(document.states, 1, 0)
38
+ @states[0] = Machine::State.new(
39
+ index: 0, id: nil, kind: :scxml, parent: nil, last: after - 1, children:, history_type: nil,
40
+ initial: root_initial(document.initial, children), history_children: histories(children), **ROOT_PARTS,
41
+ data: owned_data(0),
42
+ **document.to_h.slice(:location, :attribute_locations)
43
+ )
44
+ end
45
+
46
+ # The root's default entry: its `initial` attribute, else its first
47
+ # child.
48
+ def root_initial(ids, children) = ids.empty? ? children.first(1).freeze : resolve(ids)
49
+
50
+ # Every transition, built now that every id has its index.
51
+ def transitions = @pending.map { build_transition(_1) }.freeze
52
+
53
+ # The next content index; a container takes its own before its
54
+ # children's.
55
+ def next_c_index
56
+ @c_next += 1
57
+ @c_next - 1
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "predicator"
4
+
5
+ require_relative "document"
6
+ require_relative "machine"
7
+ require_relative "validator"
8
+ require_relative "compiler/error"
9
+ require_relative "compiler/expressions"
10
+ require_relative "compiler/numbering"
11
+ require_relative "compiler/content"
12
+ require_relative "compiler/parts"
13
+ require_relative "compiler/walk"
14
+
15
+ module Statifier
16
+ # The validated chart in, the {Machine} out: the fourth step of reading a
17
+ # chart, a port of the reference's compiler.
18
+ #
19
+ # One walk ({Walk}) interns every state, transition, content node and
20
+ # `<data>` to an index in document order, and every expression compiles
21
+ # once, through predicator: conditions and values through
22
+ # `Predicator.compile`, `<script>` bodies through
23
+ # `Predicator.compile_program`. An expression predicator refuses is an
24
+ # {Error} with the reason `:expression_compile_error`, or, for the classes
25
+ # the reference defers (an `<assign>`'s `expr`, a `<data>`'s `expr`, a
26
+ # `namelist` entry and a `<script>` body), a {Machine::Invalid} on the
27
+ # node that fails when it runs.
28
+ #
29
+ # Collect-all: every load-time refusal is answered, in document order,
30
+ # and the machine only when there is none.
31
+ #
32
+ # @api private
33
+ module Compiler
34
+ # What {Compiler.compile} answers: the {Machine} as `value`, or every
35
+ # {Error}, in document order, as `errors`.
36
+ Result = ::Data.define(:value, :errors) do
37
+ # @return [Boolean] true when the chart compiled
38
+ def ok? = errors.empty?
39
+ end
40
+
41
+ # Compiles a validated chart into its machine.
42
+ #
43
+ # @param document [Document] the chart {Validator.validate} answered
44
+ # @param warnings [Array<Validator::Warning>] the validator's warnings,
45
+ # kept on the machine
46
+ # @return [Result] the frozen {Machine}, or the {Error}s
47
+ def self.compile(document, warnings: [])
48
+ walk = Walk.new
49
+ walk.root(document)
50
+ transitions = walk.transitions
51
+ errors = sorted(walk.groups.values.flatten)
52
+ return Result.new(value: nil, errors: Ractor.make_shareable(errors)) unless errors.empty?
53
+
54
+ Result.new(value: Ractor.make_shareable(machine(walk, transitions, document, warnings)), errors: Walk::EMPTY)
55
+ end
56
+
57
+ # The machine of a walk that found no refusal.
58
+ def self.machine(walk, transitions, document, warnings)
59
+ Machine.new(
60
+ states: walk.states.freeze, id_to_index: walk.id_to_index.freeze, transitions:,
61
+ data_elements: walk.data.freeze, global_scripts: global_scripts(document.scripts),
62
+ **document.to_h.slice(:name, :datamodel, :binding, :location), warnings: warnings.dup.freeze
63
+ )
64
+ end
65
+
66
+ # The `<script>` children of `<scxml>`, each compiled as a program.
67
+ def self.global_scripts(scripts)
68
+ scripts.each_with_index.map do |script, index|
69
+ site = Expressions::Site.new(owner: { kind: :global_script, index: }, element: "script", attribute: nil,
70
+ location: script.location)
71
+ Expressions.program(script.text, site)
72
+ end.freeze
73
+ end
74
+
75
+ # The errors in document order, the groups' order kept at one offset.
76
+ def self.sorted(errors)
77
+ errors.each_with_index.sort_by { |error, index| [error.location.start_offset, index] }.map(&:first)
78
+ end
79
+
80
+ private_class_method :machine, :global_scripts, :sorted
81
+ end
82
+ end