statifier 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/{LICENSE.txt → LICENSE} +1 -1
  4. data/README.md +601 -16
  5. data/lib/statifier/accepts.rb +167 -0
  6. data/lib/statifier/basichttp/decode.rb +142 -0
  7. data/lib/statifier/basichttp/encoding.rb +224 -0
  8. data/lib/statifier/basichttp/net_http_transport.rb +157 -0
  9. data/lib/statifier/basichttp/outbound.rb +87 -0
  10. data/lib/statifier/basichttp/processor.rb +170 -0
  11. data/lib/statifier/basichttp/request.rb +94 -0
  12. data/lib/statifier/basichttp/response.rb +34 -0
  13. data/lib/statifier/basichttp.rb +47 -0
  14. data/lib/statifier/compiler/content.rb +131 -0
  15. data/lib/statifier/compiler/error.rb +30 -0
  16. data/lib/statifier/compiler/expressions.rb +64 -0
  17. data/lib/statifier/compiler/numbering.rb +145 -0
  18. data/lib/statifier/compiler/parts.rb +144 -0
  19. data/lib/statifier/compiler/walk.rb +61 -0
  20. data/lib/statifier/compiler.rb +82 -0
  21. data/lib/statifier/datamodel/data_binding.rb +114 -0
  22. data/lib/statifier/datamodel/delay.rb +54 -0
  23. data/lib/statifier/datamodel/location.rb +145 -0
  24. data/lib/statifier/datamodel/program.rb +82 -0
  25. data/lib/statifier/datamodel/reason_value.rb +42 -0
  26. data/lib/statifier/datamodel/reasons.rb +88 -0
  27. data/lib/statifier/datamodel/stores.rb +169 -0
  28. data/lib/statifier/datamodel/system_variables.rb +69 -0
  29. data/lib/statifier/datamodel.rb +120 -0
  30. data/lib/statifier/document/data.rb +36 -0
  31. data/lib/statifier/document/executable.rb +69 -0
  32. data/lib/statifier/document/invoke.rb +17 -0
  33. data/lib/statifier/document/states.rb +40 -0
  34. data/lib/statifier/document.rb +51 -0
  35. data/lib/statifier/driver/calls.rb +183 -0
  36. data/lib/statifier/driver/checks.rb +110 -0
  37. data/lib/statifier/driver/delivery.rb +45 -0
  38. data/lib/statifier/driver/invocations.rb +129 -0
  39. data/lib/statifier/driver/mailbox.rb +120 -0
  40. data/lib/statifier/driver/routing.rb +193 -0
  41. data/lib/statifier/driver/session_id.rb +40 -0
  42. data/lib/statifier/driver/work.rb +196 -0
  43. data/lib/statifier/driver.rb +205 -0
  44. data/lib/statifier/effect.rb +208 -0
  45. data/lib/statifier/event.rb +67 -0
  46. data/lib/statifier/frozen.rb +22 -0
  47. data/lib/statifier/interpreter/blocks.rb +142 -0
  48. data/lib/statifier/interpreter/content.rb +228 -0
  49. data/lib/statifier/interpreter/entering.rb +141 -0
  50. data/lib/statifier/interpreter/exit_entry.rb +174 -0
  51. data/lib/statifier/interpreter/foreach.rb +83 -0
  52. data/lib/statifier/interpreter/invoke/arguments.rb +52 -0
  53. data/lib/statifier/interpreter/invoke/passes.rb +143 -0
  54. data/lib/statifier/interpreter/invoke.rb +145 -0
  55. data/lib/statifier/interpreter/loop.rb +170 -0
  56. data/lib/statifier/interpreter/machine_state.rb +152 -0
  57. data/lib/statifier/interpreter/macrostep.rb +185 -0
  58. data/lib/statifier/interpreter/name_match.rb +43 -0
  59. data/lib/statifier/interpreter/selection.rb +221 -0
  60. data/lib/statifier/interpreter/send.rb +223 -0
  61. data/lib/statifier/interpreter/step.rb +138 -0
  62. data/lib/statifier/interpreter/stop.rb +66 -0
  63. data/lib/statifier/interpreter/targets.rb +107 -0
  64. data/lib/statifier/interpreter.rb +57 -0
  65. data/lib/statifier/lowering/branches.rb +84 -0
  66. data/lib/statifier/lowering/data.rb +102 -0
  67. data/lib/statifier/lowering/error.rb +102 -0
  68. data/lib/statifier/lowering/executable.rb +106 -0
  69. data/lib/statifier/lowering/placement.rb +65 -0
  70. data/lib/statifier/lowering/reading.rb +53 -0
  71. data/lib/statifier/lowering/states.rb +111 -0
  72. data/lib/statifier/lowering.rb +129 -0
  73. data/lib/statifier/machine/content.rb +81 -0
  74. data/lib/statifier/machine/data.rb +42 -0
  75. data/lib/statifier/machine/states.rb +56 -0
  76. data/lib/statifier/machine/values.rb +37 -0
  77. data/lib/statifier/machine.rb +103 -0
  78. data/lib/statifier/position/rebuild.rb +84 -0
  79. data/lib/statifier/position/shape.rb +73 -0
  80. data/lib/statifier/position/text.rb +43 -0
  81. data/lib/statifier/position.rb +206 -0
  82. data/lib/statifier/validator/attributes.rb +35 -0
  83. data/lib/statifier/validator/context.rb +74 -0
  84. data/lib/statifier/validator/data.rb +136 -0
  85. data/lib/statifier/validator/error.rb +90 -0
  86. data/lib/statifier/validator/executable.rb +139 -0
  87. data/lib/statifier/validator/history.rb +81 -0
  88. data/lib/statifier/validator/messages.rb +142 -0
  89. data/lib/statifier/validator/states.rb +161 -0
  90. data/lib/statifier/validator.rb +94 -0
  91. data/lib/statifier/version.rb +4 -1
  92. data/lib/statifier/xml/character_data.rb +142 -0
  93. data/lib/statifier/xml/cursor.rb +131 -0
  94. data/lib/statifier/xml/markup.rb +153 -0
  95. data/lib/statifier/xml/nodes.rb +50 -0
  96. data/lib/statifier/xml/parser.rb +121 -0
  97. data/lib/statifier/xml/scope.rb +46 -0
  98. data/lib/statifier/xml/tags.rb +112 -0
  99. data/lib/statifier/xml.rb +85 -0
  100. data/lib/statifier.rb +386 -2
  101. metadata +129 -20
  102. data/.gitignore +0 -8
  103. data/CODE_OF_CONDUCT.md +0 -74
  104. data/Gemfile +0 -11
  105. data/Gemfile.lock +0 -33
  106. data/Rakefile +0 -10
  107. data/bin/console +0 -8
  108. data/bin/setup +0 -6
  109. data/statifier.gemspec +0 -34
@@ -0,0 +1,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