File: closure.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (375 lines) | stat: -rw-r--r-- 11,845 bytes parent folder | download
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