predicator 1.2.1 → 2.0.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 +42 -0
- data/{LICENSE.txt → LICENSE} +1 -1
- data/README.md +322 -46
- data/lib/predicator/ast.rb +74 -187
- data/lib/predicator/boundary.rb +237 -0
- data/lib/predicator/civil.rb +112 -0
- data/lib/predicator/duration_text.rb +92 -0
- data/lib/predicator/emitter/chains.rb +62 -0
- data/lib/predicator/emitter/statements.rb +86 -0
- data/lib/predicator/emitter.rb +187 -0
- data/lib/predicator/errors.rb +140 -0
- data/lib/predicator/evaluator/access.rb +108 -0
- data/lib/predicator/evaluator/arithmetic.rb +132 -0
- data/lib/predicator/evaluator/calls.rb +129 -0
- data/lib/predicator/evaluator/casts.rb +158 -0
- data/lib/predicator/evaluator/chronology.rb +88 -0
- data/lib/predicator/evaluator/compare.rb +158 -0
- data/lib/predicator/evaluator/json_functions.rb +167 -0
- data/lib/predicator/evaluator/json_scan.rb +303 -0
- data/lib/predicator/evaluator/library.rb +127 -0
- data/lib/predicator/evaluator/math_functions.rb +72 -0
- data/lib/predicator/evaluator/order.rb +120 -0
- data/lib/predicator/evaluator/rich_types.rb +146 -0
- data/lib/predicator/evaluator/statements.rb +151 -0
- data/lib/predicator/evaluator.rb +391 -117
- data/lib/predicator/float_text.rb +94 -0
- data/lib/predicator/iso_text.rb +99 -0
- data/lib/predicator/lexer/literals.rb +115 -0
- data/lib/predicator/lexer/numbers.rb +91 -0
- data/lib/predicator/lexer.rb +194 -0
- data/lib/predicator/parser/durations.rb +108 -0
- data/lib/predicator/parser/operators.rb +86 -0
- data/lib/predicator/parser/postfix.rb +57 -0
- data/lib/predicator/parser/primaries.rb +122 -0
- data/lib/predicator/parser/statements.rb +114 -0
- data/lib/predicator/parser/token_text.rb +52 -0
- data/lib/predicator/parser.rb +217 -438
- data/lib/predicator/result.rb +46 -0
- data/lib/predicator/tagged/reader.rb +139 -0
- data/lib/predicator/tagged/writer.rb +106 -0
- data/lib/predicator/tagged.rb +134 -0
- data/lib/predicator/values.rb +147 -0
- data/lib/predicator/version.rb +4 -1
- data/lib/predicator.rb +384 -14
- metadata +53 -147
- data/.gitignore +0 -11
- data/.rubocop.yml +0 -46
- data/.travis.yml +0 -5
- data/CODE_OF_CONDUCT.md +0 -49
- data/Gemfile +0 -4
- data/HISTORY.md +0 -51
- data/Rakefile +0 -24
- data/lib/predicator/context.rb +0 -18
- data/lib/predicator/lexer.rex +0 -69
- data/lib/predicator/lexer.rex.rb +0 -187
- data/lib/predicator/parser.y +0 -120
- data/lib/predicator/visitors/dot.rb +0 -113
- data/lib/predicator/visitors/each.rb +0 -16
- data/lib/predicator/visitors/instructions.rb +0 -183
- data/lib/predicator/visitors/string.rb +0 -76
- data/lib/predicator/visitors/visitor.rb +0 -100
- data/lib/predicator/visitors.rb +0 -5
- data/predicator.gemspec +0 -34
data/lib/predicator/evaluator.rb
CHANGED
|
@@ -1,153 +1,427 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "values"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
require_relative "result"
|
|
6
|
+
require_relative "float_text"
|
|
7
|
+
require_relative "duration_text"
|
|
8
|
+
require_relative "iso_text"
|
|
9
|
+
require_relative "boundary"
|
|
10
|
+
require_relative "evaluator/chronology"
|
|
11
|
+
require_relative "evaluator/arithmetic"
|
|
12
|
+
require_relative "evaluator/access"
|
|
13
|
+
require_relative "evaluator/rich_types"
|
|
14
|
+
require_relative "evaluator/library"
|
|
15
|
+
require_relative "evaluator/calls"
|
|
16
|
+
require_relative "evaluator/statements"
|
|
17
|
+
require_relative "evaluator/casts"
|
|
2
18
|
|
|
3
19
|
module Predicator
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
20
|
+
# The stack machine that runs an instruction list against a context. It
|
|
21
|
+
# runs in one of two modes, carried by the entry point and not by the
|
|
22
|
+
# list: in expression mode ({evaluate}) the answer is the top of the stack
|
|
23
|
+
# at halt; in statement mode ({execute}) it is the context at halt. The
|
|
24
|
+
# instructions mean the same in both. It is private to the package: the
|
|
25
|
+
# public entry points ({Predicator.evaluate}, {Predicator.execute} and
|
|
26
|
+
# {Predicator.execute_value}) check what they are handed against the
|
|
27
|
+
# value domain ({Boundary}) and then call it.
|
|
28
|
+
#
|
|
29
|
+
# A program is a flat list of instructions run in order from index 0; it
|
|
30
|
+
# halts when the instruction pointer reaches or passes the end, so a jump
|
|
31
|
+
# past the last instruction is a normal halt. The rules every opcode keeps
|
|
32
|
+
# are the reference's ISA (docs/isa.md, section 2): opcodes validate and
|
|
33
|
+
# never coerce; a malformed operand is an unknown instruction; stack depth
|
|
34
|
+
# is checked before types.
|
|
35
|
+
#
|
|
36
|
+
# Each instruction this build runs is one row of {INSTRUCTIONS}: the shapes
|
|
37
|
+
# of its operands, the instruction-set version that retired it, and the
|
|
38
|
+
# Machine method that runs it. A later opcode is one more row and one more
|
|
39
|
+
# method; the loop, the operand check and the halt rules do not change.
|
|
40
|
+
#
|
|
41
|
+
# Options are per call: nothing here is configured globally, and every
|
|
42
|
+
# evaluation builds its own Machine. That includes the clock: an
|
|
43
|
+
# instruction that reads the time reads the clock the caller passed, at
|
|
44
|
+
# most once per evaluation, so every such instruction in one evaluation
|
|
45
|
+
# reads the same instant. It includes the host's functions too: `call`
|
|
46
|
+
# reaches the ones the caller passed to this evaluation, then the
|
|
47
|
+
# package's own library, and nothing registered anywhere else. And it
|
|
48
|
+
# includes the loop budget: the back edges one evaluation may take.
|
|
49
|
+
module Evaluator
|
|
50
|
+
# The instruction-set version this build runs: an opcode retired at or
|
|
51
|
+
# below it is refused.
|
|
52
|
+
ISA_VERSION = 6
|
|
13
53
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
54
|
+
# What a load of a root the context does not bind does: :undefined
|
|
55
|
+
# pushes undefined (and the answer is an unbound-variable error if
|
|
56
|
+
# undefined is what the program answers); :error stops at the load.
|
|
57
|
+
UNBOUND_POLICIES = %i[undefined error].freeze
|
|
18
58
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@ip += 1
|
|
23
|
-
end
|
|
24
|
-
stack.pop
|
|
25
|
-
end
|
|
59
|
+
# The back edges (`jump_backward`) one evaluation may take when the
|
|
60
|
+
# caller names no budget, the reference's default.
|
|
61
|
+
DEFAULT_LOOP_BUDGET = 10_000
|
|
26
62
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
when "load" then stack.push context[instruction.last]
|
|
34
|
-
when "to_bool" then stack.push !!stack.pop
|
|
35
|
-
when "to_int" then stack.push to_int(stack.pop)
|
|
36
|
-
when "to_str" then stack.push to_str(stack.pop)
|
|
37
|
-
when "to_date" then stack.push to_date(stack.pop)
|
|
38
|
-
when "date_ago" then stack.push date_ago(stack.pop)
|
|
39
|
-
when "date_from_now" then stack.push date_from_now(stack.pop)
|
|
40
|
-
when "blank" then stack.push blank?(stack.pop)
|
|
41
|
-
when "present" then stack.push !blank?(stack.pop)
|
|
42
|
-
when "compare"
|
|
43
|
-
if instruction.last == "BETWEEN"
|
|
44
|
-
compare_BETWEEN
|
|
45
|
-
else
|
|
46
|
-
compare instruction.last
|
|
47
|
-
end
|
|
48
|
-
end
|
|
63
|
+
# The clock an evaluation reads when the caller passes none: the system's
|
|
64
|
+
# real-time clock, read to the microsecond, as an instant in UTC. It
|
|
65
|
+
# holds no state.
|
|
66
|
+
SYSTEM_CLOCK = lambda do
|
|
67
|
+
micros = Process.clock_gettime(Process::CLOCK_REALTIME, :microsecond)
|
|
68
|
+
Predicator::DateTime.new(epoch_seconds: micros.div(1_000_000), microsecond: micros % 1_000_000)
|
|
49
69
|
end
|
|
50
70
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
end
|
|
71
|
+
# One instruction's row: the shape of each operand, the version that
|
|
72
|
+
# retired it (nil while it is live) and the Machine method that runs it.
|
|
73
|
+
Row = Data.define(:operands, :removed_in, :handler) do
|
|
74
|
+
# Whether the opcode was retired at or below {ISA_VERSION}.
|
|
75
|
+
def retired? = !removed_in.nil? && removed_in <= ISA_VERSION
|
|
57
76
|
end
|
|
58
77
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
# Each operand shape, as a test of one operand.
|
|
79
|
+
SHAPES = {
|
|
80
|
+
value: ->(_operand) { true },
|
|
81
|
+
string: ->(operand) { operand.is_a?(String) },
|
|
82
|
+
list: ->(operand) { operand.is_a?(Array) },
|
|
83
|
+
comparison_operator: ->(operand) { Compare::OPERATORS.include?(operand) },
|
|
84
|
+
positive_integer: ->(operand) { operand.is_a?(Integer) && operand.positive? },
|
|
85
|
+
count: ->(operand) { operand.is_a?(Integer) && !operand.negative? },
|
|
86
|
+
type_name: ->(operand) { Casts::TYPE_NAMES.include?(operand) }
|
|
87
|
+
}.freeze
|
|
62
88
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
89
|
+
# The instructions this build knows, by opcode. `and` and `or` were
|
|
90
|
+
# retired at instruction-set version 3; they keep their rows so that a
|
|
91
|
+
# list carrying them is refused as retired rather than as unknown.
|
|
92
|
+
INSTRUCTIONS = {
|
|
93
|
+
"lit" => Row.new(operands: [:value], removed_in: nil, handler: :push),
|
|
94
|
+
"load" => Row.new(operands: [:string], removed_in: nil, handler: :load_root),
|
|
95
|
+
"compare" => Row.new(operands: [:comparison_operator], removed_in: nil, handler: :compare),
|
|
96
|
+
"not" => Row.new(operands: [], removed_in: nil, handler: :logical_not),
|
|
97
|
+
"unary_bang" => Row.new(operands: [], removed_in: nil, handler: :unary_bang),
|
|
98
|
+
"unary_minus" => Row.new(operands: [], removed_in: nil, handler: :unary_minus),
|
|
99
|
+
"jump_if_falsy_or_pop" => Row.new(operands: [:positive_integer], removed_in: nil,
|
|
100
|
+
handler: :jump_if_falsy_or_pop),
|
|
101
|
+
"jump_if_true_or_pop" => Row.new(operands: [:positive_integer], removed_in: nil,
|
|
102
|
+
handler: :jump_if_true_or_pop),
|
|
103
|
+
"add" => Row.new(operands: [], removed_in: nil, handler: :add),
|
|
104
|
+
"subtract" => Row.new(operands: [], removed_in: nil, handler: :subtract),
|
|
105
|
+
"multiply" => Row.new(operands: [], removed_in: nil, handler: :multiply),
|
|
106
|
+
"divide" => Row.new(operands: [], removed_in: nil, handler: :divide),
|
|
107
|
+
"modulo" => Row.new(operands: [], removed_in: nil, handler: :modulo),
|
|
108
|
+
"access" => Row.new(operands: [:string], removed_in: nil, handler: :access),
|
|
109
|
+
"bracket_access" => Row.new(operands: [], removed_in: nil, handler: :bracket_access),
|
|
110
|
+
"in" => Row.new(operands: [], removed_in: nil, handler: :membership_in),
|
|
111
|
+
"contains" => Row.new(operands: [], removed_in: nil, handler: :membership_contains),
|
|
112
|
+
"make_list" => Row.new(operands: [:count], removed_in: nil, handler: :make_list),
|
|
113
|
+
"duration" => Row.new(operands: [:list], removed_in: nil, handler: :duration),
|
|
114
|
+
"relative_date" => Row.new(operands: [:string], removed_in: nil, handler: :relative_date),
|
|
115
|
+
"object_new" => Row.new(operands: [], removed_in: nil, handler: :object_new),
|
|
116
|
+
"object_set" => Row.new(operands: [:string], removed_in: nil, handler: :object_set),
|
|
117
|
+
"call" => Row.new(operands: %i[string count], removed_in: nil, handler: :call_function),
|
|
118
|
+
"store" => Row.new(operands: [:count], removed_in: nil, handler: :store),
|
|
119
|
+
"pop" => Row.new(operands: [], removed_in: nil, handler: :discard),
|
|
120
|
+
"jump" => Row.new(operands: [:positive_integer], removed_in: nil, handler: :jump),
|
|
121
|
+
"pop_jump_if_falsy" => Row.new(operands: [:positive_integer], removed_in: nil, handler: :pop_jump_if_falsy),
|
|
122
|
+
"jump_backward" => Row.new(operands: [:positive_integer], removed_in: nil, handler: :jump_backward),
|
|
123
|
+
"cast" => Row.new(operands: [:type_name], removed_in: nil, handler: :cast),
|
|
124
|
+
"and" => Row.new(operands: [], removed_in: 3, handler: nil),
|
|
125
|
+
"or" => Row.new(operands: [], removed_in: 3, handler: nil)
|
|
126
|
+
}.freeze
|
|
127
|
+
|
|
128
|
+
# The options one evaluation runs under, checked: the unbound policy,
|
|
129
|
+
# the clock, the host's functions and the loop budget.
|
|
130
|
+
Settings = Data.define(:on_unbound, :clock, :functions, :loop_budget)
|
|
131
|
+
private_constant :Settings
|
|
66
132
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
133
|
+
# What a run in statement mode answers: the context at halt and the
|
|
134
|
+
# value the last `pop` discarded (undefined when no `pop` ran), or the
|
|
135
|
+
# error that stopped the run and the context as it stood then, which
|
|
136
|
+
# holds every write made before the failing instruction.
|
|
137
|
+
#
|
|
138
|
+
# @!attribute [r] error
|
|
139
|
+
# @return [Object, nil] the error value that stopped the run, or nil
|
|
140
|
+
# @!attribute [r] context
|
|
141
|
+
# @return [Hash] the context at halt, or when the run stopped
|
|
142
|
+
# @!attribute [r] value
|
|
143
|
+
# @return [Object] what the last `pop` discarded, or undefined
|
|
144
|
+
StatementResult = Data.define(:error, :context, :value) do
|
|
145
|
+
# Whether the run reached its halt.
|
|
146
|
+
def ok? = error.nil?
|
|
70
147
|
end
|
|
71
148
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
149
|
+
# Runs instructions against context in expression mode: the answer is
|
|
150
|
+
# the top of the stack at halt.
|
|
151
|
+
#
|
|
152
|
+
# @param instructions [Array] the instruction list
|
|
153
|
+
# @param context [Hash{String => Object}] the variables a load reads
|
|
154
|
+
# @param options [Hash] the evaluation's options, each optional:
|
|
155
|
+
# @option options [Symbol] :on_unbound one of {UNBOUND_POLICIES};
|
|
156
|
+
# :undefined when not given
|
|
157
|
+
# @option options [#call, nil] :clock the clock: called with no argument,
|
|
158
|
+
# it answers the current time as a {Predicator::DateTime}. It is called
|
|
159
|
+
# at most once, and only when an instruction reads the time. nil, the
|
|
160
|
+
# default, means {SYSTEM_CLOCK}. A clock that answers anything else, or
|
|
161
|
+
# raises a StandardError, stops the run with an EvaluationError whose
|
|
162
|
+
# reason is clock_failed, whichever instruction read it.
|
|
163
|
+
# @option options [Hash{String => #call}] :functions the host's functions
|
|
164
|
+
# for this evaluation, by the name a `call` instruction names. Each is
|
|
165
|
+
# called with the call's arguments, first argument first, and answers
|
|
166
|
+
# the call's value; the counts of arguments it accepts are the ones its
|
|
167
|
+
# parameters take. A name here shadows the library function of the
|
|
168
|
+
# same name, for this evaluation only. {Calls} says how a call is
|
|
169
|
+
# refused.
|
|
170
|
+
# @option options [Integer] :loop_budget the back edges the evaluation
|
|
171
|
+
# may take, at least 0; {DEFAULT_LOOP_BUDGET} when not given. A
|
|
172
|
+
# `jump_backward` that finds the budget spent stops the run with
|
|
173
|
+
# loop_budget_exceeded.
|
|
174
|
+
# @return [Result] the top of the stack at halt, or the error that
|
|
175
|
+
# stopped the run
|
|
176
|
+
# @raise [ArgumentError] when an argument is not of its kind, or an
|
|
177
|
+
# option is unknown or malformed (a caller's bug, not an outcome of the
|
|
178
|
+
# program)
|
|
179
|
+
def self.evaluate(instructions, context = {}, **options) = machine(instructions, context, options).run
|
|
180
|
+
|
|
181
|
+
# Runs instructions against context in statement mode: the answer is
|
|
182
|
+
# the context at halt. An empty stack at halt is a normal halt, and
|
|
183
|
+
# whatever the stack holds then is discarded. The context passed is
|
|
184
|
+
# never changed; a `store` writes a new one.
|
|
185
|
+
#
|
|
186
|
+
# @param instructions [Array] the instruction list
|
|
187
|
+
# @param context [Hash{String => Object}] the variables a load reads
|
|
188
|
+
# @param options [Hash] the options {evaluate} takes
|
|
189
|
+
# @return [StatementResult] the context at halt and the value the last `pop`
|
|
190
|
+
# discarded, or the error that stopped the run
|
|
191
|
+
# @raise [ArgumentError] as {evaluate} raises it
|
|
192
|
+
def self.execute(instructions, context = {}, **options)
|
|
193
|
+
machine(instructions, context, options).run_statements
|
|
75
194
|
end
|
|
76
195
|
|
|
77
|
-
def
|
|
78
|
-
|
|
196
|
+
def self.machine(instructions, context, options)
|
|
197
|
+
raise ArgumentError, "instructions must be an Array, not #{instructions.inspect}" unless instructions.is_a?(Array)
|
|
198
|
+
raise ArgumentError, "context must be a Hash, not #{context.inspect}" unless context.is_a?(Hash)
|
|
199
|
+
|
|
200
|
+
Machine.new(instructions, context, settings(**options))
|
|
79
201
|
end
|
|
202
|
+
private_class_method :machine
|
|
80
203
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
204
|
+
def self.settings(on_unbound: :undefined, clock: nil, functions: {}, loop_budget: DEFAULT_LOOP_BUDGET)
|
|
205
|
+
unless UNBOUND_POLICIES.include?(on_unbound)
|
|
206
|
+
raise ArgumentError, "on_unbound must be one of #{UNBOUND_POLICIES.inspect}, not #{on_unbound.inspect}"
|
|
207
|
+
end
|
|
208
|
+
raise ArgumentError, "clock must respond to call, not #{clock.inspect}" unless clock?(clock)
|
|
209
|
+
unless loop_budget.is_a?(Integer) && !loop_budget.negative?
|
|
210
|
+
raise ArgumentError, "loop_budget must be an Integer of at least 0, not #{loop_budget.inspect}"
|
|
87
211
|
end
|
|
212
|
+
|
|
213
|
+
check_functions(functions)
|
|
214
|
+
Settings.new(on_unbound:, clock: clock || SYSTEM_CLOCK, functions:, loop_budget:)
|
|
88
215
|
end
|
|
216
|
+
private_class_method :settings
|
|
217
|
+
|
|
218
|
+
def self.clock?(clock) = clock.nil? || clock.respond_to?(:call)
|
|
219
|
+
private_class_method :clock?
|
|
89
220
|
|
|
90
|
-
def
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
221
|
+
def self.check_functions(functions)
|
|
222
|
+
raise ArgumentError, "functions must be a Hash, not #{functions.inspect}" unless functions.is_a?(Hash)
|
|
223
|
+
|
|
224
|
+
functions.each do |name, callable|
|
|
225
|
+
raise ArgumentError, "a function's name is a String, not #{name.inspect}" unless name.is_a?(String)
|
|
226
|
+
raise ArgumentError, "the function #{name} does not respond to call" unless callable.respond_to?(:call)
|
|
96
227
|
end
|
|
97
228
|
end
|
|
229
|
+
private_class_method :check_functions
|
|
230
|
+
|
|
231
|
+
# The error values the loop and every opcode's handler refuse with when
|
|
232
|
+
# the stack or the instruction itself is wrong. {Machine} includes it.
|
|
233
|
+
module Refusals
|
|
234
|
+
private
|
|
98
235
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if left.nil? || right.nil?
|
|
103
|
-
stack.push false
|
|
104
|
-
else
|
|
105
|
-
stack.push send("compare_#{comparison}", left, right)
|
|
236
|
+
def insufficient(opcode)
|
|
237
|
+
EvaluationError.new(reason: "insufficient_operands",
|
|
238
|
+
message: "#{opcode} needs more operands than the stack holds")
|
|
106
239
|
end
|
|
107
|
-
rescue StandardError
|
|
108
|
-
stack.push false
|
|
109
|
-
end
|
|
110
240
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
241
|
+
def unknown(at)
|
|
242
|
+
EvaluationError.new(reason: "unknown_instruction", message: "Unknown instruction at index #{at}")
|
|
243
|
+
end
|
|
114
244
|
|
|
115
|
-
|
|
116
|
-
|
|
245
|
+
def retired(opcode, version)
|
|
246
|
+
EvaluationError.new(reason: "retired_opcode",
|
|
247
|
+
message: "#{opcode} was retired at instruction-set version #{version}")
|
|
248
|
+
end
|
|
117
249
|
end
|
|
250
|
+
private_constant :Refusals
|
|
118
251
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
252
|
+
# One evaluation: the program, the context (which a `store` replaces),
|
|
253
|
+
# the policy, the clock, the host's functions, the loop budget left, the
|
|
254
|
+
# stack, the first root a load did not find bound and the value the last
|
|
255
|
+
# `pop` discarded. Each handler answers the index of the next
|
|
256
|
+
# instruction, or the error value that stops the run. The handlers of
|
|
257
|
+
# the arithmetic opcodes are in {Arithmetic}, those of access,
|
|
258
|
+
# membership and list construction in {Access}, those of durations,
|
|
259
|
+
# relative dates and maps in {RichTypes}, that of `call` in {Calls},
|
|
260
|
+
# which includes the library {Library} defines, and those of the
|
|
261
|
+
# statement and jump opcodes in {Statements}, and that of `cast` in
|
|
262
|
+
# {Casts}; Machine includes all six, and the refusals they share in
|
|
263
|
+
# {Refusals}.
|
|
264
|
+
class Machine
|
|
265
|
+
include Refusals
|
|
266
|
+
include Arithmetic
|
|
267
|
+
include Access
|
|
268
|
+
include RichTypes
|
|
269
|
+
include Calls
|
|
270
|
+
include Statements
|
|
271
|
+
include Casts
|
|
122
272
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
273
|
+
def initialize(program, context, settings)
|
|
274
|
+
@program = program
|
|
275
|
+
@context = context
|
|
276
|
+
@on_unbound = settings.on_unbound
|
|
277
|
+
@clock = settings.clock
|
|
278
|
+
@functions = settings.functions
|
|
279
|
+
@budget = settings.loop_budget
|
|
280
|
+
@stack = []
|
|
281
|
+
@unbound = nil
|
|
282
|
+
@last_value = UNDEFINED
|
|
283
|
+
end
|
|
126
284
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
end
|
|
285
|
+
# Runs in expression mode: the answer is the top of the stack at halt.
|
|
286
|
+
def run = drive&.then { |error| Results.failure(error) } || halt
|
|
130
287
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
288
|
+
# Runs in statement mode: the answer is the context at halt. Neither
|
|
289
|
+
# halt rule of expression mode applies, as there is no stack value to
|
|
290
|
+
# answer.
|
|
291
|
+
def run_statements = StatementResult.new(error: drive, context: @context, value: @last_value)
|
|
134
292
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
293
|
+
private
|
|
294
|
+
|
|
295
|
+
# Runs the program until it halts, answering the error that stopped it,
|
|
296
|
+
# or nil when it halted.
|
|
297
|
+
def drive
|
|
298
|
+
at = 0
|
|
299
|
+
while at < @program.size
|
|
300
|
+
step = step(@program[at], at)
|
|
301
|
+
return step unless step.is_a?(Integer)
|
|
302
|
+
|
|
303
|
+
at = step
|
|
304
|
+
end
|
|
305
|
+
nil
|
|
306
|
+
rescue ClockFault => e
|
|
307
|
+
e.error
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# The answer at halt. An empty stack is an error; undefined on top is
|
|
311
|
+
# an unbound-variable error naming the first root a load did not find,
|
|
312
|
+
# when a load did not find one.
|
|
313
|
+
def halt
|
|
314
|
+
return Results.failure(EvaluationError.new(reason: "empty_stack", message: "the stack is empty at halt")) \
|
|
315
|
+
if @stack.empty?
|
|
316
|
+
return Results.failure(unbound(@unbound)) if UNDEFINED.equal?(@stack.last) && @unbound
|
|
317
|
+
|
|
318
|
+
Results.ok(@stack.last)
|
|
319
|
+
end
|
|
138
320
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
321
|
+
# A retired opcode is refused by its name whatever its operands, as the
|
|
322
|
+
# reference refuses it; any other instruction's shape is checked, and
|
|
323
|
+
# only then does it run.
|
|
324
|
+
def step(instruction, at)
|
|
325
|
+
named = INSTRUCTIONS[instruction.first] if instruction.is_a?(Array)
|
|
326
|
+
return retired(instruction.first, named.removed_in) if named&.retired?
|
|
327
|
+
|
|
328
|
+
row = row_for(instruction)
|
|
329
|
+
return unknown(at) unless row
|
|
330
|
+
|
|
331
|
+
send(row.handler, at, *instruction.drop(1))
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def row_for(instruction)
|
|
335
|
+
return unless instruction.is_a?(Array)
|
|
336
|
+
|
|
337
|
+
row = INSTRUCTIONS[instruction.first]
|
|
338
|
+
return unless row && instruction.size == row.operands.size + 1
|
|
339
|
+
|
|
340
|
+
row if row.operands.each_with_index.all? { |shape, index| SHAPES.fetch(shape).call(instruction[index + 1]) }
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def push(at, value)
|
|
344
|
+
@stack.push(value)
|
|
345
|
+
at + 1
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# A root the context binds is read as it is, null and undefined
|
|
349
|
+
# included; one it does not bind is undefined, recorded for the halt
|
|
350
|
+
# rule, or under the :error policy an error at once.
|
|
351
|
+
def load_root(at, name)
|
|
352
|
+
return push(at, @context[name]) if @context.key?(name)
|
|
353
|
+
return unbound(name) if @on_unbound == :error
|
|
354
|
+
|
|
355
|
+
@unbound ||= name
|
|
356
|
+
push(at, UNDEFINED)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def compare(at, operator)
|
|
360
|
+
return insufficient("compare") if @stack.size < 2
|
|
361
|
+
|
|
362
|
+
right = @stack.pop
|
|
363
|
+
left = @stack.pop
|
|
364
|
+
push(at, Compare.call(operator, left, right))
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def logical_not(at) = negate(at, "logical_not")
|
|
368
|
+
def unary_bang(at) = negate(at, "unary_bang")
|
|
369
|
+
|
|
370
|
+
def negate(at, operation)
|
|
371
|
+
return insufficient(operation) if @stack.empty?
|
|
372
|
+
|
|
373
|
+
operand = @stack.pop
|
|
374
|
+
return mismatch(operation, "a boolean", operand) unless true.equal?(operand) || false.equal?(operand)
|
|
375
|
+
|
|
376
|
+
push(at, !operand)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def unary_minus(at)
|
|
380
|
+
return insufficient("unary_minus") if @stack.empty?
|
|
381
|
+
|
|
382
|
+
operand = @stack.pop
|
|
383
|
+
return mismatch("unary_minus", "a number", operand) unless operand.is_a?(Integer) || operand.is_a?(Float)
|
|
384
|
+
|
|
385
|
+
push(at, -operand)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def jump_if_falsy_or_pop(at, offset) = conditional_jump(at, offset, "jump_if_falsy_or_pop", false)
|
|
389
|
+
def jump_if_true_or_pop(at, offset) = conditional_jump(at, offset, "jump_if_true_or_pop", true)
|
|
390
|
+
|
|
391
|
+
# Jumps when the top's truth is jump_on, leaving it on the stack as the
|
|
392
|
+
# answer; otherwise pops it and falls through. False, null and
|
|
393
|
+
# undefined are falsy, exactly true is true, and anything else is a
|
|
394
|
+
# type mismatch.
|
|
395
|
+
def conditional_jump(at, offset, opcode, jump_on)
|
|
396
|
+
return insufficient(opcode) if @stack.empty?
|
|
397
|
+
|
|
398
|
+
truth = truth(@stack.last)
|
|
399
|
+
return mismatch(opcode, "a boolean", @stack.last) if truth.nil?
|
|
400
|
+
return at + offset if truth == jump_on
|
|
401
|
+
|
|
402
|
+
@stack.pop
|
|
403
|
+
at + 1
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def truth(value)
|
|
407
|
+
return true if true.equal?(value)
|
|
408
|
+
|
|
409
|
+
false if false.equal?(value) || value.nil? || UNDEFINED.equal?(value)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# A refused operand. When it is the undefined an unbound load pushed,
|
|
413
|
+
# the run reports the unbound variable instead, as the reference does.
|
|
414
|
+
def mismatch(operation, wanted, operand)
|
|
415
|
+
return unbound(@unbound) if UNDEFINED.equal?(operand) && @unbound
|
|
416
|
+
|
|
417
|
+
TypeMismatchError.new(reason: operation, message: "#{operation} expects #{wanted}")
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def unbound(name) = UndefinedVariableError.new(reason: "unbound_variable", message: "Undefined variable: #{name}")
|
|
151
421
|
end
|
|
152
422
|
end
|
|
423
|
+
private_constant :Evaluator
|
|
153
424
|
end
|
|
425
|
+
|
|
426
|
+
require_relative "evaluator/compare"
|
|
427
|
+
require_relative "evaluator/order"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Predicator
|
|
4
|
+
# How a Float is written as text where predicator writes a number into a
|
|
5
|
+
# string: the reference's own spelling of a float, which is its host
|
|
6
|
+
# language's shortest form. The shortest digits that read back as the same
|
|
7
|
+
# Float are placed in one of two forms:
|
|
8
|
+
#
|
|
9
|
+
# - plain, with a point and a `.0` when the number is whole: `1230.0`,
|
|
10
|
+
# `0.001`;
|
|
11
|
+
# - exponent, one digit, a point, at least one more digit, `e` and the
|
|
12
|
+
# exponent, signed only when negative: `1.0e3`, `1.5e-7`.
|
|
13
|
+
#
|
|
14
|
+
# At a magnitude of 2**53 or more the exponent form is written; below it,
|
|
15
|
+
# whichever of the two is shorter, and the plain form when they are the
|
|
16
|
+
# same length. A negative number carries a minus in front of either form;
|
|
17
|
+
# zero is `0.0` and negative zero `-0.0`.
|
|
18
|
+
#
|
|
19
|
+
# It also reads a number's decimal text as the Float nearest it ({read}).
|
|
20
|
+
#
|
|
21
|
+
# The module holds no state. It is private to the package.
|
|
22
|
+
module FloatText
|
|
23
|
+
# The magnitude from which the exponent form is always written.
|
|
24
|
+
EXPONENT_FROM = 2**53
|
|
25
|
+
|
|
26
|
+
# The magnitude from which a number rounds to no finite Float: halfway
|
|
27
|
+
# between the largest Float and 2**1024.
|
|
28
|
+
CEILING = (2**1024) - (2**970)
|
|
29
|
+
|
|
30
|
+
# The magnitude at or below which a number rounds to a zero Float: half
|
|
31
|
+
# the smallest Float above zero.
|
|
32
|
+
FLOOR = Rational(1, 2**1075)
|
|
33
|
+
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
# The Float nearest the number a decimal text writes (an optional minus,
|
|
37
|
+
# digits, and an optional point and digits; no exponent), or nil when
|
|
38
|
+
# the number is too large for a finite Float. A number too close to zero
|
|
39
|
+
# for any other Float reads as zero, signed as the text is. The
|
|
40
|
+
# magnitude is judged on the exact number first, so the host's parser is
|
|
41
|
+
# never asked for a Float outside its range, where it would print a
|
|
42
|
+
# warning.
|
|
43
|
+
#
|
|
44
|
+
# @param text [String]
|
|
45
|
+
# @return [Float, nil]
|
|
46
|
+
def read(text)
|
|
47
|
+
magnitude = Rational(text).abs
|
|
48
|
+
return if magnitude >= CEILING
|
|
49
|
+
return Float(text.start_with?("-") ? "-0" : "0") if magnitude <= FLOOR
|
|
50
|
+
|
|
51
|
+
Float(text)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @param value [Float] a finite Float
|
|
55
|
+
# @return [String]
|
|
56
|
+
def spell(value)
|
|
57
|
+
sign = value.to_s.start_with?("-") ? "-" : ""
|
|
58
|
+
return "#{sign}0.0" if value.zero?
|
|
59
|
+
|
|
60
|
+
digits, point = shortest_digits(value.abs)
|
|
61
|
+
exponent = exponent_form(digits, point)
|
|
62
|
+
return sign + exponent if value.abs >= EXPONENT_FROM
|
|
63
|
+
|
|
64
|
+
plain = plain_form(digits, point)
|
|
65
|
+
sign + (plain.size <= exponent.size ? plain : exponent)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The shortest significant digits of a positive Float, without leading or
|
|
69
|
+
# trailing zeros, and where the point falls among them: the Float is
|
|
70
|
+
# 0.DIGITS times ten to the power point. Ruby's own Float#to_s writes the
|
|
71
|
+
# same shortest digits, in a form of its own that this reads apart.
|
|
72
|
+
def shortest_digits(magnitude)
|
|
73
|
+
mantissa, exponent = magnitude.to_s.split("e")
|
|
74
|
+
whole, fraction = mantissa.split(".")
|
|
75
|
+
digits = whole + fraction
|
|
76
|
+
significant = digits.sub(/\A0+/, "")
|
|
77
|
+
point = whole.size + exponent.to_i - (digits.size - significant.size)
|
|
78
|
+
[significant.sub(/0+\z/, ""), point]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def plain_form(digits, point)
|
|
82
|
+
return "0.#{"0" * -point}#{digits}" if point <= 0
|
|
83
|
+
return "#{digits}#{"0" * (point - digits.size)}.0" if point >= digits.size
|
|
84
|
+
|
|
85
|
+
"#{digits[0, point]}.#{digits[point..]}"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def exponent_form(digits, point)
|
|
89
|
+
fraction = digits.size > 1 ? digits[1..] : "0"
|
|
90
|
+
"#{digits[0]}.#{fraction}e#{point - 1}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
private_constant :FloatText
|
|
94
|
+
end
|