predicator 1.2.0 → 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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +42 -0
  3. data/{LICENSE.txt → LICENSE} +1 -1
  4. data/README.md +322 -46
  5. data/lib/predicator/ast.rb +74 -187
  6. data/lib/predicator/boundary.rb +237 -0
  7. data/lib/predicator/civil.rb +112 -0
  8. data/lib/predicator/duration_text.rb +92 -0
  9. data/lib/predicator/emitter/chains.rb +62 -0
  10. data/lib/predicator/emitter/statements.rb +86 -0
  11. data/lib/predicator/emitter.rb +187 -0
  12. data/lib/predicator/errors.rb +140 -0
  13. data/lib/predicator/evaluator/access.rb +108 -0
  14. data/lib/predicator/evaluator/arithmetic.rb +132 -0
  15. data/lib/predicator/evaluator/calls.rb +129 -0
  16. data/lib/predicator/evaluator/casts.rb +158 -0
  17. data/lib/predicator/evaluator/chronology.rb +88 -0
  18. data/lib/predicator/evaluator/compare.rb +158 -0
  19. data/lib/predicator/evaluator/json_functions.rb +167 -0
  20. data/lib/predicator/evaluator/json_scan.rb +303 -0
  21. data/lib/predicator/evaluator/library.rb +127 -0
  22. data/lib/predicator/evaluator/math_functions.rb +72 -0
  23. data/lib/predicator/evaluator/order.rb +120 -0
  24. data/lib/predicator/evaluator/rich_types.rb +146 -0
  25. data/lib/predicator/evaluator/statements.rb +151 -0
  26. data/lib/predicator/evaluator.rb +391 -117
  27. data/lib/predicator/float_text.rb +94 -0
  28. data/lib/predicator/iso_text.rb +99 -0
  29. data/lib/predicator/lexer/literals.rb +115 -0
  30. data/lib/predicator/lexer/numbers.rb +91 -0
  31. data/lib/predicator/lexer.rb +194 -0
  32. data/lib/predicator/parser/durations.rb +108 -0
  33. data/lib/predicator/parser/operators.rb +86 -0
  34. data/lib/predicator/parser/postfix.rb +57 -0
  35. data/lib/predicator/parser/primaries.rb +122 -0
  36. data/lib/predicator/parser/statements.rb +114 -0
  37. data/lib/predicator/parser/token_text.rb +52 -0
  38. data/lib/predicator/parser.rb +217 -438
  39. data/lib/predicator/result.rb +46 -0
  40. data/lib/predicator/tagged/reader.rb +139 -0
  41. data/lib/predicator/tagged/writer.rb +106 -0
  42. data/lib/predicator/tagged.rb +134 -0
  43. data/lib/predicator/values.rb +147 -0
  44. data/lib/predicator/version.rb +4 -1
  45. data/lib/predicator.rb +384 -14
  46. metadata +54 -145
  47. data/.gitignore +0 -11
  48. data/.rubocop.yml +0 -46
  49. data/.travis.yml +0 -5
  50. data/CODE_OF_CONDUCT.md +0 -49
  51. data/Gemfile +0 -4
  52. data/HISTORY.md +0 -47
  53. data/Rakefile +0 -24
  54. data/lib/predicator/context.rb +0 -18
  55. data/lib/predicator/lexer.rex +0 -69
  56. data/lib/predicator/lexer.rex.rb +0 -187
  57. data/lib/predicator/parser.y +0 -120
  58. data/lib/predicator/visitors/dot.rb +0 -113
  59. data/lib/predicator/visitors/each.rb +0 -16
  60. data/lib/predicator/visitors/instructions.rb +0 -183
  61. data/lib/predicator/visitors/string.rb +0 -76
  62. data/lib/predicator/visitors/visitor.rb +0 -100
  63. data/lib/predicator/visitors.rb +0 -5
  64. data/predicator.gemspec +0 -29
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Parser
5
+ # The operator levels of the expression grammar, from `OR` down to the
6
+ # prefix operators, read into the parse that includes this module.
7
+ module Operators
8
+ # Each level's operator tokens and the operator each one is.
9
+ OR = { or_op: :or, or_or: :or }.freeze
10
+ AND = { and_op: :and, and_and: :and }.freeze
11
+ ADDITIVE = { plus: :add, minus: :subtract }.freeze
12
+ MULTIPLICATIVE = { multiply: :multiply, divide: :divide, modulo: :modulo }.freeze
13
+ NEGATIONS = %i[not_op bang].freeze
14
+ UNARY = { minus: :minus, bang: :bang }.freeze
15
+
16
+ # The comparison and membership tokens: the node each builds and its
17
+ # operator.
18
+ RELATIONS = {
19
+ gt: [Ast::Comparison, :gt], lt: [Ast::Comparison, :lt], gte: [Ast::Comparison, :gte],
20
+ lte: [Ast::Comparison, :lte], equal_equal: [Ast::Comparison, :equal_equal], ne: [Ast::Comparison, :ne],
21
+ strict_equal: [Ast::Comparison, :strict_eq], strict_ne: [Ast::Comparison, :strict_ne],
22
+ in_op: [Ast::Membership, :in], contains_op: [Ast::Membership, :contains]
23
+ }.freeze
24
+
25
+ # A bare `=` where an expression goes on.
26
+ EQUALS = "'=' is not an equality operator - use '==' for equality. " \
27
+ "Assignment is only valid at the start of a statement."
28
+
29
+ private
30
+
31
+ def logical_or = chain(OR, :logical_and) { |_, left, right, at| Ast::LogicalOr.new(left:, right:, **at) }
32
+
33
+ def logical_and = chain(AND, :logical_not) { |_, left, right, at| Ast::LogicalAnd.new(left:, right:, **at) }
34
+
35
+ def addition = chain(ADDITIVE, :multiplication) { |*parts| arithmetic(*parts) }
36
+
37
+ def multiplication = chain(MULTIPLICATIVE, :unary) { |*parts| arithmetic(*parts) }
38
+
39
+ def arithmetic(operator, left, right, at) = Ast::Arithmetic.new(operator:, left:, right:, **at)
40
+
41
+ # A left-associative level: operands read by the method named, joined
42
+ # by the operators given, each join built by the block.
43
+ def chain(operators, operand)
44
+ left = send(operand)
45
+ while (operator = operators[peek.type])
46
+ token = advance
47
+ right = send(operand)
48
+ left = yield(operator, left, right, infix(token, left, right))
49
+ end
50
+ left
51
+ end
52
+
53
+ # The higher of the bang's two places: here it negates a whole
54
+ # comparison, below the comparison it negates an operand.
55
+ def logical_not
56
+ return comparison unless NEGATIONS.include?(peek.type)
57
+
58
+ token = advance
59
+ operand = descend { logical_not }
60
+ Ast::LogicalNot.new(operand:, **prefixed(token, operand))
61
+ end
62
+
63
+ def comparison
64
+ left = addition
65
+ refuse("assignment_in_expression", EQUALS, peek) if at?(:eq)
66
+ kind, operator = RELATIONS[peek.type]
67
+ return left unless kind
68
+
69
+ token = advance
70
+ right = addition
71
+ kind.new(operator:, left:, right:, **infix(token, left, right))
72
+ end
73
+
74
+ # A minus never folds into the number after it: `-5` is a negation.
75
+ def unary
76
+ operator = UNARY[peek.type]
77
+ return postfix unless operator
78
+
79
+ token = advance
80
+ operand = descend { unary }
81
+ Ast::Unary.new(operator:, operand:, **prefixed(token, operand))
82
+ end
83
+ end
84
+ private_constant :Operators
85
+ end
86
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Parser
5
+ # The postfix steps after a primary, read in a loop into the parse that
6
+ # includes this module: an index, a property and a cast. Each step's
7
+ # node is positioned at what it accesses - the key, the property name,
8
+ # the type name - and spans from its object's start.
9
+ module Postfix
10
+ # Each step's opening token and the method that reads the rest.
11
+ STEPS = { lbracket: :index, dot: :property, double_colon: :cast }.freeze
12
+
13
+ # The tokens that may name a property: an identifier, and the five
14
+ # relative-date words, which the lexer reserves.
15
+ PROPERTY_NAMES = %i[identifier last_op next_op ago_op from_op now_op].freeze
16
+
17
+ private
18
+
19
+ def postfix
20
+ node = primary
21
+ while (step = STEPS[peek.type])
22
+ advance
23
+ node = send(step, node)
24
+ end
25
+ node
26
+ end
27
+
28
+ def index(object)
29
+ key_token = peek
30
+ key = expression
31
+ close = expect(:rbracket, "expected_close_bracket", "Expected ']' but found")
32
+ Ast::BracketAccess.new(object:, key:, position: start_of(key_token), span: reaching(object, close))
33
+ end
34
+
35
+ def property(object)
36
+ name = peek
37
+ unless PROPERTY_NAMES.include?(name.type)
38
+ refuse("expected_property_name", "Expected property name after '.' but found #{describe(name)}", name)
39
+ end
40
+ advance
41
+ Ast::PropertyAccess.new(object:, property: name.value, position: start_of(name), span: reaching(object, name))
42
+ end
43
+
44
+ # The type name is an identifier matched against the seven names, so
45
+ # each stays usable as a variable, a property and an object key.
46
+ def cast(expression)
47
+ name = expect(:identifier, "expected_type_name", "Expected a type name after '::' but found")
48
+ unless CAST_TYPE_NAMES.include?(name.value)
49
+ refuse("unknown_cast_type",
50
+ "Unknown cast type '#{name.value}' - expected one of: #{CAST_TYPE_NAMES.join(", ")}", name)
51
+ end
52
+ Ast::Cast.new(expression:, type_name: name.value, position: start_of(name), span: reaching(expression, name))
53
+ end
54
+ end
55
+ private_constant :Postfix
56
+ end
57
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Parser
5
+ # The primaries: literals, identifiers, calls, parentheses, lists,
6
+ # objects and relative dates written `next` or `last`, read into the
7
+ # parse that includes this module. Each reader takes the token that
8
+ # started it, already consumed.
9
+ module Primaries
10
+ # The reader for each token a primary may start with; any other token
11
+ # is refused.
12
+ READERS = {
13
+ integer: :number, fractional_number: :fraction, float: :literal, boolean: :literal,
14
+ undefined: :literal, null: :literal, date: :literal, datetime: :literal, string: :string,
15
+ identifier: :identifier, function_name: :call, qualified_function_name: :call,
16
+ lparen: :parenthesized, lbracket: :list, lbrace: :object, next_op: :relative_date,
17
+ last_op: :relative_date, if_kw: :statement_keyword, else_kw: :statement_keyword,
18
+ while_kw: :statement_keyword
19
+ }.freeze
20
+
21
+ # What a primary may be, in the refusal of anything else.
22
+ EXPECTED = "Expected number, string, boolean, date, datetime, identifier, function call, list, object, " \
23
+ "or '(' but found"
24
+
25
+ private
26
+
27
+ def primary
28
+ reader = READERS.fetch(peek.type, :not_a_primary)
29
+ send(reader, advance)
30
+ end
31
+
32
+ def not_a_primary(token) = refuse("expected_primary", "#{EXPECTED} #{describe(token)}", token)
33
+
34
+ def statement_keyword(token)
35
+ refuse("statement_keyword", "'#{token.value}' is a statement keyword, not an expression - control flow " \
36
+ "is only valid in a program (Predicator.parse_program/2).", token)
37
+ end
38
+
39
+ def literal(token) = Ast::Literal.new(value: token.value, **leaf(token))
40
+
41
+ # A whole number, or the first component of a duration.
42
+ def number(token) = duration_from(token.value, start_of(token)) || literal(token)
43
+
44
+ # A number with a point that a unit follows, which the lexer writes
45
+ # only before a unit; without one, no primary starts with it.
46
+ def fraction(token) = duration_from(token.value, start_of(token)) || not_a_primary(token)
47
+
48
+ def string(token) = Ast::StringLiteral.new(value: token.value, quote: token.quote, **leaf(token))
49
+
50
+ def identifier(token) = Ast::Identifier.new(name: token.value, **leaf(token))
51
+
52
+ # A parenthesis keeps its expression's node and position and widens
53
+ # its span to the parentheses.
54
+ def parenthesized(open)
55
+ inner = expression
56
+ close = expect(:rparen, "expected_close_paren", "Expected ')' but found")
57
+ inner.with(span: between(start_of(open), end_of(close)))
58
+ end
59
+
60
+ # A call's name stays whole: the lexer writes a dotted name as one
61
+ # token only where a parenthesis follows it.
62
+ def call(name)
63
+ expect(:lparen, "expected_primary", EXPECTED)
64
+ arguments = items(:rparen) { expression }
65
+ close = expect(:rparen, "expected_close_paren", "Expected ')' but found")
66
+ Ast::FunctionCall.new(name: name.value, arguments:, **delimited(name, close))
67
+ end
68
+
69
+ def list(open)
70
+ elements = items(:rbracket) { expression }
71
+ close = expect(:rbracket, "expected_close_bracket", "Expected ']' but found")
72
+ Ast::ListLiteral.new(elements:, **delimited(open, close))
73
+ end
74
+
75
+ def object(open)
76
+ entries = items(:rbrace) { entry }
77
+ close = expect(:rbrace, "expected_close_brace", "Expected '}' but found")
78
+ Ast::ObjectLiteral.new(entries:, **delimited(open, close))
79
+ end
80
+
81
+ # Comma-separated items up to the closing token, which is left for the
82
+ # caller; none when it comes first.
83
+ def items(closer)
84
+ return [] if at?(closer)
85
+
86
+ found = [yield]
87
+ while at?(:comma)
88
+ advance
89
+ found << yield
90
+ end
91
+ found
92
+ end
93
+
94
+ # A key, bare or quoted, a colon and a value.
95
+ def entry
96
+ token = peek
97
+ style = token.type == :identifier ? :identifier : token.quote
98
+ unless %i[identifier string].include?(token.type)
99
+ refuse("expected_object_key", "Expected identifier or string for object key but found #{describe(token)}",
100
+ token)
101
+ end
102
+ advance
103
+ key = Ast::ObjectKey.new(name: token.value, style:, **leaf(token))
104
+ expect(:colon, "expected_object_colon", "Expected ':' after object key but found")
105
+ [key, expression]
106
+ end
107
+
108
+ # `next` or `last` and a bare duration; anything else after the word
109
+ # is refused at the token it started at.
110
+ def relative_date(keyword)
111
+ direction = keyword.type == :next_op ? :next : :last
112
+ start = peek
113
+ duration = descend { primary }
114
+ unless duration.is_a?(Ast::Duration)
115
+ refuse("expected_duration", "Expected duration after '#{direction}' but found #{describe(start)}", start)
116
+ end
117
+ Ast::RelativeDate.new(duration:, direction:, **prefixed(keyword, duration))
118
+ end
119
+ end
120
+ private_constant :Primaries
121
+ end
122
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Parser
5
+ # The statement grammar over the expression grammar, read into the parse
6
+ # that includes this module: sequences, assignments, `if` with its
7
+ # optional `else` and `else if`, `while`, and blocks.
8
+ module Statements
9
+ # The statements a keyword starts.
10
+ KEYWORDS = { if_kw: :if_statement, while_kw: :while_statement, else_kw: :stray_else }.freeze
11
+
12
+ # What an assignment may write: an identifier and any run of property
13
+ # and bracket accesses.
14
+ ACCESSES = [Ast::PropertyAccess, Ast::BracketAccess].freeze
15
+
16
+ private
17
+
18
+ # One statement or more up to the terminator, which is left for the
19
+ # caller. A separator is followed by another statement unless it is
20
+ # the one the terminator may follow; without one, another follows
21
+ # only a statement that ended in a closing brace.
22
+ def statement_sequence(terminator)
23
+ statements = [statement]
24
+ statements << statement while continues?(statements.last, terminator)
25
+ statements
26
+ end
27
+
28
+ def continues?(last, terminator)
29
+ return !at?(terminator) && (last.is_a?(Ast::If) || last.is_a?(Ast::While)) unless at?(:semicolon)
30
+
31
+ advance
32
+ !at?(terminator)
33
+ end
34
+
35
+ def statement
36
+ reader = KEYWORDS[peek.type]
37
+ reader ? send(reader, advance) : assignment_or_expression
38
+ end
39
+
40
+ def stray_else(token)
41
+ refuse("unexpected_else", "Unexpected 'else' - an 'else' block must follow an 'if' block.", token)
42
+ end
43
+
44
+ # What is left over after a finished statement.
45
+ def after_statement(token)
46
+ stray_else(token) if token.type == :else_kw
47
+ refuse("trailing_token", "Unexpected token #{describe(token)} after statement", token)
48
+ end
49
+
50
+ # An assignment when an addition-level expression is followed by `=`,
51
+ # and an expression read again from the start otherwise: a probe that
52
+ # was refused, or no `=` after it, is discarded.
53
+ def assignment_or_expression
54
+ start = @index
55
+ target = catch(REFUSED) { descend { addition } }
56
+ if target.is_a?(Lexer::Refusal) || !at?(:eq)
57
+ @index = start
58
+ return expression
59
+ end
60
+
61
+ assignment(target, advance)
62
+ end
63
+
64
+ # Past the `=`, a left side that is not a location is refused at it.
65
+ def assignment(target, equals)
66
+ unless location?(target)
67
+ refuse("unassignable_location", "Left side of '=' must be an assignable location - an identifier, " \
68
+ "a property access, or a bracket access.", equals)
69
+ end
70
+ value = expression
71
+ Ast::Assignment.new(target:, value:, **infix(equals, target, value))
72
+ end
73
+
74
+ def location?(node)
75
+ node = node.object while ACCESSES.any? { |access| node.is_a?(access) }
76
+ node.is_a?(Ast::Identifier)
77
+ end
78
+
79
+ def if_statement(keyword)
80
+ condition = expression
81
+ then_block = block
82
+ else_block = else_clause
83
+ Ast::If.new(condition:, then_block:, else_block:, **prefixed(keyword, else_block || then_block))
84
+ end
85
+
86
+ # An `else if` is an else block holding the one `if`, at its position
87
+ # and over its span; it nests a level as a block does.
88
+ def else_clause
89
+ return unless at?(:else_kw)
90
+
91
+ advance
92
+ return block unless at?(:if_kw)
93
+
94
+ nested = descend { if_statement(advance) }
95
+ Ast::Block.new(statements: [nested], position: nested.position, span: nested.span)
96
+ end
97
+
98
+ def while_statement(keyword)
99
+ condition = expression
100
+ body = block
101
+ Ast::While.new(condition:, body:, **prefixed(keyword, body))
102
+ end
103
+
104
+ # Braces around zero statements or more, one level deeper.
105
+ def block
106
+ open = expect(:lbrace, "expected_open_brace", "Expected '{' to open a block but found")
107
+ statements = at?(:rbrace) ? [] : descend { statement_sequence(:rbrace) }
108
+ close = expect(:rbrace, "expected_close_brace", "Expected '}' to close the block but found")
109
+ Ast::Block.new(statements:, **delimited(open, close))
110
+ end
111
+ end
112
+ private_constant :Statements
113
+ end
114
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Predicator
4
+ module Parser
5
+ # How a refusal's message names a token, in the reference's words: the
6
+ # word connectives uppercase whatever case the source wrote, the end of
7
+ # input in words, a string in double quotes, and every other token that
8
+ # carries a value as its kind and that value in single quotes. Read into
9
+ # the parse that includes this module.
10
+ module TokenText
11
+ # The tokens named without their value.
12
+ FIXED = {
13
+ undefined: "'undefined'", null: "'null'", gt: "'>'", lt: "'<'", gte: "'>='", lte: "'<='", eq: "'='",
14
+ ne: "'!='", equal_equal: "'=='", strict_equal: "'==='", strict_ne: "'!=='", and_op: "'AND'",
15
+ or_op: "'OR'", not_op: "'NOT'", in_op: "'IN'", contains_op: "'CONTAINS'", ago_op: "'ago'",
16
+ from_op: "'from'", now_op: "'now'", next_op: "'next'", last_op: "'last'", lparen: "'('", rparen: "')'",
17
+ lbracket: "'['", rbracket: "']'", lbrace: "'{'", rbrace: "'}'", colon: "':'", double_colon: "'::'",
18
+ comma: "','", semicolon: "';'", dot: "'.'", plus: "'+'", minus: "'-'", multiply: "'*'", divide: "'/'",
19
+ modulo: "'%'", and_and: "'&&'", or_or: "'||'", bang: "'!'", if_kw: "'if'", else_kw: "'else'",
20
+ while_kw: "'while'", eof: "end of input"
21
+ }.freeze
22
+
23
+ # The kind a token carrying a value is named by.
24
+ KINDS = {
25
+ integer: "number", float: "number", fractional_number: "number", boolean: "boolean", date: "date",
26
+ datetime: "datetime", identifier: "identifier", function_name: "function",
27
+ qualified_function_name: "function", duration_unit: "duration unit"
28
+ }.freeze
29
+
30
+ private
31
+
32
+ def describe(token)
33
+ return FIXED.fetch(token.type) if FIXED.key?(token.type)
34
+ return %(string "#{token.value}") if token.type == :string
35
+
36
+ "#{KINDS.fetch(token.type)} '#{spelling(token.value)}'"
37
+ end
38
+
39
+ # A float as the reference writes one; an infinite one, which the
40
+ # lexer answers for a decimal too long for a finite Float where the
41
+ # reference's lexer raises, as Ruby writes it.
42
+ def spelling(value)
43
+ case value
44
+ when Float then value.finite? ? FloatText.spell(value) : value.to_s
45
+ when Lexer::Fraction then "#{value.whole}.#{value.fraction}"
46
+ else value.to_s
47
+ end
48
+ end
49
+ end
50
+ private_constant :TokenText
51
+ end
52
+ end