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 +4 -4
- data/CLAUDE.md +1 -1
- data/lib/kumi/parser/direct_parser.rb +57 -228
- data/lib/kumi/parser/helpers.rb +154 -0
- data/lib/kumi/parser/token_metadata.rb +3 -3
- data/lib/kumi/parser/version.rb +1 -1
- data/lib/kumi/text_schema.rb +8 -8
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 40a328f8314da0cfd5cb058b439f60ec892fe8c048dab561f8bbbc0bd073094a
|
|
4
|
+
data.tar.gz: ff549aea18eecf1b4fee4855bdf6bfafa34d7b5cd08a39a9f570222a06b35770
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c146082ac76579bd8476547175f099c2c5f193f56d3d4ae9cea7fed370a84c48a77fe7a5566b4eae79bc732d3b2a58838c75f96a3c8103bbe3ab0451b1f745c1
|
|
7
|
+
data.tar.gz: 6a98319c87b3069a23908e32748c4d8ee7a03c134d422897043537e893df59c64bbb125eae9d451579d04c69067eef61a8f4e8cd71b67cc70d28153de5fe4ba4
|
data/CLAUDE.md
CHANGED
|
@@ -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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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 == :
|
|
406
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
|
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
|
data/lib/kumi/parser/version.rb
CHANGED
data/lib/kumi/text_schema.rb
CHANGED
|
@@ -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
|
-
@
|
|
15
|
-
@__analyzer_result__ = Analyzer.analyze!(@
|
|
16
|
-
@__compiled_schema__ = Compiler.compile(@
|
|
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(@
|
|
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.
|
|
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-
|
|
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
|