1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375
|
module Puppet::Pops
module Evaluator
class Jumper < Exception
attr_reader :value
attr_reader :file
attr_reader :line
def initialize(value, file, line)
@value = value
@file = file
@line = line
end
end
class Next < Jumper
def initialize(value, file, line)
super
end
end
class Return < Jumper
def initialize(value, file, line)
super
end
end
class PuppetStopIteration < StopIteration
attr_reader :file
attr_reader :line
attr_reader :pos
def initialize(file, line, pos = nil)
@file = file
@line = line
@pos = pos
end
def message
"break() from context where this is illegal"
end
end
# 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.
#
class Closure < CallableSignature
attr_reader :evaluator
attr_reader :model
attr_reader :enclosing_scope
def initialize(evaluator, model)
@evaluator = evaluator
@model = model
end
# Evaluates a closure in its enclosing scope after having matched given arguments with parameters (from left to right)
# @api public
def call(*args)
call_with_scope(enclosing_scope, args)
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
def invoke(instance, calling_scope, args, &block)
enclosing_scope.with_global_scope do |global_scope|
call_with_scope(global_scope, args, &block)
end
end
def call_by_name_with_scope(scope, args_hash, enforce_parameters)
call_by_name_internal(scope, args_hash, enforce_parameters)
end
def call_by_name(args_hash, enforce_parameters)
call_by_name_internal(enclosing_scope, args_hash, enforce_parameters)
end
# Call closure with argument assignment by name
def call_by_name_internal(closure_scope, args_hash, enforce_parameters)
if enforce_parameters
# Push a temporary parameter scope used while resolving the parameter defaults
closure_scope.with_parameter_scope(closure_name, parameter_names) do |param_scope|
# Assign all non-nil values, even those that represent non-existent parameters.
args_hash.each { |k, v| param_scope[k] = v unless v.nil? }
parameters.each do |p|
name = p.name
arg = args_hash[name]
if arg.nil?
# Arg either wasn't given, or it was undef
if p.value.nil?
# No default. Assign nil if the args_hash included it
param_scope[name] = nil if args_hash.include?(name)
else
param_scope[name] = param_scope.evaluate(name, p.value, closure_scope, @evaluator)
end
end
end
args_hash = param_scope.to_hash
end
Types::TypeMismatchDescriber.validate_parameters(closure_name, params_struct, args_hash)
result = catch(:next) do
@evaluator.evaluate_block_with_bindings(closure_scope, args_hash, @model.body)
end
Types::TypeAsserter.assert_instance_of(nil, return_type, result) do
"value returned from #{closure_name}"
end
else
@evaluator.evaluate_block_with_bindings(closure_scope, args_hash, @model.body)
end
end
private :call_by_name_internal
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
# @api public
def parameter_names
@model.parameters.collect(&:name)
end
def return_type
@return_type ||= create_return_type
end
# @api public
def type
@callable ||= create_callable_type
end
# @api public
def params_struct
@params_struct ||= create_params_struct
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
CLOSURE_NAME = 'lambda'.freeze
# @api public
def closure_name()
CLOSURE_NAME
end
class Dynamic < Closure
def initialize(evaluator, model, scope)
@enclosing_scope = scope
super(evaluator, model)
end
def enclosing_scope
@enclosing_scope
end
def call(*args)
# A return from an unnamed closure is treated as a return from the context evaluating
# calling this closure - that is, as if it was the return call itself.
#
jumper = catch(:return) do
return call_with_scope(enclosing_scope, args)
end
raise jumper
end
end
class Named < Closure
def initialize(name, evaluator, model)
@name = name
super(evaluator, model)
end
def closure_name
@name
end
# The assigned enclosing scope, or global scope if enclosing scope was initialized to nil
#
def enclosing_scope
# Named closures are typically used for puppet functions and they cannot be defined
# in an enclosing scope as they are cashed and reused. They need to bind to the
# global scope at time of use rather at time of definition.
# Unnamed closures are always a runtime construct, they are never bound by a loader
# and are thus garbage collected at end of a compilation.
#
Puppet.lookup(:global_scope) { {} }
end
end
private
def call_with_scope(scope, args)
variable_bindings = combine_values_with_parameters(scope, args)
final_args = parameters.reduce([]) do |tmp_args, param|
if param.captures_rest
tmp_args.concat(variable_bindings[param.name])
else
tmp_args << variable_bindings[param.name]
end
end
if type.callable_with?(final_args, block_type)
result = catch(:next) do
@evaluator.evaluate_block_with_bindings(scope, variable_bindings, @model.body)
end
Types::TypeAsserter.assert_instance_of(nil, return_type, result) do
"value returned from #{closure_name}"
end
else
tc = Types::TypeCalculator.singleton
args_type = tc.infer_set(final_args)
raise ArgumentError, Types::TypeMismatchDescriber.describe_signatures(closure_name, [self], args_type)
end
end
def combine_values_with_parameters(scope, args)
scope.with_parameter_scope(closure_name, parameter_names) do |param_scope|
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 = param_scope.evaluate(parameter.name, default_expression, scope, @evaluator)
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(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
# This supports :undef in case it was used in a 3x data structure and it is passed as an arg
#
if value.size == 1 && (given_argument.nil? || given_argument == :undef) && default_expression
value = param_scope.evaluate(parameter.name, default_expression, scope, @evaluator)
# and ensure it is an array
value = [value] unless value.is_a?(Array)
end
else
value = given_argument
end
end
param_scope[parameter.name] = value
end
param_scope.to_hash
end
end
def create_callable_type()
types = []
from = 0
to = 0
in_optional_parameters = false
closure_scope = enclosing_scope
parameters.each do |param|
type, param_range = create_param_type(param, closure_scope)
types << type
if param_range[0] == 0
in_optional_parameters = true
elsif param_range[0] != 0 && in_optional_parameters
@evaluator.fail(Issues::REQUIRED_PARAMETER_AFTER_OPTIONAL, param, { :param_name => param.name })
end
from += param_range[0]
to += param_range[1]
end
param_types = Types::PTupleType.new(types, Types::PIntegerType.new(from, to))
# The block_type for a Closure is always nil for now, see comment in block_name above
Types::PCallableType.new(param_types, nil, return_type)
end
def create_params_struct
type_factory = Types::TypeFactory
members = {}
closure_scope = enclosing_scope
parameters.each do |param|
arg_type, _ = create_param_type(param, closure_scope)
key_type = type_factory.string(param.name.to_s)
key_type = type_factory.optional(key_type) unless param.value.nil?
members[key_type] = arg_type
end
type_factory.struct(members)
end
def create_return_type
if @model.return_type
@evaluator.evaluate(@model.return_type, @enclosing_scope)
else
Types::PAnyType::DEFAULT
end
end
def create_param_type(param, closure_scope)
type = if param.type_expr
@evaluator.evaluate(param.type_expr, closure_scope)
else
Types::PAnyType::DEFAULT
end
if param.captures_rest && type.is_a?(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?(Types::PArrayType)
param_range = ANY_NUMBER_RANGE
elsif param.value
param_range = OPTIONAL_SINGLE_RANGE
else
param_range = REQUIRED_SINGLE_RANGE
end
[type, param_range]
end
# Produces information about parameters compatible with a 4x Function (which can have multiple signatures)
def signatures
[ self ]
end
ANY_NUMBER_RANGE = [0, Float::INFINITY]
OPTIONAL_SINGLE_RANGE = [0, 1]
REQUIRED_SINGLE_RANGE = [1, 1]
end
end
end
|