diff --git a/.rubocop.yml b/.rubocop.yml index aebfc64..fdbcf1a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -61,6 +61,9 @@ Style/Alias: Style/EmptyMethod: EnforcedStyle: expanded +Style/HashAsLastArrayItem: + EnforcedStyle: no_braces + Style/RaiseArgs: EnforcedStyle: compact diff --git a/lib/rupkl.rb b/lib/rupkl.rb index ae37676..c90ddb4 100644 --- a/lib/rupkl.rb +++ b/lib/rupkl.rb @@ -32,6 +32,8 @@ require_relative 'rupkl/node/boolean' require_relative 'rupkl/node/number' require_relative 'rupkl/node/string' +require_relative 'rupkl/node/collection' +require_relative 'rupkl/node/list' require_relative 'rupkl/node/object' require_relative 'rupkl/node/dynamic' require_relative 'rupkl/node/mapping' diff --git a/lib/rupkl/node/base.rb b/lib/rupkl/node/base.rb index e1fb745..2da8bb9 100644 --- a/lib/rupkl/node/base.rb +++ b/lib/rupkl/node/base.rb @@ -34,11 +34,15 @@ def add_builtin_class(klass) add_builtin_class PklModule define_builtin_property(:NaN) do - Float.new(parent, ::Float::NAN, position) + Float.new(self, ::Float::NAN, position) end define_builtin_property(:Infinity) do - Float.new(parent, ::Float::INFINITY, position) + Float.new(self, ::Float::INFINITY, position) + end + + define_builtin_method(:List, elements: [Any, varparams: true]) do |elements| + List.new(nil, elements, nil) end end end diff --git a/lib/rupkl/node/boolean.rb b/lib/rupkl/node/boolean.rb index ae201d9..e360b92 100644 --- a/lib/rupkl/node/boolean.rb +++ b/lib/rupkl/node/boolean.rb @@ -17,12 +17,12 @@ def short_circuit?(operator) define_builtin_method(:xor, other: Boolean) do |other| result = value ^ other.value - Boolean.new(nil, result, position) + Boolean.new(nil, result, nil) end define_builtin_method(:implies, other: Boolean) do |other| result = !value || other.value - Boolean.new(nil, result, position) + Boolean.new(nil, result, nil) end end end diff --git a/lib/rupkl/node/collection.rb b/lib/rupkl/node/collection.rb new file mode 100644 index 0000000..37506d8 --- /dev/null +++ b/lib/rupkl/node/collection.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module RuPkl + module Node + class Collection < Any + abstract_class + uninstantiable_class + end + end +end diff --git a/lib/rupkl/node/dynamic.rb b/lib/rupkl/node/dynamic.rb index 33110fd..d0861b1 100644 --- a/lib/rupkl/node/dynamic.rb +++ b/lib/rupkl/node/dynamic.rb @@ -30,24 +30,24 @@ def find_by_key(key) define_builtin_method(:length) do result = elements&.size || 0 - Int.new(nil, result, position) + Int.new(nil, result, nil) end define_builtin_method(:hasProperty, name: String) do |name| result = find_property(name.value.to_sym) && true || false - Boolean.new(nil, result, position) + Boolean.new(nil, result, nil) end define_builtin_method(:getProperty, name: String) do |name| find_property(name.value.to_sym) || begin m = "cannot find property '#{name.value}'" - raise EvaluationError.new(m, position) + raise EvaluationError.new(m, nil) end end define_builtin_method(:getPropertyOrNull, name: String) do |name| - find_property(name.value.to_sym) || Null.new(nil, position) + find_property(name.value.to_sym) || Null.new(nil, nil) end private diff --git a/lib/rupkl/node/list.rb b/lib/rupkl/node/list.rb new file mode 100644 index 0000000..63d7149 --- /dev/null +++ b/lib/rupkl/node/list.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module RuPkl + module Node + class List < Collection + uninstantiable_class + + def initialize(parent, elements, position) + super(parent, *elements, position) + end + + alias_method :elements, :children + end + end +end diff --git a/lib/rupkl/node/listing.rb b/lib/rupkl/node/listing.rb index 22ae4aa..e72a851 100644 --- a/lib/rupkl/node/listing.rb +++ b/lib/rupkl/node/listing.rb @@ -48,7 +48,7 @@ def find_by_key(key) elements &.map { _1.value.to_string } &.join(separator.value) - String.new(nil, result || '', nil, position) + String.new(nil, result || '', nil, nil) end private diff --git a/lib/rupkl/node/mapping.rb b/lib/rupkl/node/mapping.rb index 7927dbc..ddf44af 100644 --- a/lib/rupkl/node/mapping.rb +++ b/lib/rupkl/node/mapping.rb @@ -30,11 +30,11 @@ def find_by_key(key) define_builtin_method(:containsKey, key: Any) do |key| result = find_entry(key) && true || false - Boolean.new(nil, result, position) + Boolean.new(nil, result, nil) end define_builtin_method(:getOrNull, key: Any) do |key| - find_entry(key) || Null.new(nil, position) + find_entry(key) || Null.new(nil, nil) end private diff --git a/lib/rupkl/node/method_definition.rb b/lib/rupkl/node/method_definition.rb index 43887ba..e60da24 100644 --- a/lib/rupkl/node/method_definition.rb +++ b/lib/rupkl/node/method_definition.rb @@ -17,6 +17,20 @@ def initialize(parent, name, type, position) def check_type(value, context, position) type&.check_type(value, context, position) end + + def varparam? + false + end + end + + class VariadicMethodParam < MethodParam + def check_type(values, context, position) + values.each { |v| super(v, context, position) } + end + + def varparam? + true + end end class MethodDefinition @@ -38,6 +52,7 @@ def initialize(parent, name, params, type, body, position) def call(receiver, arguments, context, position) args = evaluate_arguments(arguments, context, position) execute_method(receiver, args) + .tap { |result| overwrite_position(result, position) } end private @@ -45,26 +60,42 @@ def call(receiver, arguments, context, position) def evaluate_arguments(arguments, context, position) check_arity(arguments, position) - arguments&.zip(params)&.map do |arg, param| - evaluate_argument(arg, param, context) + params&.map&.with_index do |param, i| + arg = + if param.varparam? + Array(arguments&.[](i..)) + else + arguments&.[](i) + end + evaluate_argument(param, arg, context) end end - def evaluate_argument(arg, param, context) - value = arg.evaluate(context) - param.check_type(value, context, position) - [param.name, value] - end - def check_arity(arguments, position) n_args = arguments&.size || 0 - n_params = params&.size || 0 - return if n_args == n_params + n_params = n_params_range + return if n_args in ^n_params m = "expected #{n_params} method arguments but got #{n_args}" raise EvaluationError.new(m, position) end + def n_params_range + n_params = params&.size || 0 + params&.last&.varparam? && (n_params - 1..) || n_params + end + + def evaluate_argument(param, arg, context) + value = + if param.varparam? + arg.map { _1.evaluate(context) } + else + arg.evaluate(context) + end + param.check_type(value, context, position) + [param.name, value] + end + def execute_method(receiver, arguments) context = create_call_context(receiver, arguments) execute_body(context) @@ -85,6 +116,10 @@ def execute_body(context) .evaluate(context) .tap { type&.check_type(_1, context, position) } end + + def overwrite_position(result, position) + result.instance_exec(position) { @position = _1 } + end end class MethodCallContext @@ -125,16 +160,35 @@ def initialize(name, klass) end end + class BuiltinVariadicMethodParam < VariadicMethodParam + def initialize(name, klass) + id = Identifier.new(nil, name, nil) + type = BuiltinMethodTypeChecker.new(klass) + super(nil, id, type, nil) + end + end + class BuiltinMethodDefinition < MethodDefinition def initialize(name, **params, &body) - param_list = params.map { |n, t| BuiltinMethodParams.new(n, t) } id = Identifier.new(nil, name, nil) - super(nil, id, param_list, nil, nil, nil) + list = param_list(params) + super(nil, id, list, nil, nil, nil) @body = body end private + def param_list(params) + params.map do |name, type| + case type + in [klass, { varparams: true }] + BuiltinVariadicMethodParam.new(name, klass) + else + BuiltinMethodParams.new(name, type) + end + end + end + def execute_method(receiver, arguments) receiver.instance_exec(*arguments&.map(&:last), &body) end diff --git a/lib/rupkl/node/number.rb b/lib/rupkl/node/number.rb index 55ef52b..f89a4ec 100644 --- a/lib/rupkl/node/number.rb +++ b/lib/rupkl/node/number.rb @@ -77,32 +77,32 @@ def force_float?(operator, r_operand) end define_builtin_method(:toString) do - String.new(nil, value.to_s, nil, position) + String.new(nil, value.to_s, nil, nil) end define_builtin_method(:toInt) do - Int.new(nil, value.to_i, position) + Int.new(nil, value.to_i, nil) end define_builtin_method(:toFloat) do - Float.new(nil, value.to_f, position) + Float.new(nil, value.to_f, nil) end define_builtin_method(:round) do result = value.finite? && value.round || value - self.class.new(nil, result, position) + self.class.new(nil, result, nil) end define_builtin_method(:truncate) do result = value.finite? && value.truncate || value - self.class.new(nil, result, position) + self.class.new(nil, result, nil) end define_builtin_method(:isBetween, first: Number, last: Number) do |f, l| result = [f.value, l.value, value].all? { _1.finite? || _1.infinite? } && (f.value..l.value).include?(value) - Boolean.new(nil, result, position) + Boolean.new(nil, result, nil) end end @@ -127,12 +127,12 @@ def initialize(parent, value, position) define_builtin_method(:shl, n: Int) do |n| result = value << n.value - self.class.new(self, result, position) + self.class.new(self, result, nil) end define_builtin_method(:shr, n: Int) do |n| result = value >> n.value - self.class.new(self, result, position) + self.class.new(self, result, nil) end define_builtin_method(:ushr, n: Int) do |n| @@ -143,19 +143,19 @@ def initialize(parent, value, position) else value >> n.value end - self.class.new(self, result, position) + self.class.new(self, result, nil) end define_builtin_method(:and, n: Int) do |n| - self.class.new(nil, value & n.value, position) + self.class.new(nil, value & n.value, nil) end define_builtin_method(:or, n: Int) do |n| - self.class.new(nil, value | n.value, position) + self.class.new(nil, value | n.value, nil) end define_builtin_method(:xor, n: Int) do |n| - self.class.new(nil, value ^ n.value, position) + self.class.new(nil, value ^ n.value, nil) end end diff --git a/lib/rupkl/node/string.rb b/lib/rupkl/node/string.rb index 9e3d522..3007705 100644 --- a/lib/rupkl/node/string.rb +++ b/lib/rupkl/node/string.rb @@ -106,9 +106,9 @@ def find_by_key(key) define_builtin_method(:getOrNull, index: Int) do |index| if (0...value.size).include?(index.value) - String.new(nil, value[index.value], nil, position) + String.new(nil, value[index.value], nil, nil) else - Null.new(nil, position) + Null.new(nil, nil) end end @@ -116,14 +116,14 @@ def find_by_key(key) check_range(s.value, 0) check_range(e.value, s.value) - String.new(nil, value[s.value...e.value], nil, position) + String.new(nil, value[s.value...e.value], nil, nil) end define_builtin_method(:substringOrNull, start: Int, exclusive_end: Int) do |s, e| if inside_range?(s.value, 0) && inside_range?(e.value, s.value) - String.new(nil, value[s.value...e.value], nil, position) + String.new(nil, value[s.value...e.value], nil, nil) else - Null.new(nil, position) + Null.new(nil, nil) end end @@ -131,47 +131,47 @@ def find_by_key(key) check_positive_number(count) result = value * count.value - String.new(nil, result, nil, position) + String.new(nil, result, nil, nil) end define_builtin_method(:contains, pattern: String) do |pattern| result = value.include?(pattern.value) - Boolean.new(nil, result, position) + Boolean.new(nil, result, nil) end define_builtin_method(:startsWith, pattern: String) do |pattern| result = value.start_with?(pattern.value) - Boolean.new(nil, result, position) + Boolean.new(nil, result, nil) end define_builtin_method(:endsWith, pattern: String) do |pattern| result = value.end_with?(pattern.value) - Boolean.new(nil, result, position) + Boolean.new(nil, result, nil) end define_builtin_method(:indexOf, pattern: String) do |pattern| index_of(:index, pattern) do message = "\"#{pattern.value}\" does not occur in \"#{value}\"" - raise EvaluationError.new(message, position) + raise EvaluationError.new(message, nil) end end define_builtin_method(:indexOfOrNull, pattern: String) do |pattern| index_of(:index, pattern) do - Null.new(nil, position) + Null.new(nil, nil) end end define_builtin_method(:lastIndexOf, pattern: String) do |pattern| index_of(:rindex, pattern) do message = "\"#{pattern.value}\" does not occur in \"#{value}\"" - raise EvaluationError.new(message, position) + raise EvaluationError.new(message, nil) end end define_builtin_method(:lastIndexOfOrNull, pattern: String) do |pattern| index_of(:rindex, pattern) do - Null.new(nil, position) + Null.new(nil, nil) end end @@ -179,7 +179,7 @@ def find_by_key(key) check_positive_number(n) result = value[0, n.value] || value - String.new(nil, result, nil, position) + String.new(nil, result, nil, nil) end define_builtin_method(:takeLast, n: Int) do |n| @@ -187,14 +187,14 @@ def find_by_key(key) pos = value.size - n.value result = pos.negative? && value || value[pos..] - String.new(nil, result, nil, position) + String.new(nil, result, nil, nil) end define_builtin_method(:drop, n: Int) do |n| check_positive_number(n) result = value[n.value..] || '' - String.new(nil, result, nil, position) + String.new(nil, result, nil, nil) end define_builtin_method(:dropLast, n: Int) do |n| @@ -202,7 +202,7 @@ def find_by_key(key) length = value.size - n.value result = length.negative? && '' || value[0, length] - String.new(nil, result, nil, position) + String.new(nil, result, nil, nil) end define_builtin_method( @@ -210,7 +210,7 @@ def find_by_key(key) pattern: String, replacement: String ) do |pattern, replacement| result = value.sub(pattern.value, replacement.value) - String.new(nil, result, nil, position) + String.new(nil, result, nil, nil) end define_builtin_method( @@ -223,7 +223,7 @@ def find_by_key(key) else value end - String.new(nil, result, nil, position) + String.new(nil, result, nil, nil) end define_builtin_method( @@ -231,7 +231,7 @@ def find_by_key(key) pattern: String, replacement: String ) do |pattern, replacement| result = value.gsub(pattern.value, replacement.value) - String.new(nil, result, nil, position) + String.new(nil, result, nil, nil) end define_builtin_method( @@ -243,34 +243,34 @@ def find_by_key(key) range = start.value...exclusive_end.value result = value.dup.tap { |s| s[range] = replacement.value } - String.new(nil, result, nil, position) + String.new(nil, result, nil, nil) end define_builtin_method(:toUpperCase) do - String.new(nil, value.upcase, nil, position) + String.new(nil, value.upcase, nil, nil) end define_builtin_method(:toLowerCase) do - String.new(nil, value.downcase, nil, position) + String.new(nil, value.downcase, nil, nil) end define_builtin_method(:reverse) do - String.new(nil, value.reverse, nil, position) + String.new(nil, value.reverse, nil, nil) end define_builtin_method(:trim) do pattern = /(?:\A\p{White_Space}+)|(?:\p{White_Space}+\z)/ - String.new(nil, value.gsub(pattern, ''), nil, position) + String.new(nil, value.gsub(pattern, ''), nil, nil) end define_builtin_method(:trimStart) do pattern = /\A\p{White_Space}+/ - String.new(nil, value.sub(pattern, ''), nil, position) + String.new(nil, value.sub(pattern, ''), nil, nil) end define_builtin_method(:trimEnd) do pattern = /\p{White_Space}+\z/ - String.new(nil, value.sub(pattern, ''), nil, position) + String.new(nil, value.sub(pattern, ''), nil, nil) end define_builtin_method(:padStart, width: Int, char: String) do |width, char| @@ -284,51 +284,51 @@ def find_by_key(key) define_builtin_method(:capitalize) do result = value.empty? && value || value.dup.tap { |s| s[0] = s[0].upcase } - String.new(nil, result, nil, position) + String.new(nil, result, nil, nil) end define_builtin_method(:decapitalize) do result = value.empty? && value || value.dup.tap { |s| s[0] = s[0].downcase } - String.new(nil, result, nil, position) + String.new(nil, result, nil, nil) end define_builtin_method(:toInt) do to_int do message = "cannot parse string as Int \"#{value}\"" - raise EvaluationError.new(message, position) + raise EvaluationError.new(message, nil) end end define_builtin_method(:toIntOrNull) do to_int do - Null.new(nil, position) + Null.new(nil, nil) end end define_builtin_method(:toFloat) do to_float do message = "cannot parse string as Float \"#{value}\"" - raise EvaluationError.new(message, position) + raise EvaluationError.new(message, nil) end end define_builtin_method(:toFloatOrNull) do to_float do - Null.new(nil, position) + Null.new(nil, nil) end end define_builtin_method(:toBoolean) do to_boolean do message = "cannot parse string as Boolean \"#{value}\"" - raise EvaluationError.new(message, position) + raise EvaluationError.new(message, nil) end end define_builtin_method(:toBooleanOrNull) do to_boolean do - Null.new(nil, position) + Null.new(nil, nil) end end diff --git a/spec/rupkl/node/list_spec.rb b/spec/rupkl/node/list_spec.rb new file mode 100644 index 0000000..7ed8faf --- /dev/null +++ b/spec/rupkl/node/list_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +RSpec.describe RuPkl::Node::List do + let(:parser) do + RuPkl::Parser.new + end + + def parse(string, root: :pkl_module) + parser.parse(string, root: root) + end + + describe 'List method' do + it 'should create a List object containing the given elements' do + node = parse(<<~'PKL') + a = List() + PKL + node.evaluate(nil).properties[-1].then do |a| + expect(a.value).to be_list + end + + node = parse(<<~'PKL') + a = List(1, 2, 3) + PKL + node.evaluate(nil).properties[-1].then do |a| + expect(a.value).to be_list(1, 2, 3) + end + + node = parse(<<~'PKL') + a = List(1, "x", List(1, 2, 3)) + PKL + node.evaluate(nil).properties[-1].then do |a| + expect(a.value).to be_list(1, 'x', list(1, 2, 3)) + end + end + end +end diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb index 7184dd6..c5284f2 100644 --- a/spec/support/helpers.rb +++ b/spec/support/helpers.rb @@ -110,6 +110,19 @@ def member_ref(receiver_or_member, member = nil, nullable: false) ) end + def list(*elements) + elements_matcher = + if elements.empty? + be_nil + else + match(elements.map { |e| expression_matcher(e) }) + end + be_instance_of(Node::List) + .and have_attributes(elements: elements_matcher) + end + + alias_method :be_list, :list + def expression_matcher(expression) case expression when NilClass then be_nil