kumi-parser 0.0.23 → 0.0.25

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: a93347db05aac0a7ae24208706f40fd65586b4b7bb4872ce21443db80d21e9ed
4
- data.tar.gz: ecfbe7e5722cbfcbc16066e95f8b6a5c42d49e5d1ea50f06029072ef27fbec08
3
+ metadata.gz: 40a328f8314da0cfd5cb058b439f60ec892fe8c048dab561f8bbbc0bd073094a
4
+ data.tar.gz: ff549aea18eecf1b4fee4855bdf6bfafa34d7b5cd08a39a9f570222a06b35770
5
5
  SHA512:
6
- metadata.gz: 790e663404df430cf385eda58bea125a8467e1b587e6d4e98f4975b9ef125a61c5eadf19ac4398be543a2a17d251a3c1e091727e42b9599bc2c25135dda3017c
7
- data.tar.gz: 5d32509d9b747d267fc2b2787c94484d434d88ced020f831b9230a866a60a51f95f34655c436c125a71f017f9dfae5feeedaeb5ad8420987626081d2d79b7071
6
+ metadata.gz: c146082ac76579bd8476547175f099c2c5f193f56d3d4ae9cea7fed370a84c48a77fe7a5566b4eae79bc732d3b2a58838c75f96a3c8103bbe3ab0451b1f745c1
7
+ data.tar.gz: 6a98319c87b3069a23908e32748c4d8ee7a03c134d422897043537e893df59c64bbb125eae9d451579d04c69067eef61a8f4e8cd71b67cc70d28153de5fe4ba4
data/CLAUDE.md CHANGED
@@ -83,7 +83,7 @@ text_ast = Kumi::Parser::TextParser.parse(<<~KUMI)
83
83
  KUMI
84
84
 
85
85
  # Compare ASTs
86
- ruby_ast = TestSchema.__syntax_tree__
86
+ ruby_ast = TestSchema.__kumi_syntax_tree__
87
87
  text_ast == ruby_ast # Should be true
88
88
  ```
89
89
 
@@ -4,6 +4,8 @@ module Kumi
4
4
  module Parser
5
5
  # Direct AST construction parser using recursive descent with embedded token metadata
6
6
  class DirectParser
7
+ include Kumi::Parser::Helpers
8
+
7
9
  def initialize(tokens)
8
10
  @tokens = tokens
9
11
  @pos = 0
@@ -41,14 +43,6 @@ module Kumi
41
43
  token
42
44
  end
43
45
 
44
- def skip_newlines
45
- advance while current_token.type == :newline
46
- end
47
-
48
- def skip_comments_and_newlines
49
- advance while %i[newline comment].include?(current_token.type)
50
- end
51
-
52
46
  # Schema: 'schema' 'do' ... 'end'
53
47
  def parse_schema
54
48
  schema_token = expect_token(:schema)
@@ -102,11 +96,6 @@ module Kumi
102
96
  declarations
103
97
  end
104
98
 
105
- # Input declaration: 'integer :name' or 'array :items do ... end' or 'element :type, :name'
106
- #
107
- # IMPORTANT: For array nodes with a block, this sets the node's access_mode:
108
- # - :element if the block contains exactly one child introduced by `element`
109
- # - :field otherwise
110
99
  def parse_input_declaration
111
100
  type_token = current_token
112
101
  unless type_token.metadata[:category] == :type_keyword
@@ -114,94 +103,34 @@ module Kumi
114
103
  end
115
104
  advance
116
105
 
117
- # element :type, :name (syntactic sugar: the child was declared via `element`)
118
- declared_with_element = (type_token.metadata[:type_name] == :element)
119
- declared_with_index = (type_token.metadata[:type_name] == :index)
120
- if declared_with_element
121
- element_type_token = expect_token(:symbol)
122
- expect_token(:comma)
123
- name_token = expect_token(:symbol)
124
- actual_type = element_type_token.value
125
- elsif declared_with_index
126
- name_token = expect_token(:symbol)
127
- actual_type = :index
128
- else
129
- name_token = expect_token(:symbol)
130
- actual_type = type_token.metadata[:type_name]
131
- end
106
+ name_token = expect_token(:symbol)
107
+ actual_type = type_token.metadata[:type_name]
132
108
 
133
- # Optional: ', domain: ...'
134
- domain = nil
135
- if current_token.type == :comma
136
- advance
137
- if current_token.type == :identifier && current_token.value == 'domain'
138
- advance
139
- expect_token(:colon)
140
- domain = parse_domain_specification
141
- else
142
- @pos -= 1
143
- end
144
- end
109
+ domain, index_name = parse_optional_decl_kwargs
145
110
 
146
- # Parse nested declarations for block forms
147
- children = []
148
- any_element_children = false
149
- any_field_children = false
111
+ raise_parse_error('`index:` only valid on array declarations') if index_name && actual_type != :array
150
112
 
113
+ children = []
151
114
  if %i[array hash element].include?(actual_type) && current_token.type == :do
152
- advance # consume 'do'
115
+ advance
153
116
  skip_comments_and_newlines
154
-
155
117
  until %i[end eof].include?(current_token.type)
156
118
  break unless current_token.metadata[:category] == :type_keyword
157
119
 
158
- # Syntactic decision (NO counting): is this child introduced by `element`?
159
- child_is_element_keyword = (current_token.metadata[:type_name] == :element)
160
- child_is_index_keyword = (current_token.metadata[:type_name] == :index)
161
- any_element_children ||= child_is_element_keyword
162
- any_field_children ||= !child_is_element_keyword && !child_is_index_keyword
163
-
164
120
  children << parse_input_declaration
165
121
  skip_comments_and_newlines
166
122
  end
167
-
168
123
  expect_token(:end)
169
-
170
- # For array blocks, access_mode derives strictly from syntax:
171
- # - :element if ANY direct child used `element`
172
- # - :field if NONE used `element`
173
- # Mixing is invalid.
174
- if actual_type == :array
175
- if any_element_children && any_field_children
176
- raise_parse_error("array :#{name_token.value} mixes `element` and field children; choose one style")
177
- end
178
- access_mode = any_element_children ? :element : :field
179
- else
180
- access_mode = :field # objects/hashes with blocks behave like field containers
181
- end
182
- else
183
- access_mode = nil # leaves carry no access_mode
184
124
  end
185
125
 
186
- if children.empty?
187
- Kumi::Syntax::InputDeclaration.new(
188
- name_token.value,
189
- domain,
190
- actual_type,
191
- children,
192
- loc: type_token.location
193
- )
194
- else
195
- # 5th positional arg in your existing ctor is access_mode
196
- Kumi::Syntax::InputDeclaration.new(
197
- name_token.value,
198
- domain,
199
- actual_type,
200
- children,
201
- access_mode || :field,
202
- loc: type_token.location
203
- )
204
- end
126
+ Kumi::Syntax::InputDeclaration.new(
127
+ name_token.value,
128
+ domain,
129
+ actual_type,
130
+ children,
131
+ index_name, # <— NEW
132
+ loc: type_token.location
133
+ )
205
134
  end
206
135
 
207
136
  def parse_domain_specification
@@ -285,7 +214,7 @@ module Kumi
285
214
  Kumi::Syntax::ValueDeclaration.new(
286
215
  name_token.value,
287
216
  expression,
288
- hints:{inline: true},
217
+ hints: { inline: true },
289
218
  loc: let_token.location
290
219
  )
291
220
  end
@@ -352,15 +281,10 @@ module Kumi
352
281
  end
353
282
  end
354
283
 
355
- def advance_and_return_token
356
- token = current_token
357
- advance
358
- token
359
- end
360
-
361
284
  # Pratt parser for expressions
362
285
  def parse_expression(min_precedence = 0)
363
286
  left = parse_primary_expression
287
+ left = parse_postfix_chain(left)
364
288
  skip_comments_and_newlines
365
289
 
366
290
  while current_token.operator? && current_token.precedence >= min_precedence
@@ -381,40 +305,51 @@ module Kumi
381
305
  [left, right],
382
306
  loc: operator_token.location
383
307
  )
308
+ left = parse_postfix_chain(left)
384
309
  skip_comments_and_newlines
385
310
  end
386
311
 
387
312
  left
388
313
  end
389
314
 
315
+ def parse_postfix_chain(base)
316
+ skip_comments_and_newlines
317
+ while current_token.type == :lbracket
318
+ expect_token(:lbracket)
319
+ index_expr = parse_expression
320
+ expect_token(:rbracket)
321
+ base = Kumi::Syntax::CallExpression.new(:at, [base, index_expr], loc: base.loc)
322
+ skip_comments_and_newlines
323
+ end
324
+ base
325
+ end
326
+
390
327
  def parse_primary_expression
391
328
  token = current_token
392
329
 
393
330
  case token.type
394
- when :integer, :float, :string, :boolean, :constant
331
+ when :integer, :float, :string, :boolean, :constant, :symbol
395
332
  value = convert_literal_value(token)
396
333
  advance
397
334
  Kumi::Syntax::Literal.new(value, loc: token.location)
335
+
398
336
  when :function_sugar
399
337
  parse_function_sugar
400
338
 
401
339
  when :identifier
402
-
403
340
  if token.value == 'input' && peek_token.type == :dot
404
341
  parse_input_reference
405
- elsif peek_token.type == :lbracket
406
- parse_array_access_reference
342
+ elsif token.value == 'index' && peek_token.type == :lparen
343
+ parse_index_intrinsic
407
344
  else
408
345
  advance
409
346
  Kumi::Syntax::DeclarationReference.new(token.value.to_sym, loc: token.location)
410
347
  end
411
348
 
412
349
  when :input
413
- if peek_token.type == :dot
414
- parse_input_reference_from_input_token
415
- else
416
- raise_parse_error("Unexpected 'input' keyword in expression")
417
- end
350
+ return parse_input_reference_from_input_token if peek_token.type == :dot
351
+
352
+ raise_parse_error("Unexpected 'input' keyword in expression")
418
353
 
419
354
  when :lparen
420
355
  advance
@@ -429,18 +364,14 @@ module Kumi
429
364
  parse_hash_literal
430
365
 
431
366
  when :fn
432
- # expect_token(:fn)
433
367
  parse_function_call
434
368
 
435
369
  when :subtract
436
370
  advance
437
371
  skip_comments_and_newlines
438
372
  operand = parse_primary_expression
439
- Kumi::Syntax::CallExpression.new(
440
- :subtract,
441
- [Kumi::Syntax::Literal.new(0, loc: token.location), operand],
442
- loc: token.location
443
- )
373
+ Kumi::Syntax::CallExpression.new(:subtract, [Kumi::Syntax::Literal.new(0, loc: token.location), operand],
374
+ loc: token.location)
444
375
 
445
376
  when :newline, :comment
446
377
  skip_comments_and_newlines
@@ -451,16 +382,29 @@ module Kumi
451
382
  end
452
383
  end
453
384
 
385
+ def parse_index_intrinsic
386
+ start = current_token
387
+ if start.type == :index_type || (start.type == :identifier && start.value == 'index')
388
+ advance
389
+ else
390
+ raise_parse_error('Expected index(...)')
391
+ end
392
+
393
+ expect_token(:lparen)
394
+ sym = expect_token(:symbol) # :i, :j, ...
395
+ expect_token(:rparen)
396
+ Kumi::Syntax::IndexReference.new(sym.value, loc: start.location)
397
+ end
398
+
454
399
  def parse_input_reference
455
- input_token = expect_token(:identifier) # 'input'
400
+ input_token = expect_token(:identifier) # must be 'input'
401
+ raise_parse_error("Expected 'input'") unless input_token.value == 'input'
456
402
  expect_token(:dot)
457
-
458
403
  path = [expect_field_name_token.to_sym]
459
404
  while current_token.type == :dot
460
405
  advance
461
406
  path << expect_field_name_token.to_sym
462
407
  end
463
-
464
408
  if path.length == 1
465
409
  Kumi::Syntax::InputReference.new(path.first, loc: input_token.location)
466
410
  else
@@ -485,16 +429,6 @@ module Kumi
485
429
  end
486
430
  end
487
431
 
488
- def parse_array_access_reference
489
- name_token = expect_token(:identifier)
490
- expect_token(:lbracket)
491
- index_expr = parse_expression
492
- expect_token(:rbracket)
493
-
494
- base_ref = Kumi::Syntax::DeclarationReference.new(name_token.value.to_sym, loc: name_token.location)
495
- Kumi::Syntax::CallExpression.new(:at, [base_ref, index_expr], loc: name_token.location)
496
- end
497
-
498
432
  def parse_function_sugar
499
433
  sugar = current_token
500
434
  advance # e.g. shift(...)
@@ -514,71 +448,7 @@ module Kumi
514
448
  args, opts = parse_args_and_opts_inside_parens
515
449
  end
516
450
  # expect_token(:rparen)
517
- Kumi::Syntax::CallExpression.new(fn_name_token.value, args, loc: fn_name_token.location, opts: opts)
518
- end
519
-
520
- def parse_kw_literal_value
521
- t = current_token
522
- case t.type
523
- when :integer then advance
524
- t.value.delete('_').to_i
525
- when :float then advance
526
- t.value.delete('_').to_f
527
- when :string, :symbol then advance
528
- t.value
529
- when :boolean then advance
530
- t.value == 'true'
531
- when :label then advance
532
- t.value.to_sym # :wrap, :clamp, etc.
533
- when :subtract # allow negatives like -1
534
- advance
535
- v = parse_kw_literal_value
536
- raise_parse_error("numeric after unary '-'") unless v.is_a?(Numeric)
537
- -v
538
- else
539
- raise_parse_error('keyword value must be literal/label')
540
- end
541
- end
542
-
543
- def parse_args_and_opts_inside_parens
544
- args = []
545
- opts = {}
546
-
547
- # expect_token(:lparen)
548
-
549
- unless current_token.type == :rparen
550
- # --- positional args ---
551
- unless next_is_kwarg_after_comma?
552
- args << parse_expression
553
- while current_token.type == :comma && !next_is_kwarg_after_comma?
554
- advance
555
- args << parse_expression
556
- end
557
- end
558
- # --- kwargs (labels like `policy:`) ---
559
- if next_is_kwarg_after_comma?
560
- # subsequent pairs: `, label value`
561
- while current_token.type == :comma
562
- # stop if next token is not a kw key
563
- advance
564
-
565
- if current_token.type == :label
566
- key = current_token.value.to_sym
567
- advance
568
- end
569
- opts[key] = parse_kw_literal_value
570
-
571
- break unless next_is_kwarg_after_comma?
572
- end
573
- end
574
- end
575
-
576
- expect_token(:rparen)
577
- [args, opts]
578
- end
579
-
580
- def next_is_kwarg_after_comma?
581
- current_token.type == :comma && peek_token.type == :label
451
+ Kumi::Syntax::CallExpression.new(fn_name_token.value, args, opts, loc: fn_name_token.location)
582
452
  end
583
453
 
584
454
  def parse_array_literal
@@ -648,32 +518,6 @@ module Kumi
648
518
  [key, value]
649
519
  end
650
520
 
651
- def convert_literal_value(token)
652
- case token.type
653
- when :integer then token.value.gsub('_', '').to_i
654
- when :float then token.value.gsub('_', '').to_f
655
- when :string then token.value
656
- when :boolean then token.value == 'true'
657
- when :symbol then token.value.to_sym
658
- when :constant
659
- case token.value
660
- when 'Float::INFINITY' then Float::INFINITY
661
- else
662
- raise_parse_error("Unknown constant: #{token.value}")
663
- end
664
- end
665
- end
666
-
667
- def expect_field_name_token
668
- token = current_token
669
- if token.identifier? || token.keyword?
670
- advance
671
- token.value
672
- else
673
- raise_parse_error("Expected field name (identifier or keyword), got #{token.type}")
674
- end
675
- end
676
-
677
521
  def raise_parse_error(message)
678
522
  location = current_token.location
679
523
  raise Errors::ParseError.new(message, token: current_token)
@@ -686,21 +530,6 @@ module Kumi
686
530
  def wrap_condition_in_all(condition)
687
531
  Kumi::Syntax::CallExpression.new(:cascade_and, [condition], loc: condition.loc)
688
532
  end
689
-
690
- def map_operator_token_to_function_name(token_type)
691
- case token_type
692
- when :eq then :==
693
- when :ne then :!=
694
- when :gt then :>
695
- when :lt then :<
696
- when :gte then :>=
697
- when :lte then :<=
698
- when :and then :and
699
- when :or then :or
700
- when :exponent then :power
701
- else token_type
702
- end
703
- end
704
533
  end
705
534
  end
706
535
  end
@@ -0,0 +1,154 @@
1
+ module Kumi
2
+ module Parser
3
+ module Helpers
4
+ # Parses optional ", domain: ..., index: :sym" (order-agnostic, both optional)
5
+ # Cursor is right after the array/hash/type name.
6
+ def parse_optional_decl_kwargs
7
+ domain = nil
8
+ index = nil
9
+
10
+ # nothing to do
11
+ return [domain, index] unless current_token.type == :comma
12
+
13
+ # consume one or more ", key: value" pairs
14
+ while current_token.type == :comma
15
+ advance
16
+ key_tok = current_token
17
+
18
+ unless key_tok.type == :label && %w[domain index].include?(key_tok.value)
19
+ # roll back gracefully if it's not a kw pair
20
+ @pos -= 1
21
+ break
22
+ end
23
+
24
+ advance
25
+
26
+ case key_tok.value
27
+ when 'domain'
28
+ domain = parse_domain_specification
29
+ when 'index'
30
+ sym = expect_token(:symbol)
31
+ index = sym.value.to_sym
32
+ end
33
+ end
34
+
35
+ [domain, index]
36
+ end
37
+
38
+ def convert_literal_value(token)
39
+ case token.type
40
+ when :integer then token.value.gsub('_', '').to_i
41
+ when :float then token.value.gsub('_', '').to_f
42
+ when :string then token.value
43
+ when :boolean then token.value == 'true'
44
+ when :symbol then token.value.to_sym
45
+ when :constant
46
+ case token.value
47
+ when 'Float::INFINITY' then Float::INFINITY
48
+ else
49
+ raise_parse_error("Unknown constant: #{token.value}")
50
+ end
51
+ end
52
+ end
53
+
54
+ def parse_kw_literal_value
55
+ t = current_token
56
+ case t.type
57
+ when :integer then advance
58
+ t.value.delete('_').to_i
59
+ when :float then advance
60
+ t.value.delete('_').to_f
61
+ when :string, :symbol then advance
62
+ t.value
63
+ when :boolean then advance
64
+ t.value == 'true'
65
+ when :label then advance
66
+ t.value.to_sym # :wrap, :clamp, etc.
67
+ when :subtract # allow negatives like -1
68
+ advance
69
+ v = parse_kw_literal_value
70
+ raise_parse_error("numeric after unary '-'") unless v.is_a?(Numeric)
71
+ -v
72
+ else
73
+ raise_parse_error('keyword value must be literal/label')
74
+ end
75
+ end
76
+
77
+ def parse_args_and_opts_inside_parens
78
+ args = []
79
+ opts = {}
80
+
81
+ # expect_token(:lparen)
82
+
83
+ unless current_token.type == :rparen
84
+ # --- positional args ---
85
+ unless next_is_kwarg_after_comma?
86
+ args << parse_expression
87
+ while current_token.type == :comma && !next_is_kwarg_after_comma?
88
+ advance
89
+ args << parse_expression
90
+ end
91
+ end
92
+ # --- kwargs (labels like `policy:`) ---
93
+ if next_is_kwarg_after_comma?
94
+ # subsequent pairs: `, label value`
95
+ while current_token.type == :comma
96
+ # stop if next token is not a kw key
97
+ advance
98
+
99
+ if current_token.type == :label
100
+ key = current_token.value.to_sym
101
+ advance
102
+ end
103
+ opts[key] = parse_kw_literal_value
104
+
105
+ break unless next_is_kwarg_after_comma?
106
+ end
107
+ end
108
+ end
109
+
110
+ expect_token(:rparen)
111
+ [args, opts]
112
+ end
113
+
114
+ def expect_field_name_token
115
+ token = current_token
116
+ if token.identifier? || token.keyword?
117
+ advance
118
+ token.value
119
+ else
120
+ raise_parse_error("Expected field name (identifier or keyword), got #{token.type}")
121
+ end
122
+ end
123
+
124
+ def next_is_kwarg_after_comma?
125
+ current_token.type == :comma && peek_token.type == :label
126
+ end
127
+
128
+ def skip_comments_and_newlines
129
+ advance while %i[newline comment].include?(current_token.type)
130
+ end
131
+
132
+ def advance_and_return_token
133
+ token = current_token
134
+ advance
135
+ token
136
+ end
137
+
138
+ def map_operator_token_to_function_name(token_type)
139
+ case token_type
140
+ when :eq then :==
141
+ when :ne then :!=
142
+ when :gt then :>
143
+ when :lt then :<
144
+ when :gte then :>=
145
+ when :lte then :<=
146
+ when :and then :and
147
+ when :or then :or
148
+ when :exponent then :power
149
+ else token_type
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -400,7 +400,8 @@ module Kumi
400
400
  FUNCTION_SUGAR = {
401
401
  'select' => '__select__',
402
402
  'shift' => 'shift',
403
- 'roll' => 'roll'
403
+ 'roll' => 'roll',
404
+ 'index' => 'index'
404
405
  }
405
406
 
406
407
  # Keywords mapping
@@ -424,8 +425,7 @@ module Kumi
424
425
  'any' => :any_type,
425
426
  'array' => :array_type,
426
427
  'hash' => :hash_type,
427
- 'element' => :element_type,
428
- 'index' => :index_type
428
+ 'element' => :element_type
429
429
  }.freeze
430
430
 
431
431
  # 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.23'
5
+ VERSION = '0.0.25'
6
6
  end
7
7
  end
@@ -7,25 +7,25 @@ module Kumi
7
7
  # Text-based schema that extends Kumi::Schema with text parsing capabilities
8
8
  class TextSchema
9
9
  extend Kumi::Schema
10
-
10
+
11
11
  # Create a schema from text using the same pipeline as Ruby DSL
12
12
  def self.from_text(text, source_file: '<input>')
13
13
  # Parse text to AST (same as RubyParser::Dsl.build_syntax_tree)
14
- @__syntax_tree__ = Kumi::TextParser.parse(text, source_file: source_file).freeze
15
- @__analyzer_result__ = Analyzer.analyze!(@__syntax_tree__).freeze
16
- @__compiled_schema__ = Compiler.compile(@__syntax_tree__, analyzer: @__analyzer_result__).freeze
14
+ @__kumi_syntax_tree__ = Kumi::TextParser.parse(text, source_file: source_file).freeze
15
+ @__analyzer_result__ = Analyzer.analyze!(@__kumi_syntax_tree__).freeze
16
+ @__compiled_schema__ = Compiler.compile(@__kumi_syntax_tree__, analyzer: @__analyzer_result__).freeze
17
17
 
18
- Inspector.new(@__syntax_tree__, @__analyzer_result__, @__compiled_schema__)
18
+ Inspector.new(@__kumi_syntax_tree__, @__analyzer_result__, @__compiled_schema__)
19
19
  end
20
-
20
+
21
21
  # Validate text schema
22
22
  def self.valid?(text, source_file: '<input>')
23
23
  Kumi::TextParser.valid?(text, source_file: source_file)
24
24
  end
25
-
25
+
26
26
  # Get validation diagnostics
27
27
  def self.validate(text, source_file: '<input>')
28
28
  Kumi::TextParser.validate(text, source_file: source_file)
29
29
  end
30
30
  end
31
- end
31
+ 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.23
4
+ version: 0.0.25
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-10-03 00:00:00.000000000 Z
11
+ date: 2025-10-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parslet
@@ -132,6 +132,7 @@ files:
132
132
  - lib/kumi/parser/direct_parser.rb
133
133
  - lib/kumi/parser/error_extractor.rb
134
134
  - lib/kumi/parser/errors.rb
135
+ - lib/kumi/parser/helpers.rb
135
136
  - lib/kumi/parser/smart_tokenizer.rb
136
137
  - lib/kumi/parser/syntax_validator.rb
137
138
  - lib/kumi/parser/text_parser.rb