statifier 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +601 -16
- data/lib/statifier/accepts.rb +167 -0
- data/lib/statifier/basichttp/decode.rb +142 -0
- data/lib/statifier/basichttp/encoding.rb +224 -0
- data/lib/statifier/basichttp/net_http_transport.rb +157 -0
- data/lib/statifier/basichttp/outbound.rb +87 -0
- data/lib/statifier/basichttp/processor.rb +170 -0
- data/lib/statifier/basichttp/request.rb +94 -0
- data/lib/statifier/basichttp/response.rb +34 -0
- data/lib/statifier/basichttp.rb +47 -0
- data/lib/statifier/compiler/content.rb +131 -0
- data/lib/statifier/compiler/error.rb +30 -0
- data/lib/statifier/compiler/expressions.rb +64 -0
- data/lib/statifier/compiler/numbering.rb +145 -0
- data/lib/statifier/compiler/parts.rb +144 -0
- data/lib/statifier/compiler/walk.rb +61 -0
- data/lib/statifier/compiler.rb +82 -0
- data/lib/statifier/datamodel/data_binding.rb +114 -0
- data/lib/statifier/datamodel/delay.rb +54 -0
- data/lib/statifier/datamodel/location.rb +145 -0
- data/lib/statifier/datamodel/program.rb +82 -0
- data/lib/statifier/datamodel/reason_value.rb +42 -0
- data/lib/statifier/datamodel/reasons.rb +88 -0
- data/lib/statifier/datamodel/stores.rb +169 -0
- data/lib/statifier/datamodel/system_variables.rb +69 -0
- data/lib/statifier/datamodel.rb +120 -0
- data/lib/statifier/document/data.rb +36 -0
- data/lib/statifier/document/executable.rb +69 -0
- data/lib/statifier/document/invoke.rb +17 -0
- data/lib/statifier/document/states.rb +40 -0
- data/lib/statifier/document.rb +51 -0
- data/lib/statifier/driver/calls.rb +183 -0
- data/lib/statifier/driver/checks.rb +110 -0
- data/lib/statifier/driver/delivery.rb +45 -0
- data/lib/statifier/driver/invocations.rb +129 -0
- data/lib/statifier/driver/mailbox.rb +120 -0
- data/lib/statifier/driver/routing.rb +193 -0
- data/lib/statifier/driver/session_id.rb +40 -0
- data/lib/statifier/driver/work.rb +196 -0
- data/lib/statifier/driver.rb +205 -0
- data/lib/statifier/effect.rb +208 -0
- data/lib/statifier/event.rb +67 -0
- data/lib/statifier/frozen.rb +22 -0
- data/lib/statifier/interpreter/blocks.rb +142 -0
- data/lib/statifier/interpreter/content.rb +228 -0
- data/lib/statifier/interpreter/entering.rb +141 -0
- data/lib/statifier/interpreter/exit_entry.rb +174 -0
- data/lib/statifier/interpreter/foreach.rb +83 -0
- data/lib/statifier/interpreter/invoke/arguments.rb +52 -0
- data/lib/statifier/interpreter/invoke/passes.rb +143 -0
- data/lib/statifier/interpreter/invoke.rb +145 -0
- data/lib/statifier/interpreter/loop.rb +170 -0
- data/lib/statifier/interpreter/machine_state.rb +152 -0
- data/lib/statifier/interpreter/macrostep.rb +185 -0
- data/lib/statifier/interpreter/name_match.rb +43 -0
- data/lib/statifier/interpreter/selection.rb +221 -0
- data/lib/statifier/interpreter/send.rb +223 -0
- data/lib/statifier/interpreter/step.rb +138 -0
- data/lib/statifier/interpreter/stop.rb +66 -0
- data/lib/statifier/interpreter/targets.rb +107 -0
- data/lib/statifier/interpreter.rb +57 -0
- data/lib/statifier/lowering/branches.rb +84 -0
- data/lib/statifier/lowering/data.rb +102 -0
- data/lib/statifier/lowering/error.rb +102 -0
- data/lib/statifier/lowering/executable.rb +106 -0
- data/lib/statifier/lowering/placement.rb +65 -0
- data/lib/statifier/lowering/reading.rb +53 -0
- data/lib/statifier/lowering/states.rb +111 -0
- data/lib/statifier/lowering.rb +129 -0
- data/lib/statifier/machine/content.rb +81 -0
- data/lib/statifier/machine/data.rb +42 -0
- data/lib/statifier/machine/states.rb +56 -0
- data/lib/statifier/machine/values.rb +37 -0
- data/lib/statifier/machine.rb +103 -0
- data/lib/statifier/position/rebuild.rb +84 -0
- data/lib/statifier/position/shape.rb +73 -0
- data/lib/statifier/position/text.rb +43 -0
- data/lib/statifier/position.rb +206 -0
- data/lib/statifier/validator/attributes.rb +35 -0
- data/lib/statifier/validator/context.rb +74 -0
- data/lib/statifier/validator/data.rb +136 -0
- data/lib/statifier/validator/error.rb +90 -0
- data/lib/statifier/validator/executable.rb +139 -0
- data/lib/statifier/validator/history.rb +81 -0
- data/lib/statifier/validator/messages.rb +142 -0
- data/lib/statifier/validator/states.rb +161 -0
- data/lib/statifier/validator.rb +94 -0
- data/lib/statifier/version.rb +4 -1
- data/lib/statifier/xml/character_data.rb +142 -0
- data/lib/statifier/xml/cursor.rb +131 -0
- data/lib/statifier/xml/markup.rb +153 -0
- data/lib/statifier/xml/nodes.rb +50 -0
- data/lib/statifier/xml/parser.rb +121 -0
- data/lib/statifier/xml/scope.rb +46 -0
- data/lib/statifier/xml/tags.rb +112 -0
- data/lib/statifier/xml.rb +85 -0
- data/lib/statifier.rb +386 -2
- metadata +129 -20
- data/.gitignore +0 -8
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -11
- data/Gemfile.lock +0 -33
- data/Rakefile +0 -10
- data/bin/console +0 -8
- data/bin/setup +0 -6
- data/statifier.gemspec +0 -34
|
@@ -0,0 +1,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
|