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