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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "strscan"
|
|
5
|
+
require_relative "json_scan"
|
|
6
|
+
|
|
7
|
+
module Predicator
|
|
8
|
+
module Evaluator
|
|
9
|
+
# The library's JSON functions, over Ruby's standard json library (a
|
|
10
|
+
# default gem, not a dependency).
|
|
11
|
+
#
|
|
12
|
+
# `JSON.stringify` writes a value as JSON with every map's keys in sorted
|
|
13
|
+
# order, the order the conformance corpus fixes (a map has no order of
|
|
14
|
+
# its own). A Float is written as the reference writes one ({FloatText}).
|
|
15
|
+
# The values JSON has no form for are written as the reference writes
|
|
16
|
+
# them: a date and an instant as a JSON string of their ISO 8601 text, a
|
|
17
|
+
# duration as an object of its eight units, and undefined as the string
|
|
18
|
+
# "undefined". A value with no JSON form at all (a map key that is not a
|
|
19
|
+
# String, a String that is not valid in its encoding, an object outside
|
|
20
|
+
# the value domain) is written as the value's inspect, as the reference
|
|
21
|
+
# falls back to its own inspect, so the function does not fail on one.
|
|
22
|
+
#
|
|
23
|
+
# `JSON.parse` reads a JSON text into lists, maps with String keys and
|
|
24
|
+
# scalars; a key an object repeats keeps its first value, as the
|
|
25
|
+
# reference reads one ({JsonRead}). A text that is not JSON
|
|
26
|
+
# is refused with the reference's sentence naming the first fault and its
|
|
27
|
+
# offset in bytes ({JsonScan}): `Invalid JSON: unexpected byte 0x6F at
|
|
28
|
+
# position 1`, or `Invalid JSON: unexpected end of input at position 4`.
|
|
29
|
+
# Where the reference reports a sequence it cannot read (an unpaired
|
|
30
|
+
# surrogate escape, a number too large for a Float), the sentence is
|
|
31
|
+
# `Invalid JSON: unexpected sequence at position N`, N the offset where
|
|
32
|
+
# the sequence starts.
|
|
33
|
+
#
|
|
34
|
+
# Both refuse a list or a map nested more than {DEPTH_LIMIT} levels deep
|
|
35
|
+
# with the reason depth_limit_exceeded, the limit and the reason the
|
|
36
|
+
# tagged codec keeps; the outermost list or map is level one.
|
|
37
|
+
module JsonFunctions
|
|
38
|
+
# The deepest level of lists and maps either function reads or writes.
|
|
39
|
+
DEPTH_LIMIT = 100
|
|
40
|
+
|
|
41
|
+
# A value with no JSON form, somewhere inside the value written.
|
|
42
|
+
class NoForm < StandardError; end
|
|
43
|
+
|
|
44
|
+
# The JSON text of value, a list or a map at level depth.
|
|
45
|
+
def self.text(value, depth)
|
|
46
|
+
case value
|
|
47
|
+
when nil, true, false, Integer, String then string(value)
|
|
48
|
+
when Float then FloatText.spell(value)
|
|
49
|
+
when Array then deep(depth) && "[#{value.map { |member| text(member, depth + 1) }.join(",")}]"
|
|
50
|
+
when Hash then map(value, depth)
|
|
51
|
+
else tagged(value, depth)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A scalar's JSON text; a String that is not valid in its encoding has
|
|
56
|
+
# none.
|
|
57
|
+
def self.string(value)
|
|
58
|
+
JSON.generate(value)
|
|
59
|
+
rescue JSON::GeneratorError, EncodingError
|
|
60
|
+
raise NoForm
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.map(value, depth)
|
|
64
|
+
deep(depth)
|
|
65
|
+
raise NoForm unless value.each_key.all?(String)
|
|
66
|
+
|
|
67
|
+
members = value.keys.sort.map { |key| "#{string(key)}:#{text(value[key], depth + 1)}" }
|
|
68
|
+
"{#{members.join(",")}}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.tagged(value, depth)
|
|
72
|
+
case value
|
|
73
|
+
when Undefined then string("undefined")
|
|
74
|
+
when Predicator::Date, Predicator::DateTime then string(value.to_s)
|
|
75
|
+
when Duration then map(Access.units(value), depth)
|
|
76
|
+
else raise NoForm
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.deep(depth)
|
|
81
|
+
return true if depth <= DEPTH_LIMIT
|
|
82
|
+
|
|
83
|
+
raise Calls::Refusal, "depth_limit_exceeded"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private_class_method :string, :map, :tagged, :deep
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def json_stringify(value)
|
|
91
|
+
JsonFunctions.text(value, 1)
|
|
92
|
+
rescue JsonFunctions::NoForm
|
|
93
|
+
value.inspect
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def json_parse(text)
|
|
97
|
+
raise Calls::Refusal, "JSON.parse expects a string argument" unless text.is_a?(String)
|
|
98
|
+
|
|
99
|
+
fault = JsonScan.fault(text, DEPTH_LIMIT)
|
|
100
|
+
raise Calls::Refusal, fault if fault
|
|
101
|
+
|
|
102
|
+
JsonRead.new(text).value
|
|
103
|
+
rescue JSON::ParserError
|
|
104
|
+
# The scan accepts only what the reference reads, so Ruby's parser
|
|
105
|
+
# does not refuse a scalar of it; if it ever does, its own text stays
|
|
106
|
+
# here.
|
|
107
|
+
raise Calls::Refusal, "Invalid JSON: the text could not be read"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
private_constant :JsonFunctions
|
|
111
|
+
|
|
112
|
+
# Reads a JSON text {JsonScan} has accepted, its bytes read as UTF-8,
|
|
113
|
+
# into lists, maps and scalars. Each scalar's own text is read by Ruby's
|
|
114
|
+
# json library; the lists and maps are built here, because that library
|
|
115
|
+
# keeps the last value of a key an object repeats and the reference
|
|
116
|
+
# keeps the first. The scan has bounded the nesting, so the reader
|
|
117
|
+
# recurses.
|
|
118
|
+
class JsonRead
|
|
119
|
+
# The bytes a JSON text may hold between tokens.
|
|
120
|
+
SPACE = /[ \t\n\r]*/
|
|
121
|
+
# A scalar's text: a string, a number or a literal.
|
|
122
|
+
SCALAR = /"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null/n
|
|
123
|
+
|
|
124
|
+
def initialize(text)
|
|
125
|
+
@scanner = StringScanner.new(text.b)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The value at the scan, and past it.
|
|
129
|
+
def value
|
|
130
|
+
@scanner.skip(SPACE)
|
|
131
|
+
if @scanner.skip("[") then list
|
|
132
|
+
elsif @scanner.skip("{") then map
|
|
133
|
+
else JSON.parse(@scanner.scan(SCALAR).force_encoding(Encoding::UTF_8))
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def list
|
|
140
|
+
items = []
|
|
141
|
+
items << value until closed?("]")
|
|
142
|
+
items
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def map
|
|
146
|
+
members = {}
|
|
147
|
+
until closed?("}")
|
|
148
|
+
key = value
|
|
149
|
+
@scanner.skip(SPACE)
|
|
150
|
+
@scanner.skip(":")
|
|
151
|
+
member = value
|
|
152
|
+
members[key] = member unless members.key?(key)
|
|
153
|
+
end
|
|
154
|
+
members
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Whether the container closes here, past a comma between members.
|
|
158
|
+
def closed?(closer)
|
|
159
|
+
@scanner.skip(SPACE)
|
|
160
|
+
@scanner.skip(",")
|
|
161
|
+
@scanner.skip(SPACE)
|
|
162
|
+
!@scanner.skip(closer).nil?
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
private_constant :JsonRead
|
|
166
|
+
end
|
|
167
|
+
end
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Predicator
|
|
4
|
+
module Evaluator
|
|
5
|
+
# What every part of {JsonScan} shares: the byte at its offset, `@at`,
|
|
6
|
+
# over its bytes, `@bytes`, and the faults it throws as `:fault` with the
|
|
7
|
+
# sentence the text is refused with. The reference's reader reports
|
|
8
|
+
# three kinds: an unexpected byte at its offset, the end of the input,
|
|
9
|
+
# and a sequence it cannot read (its own report of that last kind is a
|
|
10
|
+
# term of its host language, so a sequence here is reported in a
|
|
11
|
+
# sentence of the same form as the other two).
|
|
12
|
+
module JsonFaults
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# The byte at the scan, or nil at the end.
|
|
16
|
+
def here = @bytes.getbyte(@at)
|
|
17
|
+
|
|
18
|
+
# The byte at the scan; the end of the text is a fault here.
|
|
19
|
+
def present = here || ending(@at)
|
|
20
|
+
|
|
21
|
+
def expect(byte)
|
|
22
|
+
unexpected unless present == byte
|
|
23
|
+
@at += 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def unexpected
|
|
27
|
+
throw :fault, format("Invalid JSON: unexpected byte 0x%<byte>02X at position %<at>d", byte: present, at: @at)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ending(at) = throw(:fault, "Invalid JSON: unexpected end of input at position #{at}")
|
|
31
|
+
def sequence(at) = throw(:fault, "Invalid JSON: unexpected sequence at position #{at}")
|
|
32
|
+
end
|
|
33
|
+
private_constant :JsonFaults
|
|
34
|
+
|
|
35
|
+
# A JSON string's bytes, as the reference's reader reads them: a control
|
|
36
|
+
# byte must be escaped; the bytes from 0x80 must be well-formed UTF-8,
|
|
37
|
+
# refused at the first byte where the sequence breaks; and a `\u` escape
|
|
38
|
+
# must have its four hexadecimal digits, a high surrogate must be
|
|
39
|
+
# followed by a `\u` escape of a low surrogate, and a low surrogate must
|
|
40
|
+
# not stand alone.
|
|
41
|
+
#
|
|
42
|
+
# Where the reference reads an escape's bytes ahead, it reports a text
|
|
43
|
+
# too short to hold them (six bytes from the backslash, twelve for a
|
|
44
|
+
# surrogate pair) as the end of the whole input, not of the string.
|
|
45
|
+
module JsonStrings
|
|
46
|
+
include JsonFaults
|
|
47
|
+
|
|
48
|
+
# The bytes a backslash in a string may precede, but for `u`.
|
|
49
|
+
ESCAPES = '"\\/bfnrt'.bytes.freeze
|
|
50
|
+
# For each UTF-8 lead byte, the range each byte after it must fall in.
|
|
51
|
+
CONTINUATIONS = [
|
|
52
|
+
[0xC2..0xDF, [0x80..0xBF]], [0xE0..0xE0, [0xA0..0xBF, 0x80..0xBF]],
|
|
53
|
+
[0xE1..0xEC, [0x80..0xBF] * 2], [0xED..0xED, [0x80..0x9F, 0x80..0xBF]], [0xEE..0xEF, [0x80..0xBF] * 2],
|
|
54
|
+
[0xF0..0xF0, [0x90..0xBF] + ([0x80..0xBF] * 2)], [0xF1..0xF3, [0x80..0xBF] * 3],
|
|
55
|
+
[0xF4..0xF4, [0x80..0x8F] + ([0x80..0xBF] * 2)]
|
|
56
|
+
].freeze
|
|
57
|
+
# The high and the low surrogates.
|
|
58
|
+
HIGH = 0xD800..0xDBFF
|
|
59
|
+
LOW = 0xDC00..0xDFFF
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# A string, from its opening quote past its closing one.
|
|
64
|
+
def string
|
|
65
|
+
@at += 1
|
|
66
|
+
content(present) until here == 0x22
|
|
67
|
+
@at += 1
|
|
68
|
+
:after
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# One byte of a string's content, and what it opens.
|
|
72
|
+
def content(byte)
|
|
73
|
+
case byte
|
|
74
|
+
when 0x5C then escape
|
|
75
|
+
when 0x00..0x1F then unexpected
|
|
76
|
+
when 0x80..0xFF then utf8(byte)
|
|
77
|
+
else @at += 1
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def utf8(lead)
|
|
82
|
+
ranges = CONTINUATIONS.find { |leads, _| leads.cover?(lead) }&.last || unexpected
|
|
83
|
+
@at += 1
|
|
84
|
+
ranges.each do |range|
|
|
85
|
+
unexpected unless range.cover?(present)
|
|
86
|
+
@at += 1
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# An escape, from its backslash.
|
|
91
|
+
def escape
|
|
92
|
+
start = @at
|
|
93
|
+
@at += 1
|
|
94
|
+
return unicode(start) if present == 0x75
|
|
95
|
+
|
|
96
|
+
unexpected unless ESCAPES.include?(present)
|
|
97
|
+
@at += 1
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def unicode(start)
|
|
101
|
+
code = code_at(start)
|
|
102
|
+
@at = start + 6
|
|
103
|
+
return unless HIGH.cover?(code) || LOW.cover?(code)
|
|
104
|
+
|
|
105
|
+
sequence(start) if LOW.cover?(code)
|
|
106
|
+
low_half(start)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The low surrogate a high one at start must be followed by.
|
|
110
|
+
def low_half(start)
|
|
111
|
+
ending(@bytes.bytesize) if @bytes.bytesize - start < 12
|
|
112
|
+
unexpected unless @bytes.byteslice(start + 6, 2) == "\\u"
|
|
113
|
+
sequence(start) unless LOW.cover?(digits(start + 6))
|
|
114
|
+
@at = start + 12
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The code a `\u` escape at start names.
|
|
118
|
+
def code_at(start)
|
|
119
|
+
ending(@bytes.bytesize) if @bytes.bytesize - start < 6
|
|
120
|
+
digits(start) || sequence(start)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def digits(start)
|
|
124
|
+
hex = @bytes.byteslice(start + 2, 4)
|
|
125
|
+
hex.to_i(16) if hex.match?(/\A\h{4}\z/)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
private_constant :JsonStrings
|
|
129
|
+
|
|
130
|
+
# The scalars of a JSON text besides its strings: numbers and the three
|
|
131
|
+
# literals. A number too large for a Float is a sequence the reference
|
|
132
|
+
# cannot read, reported at the number's first byte.
|
|
133
|
+
module JsonScalars
|
|
134
|
+
include JsonStrings
|
|
135
|
+
|
|
136
|
+
# The literal each first byte begins.
|
|
137
|
+
LITERALS = { 0x74 => "true", 0x66 => "false", 0x6E => "null" }.transform_values(&:bytes).freeze
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def number
|
|
142
|
+
start = @at
|
|
143
|
+
@at += 1 if here == 0x2D
|
|
144
|
+
present == 0x30 ? @at += 1 : digits_run
|
|
145
|
+
fraction_and_exponent
|
|
146
|
+
token = @bytes.byteslice(start, @at - start)
|
|
147
|
+
sequence(start) if token.match?(/[.eE]/) && !JSON.parse(token).finite?
|
|
148
|
+
:after
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def fraction_and_exponent
|
|
152
|
+
if here == 0x2E
|
|
153
|
+
@at += 1
|
|
154
|
+
digits_run
|
|
155
|
+
end
|
|
156
|
+
return unless [0x65, 0x45].include?(here)
|
|
157
|
+
|
|
158
|
+
@at += 1
|
|
159
|
+
@at += 1 if [0x2B, 0x2D].include?(here)
|
|
160
|
+
digits_run
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def digits_run
|
|
164
|
+
unexpected unless present.between?(0x30, 0x39)
|
|
165
|
+
@at += 1 while here&.between?(0x30, 0x39)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def literal(bytes)
|
|
169
|
+
bytes.each { |byte| expect(byte) }
|
|
170
|
+
:after
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
private_constant :JsonScalars
|
|
174
|
+
|
|
175
|
+
# Finds the first fault in a JSON text, by byte offset, as the
|
|
176
|
+
# reference's JSON reader reports it. JSON.parse is asked to read only a
|
|
177
|
+
# text this has accepted, because the sentence a malformed text is
|
|
178
|
+
# refused with names the offending byte and its offset, and Ruby's
|
|
179
|
+
# parser's own messages are its own.
|
|
180
|
+
#
|
|
181
|
+
# Content after the value is refused at the first byte after the value,
|
|
182
|
+
# a space included, as the reference refuses it. The scan walks the
|
|
183
|
+
# text's bytes once, keeping the open lists and maps on a stack of its
|
|
184
|
+
# own rather than recursing, so a deeply nested text cannot exhaust
|
|
185
|
+
# Ruby's stack. It holds no state between scans.
|
|
186
|
+
class JsonScan
|
|
187
|
+
include JsonScalars
|
|
188
|
+
|
|
189
|
+
# The bytes a JSON text may hold between tokens.
|
|
190
|
+
SPACE = [0x20, 0x09, 0x0A, 0x0D].freeze
|
|
191
|
+
# The byte that closes each kind of container.
|
|
192
|
+
CLOSERS = { array: 0x5D, object: 0x7D }.freeze
|
|
193
|
+
|
|
194
|
+
# Why text is not a JSON text: "Invalid JSON: " and the fault, or
|
|
195
|
+
# "depth_limit_exceeded" when it nests past limit levels; nil when it
|
|
196
|
+
# is one.
|
|
197
|
+
#
|
|
198
|
+
# @param text [String] the text, its bytes read as UTF-8
|
|
199
|
+
# @param limit [Integer] the deepest level of lists and maps accepted
|
|
200
|
+
# @return [String, nil]
|
|
201
|
+
def self.fault(text, limit) = new(text, limit).fault
|
|
202
|
+
|
|
203
|
+
def initialize(text, limit)
|
|
204
|
+
@bytes = text.b
|
|
205
|
+
@limit = limit
|
|
206
|
+
@at = 0
|
|
207
|
+
@open = []
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def fault
|
|
211
|
+
catch(:fault) do
|
|
212
|
+
space
|
|
213
|
+
walk
|
|
214
|
+
trailing
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
# Only space may follow the value; anything else is refused at the
|
|
221
|
+
# first byte after the value.
|
|
222
|
+
def trailing
|
|
223
|
+
after = @at
|
|
224
|
+
space
|
|
225
|
+
return if @at == @bytes.bytesize
|
|
226
|
+
|
|
227
|
+
@at = after
|
|
228
|
+
unexpected
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Scans one value and, while containers are open, the rest of each.
|
|
232
|
+
def walk
|
|
233
|
+
state = value
|
|
234
|
+
state = state == :first ? first_member : next_member until state == :done
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def first_member
|
|
238
|
+
space
|
|
239
|
+
return close if closer?
|
|
240
|
+
|
|
241
|
+
member_start
|
|
242
|
+
value
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def next_member
|
|
246
|
+
return :done if @open.empty?
|
|
247
|
+
|
|
248
|
+
space
|
|
249
|
+
return close if closer?
|
|
250
|
+
|
|
251
|
+
expect(0x2C)
|
|
252
|
+
space
|
|
253
|
+
member_start
|
|
254
|
+
value
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def closer? = here == CLOSERS.fetch(@open.last)
|
|
258
|
+
|
|
259
|
+
def close
|
|
260
|
+
@at += 1
|
|
261
|
+
@open.pop
|
|
262
|
+
:after
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# An object's member opens with its key and a colon.
|
|
266
|
+
def member_start
|
|
267
|
+
return unless @open.last == :object
|
|
268
|
+
|
|
269
|
+
unexpected unless present == 0x22
|
|
270
|
+
string
|
|
271
|
+
space
|
|
272
|
+
expect(0x3A)
|
|
273
|
+
space
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Scans a scalar (answering :after) or enters a container (:first).
|
|
277
|
+
def value
|
|
278
|
+
byte = here
|
|
279
|
+
case byte
|
|
280
|
+
when 0x5B then enter(:array)
|
|
281
|
+
when 0x7B then enter(:object)
|
|
282
|
+
when 0x22 then string
|
|
283
|
+
when 0x2D, 0x30..0x39 then number
|
|
284
|
+
when *LITERALS.keys then literal(LITERALS.fetch(byte))
|
|
285
|
+
else unexpected
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def enter(kind)
|
|
290
|
+
throw :fault, "depth_limit_exceeded" if @open.size >= @limit
|
|
291
|
+
|
|
292
|
+
@open.push(kind)
|
|
293
|
+
@at += 1
|
|
294
|
+
:first
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def space
|
|
298
|
+
@at += 1 while SPACE.include?(here)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
private_constant :JsonScan
|
|
302
|
+
end
|
|
303
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "math_functions"
|
|
4
|
+
require_relative "json_functions"
|
|
5
|
+
|
|
6
|
+
module Predicator
|
|
7
|
+
module Evaluator
|
|
8
|
+
# The package's function library: the functions the reference ships as
|
|
9
|
+
# its builtins (its string, list, date, math and JSON functions), by the
|
|
10
|
+
# names a `call` reaches them under, with the counts of arguments each
|
|
11
|
+
# takes. {Calls} looks a name up here after the caller's own functions.
|
|
12
|
+
#
|
|
13
|
+
# Each function is a private method of {Machine} that takes the call's
|
|
14
|
+
# arguments and answers its value, or raises {Calls::Refusal} with the
|
|
15
|
+
# sentence the reference refuses those arguments with. This module holds
|
|
16
|
+
# the string, list and date functions; {MathFunctions} and
|
|
17
|
+
# {JsonFunctions} hold the rest.
|
|
18
|
+
#
|
|
19
|
+
# A string's length and a substring's offsets count grapheme clusters,
|
|
20
|
+
# and `index_of` answers a byte offset into the string's UTF-8, as the
|
|
21
|
+
# reference's own string functions do. `trim` removes the characters
|
|
22
|
+
# with the Unicode White_Space property from both ends.
|
|
23
|
+
module Library
|
|
24
|
+
include MathFunctions
|
|
25
|
+
include JsonFunctions
|
|
26
|
+
|
|
27
|
+
# One library function: the counts of arguments it takes and the
|
|
28
|
+
# Machine method that runs it.
|
|
29
|
+
Entry = Data.define(:counts, :handler)
|
|
30
|
+
|
|
31
|
+
# The library, by name.
|
|
32
|
+
FUNCTIONS = {
|
|
33
|
+
"len" => [1..1, :string_len], "upper" => [1..1, :string_upper], "lower" => [1..1, :string_lower],
|
|
34
|
+
"trim" => [1..1, :string_trim], "starts_with" => [2..2, :string_starts_with?],
|
|
35
|
+
"ends_with" => [2..2, :string_ends_with?], "substring" => [2..3, :string_substring],
|
|
36
|
+
"index_of" => [2..2, :string_index_of], "concat" => [2..2, :list_concat],
|
|
37
|
+
"Date.year" => [1..1, :date_year], "Date.month" => [1..1, :date_month], "Date.day" => [1..1, :date_day],
|
|
38
|
+
"Date.now" => [0..0, :date_now],
|
|
39
|
+
"Math.pow" => [2..2, :math_pow], "Math.sqrt" => [1..1, :math_sqrt], "Math.abs" => [1..1, :math_abs],
|
|
40
|
+
"Math.floor" => [1..1, :math_floor], "Math.ceil" => [1..1, :math_ceil], "Math.round" => [1..1, :math_round],
|
|
41
|
+
"Math.min" => [2..2, :math_min], "Math.max" => [2..2, :math_max], "Math.random" => [0..0, :math_random],
|
|
42
|
+
"JSON.stringify" => [1..1, :json_stringify], "JSON.parse" => [1..1, :json_parse]
|
|
43
|
+
}.transform_values { |counts, handler| Entry.new(counts:, handler:) }.freeze
|
|
44
|
+
|
|
45
|
+
# The ends `trim` removes.
|
|
46
|
+
LEADING_SPACE = /\A\p{White_Space}+/
|
|
47
|
+
TRAILING_SPACE = /\p{White_Space}+\z/
|
|
48
|
+
# What substring says of arguments of the wrong types, with two
|
|
49
|
+
# arguments (true) and with three.
|
|
50
|
+
SUBSTRING_TYPES = {
|
|
51
|
+
true => "substring() expects a string and an integer start index",
|
|
52
|
+
false => "substring() expects a string, an integer start index, and an integer length"
|
|
53
|
+
}.freeze
|
|
54
|
+
private_constant :LEADING_SPACE, :TRAILING_SPACE, :SUBSTRING_TYPES
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def string_len(value) = string_arg("len", value).grapheme_clusters.size
|
|
59
|
+
def string_upper(value) = string_arg("upper", value).upcase
|
|
60
|
+
def string_lower(value) = string_arg("lower", value).downcase
|
|
61
|
+
def string_trim(value) = string_arg("trim", value).sub(LEADING_SPACE, "").sub(TRAILING_SPACE, "")
|
|
62
|
+
|
|
63
|
+
def string_arg(name, value)
|
|
64
|
+
return value if value.is_a?(String)
|
|
65
|
+
|
|
66
|
+
raise Calls::Refusal, "#{name}() expects a string argument"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def string_starts_with?(value, prefix) = two_strings("starts_with", value, prefix).start_with?(prefix)
|
|
70
|
+
def string_ends_with?(value, suffix) = two_strings("ends_with", value, suffix).end_with?(suffix)
|
|
71
|
+
def string_index_of(value, sub) = two_strings("index_of", value, sub).byteindex(sub) || -1
|
|
72
|
+
|
|
73
|
+
def two_strings(name, value, other)
|
|
74
|
+
return value if value.is_a?(String) && other.is_a?(String)
|
|
75
|
+
|
|
76
|
+
raise Calls::Refusal, "#{name}() expects two string arguments"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The grapheme clusters from start, all of them or length of them. A
|
|
80
|
+
# start or a length past the end answers what the string has. The
|
|
81
|
+
# call's shape is its count of arguments, so a third argument of null
|
|
82
|
+
# is a length that is not an Integer, not a missing one.
|
|
83
|
+
def string_substring(value, start, *length)
|
|
84
|
+
two = length.empty?
|
|
85
|
+
substring_types(value, start, length, two)
|
|
86
|
+
substring_signs(start, length.first, two)
|
|
87
|
+
clusters = value.grapheme_clusters
|
|
88
|
+
(clusters[start, two ? clusters.size : length.first] || []).join
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def substring_types(value, start, length, two)
|
|
92
|
+
return if value.is_a?(String) && start.is_a?(Integer) && length.all?(Integer)
|
|
93
|
+
|
|
94
|
+
raise Calls::Refusal, SUBSTRING_TYPES.fetch(two)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def substring_signs(start, length, two)
|
|
98
|
+
return unless start.negative? || (!two && length.negative?)
|
|
99
|
+
|
|
100
|
+
raise Calls::Refusal, "substring() expects a non-negative start index#{" and length" unless two}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def list_concat(left, right)
|
|
104
|
+
return left + right if left.is_a?(Array) && right.is_a?(Array)
|
|
105
|
+
|
|
106
|
+
raise Calls::Refusal, "concat() expects two list arguments"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def date_year(value) = civil_date("Date.year", value).year
|
|
110
|
+
def date_month(value) = civil_date("Date.month", value).month
|
|
111
|
+
def date_day(value) = civil_date("Date.day", value).day
|
|
112
|
+
|
|
113
|
+
# The date a date names, or the date in UTC an instant falls on.
|
|
114
|
+
def civil_date(name, value)
|
|
115
|
+
return value if value.is_a?(Predicator::Date)
|
|
116
|
+
return Civil.date_of(value) if value.is_a?(Predicator::DateTime)
|
|
117
|
+
|
|
118
|
+
raise Calls::Refusal, "#{name}() expects a date or datetime argument"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The current time of this evaluation: the clock the caller passed,
|
|
122
|
+
# read at most once per evaluation, as relative_date reads it.
|
|
123
|
+
def date_now = now
|
|
124
|
+
end
|
|
125
|
+
private_constant :Library
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Predicator
|
|
4
|
+
module Evaluator
|
|
5
|
+
# The library's math functions, as the reference's own: each takes
|
|
6
|
+
# numbers (an Integer or a Float) and refuses anything else with the
|
|
7
|
+
# reference's sentence for it.
|
|
8
|
+
#
|
|
9
|
+
# Integers stay exact where the reference keeps them exact: `Math.pow`
|
|
10
|
+
# of two Integers with a non-negative exponent, `Math.sqrt` of an
|
|
11
|
+
# Integer with an exact Integer root, `Math.abs`, `Math.min` and
|
|
12
|
+
# `Math.max` of Integers. `Math.floor`, `Math.ceil` and `Math.round`
|
|
13
|
+
# answer an Integer, rounding half away from zero. Where the reference's
|
|
14
|
+
# float arithmetic has no real, finite answer (`Math.pow(-8, 0.5)`, an
|
|
15
|
+
# overflow), the call is refused rather than answering a complex number
|
|
16
|
+
# or an infinity, which the value domain does not hold.
|
|
17
|
+
module MathFunctions
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def math_pow(base, exponent)
|
|
21
|
+
two_numbers("Math.pow", base, exponent)
|
|
22
|
+
return base**exponent if base.is_a?(Integer) && exponent.is_a?(Integer) && !exponent.negative?
|
|
23
|
+
|
|
24
|
+
power = base.to_f**exponent.to_f
|
|
25
|
+
return power if power.is_a?(Float) && power.finite?
|
|
26
|
+
|
|
27
|
+
raise Calls::Refusal, "Math.pow has no finite real answer for #{base} and #{exponent}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def math_sqrt(value)
|
|
31
|
+
raise Calls::Refusal, "Math.sqrt expects a numeric argument" unless number?(value)
|
|
32
|
+
raise Calls::Refusal, "Math.sqrt expects a non-negative number" if value.negative?
|
|
33
|
+
|
|
34
|
+
if value.is_a?(Integer)
|
|
35
|
+
root = Integer.sqrt(value)
|
|
36
|
+
return root if root * root == value
|
|
37
|
+
end
|
|
38
|
+
Math.sqrt(value)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def math_abs(value) = numeric("Math.abs", value).abs
|
|
42
|
+
def math_floor(value) = numeric("Math.floor", value).to_f.floor
|
|
43
|
+
def math_ceil(value) = numeric("Math.ceil", value).to_f.ceil
|
|
44
|
+
def math_round(value) = numeric("Math.round", value).round
|
|
45
|
+
|
|
46
|
+
# The smaller of the two, the first when they are equal.
|
|
47
|
+
def math_min(left, right) = two_numbers("Math.min", left, right) && [left, right].min
|
|
48
|
+
|
|
49
|
+
# The larger of the two, the first when they are equal.
|
|
50
|
+
def math_max(left, right) = two_numbers("Math.max", left, right) && [left, right].max
|
|
51
|
+
|
|
52
|
+
# A Float from 0.0 up to 1.0, from a generator of its own, so that no
|
|
53
|
+
# state outside this call is read or moved.
|
|
54
|
+
def math_random = Random.new.rand
|
|
55
|
+
|
|
56
|
+
def number?(value) = value.is_a?(Integer) || value.is_a?(Float)
|
|
57
|
+
|
|
58
|
+
def numeric(name, value)
|
|
59
|
+
return value if number?(value)
|
|
60
|
+
|
|
61
|
+
raise Calls::Refusal, "#{name} expects a numeric argument"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def two_numbers(name, left, right)
|
|
65
|
+
return true if number?(left) && number?(right)
|
|
66
|
+
|
|
67
|
+
raise Calls::Refusal, "#{name} expects two numeric arguments"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
private_constant :MathFunctions
|
|
71
|
+
end
|
|
72
|
+
end
|