ruby-rego 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/.reek.yml +80 -0
  3. data/.vscode/extensions.json +19 -0
  4. data/.vscode/launch.json +35 -0
  5. data/.vscode/settings.json +25 -0
  6. data/.vscode/tasks.json +117 -0
  7. data/.yardopts +12 -0
  8. data/ARCHITECTURE.md +39 -0
  9. data/CHANGELOG.md +25 -0
  10. data/CODE_OF_CONDUCT.md +10 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +183 -0
  13. data/RELEASING.md +37 -0
  14. data/Rakefile +38 -0
  15. data/SECURITY.md +26 -0
  16. data/Steepfile +10 -0
  17. data/TODO.md +35 -0
  18. data/benchmark/builtin_calls.rb +29 -0
  19. data/benchmark/complex_policy.rb +19 -0
  20. data/benchmark/comprehensions.rb +19 -0
  21. data/benchmark/simple_rules.rb +20 -0
  22. data/examples/README.md +27 -0
  23. data/examples/sample_config.yaml +2 -0
  24. data/examples/simple_policy.rego +7 -0
  25. data/examples/validation_policy.rego +11 -0
  26. data/exe/rego-validate +6 -0
  27. data/lib/ruby/rego/ast/base.rb +95 -0
  28. data/lib/ruby/rego/ast/binary_op.rb +64 -0
  29. data/lib/ruby/rego/ast/call.rb +27 -0
  30. data/lib/ruby/rego/ast/composite.rb +48 -0
  31. data/lib/ruby/rego/ast/comprehension.rb +63 -0
  32. data/lib/ruby/rego/ast/every.rb +37 -0
  33. data/lib/ruby/rego/ast/import.rb +32 -0
  34. data/lib/ruby/rego/ast/literal.rb +70 -0
  35. data/lib/ruby/rego/ast/module.rb +32 -0
  36. data/lib/ruby/rego/ast/package.rb +22 -0
  37. data/lib/ruby/rego/ast/query.rb +63 -0
  38. data/lib/ruby/rego/ast/reference.rb +58 -0
  39. data/lib/ruby/rego/ast/rule.rb +114 -0
  40. data/lib/ruby/rego/ast/unary_op.rb +42 -0
  41. data/lib/ruby/rego/ast/variable.rb +22 -0
  42. data/lib/ruby/rego/ast.rb +17 -0
  43. data/lib/ruby/rego/builtins/aggregates.rb +124 -0
  44. data/lib/ruby/rego/builtins/base.rb +95 -0
  45. data/lib/ruby/rego/builtins/collections/array_ops.rb +103 -0
  46. data/lib/ruby/rego/builtins/collections/object_ops.rb +120 -0
  47. data/lib/ruby/rego/builtins/collections/set_ops.rb +51 -0
  48. data/lib/ruby/rego/builtins/collections.rb +137 -0
  49. data/lib/ruby/rego/builtins/comparisons/casts.rb +139 -0
  50. data/lib/ruby/rego/builtins/comparisons.rb +84 -0
  51. data/lib/ruby/rego/builtins/numeric_helpers.rb +56 -0
  52. data/lib/ruby/rego/builtins/registry.rb +199 -0
  53. data/lib/ruby/rego/builtins/registry_helpers.rb +27 -0
  54. data/lib/ruby/rego/builtins/strings/case_ops.rb +22 -0
  55. data/lib/ruby/rego/builtins/strings/concat.rb +19 -0
  56. data/lib/ruby/rego/builtins/strings/formatting.rb +35 -0
  57. data/lib/ruby/rego/builtins/strings/helpers.rb +62 -0
  58. data/lib/ruby/rego/builtins/strings/number_helpers.rb +48 -0
  59. data/lib/ruby/rego/builtins/strings/search.rb +63 -0
  60. data/lib/ruby/rego/builtins/strings/split.rb +19 -0
  61. data/lib/ruby/rego/builtins/strings/substring.rb +22 -0
  62. data/lib/ruby/rego/builtins/strings/trim.rb +42 -0
  63. data/lib/ruby/rego/builtins/strings/trim_helpers.rb +62 -0
  64. data/lib/ruby/rego/builtins/strings.rb +58 -0
  65. data/lib/ruby/rego/builtins/types.rb +89 -0
  66. data/lib/ruby/rego/call_name.rb +55 -0
  67. data/lib/ruby/rego/cli.rb +1122 -0
  68. data/lib/ruby/rego/compiled_module.rb +114 -0
  69. data/lib/ruby/rego/compiler.rb +1097 -0
  70. data/lib/ruby/rego/environment/overrides.rb +33 -0
  71. data/lib/ruby/rego/environment/reference_resolution.rb +86 -0
  72. data/lib/ruby/rego/environment.rb +230 -0
  73. data/lib/ruby/rego/environment_pool.rb +71 -0
  74. data/lib/ruby/rego/error_handling.rb +58 -0
  75. data/lib/ruby/rego/error_payload.rb +34 -0
  76. data/lib/ruby/rego/errors.rb +196 -0
  77. data/lib/ruby/rego/evaluator/assignment_support.rb +126 -0
  78. data/lib/ruby/rego/evaluator/binding_helpers.rb +60 -0
  79. data/lib/ruby/rego/evaluator/comprehension_evaluator.rb +182 -0
  80. data/lib/ruby/rego/evaluator/expression_dispatch.rb +45 -0
  81. data/lib/ruby/rego/evaluator/expression_evaluator.rb +492 -0
  82. data/lib/ruby/rego/evaluator/object_literal_evaluator.rb +52 -0
  83. data/lib/ruby/rego/evaluator/operator_evaluator.rb +163 -0
  84. data/lib/ruby/rego/evaluator/query_node_builder.rb +38 -0
  85. data/lib/ruby/rego/evaluator/reference_key_resolver.rb +50 -0
  86. data/lib/ruby/rego/evaluator/reference_resolver.rb +352 -0
  87. data/lib/ruby/rego/evaluator/rule_evaluator/bindings.rb +70 -0
  88. data/lib/ruby/rego/evaluator/rule_evaluator.rb +550 -0
  89. data/lib/ruby/rego/evaluator/rule_value_provider.rb +56 -0
  90. data/lib/ruby/rego/evaluator/variable_collector.rb +221 -0
  91. data/lib/ruby/rego/evaluator.rb +174 -0
  92. data/lib/ruby/rego/lexer/number_reader.rb +68 -0
  93. data/lib/ruby/rego/lexer/stream.rb +137 -0
  94. data/lib/ruby/rego/lexer/string_reader.rb +90 -0
  95. data/lib/ruby/rego/lexer/template_string_reader.rb +62 -0
  96. data/lib/ruby/rego/lexer.rb +206 -0
  97. data/lib/ruby/rego/location.rb +73 -0
  98. data/lib/ruby/rego/memoization.rb +67 -0
  99. data/lib/ruby/rego/parser/collections.rb +173 -0
  100. data/lib/ruby/rego/parser/expressions.rb +216 -0
  101. data/lib/ruby/rego/parser/precedence.rb +42 -0
  102. data/lib/ruby/rego/parser/query.rb +139 -0
  103. data/lib/ruby/rego/parser/references.rb +115 -0
  104. data/lib/ruby/rego/parser/rules.rb +310 -0
  105. data/lib/ruby/rego/parser.rb +210 -0
  106. data/lib/ruby/rego/policy.rb +50 -0
  107. data/lib/ruby/rego/result.rb +91 -0
  108. data/lib/ruby/rego/token.rb +206 -0
  109. data/lib/ruby/rego/unifier.rb +451 -0
  110. data/lib/ruby/rego/value.rb +379 -0
  111. data/lib/ruby/rego/version.rb +7 -0
  112. data/lib/ruby/rego/with_modifiers/with_modifier.rb +37 -0
  113. data/lib/ruby/rego/with_modifiers/with_modifier_applier.rb +48 -0
  114. data/lib/ruby/rego/with_modifiers/with_modifier_builtin_override.rb +128 -0
  115. data/lib/ruby/rego/with_modifiers/with_modifier_context.rb +120 -0
  116. data/lib/ruby/rego/with_modifiers/with_modifier_path_key_resolver.rb +42 -0
  117. data/lib/ruby/rego/with_modifiers/with_modifier_path_override.rb +99 -0
  118. data/lib/ruby/rego/with_modifiers/with_modifier_root_scope.rb +58 -0
  119. data/lib/ruby/rego.rb +72 -0
  120. data/sig/objspace.rbs +4 -0
  121. data/sig/psych.rbs +7 -0
  122. data/sig/rego_validate.rbs +382 -0
  123. data/sig/ruby/rego.rbs +2150 -0
  124. metadata +172 -0
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ # Parsing helpers for expressions.
6
+ # rubocop:disable Metrics/ClassLength
7
+ class Parser
8
+ private
9
+
10
+ def parse_expression(precedence = Precedence::LOWEST)
11
+ parse_infix_expression(parse_primary, precedence)
12
+ end
13
+
14
+ def parse_infix_expression(left, precedence)
15
+ return left unless infix_operator?(precedence)
16
+
17
+ operator_token = advance
18
+ parse_infix_expression(parse_infix(left, operator_token), precedence)
19
+ end
20
+
21
+ def infix_operator?(precedence)
22
+ Helpers.precedence_of(current_token.type) > precedence
23
+ end
24
+
25
+ def parse_primary
26
+ handler = PRIMARY_PARSERS[current_token.type]
27
+ return send(handler) if handler
28
+
29
+ parse_error("Expected expression.")
30
+ end
31
+
32
+ # :reek:FeatureEnvy
33
+ def parse_infix(left, operator_token)
34
+ operator_type = operator_token.type
35
+ operator = BINARY_OPERATOR_MAP.fetch(operator_type) do
36
+ parse_error("Unsupported operator: #{operator_type}.")
37
+ end
38
+
39
+ right = parse_expression(Helpers.precedence_of(operator_type))
40
+ AST::BinaryOp.new(operator: operator, left: left, right: right, location: operator_token.location)
41
+ end
42
+
43
+ # :reek:TooManyStatements
44
+ def parse_call_args
45
+ parse_parenthesized_expression_list(
46
+ open_message: "Expected '('.",
47
+ close_message: "Expected ')' after arguments."
48
+ )
49
+ end
50
+
51
+ def parse_string_literal
52
+ token = current_token
53
+ parse_error("Expected string literal.") unless [TokenType::STRING, TokenType::RAW_STRING].include?(token.type)
54
+ advance
55
+ value = token.value.to_s
56
+ AST::StringLiteral.new(value: value, location: token.location)
57
+ end
58
+
59
+ def parse_template_string
60
+ token = current_token
61
+ unless [TokenType::TEMPLATE_STRING, TokenType::RAW_TEMPLATE_STRING].include?(token.type)
62
+ parse_error("Expected template string literal.")
63
+ end
64
+ advance
65
+ AST::TemplateString.new(parts: parse_template_parts(token), location: token.location)
66
+ end
67
+
68
+ # :reek:FeatureEnvy
69
+ def parse_number_literal
70
+ token = consume(TokenType::NUMBER, "Expected number literal.")
71
+ value = token.value
72
+ if value.is_a?(String)
73
+ value = if value.match?(/[eE.]/)
74
+ Float(value)
75
+ else
76
+ Integer(value, 10)
77
+ end
78
+ end
79
+ AST::NumberLiteral.new(value: value, location: token.location)
80
+ end
81
+
82
+ def parse_boolean_literal
83
+ token_type = current_token.type
84
+ location = current_token.location
85
+ advance
86
+ AST::BooleanLiteral.new(value: token_type == TokenType::TRUE, location: location)
87
+ end
88
+
89
+ def parse_null_literal
90
+ token = advance
91
+ AST::NullLiteral.new(location: token.location)
92
+ end
93
+
94
+ def parse_unary_expression
95
+ token = advance
96
+ operator = UNARY_OPERATOR_MAP.fetch(token.type)
97
+ operand = parse_expression(Precedence::UNARY)
98
+ AST::UnaryOp.new(operator: operator, operand: operand, location: token.location)
99
+ end
100
+
101
+ def parse_parenthesized_expression
102
+ advance
103
+ expression = parse_parenthesized_body
104
+ consume(TokenType::RPAREN, "Expected ')' after expression.")
105
+ expression
106
+ end
107
+
108
+ def parse_parenthesized_body
109
+ consume_newlines
110
+ expression = parse_expression
111
+ consume_newlines
112
+ expression
113
+ end
114
+
115
+ # :reek:TooManyStatements
116
+ def parse_parenthesized_expression_list(open_message:, close_message:)
117
+ consume(TokenType::LPAREN, open_message)
118
+ consume_newlines
119
+ args = [] # @type var args: Array[AST::expression]
120
+ args = parse_expression_list_until(TokenType::RPAREN) unless match?(TokenType::RPAREN)
121
+ consume_newlines
122
+ consume(TokenType::RPAREN, close_message)
123
+ args
124
+ end
125
+
126
+ # :reek:NilCheck
127
+ # :reek:DuplicateMethodCall
128
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
129
+ def parse_template_parts(token)
130
+ text = token.value.to_s
131
+ location = token.location
132
+ parts = [] # @type var parts: Array[Object]
133
+ index = 0
134
+
135
+ while index < text.length
136
+ start = text.index("{", index)
137
+ if start.nil?
138
+ literal = text[index..]
139
+ append_template_literal(parts, literal, location)
140
+ break
141
+ end
142
+
143
+ if start > index
144
+ literal = text[index...start]
145
+ append_template_literal(parts, literal, location)
146
+ end
147
+
148
+ expr_start = start + 1
149
+ expr_end = find_template_expression_end(text, expr_start)
150
+ expr_source = text[expr_start...expr_end]
151
+ parts << self.class.parse_expression_from_string(expr_source)
152
+ index = expr_end + 1
153
+ end
154
+
155
+ parts = [AST::StringLiteral.new(value: "", location: location)] if parts.empty?
156
+ parts
157
+ end
158
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
159
+
160
+ def append_template_literal(parts, literal, location)
161
+ literal_value = normalize_template_literal(literal)
162
+ parts << AST::StringLiteral.new(value: literal_value, location: location)
163
+ end
164
+
165
+ # :reek:UtilityFunction
166
+ def normalize_template_literal(literal)
167
+ literal.tr(Lexer::TEMPLATE_ESCAPE, "{")
168
+ end
169
+
170
+ # :reek:TooManyStatements
171
+ # :reek:FeatureEnvy
172
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
173
+ def find_template_expression_end(text, start_index)
174
+ depth = 0
175
+ index = start_index
176
+ in_string = nil
177
+ escaped = false
178
+
179
+ while index < text.length
180
+ char = text[index]
181
+ if in_string
182
+ if escaped
183
+ escaped = false
184
+ elsif in_string == "\"" && char == "\\"
185
+ escaped = true
186
+ elsif char == in_string
187
+ in_string = nil
188
+ end
189
+ index += 1
190
+ next
191
+ end
192
+
193
+ if char && ["\"", "`"].include?(char)
194
+ in_string = char
195
+ index += 1
196
+ next
197
+ end
198
+
199
+ if char == "{"
200
+ depth += 1
201
+ elsif char == "}"
202
+ return index if depth.zero?
203
+
204
+ depth -= 1
205
+ end
206
+
207
+ index += 1
208
+ end
209
+
210
+ parse_error("Unterminated template expression.")
211
+ end
212
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
213
+ end
214
+ # rubocop:enable Metrics/ClassLength
215
+ end
216
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../token"
4
+
5
+ module Ruby
6
+ module Rego
7
+ class Parser
8
+ # Operator precedence table for binary operators.
9
+ # :reek:TooManyConstants
10
+ module Precedence
11
+ LOWEST = 0
12
+ ASSIGNMENT = 1
13
+ OR = 2
14
+ AND = 3
15
+ EQUALS = 4
16
+ COMPARE = 5
17
+ SUM = 6
18
+ PRODUCT = 7
19
+ UNARY = 8
20
+
21
+ BINARY = {
22
+ TokenType::ASSIGN => ASSIGNMENT,
23
+ TokenType::UNIFY => ASSIGNMENT,
24
+ TokenType::OR => OR,
25
+ TokenType::AND => AND,
26
+ TokenType::IN => COMPARE,
27
+ TokenType::EQ => EQUALS,
28
+ TokenType::NEQ => EQUALS,
29
+ TokenType::LT => COMPARE,
30
+ TokenType::LTE => COMPARE,
31
+ TokenType::GT => COMPARE,
32
+ TokenType::GTE => COMPARE,
33
+ TokenType::PLUS => SUM,
34
+ TokenType::MINUS => SUM,
35
+ TokenType::STAR => PRODUCT,
36
+ TokenType::SLASH => PRODUCT,
37
+ TokenType::PERCENT => PRODUCT
38
+ }.freeze
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ # Parsing helpers for queries.
6
+ class Parser
7
+ private
8
+
9
+ # :reek:TooManyStatements
10
+ # :reek:DuplicateMethodCall
11
+ # :reek:BooleanParameter
12
+ def parse_query(*end_tokens, newline_delimiter: false)
13
+ terminators = end_tokens.flatten
14
+ literals = [] # @type var literals: Array[AST::query_literal]
15
+
16
+ loop do
17
+ consume_newlines if newline_delimiter
18
+ break if terminators.include?(current_token.type)
19
+
20
+ literals << parse_literal
21
+ break if terminators.include?(current_token.type)
22
+
23
+ break unless consume_query_separators(newline_delimiter)
24
+ end
25
+
26
+ literals
27
+ end
28
+
29
+ # rubocop:disable Metrics/MethodLength
30
+ # :reek:TooManyStatements
31
+ # :reek:ControlParameter
32
+ def consume_query_separators(newline_delimiter)
33
+ consumed = false
34
+
35
+ loop do
36
+ if match?(TokenType::SEMICOLON)
37
+ advance
38
+ consumed = true
39
+ next
40
+ end
41
+
42
+ if newline_delimiter && newline_token?
43
+ advance
44
+ consumed = true
45
+ next
46
+ end
47
+
48
+ break
49
+ end
50
+
51
+ consumed
52
+ end
53
+ # rubocop:enable Metrics/MethodLength
54
+
55
+ def parse_literal
56
+ return parse_some_decl if match?(TokenType::SOME)
57
+
58
+ expression = parse_expression
59
+ AST::QueryLiteral.new(
60
+ expression: expression,
61
+ with_modifiers: parse_with_modifiers,
62
+ location: expression.location
63
+ )
64
+ end
65
+
66
+ def parse_with_modifiers
67
+ modifiers = [] # @type var modifiers: Array[AST::WithModifier]
68
+ modifiers << parse_with_modifier while match?(TokenType::WITH)
69
+ modifiers
70
+ end
71
+
72
+ def parse_some_decl
73
+ keyword = consume(TokenType::SOME, "Expected 'some' declaration.")
74
+ variables = parse_some_variables
75
+ collection = parse_some_collection
76
+
77
+ AST::SomeDecl.new(variables: variables, collection: collection, location: keyword.location)
78
+ end
79
+
80
+ # :reek:TooManyStatements
81
+ def parse_some_variables
82
+ variables = [] # @type var variables: Array[AST::Variable]
83
+ loop do
84
+ variables << parse_variable
85
+ break unless match?(TokenType::COMMA)
86
+
87
+ advance
88
+ end
89
+ variables
90
+ end
91
+
92
+ def parse_some_collection
93
+ return nil unless match?(TokenType::IN)
94
+
95
+ advance
96
+ parse_expression
97
+ end
98
+
99
+ def parse_every
100
+ keyword = consume(TokenType::EVERY, "Expected 'every' expression.")
101
+ key_var, value_var = parse_every_variables
102
+ domain = parse_every_domain
103
+ body = parse_every_body
104
+ AST::Every.new(key_var: key_var, value_var: value_var, domain: domain, body: body, location: keyword.location)
105
+ end
106
+
107
+ def parse_every_variables
108
+ value_var = parse_variable
109
+ return [nil, value_var] unless match?(TokenType::COMMA)
110
+
111
+ advance
112
+ [value_var, parse_variable]
113
+ end
114
+
115
+ def parse_every_domain
116
+ consume(TokenType::IN, "Expected 'in' after every variables.")
117
+ parse_expression
118
+ end
119
+
120
+ # :reek:TooManyStatements
121
+ def parse_every_body
122
+ consume_newlines
123
+ consume(TokenType::LBRACE, "Expected '{' to start every body.")
124
+ consume_newlines
125
+ body = parse_query(TokenType::RBRACE, newline_delimiter: true)
126
+ consume(TokenType::RBRACE, "Expected '}' after every body.")
127
+ body
128
+ end
129
+
130
+ def parse_with_modifier
131
+ keyword = consume(TokenType::WITH, "Expected 'with' modifier.")
132
+ target = parse_expression
133
+ consume(TokenType::AS, "Expected 'as' after with target.")
134
+ value = parse_expression
135
+ AST::WithModifier.new(target: target, value: value, location: keyword.location)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ruby
4
+ module Rego
5
+ # Parsing helpers for references and identifiers.
6
+ class Parser
7
+ private
8
+
9
+ def parse_reference(base)
10
+ reference_base, path = Helpers.normalize_reference_base(base)
11
+ parse_reference_path(path)
12
+ AST::Reference.new(base: reference_base, path: path, location: base.location)
13
+ end
14
+
15
+ # :reek:TooManyStatements
16
+ # :reek:DuplicateMethodCall
17
+ # rubocop:disable Metrics/MethodLength
18
+ def parse_path(identifier_context)
19
+ segments = [] # @type var segments: Array[String]
20
+
21
+ segments << parse_path_segment(identifier_context)
22
+
23
+ loop do
24
+ if match?(TokenType::DOT)
25
+ advance
26
+ segments << parse_path_segment(identifier_context)
27
+ next
28
+ end
29
+
30
+ segment = parse_bracket_path_segment
31
+ break unless segment
32
+
33
+ segments << segment
34
+ end
35
+
36
+ segments
37
+ end
38
+ # rubocop:enable Metrics/MethodLength
39
+
40
+ def parse_path_segment(identifier_context)
41
+ token_type = current_token.type
42
+ return parse_identifier(identifier_context) if identifier_context.allowed_types.include?(token_type)
43
+
44
+ parse_error("Expected #{identifier_context.name} identifier.")
45
+ end
46
+
47
+ def parse_bracket_path_segment
48
+ return nil unless match?(TokenType::LBRACKET)
49
+ return nil unless bracket_string_segment?
50
+
51
+ advance
52
+ value = parse_string_literal
53
+ consume(TokenType::RBRACKET, "Expected ']' after path segment.")
54
+ value.value
55
+ end
56
+
57
+ def bracket_string_segment?
58
+ return false unless match?(TokenType::LBRACKET)
59
+
60
+ token = peek
61
+ [TokenType::STRING, TokenType::RAW_STRING].include?(token.type)
62
+ end
63
+
64
+ # :reek:TooManyStatements
65
+ def parse_identifier(identifier_context)
66
+ context_name = identifier_context.name
67
+ allowed_types = identifier_context.allowed_types
68
+ token = current_token
69
+ token_type = token.type
70
+
71
+ parse_error("Expected #{context_name} identifier.") unless allowed_types.include?(token_type)
72
+
73
+ advance
74
+ return token.value.to_s if token_type == TokenType::IDENT
75
+
76
+ IDENTIFIER_TOKEN_NAMES.fetch(token_type) { token_type.to_s.downcase }
77
+ end
78
+
79
+ def parse_identifier_expression
80
+ base = parse_variable
81
+ base = parse_reference(base) if match?(TokenType::DOT, TokenType::LBRACKET)
82
+ return base unless match?(TokenType::LPAREN)
83
+
84
+ AST::Call.new(name: base, args: parse_call_args, location: base.location)
85
+ end
86
+
87
+ def parse_variable
88
+ token = current_token
89
+ name = Helpers.variable_name_for(token)
90
+ advance
91
+ AST::Variable.new(name: name, location: token.location)
92
+ end
93
+
94
+ def parse_reference_path(path)
95
+ while match?(TokenType::DOT, TokenType::LBRACKET)
96
+ match?(TokenType::DOT) ? parse_dot_reference(path) : parse_bracket_reference(path)
97
+ end
98
+ end
99
+
100
+ def parse_dot_reference(path)
101
+ advance
102
+ segment_token = current_token
103
+ segment = parse_identifier(IdentifierContext.new(name: "reference", allowed_types: IDENTIFIER_TOKEN_TYPES))
104
+ path << AST::DotRefArg.new(value: segment, location: segment_token.location)
105
+ end
106
+
107
+ def parse_bracket_reference(path)
108
+ bracket_token = consume(TokenType::LBRACKET)
109
+ value = parse_expression
110
+ consume(TokenType::RBRACKET, "Expected ']' after reference path.")
111
+ path << AST::BracketRefArg.new(value: value, location: bracket_token.location)
112
+ end
113
+ end
114
+ end
115
+ end