kumi-parser 0.0.12 → 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: 935f50c0ecadd127eddc6ddef78aa2cd2b4cc25c26ef33202260895719224527
4
- data.tar.gz: ecebfbe0f95c5304d5a188eeaf8b6c22eff42357913064aa6f1ea380b4145f69
3
+ metadata.gz: 83ae18a38c78f9513121f579f024309c162ebbc1fdf909aecbe8aceff04e100a
4
+ data.tar.gz: 8bfab6fb7250f7e39289ceb07b84ae556958a77be19b66fe253b19c7284359ba
5
5
  SHA512:
6
- metadata.gz: 81fe15f8482f0a2e123c14136e10bf47d2b90f5957f8c90e6ca4a416e811deee838510d22d3eb266dc8ef64b13dc1f4d2769eeacb06e7f1cac2a82e79fe1aa3b
7
- data.tar.gz: ad2e56299872ac29522b5d63eca7e2c67ad74c07b702bf8cb0aa3a4da870bc11781e8e7a9c1df0232a75712c9eec7d8b29ebfedf4b387094790d2dfa48e3ecbf
6
+ metadata.gz: 39f5759930377f813b10d8ce148dc5f26bd9a6b4a21a577b91bbb24fe7d47d923cf3967f25454666bb9c8c23f0190909f6283fe629a7bd935d51ca9033c25047
7
+ data.tar.gz: d06bd58249f51ef7edafb51f291c2b18720ea259b3a375b9624ac593631f7786aa2c920da92bea99ec1ad27fc517c71c9b7bbd41e7626faa814949e317b56f29
data/README.md CHANGED
@@ -58,7 +58,15 @@ end
58
58
 
59
59
  **Function calls**: `fn(:name, arg1, arg2, ...)`
60
60
  **Operators**: `+` `-` `*` `/` `%` `>` `<` `>=` `<=` `==` `!=` `&` `|`
61
- **References**: `input.field`, `value_name`, `array[index]`
61
+ **References**: `input.field`, `value_name`, `array[index]`
62
+ **Strings**: Both `"double"` and `'single'` quotes supported
63
+ **Element syntax**: `element :type, :name` for array element specifications
64
+
65
+ ## Ruby DSL Differences
66
+
67
+ **String concatenation**: Ruby DSL evaluates `"Hello" + "World"` → `Literal("HelloWorld")`, text parser → `CallExpression(:add, [...])`.
68
+
69
+ **Semantically equivalent** - both should execute identically.
62
70
 
63
71
  ## Architecture
64
72
 
@@ -66,8 +74,6 @@ end
66
74
  - `direct_ast_parser.rb` - Recursive descent parser, direct AST construction
67
75
  - `token_metadata.rb` - Token types, precedence, and semantic hints
68
76
 
69
- See `docs/` for technical details.
70
-
71
77
  ## License
72
78
 
73
79
  MIT
@@ -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,18 +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
- name_token = expect_token(:symbol)
114
111
 
115
- # Handle domain specification: ', domain: [...]'
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
115
+ element_type_token = expect_token(:symbol)
116
+ expect_token(:comma)
117
+ name_token = expect_token(:symbol)
118
+ actual_type = element_type_token.value
119
+ else
120
+ name_token = expect_token(:symbol)
121
+ actual_type = type_token.metadata[:type_name]
122
+ end
123
+
124
+ # Optional: ', domain: ...'
116
125
  domain = nil
117
126
  if current_token.type == :comma
118
127
  advance
@@ -121,102 +130,112 @@ module Kumi
121
130
  expect_token(:colon)
122
131
  domain = parse_domain_specification
123
132
  else
124
- # Put comma back for other parsers
125
133
  @pos -= 1
126
134
  end
127
135
  end
128
136
 
129
- # Handle nested array and hash declarations
137
+ # Parse nested declarations for block forms
130
138
  children = []
131
- if %i[array hash].include?(type_token.metadata[:type_name]) && current_token.type == :do
139
+ any_element_children = false
140
+ any_field_children = false
141
+
142
+ if %i[array hash element].include?(actual_type) && current_token.type == :do
132
143
  advance # consume 'do'
133
144
  skip_comments_and_newlines
134
145
 
135
146
  until %i[end eof].include?(current_token.type)
136
147
  break unless current_token.metadata[:category] == :type_keyword
137
148
 
138
- 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
139
153
 
154
+ children << parse_input_declaration
140
155
  skip_comments_and_newlines
141
156
  end
142
157
 
143
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
144
174
  end
145
175
 
146
176
  if children.empty?
147
177
  Kumi::Syntax::InputDeclaration.new(
148
178
  name_token.value,
149
179
  domain,
150
- type_token.metadata[:type_name],
180
+ actual_type,
151
181
  children,
152
182
  loc: type_token.location
153
183
  )
154
184
  else
185
+ # 5th positional arg in your existing ctor is access_mode
155
186
  Kumi::Syntax::InputDeclaration.new(
156
187
  name_token.value,
157
188
  domain,
158
- type_token.metadata[:type_name],
189
+ actual_type,
159
190
  children,
160
- :field,
191
+ access_mode || :field,
161
192
  loc: type_token.location
162
193
  )
163
194
  end
164
195
  end
165
196
 
166
197
  def parse_domain_specification
167
- # Parse domain specifications: domain: ["x", "y"], domain: [1, 2, 3], domain: 1..10, domain: 1...10
168
198
  case current_token.type
169
199
  when :lbracket
170
- # Array domain: ["a", "b", "c"] or [1, 2, 3]
171
200
  array_expr = parse_array_literal
172
- # Convert ArrayExpression to Ruby Array for analyzer compatibility
173
201
  convert_array_expression_to_ruby_array(array_expr)
174
202
  when :integer, :float
175
- # Range domain: 1..10 or 1...10
176
203
  parse_range_domain
177
204
  else
178
- # Skip unknown domain specs for now
179
205
  advance until %i[comma newline eof end].include?(current_token.type)
180
206
  nil
181
207
  end
182
208
  end
183
209
 
184
210
  def parse_range_domain
185
- # Parse numeric ranges like 1..10 or 0.0...100.0
186
211
  start_token = current_token
187
212
  start_value = start_token.type == :integer ? start_token.value.to_i : start_token.value.to_f
188
213
  advance
189
214
 
190
215
  case current_token.type
191
216
  when :dot_dot
192
- # Inclusive range: start..end
193
- advance # consume ..
217
+ advance
194
218
  end_token = current_token
195
219
  end_value = end_token.type == :integer ? end_token.value.to_i : end_token.value.to_f
196
220
  advance
197
221
  (start_value..end_value)
198
222
  when :dot_dot_dot
199
- # Exclusive range: start...end
200
- advance # consume ...
223
+ advance
201
224
  end_token = current_token
202
225
  end_value = end_token.type == :integer ? end_token.value.to_i : end_token.value.to_f
203
226
  advance
204
227
  (start_value...end_value)
205
228
  else
206
- # Just a single number, treat as single-element array
207
229
  [start_value]
208
230
  end
209
231
  end
210
232
 
211
233
  def convert_array_expression_to_ruby_array(array_expr)
212
234
  return nil unless array_expr.is_a?(Kumi::Syntax::ArrayExpression)
213
-
214
235
  array_expr.elements.map do |element|
215
236
  if element.is_a?(Kumi::Syntax::Literal)
216
237
  element.value
217
238
  else
218
- # For non-literal elements, we'd need more complex evaluation
219
- # For now, just return the element as-is
220
239
  element
221
240
  end
222
241
  end
@@ -228,10 +247,8 @@ module Kumi
228
247
  name_token = expect_token(:symbol)
229
248
 
230
249
  if current_token.type == :do
231
- # Cascade expression: value :name do ... end
232
250
  expression = parse_cascade_expression
233
251
  else
234
- # Simple expression: value :name, expression
235
252
  expect_token(:comma)
236
253
  expression = parse_expression
237
254
  end
@@ -261,55 +278,42 @@ module Kumi
261
278
  def parse_cascade_expression
262
279
  start_token = expect_token(:do)
263
280
  cases = []
264
-
265
281
  skip_comments_and_newlines
266
282
  while %i[on base].include?(current_token.type)
267
283
  cases << parse_case_expression
268
284
  skip_comments_and_newlines
269
285
  end
270
-
271
286
  expect_token(:end)
272
-
273
287
  Kumi::Syntax::CascadeExpression.new(cases, loc: start_token.location)
274
288
  end
275
289
 
276
- # Case expression: 'on condition1, condition2, ..., result' or 'base result'
277
290
  def parse_case_expression
278
291
  case current_token.type
279
292
  when :on
280
293
  on_token = advance_and_return_token
281
-
282
- # Parse all comma-separated expressions
294
+
283
295
  expressions = []
284
296
  expressions << parse_expression
285
-
286
- # Continue parsing comma-separated expressions until end of case
287
297
  while current_token.type == :comma
288
- advance # consume comma
298
+ advance
289
299
  expressions << parse_expression
290
300
  end
291
-
292
- # Last expression is the result, all others are conditions
301
+
293
302
  result = expressions.pop
294
303
  conditions = expressions
295
-
296
- # Combine conditions appropriately
297
- if conditions.length == 1
298
- condition = conditions[0]
299
- # Wrap simple trait references in cascade_and to match Ruby DSL behavior
300
- condition = wrap_condition_in_all(condition) if simple_trait_reference?(condition)
301
- else
302
- # Multiple conditions: combine with cascade_and
303
- condition = Kumi::Syntax::CallExpression.new(:cascade_and, conditions, loc: on_token.location)
304
- 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
305
311
 
306
312
  Kumi::Syntax::CaseExpression.new(condition, result, loc: on_token.location)
307
313
 
308
314
  when :base
309
315
  base_token = advance_and_return_token
310
316
  result = parse_expression
311
-
312
- # Base case has condition = true
313
317
  true_literal = Kumi::Syntax::Literal.new(true, loc: base_token.location)
314
318
  Kumi::Syntax::CaseExpression.new(true_literal, result, loc: base_token.location)
315
319
 
@@ -324,26 +328,22 @@ module Kumi
324
328
  token
325
329
  end
326
330
 
327
- # Expression parsing with operator precedence
331
+ # Pratt parser for expressions
328
332
  def parse_expression(min_precedence = 0)
329
333
  left = parse_primary_expression
330
-
331
- # Skip whitespace before checking for operators
332
334
  skip_comments_and_newlines
333
335
 
334
336
  while current_token.operator? && current_token.precedence >= min_precedence
335
337
  operator_token = current_token
336
338
  advance
337
-
338
- # Skip whitespace after operator
339
339
  skip_comments_and_newlines
340
340
 
341
- # Use embedded associativity from token metadata
342
- next_min_precedence = if operator_token.left_associative?
343
- operator_token.precedence + 1
344
- else
345
- operator_token.precedence
346
- end
341
+ next_min_precedence =
342
+ if operator_token.left_associative?
343
+ operator_token.precedence + 1
344
+ else
345
+ operator_token.precedence
346
+ end
347
347
 
348
348
  right = parse_expression(next_min_precedence)
349
349
  left = Kumi::Syntax::CallExpression.new(
@@ -351,8 +351,6 @@ module Kumi
351
351
  [left, right],
352
352
  loc: operator_token.location
353
353
  )
354
-
355
- # Skip whitespace before checking for next operator
356
354
  skip_comments_and_newlines
357
355
  end
358
356
 
@@ -364,7 +362,6 @@ module Kumi
364
362
 
365
363
  case token.type
366
364
  when :integer, :float, :string, :boolean, :constant
367
- # Direct AST construction using token metadata
368
365
  value = convert_literal_value(token)
369
366
  advance
370
367
  Kumi::Syntax::Literal.new(value, loc: token.location)
@@ -382,7 +379,6 @@ module Kumi
382
379
  end
383
380
 
384
381
  when :input
385
- # Handle input references in expressions (input.field)
386
382
  if peek_token.type == :dot
387
383
  parse_input_reference_from_input_token
388
384
  else
@@ -390,7 +386,7 @@ module Kumi
390
386
  end
391
387
 
392
388
  when :lparen
393
- advance # consume '('
389
+ advance
394
390
  expr = parse_expression
395
391
  expect_token(:rparen)
396
392
  expr
@@ -402,11 +398,9 @@ module Kumi
402
398
  parse_function_call_from_fn_token
403
399
 
404
400
  when :subtract
405
- # Handle unary minus operator: -expression
406
- advance # consume '-'
401
+ advance
407
402
  skip_comments_and_newlines
408
403
  operand = parse_primary_expression
409
- # Convert to subtraction from zero: (subtract 0 operand)
410
404
  Kumi::Syntax::CallExpression.new(
411
405
  :subtract,
412
406
  [Kumi::Syntax::Literal.new(0, loc: token.location), operand],
@@ -414,7 +408,6 @@ module Kumi
414
408
  )
415
409
 
416
410
  when :newline, :comment
417
- # Skip newlines and comments in expressions
418
411
  skip_comments_and_newlines
419
412
  parse_primary_expression
420
413
 
@@ -428,10 +421,8 @@ module Kumi
428
421
  expect_token(:dot)
429
422
 
430
423
  path = [expect_field_name_token.to_sym]
431
-
432
- # Handle nested access: input.field.subfield
433
424
  while current_token.type == :dot
434
- advance # consume '.'
425
+ advance
435
426
  path << expect_field_name_token.to_sym
436
427
  end
437
428
 
@@ -443,14 +434,12 @@ module Kumi
443
434
  end
444
435
 
445
436
  def parse_input_reference_from_input_token
446
- input_token = expect_token(:input) # 'input' keyword token
437
+ input_token = expect_token(:input)
447
438
  expect_token(:dot)
448
439
 
449
440
  path = [expect_field_name_token.to_sym]
450
-
451
- # Handle nested access: input.field.subfield
452
441
  while current_token.type == :dot
453
- advance # consume '.'
442
+ advance
454
443
  path << expect_field_name_token.to_sym
455
444
  end
456
445
 
@@ -468,54 +457,40 @@ module Kumi
468
457
  expect_token(:rbracket)
469
458
 
470
459
  base_ref = Kumi::Syntax::DeclarationReference.new(name_token.value.to_sym, loc: name_token.location)
471
- Kumi::Syntax::CallExpression.new(
472
- :at,
473
- [base_ref, index_expr],
474
- loc: name_token.location
475
- )
460
+ Kumi::Syntax::CallExpression.new(:at, [base_ref, index_expr], loc: name_token.location)
476
461
  end
477
462
 
478
463
  def parse_function_call
479
- fn_token = expect_token(:identifier) # 'fn'
480
-
464
+ fn_token = expect_token(:identifier)
481
465
  if current_token.type == :lparen
482
- # Only syntax: fn(:symbol, args...)
483
- advance # consume '('
466
+ advance
484
467
  fn_name_token = expect_token(:symbol)
485
468
  fn_name = fn_name_token.value
486
-
487
469
  args = []
488
470
  while current_token.type == :comma
489
- advance # consume comma
471
+ advance
490
472
  args << parse_expression
491
473
  end
492
-
493
474
  expect_token(:rparen)
494
475
  Kumi::Syntax::CallExpression.new(fn_name, args, loc: fn_name_token.location)
495
-
496
476
  else
497
477
  raise_parse_error("Expected '(' after 'fn'")
498
478
  end
499
479
  end
500
480
 
501
481
  def parse_function_call_from_fn_token
502
- fn_token = expect_token(:fn) # 'fn' keyword token
503
-
482
+ fn_token = expect_token(:fn)
504
483
  if current_token.type == :lparen
505
- # Only syntax: fn(:symbol, args...)
506
- advance # consume '('
484
+ advance
507
485
  fn_name_token = expect_token(:symbol)
508
486
  fn_name = fn_name_token.value
509
-
510
487
  args = []
511
488
  while current_token.type == :comma
512
- advance # consume comma
489
+ advance
513
490
  args << parse_expression
514
491
  end
515
-
516
492
  expect_token(:rparen)
517
493
  Kumi::Syntax::CallExpression.new(fn_name, args, loc: fn_name_token.location)
518
-
519
494
  else
520
495
  raise_parse_error("Expected '(' after 'fn'")
521
496
  end
@@ -523,40 +498,36 @@ module Kumi
523
498
 
524
499
  def parse_argument_list
525
500
  args = []
526
-
527
501
  unless current_token.type == :rparen
528
502
  args << parse_expression
529
503
  while current_token.type == :comma
530
- advance # consume comma
504
+ advance
531
505
  args << parse_expression
532
506
  end
533
507
  end
534
-
535
508
  args
536
509
  end
537
510
 
538
511
  def parse_array_literal
539
512
  start_token = expect_token(:lbracket)
540
513
  elements = []
541
-
542
514
  unless current_token.type == :rbracket
543
515
  elements << parse_expression
544
516
  while current_token.type == :comma
545
- advance # consume comma
517
+ advance
546
518
  elements << parse_expression unless current_token.type == :rbracket
547
519
  end
548
520
  end
549
-
550
521
  expect_token(:rbracket)
551
522
  Kumi::Syntax::ArrayExpression.new(elements, loc: start_token.location)
552
523
  end
553
524
 
554
525
  def convert_literal_value(token)
555
526
  case token.type
556
- when :integer then token.value.gsub('_', '').to_i
557
- when :float then token.value.gsub('_', '').to_f
558
- when :string then token.value
559
- 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'
560
531
  when :constant
561
532
  case token.value
562
533
  when 'Float::INFINITY' then Float::INFINITY
@@ -567,7 +538,6 @@ module Kumi
567
538
  end
568
539
 
569
540
  def expect_field_name_token
570
- # Field names can be identifiers or keywords (like 'base', 'input', etc.)
571
541
  token = current_token
572
542
  if token.identifier? || token.keyword?
573
543
  advance
@@ -582,28 +552,24 @@ module Kumi
582
552
  raise Errors::ParseError.new(message, token: current_token)
583
553
  end
584
554
 
585
- # Helper method to check if condition is a simple trait reference
586
555
  def simple_trait_reference?(condition)
587
556
  condition.is_a?(Kumi::Syntax::DeclarationReference)
588
557
  end
589
558
 
590
-
591
- # Helper method to wrap condition in cascade_and function call
592
559
  def wrap_condition_in_all(condition)
593
560
  Kumi::Syntax::CallExpression.new(:cascade_and, [condition], loc: condition.loc)
594
561
  end
595
562
 
596
- # Map operator token types to function names for Ruby DSL compatibility
597
563
  def map_operator_token_to_function_name(token_type)
598
564
  case token_type
599
- when :eq then :==
600
- when :ne then :!=
601
- when :gt then :>
602
- when :lt then :<
565
+ when :eq then :==
566
+ when :ne then :!=
567
+ when :gt then :>
568
+ when :lt then :<
603
569
  when :gte then :>=
604
570
  when :lte then :<=
605
571
  when :and then :and
606
- when :or then :or
572
+ when :or then :or
607
573
  when :exponent then :power
608
574
  else token_type
609
575
  end
@@ -26,7 +26,7 @@ module Kumi
26
26
  when nil then break
27
27
  when "\n" then handle_newline
28
28
  when '#' then consume_comment
29
- when '"' then consume_string
29
+ when '"', "'" then consume_string
30
30
  when /\d/ then consume_number
31
31
  when '-'
32
32
  if peek_char && peek_char.match?(/\d/)
@@ -95,10 +95,11 @@ module Kumi
95
95
 
96
96
  def consume_string
97
97
  start_column = @column
98
+ quote_char = current_char # Remember which quote type we're using
98
99
  advance # skip opening quote
99
100
 
100
101
  string_content = ''
101
- while current_char && current_char != '"'
102
+ while current_char && current_char != quote_char
102
103
  if current_char == '\\'
103
104
  advance
104
105
  # Handle escape sequences
@@ -108,6 +109,7 @@ module Kumi
108
109
  when 'r' then string_content += "\r"
109
110
  when '\\' then string_content += '\\'
110
111
  when '"' then string_content += '"'
112
+ when "'" then string_content += "'"
111
113
  else
112
114
  string_content += current_char if current_char
113
115
  end
@@ -117,7 +119,7 @@ module Kumi
117
119
  advance
118
120
  end
119
121
 
120
- raise_tokenizer_error('Unterminated string literal') if current_char != '"'
122
+ raise_tokenizer_error('Unterminated string literal') if current_char != quote_char
121
123
 
122
124
  advance # skip closing quote
123
125
 
@@ -32,6 +32,7 @@ module Kumi
32
32
  BOOLEAN_TYPE = :boolean_type # boolean
33
33
  ANY_TYPE = :any_type # any
34
34
  ARRAY_TYPE = :array_type # array
35
+ ELEMENT_TYPE = :element_type # element
35
36
 
36
37
  # Function keywords
37
38
  FN = :fn
@@ -149,6 +150,11 @@ module Kumi
149
150
  starts_declaration: true,
150
151
  type_name: :hash
151
152
  },
153
+ element_type: {
154
+ category: :type_keyword,
155
+ starts_declaration: true,
156
+ type_name: :element
157
+ },
152
158
 
153
159
  # Function keyword
154
160
  fn: {
@@ -385,7 +391,8 @@ module Kumi
385
391
  'boolean' => :boolean_type,
386
392
  'any' => :any_type,
387
393
  'array' => :array_type,
388
- 'hash' => :hash_type
394
+ 'hash' => :hash_type,
395
+ 'element' => :element_type
389
396
  }.freeze
390
397
 
391
398
  # Opener to closer mappings for error recovery
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kumi
4
4
  module Parser
5
- VERSION = '0.0.12'
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.12
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-25 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