File: functions.rb

package info (click to toggle)
puppet-agent 8.10.0-6
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 27,404 kB
  • sloc: ruby: 286,820; sh: 492; xml: 116; makefile: 88; cs: 68
file content (858 lines) | stat: -rw-r--r-- 33,536 bytes parent folder | download | duplicates (2)
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
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
# frozen_string_literal: true

# Functions in the puppet language can be written in Ruby and distributed in
# puppet modules. The function is written by creating a file in the module's
# `lib/puppet/functions/<modulename>` directory, where `<modulename>` is
# replaced with the module's name. The file should have the name of the function.
# For example, to create a function named `min` in a module named `math` create
# a file named `lib/puppet/functions/math/min.rb` in the module.
#
# A function is implemented by calling {Puppet::Functions.create_function}, and
# passing it a block that defines the implementation of the function.
#
# Functions are namespaced inside the module that contains them. The name of
# the function is prefixed with the name of the module. For example,
# `math::min`.
#
# @example A simple function
#   Puppet::Functions.create_function('math::min') do
#     def min(a, b)
#       a <= b ? a : b
#     end
#   end
#
# Anatomy of a function
# ---
#
# Functions are composed of four parts: the name, the implementation methods,
# the signatures, and the dispatches.
#
# The name is the string given to the {Puppet::Functions.create_function}
# method. It specifies the name to use when calling the function in the puppet
# language, or from other functions.
#
# The implementation methods are ruby methods (there can be one or more) that
# provide that actual implementation of the function's behavior. In the
# simplest case the name of the function (excluding any namespace) and the name
# of the method are the same. When that is done no other parts (signatures and
# dispatches) need to be used.
#
# Signatures are a way of specifying the types of the function's parameters.
# The types of any arguments will be checked against the types declared in the
# signature and an error will be produced if they don't match. The types are
# defined by using the same syntax for types as in the puppet language.
#
# Dispatches are how signatures and implementation methods are tied together.
# When the function is called, puppet searches the signatures for one that
# matches the supplied arguments. Each signature is part of a dispatch, which
# specifies the method that should be called for that signature. When a
# matching signature is found, the corresponding method is called.
#
# Special dispatches designed to create error messages for an argument mismatch
# can be added using the keyword `argument_mismatch` instead of `dispatch`. The
# method appointed by an `argument_mismatch` will be called with arguments
# just like a normal `dispatch` would, but the method must produce a string.
# The string is then used as the message in the `ArgumentError` that is raised
# when the method returns. A block parameter can be given, but it is not
# propagated in the method call.
#
# Documentation for the function should be placed as comments to the
# implementation method(s).
#
# @todo Documentation for individual instances of these new functions is not
#   yet tied into the puppet doc system.
#
# @example Dispatching to different methods by type
#   Puppet::Functions.create_function('math::min') do
#     dispatch :numeric_min do
#       param 'Numeric', :a
#       param 'Numeric', :b
#     end
#
#     dispatch :string_min do
#       param 'String', :a
#       param 'String', :b
#     end
#
#     def numeric_min(a, b)
#       a <= b ? a : b
#     end
#
#     def string_min(a, b)
#       a.downcase <= b.downcase ? a : b
#     end
#   end
#
# @example Using an argument mismatch handler
#   Puppet::Functions.create_function('math::min') do
#     dispatch :numeric_min do
#       param 'Numeric', :a
#       param 'Numeric', :b
#     end
#
#     argument_mismatch :on_error do
#       param 'Any', :a
#       param 'Any', :b
#     end
#
#     def numeric_min(a, b)
#       a <= b ? a : b
#     end
#
#     def on_error(a, b)
#       'both arguments must be of type Numeric'
#     end
#   end
#
# Specifying Signatures
# ---
#
# If nothing is specified, the number of arguments given to the function must
# be the same as the number of parameters, and all of the parameters are of
# type 'Any'.
#
# The following methods can be used to define a parameter
#
#  - _param_ - the argument must be given in the call.
#  - _optional_param_ - the argument may be missing in the call. May not be followed by a required parameter
#  - _repeated_param_ - the type specifies a repeating type that occurs 0 to "infinite" number of times. It may only appear last or just before a block parameter.
#  - _block_param_ - a block must be given in the call. May only appear last.
#  - _optional_block_param_ - a block may be given in the call. May only appear last.
#
# The method name _required_param_ is an alias for _param_ and _required_block_param_ is an alias for _block_param_
#
# A parameter definition takes 2 arguments:
#  - _type_ A string that must conform to a type in the puppet language
#  - _name_ A symbol denoting the parameter name
#
# Both arguments are optional when defining a block parameter. The _type_ defaults to "Callable"
# and the _name_ to :block.
#
# Note that the dispatch definition is used to match arguments given in a call to the function with the defined
# parameters. It then dispatches the call to the implementation method simply passing the given arguments on to
# that method without any further processing and it is the responsibility of that method's implementor to ensure
# that it can handle those arguments.
#
# @example Variable number of arguments
#   Puppet::Functions.create_function('foo') do
#     dispatch :foo do
#       param 'Numeric', :first
#       repeated_param 'Numeric', :values
#     end
#
#     def foo(first, *values)
#       # do something
#     end
#   end
#
# There is no requirement for direct mapping between parameter definitions and the parameters in the
# receiving implementation method so the following example is also legal. Here the dispatch will ensure
# that `*values` in the receiver will be an array with at least one entry of type String and that any
# remaining entries are of type Numeric:
#
# @example Inexact mapping or parameters
#   Puppet::Functions.create_function('foo') do
#     dispatch :foo do
#       param 'String', :first
#       repeated_param 'Numeric', :values
#     end
#
#     def foo(*values)
#       # do something
#     end
#   end
#
# Access to Scope
# ---
# In general, functions should not need access to scope; they should be
# written to act on their given input only. If they absolutely must look up
# variable values, they should do so via the closure scope (the scope where
# they are defined) - this is done by calling `closure_scope()`.
#
# Calling other Functions
# ---
# Calling other functions by name is directly supported via
# {Puppet::Pops::Functions::Function#call_function}. This allows a function to
# call other functions visible from its loader.
#
# @api public
module Puppet::Functions
  # @param func_name [String, Symbol] a simple or qualified function name
  # @param block [Proc] the block that defines the methods and dispatch of the
  #   Function to create
  # @return [Class<Function>] the newly created Function class
  #
  # @api public
  def self.create_function(func_name, function_base = Function, &block)
    # Ruby < 2.1.0 does not have method on Binding, can only do eval
    # and it will fail unless protected with an if defined? if the local
    # variable does not exist in the block's binder.
    #

    loader = block.binding.eval('loader_injected_arg if defined?(loader_injected_arg)')
    create_loaded_function(func_name, loader, function_base, &block)
  rescue StandardError => e
    raise ArgumentError, _("Function Load Error for function '%{function_name}': %{message}") % { function_name: func_name, message: e.message }
  end

  # Creates a function in, or in a local loader under the given loader.
  # This method should only be used when manually creating functions
  # for the sake of testing. Functions that are autoloaded should
  # always use the `create_function` method and the autoloader will supply
  # the correct loader.
  #
  # @param func_name [String, Symbol] a simple or qualified function name
  # @param loader [Puppet::Pops::Loaders::Loader] the loader loading the function
  # @param block [Proc] the block that defines the methods and dispatch of the
  #   Function to create
  # @return [Class<Function>] the newly created Function class
  #
  # @api public
  def self.create_loaded_function(func_name, loader, function_base = Function, &block)
    if function_base.ancestors.none? { |s| s == Puppet::Pops::Functions::Function }
      raise ArgumentError, _("Functions must be based on Puppet::Pops::Functions::Function. Got %{function_base}") % { function_base: function_base }
    end

    func_name = func_name.to_s
    # Creates an anonymous class to represent the function
    # The idea being that it is garbage collected when there are no more
    # references to it.
    #
    # (Do not give the class the block here, as instance variables should be set first)
    the_class = Class.new(function_base)

    unless loader.nil?
      the_class.instance_variable_set(:'@loader', loader.private_loader)
    end

    # Make the anonymous class appear to have the class-name <func_name>
    # Even if this class is not bound to such a symbol in a global ruby scope and
    # must be resolved via the loader.
    # This also overrides any attempt to define a name method in the given block
    # (Since it redefines it)
    #
    # TODO, enforce name in lower case (to further make it stand out since Ruby
    # class names are upper case)
    #
    the_class.instance_eval do
      @func_name = func_name

      def name
        @func_name
      end

      def loader
        @loader
      end
    end

    # The given block can now be evaluated and have access to name and loader
    #
    the_class.class_eval(&block)

    # Automatically create an object dispatcher based on introspection if the
    # loaded user code did not define any dispatchers. Fail if function name
    # does not match a given method name in user code.
    #
    if the_class.dispatcher.empty?
      simple_name = func_name.split(/::/)[-1]
      type, names = default_dispatcher(the_class, simple_name)
      last_captures_rest = (type.size_range[1] == Float::INFINITY)
      the_class.dispatcher.add(Puppet::Pops::Functions::Dispatch.new(type, simple_name, names, last_captures_rest))
    end

    # The function class is returned as the result of the create function method
    the_class
  end

  # Creates a default dispatcher configured from a method with the same name as the function
  #
  # @api private
  def self.default_dispatcher(the_class, func_name)
    unless the_class.method_defined?(func_name)
      raise ArgumentError, _("Function Creation Error, cannot create a default dispatcher for function '%{func_name}', no method with this name found") % { func_name: func_name }
    end

    any_signature(*min_max_param(the_class.instance_method(func_name)))
  end

  # @api private
  def self.min_max_param(method)
    result = { :req => 0, :opt => 0, :rest => 0 }
    # count per parameter kind, and get array of names
    names = method.parameters.map { |p| result[p[0]] += 1; p[1].to_s }
    from = result[:req]
    to = result[:rest] > 0 ? :default : from + result[:opt]
    [from, to, names]
  end

  # Construct a signature consisting of Object type, with min, and max, and given names.
  # (there is only one type entry).
  #
  # @api private
  def self.any_signature(from, to, names)
    # Construct the type for the signature
    # Tuple[Object, from, to]
    param_types = Puppet::Pops::Types::PTupleType.new([Puppet::Pops::Types::PAnyType::DEFAULT], Puppet::Pops::Types::PIntegerType.new(from, to))
    [Puppet::Pops::Types::PCallableType.new(param_types), names]
  end

  # Function
  # ===
  # This class is the base class for all Puppet 4x Function API functions. A
  # specialized class is created for each puppet function.
  #
  # @api public
  class Function < Puppet::Pops::Functions::Function
    # @api private
    def self.builder
      DispatcherBuilder.new(dispatcher, Puppet::Pops::Types::PCallableType::DEFAULT, loader)
    end

    # Dispatch any calls that match the signature to the provided method name.
    #
    # @param meth_name [Symbol] The name of the implementation method to call
    #   when the signature defined in the block matches the arguments to a call
    #   to the function.
    # @return [Void]
    #
    # @api public
    def self.dispatch(meth_name, &block)
      builder().instance_eval do
        dispatch(meth_name, false, &block)
      end
    end

    # Like `dispatch` but used for a specific type of argument mismatch. Will not be include in the list of valid
    # parameter overloads for the function.
    #
    # @param meth_name [Symbol] The name of the implementation method to call
    #   when the signature defined in the block matches the arguments to a call
    #   to the function.
    # @return [Void]
    #
    # @api public
    def self.argument_mismatch(meth_name, &block)
      builder().instance_eval do
        dispatch(meth_name, true, &block)
      end
    end

    # Allows types local to the function to be defined to ease the use of complex types
    # in a 4.x function. Within the given block, calls to `type` can be made with a string
    # 'AliasType = ExistingType` can be made to define aliases. The defined aliases are
    # available for further aliases, and in all dispatchers.
    #
    # @since 4.5.0
    # @api public
    #
    def self.local_types(&block)
      if loader.nil?
        raise ArgumentError, _("No loader present. Call create_loaded_function(:myname, loader,...), instead of 'create_function' if running tests")
      end

      aliases = LocalTypeAliasesBuilder.new(loader, name)
      aliases.instance_eval(&block)
      # Add the loaded types to the builder
      aliases.local_types.each do |type_alias_expr|
        # Bind the type alias to the local_loader using the alias
        t = Puppet::Pops::Loader::TypeDefinitionInstantiator.create_from_model(type_alias_expr, aliases.loader)

        # Also define a method for convenient access to the defined type alias.
        # Since initial capital letter in Ruby means a Constant these names use a prefix of
        # `type`. As an example, the type 'MyType' is accessed by calling `type_MyType`.
        define_method("type_#{t.name}") { t }
      end
      # Store the loader in the class
      @loader = aliases.loader
    end

    # Creates a new function instance in the given closure scope (visibility to variables), and a loader
    # (visibility to other definitions). The created function will either use the `given_loader` or
    # (if it has local type aliases) a loader that was constructed from the loader used when loading
    # the function's class.
    #
    # TODO: It would be of value to get rid of the second parameter here, but that would break API.
    #
    def self.new(closure_scope, given_loader)
      super(closure_scope, @loader || given_loader)
    end
  end

  # Base class for all functions implemented in the puppet language
  class PuppetFunction < Function
    def self.init_dispatch(a_closure)
      # A closure is compatible with a dispatcher - they are both callable signatures
      dispatcher.add(a_closure)
    end
  end

  # Public api methods of the DispatcherBuilder are available within dispatch()
  # blocks declared in a Puppet::Function.create_function() call.
  #
  # @api public
  class DispatcherBuilder
    attr_reader :loader

    # @api private
    def initialize(dispatcher, all_callables, loader)
      @all_callables = all_callables
      @dispatcher = dispatcher
      @loader = loader
    end

    # Defines a required positional parameter with _type_ and _name_.
    #
    # @param type [String] The type specification for the parameter.
    # @param name [Symbol] The name of the parameter. This is primarily used
    #   for error message output and does not have to match an implementation
    #   method parameter.
    # @return [Void]
    #
    # @api public
    def param(type, name)
      internal_param(type, name)
      raise ArgumentError, _('A required parameter cannot be added after an optional parameter') if @min != @max

      @min += 1
      @max += 1
    end
    alias required_param param

    # Defines an optional positional parameter with _type_ and _name_.
    # May not be followed by a required parameter.
    #
    # @param type [String] The type specification for the parameter.
    # @param name [Symbol] The name of the parameter. This is primarily used
    #   for error message output and does not have to match an implementation
    #   method parameter.
    # @return [Void]
    #
    # @api public
    def optional_param(type, name)
      internal_param(type, name)
      @max += 1
    end

    # Defines a repeated positional parameter with _type_ and _name_ that may occur 0 to "infinite" number of times.
    # It may only appear last or just before a block parameter.
    #
    # @param type [String] The type specification for the parameter.
    # @param name [Symbol] The name of the parameter. This is primarily used
    #   for error message output and does not have to match an implementation
    #   method parameter.
    # @return [Void]
    #
    # @api public
    def repeated_param(type, name)
      internal_param(type, name, true)
      @max = :default
    end
    alias optional_repeated_param repeated_param

    # Defines a repeated positional parameter with _type_ and _name_ that may occur 1 to "infinite" number of times.
    # It may only appear last or just before a block parameter.
    #
    # @param type [String] The type specification for the parameter.
    # @param name [Symbol] The name of the parameter. This is primarily used
    #   for error message output and does not have to match an implementation
    #   method parameter.
    # @return [Void]
    #
    # @api public
    def required_repeated_param(type, name)
      internal_param(type, name, true)
      raise ArgumentError, _('A required repeated parameter cannot be added after an optional parameter') if @min != @max

      @min += 1
      @max = :default
    end

    # Defines one required block parameter that may appear last. If type and name is missing the
    # default type is "Callable", and the name is "block". If only one
    # parameter is given, then that is the name and the type is "Callable".
    #
    # @api public
    def block_param(*type_and_name)
      case type_and_name.size
      when 0
        type = @all_callables
        name = :block
      when 1
        type = @all_callables
        name = type_and_name[0]
      when 2
        type, name = type_and_name
        type = Puppet::Pops::Types::TypeParser.singleton.parse(type, loader) unless type.is_a?(Puppet::Pops::Types::PAnyType)
      else
        raise ArgumentError, _("block_param accepts max 2 arguments (type, name), got %{size}.") % { size: type_and_name.size }
      end

      unless Puppet::Pops::Types::TypeCalculator.is_kind_of_callable?(type, false)
        raise ArgumentError, _("Expected PCallableType or PVariantType thereof, got %{type_class}") % { type_class: type.class }
      end

      unless name.is_a?(Symbol)
        raise ArgumentError, _("Expected block_param name to be a Symbol, got %{name_class}") % { name_class: name.class }
      end

      if @block_type.nil?
        @block_type = type
        @block_name = name
      else
        raise ArgumentError, _('Attempt to redefine block')
      end
    end
    alias required_block_param block_param

    # Defines one optional block parameter that may appear last. If type or name is missing the
    # defaults are "any callable", and the name is "block". The implementor of the dispatch target
    # must use block = nil when it is optional (or an error is raised when the call is made).
    #
    # @api public
    def optional_block_param(*type_and_name)
      # same as required, only wrap the result in an optional type
      required_block_param(*type_and_name)
      @block_type = Puppet::Pops::Types::TypeFactory.optional(@block_type)
    end

    # Defines the return type. Defaults to 'Any'
    # @param [String] type a reference to a Puppet Data Type
    #
    # @api public
    def return_type(type)
      unless type.is_a?(String) || type.is_a?(Puppet::Pops::Types::PAnyType)
        raise ArgumentError, _("Argument to 'return_type' must be a String reference to a Puppet Data Type. Got %{type_class}") % { type_class: type.class }
      end

      @return_type = type
    end

    private

    # @api private
    def internal_param(type, name, repeat = false)
      raise ArgumentError, _('Parameters cannot be added after a block parameter') unless @block_type.nil?
      raise ArgumentError, _('Parameters cannot be added after a repeated parameter') if @max == :default

      if name.is_a?(String)
        raise ArgumentError, _("Parameter name argument must be a Symbol. Got %{name_class}") % { name_class: name.class }
      end

      if type.is_a?(String) || type.is_a?(Puppet::Pops::Types::PAnyType)
        @types << type
        @names << name
        # mark what should be picked for this position when dispatching
        if repeat
          @weaving << -@names.size()
        else
          @weaving << @names.size() - 1
        end
      else
        raise ArgumentError, _("Parameter 'type' must be a String reference to a Puppet Data Type. Got %{type_class}") % { type_class: type.class }
      end
    end

    # @api private
    def dispatch(meth_name, argument_mismatch_handler, &block)
      # an array of either an index into names/types, or an array with
      # injection information [type, name, injection_name] used when the call
      # is being made to weave injections into the given arguments.
      #
      @types = []
      @names = []
      @weaving = []
      @injections = []
      @min = 0
      @max = 0
      @block_type = nil
      @block_name = nil
      @return_type = nil
      @argument_mismatch_hander = argument_mismatch_handler
      instance_eval(&block)
      callable_t = create_callable(@types, @block_type, @return_type, @min, @max)
      @dispatcher.add(Puppet::Pops::Functions::Dispatch.new(callable_t, meth_name, @names, @max == :default, @block_name, @injections, @weaving, @argument_mismatch_hander))
    end

    # Handles creation of a callable type from strings specifications of puppet
    # types and allows the min/max occurs of the given types to be given as one
    # or two integer values at the end.  The given block_type should be
    # Optional[Callable], Callable, or nil.
    #
    # @api private
    def create_callable(types, block_type, return_type, from, to)
      mapped_types = types.map do |t|
        t.is_a?(Puppet::Pops::Types::PAnyType) ? t : internal_type_parse(t, loader)
      end
      param_types = Puppet::Pops::Types::PTupleType.new(mapped_types, from > 0 && from == to ? nil : Puppet::Pops::Types::PIntegerType.new(from, to))
      return_type = internal_type_parse(return_type, loader) unless return_type.nil? || return_type.is_a?(Puppet::Pops::Types::PAnyType)
      Puppet::Pops::Types::PCallableType.new(param_types, block_type, return_type)
    end

    def internal_type_parse(type_string, loader)
      Puppet::Pops::Types::TypeParser.singleton.parse(type_string, loader)
    rescue StandardError => e
      raise ArgumentError, _("Parsing of type string '\"%{type_string}\"' failed with message: <%{message}>.\n") % {
        type_string: type_string,
        message: e.message
      }
    end
    private :internal_type_parse
  end

  # The LocalTypeAliasBuilder is used by the 'local_types' method to collect the individual
  # type aliases given by the function's author.
  #
  class LocalTypeAliasesBuilder
    attr_reader :local_types, :parser, :loader

    def initialize(loader, name)
      @loader = Puppet::Pops::Loader::PredefinedLoader.new(loader, :"local_function_#{name}", loader.environment)
      @local_types = []
      # get the shared parser used by puppet's compiler
      @parser = Puppet::Pops::Parser::EvaluatingParser.singleton()
    end

    # Defines a local type alias, the given string should be a Puppet Language type alias expression
    # in string form without the leading 'type' keyword.
    # Calls to local_type must be made before the first parameter definition or an error will
    # be raised.
    #
    # @param assignment_string [String] a string on the form 'AliasType = ExistingType'
    # @api public
    #
    def type(assignment_string)
      # Get location to use in case of error - this produces ruby filename and where call to 'type' occurred
      # but strips off the rest of the internal "where" as it is not meaningful to user.
      #
      rb_location = caller(1, 1).first
      begin
        result = parser.parse_string("type #{assignment_string}", nil)
      rescue StandardError => e
        rb_location = rb_location.gsub(/:in.*$/, '')
        # Create a meaningful location for parse errors - show both what went wrong with the parsing
        # and in which ruby file it was found.
        raise ArgumentError, _("Parsing of 'type \"%{assignment_string}\"' failed with message: <%{message}>.\n" \
                               "Called from <%{ruby_file_location}>") % {
                                 assignment_string: assignment_string,
                                 message: e.message,
                                 ruby_file_location: rb_location
                               }
      end
      unless result.body.is_a?(Puppet::Pops::Model::TypeAlias)
        rb_location = rb_location.gsub(/:in.*$/, '')
        raise ArgumentError, _("Expected a type alias assignment on the form 'AliasType = T', got '%{assignment_string}'.\n" \
                               "Called from <%{ruby_file_location}>") % {
                                 assignment_string: assignment_string,
                                 ruby_file_location: rb_location
                               }
      end
      @local_types << result.body
    end
  end

  # @note WARNING: This style of creating functions is not public. It is a system
  #   under development that will be used for creating "system" functions.
  #
  # This is a private, internal, system for creating functions. It supports
  # everything that the public function definition system supports as well as a
  # few extra features such as injection of well known parameters.
  #
  # @api private
  class InternalFunction < Function
    # @api private
    def self.builder
      InternalDispatchBuilder.new(dispatcher, Puppet::Pops::Types::PCallableType::DEFAULT, loader)
    end

    # Allows the implementation of a function to call other functions by name and pass the caller
    # scope. The callable functions are those visible to the same loader that loaded this function
    # (the calling function).
    #
    # @param scope [Puppet::Parser::Scope] The caller scope
    # @param function_name [String] The name of the function
    # @param *args [Object] splat of arguments
    # @return [Object] The result returned by the called function
    #
    # @api public
    def call_function_with_scope(scope, function_name, *args, &block)
      internal_call_function(scope, function_name, args, &block)
    end
  end

  class Function3x < InternalFunction
    # Table of optimized parameter names - 0 to 5 parameters
    PARAM_NAMES = [
      [],
      ['p0'].freeze,
      %w[p0 p1].freeze,
      %w[p0 p1 p2].freeze,
      %w[p0 p1 p2 p3].freeze,
      %w[p0 p1 p2 p3 p4].freeze
    ]

    # Creates an anonymous Function3x class that wraps a 3x function
    #
    # @api private
    def self.create_function(func_name, func_info, loader)
      func_name = func_name.to_s

      # Creates an anonymous class to represent the function
      # The idea being that it is garbage collected when there are no more
      # references to it.
      #
      # (Do not give the class the block here, as instance variables should be set first)
      the_class = Class.new(Function3x)

      unless loader.nil?
        the_class.instance_variable_set(:'@loader', loader.private_loader)
      end

      the_class.instance_variable_set(:'@func_name', func_name)
      the_class.instance_variable_set(:'@method3x', :"function_#{func_name}")

      # Make the anonymous class appear to have the class-name <func_name>
      # Even if this class is not bound to such a symbol in a global ruby scope and
      # must be resolved via the loader.
      # This also overrides any attempt to define a name method in the given block
      # (Since it redefines it)
      #
      the_class.instance_eval do
        def name
          @func_name
        end

        def loader
          @loader
        end

        def method3x
          @method3x
        end
      end

      # Add the method that is called - it simply delegates to
      # the 3.x function by calling it via the calling scope using the @method3x symbol
      # :"function_#{name}".
      #
      # When function is not an rvalue function, make sure it produces nil
      #
      the_class.class_eval do
        # Bypasses making the  call via the dispatcher to make sure errors
        # are reported exactly the same way as in 3x. The dispatcher is still needed as it is
        # used to support other features than calling.
        #
        def call(scope, *args, &block)
          result = catch(:return) do
            mapped_args = Puppet::Pops::Evaluator::Runtime3FunctionArgumentConverter.map_args(args, scope, '')
            # this is the scope.function_xxx(...) call
            return scope.send(self.class.method3x, mapped_args)
          end
          result.value
        rescue Puppet::Pops::Evaluator::Next => jumper
          begin
            throw :next, jumper.value
          rescue Puppet::Parser::Scope::UNCAUGHT_THROW_EXCEPTION
            raise Puppet::ParseError.new("next() from context where this is illegal", jumper.file, jumper.line)
          end
        rescue Puppet::Pops::Evaluator::Return => jumper
          begin
            throw :return, jumper
          rescue Puppet::Parser::Scope::UNCAUGHT_THROW_EXCEPTION
            raise Puppet::ParseError.new("return() from context where this is illegal", jumper.file, jumper.line)
          end
        end
      end

      # Create a dispatcher based on func_info
      type, names = Puppet::Functions.any_signature(*from_to_names(func_info))
      last_captures_rest = (type.size_range[1] == Float::INFINITY)

      # The method '3x_function' here is a dummy as the dispatcher is not used for calling, only for information.
      the_class.dispatcher.add(Puppet::Pops::Functions::Dispatch.new(type, '3x_function', names, last_captures_rest))
      # The function class is returned as the result of the create function method
      the_class
    end

    # Compute min and max number of arguments and a list of constructed
    # parameter names p0 - pn (since there are no parameter names in 3x functions).
    #
    # @api private
    def self.from_to_names(func_info)
      arity = func_info[:arity]
      if arity.nil?
        arity = -1
      end
      if arity < 0
        from = -arity - 1 # arity -1 is 0 min param, -2 is min 1 param
        to = :default     # infinite range
        count = -arity    # the number of named parameters
      else
        count = from = to = arity
      end
      # Names of parameters, up to 5 are optimized and use frozen version
      # Note that (0..count-1) produces expected empty array for count == 0, 0-n for count >= 1
      names = count <= 5 ? PARAM_NAMES[count] : (0..count - 1).map { |n| "p#{n}" }
      [from, to, names]
    end
  end

  # Injection and Weaving of parameters
  # ---
  # It is possible to inject and weave a set of well known parameters into a call.
  # These extra parameters are not part of the parameters passed from the Puppet
  # logic, and  they can not be overridden by parameters given as arguments in the
  # call. They are invisible to the Puppet Language.
  #
  # @example using injected parameters
  #   Puppet::Functions.create_function('test') do
  #     dispatch :test do
  #       param 'Scalar', 'a'
  #       param 'Scalar', 'b'
  #       scope_param
  #     end
  #     def test(a, b, scope)
  #       a > b ? scope['a'] : scope['b']
  #     end
  #   end
  #
  # The function in the example above is called like this:
  #
  #     test(10, 20)
  #
  # @api private
  class InternalDispatchBuilder < DispatcherBuilder
    # Inject parameter for `Puppet::Parser::Scope`
    def scope_param
      inject(:scope)
    end

    # Inject parameter for `Puppet::Pal::ScriptCompiler`
    def script_compiler_param
      inject(:pal_script_compiler)
    end

    # Inject a parameter getting a cached hash for this function
    def cache_param
      inject(:cache)
    end

    # Inject parameter for `Puppet::Pal::CatalogCompiler`
    def compiler_param
      inject(:pal_catalog_compiler)
    end

    # Inject parameter for either `Puppet::Pal::CatalogCompiler` or `Puppet::Pal::ScriptCompiler`
    def pal_compiler_param
      inject(:pal_compiler)
    end

    private

    def inject(injection_name)
      @injections << injection_name
      # mark what should be picked for this position when dispatching
      @weaving << [@injections.size() - 1]
    end
  end
end