diff --git a/lib/puppet/pops/evaluator/closure.rb b/lib/puppet/pops/evaluator/closure.rb index dbc885918..e9e6ea04d 100644 --- a/lib/puppet/pops/evaluator/closure.rb +++ b/lib/puppet/pops/evaluator/closure.rb @@ -1,235 +1,222 @@ # A Closure represents logic bound to a particular scope. # As long as the runtime (basically the scope implementation) has the behaviour of Puppet 3x it is not # safe to use this closure when the scope given to it when initialized goes "out of scope". # # Note that the implementation is backwards compatible in that the call method accepts a scope, but this # scope is not used. # # Note that this class is a CallableSignature, and the methods defined there should be used # as the API for obtaining information in a callable implementation agnostic way. # class Puppet::Pops::Evaluator::Closure < Puppet::Pops::Evaluator::CallableSignature attr_reader :evaluator attr_reader :model attr_reader :enclosing_scope def initialize(evaluator, model, scope) @evaluator = evaluator @model = model @enclosing_scope = scope end # marker method checked with respond_to :puppet_lambda # @api private # @deprecated Use the type system to query if an object is of Callable type, then use its signatures method for info def puppet_lambda() true end # compatible with 3x AST::Lambda # @api public def call(scope, *args) tc = Puppet::Pops::Types::TypeCalculator - args_size = args.size - - args_diff = parameters.size - args.size - # associate values with parameters (NOTE: excess args for captures rest are not included in merged) - merged = parameters.zip(args.fill(:missing, args.size, args_diff)) - - variable_bindings = {} - merged.each do |arg_assoc| - # m can be one of - # m = [Parameter{name => "name", value => nil], "given"] - # | [Parameter{name => "name", value => Expression}, "given"] - # | [Parameter{name => "name", value => Expression}, :missing] - # - # "given" may be nil or :undef which means that this is the value to use, - # not a default expression. - # - - # "given" is always present. If a parameter was provided then - # the entry is that value, else the symbol :missing - given_argument = arg_assoc[1] - param_captures = arg_assoc[0].captures_rest - default_expression = arg_assoc[0].value - - if given_argument == :missing - # not given - if default_expression - # not given, has default - value = @evaluator.evaluate(default_expression, scope) - if param_captures && !value.is_a?(Array) - # correct non array default value - value = [ value ] - end - else - # not given, does not have default - if param_captures - # default for captures rest is an empty array - value = [ ] - else - @evaluator.fail(Puppet::Pops::Issues::MISSING_REQUIRED_PARAMETER, arg_assoc[0], { :param_name => arg_assoc[0].name }) - raise ArgumentError, "" - end - end - else - # given - if param_captures - # get excess arguments - value = args[(parameter_count-1)..-1] - # If the input was a single nil, or undef, and there is a default, use the default - if value.size == 1 && (given_argument.nil? || given_argument == :undef) && default_expression - value = @evaluator.evaluate(default_expression, scope) - # and ensure it is an array - value = [value] unless value.is_a?(Array) - end - else - value = given_argument - end - end - - variable_bindings[arg_assoc[0].name] = value - end + variable_bindings = combine_values_with_parameters(args) final_args = tc.infer_set(parameters.inject([]) do |final_args, param| if param.captures_rest final_args.concat(variable_bindings[param.name]) else final_args << variable_bindings[param.name] end end) if !tc.callable?(type, final_args) raise ArgumentError, "lambda called with mis-matched arguments\n#{Puppet::Pops::Evaluator::CallableMismatchDescriber.diff_string('lambda', final_args, [self])}" end @evaluator.evaluate_block_with_bindings(@enclosing_scope, variable_bindings, @model.body) end # Call closure with argument assignment by name def call_by_name(scope, args_hash, spill_over = false) parameters = @model.parameters || [] if !spill_over && args_hash.size > parameters.size raise ArgumentError, "Too many arguments: #{args_hash.size} for #{parameters.size}" end # associate values with parameters scope_hash = {} parameters.each do |p| scope_hash[p.name] = args_hash[p.name] || @evaluator.evaluate(p.value, scope) end missing = scope_hash.reduce([]) {|memo, entry| memo << entry[0] if entry[1].nil?; memo } unless missing.empty? optional = parameters.count { |p| !p.value.nil? } raise ArgumentError, "Too few arguments; no value given for required parameters #{missing.join(" ,")}" end if spill_over # all args from given hash should be used, nil entries replaced by default values should win scope_hash = args_hash.merge(scope_hash) end @evaluator.evaluate_block_with_bindings(@enclosing_scope, scope_hash, @model.body) end def parameters() @model.parameters || [] end # Returns the number of parameters (required and optional) # @return [Integer] the total number of accepted parameters def parameter_count # yes, this is duplication of code, but it saves a method call (@model.parameters || []).size end # Returns the number of optional parameters. # @return [Integer] the number of optional accepted parameters def optional_parameter_count parameters.count { |p| !p.value.nil? || p.captures_rest } end # @api public def parameter_names @model.parameters.collect {|p| p.name } end # @api public def type @callable ||= create_callable_type end # @api public def last_captures_rest? last = (@model.parameters || [])[-1] last && last.captures_rest end # @api public def block_name # TODO: Lambda's does not support blocks yet. This is a placeholder 'unsupported_block' end private + def combine_values_with_parameters(args) + variable_bindings = {} + + parameters.each_with_index do |parameter, index| + param_captures = parameter.captures_rest + default_expression = parameter.value + + if index >= args.size + if default_expression + # not given, has default + value = @evaluator.evaluate(default_expression, @enclosing_scope) + if param_captures && !value.is_a?(Array) + # correct non array default value + value = [ value ] + end + else + # not given, does not have default + if param_captures + # default for captures rest is an empty array + value = [ ] + else + @evaluator.fail(Puppet::Pops::Issues::MISSING_REQUIRED_PARAMETER, parameter, { :param_name => parameter.name }) + end + end + else + given_argument = args[index] + if param_captures + # get excess arguments + value = args[(parameter_count-1)..-1] + # If the input was a single nil, or undef, and there is a default, use the default + if value.size == 1 && (given_argument.nil? || given_argument == :undef) && default_expression + value = @evaluator.evaluate(default_expression, scope) + # and ensure it is an array + value = [value] unless value.is_a?(Array) + end + else + value = given_argument + end + end + + variable_bindings[parameter.name] = value + end + + variable_bindings + end + def unify_ranges(left, right) [left[0] + right[0], left[1] + right[1]] end def create_callable_type() types = [] range = [0, 0] in_optional_parameters = false parameters.each do |param| type = if param.type_expr @evaluator.evaluate(param.type_expr, @enclosing_scope) else Puppet::Pops::Types::TypeFactory.optional_object() end if param.captures_rest && type.is_a?(Puppet::Pops::Types::PArrayType) # An array on a slurp parameter is how a size range is defined for a # slurp (Array[Integer, 1, 3] *$param). However, the callable that is # created can't have the array in that position or else type checking # will require the parameters to be arrays, which isn't what is # intended. The array type contains the intended information and needs # to be unpacked. param_range = type.size_range type = type.element_type elsif param.captures_rest && !type.is_a?(Puppet::Pops::Types::PArrayType) param_range = ANY_NUMBER_RANGE elsif param.value param_range = OPTIONAL_SINGLE_RANGE else param_range = REQUIRED_SINGLE_RANGE end types << type if param_range[0] == 0 in_optional_parameters = true elsif param_range[0] != 0 && in_optional_parameters @evaluator.fail(Puppet::Pops::Issues::REQUIRED_PARAMETER_AFTER_OPTIONAL, param, { :param_name => param.name }) end range = [range[0] + param_range[0], range[1] + param_range[1]] end if range[1] == Puppet::Pops::Types::INFINITY range[1] = :default end Puppet::Pops::Types::TypeFactory.callable(*(types + range)) end # Produces information about parameters compatible with a 4x Function (which can have multiple signatures) def signatures [ self ] end ANY_NUMBER_RANGE = [0, Puppet::Pops::Types::INFINITY] OPTIONAL_SINGLE_RANGE = [0, 1] REQUIRED_SINGLE_RANGE = [1, 1] end