File: evaluator_impl.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (1317 lines) | stat: -rw-r--r-- 46,030 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
require_relative '../../../puppet/parser/scope'
require_relative '../../../puppet/pops/evaluator/compare_operator'
require_relative '../../../puppet/pops/evaluator/relationship_operator'
require_relative '../../../puppet/pops/evaluator/access_operator'
require_relative '../../../puppet/pops/evaluator/closure'
require_relative '../../../puppet/pops/evaluator/external_syntax_support'
require_relative '../../../puppet/pops/types/iterable'

module Puppet::Pops
module Evaluator
# This implementation of {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 {Visitable} and {Visitor} for more information about
# polymorphic calling.
#
class EvaluatorImpl
  include 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 Runtime3Support
  include ExternalSyntaxSupport

  COMMA_SEPARATOR = ', '.freeze

  # Reference to Issues name space makes it easier to refer to issues
  # (Issues are shared with the validator).
  #
  Issues = Issues

  def initialize
    @@initialized ||= static_initialize

    # Use null migration checker unless given in context
    @migration_checker = Puppet.lookup(:migration_checker) { Migration::MigrationChecker.singleton }
  end

  # @api private
  def static_initialize
    @@eval_visitor     ||= Visitor.new(self, "eval", 1, 1)
    @@lvalue_visitor   ||= Visitor.new(self, "lvalue", 1, 1)
    @@assign_visitor   ||= Visitor.new(self, "assign", 3, 3)
    @@string_visitor   ||= Visitor.new(self, "string", 1, 1)

    @@type_calculator  ||= Types::TypeCalculator.singleton

    @@compare_operator      ||= CompareOperator.new
    @@relationship_operator ||= RelationshipOperator.new
    true
  end
  private :static_initialize

  # @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 SemanticError => e
      # A raised issue may not know the semantic target, use errors call stack, but fill in the
      # rest from a supplied semantic object, or the target instruction if there is not semantic
      # object.
      #
      fail(e.issue, e.semantic || target, e.options, e)

    rescue Puppet::PreformattedError => e
      # Already formatted with location information, and with the wanted call stack.
      # Note this is currently a specialized ParseError, so rescue-order is important
      #
      raise e

    rescue Puppet::ParseError => e
      # ParseError may be raised in ruby code without knowing the location
      # in puppet code.
      # Accept a ParseError that has file or line information available
      # as an error that should be used verbatim. (Tests typically run without
      # setting a file name).
      # ParseError can supply an original - it is impossible to determine which
      # call stack that should be propagated, using the ParseError's backtrace.
      #
      if e.file || e.line
        raise e
      else
        # Since it had no location information, treat it as user intended a general purpose
        # error. Pass on its call stack.
        fail(Issues::RUNTIME_ERROR, target, {:detail => e.message}, e)
      end


    rescue Puppet::Error => e
      # PuppetError has the ability to wrap an exception, if so, use the wrapped exception's
      # call stack instead
      fail(Issues::RUNTIME_ERROR, target, {:detail => e.message}, e.original || e)

    rescue StopIteration => e
      # Ensure these are not rescued as StandardError
      raise e

    rescue StandardError => e
      # All other errors, use its message and call stack
      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 [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 [Model::BlockExpression] the sequence of expressions to evaluate in the new scope
  #
  # @api private
  #
  def evaluate_block_with_bindings(scope, variable_bindings, block_expr)
    scope.with_guarded_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

  # 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 match?(left, right)
    @@compare_operator.match(left, right, nil)
  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

  # An array is assignable if all entries are lvalues
  def lvalue_LiteralList(o, scope)
    o.values.map {|x| lvalue(x, scope) }
  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 [Model::PopsObject] originating instruction
  # @param scope [Object] the runtime specific scope where evaluation should take place
  # @return [value<Object>]
  #
  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 assign_Array(lvalues, values, o, scope)
    if values.is_a?(Hash)
      lvalues.map do |lval|
        assign(lval,
          values.fetch(lval) {|k| fail(Issues::MISSING_MULTI_ASSIGNMENT_KEY, o, :key =>k)},
          o, scope)
      end
    elsif values.is_a?(Puppet::Pops::Types::PClassType)
      if Puppet[:tasks]
        fail(Issues::CATALOG_OPERATION_NOT_SUPPORTED_WHEN_SCRIPTING, o, {:operation => _('multi var assignment from class')})
      end
      # assign variables from class variables
      # lookup class resource and return one or more parameter values
      # TODO: behavior when class_name is nil
      resource = find_resource(scope, 'class', values.class_name)
      if resource
        base_name = "#{values.class_name.downcase}::"
        idx = -1
        result = lvalues.map do |lval|
          idx += 1
          varname = "#{base_name}#{lval}"
          if variable_exists?(varname, scope)
            result = get_variable_value(varname, o, scope)
            assign(lval, result, o, scope)
          else
            fail(Puppet::Pops::Issues::MISSING_MULTI_ASSIGNMENT_VARIABLE, o.left_expr.values[idx], {:name => varname})
          end
        end
      else
        fail(Issues::UNKNOWN_RESOURCE, o.right_expr, {:type_name => 'Class', :title => values.class_name})
      end

    else
      values = [values] unless values.is_a?(Array)
      if values.size != lvalues.size
        fail(Issues::ILLEGAL_MULTI_ASSIGNMENT_SIZE, o, :expected =>lvalues.size, :actual => values.size)
      end
      lvalues.zip(values).map { |lval, val| assign(lval, val, o, scope) }
    end
  end

  def eval_Factory(o, scope)
    evaluate(o.model, 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)
    if !o.future
      fail(Issues::RESERVED_WORD, o, {:word => o.word})
    else
      o.word
    end
  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 PTypeType
  #
  def eval_QualifiedReference(o, scope)
    type = Types::TypeParser.singleton.interpret(o)
    fail(Issues::UNKNOWN_RESOURCE_TYPE, o, {:type_name => type.type_string }) if type.is_a?(Types::PTypeReferenceType)
    type
  end

  def eval_NotExpression(o, scope)
    ! is_true?(evaluate(o.expr, scope), o.expr)
  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 nil
      []
    when Array
      candidate
    when Hash
      candidate.to_a
    when Puppet::Pops::Types::Iterable
      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<Object, Object>> 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)

    if o.operator == '='
      assign(name, value, o, scope)
    else
      fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator})
    end
    value
  end

  ARITHMETIC_OPERATORS = ['+', '-', '*', '/', '%', '<<', '>>'].freeze
  COLLECTION_OPERATORS = ['+', '-', '<<'].freeze

  # 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, 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, bin_expr, scope)
    operator = bin_expr.operator
    unless ARITHMETIC_OPERATORS.include?(operator)
      fail(Issues::UNSUPPORTED_OPERATOR, bin_expr, {:operator => operator})
    end

    left_o = bin_expr.left_expr
    if (left.is_a?(URI) || left.is_a?(Types::PBinaryType::Binary)) && operator == '+'
      concatenate(left, right)
    elsif (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, bin_expr.right_expr, 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}) if left.is_a?(Float)
          fail(Issues::OPERATOR_NOT_APPLICABLE_WHEN, left_o, {:operator => operator, :left_value => left, :right_value => right})
        end
        if right.is_a?(Time::TimeData) && !left.is_a?(Time::TimeData)
          if operator == '+' || operator == '*' && right.is_a?(Time::Timespan)
            # Switch places. Let the TimeData do the arithmetic
            x = left
            left = right
            right = x
          elsif operator == '-' && right.is_a?(Time::Timespan)
            left = Time::Timespan.new((left * Time::NSECS_PER_SEC).to_i)
          else
            fail(Issues::OPERATOR_NOT_APPLICABLE_WHEN, left_o, {:operator => operator, :left_value => left, :right_value => right})
          end
        end
        result = left.send(operator, right)
      rescue NoMethodError
        fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left})
      rescue ZeroDivisionError
        fail(Issues::DIV_BY_ZERO, bin_expr.right_expr)
      end
      case result
      when Float
        if result == Float::INFINITY || result == -Float::INFINITY
          fail(Issues::RESULT_IS_INFINITY, left_o, {:operator => operator})
        end
      when Integer
        if result < MIN_INTEGER || result > MAX_INTEGER
          fail(Issues::NUMERIC_OVERFLOW, bin_expr, {:value => result})
        end
      end
      result
    end
  end

  def eval_EppExpression(o, scope)
    contains_sensitive = false

    scope["@epp"] = []
    evaluate(o.body, scope)
    result = scope["@epp"].map do |r|
      if r.instance_of?(Puppet::Pops::Types::PSensitiveType::Sensitive)
        contains_sensitive = true
        string(r.unwrap, scope)
      else
        r
      end
    end.join

    if contains_sensitive
      Puppet::Pops::Types::PSensitiveType::Sensitive.new(result)
    else
      result
    end
  end

  def eval_RenderStringExpression(o, scope)
    scope["@epp"] << o.value.dup
    nil
  end

  def eval_RenderExpression(o, scope)
    result = evaluate(o.expr, scope)
    if result.instance_of?(Puppet::Pops::Types::PSensitiveType::Sensitive)
      scope["@epp"] << result
    else
      scope["@epp"] << string(result, scope)
    end
    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 || []
    if left.is_a?(Types::PClassType)
      # Evaluate qualified references without errors no undefined types
      keys = keys.map {|key| key.is_a?(Model::QualifiedReference) ? Types::TypeParser.singleton.interpret(key) : evaluate(key, scope) }
    else
      keys = keys.map {|key| evaluate(key, scope) }
      # Resource[File] becomes File
      return keys[0] if Types::PResourceType::DEFAULT == left && keys.size == 1 && keys[0].is_a?(Types::PResourceType)
    end
    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?(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?(Types::PAnyType)
      # evaluate as instance? of type check
      matched = pattern.instance?(left)
      # convert match result to Boolean true, or false
      return o.operator == '=~' ? !!matched : !matched
    end

    if pattern.is_a?(SemanticPuppet::VersionRange)
      # evaluate if range includes version
      matched = Types::PSemVerRangeType.include?(pattern, left)
      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), o.left_expr) ? is_true?(evaluate(o.right_expr, scope), o.right_expr) : 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), o.left_expr) ? true : is_true?(evaluate(o.right_expr, scope), o.right_expr)
  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
    o.statements.reduce(nil) {|memo, s| evaluate(s, scope)}
  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.
    #
    scope.with_guarded_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|
          c = unwind_parentheses(c)
          case c
          when Model::LiteralDefault
            the_default = co.then_expr
            next false
          when 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, co, scope) }
          else
            is_match?(test, evaluate(c, scope), c, co, 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 creating a collector transformer. The transformer
  # will evaluate the collection, create the appropriate collector, and hand it off
  # to the compiler to collect the resources specified by the query.
  #
  def eval_CollectExpression o, scope
    if o.query.is_a?(Model::ExportedQuery)
      optionally_fail(Issues::RT_NO_STORECONFIGS, o);
    end
    CollectorTransformer.new().transform(o,scope)
  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)
    begin
      file = o.locator.file
      line = 0
      # Add stack frame for "top scope" logic. See Puppet::Pops::PuppetStack
      return Puppet::Pops::PuppetStack.stack(file, line, self, 'evaluate', [o.body, scope])
      #evaluate(o.body, scope)
    rescue Puppet::Pops::Evaluator::PuppetStopIteration => ex
      # breaking out of a file level program is not allowed
      #TRANSLATOR break() is a method that should not be translated
      raise Puppet::ParseError.new(_("break() from context where this is illegal"), ex.file, ex.line)
    end
  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?(Model::QualifiedName)
      tmp_name.value # already validated as a name
    else
      type_name_acceptable =
      case o.type_name
      when Model::QualifiedReference
        true
      when Model::AccessExpression
        o.type_name.left_expr.is_a?(Model::QualifiedReference)
      end

      evaluated_name = evaluate(tmp_name, scope)
      unless type_name_acceptable
        actual = type_calculator.generalize(type_calculator.infer(evaluated_name)).to_s
        fail(Issues::ILLEGAL_RESOURCE_TYPE, o.type_name, {:actual => actual})
      end

      # must be a CatalogEntry subtype
      case evaluated_name
      when Types::PClassType
        unless evaluated_name.class_name.nil?
          fail(Issues::ILLEGAL_RESOURCE_TYPE, o.type_name, {:actual=> evaluated_name.to_s})
        end
        'class'

      when Types::PResourceType
        unless evaluated_name.title().nil?
          fail(Issues::ILLEGAL_RESOURCE_TYPE, o.type_name, {:actual=> evaluated_name.to_s})
        end
        evaluated_name.type_name # assume validated

      when Types::PTypeReferenceType
        fail(Issues::UNKNOWN_RESOURCE_TYPE, o.type_string, {:type_name => evaluated_name.to_s})

      else
        actual = type_calculator.generalize(type_calculator.infer(evaluated_name)).to_s
        fail(Issues::ILLEGAL_RESOURCE_TYPE, o.type_name, {:actual=>actual})
      end
    end

    # This is a runtime check - the model is valid, but will have runtime issues when evaluated
    # and storeconfigs is not set.
    if(o.exported)
      optionally_fail(Issues::RT_NO_STORECONFIGS_EXPORT, o);
    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 may not be an empty String
      # 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(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(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(Issues::ILLEGAL_TITLE_TYPE_AT, body.title, {:index => index, :actual => actual})

        elsif title == EMPTY_STRING
         fail(Issues::EMPTY_STRING_TITLE_AT, body.title, {:index => index})

        elsif titles_to_body[title]
          fail(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 do |p|
          if param_memo.include? p.name
            fail(Issues::DUPLICATE_ATTRIBUTE, o, {:attribute => p.name})
          end
          param_memo[p.name] = p
        end
        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

  def eval_ApplyExpression(o, scope)
    # All expressions are wrapped in an ApplyBlockExpression so we can identify the contents of
    # that block. However we don't want to serialize the block expression, so unwrap here.
    body = if o.body.statements.count == 1
             o.body.statements[0]
           else
             Model::BlockExpression.from_asserted_hash(o.body._pcore_init_hash)
           end

    Puppet.lookup(:apply_executor).apply(unfold([], o.arguments, scope), body, scope)
  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(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?(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)
    # If LHS is a type (i.e. Integer, or Integer[...]
    # the call is taken as an instantiation of the given type
    #
    functor = o.functor_expr
    if functor.is_a?(Model::QualifiedReference) ||
      functor.is_a?(Model::AccessExpression) && functor.left_expr.is_a?(Model::QualifiedReference)
      # instantiation
      type = evaluate(functor, scope)
      return call_function_with_block('new', unfold([type], o.arguments || [], scope), o, scope)
    end

    # The functor expression is not evaluated, it is not possible to select the function to call
    # via an expression like $a()
    case functor
    when Model::QualifiedName
      # ok
    when 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
    call_function_with_block(name, unfold([], o.arguments, scope), o, scope)
  end

  # Evaluation of CallMethodExpression handles a NamedAccessExpression functor (receiver.function_name)
  #
  def eval_CallMethodExpression(o, scope)
    unless o.functor_expr.is_a? Model::NamedAccessExpression
      fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function accessor', :container => o})
    end
    receiver = unfold([], [o.functor_expr.left_expr], scope)
    name = o.functor_expr.right_expr
    unless name.is_a? Model::QualifiedName
      fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o})
    end
    name = name.value # the string function name

    obj = receiver[0]
    receiver_type = Types::TypeCalculator.infer_callable_methods_t(obj)
    if receiver_type.is_a?(Types::TypeWithMembers)
      member = receiver_type[name]
      unless member.nil?
        args = unfold([], o.arguments || [], scope)
        return o.lambda.nil? ? member.invoke(obj, scope, args) : member.invoke(obj, scope, args, &proc_from_lambda(o.lambda, scope))
      end
    end

    call_function_with_block(name, unfold(receiver, o.arguments || [], scope), o, scope)
  end

  def call_function_with_block(name, evaluated_arguments, o, scope)
    if o.lambda.nil?
      call_function(name, evaluated_arguments, o, scope)
    else
      call_function(name, evaluated_arguments, o, scope, &proc_from_lambda(o.lambda, scope))
    end
  end
  private :call_function_with_block

  def proc_from_lambda(lambda, scope)
    closure = Closure::Dynamic.new(self, lambda, scope)
    PuppetProc.new(closure) { |*args| closure.call(*args) }
  end
  private :proc_from_lambda

  # @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.
    #
    scope.with_guarded_scope do
      test = evaluate(o.left_expr, scope)

      the_default = nil
      selected = o.selectors.find do |s|
        me = unwind_parentheses(s.matching_expr)
        case me
        when Model::LiteralDefault
          the_default = s.value_expr
          false
        when 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, s, scope) }
        else
          is_match?(test, evaluate(me, scope), me, s, 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

  # Evaluates Puppet DSL Heredoc
  def eval_HeredocExpression o, scope
    expr = o.text_expr
    result = evaluate(o.text_expr, scope)
    unless expr.is_a?(Model::LiteralString)
      # When expr is a LiteralString, validation has already validated this
      assert_external_syntax(scope, result, o.syntax, o.text_expr)
    end
    result
  end

  # Evaluates Puppet DSL `if`
  def eval_IfExpression o, scope
    scope.with_guarded_scope do
      if is_true?(evaluate(o.test, scope), o.test)
        evaluate(o.then_expr, scope)
      else
        evaluate(o.else_expr, scope)
      end
    end
  end

  # Evaluates Puppet DSL `unless`
  def eval_UnlessExpression o, scope
    scope.with_guarded_scope do
      unless is_true?(evaluate(o.test, scope), o.test)
        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?(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)
    Types::PRegexpType.regexp_to_s_with_delimiters(o)
  end

  def string_PAnyType(o, scope)
    o.to_s
  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 a URI, y of type produces:
  #
  # * String => merge of URI interpreted x + URI(y) using URI merge semantics
  # * URI => merge of URI interpreted x + y using URI merge semantics
  # * any other => error
  #
  # 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`
  # @overload concatenate(uri_x, uri_y)
  #   @param uri_x [URI] the uri to merge to
  #   @param uri_y [URI] uri to merged with `uri_x`
  #   @return [URI] new uri with `uri_x` merged with `uri_y`
  # @overload concatenate(uri_x, string_y)
  #   @param uri_x [URI] the uri to merge to
  #   @param string_y [String] string to merge with `uri_x`
  #   @return [URI] new uri with `uri_x` merged with `string_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)
    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 calculator 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? 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
    when URI
      raise ArgumentError.new(_('An URI can only be merged with an URI or String')) unless y.is_a?(String) || y.is_a?(URI)
      x + y
    when Types::PBinaryType::Binary
      raise ArgumentError.new(_('Can only append Binary to a Binary')) unless y.is_a?(Types::PBinaryType::Binary)
      Types::PBinaryType::Binary.from_binary_string(x.binary_buffer + y.binary_buffer)
    else
      concatenate([x], y)
    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, option_expr, scope)
    @@compare_operator.match(left, right, scope)
  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[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|
      x = unwind_parentheses(x)
      if x.is_a?(Model::UnfoldExpression)
        result.concat(evaluate(x, scope))
      else
        result << evaluate(x, scope)
      end
    end
    result
  end
  private :unfold

  def unwind_parentheses(o)
    return o unless o.is_a?(Model::ParenthesizedExpression)
    unwind_parentheses(o.expr)
  end
  private :unwind_parentheses
end
end
end