diff --git a/lib/puppet/parser/ast/lambda.rb b/lib/puppet/parser/ast/lambda.rb index 74ffa710c..64d78d49b 100644 --- a/lib/puppet/parser/ast/lambda.rb +++ b/lib/puppet/parser/ast/lambda.rb @@ -1,131 +1,135 @@ require 'puppet/parser/ast/block_expression' class Puppet::Parser::AST # A block of statements/expressions with additional parameters # Requires scope to contain the values for the defined parameters when evaluated # If evaluated without a prepared scope, the lambda will behave like its super class. # class Lambda < AST::BlockExpression # The lambda parameters. # These are encoded as an array where each entry is an array of one or two object. The first # is the parameter name, and the optional second object is the value expression (that will # be evaluated when bound to a scope). # The value expression is the default value for the parameter. All default values must be # at the end of the parameter list. # # @return [Array>] list of parameter names with optional value expression attr_accessor :parameters # Evaluates each expression/statement and produce the last expression evaluation result # @return [Object] what the last expression evaluated to def evaluate(scope) if @children.is_a? Puppet::Parser::AST::ASTArray result = nil @children.each {|expr| result = expr.evaluate(scope) } result else @children.evaluate(scope) end end # Calls the lambda. # Assigns argument values in a nested local scope that should be used to evaluate the lambda # and then evaluates the lambda. # @param scope [Puppet::Scope] the calling scope # @return [Object] the result of evaluating the expression(s) in the lambda # def call(scope, *args) raise Puppet::ParseError, "Too many arguments: #{args.size} for #{parameters.size}" unless args.size <= parameters.size # associate values with parameters merged = parameters.zip(args) # calculate missing arguments missing = parameters.slice(args.size, parameters.size - args.size).select {|e| e.size == 1} unless missing.empty? optional = parameters.count { |p| p.size == 2 } raise Puppet::ParseError, "Too few arguments; #{args.size} for #{optional > 0 ? ' min ' : ''}#{parameters.size - optional}" end evaluated = merged.collect do |m| # m can be one of # m = [["name"], "given"] # | [["name", default_expr], "given"] # # "given" is always an optional entry. If a parameter was provided then # the entry will be in the array, otherwise the m array will be a # single element. given_argument = m[1] argument_name = m[0][0] default_expression = m[0][1] value = if m.size == 1 default_expression.safeevaluate(scope) else given_argument end [argument_name, value] end # Store the evaluated name => value associations in a new inner/local/ephemeral scope # (This is made complicated due to the fact that the implementation of scope is overloaded with # functionality and an inner ephemeral scope must be used (as opposed to just pushing a local scope # on a scope "stack"). # Ensure variable exists with nil value if error occurs. # Some ruby implementations does not like creating variable on return result = nil begin elevel = scope.ephemeral_level scope.ephemeral_from(Hash[evaluated], file, line) result = safeevaluate(scope) ensure scope.unset_ephemeral_var(elevel) end result end # Validates the lambda. # Validation checks if parameters with default values are at the end of the list. (It is illegal # to have a parameter with default value followed by one without). # # @raise [Puppet::ParseError] if a parameter with a default comes before a parameter without default value # def validate params = parameters || [] defaults = params.drop_while {|p| p.size < 2 } trailing = defaults.drop_while {|p| p.size == 2 } raise Puppet::ParseError, "Lambda parameters with default values must be placed last" unless trailing.empty? end # Returns the number of parameters (required and optional) # @return [Integer] the total number of accepted parameters def parameter_count @parameters.size end # Returns the number of optional parameters. # @return [Integer] the number of optional accepted parameters def optional_parameter_count @parameters.count {|p| p.size == 2 } end def initialize(options) super(options) # ensure there is an empty parameters structure if not given by creator @parameters = [] unless options[:parameters] validate end def to_s result = ["{|"] result += @parameters.collect {|p| "#{p[0]}" + (p.size == 2 && p[1]) ? p[1].to_s() : '' }.join(', ') result << "| ... }" result.join('') end # marker method checked with respond_to :puppet_lambda def puppet_lambda() true end + + def parameter_names + @parameters.collect {|p| p[0] } + end end end diff --git a/lib/puppet/parser/functions/each.rb b/lib/puppet/parser/functions/each.rb index f4de4a155..39d26ba38 100644 --- a/lib/puppet/parser/functions/each.rb +++ b/lib/puppet/parser/functions/each.rb @@ -1,107 +1,109 @@ Puppet::Parser::Functions::newfunction( :each, :type => :rvalue, :arity => 2, :doc => <<-'ENDHEREDOC') do |args| Applies a parameterized block to each element in a sequence of selected entries from the first argument and returns the first argument. This function takes two mandatory arguments: the first should be an Array or a Hash or something that is of enumerable type (integer, Integer range, or String), and the second a parameterized block as produced by the puppet syntax: $a.each |$x| { ... } each($a) |$x| { ... } When the first argument is an Array (or of enumerable type other than Hash), the parameterized block should define one or two block parameters. For each application of the block, the next element from the array is selected, and it is passed to the block if the block has one parameter. If the block has two parameters, the first is the elements index, and the second the value. The index starts from 0. $a.each |$index, $value| { ... } each($a) |$index, $value| { ... } When the first argument is a Hash, the parameterized block should define one or two parameters. When one parameter is defined, the iteration is performed with each entry as an array of `[key, value]`, and when two parameters are defined the iteration is performed with key and value. $a.each |$entry| { ..."key ${$entry[0]}, value ${$entry[1]}" } $a.each |$key, $value| { ..."key ${key}, value ${value}" } *Examples* [1,2,3].each |$val| { ... } # 1, 2, 3 [5,6,7].each |$index, $val| { ... } # (0, 5), (1, 6), (2, 7) {a=>1, b=>2, c=>3}].each |$val| { ... } # ['a', 1], ['b', 2], ['c', 3] {a=>1, b=>2, c=>3}.each |$key, $val| { ... } # ('a', 1), ('b', 2), ('c', 3) Integer[ 10, 20 ].each |$index, $value| { ... } # (0, 10), (1, 11) ... "hello".each |$char| { ... } # 'h', 'e', 'l', 'l', 'o' 3.each |$number| { ... } # 0, 1, 2 - Since 3.2 for Array and Hash - Since 3.5 for other enumerables - requires `parser = future`. ENDHEREDOC require 'puppet/parser/ast/lambda' def foreach_Hash(o, scope, pblock, serving_size) enumerator = o.each_pair if serving_size == 1 (o.size).times do pblock.call(scope, enumerator.next) end else (o.size).times do pblock.call(scope, *enumerator.next) end end end def foreach_Enumerator(enumerator, scope, pblock, serving_size) index = 0 if serving_size == 1 begin loop { pblock.call(scope, enumerator.next) } rescue StopIteration end else begin loop do pblock.call(scope, index, enumerator.next) index = index +1 end rescue StopIteration end end end - raise ArgumentError, ("each(): wrong number of arguments (#{args.length}; must be 2)") if args.length != 2 + raise ArgumentError, ("each(): wrong number of arguments (#{args.length}; expected 2, got #{args.length})") if args.length != 2 receiver = args[0] pblock = args[1] raise ArgumentError, ("each(): wrong argument type (#{args[1].class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) serving_size = pblock.parameter_count if serving_size == 0 - raise ArgumentError, "each(): block must define at least one parameter; value." + raise ArgumentError, "each(): block must define at least one parameter; value. Block has 0." end case receiver when Hash if serving_size > 2 - raise ArgumentError, "each(): block must define at most two parameters; key, value" + raise ArgumentError, "each(): block must define at most two parameters; key, value. Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') end foreach_Hash(receiver, self, pblock, serving_size) else if serving_size > 2 - raise ArgumentError, "each(): block must define at most two parameters; index, value" + raise ArgumentError, "each(): block must define at most two parameters; index, value. Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') end enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) unless enum raise ArgumentError, ("each(): wrong argument type (#{receiver.class}; must be something enumerable.") end foreach_Enumerator(enum, self, pblock, serving_size) end # each always produces the receiver receiver end diff --git a/lib/puppet/parser/functions/filter.rb b/lib/puppet/parser/functions/filter.rb index 70ab024de..0296e3337 100644 --- a/lib/puppet/parser/functions/filter.rb +++ b/lib/puppet/parser/functions/filter.rb @@ -1,98 +1,100 @@ require 'puppet/parser/ast/lambda' Puppet::Parser::Functions::newfunction( :filter, :type => :rvalue, :arity => 2, :doc => <<-'ENDHEREDOC') do |args| Applies a parameterized block to each element in a sequence of entries from the first argument and returns an array or hash (same type as left operand for array/hash, and array for other enumerable types) with the entries for which the block evaluates to `true`. This function takes two mandatory arguments: the first should be an Array, a Hash, or an Enumerable object (integer, Integer range, or String), and the second a parameterized block as produced by the puppet syntax: $a.filter |$x| { ... } filter($a) |$x| { ... } When the first argument is something other than a Hash, the block is called with each entry in turn. When the first argument is a Hash the entry is an array with `[key, value]`. *Examples* # selects all that end with berry $a = ["raspberry", "blueberry", "orange"] $a.filter |$x| { $x =~ /berry$/ } # rasberry, blueberry If the the block defines two parameters, they will be set to `index, value` (with index starting at 0) for all enumerables except Hash, and to `key, value` for a Hash. *Examples* # selects all that end with 'berry' at an even numbered index $a = ["raspberry", "blueberry", "orange"] $a.filter |$index, $x| { $index % 2 == 0 and $x =~ /berry$/ } # raspberry # selects all that end with 'berry' and value >= 1 $a = {"raspberry"=>0, "blueberry"=>1, "orange"=>1} $a.filter |$key, $x| { $x =~ /berry$/ and $x >= 1 } # blueberry - Since 3.4 for Array and Hash - Since 3.5 for other enumerables - requires `parser = future` ENDHEREDOC def filter_Enumerator(enumerator, scope, pblock, serving_size) result = [] index = 0 if serving_size == 1 begin loop { pblock.call(scope, it = enumerator.next) == true ? result << it : nil } rescue StopIteration end else begin loop do pblock.call(scope, index, it = enumerator.next) == true ? result << it : nil index = index +1 end rescue StopIteration end end result end receiver = args[0] pblock = args[1] raise ArgumentError, ("filter(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) serving_size = pblock.parameter_count if serving_size == 0 - raise ArgumentError, "filter(): block must define at least one parameter; value." + raise ArgumentError, "filter(): block must define at least one parameter; value. Block has 0." end case receiver when Hash if serving_size > 2 - raise ArgumentError, "filter(): block must define at most two parameters; key, value" + raise ArgumentError, "filter(): block must define at most two parameters; key, value. Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') end if serving_size == 1 result = receiver.select {|x, y| pblock.call(self, [x, y]) } else result = receiver.select {|x, y| pblock.call(self, x, y) } end # Ruby 1.8.7 returns Array result = Hash[result] unless result.is_a? Hash result else if serving_size > 2 - raise ArgumentError, "filter(): block must define at most two parameters; index, value" + raise ArgumentError, "filter(): block must define at most two parameters; index, value. Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') end enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) unless enum raise ArgumentError, ("filter(): wrong argument type (#{receiver.class}; must be something enumerable.") end filter_Enumerator(enum, self, pblock, serving_size) end end diff --git a/lib/puppet/parser/functions/map.rb b/lib/puppet/parser/functions/map.rb index 8cfd60daf..8bc9fd383 100644 --- a/lib/puppet/parser/functions/map.rb +++ b/lib/puppet/parser/functions/map.rb @@ -1,94 +1,96 @@ require 'puppet/parser/ast/lambda' Puppet::Parser::Functions::newfunction( :map, :type => :rvalue, :arity => 2, :doc => <<-'ENDHEREDOC') do |args| Applies a parameterized block to each element in a sequence of entries from the first argument and returns an array with the result of each invocation of the parameterized block. This function takes two mandatory arguments: the first should be an Array, Hash, or of Enumerable type (integer, Integer range, or String), and the second a parameterized block as produced by the puppet syntax: $a.map |$x| { ... } map($a) |$x| { ... } When the first argument `$a` is an Array or of enumerable type, the block is called with each entry in turn. When the first argument is a hash the entry is an array with `[key, value]`. *Examples* # Turns hash into array of values $a.map |$x|{ $x[1] } # Turns hash into array of keys $a.map |$x| { $x[0] } When using a block with 2 parameters, the element's index (starting from 0) for an array, and the key for a hash is given to the block's first parameter, and the value is given to the block's second parameter.args. *Examples* # Turns hash into array of values $a.map |$key,$val|{ $val } # Turns hash into array of keys $a.map |$key,$val|{ $key } - Since 3.4 for Array and Hash - Since 3.5 for other enumerables, and support for blocks with 2 parameters - requires `parser = future` ENDHEREDOC def map_Enumerator(enumerator, scope, pblock, serving_size) result = [] index = 0 if serving_size == 1 begin loop { result << pblock.call(scope, enumerator.next) } rescue StopIteration end else begin loop do result << pblock.call(scope, index, enumerator.next) index = index +1 end rescue StopIteration end end result end receiver = args[0] pblock = args[1] raise ArgumentError, ("map(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) serving_size = pblock.parameter_count if serving_size == 0 - raise ArgumentError, "map(): block must define at least one parameter; value." + raise ArgumentError, "map(): block must define at least one parameter; value. Block has 0." end case receiver when Hash if serving_size > 2 - raise ArgumentError, "map(): block must define at most two parameters; key, value" + raise ArgumentError, "map(): block must define at most two parameters; key, value.args Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') end if serving_size == 1 result = receiver.map {|x, y| pblock.call(self, [x, y]) } else result = receiver.map {|x, y| pblock.call(self, x, y) } end else if serving_size > 2 - raise ArgumentError, "map(): block must define at most two parameters; index, value" + raise ArgumentError, "map(): block must define at most two parameters; index, value. Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') end enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) unless enum raise ArgumentError, ("map(): wrong argument type (#{receiver.class}; must be something enumerable.") end result = map_Enumerator(enum, self, pblock, serving_size) end result end diff --git a/lib/puppet/parser/functions/reduce.rb b/lib/puppet/parser/functions/reduce.rb index 97632b676..078ebc2e9 100644 --- a/lib/puppet/parser/functions/reduce.rb +++ b/lib/puppet/parser/functions/reduce.rb @@ -1,94 +1,100 @@ Puppet::Parser::Functions::newfunction( :reduce, :type => :rvalue, :arity => -2, :doc => <<-'ENDHEREDOC') do |args| Applies a parameterized block to each element in a sequence of entries from the first argument (_the enumerable_) and returns the last result of the invocation of the parameterized block. This function takes two mandatory arguments: the first should be an Array, Hash, or something of enumerable type, and the last a parameterized block as produced by the puppet syntax: $a.reduce |$memo, $x| { ... } reduce($a) |$memo, $x| { ... } When the first argument is an Array or someting of an enumerable type, the block is called with each entry in turn. When the first argument is a hash each entry is converted to an array with `[key, value]` before being fed to the block. An optional 'start memo' value may be supplied as an argument between the array/hash and mandatory block. $a.reduce(start) |$memo, $x| { ... } reduce($a, start) |$memo, $x| { ... } If no 'start memo' is given, the first invocation of the parameterized block will be given the first and second elements of the enumeration, and if the enumerable has fewer than 2 elements, the first element is produced as the result of the reduction without invocation of the block. On each subsequent invocation, the produced value of the invoked parameterized block is given as the memo in the next invocation. *Examples* # Reduce an array $a = [1,2,3] $a.reduce |$memo, $entry| { $memo + $entry } #=> 6 # Reduce hash values $a = {a => 1, b => 2, c => 3} $a.reduce |$memo, $entry| { [sum, $memo[1]+$entry[1]] } #=> [sum, 6] # reverse a string "abc".reduce |$memo, $char| { "$char$memo" } #=>"cbe" It is possible to provide a starting 'memo' as an argument. *Examples* # Reduce an array $a = [1,2,3] $a.reduce(4) |$memo, $entry| { $memo + $entry } #=> 10 # Reduce hash values $a = {a => 1, b => 2, c => 3} $a.reduce([na, 4]) |$memo, $entry| { [sum, $memo[1]+$entry[1]] } #=> [sum, 10] *Examples* Integer[1,4].reduce |$memo, $x| { $memo + $x } #=> 10 - Since 3.2 for Array and Hash - Since 3.5 for additional enumerable types - requires `parser = future`. ENDHEREDOC require 'puppet/parser/ast/lambda' case args.length when 2 pblock = args[1] when 3 pblock = args[2] else - raise ArgumentError, ("reduce(): wrong number of arguments (#{args.length}; must be 2 or 3)") + raise ArgumentError, ("reduce(): wrong number of arguments (#{args.length}; expected 2 or 3, got #{args.length})") end unless pblock.respond_to?(:puppet_lambda) raise ArgumentError, ("reduce(): wrong argument type (#{pblock.class}; must be a parameterized block.") end receiver = args[0] enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) unless enum raise ArgumentError, ("reduce(): wrong argument type (#{receiver.class}; must be something enumerable.") end + serving_size = pblock.parameter_count + if serving_size != 2 + raise ArgumentError, "reduce(): block must define 2 parameters; memo, value. Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') + end + if args.length == 3 enum.reduce(args[1]) {|memo, x| pblock.call(self, memo, x) } else enum.reduce {|memo, x| pblock.call(self, memo, x) } end end diff --git a/lib/puppet/parser/functions/slice.rb b/lib/puppet/parser/functions/slice.rb index e18e043b5..bcc830f74 100644 --- a/lib/puppet/parser/functions/slice.rb +++ b/lib/puppet/parser/functions/slice.rb @@ -1,114 +1,116 @@ Puppet::Parser::Functions::newfunction( :slice, :type => :rvalue, :arity => -2, :doc => <<-'ENDHEREDOC') do |args| Applies a parameterized block to each _slice_ of elements in a sequence of selected entries from the first argument and returns the first argument, or if no block is given returns a new array with a concatenation of the slices. This function takes two mandatory arguments: the first, `$a`, should be an Array, Hash, or something of enumerable type (integer, Integer range, or String), and the second, `$n`, the number of elements to include in each slice. The optional third argument should be a a parameterized block as produced by the puppet syntax: $a.slice($n) |$x| { ... } slice($a) |$x| { ... } The parameterized block should have either one parameter (receiving an array with the slice), or the same number of parameters as specified by the slice size (each parameter receiving its part of the slice). In case there are fewer remaining elements than the slice size for the last slice it will contain the remaining elements. When the block has multiple parameters, excess parameters are set to :undef for an array or enumerable type, and to empty arrays for a Hash. $a.slice(2) |$first, $second| { ... } When the first argument is a Hash, each `key,value` entry is counted as one, e.g, a slice size of 2 will produce an array of two arrays with key, and value. $a.slice(2) |$entry| { notice "first ${$entry[0]}, second ${$entry[1]}" } $a.slice(2) |$first, $second| { notice "first ${first}, second ${second}" } When called without a block, the function produces a concatenated result of the slices. slice([1,2,3,4,5,6], 2) # produces [[1,2], [3,4], [5,6]] slice(Integer[1,6], 2) # produces [[1,2], [3,4], [5,6]] slice(4,2) # produces [[0,1], [2,3]] slice('hello',2) # produces [[h, e], [l, l], [o]] - Since 3.2 for Array and Hash - Since 3.5 for additional enumerable types - requires `parser = future`. ENDHEREDOC require 'puppet/parser/ast/lambda' require 'puppet/parser/scope' def each_Common(o, slice_size, filler, scope, pblock) serving_size = pblock ? pblock.parameter_count : 1 if serving_size == 0 - raise ArgumentError, "slice(): block must define at least one parameter." + raise ArgumentError, "slice(): block must define at least one parameter. Block has 0." end unless serving_size == 1 || serving_size == slice_size - raise ArgumentError, "slice(): block must define one parameter, or the same number of parameters as the given size of the slice (#{slice_size})." + raise ArgumentError, "slice(): block must define one parameter, or " + + "the same number of parameters as the given size of the slice (#{slice_size}). Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') end enumerator = o.each_slice(slice_size) result = [] if serving_size == 1 begin if pblock loop do pblock.call(scope, enumerator.next) end else loop do result << enumerator.next end end rescue StopIteration end else begin loop do a = enumerator.next if a.size < serving_size a = a.dup.fill(filler, a.length...serving_size) end pblock.call(scope, *a) end rescue StopIteration end end if pblock o else result end end raise ArgumentError, ("slice(): wrong number of arguments (#{args.length}; must be 2 or 3)") unless args.length == 2 || args.length == 3 if args.length >= 2 begin slice_size = Puppet::Parser::Scope.number?(args[1]) rescue raise ArgumentError, ("slice(): wrong argument type (#{args[1]}; must be number.") end end raise ArgumentError, ("slice(): wrong argument type (#{args[1]}; must be number.") unless slice_size raise ArgumentError, ("slice(): wrong argument value: #{slice_size}; is not a positive integer number > 0") unless slice_size.is_a?(Fixnum) && slice_size > 0 receiver = args[0] # the block is optional, ok if nil, function then produces an array pblock = args[2] raise ArgumentError, ("slice(): wrong argument type (#{args[2].class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) || args.length == 2 case receiver when Hash each_Common(receiver, slice_size, [], self, pblock) else enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) if enum.nil? raise ArgumentError, ("slice(): given type '#{tc.string(receiver)}' is not enumerable") end result = each_Common(enum, slice_size, :undef, self, pblock) pblock ? receiver : result end end diff --git a/lib/puppet/pops/evaluator/closure.rb b/lib/puppet/pops/evaluator/closure.rb index 05a4fd55a..d1afed8d1 100644 --- a/lib/puppet/pops/evaluator/closure.rb +++ b/lib/puppet/pops/evaluator/closure.rb @@ -1,48 +1,52 @@ # A Closure represents logic bound to a particular scope. # As long as the runtime (basically the scope implementation) has the behaviour of Puppet 3x it is not # safe to use this closure when the the scope given to it when initialized goes "out of scope". # # Note that the implementation is backwards compatible in that the call method accepts a scope, but this # scope is not used. # class Puppet::Pops::Evaluator::Closure attr_reader :evaluator attr_reader :model attr_reader :enclosing_scope def initialize(evaluator, model, scope) @evaluator = evaluator @model = model @enclosing_scope = scope end # marker method checked with respond_to :puppet_lambda def puppet_lambda() true end # compatible with 3x AST::Lambda def call(scope, *args) @evaluator.call(self, args, @enclosing_scope) end # incompatible with 3x except that it is an array of the same size 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 # Returns the number of optional parameters. # @return [Integer] the number of optional accepted parameters def optional_parameter_count @model.parameters.count { |p| !p.value.nil? } end + def parameter_names + @model.parameters.collect {|p| p.name } + end + end