kumi-parser 0.0.3 → 0.0.4

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.
@@ -1,214 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'parslet'
4
-
5
- module Kumi
6
- module Parser
7
- module TextParser
8
- # Parslet grammar with proper arithmetic operator precedence
9
- class Grammar < Parslet::Parser
10
- # Basic tokens
11
- rule(:space) { match('\s').repeat(1) }
12
- rule(:space?) { space.maybe }
13
- rule(:newline?) { match('\n').maybe }
14
-
15
- # Comments
16
- rule(:comment) { str('#') >> match('[^\n]').repeat }
17
- rule(:ws) { (space | comment).repeat }
18
- rule(:ws?) { ws.maybe }
19
-
20
- # Identifiers and symbols
21
- rule(:identifier) { match('[a-zA-Z_]') >> match('[a-zA-Z0-9_]').repeat }
22
- rule(:symbol) { str(':') >> identifier.as(:symbol) }
23
-
24
- # Literals
25
- rule(:integer) { match('[0-9]').repeat(1) }
26
- rule(:float) { integer >> str('.') >> match('[0-9]').repeat(1) }
27
- rule(:number) { float.as(:float) | integer.as(:integer) }
28
- rule(:string_literal) do
29
- str('"') >> (str('"').absent? >> any).repeat.as(:string) >> str('"')
30
- end
31
- rule(:boolean) { (str('true').as(:true) | str('false').as(:false)) }
32
- rule(:literal) { number | string_literal | boolean }
33
-
34
- # Keywords
35
- rule(:schema_kw) { str('schema') }
36
- rule(:input_kw) { str('input') }
37
- rule(:value_kw) { str('value') }
38
- rule(:trait_kw) { str('trait') }
39
- rule(:do_kw) { str('do') }
40
- rule(:end_kw) { str('end') }
41
-
42
- # Type keywords
43
- rule(:type_name) do
44
- str('integer') | str('float') | str('string') | str('boolean') | str('any')
45
- end
46
-
47
- # Operators (ordered by precedence, highest to lowest)
48
- rule(:mult_op) { str('*').as(:multiply) | str('/').as(:divide) | str('%').as(:modulo) }
49
- rule(:add_op) { str('+').as(:add) | str('-').as(:subtract) }
50
- rule(:comp_op) do
51
- str('>=').as(:>=) | str('<=').as(:<=) | str('==').as(:==) |
52
- str('!=').as(:!=) | str('>').as(:>) | str('<').as(:<)
53
- end
54
- rule(:logical_and_op) { str('&').as(:and) }
55
- rule(:logical_or_op) { str('|').as(:or) }
56
-
57
- # Expressions with proper precedence (using left recursion elimination)
58
- rule(:primary_expr) do
59
- str('(') >> ws? >> expression >> ws? >> str(')') |
60
- function_call |
61
- input_reference |
62
- declaration_reference |
63
- literal
64
- end
65
-
66
- # Function calls: fn(:name, arg1, arg2, ...)
67
- rule(:function_call) do
68
- str('fn(') >> ws? >>
69
- symbol.as(:fn_name) >>
70
- (str(',') >> ws? >> expression).repeat(0).as(:args) >>
71
- ws? >> str(')')
72
- end
73
-
74
- # Multiplication/Division (left-associative)
75
- rule(:mult_expr) do
76
- primary_expr.as(:left) >>
77
- (space? >> mult_op.as(:op) >> space? >> primary_expr.as(:right)).repeat.as(:ops)
78
- end
79
-
80
- # Addition/Subtraction (left-associative)
81
- rule(:add_expr) do
82
- mult_expr.as(:left) >>
83
- (space? >> add_op.as(:op) >> space? >> mult_expr.as(:right)).repeat.as(:ops)
84
- end
85
-
86
- # Comparison operators
87
- rule(:comp_expr) do
88
- add_expr.as(:left) >>
89
- (space? >> comp_op.as(:op) >> space? >> add_expr.as(:right)).maybe.as(:comp)
90
- end
91
-
92
- # Logical AND (higher precedence than OR)
93
- rule(:logical_and_expr) do
94
- comp_expr.as(:left) >>
95
- (space? >> logical_and_op.as(:op) >> space? >> comp_expr.as(:right)).repeat.as(:ops)
96
- end
97
-
98
- # Logical OR (lowest precedence)
99
- rule(:logical_or_expr) do
100
- logical_and_expr.as(:left) >>
101
- (space? >> logical_or_op.as(:op) >> space? >> logical_and_expr.as(:right)).repeat.as(:ops)
102
- end
103
-
104
- rule(:expression) { logical_or_expr }
105
-
106
- # Input references: input.field or input.field.subfield
107
- rule(:input_reference) do
108
- str('input.') >> input_path.as(:input_ref)
109
- end
110
-
111
- rule(:input_path) do
112
- identifier >> (str('.') >> identifier).repeat
113
- end
114
-
115
- # Declaration references: just identifier
116
- rule(:declaration_reference) do
117
- identifier.as(:decl_ref)
118
- end
119
-
120
- # Input declarations
121
- rule(:input_declaration) do
122
- nested_array_declaration | simple_input_declaration
123
- end
124
-
125
- rule(:simple_input_declaration) do
126
- ws? >> type_name.as(:type) >> space >> symbol.as(:name) >>
127
- (str(',') >> ws? >> domain_spec).maybe.as(:domain) >> ws? >> newline?
128
- end
129
-
130
- rule(:nested_array_declaration) do
131
- ws? >> str('array') >> space >> symbol.as(:name) >> space >> do_kw >> ws? >> newline? >>
132
- (ws? >> input_declaration >> ws?).repeat.as(:nested_fields) >>
133
- ws? >> end_kw >> ws? >> newline?
134
- end
135
-
136
- rule(:domain_spec) do
137
- str('domain:') >> ws? >> domain_value.as(:domain_value)
138
- end
139
-
140
- rule(:domain_value) do
141
- # Ranges: 1..10, 1...10, 0.0..100.0
142
- range_value |
143
- # Word arrays: %w[active inactive]
144
- word_array_value |
145
- # String arrays: ["active", "inactive"]
146
- string_array_value
147
- end
148
-
149
- rule(:range_value) do
150
- (float | integer) >> str('..') >> (float | integer)
151
- end
152
-
153
- rule(:word_array_value) do
154
- str('%w[') >> (identifier >> space?).repeat.as(:words) >> str(']')
155
- end
156
-
157
- rule(:string_array_value) do
158
- str('[') >> space? >>
159
- (string_literal >> (str(',') >> space? >> string_literal).repeat).maybe >>
160
- space? >> str(']')
161
- end
162
-
163
- # Value declarations
164
- rule(:value_declaration) do
165
- cascade_value_declaration | simple_value_declaration
166
- end
167
-
168
- rule(:simple_value_declaration) do
169
- ws? >> value_kw.as(:type) >> space >> symbol.as(:name) >> str(',') >> ws? >>
170
- expression.as(:expr) >> ws? >> newline?
171
- end
172
-
173
- rule(:cascade_value_declaration) do
174
- ws? >> value_kw.as(:type) >> space >> symbol.as(:name) >> space >> do_kw >> ws? >> newline? >>
175
- (ws? >> cascade_case >> ws?).repeat.as(:cases) >>
176
- ws? >> end_kw >> ws? >> newline?
177
- end
178
-
179
- rule(:cascade_case) do
180
- (ws? >> str('on') >> space >> identifier.as(:condition) >> str(',') >> ws? >>
181
- expression.as(:result) >> ws? >> newline?) |
182
- (ws? >> str('base') >> space >> expression.as(:base_result) >> ws? >> newline?)
183
- end
184
-
185
- # Trait declarations
186
- rule(:trait_declaration) do
187
- ws? >> trait_kw.as(:type) >> space >> symbol.as(:name) >> str(',') >> ws? >>
188
- expression.as(:expr) >> ws? >> newline?
189
- end
190
-
191
- # Input block
192
- rule(:input_block) do
193
- ws? >> input_kw >> space >> do_kw >> ws? >> newline? >>
194
- (ws? >> input_declaration >> ws?).repeat.as(:declarations) >>
195
- ws? >> end_kw >> ws? >> newline?
196
- end
197
-
198
- # Schema structure
199
- rule(:schema_body) do
200
- input_block.as(:input) >>
201
- (ws? >> (value_declaration | trait_declaration) >> ws?).repeat.as(:declarations)
202
- end
203
-
204
- rule(:schema) do
205
- ws? >> schema_kw >> space >> do_kw >> ws? >> newline? >>
206
- schema_body >>
207
- ws? >> end_kw >> ws?
208
- end
209
-
210
- root(:schema)
211
- end
212
- end
213
- end
214
- end
@@ -1,168 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'grammar'
4
- require_relative 'transform'
5
- require 'kumi/core/error_reporting'
6
-
7
- module Kumi
8
- module Parser
9
- module TextParser
10
- # Parslet-based parser with proper arithmetic operator precedence
11
- class Parser
12
- include Kumi::Core::ErrorReporting
13
-
14
- def initialize
15
- @grammar = Grammar.new
16
- @transform = Transform.new
17
- end
18
-
19
- def parse(text_dsl, source_file: '<parslet_parser>')
20
- # Parse with Parslet grammar
21
- parse_tree = @grammar.parse(text_dsl)
22
-
23
- # Transform to AST
24
- ast = @transform.apply(parse_tree)
25
-
26
- # Post-process to create final Root node if needed
27
- post_process(ast)
28
- rescue Parslet::ParseFailed => e
29
- raise_syntax_error(
30
- "Parse error: #{e.parse_failure_cause.ascii_tree}",
31
- location: create_location(source_file, 1, 1)
32
- )
33
- end
34
-
35
- private
36
-
37
- def post_process(ast)
38
- # If it's already a Root node, return it
39
- return ast if ast.is_a?(Syntax::Root)
40
-
41
- # If it's a hash with input and declarations, convert it
42
- if ast.is_a?(Hash) && ast[:input] && ast[:declarations]
43
- input_decls = ast[:input][:declarations] || []
44
- input_decls = [input_decls] unless input_decls.is_a?(Array)
45
-
46
- # Process input declarations
47
- processed_input_decls = input_decls.map do |input_decl|
48
- process_input_declaration(input_decl)
49
- end
50
-
51
- other_decls = ast[:declarations] || []
52
- other_decls = [other_decls] unless other_decls.is_a?(Array)
53
-
54
- # Convert remaining hash values to proper nodes
55
- values = []
56
- traits = []
57
-
58
- other_decls.each do |decl|
59
- if decl.is_a?(Hash) && decl[:name] && decl[:expr]
60
- expr = process_expression(decl[:expr])
61
- values << Syntax::ValueDeclaration.new(decl[:name], expr, loc: create_location('<parslet>', 1, 1))
62
- elsif decl.is_a?(Syntax::ValueDeclaration)
63
- # Need to process the expression if it's still a hash
64
- if decl.expression.is_a?(Hash)
65
- processed_expr = process_expression(decl.expression)
66
- values << Syntax::ValueDeclaration.new(decl.name, processed_expr, loc: decl.loc)
67
- else
68
- values << decl
69
- end
70
- elsif decl.is_a?(Syntax::TraitDeclaration)
71
- traits << decl
72
- end
73
- end
74
-
75
- Syntax::Root.new(processed_input_decls, values, traits, loc: create_location('<parslet>', 1, 1))
76
- else
77
- ast
78
- end
79
- end
80
-
81
- def process_expression(expr)
82
- # If it's already a proper AST node, return it
83
- return expr unless expr.is_a?(Hash)
84
-
85
- # Handle nested expression structures
86
- if expr[:left] && expr[:ops]
87
- left_expr = process_expression(expr[:left])
88
- ops = expr[:ops] || []
89
- ops = [ops] unless ops.is_a?(Array)
90
-
91
- result = ops.inject(left_expr) do |left, op|
92
- if op.is_a?(Hash) && op[:op] && op[:right]
93
- op_name = op[:op].keys.first
94
- right_expr = process_expression(op[:right])
95
- Syntax::CallExpression.new(op_name, [left, right_expr], loc: create_location('<parslet>', 1, 1))
96
- else
97
- left
98
- end
99
- end
100
-
101
- # Handle comparison
102
- if expr[:comp] && expr[:comp][:op] && expr[:comp][:right]
103
- comp = expr[:comp]
104
- op_name = comp[:op].keys.first
105
- right_expr = process_expression(comp[:right])
106
- Syntax::CallExpression.new(op_name, [result, right_expr], loc: create_location('<parslet>', 1, 1))
107
- else
108
- result
109
- end
110
- else
111
- expr
112
- end
113
- end
114
-
115
- def process_input_declaration(input_decl)
116
- return input_decl if input_decl.is_a?(Syntax::InputDeclaration)
117
-
118
- if input_decl.is_a?(Hash)
119
- if input_decl[:nested_fields]
120
- # Array input declaration
121
- nested_fields = input_decl[:nested_fields] || []
122
- nested_fields = [nested_fields] unless nested_fields.is_a?(Array)
123
-
124
- processed_fields = nested_fields.map do |field|
125
- if field.is_a?(Hash) && field[:type] && field[:name]
126
- Syntax::InputDeclaration.new(
127
- field[:name],
128
- field[:domain],
129
- field[:type].to_sym,
130
- [],
131
- loc: create_location('<parslet>', 1, 1)
132
- )
133
- else
134
- field
135
- end
136
- end
137
-
138
- Syntax::InputDeclaration.new(
139
- input_decl[:name],
140
- nil,
141
- :array,
142
- processed_fields,
143
- loc: create_location('<parslet>', 1, 1)
144
- )
145
- elsif input_decl[:type] && input_decl[:name]
146
- # Simple input declaration
147
- Syntax::InputDeclaration.new(
148
- input_decl[:name],
149
- input_decl[:domain],
150
- input_decl[:type].to_sym,
151
- [],
152
- loc: create_location('<parslet>', 1, 1)
153
- )
154
- else
155
- input_decl
156
- end
157
- else
158
- input_decl
159
- end
160
- end
161
-
162
- def create_location(file, line, column)
163
- Syntax::Location.new(file: file, line: line, column: column)
164
- end
165
- end
166
- end
167
- end
168
- end
@@ -1,170 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'parslet'
4
- require 'kumi/syntax/node'
5
- require 'kumi/syntax/root'
6
- require 'kumi/syntax/input_declaration'
7
- require 'kumi/syntax/value_declaration'
8
- require 'kumi/syntax/trait_declaration'
9
- require 'kumi/syntax/call_expression'
10
- require 'kumi/syntax/input_reference'
11
- require 'kumi/syntax/input_element_reference'
12
- require 'kumi/syntax/declaration_reference'
13
- require 'kumi/syntax/literal'
14
-
15
- module Kumi
16
- module Parser
17
- module TextParser
18
- class Transform < Parslet::Transform
19
- LOC = Kumi::Syntax::Location.new(file: '<parslet_parser>', line: 1, column: 1)
20
-
21
- # Literals
22
- rule(integer: simple(:x)) { Kumi::Syntax::Literal.new(x.to_i, loc: LOC) }
23
- rule(float: simple(:x)) { Kumi::Syntax::Literal.new(x.to_f, loc: LOC) }
24
- rule(string: simple(:x)) { Kumi::Syntax::Literal.new(x.to_s, loc: LOC) }
25
- rule(true: simple(:_)) { Kumi::Syntax::Literal.new(true, loc: LOC) }
26
- rule(false: simple(:_)) { Kumi::Syntax::Literal.new(false, loc: LOC) }
27
-
28
- # Symbols
29
- rule(symbol: simple(:name)) { name.to_sym }
30
-
31
- # Input and declaration references
32
- rule(input_ref: simple(:path)) do
33
- # Handle multi-level paths like "items.price"
34
- path_parts = path.to_s.split('.')
35
- if path_parts.length == 1
36
- Kumi::Syntax::InputReference.new(path_parts[0].to_sym, loc: LOC)
37
- else
38
- Kumi::Syntax::InputElementReference.new(path_parts.map(&:to_sym), loc: LOC)
39
- end
40
- end
41
-
42
- rule(decl_ref: simple(:name)) { Kumi::Syntax::DeclarationReference.new(name.to_sym, loc: LOC) }
43
-
44
- # Function calls
45
- rule(fn_name: simple(:name), args: sequence(:args)) do
46
- Kumi::Syntax::CallExpression.new(name, args, loc: LOC)
47
- end
48
-
49
- rule(fn_name: simple(:name), args: []) do
50
- Kumi::Syntax::CallExpression.new(name, [], loc: LOC)
51
- end
52
-
53
- # Arithmetic expressions with left-associativity
54
- rule(left: simple(:l), ops: sequence(:operations)) do
55
- operations.inject(l) do |left_expr, op|
56
- op_name = op[:op].keys.first
57
- Kumi::Syntax::CallExpression.new(op_name, [left_expr, op[:right]], loc: LOC)
58
- end
59
- end
60
-
61
- rule(left: simple(:l), ops: []) { l }
62
-
63
- # Comparison expressions
64
- rule(left: simple(:l), comp: simple(:comparison)) do
65
- if comparison && comparison[:op] && comparison[:right]
66
- op_name = comparison[:op].keys.first
67
- Kumi::Syntax::CallExpression.new(op_name, [l, comparison[:right]], loc: LOC)
68
- else
69
- l
70
- end
71
- end
72
-
73
- rule(left: simple(:l), comp: nil) { l }
74
-
75
- # Simple input declarations
76
- rule(type: simple(:type), name: simple(:name)) do
77
- Kumi::Syntax::InputDeclaration.new(name, nil, type.to_sym, [], loc: LOC)
78
- end
79
-
80
- # Nested array declarations
81
- rule(name: simple(:name), nested_fields: sequence(:fields)) do
82
- # Transform nested field hashes to InputDeclaration objects
83
- transformed_fields = fields.map do |field|
84
- if field.is_a?(Hash) && field[:type] && field[:name]
85
- Kumi::Syntax::InputDeclaration.new(
86
- field[:name],
87
- field[:domain],
88
- field[:type].to_sym,
89
- [],
90
- loc: LOC
91
- )
92
- else
93
- field
94
- end
95
- end
96
-
97
- # Create an array input declaration with nested fields
98
- Kumi::Syntax::InputDeclaration.new(
99
- name,
100
- nil,
101
- :array,
102
- transformed_fields,
103
- loc: LOC
104
- )
105
- end
106
-
107
- rule(name: simple(:name), nested_fields: simple(:field)) do
108
- # Single nested field case
109
- transformed_field = if field.is_a?(Hash) && field[:type] && field[:name]
110
- Kumi::Syntax::InputDeclaration.new(
111
- field[:name],
112
- field[:domain],
113
- field[:type].to_sym,
114
- [],
115
- loc: LOC
116
- )
117
- else
118
- field
119
- end
120
-
121
- Kumi::Syntax::InputDeclaration.new(
122
- name,
123
- nil,
124
- :array,
125
- [transformed_field],
126
- loc: LOC
127
- )
128
- end
129
-
130
- rule(type: simple(:type), name: simple(:name), expr: simple(:expr)) do
131
- # Differentiate between value and trait declarations based on type
132
- if type.to_s == 'value'
133
- Kumi::Syntax::ValueDeclaration.new(name, expr, loc: LOC)
134
- elsif type.to_s == 'trait'
135
- Kumi::Syntax::TraitDeclaration.new(name, expr, loc: LOC)
136
- else
137
- # Fallback - shouldn't happen
138
- Kumi::Syntax::ValueDeclaration.new(name, expr, loc: LOC)
139
- end
140
- end
141
-
142
- # Handle the intermediate case before the expression gets fully transformed
143
- rule(type: simple(:type), name: { symbol: simple(:name) }, expr: simple(:expr)) do
144
- # Differentiate between value and trait declarations based on type
145
- if type.to_s == 'value'
146
- Kumi::Syntax::ValueDeclaration.new(name.to_sym, expr, loc: LOC)
147
- elsif type.to_s == 'trait'
148
- Kumi::Syntax::TraitDeclaration.new(name.to_sym, expr, loc: LOC)
149
- else
150
- # Fallback - shouldn't happen
151
- Kumi::Syntax::ValueDeclaration.new(name.to_sym, expr, loc: LOC)
152
- end
153
- end
154
-
155
- # Schema structure - convert the hash to Root node
156
- rule(input: { declarations: sequence(:input_decls) }, declarations: sequence(:other_decls)) do
157
- values = other_decls.select { |d| d.is_a?(Kumi::Syntax::ValueDeclaration) }
158
- traits = other_decls.select { |d| d.is_a?(Kumi::Syntax::TraitDeclaration) }
159
- Kumi::Syntax::Root.new(input_decls, values, traits, loc: LOC)
160
- end
161
-
162
- rule(input: { declarations: simple(:input_decl) }, declarations: sequence(:other_decls)) do
163
- values = other_decls.select { |d| d.is_a?(Kumi::Syntax::ValueDeclaration) }
164
- traits = other_decls.select { |d| d.is_a?(Kumi::Syntax::TraitDeclaration) }
165
- Kumi::Syntax::Root.new([input_decl], values, traits, loc: LOC)
166
- end
167
- end
168
- end
169
- end
170
- end
data/lib/kumi/parser.rb DELETED
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kumi
4
- module Parser
5
- # This namespace will contain TextParser and related classes
6
- # All classes will be autoloaded by Zeitwerk
7
- end
8
- end
data/test_basic.rb DELETED
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # Basic functionality test for kumi-parser
3
-
4
- require_relative 'lib/kumi/parser'
5
-
6
- schema_text = <<~SCHEMA
7
- schema do
8
- input do
9
- integer :age
10
- end
11
- #{' '}
12
- trait :adult, input.age >= 18
13
- value :bonus, 100
14
- end
15
- SCHEMA
16
-
17
- puts 'Testing kumi-parser...'
18
- puts '=' * 40
19
-
20
- begin
21
- # Test validation
22
- puts '1. Testing validation...'
23
- diagnostics = Kumi::Parser::TextParser.validate(schema_text)
24
- puts " ✅ Validation: #{diagnostics.empty? ? 'PASSED' : 'FAILED'}"
25
-
26
- unless diagnostics.empty?
27
- diagnostics.to_a.each do |d|
28
- puts " Error: Line #{d.line}, Column #{d.column}: #{d.message}"
29
- end
30
- end
31
-
32
- # Test parsing if validation passed
33
- if diagnostics.empty?
34
- puts '2. Testing parsing...'
35
- ast = Kumi::Parser::TextParser.parse(schema_text)
36
- puts " ✅ Parsing: #{ast ? 'PASSED' : 'FAILED'}"
37
- puts " AST type: #{ast.class}"
38
- end
39
-
40
- puts "\n🎉 Basic functionality test completed!"
41
- rescue StandardError => e
42
- puts "❌ Error: #{e.message}"
43
- puts e.backtrace.first(5)
44
- end