kumi-parser 0.0.13 → 0.0.14

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