class Puppet::Pops::Evaluator::Closure

A Closure represents logic bound to a particular scope. As long as the runtime (basically the scope implementation) has the behavior of Puppet 3x it is not safe to return and later use this closure.

The 3x scope is essentially a named scope with an additional internal local/ephemeral nested scope state. In 3x there is no way to directly refer to the nested scopes, instead, the named scope must be in a particular state. Specifically, closures that require a local/ephemeral scope to exist at a later point will fail. It is safe to call a closure (even with 3x scope) from the very same place it was defined, but not returning it and expecting the closure to reference the scope's state at the point it was created.

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.

Constants

ANY_NUMBER_RANGE
CLOSURE_NAME
OPTIONAL_SINGLE_RANGE
REQUIRED_SINGLE_RANGE

Attributes

enclosing_scope[R]
evaluator[R]
model[R]

Public Class Methods

new(evaluator, model) click to toggle source
   # File lib/puppet/pops/evaluator/closure.rb
60 def initialize(evaluator, model)
61   @evaluator = evaluator
62   @model = model
63 end

Public Instance Methods

block_name() click to toggle source

@api public

    # File lib/puppet/pops/evaluator/closure.rb
161 def block_name
162   # TODO: Lambda's does not support blocks yet. This is a placeholder
163   'unsupported_block'
164 end
call(*args) click to toggle source

Evaluates a closure in its enclosing scope after having matched given arguments with parameters (from left to right) @api public

   # File lib/puppet/pops/evaluator/closure.rb
67 def call(*args)
68   call_with_scope(enclosing_scope, args)
69 end
call_by_name(args_hash, enforce_parameters) click to toggle source
   # File lib/puppet/pops/evaluator/closure.rb
85 def call_by_name(args_hash, enforce_parameters)
86   call_by_name_internal(enclosing_scope, args_hash, enforce_parameters)
87 end
call_by_name_with_scope(scope, args_hash, enforce_parameters) click to toggle source
   # File lib/puppet/pops/evaluator/closure.rb
81 def call_by_name_with_scope(scope, args_hash, enforce_parameters)
82   call_by_name_internal(scope, args_hash, enforce_parameters)
83 end
closure_name() click to toggle source

@api public

    # File lib/puppet/pops/evaluator/closure.rb
169 def closure_name()
170   CLOSURE_NAME
171 end
invoke(instance, calling_scope, args, &block) click to toggle source

This method makes a Closure compatible with a Dispatch. This is used when the closure is wrapped in a Function and the function is called. (Saves an extra Dispatch that just delegates to a Closure and avoids having two checks of the argument type/arity validity). @api private

   # File lib/puppet/pops/evaluator/closure.rb
75 def invoke(instance, calling_scope, args, &block)
76   enclosing_scope.with_global_scope do |global_scope|
77     call_with_scope(global_scope, args, &block)
78   end
79 end
last_captures_rest?() click to toggle source

@api public

    # File lib/puppet/pops/evaluator/closure.rb
155 def last_captures_rest?
156   last = @model.parameters[-1]
157   last && last.captures_rest
158 end
parameter_count() click to toggle source

Returns the number of parameters (required and optional) @return [Integer] the total number of accepted parameters

    # File lib/puppet/pops/evaluator/closure.rb
130 def parameter_count
131   # yes, this is duplication of code, but it saves a method call
132   @model.parameters.size
133 end
parameter_names() click to toggle source

@api public

    # File lib/puppet/pops/evaluator/closure.rb
136 def parameter_names
137   @model.parameters.collect(&:name)
138 end
parameters() click to toggle source
    # File lib/puppet/pops/evaluator/closure.rb
124 def parameters
125   @model.parameters
126 end
params_struct() click to toggle source

@api public

    # File lib/puppet/pops/evaluator/closure.rb
150 def params_struct
151   @params_struct ||= create_params_struct
152 end
return_type() click to toggle source
    # File lib/puppet/pops/evaluator/closure.rb
140 def return_type
141   @return_type ||= create_return_type
142 end
type() click to toggle source

@api public

    # File lib/puppet/pops/evaluator/closure.rb
145 def type
146   @callable ||= create_callable_type
147 end

Private Instance Methods

call_by_name_internal(closure_scope, args_hash, enforce_parameters) click to toggle source

Call closure with argument assignment by name

    # File lib/puppet/pops/evaluator/closure.rb
 90 def call_by_name_internal(closure_scope, args_hash, enforce_parameters)
 91   if enforce_parameters
 92     # Push a temporary parameter scope used while resolving the parameter defaults
 93     closure_scope.with_parameter_scope(closure_name, parameter_names) do |param_scope|
 94       # Assign all non-nil values, even those that represent non-existent parameters.
 95       args_hash.each { |k, v| param_scope[k] = v unless v.nil? }
 96       parameters.each do |p|
 97         name = p.name
 98         arg = args_hash[name]
 99         if arg.nil?
100           # Arg either wasn't given, or it was undef
101           if p.value.nil?
102             # No default. Assign nil if the args_hash included it
103             param_scope[name] = nil if args_hash.include?(name)
104           else
105             param_scope[name] = param_scope.evaluate(name, p.value, closure_scope, @evaluator)
106           end
107         end
108       end
109       args_hash = param_scope.to_hash
110     end
111     Types::TypeMismatchDescriber.validate_parameters(closure_name, params_struct, args_hash)
112     result = catch(:next) do
113       @evaluator.evaluate_block_with_bindings(closure_scope, args_hash, @model.body)
114     end
115     Types::TypeAsserter.assert_instance_of(nil, return_type, result) do
116       "value returned from #{closure_name}"
117     end
118   else
119     @evaluator.evaluate_block_with_bindings(closure_scope, args_hash, @model.body)
120   end
121 end
call_with_scope(scope, args) click to toggle source
    # File lib/puppet/pops/evaluator/closure.rb
219 def call_with_scope(scope, args)
220   variable_bindings = combine_values_with_parameters(scope, args)
221 
222   tc = Types::TypeCalculator.singleton
223   final_args = tc.infer_set(parameters.reduce([]) do |tmp_args, param|
224     if param.captures_rest
225       tmp_args.concat(variable_bindings[param.name])
226     else
227       tmp_args << variable_bindings[param.name]
228     end
229   end)
230 
231   if type.callable?(final_args)
232     result = catch(:next) do
233       @evaluator.evaluate_block_with_bindings(scope, variable_bindings, @model.body)
234     end
235     Types::TypeAsserter.assert_instance_of(nil, return_type, result) do
236       "value returned from #{closure_name}"
237     end
238   else
239     raise ArgumentError, Types::TypeMismatchDescriber.describe_signatures(closure_name, [self], final_args)
240   end
241 end
combine_values_with_parameters(scope, args) click to toggle source
    # File lib/puppet/pops/evaluator/closure.rb
243 def combine_values_with_parameters(scope, args)
244   scope.with_parameter_scope(closure_name, parameter_names) do |param_scope|
245     parameters.each_with_index do |parameter, index|
246       param_captures     = parameter.captures_rest
247       default_expression = parameter.value
248 
249       if index >= args.size
250         if default_expression
251           # not given, has default
252           value = param_scope.evaluate(parameter.name, default_expression, scope, @evaluator)
253 
254           if param_captures && !value.is_a?(Array)
255             # correct non array default value
256             value = [value]
257           end
258         else
259           # not given, does not have default
260           if param_captures
261             # default for captures rest is an empty array
262             value = []
263           else
264             @evaluator.fail(Issues::MISSING_REQUIRED_PARAMETER, parameter, { :param_name => parameter.name })
265           end
266         end
267       else
268         given_argument = args[index]
269         if param_captures
270           # get excess arguments
271           value = args[(parameter_count-1)..-1]
272           # If the input was a single nil, or undef, and there is a default, use the default
273           # This supports :undef in case it was used in a 3x data structure and it is passed as an arg
274           #
275           if value.size == 1 && (given_argument.nil? || given_argument == :undef) && default_expression
276             value = param_scope.evaluate(parameter.name, default_expression, scope, @evaluator)
277             # and ensure it is an array
278             value = [value] unless value.is_a?(Array)
279           end
280         else
281           value = given_argument
282         end
283       end
284       param_scope[parameter.name] = value
285     end
286     param_scope.to_hash
287   end
288 end
create_callable_type() click to toggle source
    # File lib/puppet/pops/evaluator/closure.rb
290 def create_callable_type()
291   types = []
292   from = 0
293   to = 0
294   in_optional_parameters = false
295   closure_scope = enclosing_scope
296 
297   parameters.each do |param|
298     type, param_range = create_param_type(param, closure_scope)
299 
300     types << type
301 
302     if param_range[0] == 0
303       in_optional_parameters = true
304     elsif param_range[0] != 0 && in_optional_parameters
305       @evaluator.fail(Issues::REQUIRED_PARAMETER_AFTER_OPTIONAL, param, { :param_name => param.name })
306     end
307 
308     from += param_range[0]
309     to += param_range[1]
310   end
311   param_types = Types::PTupleType.new(types, Types::PIntegerType.new(from, to))
312   Types::PCallableType.new(param_types, nil, return_type)
313 end
create_param_type(param, closure_scope) click to toggle source
    # File lib/puppet/pops/evaluator/closure.rb
337 def create_param_type(param, closure_scope)
338   type = if param.type_expr
339            @evaluator.evaluate(param.type_expr, closure_scope)
340          else
341            Types::PAnyType::DEFAULT
342          end
343 
344   if param.captures_rest && type.is_a?(Types::PArrayType)
345     # An array on a slurp parameter is how a size range is defined for a
346     # slurp (Array[Integer, 1, 3] *$param). However, the callable that is
347     # created can't have the array in that position or else type checking
348     # will require the parameters to be arrays, which isn't what is
349     # intended. The array type contains the intended information and needs
350     # to be unpacked.
351     param_range = type.size_range
352     type = type.element_type
353   elsif param.captures_rest && !type.is_a?(Types::PArrayType)
354     param_range = ANY_NUMBER_RANGE
355   elsif param.value
356     param_range = OPTIONAL_SINGLE_RANGE
357   else
358     param_range = REQUIRED_SINGLE_RANGE
359   end
360   [type, param_range]
361 end
create_params_struct() click to toggle source
    # File lib/puppet/pops/evaluator/closure.rb
315 def create_params_struct
316   type_factory = Types::TypeFactory
317   members = {}
318   closure_scope = enclosing_scope
319 
320   parameters.each do |param|
321     arg_type, _ = create_param_type(param, closure_scope)
322     key_type = type_factory.string(param.name.to_s)
323     key_type = type_factory.optional(key_type) unless param.value.nil?
324     members[key_type] = arg_type
325   end
326   type_factory.struct(members)
327 end
create_return_type() click to toggle source
    # File lib/puppet/pops/evaluator/closure.rb
329 def create_return_type
330   if @model.return_type
331     @evaluator.evaluate(@model.return_type, @enclosing_scope)
332   else
333     Types::PAnyType::DEFAULT
334   end
335 end
signatures() click to toggle source

Produces information about parameters compatible with a 4x Function (which can have multiple signatures)

    # File lib/puppet/pops/evaluator/closure.rb
364 def signatures
365   [ self ]
366 end