dentaku 3.4.2 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +3 -4
  4. data/lib/dentaku/ast/access.rb +6 -0
  5. data/lib/dentaku/ast/arithmetic.rb +5 -1
  6. data/lib/dentaku/ast/array.rb +4 -0
  7. data/lib/dentaku/ast/bitwise.rb +8 -0
  8. data/lib/dentaku/ast/case/case_conditional.rb +4 -0
  9. data/lib/dentaku/ast/case/case_else.rb +6 -0
  10. data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
  11. data/lib/dentaku/ast/case/case_then.rb +6 -0
  12. data/lib/dentaku/ast/case/case_when.rb +10 -0
  13. data/lib/dentaku/ast/case.rb +6 -0
  14. data/lib/dentaku/ast/comparators.rb +25 -35
  15. data/lib/dentaku/ast/function.rb +6 -8
  16. data/lib/dentaku/ast/functions/all.rb +4 -17
  17. data/lib/dentaku/ast/functions/any.rb +4 -17
  18. data/lib/dentaku/ast/functions/duration.rb +2 -2
  19. data/lib/dentaku/ast/functions/enum.rb +37 -0
  20. data/lib/dentaku/ast/functions/filter.rb +4 -17
  21. data/lib/dentaku/ast/functions/if.rb +4 -0
  22. data/lib/dentaku/ast/functions/map.rb +3 -16
  23. data/lib/dentaku/ast/functions/pluck.rb +8 -7
  24. data/lib/dentaku/ast/functions/ruby_math.rb +3 -2
  25. data/lib/dentaku/ast/functions/xor.rb +44 -0
  26. data/lib/dentaku/ast/identifier.rb +8 -0
  27. data/lib/dentaku/ast/literal.rb +10 -0
  28. data/lib/dentaku/ast/negation.rb +4 -0
  29. data/lib/dentaku/ast/nil.rb +4 -0
  30. data/lib/dentaku/ast/node.rb +4 -0
  31. data/lib/dentaku/ast/operation.rb +9 -0
  32. data/lib/dentaku/ast/string.rb +7 -0
  33. data/lib/dentaku/ast.rb +2 -0
  34. data/lib/dentaku/parser.rb +5 -3
  35. data/lib/dentaku/print_visitor.rb +101 -0
  36. data/lib/dentaku/token_scanner.rb +1 -1
  37. data/lib/dentaku/version.rb +1 -1
  38. data/lib/dentaku/visitor/infix.rb +82 -0
  39. data/spec/ast/all_spec.rb +25 -0
  40. data/spec/ast/any_spec.rb +23 -0
  41. data/spec/ast/comparator_spec.rb +6 -9
  42. data/spec/ast/filter_spec.rb +7 -0
  43. data/spec/ast/function_spec.rb +5 -0
  44. data/spec/ast/map_spec.rb +12 -0
  45. data/spec/ast/pluck_spec.rb +32 -0
  46. data/spec/ast/xor_spec.rb +35 -0
  47. data/spec/calculator_spec.rb +58 -2
  48. data/spec/parser_spec.rb +18 -3
  49. data/spec/print_visitor_spec.rb +66 -0
  50. data/spec/tokenizer_spec.rb +6 -0
  51. data/spec/visitor/infix_spec.rb +31 -0
  52. data/spec/visitor_spec.rb +137 -0
  53. metadata +24 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3dadbc209e58b2d2206dfaf48b83be3224db37f71d60cf590aadb63be9f4bebd
4
- data.tar.gz: 86530c9c3333139994775a6bde16202005d89161a0b98670db58bcdda47f8e55
3
+ metadata.gz: 9a0b093e29b178197c0f92c1f63ec56342716c1fa84a4d12a73a7d355d42bc76
4
+ data.tar.gz: 480ccf9248568006227518363a0ef6350843007389f2e94ac5610ae62028e87e
5
5
  SHA512:
6
- metadata.gz: f132401168218a3c021124ddabb56bbf485550bdff60a03a291ad811d8628e25caab2ba36e4bed4f8148bde048e56bed1fa31480774f2c19617a79282da81280
7
- data.tar.gz: 3b6a8fc5e40988a9442be9dbdf890dc813e13b95fc2d0cb2f60252b2492d07fdf4ba47cdfedd0a85c002aaaa23a83bffb141b8f8f31fbf518678439d55c1e795
6
+ metadata.gz: 904292b2d2fd834701fd18900d689b9125579d58421fc58aaad03cd75c0fb556eeaeb5635dcd034a3f29e9f70f5c8fe0370e605acefd599c3dd75eae64436fdd
7
+ data.tar.gz: 3b6ed8763b9241e55e85f1a1e5a09d2652ec91eee21c080ca825029e04867b8fe65b128ddfc557cab3ef4fae76abb30b936f5971326355ea763081f90ba66638
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Change Log
2
2
 
3
+ ## [v3.5.0]
4
+ - fix bug with function argument count
5
+ - add XOR operator
6
+ - make function args publicly accessible
7
+ - better argument handling for collection functions
8
+ - better dependency reporting for collection functions
9
+ - allow ruby math-backed functions to be serialized
10
+ - improve scientific notation handling
11
+ - improve comparator argument errors
12
+ - respect case sensitivity in nested case statments
13
+ - add visitor pattern
14
+
3
15
  ## [v3.4.2]
4
16
  - add FILTER function
5
17
  - add concurrent-ruby dependency to make global calculator object thread safe
@@ -212,6 +224,7 @@
212
224
  ## [v0.1.0] 2012-01-20
213
225
  - initial release
214
226
 
227
+ [v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
215
228
  [v3.4.2]: https://github.com/rubysolo/dentaku/compare/v3.4.1...v3.4.2
216
229
  [v3.4.1]: https://github.com/rubysolo/dentaku/compare/v3.4.0...v3.4.1
217
230
  [v3.4.0]: https://github.com/rubysolo/dentaku/compare/v3.3.4...v3.4.0
data/README.md CHANGED
@@ -5,7 +5,6 @@ Dentaku
5
5
  [![Gem Version](https://badge.fury.io/rb/dentaku.png)](http://badge.fury.io/rb/dentaku)
6
6
  [![Build Status](https://travis-ci.org/rubysolo/dentaku.png?branch=master)](https://travis-ci.org/rubysolo/dentaku)
7
7
  [![Code Climate](https://codeclimate.com/github/rubysolo/dentaku.png)](https://codeclimate.com/github/rubysolo/dentaku)
8
- [![Hakiri](https://hakiri.io/github/rubysolo/dentaku/master.svg)](https://hakiri.io/github/rubysolo/dentaku)
9
8
  [![Coverage](https://codecov.io/gh/rubysolo/dentaku/branch/master/graph/badge.svg)](https://codecov.io/gh/rubysolo/dentaku)
10
9
 
11
10
 
@@ -144,11 +143,11 @@ Also, all functions from Ruby's Math module, including `SIN`, `COS`, `TAN`, etc.
144
143
 
145
144
  Comparison: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`,
146
145
 
147
- Logic: `IF`, `AND`, `OR`, `NOT`, `SWITCH`
146
+ Logic: `IF`, `AND`, `OR`, `XOR`, `NOT`, `SWITCH`
148
147
 
149
148
  Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
150
149
 
151
- Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L292))
150
+ Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L593))
152
151
 
153
152
  String: `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, `CONCAT`, `CONTAINS`
154
153
 
@@ -323,7 +322,7 @@ LICENSE
323
322
 
324
323
  (The MIT License)
325
324
 
326
- Copyright © 2012-2019 Solomon White
325
+ Copyright © 2012-2022 Solomon White
327
326
 
328
327
  Permission is hereby granted, free of charge, to any person obtaining a copy of
329
328
  this software and associated documentation files (the ‘Software’), to deal in
@@ -3,6 +3,8 @@ require_relative "./node"
3
3
  module Dentaku
4
4
  module AST
5
5
  class Access < Node
6
+ attr_reader :structure, :index
7
+
6
8
  def self.arity
7
9
  2
8
10
  end
@@ -36,6 +38,10 @@ module Dentaku
36
38
  def type
37
39
  nil
38
40
  end
41
+
42
+ def accept(visitor)
43
+ visitor.visit_access(self)
44
+ end
39
45
  end
40
46
  end
41
47
  end
@@ -57,7 +57,7 @@ module Dentaku
57
57
  end
58
58
 
59
59
  def valid_node?(node)
60
- node && (node.type == :numeric || node.dependencies.any?)
60
+ node && (node.type == :numeric || node.type == :integer || node.dependencies.any?)
61
61
  end
62
62
 
63
63
  def valid_left?
@@ -220,6 +220,10 @@ module Dentaku
220
220
  :**
221
221
  end
222
222
 
223
+ def display_operator
224
+ "^"
225
+ end
226
+
223
227
  def self.precedence
224
228
  30
225
229
  end
@@ -32,6 +32,10 @@ module Dentaku
32
32
  def type
33
33
  nil
34
34
  end
35
+
36
+ def accept(visitor)
37
+ visitor.visit_array(self)
38
+ end
35
39
  end
36
40
  end
37
41
  end
@@ -6,12 +6,20 @@ module Dentaku
6
6
  def value(context = {})
7
7
  left.value(context) | right.value(context)
8
8
  end
9
+
10
+ def operator
11
+ :|
12
+ end
9
13
  end
10
14
 
11
15
  class BitwiseAnd < Operation
12
16
  def value(context = {})
13
17
  left.value(context) & right.value(context)
14
18
  end
19
+
20
+ def operator
21
+ :&
22
+ end
15
23
  end
16
24
  end
17
25
  end
@@ -29,6 +29,10 @@ module Dentaku
29
29
  def dependencies(context = {})
30
30
  @when.dependencies(context) + @then.dependencies(context)
31
31
  end
32
+
33
+ def accept(visitor)
34
+ visitor.visit_case_conditional(self)
35
+ end
32
36
  end
33
37
  end
34
38
  end
@@ -1,6 +1,8 @@
1
1
  module Dentaku
2
2
  module AST
3
3
  class CaseElse < Node
4
+ attr_reader :node
5
+
4
6
  def initialize(node)
5
7
  @node = node
6
8
  end
@@ -24,6 +26,10 @@ module Dentaku
24
26
  def self.max_param_count
25
27
  1
26
28
  end
29
+
30
+ def accept(visitor)
31
+ visitor.visit_else(self)
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -1,6 +1,8 @@
1
1
  module Dentaku
2
2
  module AST
3
3
  class CaseSwitchVariable < Node
4
+ attr_reader :node
5
+
4
6
  def initialize(node)
5
7
  @node = node
6
8
  end
@@ -24,6 +26,10 @@ module Dentaku
24
26
  def self.max_param_count
25
27
  1
26
28
  end
29
+
30
+ def accept(visitor)
31
+ visitor.visit_switch(self)
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -1,6 +1,8 @@
1
1
  module Dentaku
2
2
  module AST
3
3
  class CaseThen < Node
4
+ attr_reader :node
5
+
4
6
  def initialize(node)
5
7
  @node = node
6
8
  end
@@ -24,6 +26,10 @@ module Dentaku
24
26
  def self.max_param_count
25
27
  1
26
28
  end
29
+
30
+ def accept(visitor)
31
+ visitor.visit_then(self)
32
+ end
27
33
  end
28
34
  end
29
35
  end
@@ -1,6 +1,8 @@
1
1
  module Dentaku
2
2
  module AST
3
3
  class CaseWhen < Operation
4
+ attr_reader :node
5
+
4
6
  def initialize(node)
5
7
  @node = node
6
8
  end
@@ -24,6 +26,14 @@ module Dentaku
24
26
  def self.max_param_count
25
27
  1
26
28
  end
29
+
30
+ def accept(visitor)
31
+ visitor.visit_when(self)
32
+ end
33
+
34
+ def to_s
35
+ 'WHEN'
36
+ end
27
37
  end
28
38
  end
29
39
  end
@@ -8,6 +8,8 @@ require 'dentaku/exceptions'
8
8
  module Dentaku
9
9
  module AST
10
10
  class Case < Node
11
+ attr_reader :switch, :conditions, :else
12
+
11
13
  def self.min_param_count
12
14
  2
13
15
  end
@@ -57,6 +59,10 @@ module Dentaku
57
59
  else_dependencies(context)
58
60
  end
59
61
 
62
+ def accept(visitor)
63
+ visitor.visit_case(self)
64
+ end
65
+
60
66
  private
61
67
 
62
68
  def switch_dependencies(context = {})
@@ -15,74 +15,64 @@ module Dentaku
15
15
  raise NotImplementedError
16
16
  end
17
17
 
18
+ def value(context = {})
19
+ l = validate_value(left.value(context))
20
+ r = validate_value(right.value(context))
21
+
22
+ l.public_send(operator, r)
23
+ rescue ::ArgumentError => e
24
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
25
+ end
26
+
18
27
  private
19
28
 
20
- def value
21
- yield
22
- rescue ::ArgumentError => argument_error
23
- raise Dentaku::ArgumentError, argument_error.message
24
- rescue NoMethodError => no_method_error
25
- raise Dentaku::Error, no_method_error.message
29
+ def validate_value(value)
30
+ unless value.respond_to?(operator)
31
+ raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator),
32
+ "#{ self.class } requires operands that respond to #{operator}"
33
+ end
34
+
35
+ value
26
36
  end
27
37
  end
28
38
 
29
39
  class LessThan < Comparator
30
- def value(context = {})
31
- super() { left.value(context) < right.value(context) }
32
- end
33
-
34
40
  def operator
35
- return :<
41
+ :<
36
42
  end
37
43
  end
38
44
 
39
45
  class LessThanOrEqual < Comparator
40
- def value(context = {})
41
- super() { left.value(context) <= right.value(context) }
42
- end
43
-
44
46
  def operator
45
- return :<=
47
+ :<=
46
48
  end
47
49
  end
48
50
 
49
51
  class GreaterThan < Comparator
50
- def value(context = {})
51
- super() { left.value(context) > right.value(context) }
52
- end
53
-
54
52
  def operator
55
- return :>
53
+ :>
56
54
  end
57
55
  end
58
56
 
59
57
  class GreaterThanOrEqual < Comparator
60
- def value(context = {})
61
- super() { left.value(context) >= right.value(context) }
62
- end
63
-
64
58
  def operator
65
- return :>=
59
+ :>=
66
60
  end
67
61
  end
68
62
 
69
63
  class NotEqual < Comparator
70
- def value(context = {})
71
- super() { left.value(context) != right.value(context) }
72
- end
73
-
74
64
  def operator
75
- return :!=
65
+ :!=
76
66
  end
77
67
  end
78
68
 
79
69
  class Equal < Comparator
80
- def value(context = {})
81
- super() { left.value(context) == right.value(context) }
70
+ def operator
71
+ :==
82
72
  end
83
73
 
84
- def operator
85
- return :==
74
+ def display_operator
75
+ "="
86
76
  end
87
77
  end
88
78
  end
@@ -4,6 +4,8 @@ require_relative 'function_registry'
4
4
  module Dentaku
5
5
  module AST
6
6
  class Function < Node
7
+ attr_reader :args
8
+
7
9
  # @return [Integer] with the number of significant decimal digits to use.
8
10
  DIG = Float::DIG + 1
9
11
 
@@ -11,19 +13,15 @@ module Dentaku
11
13
  @args = args
12
14
  end
13
15
 
16
+ def accept(visitor)
17
+ visitor.visit_function(self)
18
+ end
19
+
14
20
  def dependencies(context = {})
15
- deferred = deferred_args
16
21
  @args.each_with_index
17
- .reject { |_, i| deferred.include? i }
18
22
  .flat_map { |a, _| a.dependencies(context) }
19
23
  end
20
24
 
21
- # override if your function implementation needs to defer evaluation of
22
- # any arguments
23
- def deferred_args
24
- []
25
- end
26
-
27
25
  def self.get(name)
28
26
  registry.get(name)
29
27
  end
@@ -1,27 +1,14 @@
1
- require_relative '../function'
2
- require_relative '../../exceptions'
1
+ require_relative './enum'
3
2
 
4
3
  module Dentaku
5
4
  module AST
6
- class All < Function
7
- def self.min_param_count
8
- 3
9
- end
10
-
11
- def self.max_param_count
12
- 3
13
- end
14
-
15
- def deferred_args
16
- [1, 2]
17
- end
18
-
5
+ class All < Enum
19
6
  def value(context = {})
20
- collection = @args[0].value(context)
7
+ collection = Array(@args[0].value(context))
21
8
  item_identifier = @args[1].identifier
22
9
  expression = @args[2]
23
10
 
24
- Array(collection).all? do |item_value|
11
+ collection.all? do |item_value|
25
12
  expression.value(
26
13
  context.merge(
27
14
  FlatHash.from_hash_with_intermediates(item_identifier => item_value)
@@ -1,27 +1,14 @@
1
- require_relative '../function'
2
- require_relative '../../exceptions'
1
+ require_relative './enum'
3
2
 
4
3
  module Dentaku
5
4
  module AST
6
- class Any < Function
7
- def self.min_param_count
8
- 3
9
- end
10
-
11
- def self.max_param_count
12
- 3
13
- end
14
-
15
- def deferred_args
16
- [1, 2]
17
- end
18
-
5
+ class Any < Enum
19
6
  def value(context = {})
20
- collection = @args[0].value(context)
7
+ collection = Array(@args[0].value(context))
21
8
  item_identifier = @args[1].identifier
22
9
  expression = @args[2]
23
10
 
24
- Array(collection).any? do |item_value|
11
+ collection.any? do |item_value|
25
12
  expression.value(
26
13
  context.merge(
27
14
  FlatHash.from_hash_with_intermediates(item_identifier => item_value)
@@ -4,11 +4,11 @@ module Dentaku
4
4
  module AST
5
5
  class Duration < Function
6
6
  def self.min_param_count
7
- 1
7
+ 2
8
8
  end
9
9
 
10
10
  def self.max_param_count
11
- 1
11
+ 2
12
12
  end
13
13
 
14
14
  class Value
@@ -0,0 +1,37 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Enum < Function
7
+ def self.min_param_count
8
+ 3
9
+ end
10
+
11
+ def self.max_param_count
12
+ 3
13
+ end
14
+
15
+ def dependencies(context = {})
16
+ validate_identifier(@args[1])
17
+
18
+ collection = @args[0]
19
+ item_identifier = @args[1].identifier
20
+ expression = @args[2]
21
+
22
+ collection_deps = collection.dependencies(context)
23
+ expression_deps = (expression&.dependencies(context) || []).reject do |i|
24
+ i == item_identifier || i.start_with?("#{item_identifier}.")
25
+ end
26
+
27
+ collection_deps + expression_deps
28
+ end
29
+
30
+ def validate_identifier(arg, message = "#{name}() requires second argument to be an identifier")
31
+ unless arg.is_a?(Identifier)
32
+ raise ArgumentError.for(:incompatible_type, value: arg, for: Identifier), message
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,27 +1,14 @@
1
- require_relative '../function'
2
- require_relative '../../exceptions'
1
+ require_relative './enum'
3
2
 
4
3
  module Dentaku
5
4
  module AST
6
- class Filter < Function
7
- def self.min_param_count
8
- 3
9
- end
10
-
11
- def self.max_param_count
12
- 3
13
- end
14
-
15
- def deferred_args
16
- [1, 2]
17
- end
18
-
5
+ class Filter < Enum
19
6
  def value(context = {})
20
- collection = @args[0].value(context)
7
+ collection = Array(@args[0].value(context))
21
8
  item_identifier = @args[1].identifier
22
9
  expression = @args[2]
23
10
 
24
- Array(collection).select do |item_value|
11
+ collection.select do |item_value|
25
12
  expression.value(
26
13
  context.merge(
27
14
  FlatHash.from_hash_with_intermediates(item_identifier => item_value)
@@ -19,6 +19,10 @@ module Dentaku
19
19
  @right = right
20
20
  end
21
21
 
22
+ def args
23
+ [predicate, left, right]
24
+ end
25
+
22
26
  def value(context = {})
23
27
  predicate.value(context) ? left.value(context) : right.value(context)
24
28
  end
@@ -1,23 +1,10 @@
1
- require_relative '../function'
2
- require_relative '../../exceptions'
1
+ require_relative './enum'
3
2
 
4
3
  module Dentaku
5
4
  module AST
6
- class Map < Function
7
- def self.min_param_count
8
- 3
9
- end
10
-
11
- def self.max_param_count
12
- 3
13
- end
14
-
15
- def deferred_args
16
- [1, 2]
17
- end
18
-
5
+ class Map < Enum
19
6
  def value(context = {})
20
- collection = @args[0].value(context)
7
+ collection = Array(@args[0].value(context))
21
8
  item_identifier = @args[1].identifier
22
9
  expression = @args[2]
23
10
 
@@ -1,9 +1,9 @@
1
- require_relative '../function'
1
+ require_relative './enum'
2
2
  require_relative '../../exceptions'
3
3
 
4
4
  module Dentaku
5
5
  module AST
6
- class Pluck < Function
6
+ class Pluck < Enum
7
7
  def self.min_param_count
8
8
  2
9
9
  end
@@ -12,12 +12,13 @@ module Dentaku
12
12
  2
13
13
  end
14
14
 
15
- def deferred_args
16
- [1]
17
- end
18
-
19
15
  def value(context = {})
20
- collection = @args[0].value(context)
16
+ collection = Array(@args[0].value(context))
17
+ unless collection.all? { |elem| elem.is_a?(Hash) }
18
+ raise ArgumentError.for(:incompatible_type, value: collection),
19
+ 'PLUCK() requires first argument to be an array of hashes'
20
+ end
21
+
21
22
  pluck_path = @args[1].identifier
22
23
 
23
24
  collection.map { |h| h.transform_keys(&:to_s)[pluck_path] }
@@ -5,9 +5,10 @@ module Dentaku
5
5
  module AST
6
6
  class RubyMath < Function
7
7
  def self.[](method)
8
- klass = Class.new(self)
8
+ klass_name = method.to_s.capitalize
9
+ klass = const_set(klass_name , Class.new(self))
9
10
  klass.implement(method)
10
- klass
11
+ const_get(klass_name)
11
12
  end
12
13
 
13
14
  def self.implement(method)
@@ -0,0 +1,44 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Xor < Function
7
+ def self.min_param_count
8
+ 1
9
+ end
10
+
11
+ def self.max_param_count
12
+ Float::INFINITY
13
+ end
14
+
15
+ def value(context = {})
16
+ if @args.empty?
17
+ raise Dentaku::ArgumentError.for(
18
+ :too_few_arguments,
19
+ function_name: 'XOR()', at_least: 1, given: 0
20
+ ), 'XOR() requires at least one argument'
21
+ end
22
+
23
+ true_arg_count = 0
24
+ @args.each do |arg|
25
+ case arg.value(context)
26
+ when TrueClass
27
+ true_arg_count += 1
28
+ break if true_arg_count > 1
29
+ when FalseClass, nil
30
+ next
31
+ else
32
+ raise Dentaku::ArgumentError.for(
33
+ :incompatible_type,
34
+ function_name: 'XOR()', expect: :logical, actual: arg.class
35
+ ), 'XOR() requires arguments to be logical expressions'
36
+ end
37
+ end
38
+ true_arg_count == 1
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ Dentaku::AST::Function.register_class(:xor, Dentaku::AST::Xor)