diff --git a/lib/puppet/pops/evaluator/evaluator_impl.rb b/lib/puppet/pops/evaluator/evaluator_impl.rb index 45cae03fc..f80cb2839 100644 --- a/lib/puppet/pops/evaluator/evaluator_impl.rb +++ b/lib/puppet/pops/evaluator/evaluator_impl.rb @@ -1,1119 +1,1124 @@ require 'rgen/ecore/ecore' require 'puppet/pops/evaluator/compare_operator' require 'puppet/pops/evaluator/relationship_operator' require 'puppet/pops/evaluator/access_operator' require 'puppet/pops/evaluator/closure' require 'puppet/pops/evaluator/external_syntax_support' # This implementation of {Puppet::Pops::Evaluator} performs evaluation using the puppet 3.x runtime system # in a manner largely compatible with Puppet 3.x, but adds new features and introduces constraints. # # The evaluation uses _polymorphic dispatch_ which works by dispatching to the first found method named after # the class or one of its super-classes. The EvaluatorImpl itself mainly deals with evaluation (it currently # also handles assignment), and it uses a delegation pattern to more specialized handlers of some operators # that in turn use polymorphic dispatch; this to not clutter EvaluatorImpl with too much responsibility). # # Since a pattern is used, only the main entry points are fully documented. The parameters _o_ and _scope_ are # the same in all the polymorphic methods, (the type of the parameter _o_ is reflected in the method's name; # either the actual class, or one of its super classes). The _scope_ parameter is always the scope in which # the evaluation takes place. If nothing else is mentioned, the return is always the result of evaluation. # # See {Puppet::Pops::Visitable} and {Puppet::Pops::Visitor} for more information about # polymorphic calling. # class Puppet::Pops::Evaluator::EvaluatorImpl include Puppet::Pops::Utils # Provides access to the Puppet 3.x runtime (scope, etc.) # This separation has been made to make it easier to later migrate the evaluator to an improved runtime. # include Puppet::Pops::Evaluator::Runtime3Support include Puppet::Pops::Evaluator::ExternalSyntaxSupport # This constant is not defined as Float::INFINITY in Ruby 1.8.7 (but is available in later version # Refactor when support is dropped for Ruby 1.8.7. # INFINITY = 1.0 / 0.0 EMPTY_STRING = ''.freeze COMMA_SEPARATOR = ', '.freeze # Reference to Issues name space makes it easier to refer to issues # (Issues are shared with the validator). # Issues = Puppet::Pops::Issues def initialize @@eval_visitor ||= Puppet::Pops::Visitor.new(self, "eval", 1, 1) @@lvalue_visitor ||= Puppet::Pops::Visitor.new(self, "lvalue", 1, 1) @@assign_visitor ||= Puppet::Pops::Visitor.new(self, "assign", 3, 3) @@string_visitor ||= Puppet::Pops::Visitor.new(self, "string", 1, 1) @@type_calculator ||= Puppet::Pops::Types::TypeCalculator.new() @@type_parser ||= Puppet::Pops::Types::TypeParser.new() @@compare_operator ||= Puppet::Pops::Evaluator::CompareOperator.new() @@relationship_operator ||= Puppet::Pops::Evaluator::RelationshipOperator.new() # Initialize the runtime module Puppet::Pops::Evaluator::Runtime3Support.instance_method(:initialize).bind(self).call() end # @api private def type_calculator @@type_calculator end # Evaluates the given _target_ object in the given scope. # # @overload evaluate(target, scope) # @param target [Object] evaluation target - see methods on the pattern assign_TYPE for actual supported types. # @param scope [Object] the runtime specific scope class where evaluation should take place # @return [Object] the result of the evaluation # # @api public # def evaluate(target, scope) begin @@eval_visitor.visit_this_1(self, target, scope) rescue Puppet::Pops::SemanticError => e # a raised issue may not know the semantic target fail(e.issue, e.semantic || target, e.options, e) rescue StandardError => e if e.is_a? Puppet::ParseError # ParseError's are supposed to be fully configured with location information raise e end fail(Issues::RUNTIME_ERROR, target, {:detail => e.message}, e) end end # Assigns the given _value_ to the given _target_. The additional argument _o_ is the instruction that # produced the target/value tuple and it is used to set the origin of the result. # # @param target [Object] assignment target - see methods on the pattern assign_TYPE for actual supported types. # @param value [Object] the value to assign to `target` # @param o [Puppet::Pops::Model::PopsObject] originating instruction # @param scope [Object] the runtime specific scope where evaluation should take place # # @api private # def assign(target, value, o, scope) @@assign_visitor.visit_this_3(self, target, value, o, scope) end # Computes a value that can be used as the LHS in an assignment. # @param o [Object] the expression to evaluate as a left (assignable) entity # @param scope [Object] the runtime specific scope where evaluation should take place # # @api private # def lvalue(o, scope) @@lvalue_visitor.visit_this_1(self, o, scope) end # Produces a String representation of the given object _o_ as used in interpolation. # @param o [Object] the expression of which a string representation is wanted # @param scope [Object] the runtime specific scope where evaluation should take place # # @api public # def string(o, scope) @@string_visitor.visit_this_1(self, o, scope) end # Evaluate a BlockExpression in a new scope with variables bound to the # given values. # # @param scope [Puppet::Parser::Scope] the parent scope # @param variable_bindings [Hash{String => Object}] the variable names and values to bind (names are keys, bound values are values) # @param block [Puppet::Pops::Model::BlockExpression] the sequence of expressions to evaluate in the new scope # # @api private # def evaluate_block_with_bindings(scope, variable_bindings, block_expr) with_guarded_scope(scope) do # change to create local scope_from - cannot give it file and line - # that is the place of the call, not "here" create_local_scope_from(variable_bindings, scope) evaluate(block_expr, scope) end end protected def lvalue_VariableExpression(o, scope) # evaluate the name evaluate(o.expr, scope) end # Catches all illegal lvalues # def lvalue_Object(o, scope) fail(Issues::ILLEGAL_ASSIGNMENT, o) end # Assign value to named variable. # The '$' sign is never part of the name. # @example In Puppet DSL # $name = value # @param name [String] name of variable without $ # @param value [Object] value to assign to the variable # @param o [Puppet::Pops::Model::PopsObject] originating instruction # @param scope [Object] the runtime specific scope where evaluation should take place # @return [value] # def assign_String(name, value, o, scope) if name =~ /::/ fail(Issues::CROSS_SCOPE_ASSIGNMENT, o.left_expr, {:name => name}) end set_variable(name, value, o, scope) value end def assign_Numeric(n, value, o, scope) fail(Issues::ILLEGAL_NUMERIC_ASSIGNMENT, o.left_expr, {:varname => n.to_s}) end # Catches all illegal assignment (e.g. 1 = 2, {'a'=>1} = 2, etc) # def assign_Object(name, value, o, scope) fail(Issues::ILLEGAL_ASSIGNMENT, o) end def eval_Factory(o, scope) evaluate(o.current, scope) end # Evaluates any object not evaluated to something else to itself. def eval_Object o, scope o end # Allows nil to be used as a Nop, Evaluates to nil def eval_NilClass(o, scope) nil end # Evaluates Nop to nil. def eval_Nop(o, scope) nil end # Captures all LiteralValues not handled elsewhere. # def eval_LiteralValue(o, scope) o.value end # Reserved Words fail to evaluate # def eval_ReservedWord(o, scope) fail(Puppet::Pops::Issues::RESERVED_WORD, o, {:word => o.word}) end def eval_LiteralDefault(o, scope) :default end def eval_LiteralUndef(o, scope) nil end # A QualifiedReference (i.e. a capitalized qualified name such as Foo, or Foo::Bar) evaluates to a PType # def eval_QualifiedReference(o, scope) @@type_parser.interpret(o) end def eval_NotExpression(o, scope) ! is_true?(evaluate(o.expr, scope)) end def eval_UnaryMinusExpression(o, scope) - coerce_numeric(evaluate(o.expr, scope), o, scope) end def eval_UnfoldExpression(o, scope) candidate = evaluate(o.expr, scope) case candidate when Array candidate when Hash candidate.to_a else # turns anything else into an array (so result can be unfolded) [candidate] end end # Abstract evaluation, returns array [left, right] with the evaluated result of left_expr and # right_expr # @return > array with result of evaluating left and right expressions # def eval_BinaryExpression o, scope [ evaluate(o.left_expr, scope), evaluate(o.right_expr, scope) ] end # Evaluates assignment with operators =, +=, -= and # # @example Puppet DSL # $a = 1 # $a += 1 # $a -= 1 # def eval_AssignmentExpression(o, scope) name = lvalue(o.left_expr, scope) value = evaluate(o.right_expr, scope) case o.operator when :'=' # regular assignment assign(name, value, o, scope) when :'+=' # if value does not exist and strict is on, looking it up fails, else it is nil or :undef existing_value = get_variable_value(name, o, scope) begin # Supports :undef as it may come from a 3x structure. if existing_value.nil? || existing_value == :undef assign(name, value, o, scope) else # Delegate to calculate function to deal with check of LHS, and perform ´+´ as arithmetic or concatenation the # same way as ArithmeticExpression performs `+`. assign(name, calculate(existing_value, value, :'+', o.left_expr, o.right_expr, scope), o, scope) end rescue ArgumentError => e fail(Issues::APPEND_FAILED, o, {:message => e.message}) end when :'-=' # If an attempt is made to delete values from something that does not exists, the value is :undef (it is guaranteed to not # include any values the user wants deleted anyway :-) # # if value does not exist and strict is on, looking it up fails, else it is nil or :undef existing_value = get_variable_value(name, o, scope) begin # Supports :undef as it may come from a 3x structure. if existing_value.nil? || existing_value == :undef assign(name, nil, o, scope) else # Delegate to delete function to deal with check of LHS, and perform deletion assign(name, delete(get_variable_value(name, o, scope), value), o, scope) end rescue ArgumentError => e fail(Issues::APPEND_FAILED, o, {:message => e.message}, e) end else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end value end ARITHMETIC_OPERATORS = [:'+', :'-', :'*', :'/', :'%', :'<<', :'>>'] COLLECTION_OPERATORS = [:'+', :'-', :'<<'] # Handles binary expression where lhs and rhs are array/hash or numeric and operator is +, - , *, % / << >> # def eval_ArithmeticExpression(o, scope) left = evaluate(o.left_expr, scope) right = evaluate(o.right_expr, scope) begin result = calculate(left, right, o.operator, o.left_expr, o.right_expr, scope) rescue ArgumentError => e fail(Issues::RUNTIME_ERROR, o, {:detail => e.message}, e) end result end # Handles binary expression where lhs and rhs are array/hash or numeric and operator is +, - , *, % / << >> # def calculate(left, right, operator, left_o, right_o, scope) unless ARITHMETIC_OPERATORS.include?(operator) fail(Issues::UNSUPPORTED_OPERATOR, left_o.eContainer, {:operator => o.operator}) end if (left.is_a?(Array) || left.is_a?(Hash)) && COLLECTION_OPERATORS.include?(operator) # Handle operation on collections case operator when :'+' concatenate(left, right) when :'-' delete(left, right) when :'<<' unless left.is_a?(Array) fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) end left + [right] end else # Handle operation on numeric left = coerce_numeric(left, left_o, scope) right = coerce_numeric(right, right_o, scope) begin if operator == :'%' && (left.is_a?(Float) || right.is_a?(Float)) # Deny users the fun of seeing severe rounding errors and confusing results fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) end result = left.send(operator, right) rescue NoMethodError => e fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) rescue ZeroDivisionError => e fail(Issues::DIV_BY_ZERO, right_o) end if result == INFINITY || result == -INFINITY fail(Issues::RESULT_IS_INFINITY, left_o, {:operator => operator}) end result end end def eval_EppExpression(o, scope) scope["@epp"] = [] evaluate(o.body, scope) result = scope["@epp"].join result end def eval_RenderStringExpression(o, scope) scope["@epp"] << o.value.dup nil end def eval_RenderExpression(o, scope) scope["@epp"] << string(evaluate(o.expr, scope), scope) nil end # Evaluates Puppet DSL ->, ~>, <-, and <~ def eval_RelationshipExpression(o, scope) # First level evaluation, reduction to basic data types or puppet types, the relationship operator then translates this # to the final set of references (turning strings into references, which can not naturally be done by the main evaluator since # all strings should not be turned into references. # real = eval_BinaryExpression(o, scope) @@relationship_operator.evaluate(real, o, scope) end # Evaluates x[key, key, ...] # def eval_AccessExpression(o, scope) left = evaluate(o.left_expr, scope) keys = o.keys.nil? ? [] : o.keys.collect {|key| evaluate(key, scope) } Puppet::Pops::Evaluator::AccessOperator.new(o).access(left, scope, *keys) end # Evaluates <, <=, >, >=, and == # def eval_ComparisonExpression o, scope left = evaluate(o.left_expr, scope) right = evaluate(o.right_expr, scope) begin # Left is a type if left.is_a?(Puppet::Pops::Types::PAnyType) case o.operator when :'==' @@type_calculator.equals(left,right) when :'!=' !@@type_calculator.equals(left,right) when :'<' # left can be assigned to right, but they are not equal @@type_calculator.assignable?(right, left) && ! @@type_calculator.equals(left,right) when :'<=' # left can be assigned to right @@type_calculator.assignable?(right, left) when :'>' # right can be assigned to left, but they are not equal @@type_calculator.assignable?(left,right) && ! @@type_calculator.equals(left,right) when :'>=' # right can be assigned to left @@type_calculator.assignable?(left, right) else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end else case o.operator when :'==' @@compare_operator.equals(left,right) when :'!=' ! @@compare_operator.equals(left,right) when :'<' @@compare_operator.compare(left,right) < 0 when :'<=' @@compare_operator.compare(left,right) <= 0 when :'>' @@compare_operator.compare(left,right) > 0 when :'>=' @@compare_operator.compare(left,right) >= 0 else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end end rescue ArgumentError => e fail(Issues::COMPARISON_NOT_POSSIBLE, o, { :operator => o.operator, :left_value => left, :right_value => right, :detail => e.message}, e) end end # Evaluates matching expressions with type, string or regexp rhs expression. # If RHS is a type, the =~ matches compatible (instance? of) type. # # @example # x =~ /abc.*/ # @example # x =~ "abc.*/" # @example # y = "abc" # x =~ "${y}.*" # @example # [1,2,3] =~ Array[Integer[1,10]] # # Note that a string is not instance? of Regexp, only Regular expressions are. # The Pattern type should instead be used as it is specified as subtype of String. # # @return [Boolean] if a match was made or not. Also sets $0..$n to matchdata in current scope. # def eval_MatchExpression o, scope left = evaluate(o.left_expr, scope) pattern = evaluate(o.right_expr, scope) # matches RHS types as instance of for all types except a parameterized Regexp[R] if pattern.is_a?(Puppet::Pops::Types::PAnyType) # evaluate as instance? of type check matched = @@type_calculator.instance?(pattern, left) # convert match result to Boolean true, or false return o.operator == :'=~' ? !!matched : !matched end begin pattern = Regexp.new(pattern) unless pattern.is_a?(Regexp) rescue StandardError => e fail(Issues::MATCH_NOT_REGEXP, o.right_expr, {:detail => e.message}, e) end unless left.is_a?(String) fail(Issues::MATCH_NOT_STRING, o.left_expr, {:left_value => left}) end matched = pattern.match(left) # nil, or MatchData set_match_data(matched,scope) # creates ephemeral # convert match result to Boolean true, or false o.operator == :'=~' ? !!matched : !matched end # Evaluates Puppet DSL `in` expression # def eval_InExpression o, scope left = evaluate(o.left_expr, scope) right = evaluate(o.right_expr, scope) @@compare_operator.include?(right, left, scope) end # @example # $a and $b # b is only evaluated if a is true # def eval_AndExpression o, scope is_true?(evaluate(o.left_expr, scope)) ? is_true?(evaluate(o.right_expr, scope)) : false end # @example # a or b # b is only evaluated if a is false # def eval_OrExpression o, scope is_true?(evaluate(o.left_expr, scope)) ? true : is_true?(evaluate(o.right_expr, scope)) end # Evaluates each entry of the literal list and creates a new Array # Supports unfolding of entries # @return [Array] with the evaluated content # def eval_LiteralList o, scope unfold([], o.values, scope) end # Evaluates each entry of the literal hash and creates a new Hash. # @return [Hash] with the evaluated content # def eval_LiteralHash o, scope # optimized o.entries.reduce({}) {|h,entry| h[evaluate(entry.key, scope)] = evaluate(entry.value, scope); h } end # Evaluates all statements and produces the last evaluated value # def eval_BlockExpression o, scope r = nil o.statements.each {|s| r = evaluate(s, scope)} r end # Performs optimized search over case option values, lazily evaluating each # until there is a match. If no match is found, the case expression's default expression # is evaluated (it may be nil or Nop if there is no default, thus producing nil). # If an option matches, the result of evaluating that option is returned. # @return [Object, nil] what a matched option returns, or nil if nothing matched. # def eval_CaseExpression(o, scope) # memo scope level before evaluating test - don't want a match in the case test to leak $n match vars # to expressions after the case expression. # with_guarded_scope(scope) do test = evaluate(o.test, scope) result = nil the_default = nil if o.options.find do |co| # the first case option that matches if co.values.find do |c| case c when Puppet::Pops::Model::LiteralDefault the_default = co.then_expr is_match?(test, evaluate(c, scope), c, scope) when Puppet::Pops::Model::UnfoldExpression # not ideal for error reporting, since it is not known which unfolded result # that caused an error - the entire unfold expression is blamed (i.e. the var c, passed to is_match?) evaluate(c, scope).any? {|v| is_match?(test, v, c, scope) } else is_match?(test, evaluate(c, scope), c, scope) end end result = evaluate(co.then_expr, scope) true # the option was picked end end result # an option was picked, and produced a result else evaluate(the_default, scope) # evaluate the default (should be a nop/nil) if there is no default). end end end # Evaluates a CollectExpression by transforming it into a 3x AST::Collection and then evaluating that. # This is done because of the complex API between compiler, indirector, backends, and difference between # collecting virtual resources and exported resources. # def eval_CollectExpression o, scope # The Collect Expression and its contained query expressions are implemented in such a way in # 3x that it is almost impossible to do anything about them (the AST objects are lazily evaluated, # and the built structure consists of both higher order functions and arrays with query expressions # that are either used as a predicate filter, or given to an indirection terminus (such as the Puppet DB # resource terminus). Unfortunately, the 3x implementation has many inconsistencies that the implementation # below carries forward. # collect_3x = Puppet::Pops::Model::AstTransformer.new().transform(o) collected = collect_3x.evaluate(scope) # the 3x returns an instance of Parser::Collector (but it is only registered with the compiler at this # point and does not contain any valuable information (like the result) # Dilemma: If this object is returned, it is a first class value in the Puppet Language and we # need to be able to perform operations on it. We can forbid it from leaking by making CollectExpression # a non R-value. This makes it possible for the evaluator logic to make use of the Collector. collected end def eval_ParenthesizedExpression(o, scope) evaluate(o.expr, scope) end # This evaluates classes, nodes and resource type definitions to nil, since 3x: # instantiates them, and evaluates their parameters and body. This is achieved by # providing bridge AST classes in Puppet::Parser::AST::PopsBridge that bridges a # Pops Program and a Pops Expression. # # Since all Definitions are handled "out of band", they are treated as a no-op when # evaluated. # def eval_Definition(o, scope) nil end def eval_Program(o, scope) evaluate(o.body, scope) end # Produces Array[PAnyType], an array of resource references # def eval_ResourceExpression(o, scope) exported = o.exported virtual = o.virtual # Get the type name type_name = if (tmp_name = o.type_name).is_a?(Puppet::Pops::Model::QualifiedName) tmp_name.value # already validated as a name else evaluated_name = evaluate(tmp_name, scope) + # must be String or Resource Type case evaluated_name when String - if evaluated_name !~ Puppet::Pops::Patterns::CLASSREF - fail(Puppet::Pops::Issues::ILLEGAL_CLASSREF, blamed, {:name=>type_name}) + resulting_name = evaluated_name.downcase + if resulting_name !~ Puppet::Pops::Patterns::CLASSREF + fail(Puppet::Pops::Issues::ILLEGAL_CLASSREF, o.type_name, {:name=>resulting_name}) end + resulting_name + when Puppet::Pops::Types::PResourceType unless evaluated_name.title().nil? fail(Puppet::Pops::Issues::ILLEGAL_RESOURCE_TYPE, o.type_name, {:actual=> evaluated_name.to_s}) end evaluated_name.type_name # assume validated + else actual = type_calculator.generalize!(type_calculator.infer(evaluated_name)).to_s fail(Puppet::Pops::Issues::ILLEGAL_RESOURCE_TYPE, o.type_name, {:actual=>actual}) end end titles_to_body = {} body_to_titles = {} body_to_params = {} # titles are evaluated before attribute operations o.bodies.map do | body | titles = evaluate(body.title, scope) # Title may not be nil # Titles may be given as an array, it is ok if it is empty, but not if it contains nil entries # Titles must be unique in the same resource expression # There may be a :default entry, its entries apply with lower precedence # if titles.nil? fail(Puppet::Pops::Issues::MISSING_TITLE, body.title) end titles = [titles].flatten # Check types of evaluated titles and duplicate entries titles.each_with_index do |title, index| if title.nil? fail(Puppet::Pops::Issues::MISSING_TITLE_AT, body.title, {:index => index}) elsif !(title.is_a?(String) || title == :default) actual = type_calculator.generalize!(type_calculator.infer(title)).to_s fail(Puppet::Pops::Issues::ILLEGAL_TITLE_TYPE_AT, body.title, {:index => index, :actual => actual}) elsif titles_to_body[title] fail(Puppet::Pops::Issues::DUPLICATE_TITLE, o, {:title => title}) end titles_to_body[title] = body end # Do not create a real instance from the :default case titles.delete(:default) body_to_titles[body] = titles # Store evaluated parameters in a hash associated with the body, but do not yet create resource # since the entry containing :defaults may appear later body_to_params[body] = body.operations.reduce({}) do |param_memo, op| params = evaluate(op, scope) params = [params] unless params.is_a?(Array) params.each { |p| param_memo[p.name] = p } param_memo end end # Titles and Operations have now been evaluated and resources can be created # Each production is a PResource, and an array of all is produced as the result of # evaluating the ResourceExpression. # defaults_hash = body_to_params[titles_to_body[:default]] || {} o.bodies.map do | body | titles = body_to_titles[body] params = defaults_hash.merge(body_to_params[body] || {}) create_resources(o, scope, virtual, exported, type_name, titles, params.values) end.flatten.compact end def eval_ResourceOverrideExpression(o, scope) evaluated_resources = evaluate(o.resources, scope) evaluated_parameters = o.operations.map { |op| evaluate(op, scope) } create_resource_overrides(o, scope, [evaluated_resources].flatten, evaluated_parameters) evaluated_resources end # Produces 3x parameter def eval_AttributeOperation(o, scope) create_resource_parameter(o, scope, o.attribute_name, evaluate(o.value_expr, scope), o.operator) end def eval_AttributesOperation(o, scope) hashed_params = evaluate(o.expr, scope) unless hashed_params.is_a?(Hash) actual = type_calculator.generalize!(type_calculator.infer(hashed_params)).to_s fail(Puppet::Pops::Issues::TYPE_MISMATCH, o.expr, {:expected => 'Hash', :actual => actual}) end hashed_params.map { |k,v| create_resource_parameter(o, scope, k, v, :'=>') } end # Sets default parameter values for a type, produces the type # def eval_ResourceDefaultsExpression(o, scope) type = evaluate(o.type_ref, scope) type_name = if type.is_a?(Puppet::Pops::Types::PResourceType) && !type.type_name.nil? && type.title.nil? type.type_name # assume it is a valid name else actual = type_calculator.generalize!(type_calculator.infer(type)) fail(Issues::ILLEGAL_RESOURCE_TYPE, o.type_ref, {:actual => actual}) end evaluated_parameters = o.operations.map {|op| evaluate(op, scope) } create_resource_defaults(o, scope, type_name, evaluated_parameters) # Produce the type type end # Evaluates function call by name. # def eval_CallNamedFunctionExpression(o, scope) # The functor expression is not evaluated, it is not possible to select the function to call # via an expression like $a() case o.functor_expr when Puppet::Pops::Model::QualifiedName # ok when Puppet::Pops::Model::RenderStringExpression # helpful to point out this easy to make Epp error fail(Issues::ILLEGAL_EPP_PARAMETERS, o) else fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o}) end name = o.functor_expr.value evaluated_arguments = unfold([], o.arguments, scope) # wrap lambda in a callable block if it is present evaluated_arguments << Puppet::Pops::Evaluator::Closure.new(self, o.lambda, scope) if o.lambda call_function(name, evaluated_arguments, o, scope) end # Evaluation of CallMethodExpression handles a NamedAccessExpression functor (receiver.function_name) # def eval_CallMethodExpression(o, scope) unless o.functor_expr.is_a? Puppet::Pops::Model::NamedAccessExpression fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function accessor', :container => o}) end receiver = evaluate(o.functor_expr.left_expr, scope) name = o.functor_expr.right_expr unless name.is_a? Puppet::Pops::Model::QualifiedName fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o}) end name = name.value # the string function name evaluated_arguments = unfold([receiver], o.arguments || [], scope) # wrap lambda in a callable block if it is present evaluated_arguments << Puppet::Pops::Evaluator::Closure.new(self, o.lambda, scope) if o.lambda call_function(name, evaluated_arguments, o, scope) end # @example # $x ? { 10 => true, 20 => false, default => 0 } # def eval_SelectorExpression o, scope # memo scope level before evaluating test - don't want a match in the case test to leak $n match vars # to expressions after the selector expression. # with_guarded_scope(scope) do test = evaluate(o.left_expr, scope) the_default = nil selected = o.selectors.find do |s| me = s.matching_expr case me when Puppet::Pops::Model::LiteralDefault the_default = s.value_expr false when Puppet::Pops::Model::UnfoldExpression # not ideal for error reporting, since it is not known which unfolded result # that caused an error - the entire unfold expression is blamed (i.e. the var c, passed to is_match?) evaluate(me, scope).any? {|v| is_match?(test, v, me, scope) } else is_match?(test, evaluate(me, scope), me, scope) end end if selected evaluate(selected.value_expr, scope) elsif the_default evaluate(the_default, scope) else fail(Issues::UNMATCHED_SELECTOR, o.left_expr, :param_value => test) end end end # SubLocatable is simply an expression that holds location information def eval_SubLocatedExpression o, scope evaluate(o.expr, scope) end # Evaluates Puppet DSL Heredoc def eval_HeredocExpression o, scope result = evaluate(o.text_expr, scope) assert_external_syntax(scope, result, o.syntax, o.text_expr) result end # Evaluates Puppet DSL `if` def eval_IfExpression o, scope with_guarded_scope(scope) do if is_true?(evaluate(o.test, scope)) evaluate(o.then_expr, scope) else evaluate(o.else_expr, scope) end end end # Evaluates Puppet DSL `unless` def eval_UnlessExpression o, scope with_guarded_scope(scope) do unless is_true?(evaluate(o.test, scope)) evaluate(o.then_expr, scope) else evaluate(o.else_expr, scope) end end end # Evaluates a variable (getting its value) # The evaluator is lenient; any expression producing a String is used as a name # of a variable. # def eval_VariableExpression o, scope # Evaluator is not too fussy about what constitutes a name as long as the result # is a String and a valid variable name # name = evaluate(o.expr, scope) # Should be caught by validation, but make this explicit here as well, or mysterious evaluation issues # may occur for some evaluation use cases. case name when String when Numeric else fail(Issues::ILLEGAL_VARIABLE_EXPRESSION, o.expr) end get_variable_value(name, o, scope) end # Evaluates double quoted strings that may contain interpolation # def eval_ConcatenatedString o, scope o.segments.collect {|expr| string(evaluate(expr, scope), scope)}.join end # If the wrapped expression is a QualifiedName, it is taken as the name of a variable in scope. # Note that this is different from the 3.x implementation, where an initial qualified name # is accepted. (e.g. `"---${var + 1}---"` is legal. This implementation requires such concrete # syntax to be expressed in a model as `(TextExpression (+ (Variable var) 1)` - i.e. moving the decision to # the parser. # # Semantics; the result of an expression is turned into a string, nil is silently transformed to empty # string. # @return [String] the interpolated result # def eval_TextExpression o, scope if o.expr.is_a?(Puppet::Pops::Model::QualifiedName) string(get_variable_value(o.expr.value, o, scope), scope) else string(evaluate(o.expr, scope), scope) end end def string_Object(o, scope) o.to_s end def string_Symbol(o, scope) if :undef == o # optimized comparison 1.44 vs 1.95 EMPTY_STRING else o.to_s end end def string_Array(o, scope) "[#{o.map {|e| string(e, scope)}.join(COMMA_SEPARATOR)}]" end def string_Hash(o, scope) "{#{o.map {|k,v| "#{string(k, scope)} => #{string(v, scope)}"}.join(COMMA_SEPARATOR)}}" end def string_Regexp(o, scope) "/#{o.source}/" end def string_PAnyType(o, scope) @@type_calculator.string(o) end # Produces concatenation / merge of x and y. # # When x is an Array, y of type produces: # # * Array => concatenation `[1,2], [3,4] => [1,2,3,4]` # * Hash => concatenation of hash as array `[key, value, key, value, ...]` # * any other => concatenation of single value # # When x is a Hash, y of type produces: # # * Array => merge of array interpreted as `[key, value, key, value,...]` # * Hash => a merge, where entries in `y` overrides # * any other => error # # When x is something else, wrap it in an array first. # # When x is nil, an empty array is used instead. # # @note to concatenate an Array, nest the array - i.e. `[1,2], [[2,3]]` # # @overload concatenate(obj_x, obj_y) # @param obj_x [Object] object to wrap in an array and concatenate to; see other overloaded methods for return type # @param ary_y [Object] array to concatenate at end of `ary_x` # @return [Object] wraps obj_x in array before using other overloaded option based on type of obj_y # @overload concatenate(ary_x, ary_y) # @param ary_x [Array] array to concatenate to # @param ary_y [Array] array to concatenate at end of `ary_x` # @return [Array] new array with `ary_x` + `ary_y` # @overload concatenate(ary_x, hsh_y) # @param ary_x [Array] array to concatenate to # @param hsh_y [Hash] converted to array form, and concatenated to array # @return [Array] new array with `ary_x` + `hsh_y` converted to array # @overload concatenate (ary_x, obj_y) # @param ary_x [Array] array to concatenate to # @param obj_y [Object] non array or hash object to add to array # @return [Array] new array with `ary_x` + `obj_y` added as last entry # @overload concatenate(hsh_x, ary_y) # @param hsh_x [Hash] the hash to merge with # @param ary_y [Array] array interpreted as even numbered sequence of key, value merged with `hsh_x` # @return [Hash] new hash with `hsh_x` merged with `ary_y` interpreted as hash in array form # @overload concatenate(hsh_x, hsh_y) # @param hsh_x [Hash] the hash to merge to # @param hsh_y [Hash] hash merged with `hsh_x` # @return [Hash] new hash with `hsh_x` merged with `hsh_y` # @raise [ArgumentError] when `xxx_x` is neither an Array nor a Hash # @raise [ArgumentError] when `xxx_x` is a Hash, and `xxx_y` is neither Array nor Hash. # def concatenate(x, y) x = [x] unless x.is_a?(Array) || x.is_a?(Hash) case x when Array y = case y when Array then y when Hash then y.to_a else [y] end x + y # new array with concatenation when Hash y = case y when Hash then y when Array # Hash[[a, 1, b, 2]] => {} # Hash[a,1,b,2] => {a => 1, b => 2} # Hash[[a,1], [b,2]] => {[a,1] => [b,2]} # Hash[[[a,1], [b,2]]] => {a => 1, b => 2} # Use type calcultor to determine if array is Array[Array[?]], and if so use second form # of call t = @@type_calculator.infer(y) if t.element_type.is_a? Puppet::Pops::Types::PArrayType Hash[y] else Hash[*y] end else raise ArgumentError.new("Can only append Array or Hash to a Hash") end x.merge y # new hash with overwrite else raise ArgumentError.new("Can only append to an Array or a Hash.") end end # Produces the result x \ y (set difference) # When `x` is an Array, `y` is transformed to an array and then all matching elements removed from x. # When `x` is a Hash, all contained keys are removed from x as listed in `y` if it is an Array, or all its keys if it is a Hash. # The difference is returned. The given `x` and `y` are not modified by this operation. # @raise [ArgumentError] when `x` is neither an Array nor a Hash # def delete(x, y) result = x.dup case x when Array y = case y when Array then y when Hash then y.to_a else [y] end y.each {|e| result.delete(e) } when Hash y = case y when Array then y when Hash then y.keys else [y] end y.each {|e| result.delete(e) } else raise ArgumentError.new("Can only delete from an Array or Hash.") end result end # Implementation of case option matching. # # This is the type of matching performed in a case option, using == for every type # of value except regular expression where a match is performed. # def is_match? left, right, o, scope if right.is_a?(Regexp) return false unless left.is_a? String matched = right.match(left) set_match_data(matched, scope) # creates or clears ephemeral !!matched # convert to boolean elsif right.is_a?(Puppet::Pops::Types::PAnyType) # right is a type and left is not - check if left is an instance of the given type # (The reverse is not terribly meaningful - computing which of the case options that first produces # an instance of a given type). # @@type_calculator.instance?(right, left) else # Handle equality the same way as the language '==' operator (case insensitive etc.) @@compare_operator.equals(left,right) end end def with_guarded_scope(scope) scope_memo = get_scope_nesting_level(scope) begin yield ensure set_scope_nesting_level(scope, scope_memo) end end # Maps the expression in the given array to their product except for UnfoldExpressions which are first unfolded. # The result is added to the given result Array. # @param result [Array] Where to add the result (may contain information to add to) # @param array [Array[Puppet::Pops::Model::Expression] the expressions to map # @param scope [Puppet::Parser::Scope] the scope to evaluate in # @return [Array] the given result array with content added from the operation # def unfold(result, array, scope) array.each do |x| if x.is_a?(Puppet::Pops::Model::UnfoldExpression) result.concat(evaluate(x, scope)) else result << evaluate(x, scope) end end result end private :unfold end diff --git a/lib/puppet/pops/evaluator/relationship_operator.rb b/lib/puppet/pops/evaluator/relationship_operator.rb index 223c874b0..866afa8ef 100644 --- a/lib/puppet/pops/evaluator/relationship_operator.rb +++ b/lib/puppet/pops/evaluator/relationship_operator.rb @@ -1,158 +1,157 @@ # The RelationshipOperator implements the semantics of the -> <- ~> <~ operators creating relationships or notification # relationships between the left and right hand side's references to resources. # # This is separate class since a second level of evaluation is required that transforms string in left or right hand # to type references. The task of "making a relationship" is delegated to the "runtime support" class that is included. # This is done to separate the concerns of the new evaluator from the 3x runtime; messy logic goes into the runtime support # module. Later when more is cleaned up this can be simplified further. # class Puppet::Pops::Evaluator::RelationshipOperator # Provides access to the Puppet 3.x runtime (scope, etc.) # This separation has been made to make it easier to later migrate the evaluator to an improved runtime. # include Puppet::Pops::Evaluator::Runtime3Support Issues = Puppet::Pops::Issues class IllegalRelationshipOperandError < RuntimeError attr_reader :operand def initialize operand @operand = operand end end class NotCatalogTypeError < RuntimeError attr_reader :type def initialize type @type = type end end def initialize @type_transformer_visitor = Puppet::Pops::Visitor.new(self, "transform", 1, 1) @type_calculator = Puppet::Pops::Types::TypeCalculator.new() @type_parser = Puppet::Pops::Types::TypeParser.new() tf = Puppet::Pops::Types::TypeFactory @catalog_type = tf.variant(tf.catalog_entry, tf.type_type(tf.catalog_entry)) end def transform(o, scope) @type_transformer_visitor.visit_this_1(self, o, scope) end # Catch all non transformable objects # @api private def transform_Object(o, scope) raise IllegalRelationshipOperandError.new(o) end # A string must be a type reference in string format # @api private def transform_String(o, scope) assert_catalog_type(@type_parser.parse(o), scope) end # A qualified name is short hand for a class with this name # @api private def transform_QualifiedName(o, scope) Puppet::Pops::Types::TypeFactory.host_class(o.value) end # Types are what they are, just check the type # @api private def transform_PAnyType(o, scope) assert_catalog_type(o, scope) end # This transforms a 3x Collector (the result of evaluating a 3x AST::Collection). # It is passed through verbatim since it is evaluated late by the compiler. At the point # where the relationship is evaluated, it is simply recorded with the compiler for later evaluation. # If one of the sides of the relationship is a Collector it is evaluated before the actual # relationship is formed. (All of this happens at a later point in time. # def transform_Collector(o, scope) o end # Array content needs to be transformed def transform_Array(o, scope) o.map{|x| transform(x, scope) } end # Asserts (and returns) the type if it is a PCatalogEntryType # (A PCatalogEntryType is the base class of PHostClassType, and PResourceType). # def assert_catalog_type(o, scope) unless @type_calculator.assignable?(@catalog_type, o) - require 'debugger'; debugger raise NotCatalogTypeError.new(o) end # TODO must check if this is an abstract PResourceType (i.e. without a type_name) - which should fail ? # e.g. File -> File (and other similar constructs) - maybe the catalog protects against this since references # may be to future objects... o end RELATIONSHIP_OPERATORS = [:'->', :'~>', :'<-', :'<~'] REVERSE_OPERATORS = [:'<-', :'<~'] RELATION_TYPE = { :'->' => :relationship, :'<-' => :relationship, :'~>' => :subscription, :'<~' => :subscription } # Evaluate a relationship. # TODO: The error reporting is not fine grained since evaluation has already taken place # There is no references to the original source expressions at this point, only the overall # relationship expression. (e.g.. the expression may be ['string', func_call(), etc.] -> func_call()) # To implement this, the general evaluator needs to be able to track each evaluation result and associate # it with a corresponding expression. This structure should then be passed to the relationship operator. # def evaluate (left_right_evaluated, relationship_expression, scope) # assert operator (should have been validated, but this logic makes assumptions which would # screw things up royally). Better safe than sorry. unless RELATIONSHIP_OPERATORS.include?(relationship_expression.operator) fail(Issues::UNSUPPORTED_OPERATOR, relationship_expression, {:operator => relationship_expression.operator}) end begin # Turn each side into an array of types (this also asserts their type) # (note wrap in array first if value is not already an array) # # TODO: Later when objects are Puppet Runtime Objects and know their type, it will be more efficient to check/infer # the type first since a chained operation then does not have to visit each element again. This is not meaningful now # since inference needs to visit each object each time, and this is what the transformation does anyway). # # real is [left, right], and both the left and right may be a single value or an array. In each case all content # should be flattened, and then transformed to a type. left or right may also be a value that is transformed # into an array, and thus the resulting left and right must be flattened individually # Once flattened, the operands should be sets (to remove duplicate entries) # real = left_right_evaluated.collect {|x| [x].flatten.collect {|x| transform(x, scope) }} real[0].flatten! real[1].flatten! real[0].uniq! real[1].uniq! # reverse order if operator is Right to Left source, target = reverse_operator?(relationship_expression) ? real.reverse : real # Add the relationships to the catalog source.each {|s| target.each {|t| add_relationship(s, t, RELATION_TYPE[relationship_expression.operator], scope) }} # Produce the transformed source RHS (if this is a chain, this does not need to be done again) real.slice(1) rescue NotCatalogTypeError => e fail(Issues::ILLEGAL_RELATIONSHIP_OPERAND_TYPE, relationship_expression, {:type => @type_calculator.string(e.type)}) rescue IllegalRelationshipOperandError => e fail(Issues::ILLEGAL_RELATIONSHIP_OPERAND_TYPE, relationship_expression, {:operand => e.operand}) end end def reverse_operator?(o) REVERSE_OPERATORS.include?(o.operator) end end