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
Public Class Methods
# File lib/puppet/pops/evaluator/closure.rb 60 def initialize(evaluator, model) 61 @evaluator = evaluator 62 @model = model 63 end
Public Instance Methods
@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
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
# 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
# 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
@api public
# File lib/puppet/pops/evaluator/closure.rb 169 def closure_name() 170 CLOSURE_NAME 171 end
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
@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
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
@api public
# File lib/puppet/pops/evaluator/closure.rb 136 def parameter_names 137 @model.parameters.collect(&:name) 138 end
# File lib/puppet/pops/evaluator/closure.rb 124 def parameters 125 @model.parameters 126 end
@api public
# File lib/puppet/pops/evaluator/closure.rb 150 def params_struct 151 @params_struct ||= create_params_struct 152 end
# File lib/puppet/pops/evaluator/closure.rb 140 def return_type 141 @return_type ||= create_return_type 142 end
@api public
# File lib/puppet/pops/evaluator/closure.rb 145 def type 146 @callable ||= create_callable_type 147 end
Private Instance Methods
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
# 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
# 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
# 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
# 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
# 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
# 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
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