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,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Datamodel
|
|
5
|
+
# The `<data>` binding, as the reference binds (the specification's
|
|
6
|
+
# sections 5.3.2 and 5.3.3 and B.2.2; its appendix gives no procedure
|
|
7
|
+
# body).
|
|
8
|
+
#
|
|
9
|
+
# When the chart starts every `<data>` id is created, as undefined, and
|
|
10
|
+
# what binds then binds: every `<data>` under early binding, the root's
|
|
11
|
+
# own under late binding. A root `<data>` whose id the host's datamodel
|
|
12
|
+
# already held keeps the host's value. Under late binding a state's own
|
|
13
|
+
# `<data>` binds when the state is first entered; the caller decides
|
|
14
|
+
# that it is the first entry.
|
|
15
|
+
#
|
|
16
|
+
# One context serves a whole pass, since no `<data>` may read another in
|
|
17
|
+
# the same pass. A value that fails (an `expr` that did not compile, a
|
|
18
|
+
# `src`, which nothing here fetches, or an evaluation that fails) leaves
|
|
19
|
+
# the id undefined and answers a {Failure}, which the interpreter raises
|
|
20
|
+
# as `error.execution` with the origin `{kind: :data, d_index:}`.
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
module DataBinding
|
|
24
|
+
# What a binding pass answers: `data`, the datamodel it leaves;
|
|
25
|
+
# `changes`, a {Change} per `<data>` that bound; and `failures`, a
|
|
26
|
+
# {Failure} per `<data>` that did not; both in `d_index` order.
|
|
27
|
+
Bound = ::Data.define(:data, :changes, :failures)
|
|
28
|
+
|
|
29
|
+
# One `<data>` bound: its `d_index` and `id`, the value the id held
|
|
30
|
+
# before (`Predicator::UNDEFINED` once seeded) and the value it holds
|
|
31
|
+
# now.
|
|
32
|
+
Change = ::Data.define(:d_index, :id, :prior_value, :new_value)
|
|
33
|
+
|
|
34
|
+
# One `<data>` that failed to bind: its `d_index` and the `reason`
|
|
35
|
+
# its `error.execution` carries.
|
|
36
|
+
Failure = ::Data.define(:d_index, :reason)
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
# The binding when the chart starts. `context.data` is the host's
|
|
41
|
+
# datamodel with the system variables seeded.
|
|
42
|
+
#
|
|
43
|
+
# @return [Bound]
|
|
44
|
+
def start(context)
|
|
45
|
+
bind_all(context.with(data: seed(context)), starting_indexes(context.machine, context.data.keys))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The datamodel with every `<data>` id the host did not supply
|
|
49
|
+
# declared, as undefined: what the chart starts with before anything
|
|
50
|
+
# binds.
|
|
51
|
+
def seed(context)
|
|
52
|
+
context.machine.data_elements.reduce(context.data) do |data, element|
|
|
53
|
+
data.key?(element.id) ? data : data.merge(element.id => Predicator::UNDEFINED)
|
|
54
|
+
end.freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The `<data>` that bind when the chart starts: every one under early
|
|
58
|
+
# binding, the root's own under late, less the root's whose id the
|
|
59
|
+
# host supplied.
|
|
60
|
+
def starting_indexes(machine, host_ids)
|
|
61
|
+
root_data = machine.state(0).data
|
|
62
|
+
indexes = machine.binding == :early ? machine.data_elements.map(&:d_index) : root_data
|
|
63
|
+
indexes.reject { root_data.include?(_1) && host_ids.include?(machine.data_elements[_1].id) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The binding of `state_index`'s own `<data>` on its first entry:
|
|
67
|
+
# nothing under early binding, and nothing for the root, whose own
|
|
68
|
+
# bound when the chart started (binding it again would overwrite a
|
|
69
|
+
# value the host supplied).
|
|
70
|
+
#
|
|
71
|
+
# @return [Bound]
|
|
72
|
+
def enter_state(context, state_index)
|
|
73
|
+
machine = context.machine
|
|
74
|
+
return unbound(context) if machine.binding == :early || state_index.zero?
|
|
75
|
+
|
|
76
|
+
bind_all(context, machine.state(state_index).data)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# A pass that binds nothing.
|
|
80
|
+
def unbound(context) = Bound.new(data: context.data, changes: [].freeze, failures: [].freeze)
|
|
81
|
+
|
|
82
|
+
# Each of `indexes` bound in ascending order against the one context.
|
|
83
|
+
def bind_all(context, indexes)
|
|
84
|
+
data, changes, failures = indexes.sort.reduce([context.data, [], []]) do |bound, index|
|
|
85
|
+
bind_one(context, bound, index)
|
|
86
|
+
end
|
|
87
|
+
Bound.new(data: data.freeze, changes: changes.freeze, failures: failures.freeze)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The pass so far, `[data, changes, failures]`, with the `<data>` at
|
|
91
|
+
# `d_index` bound, or its failure recorded.
|
|
92
|
+
def bind_one(context, (data, changes, failures), d_index)
|
|
93
|
+
element = context.machine.data_elements[d_index]
|
|
94
|
+
evaluated = value_of(context, element.value)
|
|
95
|
+
return [data, changes, failures + [Failure.new(d_index:, reason: evaluated.error)]] unless evaluated.ok?
|
|
96
|
+
|
|
97
|
+
[data.merge(element.id => evaluated.value), changes + [change(data, element, evaluated.value)], failures]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The {Change} binding `element` to `value` makes over `data`.
|
|
101
|
+
def change(data, element, value)
|
|
102
|
+
Change.new(d_index: element.d_index, id: element.id, prior_value: data.fetch(element.id, Predicator::UNDEFINED),
|
|
103
|
+
new_value: value)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# A `<data>` value evaluated; a `src` fails, as nothing here fetches it.
|
|
107
|
+
def value_of(context, value)
|
|
108
|
+
return Evaluated.new(value: nil, error: SrcValue.new(src: value.src)) if value.is_a?(Machine::Src)
|
|
109
|
+
|
|
110
|
+
Datamodel.evaluate(context, value)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Datamodel
|
|
5
|
+
# A `<send>` delay in whole milliseconds, read through predicator's own
|
|
6
|
+
# duration parser (`Predicator.parse_duration`), as the reference reads
|
|
7
|
+
# it through its expression language's: one duration vocabulary, so the
|
|
8
|
+
# units are predicator's eight (`y`, `mo`, `w`, `d`, `h`, `m`, `s`,
|
|
9
|
+
# `ms`), a superset of the five the SCXML schema names.
|
|
10
|
+
#
|
|
11
|
+
# The one rewrite is the reference's: the schema admits a bare leading
|
|
12
|
+
# dot (`.5s`) and the parser does not, so a leading `.` becomes `0.`.
|
|
13
|
+
# The sum is the reference's expression language's conversion: a month
|
|
14
|
+
# is thirty days and a year three hundred and sixty five.
|
|
15
|
+
#
|
|
16
|
+
# @api private
|
|
17
|
+
module Delay
|
|
18
|
+
# Each unit's worth in milliseconds.
|
|
19
|
+
WEIGHTS = {
|
|
20
|
+
years: 31_536_000_000, months: 2_592_000_000, weeks: 604_800_000, days: 86_400_000,
|
|
21
|
+
hours: 3_600_000, minutes: 60_000, seconds: 1_000, milliseconds: 1
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# `value` (a delay's text, or what a `delayexpr` evaluated to) in
|
|
27
|
+
# milliseconds. Text is parsed; a `Predicator::Duration` is summed. A
|
|
28
|
+
# map, a date or an instant is 0, as the reference reads any map, which
|
|
29
|
+
# carries none of its duration keys; anything else, and text the parser
|
|
30
|
+
# refuses, is an {InvalidDelay} carrying the value as given.
|
|
31
|
+
#
|
|
32
|
+
# @return [Evaluated] the milliseconds, or the refusal
|
|
33
|
+
def milliseconds(value)
|
|
34
|
+
case value
|
|
35
|
+
when String then parsed(value)
|
|
36
|
+
when Predicator::Duration then Evaluated.new(value: total(value), error: nil)
|
|
37
|
+
when Hash, Predicator::Date, Predicator::DateTime then Evaluated.new(value: 0, error: nil)
|
|
38
|
+
else Evaluated.new(value: nil, error: InvalidDelay.new(value:))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Delay text parsed after the leading-dot rewrite.
|
|
43
|
+
def parsed(text)
|
|
44
|
+
parsed = Predicator.parse_duration(text.start_with?(".") ? "0#{text}" : text)
|
|
45
|
+
return Evaluated.new(value: nil, error: InvalidDelay.new(value: text)) unless parsed.ok?
|
|
46
|
+
|
|
47
|
+
Evaluated.new(value: total(parsed.value), error: nil)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A duration's milliseconds.
|
|
51
|
+
def total(duration) = WEIGHTS.sum { |unit, weight| duration.public_send(unit) * weight }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Datamodel
|
|
5
|
+
# A write of a value at a location, as an `<assign>` makes one, in the
|
|
6
|
+
# reference's order: the location resolves to a path; a root that begins
|
|
7
|
+
# with an underscore is refused; a root the datamodel does not hold is
|
|
8
|
+
# refused; the value the path holds is read; then the value is written
|
|
9
|
+
# at the path. A write never declares a root, and creates only what lies
|
|
10
|
+
# between the root and the leaf, as predicator's store does.
|
|
11
|
+
#
|
|
12
|
+
# The published predicator has no location surface of its own, so the
|
|
13
|
+
# location is read through its compiler: the text compiled as the
|
|
14
|
+
# program `location = null` is one assignment whose target segments are
|
|
15
|
+
# the instructions between the root's `lit` and the value's. Text that
|
|
16
|
+
# is not exactly one assignment target is refused as an
|
|
17
|
+
# {InvalidLocation}. A bracket key is taken only in the forms the
|
|
18
|
+
# reference's location reader takes (a string literal, an integer
|
|
19
|
+
# literal, a negative integer literal, an identifier); any other key is
|
|
20
|
+
# refused as a {ComputedKey}, as the reference refuses it.
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
module Location
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# Writes `value` at `location` (its text) in `context`'s datamodel.
|
|
27
|
+
#
|
|
28
|
+
# @return [Written] the context with the written root bound, the path
|
|
29
|
+
# and the value it held before, or the reason the write was refused
|
|
30
|
+
def write(context, location, value)
|
|
31
|
+
target = target(location)
|
|
32
|
+
return target if target.is_a?(Written)
|
|
33
|
+
|
|
34
|
+
resolved = Predicator.evaluate(target, context.data, **context.options)
|
|
35
|
+
return refused(EvaluatorError.new(source: location, error: resolved.error)) unless resolved.ok?
|
|
36
|
+
|
|
37
|
+
checked(context, location, resolved.value, value)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The instructions that push the location's path as one list, or the
|
|
41
|
+
# refusal of text that is not one assignment target.
|
|
42
|
+
def target(location)
|
|
43
|
+
compiled = Predicator.compile_program("#{location} = null")
|
|
44
|
+
return refused(EvaluatorError.new(source: location, error: compiled.error)) unless compiled.ok?
|
|
45
|
+
|
|
46
|
+
shape_refusal(compiled.value, location) || (compiled.value[0...-2] + [["make_list", compiled.value.last[1]]])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The refusal of compiled `location = null` instructions that are not
|
|
50
|
+
# one assignment target, or whose target has a computed key; nil when
|
|
51
|
+
# they are neither.
|
|
52
|
+
def shape_refusal(instructions, location)
|
|
53
|
+
return refused(InvalidLocation.new(location:)) unless one_target?(instructions)
|
|
54
|
+
|
|
55
|
+
refused(ComputedKey.new(location:)) unless plain_keys?(instructions[1...-2])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Whether `instructions` are one store of null whose root is the first
|
|
59
|
+
# instruction and stays on the stack to the store, so nothing but the
|
|
60
|
+
# target runs.
|
|
61
|
+
def one_target?(instructions)
|
|
62
|
+
return false unless Stores.readable?(instructions)
|
|
63
|
+
|
|
64
|
+
depths = Stores.depths(instructions)
|
|
65
|
+
instructions.one? { _1.first == "store" } && instructions.last.first == "store" &&
|
|
66
|
+
instructions[-2] == ["lit", nil] && !Stores.root_name(instructions, 0).nil? &&
|
|
67
|
+
depths[1...-1].all? { _1&.positive? }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Whether every segment after the root is one the reference takes as a
|
|
71
|
+
# key: a name or a string literal (one `lit` of a String), an integer
|
|
72
|
+
# literal (one `lit` of an Integer), a negative one (that `lit` and
|
|
73
|
+
# `unary_minus`), or an identifier (one `load`), whose value the write
|
|
74
|
+
# then takes. Any other key is computed, and the reference refuses it.
|
|
75
|
+
def plain_keys?(segments)
|
|
76
|
+
until segments.empty?
|
|
77
|
+
taken = key_size(segments)
|
|
78
|
+
return false if taken.zero?
|
|
79
|
+
|
|
80
|
+
segments = segments.drop(taken)
|
|
81
|
+
end
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# How many instructions the key at the head of `segments` takes, 0 when
|
|
86
|
+
# the head is not a key the reference takes.
|
|
87
|
+
def key_size(segments)
|
|
88
|
+
(opcode, operand), following = segments
|
|
89
|
+
case opcode
|
|
90
|
+
when "load" then 1
|
|
91
|
+
when "lit" then lit_key_size(operand, following)
|
|
92
|
+
else 0
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# How many instructions a `lit` of `operand` followed by `following`
|
|
97
|
+
# takes as a key: 2 for a negative integer literal, 1 for a String or
|
|
98
|
+
# an Integer, 0 for any other literal.
|
|
99
|
+
def lit_key_size(operand, following)
|
|
100
|
+
return 0 unless operand.is_a?(String) || operand.is_a?(Integer)
|
|
101
|
+
|
|
102
|
+
operand.is_a?(Integer) && following == ["unary_minus"] ? 2 : 1
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# The write of `value` at `path`, after the reference's two root checks.
|
|
106
|
+
def checked(context, location, path, value)
|
|
107
|
+
root = path.first
|
|
108
|
+
return refused(SystemVariable.new(root:)) if Datamodel.system_root?(root)
|
|
109
|
+
return refused(UnboundLocation.new(location:)) unless context.data.key?(root)
|
|
110
|
+
|
|
111
|
+
stored(context, location, path, value)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The write itself: `value` stored at `path` in the root's value, as
|
|
115
|
+
# predicator's store writes it, or predicator's refusal.
|
|
116
|
+
def stored(context, location, path, value)
|
|
117
|
+
root = path.first
|
|
118
|
+
result = Predicator.execute(store_of(path, value), { root => context.data[root] })
|
|
119
|
+
return refused(EvaluatorError.new(source: location, error: result.error)) unless result.ok?
|
|
120
|
+
|
|
121
|
+
Written.new(context: context.bind(root, result.value[root]), path: path.freeze,
|
|
122
|
+
prior_value: read(context.data, path), error: nil)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The instructions that store `value` at `path`.
|
|
126
|
+
def store_of(path, value) = path.map { ["lit", _1] } + [["lit", value], ["store", path.size]]
|
|
127
|
+
|
|
128
|
+
# The value at `path` before the write: a String key against a map, an
|
|
129
|
+
# Integer index against a list (a negative one from the end, as the
|
|
130
|
+
# reference reads it), and undefined on any miss or any step through
|
|
131
|
+
# something else.
|
|
132
|
+
def read(data, path)
|
|
133
|
+
path.reduce(data) do |container, segment|
|
|
134
|
+
case [container, segment]
|
|
135
|
+
in [Hash, String] | [Array, Integer] then container.fetch(segment) { return Predicator::UNDEFINED }
|
|
136
|
+
else return Predicator::UNDEFINED
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# A refused write.
|
|
142
|
+
def refused(reason) = Written.new(context: nil, path: nil, prior_value: nil, error: reason)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Datamodel
|
|
5
|
+
# A `<script>` program's run, with the reference's outcome.
|
|
6
|
+
#
|
|
7
|
+
# The reference runs a program with every underscore-prefixed root the
|
|
8
|
+
# datamodel already holds protected: a store into one fails at the
|
|
9
|
+
# attempt, so no later statement runs and none reads the value. The
|
|
10
|
+
# writes made before a failure (that one or any other) are kept. A
|
|
11
|
+
# system root the program creates fresh is not on that list; its write
|
|
12
|
+
# lands, the run goes on, and the change found after the run is never
|
|
13
|
+
# kept and fails the run, naming the first such root in sort order.
|
|
14
|
+
#
|
|
15
|
+
# The published predicator offers no protected roots and answers no
|
|
16
|
+
# context for a run that stops, so the program runs rewritten
|
|
17
|
+
# ({Stores.guarded}): a store into a protected root becomes a call of a
|
|
18
|
+
# host function that refuses, which stops the run at that statement as
|
|
19
|
+
# the reference's refusal does; every other store reports its root's
|
|
20
|
+
# new value to a host function that records it, so the writes made
|
|
21
|
+
# before any stop are known. Both host functions live only for the run,
|
|
22
|
+
# and the datamodel the run leaves is the one it started with plus the
|
|
23
|
+
# recorded roots that are not system roots: exactly the roots the
|
|
24
|
+
# reference finds changed, with the same values. A store whose root the
|
|
25
|
+
# rewrite cannot read fails the run rather than write unguarded.
|
|
26
|
+
#
|
|
27
|
+
# Each noted store hands the host function a copy of its root's whole
|
|
28
|
+
# value, so a loop that grows one root costs more per store than the
|
|
29
|
+
# reference's store does; the loop budget bounds it.
|
|
30
|
+
#
|
|
31
|
+
# @api private
|
|
32
|
+
module Program
|
|
33
|
+
# What a host function raises to stop a run at a protected store.
|
|
34
|
+
class Refused < StandardError; end
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Runs `program` (a {Machine::Program}) against `context`.
|
|
39
|
+
#
|
|
40
|
+
# @return [Executed] the datamodel the run leaves and the reason it
|
|
41
|
+
# failed, if it did: {SystemVariable} for a system root written,
|
|
42
|
+
# else {EvaluatorError} for predicator's refusal
|
|
43
|
+
def run(context, program)
|
|
44
|
+
before = context.data
|
|
45
|
+
halted = []
|
|
46
|
+
written = {}
|
|
47
|
+
protected_roots = before.keys.select { Datamodel.system_root?(_1) }
|
|
48
|
+
result = Predicator.execute(Stores.guarded(program.instructions, protected_roots), before,
|
|
49
|
+
**context.options, functions: functions(context, halted, written))
|
|
50
|
+
outcome(before, written, failure(program, result, halted.first))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The context's functions and the run's two: {Stores::GUARD} records
|
|
54
|
+
# the root it refuses in `halted`, {Stores::NOTE} each root's latest
|
|
55
|
+
# value in `written`.
|
|
56
|
+
def functions(context, halted, written)
|
|
57
|
+
context.options[:functions].merge(
|
|
58
|
+
Stores::GUARD => ->(root) { raise Refused, halted.push(root).first },
|
|
59
|
+
Stores::NOTE => ->(root, value) { written.store(root, value).then { nil } },
|
|
60
|
+
Stores::UNREAD => -> { raise Refused, "a store whose root cannot be read" }
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The reason a run stopped, or nil: a protected root's refusal names
|
|
65
|
+
# its root; any other failure is predicator's.
|
|
66
|
+
def failure(program, result, halted)
|
|
67
|
+
return SystemVariable.new(root: halted) if halted
|
|
68
|
+
return nil if result.ok?
|
|
69
|
+
|
|
70
|
+
EvaluatorError.new(source: program.source, error: result.error)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The datamodel the run leaves and its failure: a system root the run
|
|
74
|
+
# created fails it when nothing else did.
|
|
75
|
+
def outcome(before, written, failure)
|
|
76
|
+
system, other = written.partition { |root, _value| Datamodel.system_root?(root) }
|
|
77
|
+
failure ||= SystemVariable.new(root: system.map(&:first).min) unless system.empty?
|
|
78
|
+
Executed.new(data: before.merge(other.to_h).freeze, error: failure)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Datamodel
|
|
5
|
+
# A failure as the predicator value its `error.*` event carries as data,
|
|
6
|
+
# so a chart can read `_event.data` and every evaluation that reads
|
|
7
|
+
# `_event` stays a predicator evaluation. The reference carries its
|
|
8
|
+
# failure terms as they are; predicator refuses a context holding a
|
|
9
|
+
# value that is not its own, so here a reason becomes a Hash.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
module ReasonValue
|
|
13
|
+
# The predicator values a reason may carry, kept as they are.
|
|
14
|
+
VALUE_TYPES = [Predicator::Duration, Predicator::Date, Predicator::DateTime].freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# `reason` as a predicator value: a reason becomes a Hash of its fields
|
|
19
|
+
# under String keys, with `"kind"` its name in snake case (an
|
|
20
|
+
# `InvalidTarget` is `{"kind" => "invalid_target", "target" => ...}`),
|
|
21
|
+
# applied all the way down; a Symbol becomes its String; a predicator
|
|
22
|
+
# value stays as it is.
|
|
23
|
+
def of(reason)
|
|
24
|
+
case reason
|
|
25
|
+
when ::Data then VALUE_TYPES.include?(reason.class) ? reason : fields(reason)
|
|
26
|
+
when Symbol then reason.to_s
|
|
27
|
+
when Hash then reason.to_h { |key, value| [of(key), of(value)] }
|
|
28
|
+
when Array then reason.map { of(_1) }
|
|
29
|
+
else reason
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A reason's fields with its kind first.
|
|
34
|
+
def fields(reason)
|
|
35
|
+
{ "kind" => kind(reason.class) }.merge(reason.to_h.to_h { |key, value| [key.to_s, of(value)] })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A reason class's name in snake case.
|
|
39
|
+
def kind(klass) = klass.name.split("::").last.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Statifier
|
|
4
|
+
module Datamodel
|
|
5
|
+
# What an evaluation answered: `value`, or, as `error`, the reason it
|
|
6
|
+
# failed. Read `ok?`, not the value: nil is predicator's null.
|
|
7
|
+
#
|
|
8
|
+
# @api private
|
|
9
|
+
Evaluated = ::Data.define(:value, :error) do
|
|
10
|
+
# @return [Boolean] whether the evaluation succeeded
|
|
11
|
+
def ok? = error.nil?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# What a program run answered: `data`, the datamodel it leaves (the
|
|
15
|
+
# writes made before a failure included), and `error`, the reason it
|
|
16
|
+
# failed, nil when it did not.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
Executed = ::Data.define(:data, :error) do
|
|
20
|
+
# @return [Boolean] whether the run succeeded
|
|
21
|
+
def ok? = error.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# What a write to a location answered: the `context` it leaves, the
|
|
25
|
+
# `path` the location resolved to and the `prior_value` that path held
|
|
26
|
+
# before the write, or, as `error`, the reason it was refused (and the
|
|
27
|
+
# other fields nil).
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
Written = ::Data.define(:context, :path, :prior_value, :error) do
|
|
31
|
+
# @return [Boolean] whether the write was made
|
|
32
|
+
def ok? = error.nil?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The reasons a failure carries, each the data of the `error.execution`
|
|
36
|
+
# it raises. A deferred compile refusal is carried as the
|
|
37
|
+
# {Compiler::Error} itself, as the reference carries its compiler's.
|
|
38
|
+
|
|
39
|
+
# Predicator refused: `source` is the expression's or the location's
|
|
40
|
+
# text and `error` predicator's own error value, unwrapped.
|
|
41
|
+
#
|
|
42
|
+
# @api private
|
|
43
|
+
EvaluatorError = ::Data.define(:source, :error)
|
|
44
|
+
|
|
45
|
+
# A condition answered something other than true or false.
|
|
46
|
+
#
|
|
47
|
+
# @api private
|
|
48
|
+
NonBooleanCond = ::Data.define(:value)
|
|
49
|
+
|
|
50
|
+
# A write reached `root`, a system variable's root (underscore-prefixed).
|
|
51
|
+
#
|
|
52
|
+
# @api private
|
|
53
|
+
SystemVariable = ::Data.define(:root)
|
|
54
|
+
|
|
55
|
+
# A write named a location whose root the datamodel does not hold; a
|
|
56
|
+
# write never declares a root.
|
|
57
|
+
#
|
|
58
|
+
# @api private
|
|
59
|
+
UnboundLocation = ::Data.define(:location)
|
|
60
|
+
|
|
61
|
+
# A location's text is not one assignment target.
|
|
62
|
+
#
|
|
63
|
+
# @api private
|
|
64
|
+
InvalidLocation = ::Data.define(:location)
|
|
65
|
+
|
|
66
|
+
# A location's bracket key is a computed expression, which the
|
|
67
|
+
# reference refuses as an assignment target key; `message` is the
|
|
68
|
+
# reference's.
|
|
69
|
+
#
|
|
70
|
+
# @api private
|
|
71
|
+
ComputedKey = ::Data.define(:location) do
|
|
72
|
+
# @return [String] the reference's message for the refusal
|
|
73
|
+
def message = "Cannot use computed expression as assignment target key"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# A delay that is not one: text predicator's duration parser refuses, or
|
|
77
|
+
# a value that is neither text nor a duration.
|
|
78
|
+
#
|
|
79
|
+
# @api private
|
|
80
|
+
InvalidDelay = ::Data.define(:value)
|
|
81
|
+
|
|
82
|
+
# A `<data>` value is a `src`, which the host resolves and nothing here
|
|
83
|
+
# fetches.
|
|
84
|
+
#
|
|
85
|
+
# @api private
|
|
86
|
+
SrcValue = ::Data.define(:src)
|
|
87
|
+
end
|
|
88
|
+
end
|