# frozen_string_literal: true # Factory is a helper class that makes construction of a Pops Model # much more convenient. It can be viewed as a small internal DSL for model # constructions. # For usage see tests using the factory. # # @todo All those uppercase methods ... they look bad in one way, but stand out nicely in the grammar... # decide if they should change into lower case names (some of the are lower case)... # module Puppet::Pops module Model class Factory # Shared build_visitor, since there are many instances of Factory being used KEY_LENGTH = 'length'.freeze KEY_OFFSET = 'offset'.freeze KEY_LOCATOR = 'locator'.freeze KEY_OPERATOR = 'operator'.freeze KEY_VALUE = 'value'.freeze KEY_KEYS = 'keys'.freeze KEY_NAME = 'name'.freeze KEY_BODY = 'body'.freeze KEY_EXPR = 'expr'.freeze KEY_LEFT_EXPR = 'left_expr'.freeze KEY_RIGHT_EXPR = 'right_expr'.freeze KEY_PARAMETERS = 'parameters'.freeze BUILD_VISITOR = Visitor.new(self, 'build') INFER_VISITOR = Visitor.new(self, 'infer') INTERPOLATION_VISITOR = Visitor.new(self, 'interpolate') MAPOFFSET_VISITOR = Visitor.new(self, 'map_offset') def self.infer(o) if o.instance_of?(Factory) o else new(o) end end attr_reader :model_class, :unfolded def [](key) @init_hash[key] end def []=(key, value) @init_hash[key] = value end def all_factories(&block) block.call(self) @init_hash.each_value { |value| value.all_factories(&block) if value.instance_of?(Factory) } end def model if @current.nil? # Assign a default Locator if it's missing. Should only happen when the factory is used by other # means than from a parser (e.g. unit tests) unless @init_hash.include?(KEY_LOCATOR) @init_hash[KEY_LOCATOR] = Parser::Locator.locator('', 'no file') unless @model_class <= Program @init_hash[KEY_OFFSET] = 0 @init_hash[KEY_LENGTH] = 0 end end @current = create_model end @current end # Backward API compatibility alias current model def create_model @init_hash.each_pair { |key, elem| @init_hash[key] = factory_to_model(elem) } model_class.from_asserted_hash(@init_hash) end # Initialize a factory with a single object, or a class with arguments applied to build of # created instance # def initialize(o, *args) @init_hash = {} if o.instance_of?(Class) @model_class = o BUILD_VISITOR.visit_this_class(self, o, args) else INFER_VISITOR.visit_this(self, o, EMPTY_ARRAY) end end def map_offset(model, locator) MAPOFFSET_VISITOR.visit_this_1(self, model, locator) end def map_offset_Object(o, locator) o end def map_offset_Factory(o, locator) map_offset(o.model, locator) end def map_offset_Positioned(o, locator) # Transpose the local offset, length to global "coordinates" global_offset, global_length = locator.to_global(o.offset, o.length) # mutate o.instance_variable_set(:'@offset', global_offset) o.instance_variable_set(:'@length', global_length) # Change locator since the positions were transposed to the global coordinates o.instance_variable_set(:'@locator', locator.locator) if locator.is_a? Puppet::Pops::Parser::Locator::SubLocator end # Polymorphic interpolate def interpolate() INTERPOLATION_VISITOR.visit_this_class(self, @model_class, EMPTY_ARRAY) end # Building of Model classes def build_Application(o, n, ps, body) @init_hash[KEY_NAME] = n @init_hash[KEY_PARAMETERS] = ps b = f_build_body(body) @init_hash[KEY_BODY] = b unless b.nil? end def build_ArithmeticExpression(o, op, a, b) @init_hash[KEY_OPERATOR] = op build_BinaryExpression(o, a, b) end def build_AssignmentExpression(o, op, a, b) @init_hash[KEY_OPERATOR] = op build_BinaryExpression(o, a, b) end def build_AttributeOperation(o, name, op, value) @init_hash[KEY_OPERATOR] = op @init_hash['attribute_name'] = name.to_s # BOOLEAN is allowed in the grammar @init_hash['value_expr'] = value end def build_AttributesOperation(o, value) @init_hash[KEY_EXPR] = value end def build_AccessExpression(o, left, keys) @init_hash[KEY_LEFT_EXPR] = left @init_hash[KEY_KEYS] = keys end def build_BinaryExpression(o, left, right) @init_hash[KEY_LEFT_EXPR] = left @init_hash[KEY_RIGHT_EXPR] = right end def build_BlockExpression(o, args) @init_hash['statements'] = args end def build_EppExpression(o, parameters_specified, body) @init_hash['parameters_specified'] = parameters_specified b = f_build_body(body) @init_hash[KEY_BODY] = b unless b.nil? end # @param rval_required [Boolean] if the call must produce a value def build_CallExpression(o, functor, rval_required, args) @init_hash['functor_expr'] = functor @init_hash['rval_required'] = rval_required @init_hash['arguments'] = args end def build_CallMethodExpression(o, functor, rval_required, lambda, args) build_CallExpression(o, functor, rval_required, args) @init_hash['lambda'] = lambda end def build_CaseExpression(o, test, args) @init_hash['test'] = test @init_hash['options'] = args end def build_CaseOption(o, value_list, then_expr) value_list = [value_list] unless value_list.is_a?(Array) @init_hash['values'] = value_list b = f_build_body(then_expr) @init_hash['then_expr'] = b unless b.nil? end def build_CollectExpression(o, type_expr, query_expr, attribute_operations) @init_hash['type_expr'] = type_expr @init_hash['query'] = query_expr @init_hash['operations'] = attribute_operations end def build_ComparisonExpression(o, op, a, b) @init_hash[KEY_OPERATOR] = op build_BinaryExpression(o, a, b) end def build_ConcatenatedString(o, args) # Strip empty segments @init_hash['segments'] = args.reject { |arg| arg.model_class == LiteralString && arg['value'].empty? } end def build_HeredocExpression(o, name, expr) @init_hash['syntax'] = name @init_hash['text_expr'] = expr end # @param name [String] a valid classname # @param parameters [Array] may be empty # @param parent_class_name [String, nil] a valid classname referencing a parent class, optional. # @param body [Array, Expression, nil] expression that constitute the body # @return [HostClassDefinition] configured from the parameters # def build_HostClassDefinition(o, name, parameters, parent_class_name, body) build_NamedDefinition(o, name, parameters, body) @init_hash['parent_class'] = parent_class_name unless parent_class_name.nil? end def build_ResourceOverrideExpression(o, resources, attribute_operations) @init_hash['resources'] = resources @init_hash['operations'] = attribute_operations end def build_ReservedWord(o, name, future) @init_hash['word'] = name @init_hash['future'] = future end def build_KeyedEntry(o, k, v) @init_hash['key'] = k @init_hash[KEY_VALUE] = v end def build_LiteralHash(o, keyed_entries, unfolded) @init_hash['entries'] = keyed_entries @unfolded = unfolded end def build_LiteralList(o, values) @init_hash['values'] = values end def build_LiteralFloat(o, val) @init_hash[KEY_VALUE] = val end def build_LiteralInteger(o, val, radix) @init_hash[KEY_VALUE] = val @init_hash['radix'] = radix end def build_LiteralString(o, value) @init_hash[KEY_VALUE] = val end def build_IfExpression(o, t, ift, els) @init_hash['test'] = t @init_hash['then_expr'] = ift @init_hash['else_expr'] = els end def build_ApplyExpression(o, args, body) @init_hash['arguments'] = args @init_hash['body'] = body end def build_MatchExpression(o, op, a, b) @init_hash[KEY_OPERATOR] = op build_BinaryExpression(o, a, b) end # Building model equivalences of Ruby objects # Allows passing regular ruby objects to the factory to produce instructions # that when evaluated produce the same thing. def infer_String(o) @model_class = LiteralString @init_hash[KEY_VALUE] = o end def infer_NilClass(o) @model_class = Nop end def infer_TrueClass(o) @model_class = LiteralBoolean @init_hash[KEY_VALUE] = o end def infer_FalseClass(o) @model_class = LiteralBoolean @init_hash[KEY_VALUE] = o end def infer_Integer(o) @model_class = LiteralInteger @init_hash[KEY_VALUE] = o end def infer_Float(o) @model_class = LiteralFloat @init_hash[KEY_VALUE] = o end def infer_Regexp(o) @model_class = LiteralRegularExpression @init_hash['pattern'] = o.inspect @init_hash[KEY_VALUE] = o end # Creates a String literal, unless the symbol is one of the special :undef, or :default # which instead creates a LiterlUndef, or a LiteralDefault. # Supports :undef because nil creates a no-op instruction. def infer_Symbol(o) case o when :undef @model_class = LiteralUndef when :default @model_class = LiteralDefault else infer_String(o.to_s) end end # Creates a LiteralList instruction from an Array, where the entries are built. def infer_Array(o) @model_class = LiteralList @init_hash['values'] = o.map { |e| Factory.infer(e) } end # Create a LiteralHash instruction from a hash, where keys and values are built # The hash entries are added in sorted order based on key.to_s # def infer_Hash(o) @model_class = LiteralHash @init_hash['entries'] = o.sort_by { |k,_| k.to_s }.map { |k, v| Factory.new(KeyedEntry, Factory.infer(k), Factory.infer(v)) } @unfolded = false end def f_build_body(body) case body when NilClass nil when Array Factory.new(BlockExpression, body) when Factory body else Factory.infer(body) end end def build_LambdaExpression(o, parameters, body, return_type) @init_hash[KEY_PARAMETERS] = parameters b = f_build_body(body) @init_hash[KEY_BODY] = b unless b.nil? @init_hash['return_type'] = return_type unless return_type.nil? end def build_NamedDefinition(o, name, parameters, body) @init_hash[KEY_PARAMETERS] = parameters b = f_build_body(body) @init_hash[KEY_BODY] = b unless b.nil? @init_hash[KEY_NAME] = name end def build_FunctionDefinition(o, name, parameters, body, return_type) @init_hash[KEY_PARAMETERS] = parameters b = f_build_body(body) @init_hash[KEY_BODY] = b unless b.nil? @init_hash[KEY_NAME] = name @init_hash['return_type'] = return_type unless return_type.nil? end def build_PlanDefinition(o, name, parameters, body, return_type=nil) @init_hash[KEY_PARAMETERS] = parameters b = f_build_body(body) @init_hash[KEY_BODY] = b unless b.nil? @init_hash[KEY_NAME] = name @init_hash['return_type'] = return_type unless return_type.nil? end def build_CapabilityMapping(o, kind, component, capability, mappings) @init_hash['kind'] = kind @init_hash['component'] = component @init_hash['capability'] = capability @init_hash['mappings'] = mappings end def build_NodeDefinition(o, hosts, parent, body) @init_hash['host_matches'] = hosts @init_hash['parent'] = parent unless parent.nil? # no nop here b = f_build_body(body) @init_hash[KEY_BODY] = b unless b.nil? end def build_SiteDefinition(o, body) b = f_build_body(body) @init_hash[KEY_BODY] = b unless b.nil? end def build_Parameter(o, name, expr) @init_hash[KEY_NAME] = name @init_hash[KEY_VALUE] = expr end def build_QualifiedReference(o, name) @init_hash['cased_value'] = name.to_s end def build_RelationshipExpression(o, op, a, b) @init_hash[KEY_OPERATOR] = op build_BinaryExpression(o, a, b) end def build_ResourceExpression(o, type_name, bodies) @init_hash['type_name'] = type_name @init_hash['bodies'] = bodies end def build_RenderStringExpression(o, string) @init_hash[KEY_VALUE] = string; end def build_ResourceBody(o, title_expression, attribute_operations) @init_hash['title'] = title_expression @init_hash['operations'] = attribute_operations end def build_ResourceDefaultsExpression(o, type_ref, attribute_operations) @init_hash['type_ref'] = type_ref @init_hash['operations'] = attribute_operations end def build_SelectorExpression(o, left, *selectors) @init_hash[KEY_LEFT_EXPR] = left @init_hash['selectors'] = selectors end # Builds a SubLocatedExpression - this wraps the expression in a sublocation configured # from the given token # A SubLocated holds its own locator that is used for subexpressions holding positions relative # to what it describes. # def build_SubLocatedExpression(o, token, expression) @init_hash[KEY_EXPR] = expression @init_hash[KEY_OFFSET] = token.offset @init_hash[KEY_LENGTH] = token.length locator = token.locator @init_hash[KEY_LOCATOR] = locator @init_hash['leading_line_count'] = locator.leading_line_count @init_hash['leading_line_offset'] = locator.leading_line_offset @init_hash['line_offsets'] = locator.line_index # index of lines in sublocated end def build_SelectorEntry(o, matching, value) @init_hash['matching_expr'] = matching @init_hash['value_expr'] = value end def build_QueryExpression(o, expr) @init_hash[KEY_EXPR] = expr unless Factory.nop?(expr) end def build_TypeAlias(o, name, type_expr) if type_expr.model_class <= KeyedEntry # KeyedEntry is used for the form: # # type Foo = Bar { ... } # # The entry contains Bar => { ... } and must be transformed into: # # Object[{parent => Bar, ... }] # parent = type_expr['key'] hash = type_expr['value'] pn = parent['cased_value'] unless pn == 'Object' || pn == 'TypeSet' hash['entries'] << Factory.KEY_ENTRY(Factory.QNAME('parent'), parent) parent = Factory.QREF('Object') end type_expr = parent.access([hash]) elsif type_expr.model_class <= LiteralHash # LiteralHash is used for the form: # # type Foo = { ... } # # The hash must be transformed into: # # Object[{ ... }] # type_expr = Factory.QREF('Object').access([type_expr]) end @init_hash['type_expr'] = type_expr @init_hash[KEY_NAME] = name end def build_TypeMapping(o, lhs, rhs) @init_hash['type_expr'] = lhs @init_hash['mapping_expr'] = rhs end def build_TypeDefinition(o, name, parent, body) b = f_build_body(body) @init_hash[KEY_BODY] = b unless b.nil? @init_hash['parent'] = parent @init_hash[KEY_NAME] = name end def build_UnaryExpression(o, expr) @init_hash[KEY_EXPR] = expr unless Factory.nop?(expr) end def build_Program(o, body, definitions, locator) @init_hash[KEY_BODY] = body # non containment @init_hash['definitions'] = definitions @init_hash[KEY_LOCATOR] = locator end def build_QualifiedName(o, name) @init_hash[KEY_VALUE] = name end def build_TokenValue(o) raise "Factory can not deal with a Lexer Token. Got token: #{o}. Probably caused by wrong index in grammar val[n]." end # Factory helpers def f_build_unary(klazz, expr) Factory.new(klazz, expr) end def f_build_binary_op(klazz, op, left, right) Factory.new(klazz, op, left, right) end def f_build_binary(klazz, left, right) Factory.new(klazz, left, right) end def f_arithmetic(op, r) f_build_binary_op(ArithmeticExpression, op, self, r) end def f_comparison(op, r) f_build_binary_op(ComparisonExpression, op, self, r) end def f_match(op, r) f_build_binary_op(MatchExpression, op, self, r) end # Operator helpers def in(r) f_build_binary(InExpression, self, r); end def or(r) f_build_binary(OrExpression, self, r); end def and(r) f_build_binary(AndExpression, self, r); end def not(); f_build_unary(NotExpression, self); end def minus(); f_build_unary(UnaryMinusExpression, self); end def unfold(); f_build_unary(UnfoldExpression, self); end def text(); f_build_unary(TextExpression, self); end def var(); f_build_unary(VariableExpression, self); end def access(r); f_build_binary(AccessExpression, self, r); end def dot r; f_build_binary(NamedAccessExpression, self, r); end def + r; f_arithmetic('+', r); end def - r; f_arithmetic('-', r); end def / r; f_arithmetic('/', r); end def * r; f_arithmetic('*', r); end def % r; f_arithmetic('%', r); end def << r; f_arithmetic('<<', r); end def >> r; f_arithmetic('>>', r); end def < r; f_comparison('<', r); end def <= r; f_comparison('<=', r); end def > r; f_comparison('>', r); end def >= r; f_comparison('>=', r); end def eq r; f_comparison('==', r); end def ne r; f_comparison('!=', r); end def =~ r; f_match('=~', r); end def mne r; f_match('!~', r); end def paren; f_build_unary(ParenthesizedExpression, self); end def relop(op, r) f_build_binary_op(RelationshipExpression, op, self, r) end def select(*args) Factory.new(SelectorExpression, self, *args) end # Same as access, but with varargs and arguments that must be inferred. For testing purposes def access_at(*r) f_build_binary(AccessExpression, self, r.map { |arg| Factory.infer(arg) }) end # For CaseExpression, setting the default for an already build CaseExpression def default(r) @init_hash['options'] << Factory.WHEN(Factory.infer(:default), r) self end def lambda=(lambda) @init_hash['lambda'] = lambda self end # Assignment = def set(r) f_build_binary_op(AssignmentExpression, '=', self, r) end # Assignment += def plus_set(r) f_build_binary_op(AssignmentExpression, '+=', self, r) end # Assignment -= def minus_set(r) f_build_binary_op(AssignmentExpression, '-=', self, r) end def attributes(*args) @init_hash['attributes'] = args self end def offset @init_hash[KEY_OFFSET] end def length @init_hash[KEY_LENGTH] end # Records the position (start -> end) and computes the resulting length. # def record_position(locator, start_locatable, end_locatable) # record information directly in the Positioned object start_offset = start_locatable.offset @init_hash[KEY_LOCATOR] = locator @init_hash[KEY_OFFSET] = start_offset @init_hash[KEY_LENGTH] = end_locatable.nil? ? start_locatable.length : end_locatable.offset + end_locatable.length - start_offset self end # Sets the form of the resource expression (:regular (the default), :virtual, or :exported). # Produces true if the expression was a resource expression, false otherwise. # def self.set_resource_form(expr, form) # Note: Validation handles illegal combinations return false unless expr.instance_of?(self) && expr.model_class <= AbstractResource expr['form'] = form return true end # Returns symbolic information about an expected shape of a resource expression given the LHS of a resource expr. # # * `name { }` => `:resource`, create a resource of the given type # * `Name { }` => ':defaults`, set defaults for the referenced type # * `Name[] { }` => `:override`, overrides instances referenced by LHS # * _any other_ => ':error', all other are considered illegal # def self.resource_shape(expr) if expr == 'class' :class elsif expr.instance_of?(self) mc = expr.model_class if mc <= QualifiedName :resource elsif mc <= QualifiedReference :defaults elsif mc <= AccessExpression # if Resource[e], then it is not resource specific lhs = expr[KEY_LEFT_EXPR] if lhs.model_class <= QualifiedReference && lhs[KEY_VALUE] == 'resource' && expr[KEY_KEYS].size == 1 :defaults else :override end else :error end else :error end end # Factory starting points def self.literal(o); infer(o); end def self.minus(o); infer(o).minus; end def self.unfold(o); infer(o).unfold; end def self.var(o); infer(o).var; end def self.block(*args); new(BlockExpression, args.map { |arg| infer(arg) }); end def self.string(*args); new(ConcatenatedString, args.map { |arg| infer(arg) }); end def self.text(o); infer(o).text; end def self.IF(test_e,then_e,else_e); new(IfExpression, test_e, then_e, else_e); end def self.UNLESS(test_e,then_e,else_e); new(UnlessExpression, test_e, then_e, else_e); end def self.CASE(test_e,*options); new(CaseExpression, test_e, options); end def self.WHEN(values_list, block); new(CaseOption, values_list, block); end def self.MAP(match, value); new(SelectorEntry, match, value); end def self.KEY_ENTRY(key, val); new(KeyedEntry, key, val); end def self.HASH(entries); new(LiteralHash, entries, false); end def self.HASH_UNFOLDED(entries); new(LiteralHash, entries, true); end def self.HEREDOC(name, expr); new(HeredocExpression, name, expr); end def self.STRING(*args); new(ConcatenatedString, args); end def self.LIST(entries); new(LiteralList, entries); end def self.PARAM(name, expr=nil); new(Parameter, name, expr); end def self.NODE(hosts, parent, body); new(NodeDefinition, hosts, parent, body); end def self.SITE(body); new(SiteDefinition, body); end # Parameters # Mark parameter as capturing the rest of arguments def captures_rest @init_hash['captures_rest'] = true end # Set Expression that should evaluate to the parameter's type def type_expr(o) @init_hash['type_expr'] = o end # Creates a QualifiedName representation of o, unless o already represents a QualifiedName in which # case it is returned. # def self.fqn(o) o.instance_of?(Factory) && o.model_class <= QualifiedName ? self : new(QualifiedName, o) end # Creates a QualifiedName representation of o, unless o already represents a QualifiedName in which # case it is returned. # def self.fqr(o) o.instance_of?(Factory) && o.model_class <= QualifiedReference ? self : new(QualifiedReference, o) end def self.SUBLOCATE(token, expr_factory) # expr is a Factory wrapped LiteralString, or ConcatenatedString # The token is SUBLOCATED token which has a SubLocator as the token's locator # Use the SubLocator to recalculate the offsets and lengths. model = expr_factory.model locator = token.locator expr_factory.map_offset(model, locator) model._pcore_all_contents([]) { |element| expr_factory.map_offset(element, locator) } # Returned the factory wrapping the now offset/length transformed expression(s) expr_factory end def self.TEXT(expr) new(TextExpression, infer(expr).interpolate) end # TODO_EPP def self.RENDER_STRING(o) new(RenderStringExpression, o) end def self.RENDER_EXPR(expr) new(RenderExpression, expr) end def self.EPP(parameters, body) if parameters.nil? params = [] parameters_specified = false else params = parameters parameters_specified = true end LAMBDA(params, new(EppExpression, parameters_specified, body), nil) end def self.RESERVED(name, future=false) new(ReservedWord, name, future) end # TODO: This is the same a fqn factory method, don't know if callers to fqn and QNAME can live with the # same result or not yet - refactor into one method when decided. # def self.QNAME(name) new(QualifiedName, name) end def self.NUMBER(name_or_numeric) n_radix = Utils.to_n_with_radix(name_or_numeric) if n_radix val, radix = n_radix if val.is_a?(Float) new(LiteralFloat, val) else new(LiteralInteger, val, radix) end else # Bad number should already have been caught by lexer - this should never happen #TRANSLATORS 'NUMBER' refers to a method name and the 'name_or_numeric' was the passed in value and should not be translated raise ArgumentError, _("Internal Error, NUMBER token does not contain a valid number, %{name_or_numeric}") % { name_or_numeric: name_or_numeric } end end # Convert input string to either a qualified name, a LiteralInteger with radix, or a LiteralFloat # def self.QNAME_OR_NUMBER(name) n_radix = Utils.to_n_with_radix(name) if n_radix val, radix = n_radix if val.is_a?(Float) new(LiteralFloat, val) else new(LiteralInteger, val, radix) end else new(QualifiedName, name) end end def self.QREF(name) new(QualifiedReference, name) end def self.VIRTUAL_QUERY(query_expr) new(VirtualQuery, query_expr) end def self.EXPORTED_QUERY(query_expr) new(ExportedQuery, query_expr) end def self.ARGUMENTS(args, arg) if !args.empty? && arg.model_class <= LiteralHash && arg.unfolded last = args[args.size() - 1] if last.model_class <= LiteralHash && last.unfolded last['entries'].concat(arg['entries']) return args end end args.push(arg) end def self.ATTRIBUTE_OP(name, op, expr) new(AttributeOperation, name, op, expr) end def self.ATTRIBUTES_OP(expr) new(AttributesOperation, expr) end # Same as CALL_NAMED but with inference and varargs (for testing purposes) def self.call_named(name, rval_required, *argument_list) new(CallNamedFunctionExpression, fqn(name), rval_required, argument_list.map { |arg| infer(arg) }) end def self.CALL_NAMED(name, rval_required, argument_list) new(CallNamedFunctionExpression, name, rval_required, argument_list) end def self.CALL_METHOD(functor, argument_list) new(CallMethodExpression, functor, true, nil, argument_list) end def self.COLLECT(type_expr, query_expr, attribute_operations) new(CollectExpression, type_expr, query_expr, attribute_operations) end def self.NAMED_ACCESS(type_name, bodies) new(NamedAccessExpression, type_name, bodies) end def self.RESOURCE(type_name, bodies) new(ResourceExpression, type_name, bodies) end def self.RESOURCE_DEFAULTS(type_name, attribute_operations) new(ResourceDefaultsExpression, type_name, attribute_operations) end def self.RESOURCE_OVERRIDE(resource_ref, attribute_operations) new(ResourceOverrideExpression, resource_ref, attribute_operations) end def self.RESOURCE_BODY(resource_title, attribute_operations) new(ResourceBody, resource_title, attribute_operations) end def self.PROGRAM(body, definitions, locator) new(Program, body, definitions, locator) end # Builds a BlockExpression if args size > 1, else the single expression/value in args def self.block_or_expression(args, left_brace = nil, right_brace = nil) if args.size > 1 block_expr = new(BlockExpression, args) # If given a left and right brace position, use those # otherwise use the first and last element of the block if !left_brace.nil? && !right_brace.nil? block_expr.record_position(args.first[KEY_LOCATOR], left_brace, right_brace) else block_expr.record_position(args.first[KEY_LOCATOR], args.first, args.last) end block_expr else args[0] end end def self.HOSTCLASS(name, parameters, parent, body) new(HostClassDefinition, name, parameters, parent, body) end def self.DEFINITION(name, parameters, body) new(ResourceTypeDefinition, name, parameters, body) end def self.CAPABILITY_MAPPING(kind, component, cap_name, mappings) new(CapabilityMapping, kind, component, cap_name, mappings) end def self.APPLICATION(name, parameters, body) new(Application, name, parameters, body) end def self.PLAN(name, parameters, body) new(PlanDefinition, name, parameters, body, nil) end def self.APPLY(arguments, body) new(ApplyExpression, arguments, body) end def self.APPLY_BLOCK(statements) new(ApplyBlockExpression, statements) end def self.FUNCTION(name, parameters, body, return_type) new(FunctionDefinition, name, parameters, body, return_type) end def self.LAMBDA(parameters, body, return_type) new(LambdaExpression, parameters, body, return_type) end def self.TYPE_ASSIGNMENT(lhs, rhs) if lhs.model_class <= AccessExpression new(TypeMapping, lhs, rhs) else new(TypeAlias, lhs['cased_value'], rhs) end end def self.TYPE_DEFINITION(name, parent, body) new(TypeDefinition, name, parent, body) end def self.nop? o o.nil? || o.instance_of?(Factory) && o.model_class <= Nop end STATEMENT_CALLS = { 'require' => true, 'realize' => true, 'include' => true, 'contain' => true, 'tag' => true, 'debug' => true, 'info' => true, 'notice' => true, 'warning' => true, 'err' => true, 'fail' => true, 'import' => true, # discontinued, but transform it to make it call error reporting function 'break' => true, 'next' => true, 'return' => true }.freeze # Returns true if the given name is a "statement keyword" (require, include, contain, # error, notice, info, debug # def self.name_is_statement?(name) STATEMENT_CALLS.include?(name) end class ArgsToNonCallError < RuntimeError attr_reader :args, :name_expr def initialize(args, name_expr) @args = args @name_expr = name_expr end end # Transforms an array of expressions containing literal name expressions to calls if followed by an # expression, or expression list. # def self.transform_calls(expressions) expressions.reduce([]) do |memo, expr| name = memo[-1] if name.instance_of?(Factory) && name.model_class <= QualifiedName && name_is_statement?(name[KEY_VALUE]) if expr.is_a?(Array) expr = expr.reject { |e| e.is_a?(Parser::LexerSupport::TokenValue) } else expr = [expr] end the_call = self.CALL_NAMED(name, false, expr) # last positioned is last arg if there are several the_call.record_position(name[KEY_LOCATOR], name, expr[-1]) memo[-1] = the_call if expr.is_a?(CallNamedFunctionExpression) # Patch statement function call to expression style # This is needed because it is first parsed as a "statement" and the requirement changes as it becomes # an argument to the name to call transform above. expr.rval_required = true end elsif expr.is_a?(Array) raise ArgsToNonCallError.new(expr, name) else memo << expr if expr.model_class <= CallNamedFunctionExpression # Patch rvalue expression function call to statement style. # This is not really required but done to be AST model compliant expr['rval_required'] = false end end memo end end # Transforms a left expression followed by an untitled resource (in the form of attribute_operations) # @param left [Factory, Expression] the lhs followed what may be a hash def self.transform_resource_wo_title(left, attribute_ops, lbrace_token, rbrace_token) # Returning nil means accepting the given as a potential resource expression return nil unless attribute_ops.is_a? Array return nil unless left.model_class <= QualifiedName keyed_entries = attribute_ops.map do |ao| return nil if ao[KEY_OPERATOR] == '+>' KEY_ENTRY(infer(ao['attribute_name']), ao['value_expr']) end a_hash = HASH(keyed_entries) a_hash.record_position(left[KEY_LOCATOR], lbrace_token, rbrace_token) result = block_or_expression(transform_calls([left, a_hash])) result end def interpolate_Factory(c) self end def interpolate_LiteralInteger(c) # convert number to a variable self.var end def interpolate_Object(c) self end def interpolate_QualifiedName(c) self.var end # rewrite left expression to variable if it is name, number, and recurse if it is an access expression # this is for interpolation support in new lexer (${NAME}, ${NAME[}}, ${NUMBER}, ${NUMBER[]} - all # other expressions requires variables to be preceded with $ # def interpolate_AccessExpression(c) lhs = @init_hash[KEY_LEFT_EXPR] if is_interop_rewriteable?(lhs) @init_hash[KEY_LEFT_EXPR] = lhs.interpolate end self end def interpolate_NamedAccessExpression(c) lhs = @init_hash[KEY_LEFT_EXPR] if is_interop_rewriteable?(lhs) @init_hash[KEY_LEFT_EXPR] = lhs.interpolate end self end # Rewrite method calls on the form ${x.each ...} to ${$x.each} def interpolate_CallMethodExpression(c) functor_expr = @init_hash['functor_expr'] if is_interop_rewriteable?(functor_expr) @init_hash['functor_expr'] = functor_expr.interpolate end self end def is_interop_rewriteable?(o) mc = o.model_class if mc <= AccessExpression || mc <= QualifiedName || mc <= NamedAccessExpression || mc <= CallMethodExpression true elsif mc <= LiteralInteger # Only decimal integers can represent variables, else it is a number o['radix'] == 10 else false end end def self.concat(*args) result = ''.dup args.each do |e| if e.instance_of?(Factory) && e.model_class <= LiteralString result << e[KEY_VALUE] elsif e.is_a?(String) result << e else raise ArgumentError, _("can only concatenate strings, got %{class_name}") % { class_name: e.class } end end infer(result) end def to_s "Factory for #{@model_class}" end def factory_to_model(value) if value.instance_of?(Factory) value.contained_current(self) elsif value.instance_of?(Array) value.each_with_index { |el, idx| value[idx] = el.contained_current(self) if el.instance_of?(Factory) } else value end end def contained_current(container) if @current.nil? unless @init_hash.include?(KEY_LOCATOR) @init_hash[KEY_LOCATOR] = container[KEY_LOCATOR] @init_hash[KEY_OFFSET] = container[KEY_OFFSET] || 0 @init_hash[KEY_LENGTH] = 0 end @current = create_model end @current end end end end