kumi-parser 0.0.13 → 0.0.15

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7dbd29c99827160178d8ebaebc68ada7b754f2ecdbc99d5c204dc6396431e18f
4
- data.tar.gz: cef86a4ebd6b3398d70255a9111832ad9094626da74936e3d858fcba41999efa
3
+ metadata.gz: 1d86288cacf8d5e80d38541472d08b58ff202a7ec04efd0689c3b2698480fde1
4
+ data.tar.gz: 86088f162d87519650cb0e6473176962967f292479bafb71973294bf723a4752
5
5
  SHA512:
6
- metadata.gz: af373812dec2bc63ffa5279d5f7adbd2c852f272969dcec65196ddca750d965dbe8b37496f99004975e1a2dbbabac3e7f5cfc5e348bd16b51b2a1261315b97eb
7
- data.tar.gz: b3faf218a65da205f4f20651e808dcbfc32bd8ace3a63a479aa576909bcb0f0e9ea066fab04d8013a4046faaa0332a8cf5e223f8f785d8652518d7b89134168e
6
+ metadata.gz: 5ab143e2e5a1f3e2e84c3ebb1a23283d4c24ce727dd4a4145d0beac7415e823bb0a0ce29711a8354010c662402f0ecaea7da0a254b8a352f200dee3bcae7fa61
7
+ data.tar.gz: 76abe4b2565162a1b49421de36355bf011aa444ca3372bfb3b42ff4361e50959079103b2f85b704c17ffe06ee689c3dc39f45359fe84cd165590a82c9e74982c
data/README.md CHANGED
@@ -57,7 +57,7 @@ end
57
57
  ```
58
58
 
59
59
  **Function calls**: `fn(:name, arg1, arg2, ...)`
60
- **Operators**: `+` `-` `*` `/` `%` `>` `<` `>=` `<=` `==` `!=` `&` `|`
60
+ **Operators**: `+` `-` `*` `**` `` `/` `%` `>` `<` `>=` `<=` `==` `!=` `&` `|`
61
61
  **References**: `input.field`, `value_name`, `array[index]`
62
62
  **Strings**: Both `"double"` and `'single'` quotes supported
63
63
  **Element syntax**: `element :type, :name` for array element specifications
@@ -72,7 +72,6 @@ module Kumi
72
72
 
73
73
  expect_token(:end)
74
74
 
75
- # Construct Root with exact AST.md structure
76
75
  Kumi::Syntax::Root.new(
77
76
  input_declarations,
78
77
  value_declarations, # values
@@ -93,7 +92,6 @@ module Kumi
93
92
  break unless current_token.metadata[:category] == :type_keyword
94
93
 
95
94
  declarations << parse_input_declaration
96
-
97
95
  skip_comments_and_newlines
98
96
  end
99
97
 
@@ -101,28 +99,31 @@ module Kumi
101
99
  declarations
102
100
  end
103
101
 
104
- # Input declaration: 'integer :name' or 'array :items do ... end'
102
+ # Input declaration: 'integer :name' or 'array :items do ... end' or 'element :type, :name'
103
+ #
104
+ # IMPORTANT: For array nodes with a block, this sets the node's access_mode:
105
+ # - :element if the block contains exactly one child introduced by `element`
106
+ # - :field otherwise
105
107
  def parse_input_declaration
106
108
  type_token = current_token
107
-
108
- if type_token.metadata[:category] != :type_keyword
109
+ unless type_token.metadata[:category] == :type_keyword
109
110
  raise_parse_error("Expected type keyword, got #{type_token.type}")
110
111
  end
111
-
112
112
  advance
113
-
114
- # Handle element syntax: element :type, :name
115
- if type_token.metadata[:type_name] == :element
113
+
114
+ # element :type, :name (syntactic sugar: the child was declared via `element`)
115
+ declared_with_element = (type_token.metadata[:type_name] == :element)
116
+ if declared_with_element
116
117
  element_type_token = expect_token(:symbol)
117
118
  expect_token(:comma)
118
119
  name_token = expect_token(:symbol)
119
120
  actual_type = element_type_token.value
120
121
  else
121
- name_token = expect_token(:symbol)
122
+ name_token = expect_token(:symbol)
122
123
  actual_type = type_token.metadata[:type_name]
123
124
  end
124
125
 
125
- # Handle domain specification: ', domain: [...]'
126
+ # Optional: ', domain: ...'
126
127
  domain = nil
127
128
  if current_token.type == :comma
128
129
  advance
@@ -131,13 +132,15 @@ module Kumi
131
132
  expect_token(:colon)
132
133
  domain = parse_domain_specification
133
134
  else
134
- # Put comma back for other parsers
135
135
  @pos -= 1
136
136
  end
137
137
  end
138
138
 
139
- # Handle nested array, hash, and element declarations
139
+ # Parse nested declarations for block forms
140
140
  children = []
141
+ any_element_children = false
142
+ any_field_children = false
143
+
141
144
  if %i[array hash element].include?(actual_type) && current_token.type == :do
142
145
  advance # consume 'do'
143
146
  skip_comments_and_newlines
@@ -145,12 +148,31 @@ module Kumi
145
148
  until %i[end eof].include?(current_token.type)
146
149
  break unless current_token.metadata[:category] == :type_keyword
147
150
 
148
- children << parse_input_declaration
151
+ # Syntactic decision (NO counting): is this child introduced by `element`?
152
+ child_is_element_keyword = (current_token.metadata[:type_name] == :element)
153
+ any_element_children ||= child_is_element_keyword
154
+ any_field_children ||= !child_is_element_keyword
149
155
 
156
+ children << parse_input_declaration
150
157
  skip_comments_and_newlines
151
158
  end
152
159
 
153
160
  expect_token(:end)
161
+
162
+ # For array blocks, access_mode derives strictly from syntax:
163
+ # - :element if ANY direct child used `element`
164
+ # - :field if NONE used `element`
165
+ # Mixing is invalid.
166
+ if actual_type == :array
167
+ if any_element_children && any_field_children
168
+ raise_parse_error("array :#{name_token.value} mixes `element` and field children; choose one style")
169
+ end
170
+ access_mode = any_element_children ? :element : :field
171
+ else
172
+ access_mode = :field # objects/hashes with blocks behave like field containers
173
+ end
174
+ else
175
+ access_mode = nil # leaves carry no access_mode
154
176
  end
155
177
 
156
178
  if children.empty?
@@ -162,58 +184,50 @@ module Kumi
162
184
  loc: type_token.location
163
185
  )
164
186
  else
187
+ # 5th positional arg in your existing ctor is access_mode
165
188
  Kumi::Syntax::InputDeclaration.new(
166
189
  name_token.value,
167
190
  domain,
168
191
  actual_type,
169
192
  children,
170
- :field,
193
+ access_mode || :field,
171
194
  loc: type_token.location
172
195
  )
173
196
  end
174
197
  end
175
198
 
176
199
  def parse_domain_specification
177
- # Parse domain specifications: domain: ["x", "y"], domain: [1, 2, 3], domain: 1..10, domain: 1...10
178
200
  case current_token.type
179
201
  when :lbracket
180
- # Array domain: ["a", "b", "c"] or [1, 2, 3]
181
202
  array_expr = parse_array_literal
182
- # Convert ArrayExpression to Ruby Array for analyzer compatibility
183
203
  convert_array_expression_to_ruby_array(array_expr)
184
204
  when :integer, :float
185
- # Range domain: 1..10 or 1...10
186
205
  parse_range_domain
187
206
  else
188
- # Skip unknown domain specs for now
189
207
  advance until %i[comma newline eof end].include?(current_token.type)
190
208
  nil
191
209
  end
192
210
  end
193
211
 
194
212
  def parse_range_domain
195
- # Parse numeric ranges like 1..10 or 0.0...100.0
196
213
  start_token = current_token
197
214
  start_value = start_token.type == :integer ? start_token.value.to_i : start_token.value.to_f
198
215
  advance
199
216
 
200
217
  case current_token.type
201
218
  when :dot_dot
202
- # Inclusive range: start..end
203
- advance # consume ..
219
+ advance
204
220
  end_token = current_token
205
221
  end_value = end_token.type == :integer ? end_token.value.to_i : end_token.value.to_f
206
222
  advance
207
223
  (start_value..end_value)
208
224
  when :dot_dot_dot
209
- # Exclusive range: start...end
210
- advance # consume ...
225
+ advance
211
226
  end_token = current_token
212
227
  end_value = end_token.type == :integer ? end_token.value.to_i : end_token.value.to_f
213
228
  advance
214
229
  (start_value...end_value)
215
230
  else
216
- # Just a single number, treat as single-element array
217
231
  [start_value]
218
232
  end
219
233
  end
@@ -225,8 +239,6 @@ module Kumi
225
239
  if element.is_a?(Kumi::Syntax::Literal)
226
240
  element.value
227
241
  else
228
- # For non-literal elements, we'd need more complex evaluation
229
- # For now, just return the element as-is
230
242
  element
231
243
  end
232
244
  end
@@ -238,10 +250,8 @@ module Kumi
238
250
  name_token = expect_token(:symbol)
239
251
 
240
252
  if current_token.type == :do
241
- # Cascade expression: value :name do ... end
242
253
  expression = parse_cascade_expression
243
254
  else
244
- # Simple expression: value :name, expression
245
255
  expect_token(:comma)
246
256
  expression = parse_expression
247
257
  end
@@ -271,55 +281,42 @@ module Kumi
271
281
  def parse_cascade_expression
272
282
  start_token = expect_token(:do)
273
283
  cases = []
274
-
275
284
  skip_comments_and_newlines
276
285
  while %i[on base].include?(current_token.type)
277
286
  cases << parse_case_expression
278
287
  skip_comments_and_newlines
279
288
  end
280
-
281
289
  expect_token(:end)
282
-
283
290
  Kumi::Syntax::CascadeExpression.new(cases, loc: start_token.location)
284
291
  end
285
292
 
286
- # Case expression: 'on condition1, condition2, ..., result' or 'base result'
287
293
  def parse_case_expression
288
294
  case current_token.type
289
295
  when :on
290
296
  on_token = advance_and_return_token
291
-
292
- # Parse all comma-separated expressions
297
+
293
298
  expressions = []
294
299
  expressions << parse_expression
295
-
296
- # Continue parsing comma-separated expressions until end of case
297
300
  while current_token.type == :comma
298
- advance # consume comma
301
+ advance
299
302
  expressions << parse_expression
300
303
  end
301
-
302
- # Last expression is the result, all others are conditions
304
+
303
305
  result = expressions.pop
304
306
  conditions = expressions
305
-
306
- # Combine conditions appropriately
307
- if conditions.length == 1
308
- condition = conditions[0]
309
- # Wrap simple trait references in cascade_and to match Ruby DSL behavior
310
- condition = wrap_condition_in_all(condition) if simple_trait_reference?(condition)
311
- else
312
- # Multiple conditions: combine with cascade_and
313
- condition = Kumi::Syntax::CallExpression.new(:cascade_and, conditions, loc: on_token.location)
314
- end
307
+ condition =
308
+ if conditions.length == 1
309
+ c = conditions[0]
310
+ simple_trait_reference?(c) ? wrap_condition_in_all(c) : c
311
+ else
312
+ Kumi::Syntax::CallExpression.new(:cascade_and, conditions, loc: on_token.location)
313
+ end
315
314
 
316
315
  Kumi::Syntax::CaseExpression.new(condition, result, loc: on_token.location)
317
316
 
318
317
  when :base
319
318
  base_token = advance_and_return_token
320
319
  result = parse_expression
321
-
322
- # Base case has condition = true
323
320
  true_literal = Kumi::Syntax::Literal.new(true, loc: base_token.location)
324
321
  Kumi::Syntax::CaseExpression.new(true_literal, result, loc: base_token.location)
325
322
 
@@ -334,26 +331,22 @@ module Kumi
334
331
  token
335
332
  end
336
333
 
337
- # Expression parsing with operator precedence
334
+ # Pratt parser for expressions
338
335
  def parse_expression(min_precedence = 0)
339
336
  left = parse_primary_expression
340
-
341
- # Skip whitespace before checking for operators
342
337
  skip_comments_and_newlines
343
338
 
344
339
  while current_token.operator? && current_token.precedence >= min_precedence
345
340
  operator_token = current_token
346
341
  advance
347
-
348
- # Skip whitespace after operator
349
342
  skip_comments_and_newlines
350
343
 
351
- # Use embedded associativity from token metadata
352
- next_min_precedence = if operator_token.left_associative?
353
- operator_token.precedence + 1
354
- else
355
- operator_token.precedence
356
- end
344
+ next_min_precedence =
345
+ if operator_token.left_associative?
346
+ operator_token.precedence + 1
347
+ else
348
+ operator_token.precedence
349
+ end
357
350
 
358
351
  right = parse_expression(next_min_precedence)
359
352
  left = Kumi::Syntax::CallExpression.new(
@@ -361,8 +354,6 @@ module Kumi
361
354
  [left, right],
362
355
  loc: operator_token.location
363
356
  )
364
-
365
- # Skip whitespace before checking for next operator
366
357
  skip_comments_and_newlines
367
358
  end
368
359
 
@@ -374,25 +365,24 @@ module Kumi
374
365
 
375
366
  case token.type
376
367
  when :integer, :float, :string, :boolean, :constant
377
- # Direct AST construction using token metadata
378
368
  value = convert_literal_value(token)
379
369
  advance
380
370
  Kumi::Syntax::Literal.new(value, loc: token.location)
371
+ when :function_sugar
372
+ parse_function_sugar
381
373
 
382
374
  when :identifier
375
+
383
376
  if token.value == 'input' && peek_token.type == :dot
384
377
  parse_input_reference
385
378
  elsif peek_token.type == :lbracket
386
379
  parse_array_access_reference
387
- elsif token.value == 'fn'
388
- parse_function_call
389
380
  else
390
381
  advance
391
382
  Kumi::Syntax::DeclarationReference.new(token.value.to_sym, loc: token.location)
392
383
  end
393
384
 
394
385
  when :input
395
- # Handle input references in expressions (input.field)
396
386
  if peek_token.type == :dot
397
387
  parse_input_reference_from_input_token
398
388
  else
@@ -400,7 +390,7 @@ module Kumi
400
390
  end
401
391
 
402
392
  when :lparen
403
- advance # consume '('
393
+ advance
404
394
  expr = parse_expression
405
395
  expect_token(:rparen)
406
396
  expr
@@ -409,14 +399,13 @@ module Kumi
409
399
  parse_array_literal
410
400
 
411
401
  when :fn
412
- parse_function_call_from_fn_token
402
+ # expect_token(:fn)
403
+ parse_function_call
413
404
 
414
405
  when :subtract
415
- # Handle unary minus operator: -expression
416
- advance # consume '-'
406
+ advance
417
407
  skip_comments_and_newlines
418
408
  operand = parse_primary_expression
419
- # Convert to subtraction from zero: (subtract 0 operand)
420
409
  Kumi::Syntax::CallExpression.new(
421
410
  :subtract,
422
411
  [Kumi::Syntax::Literal.new(0, loc: token.location), operand],
@@ -424,7 +413,6 @@ module Kumi
424
413
  )
425
414
 
426
415
  when :newline, :comment
427
- # Skip newlines and comments in expressions
428
416
  skip_comments_and_newlines
429
417
  parse_primary_expression
430
418
 
@@ -438,10 +426,8 @@ module Kumi
438
426
  expect_token(:dot)
439
427
 
440
428
  path = [expect_field_name_token.to_sym]
441
-
442
- # Handle nested access: input.field.subfield
443
429
  while current_token.type == :dot
444
- advance # consume '.'
430
+ advance
445
431
  path << expect_field_name_token.to_sym
446
432
  end
447
433
 
@@ -453,14 +439,12 @@ module Kumi
453
439
  end
454
440
 
455
441
  def parse_input_reference_from_input_token
456
- input_token = expect_token(:input) # 'input' keyword token
442
+ input_token = expect_token(:input)
457
443
  expect_token(:dot)
458
444
 
459
445
  path = [expect_field_name_token.to_sym]
460
-
461
- # Handle nested access: input.field.subfield
462
446
  while current_token.type == :dot
463
- advance # consume '.'
447
+ advance
464
448
  path << expect_field_name_token.to_sym
465
449
  end
466
450
 
@@ -478,54 +462,29 @@ module Kumi
478
462
  expect_token(:rbracket)
479
463
 
480
464
  base_ref = Kumi::Syntax::DeclarationReference.new(name_token.value.to_sym, loc: name_token.location)
481
- Kumi::Syntax::CallExpression.new(
482
- :at,
483
- [base_ref, index_expr],
484
- loc: name_token.location
485
- )
465
+ Kumi::Syntax::CallExpression.new(:at, [base_ref, index_expr], loc: name_token.location)
486
466
  end
487
467
 
488
- def parse_function_call
489
- fn_token = expect_token(:identifier) # 'fn'
490
-
491
- if current_token.type == :lparen
492
- # Only syntax: fn(:symbol, args...)
493
- advance # consume '('
494
- fn_name_token = expect_token(:symbol)
495
- fn_name = fn_name_token.value
496
-
497
- args = []
498
- while current_token.type == :comma
499
- advance # consume comma
500
- args << parse_expression
501
- end
502
-
503
- expect_token(:rparen)
504
- Kumi::Syntax::CallExpression.new(fn_name, args, loc: fn_name_token.location)
505
-
506
- else
507
- raise_parse_error("Expected '(' after 'fn'")
508
- end
468
+ def parse_function_sugar
469
+ sugar_token = current_token
470
+ advance
471
+ args = parse_argument_list
472
+ Kumi::Syntax::CallExpression.new(sugar_token.value.to_sym, args, loc: sugar_token.location)
509
473
  end
510
474
 
511
- def parse_function_call_from_fn_token
512
- fn_token = expect_token(:fn) # 'fn' keyword token
513
-
475
+ def parse_function_call
476
+ advance
514
477
  if current_token.type == :lparen
515
- # Only syntax: fn(:symbol, args...)
516
- advance # consume '('
478
+ advance
517
479
  fn_name_token = expect_token(:symbol)
518
480
  fn_name = fn_name_token.value
519
-
520
481
  args = []
521
482
  while current_token.type == :comma
522
- advance # consume comma
483
+ advance
523
484
  args << parse_expression
524
485
  end
525
-
526
486
  expect_token(:rparen)
527
487
  Kumi::Syntax::CallExpression.new(fn_name, args, loc: fn_name_token.location)
528
-
529
488
  else
530
489
  raise_parse_error("Expected '(' after 'fn'")
531
490
  end
@@ -534,13 +493,15 @@ module Kumi
534
493
  def parse_argument_list
535
494
  args = []
536
495
 
496
+ expect_token(:lparen)
537
497
  unless current_token.type == :rparen
538
498
  args << parse_expression
539
499
  while current_token.type == :comma
540
- advance # consume comma
500
+ advance
541
501
  args << parse_expression
542
502
  end
543
503
  end
504
+ expect_token(:rparen)
544
505
 
545
506
  args
546
507
  end
@@ -548,25 +509,23 @@ module Kumi
548
509
  def parse_array_literal
549
510
  start_token = expect_token(:lbracket)
550
511
  elements = []
551
-
552
512
  unless current_token.type == :rbracket
553
513
  elements << parse_expression
554
514
  while current_token.type == :comma
555
- advance # consume comma
515
+ advance
556
516
  elements << parse_expression unless current_token.type == :rbracket
557
517
  end
558
518
  end
559
-
560
519
  expect_token(:rbracket)
561
520
  Kumi::Syntax::ArrayExpression.new(elements, loc: start_token.location)
562
521
  end
563
522
 
564
523
  def convert_literal_value(token)
565
524
  case token.type
566
- when :integer then token.value.gsub('_', '').to_i
567
- when :float then token.value.gsub('_', '').to_f
568
- when :string then token.value
569
- when :boolean then token.value == 'true'
525
+ when :integer then token.value.gsub('_', '').to_i
526
+ when :float then token.value.gsub('_', '').to_f
527
+ when :string then token.value
528
+ when :boolean then token.value == 'true'
570
529
  when :constant
571
530
  case token.value
572
531
  when 'Float::INFINITY' then Float::INFINITY
@@ -577,7 +536,6 @@ module Kumi
577
536
  end
578
537
 
579
538
  def expect_field_name_token
580
- # Field names can be identifiers or keywords (like 'base', 'input', etc.)
581
539
  token = current_token
582
540
  if token.identifier? || token.keyword?
583
541
  advance
@@ -592,28 +550,24 @@ module Kumi
592
550
  raise Errors::ParseError.new(message, token: current_token)
593
551
  end
594
552
 
595
- # Helper method to check if condition is a simple trait reference
596
553
  def simple_trait_reference?(condition)
597
554
  condition.is_a?(Kumi::Syntax::DeclarationReference)
598
555
  end
599
556
 
600
-
601
- # Helper method to wrap condition in cascade_and function call
602
557
  def wrap_condition_in_all(condition)
603
558
  Kumi::Syntax::CallExpression.new(:cascade_and, [condition], loc: condition.loc)
604
559
  end
605
560
 
606
- # Map operator token types to function names for Ruby DSL compatibility
607
561
  def map_operator_token_to_function_name(token_type)
608
562
  case token_type
609
- when :eq then :==
610
- when :ne then :!=
611
- when :gt then :>
612
- when :lt then :<
563
+ when :eq then :==
564
+ when :ne then :!=
565
+ when :gt then :>
566
+ when :lt then :<
613
567
  when :gte then :>=
614
568
  when :lte then :<=
615
569
  when :and then :and
616
- when :or then :or
570
+ when :or then :or
617
571
  when :exponent then :power
618
572
  else token_type
619
573
  end
@@ -168,12 +168,14 @@ module Kumi
168
168
  advance # consume second :
169
169
  constant_name = consume_while { |c| c.match?(/[a-zA-Z0-9_]/) }
170
170
  full_constant = "#{identifier}::#{constant_name}"
171
-
171
+
172
172
  location = Kumi::Syntax::Location.new(file: @source_file, line: @line, column: start_column)
173
173
  @tokens << Token.new(:constant, full_constant, location, Kumi::Parser::TOKEN_METADATA[:constant])
174
174
  return
175
175
  end
176
176
 
177
+ location = Kumi::Syntax::Location.new(file: @source_file, line: @line, column: start_column)
178
+
177
179
  # Check if it's a keyword
178
180
  if keyword_type = Kumi::Parser::KEYWORDS[identifier]
179
181
  metadata = Kumi::Parser::TOKEN_METADATA[keyword_type].dup
@@ -188,23 +190,29 @@ module Kumi
188
190
  metadata[:closes_context] = closed_context
189
191
  end
190
192
 
191
- location = Kumi::Syntax::Location.new(file: @source_file, line: @line, column: start_column)
192
193
  @tokens << Token.new(keyword_type, identifier, location, metadata)
193
- else
194
- # It's an identifier - determine its role based on context
195
- metadata = Kumi::Parser::TOKEN_METADATA[:identifier].dup
196
-
197
- # Add context-specific metadata
198
- case current_context
199
- when :input
200
- metadata[:context] = :input_declaration
201
- when :schema
202
- metadata[:context] = :schema_body
203
- end
194
+ return
195
+ end
204
196
 
205
- location = Kumi::Syntax::Location.new(file: @source_file, line: @line, column: start_column)
206
- @tokens << Token.new(:identifier, identifier, location, metadata)
197
+ # Check if its a function sugar
198
+ if Kumi::Parser::FUNCTION_SUGAR[identifier]
199
+ metadata = Kumi::Parser::TOKEN_METADATA[:function_sugar].dup
200
+ @tokens << Token.new(:function_sugar, identifier, location, metadata)
201
+ return
202
+ end
203
+
204
+ # Otherwise is an Idenfier
205
+ metadata = Kumi::Parser::TOKEN_METADATA[:identifier].dup
206
+
207
+ # Add context-specific metadata
208
+ case current_context
209
+ when :input
210
+ metadata[:context] = :input_declaration
211
+ when :schema
212
+ metadata[:context] = :schema_body
207
213
  end
214
+
215
+ @tokens << Token.new(:identifier, identifier, location, metadata)
208
216
  end
209
217
 
210
218
  def consume_symbol_or_colon
@@ -81,4 +81,4 @@ module Kumi
81
81
  end
82
82
  end
83
83
  end
84
- end
84
+ end
@@ -38,8 +38,8 @@ module Kumi
38
38
  FN = :fn
39
39
 
40
40
  # Operators (by precedence)
41
- EXPONENT = :exponent # **
42
- MULTIPLY = :multiply # *
41
+ EXPONENT = :exponent # **
42
+ MULTIPLY = :multiply # *
43
43
  DIVIDE = :divide # /
44
44
  MODULO = :modulo # %
45
45
  ADD = :add # +
@@ -67,7 +67,7 @@ module Kumi
67
67
  # Special
68
68
  NEWLINE = :newline
69
69
  EOF = :eof
70
- COMMENT = :comment # # comment
70
+ COMMENT = :comment # # comment
71
71
  end
72
72
 
73
73
  # Rich metadata for each token type
@@ -163,6 +163,11 @@ module Kumi
163
163
  starts_expression: true
164
164
  },
165
165
 
166
+ function_sugar: {
167
+ function_keyword: true,
168
+ starts_expression: true
169
+ },
170
+
166
171
  # Operators with precedence and associativity
167
172
  exponent: {
168
173
  category: :operator,
@@ -372,6 +377,10 @@ module Kumi
372
377
  '|' => :or
373
378
  }.freeze
374
379
 
380
+ FUNCTION_SUGAR = {
381
+ 'select' => '__select__'
382
+ }
383
+
375
384
  # Keywords mapping
376
385
  KEYWORDS = {
377
386
  'schema' => :schema,
@@ -401,4 +410,4 @@ module Kumi
401
410
  rbracket: :lbracket
402
411
  }.freeze
403
412
  end
404
- end
413
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kumi
4
4
  module Parser
5
- VERSION = '0.0.13'
5
+ VERSION = '0.0.15'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kumi-parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.13
4
+ version: 0.0.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kumi Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-28 00:00:00.000000000 Z
11
+ date: 2025-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: kumi