t-ruby 0.0.41 → 0.0.43
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/lib/t_ruby/ast_type_inferrer.rb +2 -0
- data/lib/t_ruby/cache.rb +40 -10
- data/lib/t_ruby/cli.rb +14 -9
- data/lib/t_ruby/code_emitter.rb +254 -0
- data/lib/t_ruby/compiler.rb +186 -3
- data/lib/t_ruby/config.rb +18 -3
- data/lib/t_ruby/diagnostic.rb +115 -0
- data/lib/t_ruby/diagnostic_formatter.rb +162 -0
- data/lib/t_ruby/error_handler.rb +201 -35
- data/lib/t_ruby/error_reporter.rb +57 -0
- data/lib/t_ruby/ir.rb +39 -1
- data/lib/t_ruby/lsp_server.rb +40 -97
- data/lib/t_ruby/parser.rb +18 -4
- data/lib/t_ruby/parser_combinator/combinators/alternative.rb +20 -0
- data/lib/t_ruby/parser_combinator/combinators/chain_left.rb +34 -0
- data/lib/t_ruby/parser_combinator/combinators/choice.rb +29 -0
- data/lib/t_ruby/parser_combinator/combinators/flat_map.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/label.rb +22 -0
- data/lib/t_ruby/parser_combinator/combinators/lookahead.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/many.rb +29 -0
- data/lib/t_ruby/parser_combinator/combinators/many1.rb +32 -0
- data/lib/t_ruby/parser_combinator/combinators/map.rb +17 -0
- data/lib/t_ruby/parser_combinator/combinators/not_followed_by.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/optional.rb +21 -0
- data/lib/t_ruby/parser_combinator/combinators/sep_by.rb +34 -0
- data/lib/t_ruby/parser_combinator/combinators/sep_by1.rb +34 -0
- data/lib/t_ruby/parser_combinator/combinators/sequence.rb +23 -0
- data/lib/t_ruby/parser_combinator/combinators/skip_right.rb +23 -0
- data/lib/t_ruby/parser_combinator/declaration_parser.rb +147 -0
- data/lib/t_ruby/parser_combinator/dsl.rb +115 -0
- data/lib/t_ruby/parser_combinator/parse_error.rb +48 -0
- data/lib/t_ruby/parser_combinator/parse_result.rb +46 -0
- data/lib/t_ruby/parser_combinator/parser.rb +84 -0
- data/lib/t_ruby/parser_combinator/primitives/end_of_input.rb +16 -0
- data/lib/t_ruby/parser_combinator/primitives/fail.rb +16 -0
- data/lib/t_ruby/parser_combinator/primitives/lazy.rb +18 -0
- data/lib/t_ruby/parser_combinator/primitives/literal.rb +21 -0
- data/lib/t_ruby/parser_combinator/primitives/pure.rb +16 -0
- data/lib/t_ruby/parser_combinator/primitives/regex.rb +25 -0
- data/lib/t_ruby/parser_combinator/primitives/satisfy.rb +21 -0
- data/lib/t_ruby/parser_combinator/token/expression_parser.rb +541 -0
- data/lib/t_ruby/parser_combinator/token/statement_parser.rb +644 -0
- data/lib/t_ruby/parser_combinator/token/token_alternative.rb +20 -0
- data/lib/t_ruby/parser_combinator/token/token_body_parser.rb +54 -0
- data/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb +920 -0
- data/lib/t_ruby/parser_combinator/token/token_dsl.rb +16 -0
- data/lib/t_ruby/parser_combinator/token/token_label.rb +22 -0
- data/lib/t_ruby/parser_combinator/token/token_many.rb +29 -0
- data/lib/t_ruby/parser_combinator/token/token_many1.rb +32 -0
- data/lib/t_ruby/parser_combinator/token/token_map.rb +17 -0
- data/lib/t_ruby/parser_combinator/token/token_matcher.rb +29 -0
- data/lib/t_ruby/parser_combinator/token/token_optional.rb +21 -0
- data/lib/t_ruby/parser_combinator/token/token_parse_result.rb +40 -0
- data/lib/t_ruby/parser_combinator/token/token_parser.rb +62 -0
- data/lib/t_ruby/parser_combinator/token/token_sep_by.rb +34 -0
- data/lib/t_ruby/parser_combinator/token/token_sep_by1.rb +34 -0
- data/lib/t_ruby/parser_combinator/token/token_sequence.rb +23 -0
- data/lib/t_ruby/parser_combinator/token/token_skip_right.rb +23 -0
- data/lib/t_ruby/parser_combinator/type_parser.rb +103 -0
- data/lib/t_ruby/parser_combinator.rb +64 -936
- data/lib/t_ruby/ruby_version.rb +112 -0
- data/lib/t_ruby/scanner.rb +883 -0
- data/lib/t_ruby/version.rb +1 -1
- data/lib/t_ruby/watcher.rb +83 -76
- data/lib/t_ruby.rb +17 -1
- metadata +58 -7
- data/lib/t_ruby/body_parser.rb +0 -561
data/lib/t_ruby/ir.rb
CHANGED
|
@@ -234,6 +234,20 @@ module TRuby
|
|
|
234
234
|
end
|
|
235
235
|
end
|
|
236
236
|
|
|
237
|
+
# Interpolated string (string with #{...} expressions)
|
|
238
|
+
class InterpolatedString < Node
|
|
239
|
+
attr_accessor :parts
|
|
240
|
+
|
|
241
|
+
def initialize(parts: [], **opts)
|
|
242
|
+
super(**opts)
|
|
243
|
+
@parts = parts
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def children
|
|
247
|
+
@parts
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
237
251
|
# Array literal
|
|
238
252
|
class ArrayLiteral < Node
|
|
239
253
|
attr_accessor :elements, :element_type
|
|
@@ -658,6 +672,26 @@ module TRuby
|
|
|
658
672
|
end
|
|
659
673
|
end
|
|
660
674
|
|
|
675
|
+
# Hash literal type: { key: Type, key2: Type }
|
|
676
|
+
class HashLiteralType < TypeNode
|
|
677
|
+
attr_accessor :fields # Array of { name: String, type: TypeNode }
|
|
678
|
+
|
|
679
|
+
def initialize(fields:, **opts)
|
|
680
|
+
super(**opts)
|
|
681
|
+
@fields = fields
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def to_rbs
|
|
685
|
+
# Hash literal types in RBS are represented as Hash[Symbol, untyped] or specific record types
|
|
686
|
+
"Hash[Symbol, untyped]"
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def to_trb
|
|
690
|
+
field_strs = @fields.map { |f| "#{f[:name]}: #{f[:type].to_trb}" }
|
|
691
|
+
"{ #{field_strs.join(", ")} }"
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
661
695
|
#==========================================================================
|
|
662
696
|
# Visitor Pattern
|
|
663
697
|
#==========================================================================
|
|
@@ -778,12 +812,16 @@ module TRuby
|
|
|
778
812
|
# 본문 IR이 있으면 사용 (BodyParser에서 파싱됨)
|
|
779
813
|
body = info[:body_ir]
|
|
780
814
|
|
|
815
|
+
# Build location string from line/column info
|
|
816
|
+
location = info[:line] && info[:column] ? "#{info[:line]}:#{info[:column]}" : nil
|
|
817
|
+
|
|
781
818
|
MethodDef.new(
|
|
782
819
|
name: info[:name],
|
|
783
820
|
params: params,
|
|
784
821
|
return_type: info[:return_type] ? parse_type(info[:return_type]) : nil,
|
|
785
822
|
body: body,
|
|
786
|
-
visibility: info[:visibility] || :public
|
|
823
|
+
visibility: info[:visibility] || :public,
|
|
824
|
+
location: location
|
|
787
825
|
)
|
|
788
826
|
end
|
|
789
827
|
|
data/lib/t_ruby/lsp_server.rb
CHANGED
|
@@ -122,6 +122,8 @@ module TRuby
|
|
|
122
122
|
@initialized = false
|
|
123
123
|
@shutdown_requested = false
|
|
124
124
|
@type_alias_registry = TypeAliasRegistry.new
|
|
125
|
+
# Use Compiler for unified diagnostics (same as CLI)
|
|
126
|
+
@compiler = Compiler.new
|
|
125
127
|
end
|
|
126
128
|
|
|
127
129
|
# Main run loop for the LSP server
|
|
@@ -368,114 +370,55 @@ module TRuby
|
|
|
368
370
|
})
|
|
369
371
|
end
|
|
370
372
|
|
|
371
|
-
def analyze_document(text)
|
|
372
|
-
|
|
373
|
+
def analyze_document(text, uri: nil)
|
|
374
|
+
# Use unified Compiler.analyze for diagnostics
|
|
375
|
+
# This ensures CLI and LSP show the same errors
|
|
376
|
+
file_path = uri ? uri_to_path(uri) : "<source>"
|
|
377
|
+
compiler_diagnostics = @compiler.analyze(text, file: file_path)
|
|
373
378
|
|
|
374
|
-
#
|
|
375
|
-
|
|
376
|
-
errors = error_handler.check
|
|
377
|
-
|
|
378
|
-
errors.each do |error|
|
|
379
|
-
# Parse line number from error message
|
|
380
|
-
next unless error =~ /^Line (\d+):\s*(.+)$/
|
|
381
|
-
|
|
382
|
-
line_num = Regexp.last_match(1).to_i - 1 # LSP uses 0-based line numbers
|
|
383
|
-
message = Regexp.last_match(2)
|
|
384
|
-
|
|
385
|
-
diagnostics << create_diagnostic(line_num, message, DiagnosticSeverity::ERROR)
|
|
386
|
-
end
|
|
387
|
-
|
|
388
|
-
# Additional validation using Parser
|
|
389
|
-
begin
|
|
390
|
-
parser = Parser.new(text)
|
|
391
|
-
result = parser.parse
|
|
392
|
-
|
|
393
|
-
# Validate type aliases
|
|
394
|
-
validate_type_aliases(result[:type_aliases] || [], diagnostics, text)
|
|
395
|
-
|
|
396
|
-
# Validate function types
|
|
397
|
-
validate_functions(result[:functions] || [], diagnostics, text)
|
|
398
|
-
rescue StandardError => e
|
|
399
|
-
diagnostics << create_diagnostic(0, "Parse error: #{e.message}", DiagnosticSeverity::ERROR)
|
|
400
|
-
end
|
|
401
|
-
|
|
402
|
-
diagnostics
|
|
379
|
+
# Convert TRuby::Diagnostic objects to LSP diagnostic format
|
|
380
|
+
compiler_diagnostics.map { |d| diagnostic_to_lsp(d) }
|
|
403
381
|
end
|
|
404
382
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
line_num = find_line_number(lines, /^\s*type\s+#{Regexp.escape(alias_info[:name])}\s*=/)
|
|
411
|
-
next unless line_num
|
|
412
|
-
|
|
413
|
-
begin
|
|
414
|
-
registry.register(alias_info[:name], alias_info[:definition])
|
|
415
|
-
rescue DuplicateTypeAliasError => e
|
|
416
|
-
diagnostics << create_diagnostic(line_num, e.message, DiagnosticSeverity::ERROR)
|
|
417
|
-
rescue CircularTypeAliasError => e
|
|
418
|
-
diagnostics << create_diagnostic(line_num, e.message, DiagnosticSeverity::ERROR)
|
|
419
|
-
end
|
|
420
|
-
end
|
|
421
|
-
end
|
|
383
|
+
# Convert TRuby::Diagnostic to LSP diagnostic format
|
|
384
|
+
def diagnostic_to_lsp(diagnostic)
|
|
385
|
+
# LSP uses 0-based line numbers
|
|
386
|
+
line = (diagnostic.line || 1) - 1
|
|
387
|
+
line = 0 if line.negative?
|
|
422
388
|
|
|
423
|
-
|
|
424
|
-
|
|
389
|
+
col = (diagnostic.column || 1) - 1
|
|
390
|
+
col = 0 if col.negative?
|
|
425
391
|
|
|
426
|
-
|
|
427
|
-
line_num = find_line_number(lines, /^\s*def\s+#{Regexp.escape(func[:name])}\s*\(/)
|
|
428
|
-
next unless line_num
|
|
429
|
-
|
|
430
|
-
# Validate return type
|
|
431
|
-
if func[:return_type] && !valid_type?(func[:return_type])
|
|
432
|
-
diagnostics << create_diagnostic(
|
|
433
|
-
line_num,
|
|
434
|
-
"Unknown return type '#{func[:return_type]}'",
|
|
435
|
-
DiagnosticSeverity::WARNING
|
|
436
|
-
)
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
# Validate parameter types
|
|
440
|
-
func[:params]&.each do |param|
|
|
441
|
-
next unless param[:type] && !valid_type?(param[:type])
|
|
442
|
-
|
|
443
|
-
diagnostics << create_diagnostic(
|
|
444
|
-
line_num,
|
|
445
|
-
"Unknown parameter type '#{param[:type]}' for '#{param[:name]}'",
|
|
446
|
-
DiagnosticSeverity::WARNING
|
|
447
|
-
)
|
|
448
|
-
end
|
|
449
|
-
end
|
|
450
|
-
end
|
|
392
|
+
end_col = diagnostic.end_column ? diagnostic.end_column - 1 : col + 1
|
|
451
393
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
394
|
+
severity = case diagnostic.severity
|
|
395
|
+
when :error then DiagnosticSeverity::ERROR
|
|
396
|
+
when :warning then DiagnosticSeverity::WARNING
|
|
397
|
+
when :info then DiagnosticSeverity::INFORMATION
|
|
398
|
+
else DiagnosticSeverity::ERROR
|
|
399
|
+
end
|
|
458
400
|
|
|
459
|
-
|
|
460
|
-
|
|
401
|
+
lsp_diag = {
|
|
402
|
+
"range" => {
|
|
403
|
+
"start" => { "line" => line, "character" => col },
|
|
404
|
+
"end" => { "line" => line, "character" => end_col },
|
|
405
|
+
},
|
|
406
|
+
"severity" => severity,
|
|
407
|
+
"source" => "t-ruby",
|
|
408
|
+
"message" => diagnostic.message,
|
|
409
|
+
}
|
|
461
410
|
|
|
462
|
-
#
|
|
463
|
-
if
|
|
464
|
-
return type_str.split("|").map(&:strip).all? { |t| valid_type?(t) }
|
|
465
|
-
end
|
|
411
|
+
# Add error code if available
|
|
412
|
+
lsp_diag["code"] = diagnostic.code if diagnostic.code
|
|
466
413
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
return type_str.split("&").map(&:strip).all? { |t| valid_type?(t) }
|
|
470
|
-
end
|
|
414
|
+
lsp_diag
|
|
415
|
+
end
|
|
471
416
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
return BUILT_IN_TYPES.include?(base_type) || @type_alias_registry.valid_type?(base_type)
|
|
476
|
-
end
|
|
417
|
+
def uri_to_path(uri)
|
|
418
|
+
# Convert file:// URI to filesystem path
|
|
419
|
+
return uri unless uri.start_with?("file://")
|
|
477
420
|
|
|
478
|
-
|
|
421
|
+
uri.sub(%r{^file://}, "")
|
|
479
422
|
end
|
|
480
423
|
|
|
481
424
|
def create_diagnostic(line, message, severity)
|
data/lib/t_ruby/parser.rb
CHANGED
|
@@ -15,6 +15,9 @@ module TRuby
|
|
|
15
15
|
# Visibility modifiers for method definitions
|
|
16
16
|
VISIBILITY_PATTERN = '(?:(?:private|protected|public)\s+)?'
|
|
17
17
|
|
|
18
|
+
# TODO: Replace regex-based parsing with TokenDeclarationParser
|
|
19
|
+
# See: lib/t_ruby/parser_combinator/token/token_declaration_parser.rb
|
|
20
|
+
|
|
18
21
|
attr_reader :source, :ir_program
|
|
19
22
|
|
|
20
23
|
def initialize(source, parse_body: true)
|
|
@@ -22,7 +25,7 @@ module TRuby
|
|
|
22
25
|
@lines = source.split("\n")
|
|
23
26
|
@parse_body = parse_body
|
|
24
27
|
@type_parser = ParserCombinator::TypeParser.new
|
|
25
|
-
@body_parser =
|
|
28
|
+
@body_parser = ParserCombinator::TokenBodyParser.new if parse_body
|
|
26
29
|
@ir_program = nil
|
|
27
30
|
end
|
|
28
31
|
|
|
@@ -97,6 +100,8 @@ module TRuby
|
|
|
97
100
|
@ir_program = builder.build(result, source: @source)
|
|
98
101
|
|
|
99
102
|
result
|
|
103
|
+
rescue Scanner::ScanError => e
|
|
104
|
+
raise ParseError.new(e.message, line: e.line, column: e.column)
|
|
100
105
|
end
|
|
101
106
|
|
|
102
107
|
# Parse to IR directly (new API)
|
|
@@ -116,10 +121,14 @@ module TRuby
|
|
|
116
121
|
# 최상위 함수를 본문까지 포함하여 파싱
|
|
117
122
|
def parse_function_with_body(start_index)
|
|
118
123
|
line = @lines[start_index]
|
|
119
|
-
func_info = parse_function_definition(line)
|
|
124
|
+
func_info = parse_function_definition(line, line_number: start_index + 1)
|
|
120
125
|
return [nil, start_index] unless func_info
|
|
121
126
|
|
|
127
|
+
# Add location info (1-based line number, column is 1 + indentation)
|
|
122
128
|
def_indent = line.match(/^(\s*)/)[1].length
|
|
129
|
+
func_info[:line] = start_index + 1
|
|
130
|
+
func_info[:column] = def_indent + 1
|
|
131
|
+
|
|
123
132
|
i = start_index + 1
|
|
124
133
|
body_start = i
|
|
125
134
|
body_end = i
|
|
@@ -171,13 +180,14 @@ module TRuby
|
|
|
171
180
|
}
|
|
172
181
|
end
|
|
173
182
|
|
|
174
|
-
def parse_function_definition(line)
|
|
183
|
+
def parse_function_definition(line, line_number: 1) # rubocop:disable Lint/UnusedMethodArgument
|
|
175
184
|
# Match methods with or without parentheses
|
|
176
185
|
# def foo(params): Type - with params and return type
|
|
177
186
|
# def foo(): Type - no params but with return type
|
|
178
187
|
# def foo(params) - with params, no return type
|
|
179
188
|
# def foo - no params, no return type
|
|
180
189
|
# Also supports visibility modifiers: private def, protected def, public def
|
|
190
|
+
|
|
181
191
|
match = line.match(/^\s*(?:(private|protected|public)\s+)?def\s+(#{METHOD_NAME_PATTERN})\s*(?:\((.*?)\))?\s*(?::\s*(.+?))?\s*$/)
|
|
182
192
|
return nil unless match
|
|
183
193
|
|
|
@@ -547,10 +557,14 @@ module TRuby
|
|
|
547
557
|
# 클래스 내부의 메서드를 본문까지 포함하여 파싱
|
|
548
558
|
def parse_method_in_class(start_index, class_end)
|
|
549
559
|
line = @lines[start_index]
|
|
550
|
-
method_info = parse_function_definition(line)
|
|
560
|
+
method_info = parse_function_definition(line, line_number: start_index + 1)
|
|
551
561
|
return [nil, start_index] unless method_info
|
|
552
562
|
|
|
563
|
+
# Add location info (1-based line number, column is 1 + indentation)
|
|
553
564
|
def_indent = line.match(/^(\s*)/)[1].length
|
|
565
|
+
method_info[:line] = start_index + 1
|
|
566
|
+
method_info[:column] = def_indent + 1
|
|
567
|
+
|
|
554
568
|
i = start_index + 1
|
|
555
569
|
body_start = i
|
|
556
570
|
body_end = i
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Alternative: try first, if fails try second
|
|
6
|
+
class Alternative < Parser
|
|
7
|
+
def initialize(left, right)
|
|
8
|
+
@left = left
|
|
9
|
+
@right = right
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse(input, position = 0)
|
|
13
|
+
result = @left.parse(input, position)
|
|
14
|
+
return result if result.success?
|
|
15
|
+
|
|
16
|
+
@right.parse(input, position)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Chainl: left-associative chain
|
|
6
|
+
class ChainLeft < Parser
|
|
7
|
+
def initialize(term, op)
|
|
8
|
+
@term = term
|
|
9
|
+
@op = op
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse(input, position = 0)
|
|
13
|
+
first = @term.parse(input, position)
|
|
14
|
+
return first if first.failure?
|
|
15
|
+
|
|
16
|
+
result = first.value
|
|
17
|
+
current_pos = first.position
|
|
18
|
+
|
|
19
|
+
loop do
|
|
20
|
+
op_result = @op.parse(input, current_pos)
|
|
21
|
+
break if op_result.failure?
|
|
22
|
+
|
|
23
|
+
term_result = @term.parse(input, op_result.position)
|
|
24
|
+
break if term_result.failure?
|
|
25
|
+
|
|
26
|
+
result = op_result.value.call(result, term_result.value)
|
|
27
|
+
current_pos = term_result.position
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
ParseResult.success(result, input, current_pos)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Choice: try multiple parsers in order
|
|
6
|
+
class Choice < Parser
|
|
7
|
+
def initialize(*parsers)
|
|
8
|
+
@parsers = parsers
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def parse(input, position = 0)
|
|
12
|
+
best_error = nil
|
|
13
|
+
best_position = position
|
|
14
|
+
|
|
15
|
+
@parsers.each do |parser|
|
|
16
|
+
result = parser.parse(input, position)
|
|
17
|
+
return result if result.success?
|
|
18
|
+
|
|
19
|
+
if result.position >= best_position
|
|
20
|
+
best_error = result.error
|
|
21
|
+
best_position = result.position
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
ParseResult.failure(best_error || "No alternative matched", input, best_position)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# FlatMap (bind)
|
|
6
|
+
class FlatMap < Parser
|
|
7
|
+
def initialize(parser, func)
|
|
8
|
+
@parser = parser
|
|
9
|
+
@func = func
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse(input, position = 0)
|
|
13
|
+
result = @parser.parse(input, position)
|
|
14
|
+
return result if result.failure?
|
|
15
|
+
|
|
16
|
+
next_parser = @func.call(result.value)
|
|
17
|
+
next_parser.parse(input, result.position)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Label for error messages
|
|
6
|
+
class Label < Parser
|
|
7
|
+
def initialize(parser, name)
|
|
8
|
+
@parser = parser
|
|
9
|
+
@name = name
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse(input, position = 0)
|
|
13
|
+
result = @parser.parse(input, position)
|
|
14
|
+
if result.failure?
|
|
15
|
+
ParseResult.failure("Expected #{@name}", input, position)
|
|
16
|
+
else
|
|
17
|
+
result
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Lookahead: check without consuming
|
|
6
|
+
class Lookahead < Parser
|
|
7
|
+
def initialize(parser)
|
|
8
|
+
@parser = parser
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def parse(input, position = 0)
|
|
12
|
+
result = @parser.parse(input, position)
|
|
13
|
+
if result.success?
|
|
14
|
+
ParseResult.success(result.value, input, position)
|
|
15
|
+
else
|
|
16
|
+
result
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Many: zero or more
|
|
6
|
+
class Many < Parser
|
|
7
|
+
def initialize(parser)
|
|
8
|
+
@parser = parser
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def parse(input, position = 0)
|
|
12
|
+
results = []
|
|
13
|
+
current_pos = position
|
|
14
|
+
|
|
15
|
+
loop do
|
|
16
|
+
result = @parser.parse(input, current_pos)
|
|
17
|
+
break if result.failure?
|
|
18
|
+
|
|
19
|
+
results << result.value
|
|
20
|
+
break if result.position == current_pos # Prevent infinite loop
|
|
21
|
+
|
|
22
|
+
current_pos = result.position
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
ParseResult.success(results, input, current_pos)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Many1: one or more
|
|
6
|
+
class Many1 < Parser
|
|
7
|
+
def initialize(parser)
|
|
8
|
+
@parser = parser
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def parse(input, position = 0)
|
|
12
|
+
first = @parser.parse(input, position)
|
|
13
|
+
return first if first.failure?
|
|
14
|
+
|
|
15
|
+
results = [first.value]
|
|
16
|
+
current_pos = first.position
|
|
17
|
+
|
|
18
|
+
loop do
|
|
19
|
+
result = @parser.parse(input, current_pos)
|
|
20
|
+
break if result.failure?
|
|
21
|
+
|
|
22
|
+
results << result.value
|
|
23
|
+
break if result.position == current_pos
|
|
24
|
+
|
|
25
|
+
current_pos = result.position
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
ParseResult.success(results, input, current_pos)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Map result
|
|
6
|
+
class Map < Parser
|
|
7
|
+
def initialize(parser, func)
|
|
8
|
+
@parser = parser
|
|
9
|
+
@func = func
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse(input, position = 0)
|
|
13
|
+
@parser.parse(input, position).map(&@func)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Not followed by
|
|
6
|
+
class NotFollowedBy < Parser
|
|
7
|
+
def initialize(parser)
|
|
8
|
+
@parser = parser
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def parse(input, position = 0)
|
|
12
|
+
result = @parser.parse(input, position)
|
|
13
|
+
if result.failure?
|
|
14
|
+
ParseResult.success(nil, input, position)
|
|
15
|
+
else
|
|
16
|
+
ParseResult.failure("Unexpected match", input, position)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Optional: zero or one
|
|
6
|
+
class Optional < Parser
|
|
7
|
+
def initialize(parser)
|
|
8
|
+
@parser = parser
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def parse(input, position = 0)
|
|
12
|
+
result = @parser.parse(input, position)
|
|
13
|
+
if result.success?
|
|
14
|
+
result
|
|
15
|
+
else
|
|
16
|
+
ParseResult.success(nil, input, position)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Separated by delimiter
|
|
6
|
+
class SepBy < Parser
|
|
7
|
+
def initialize(parser, delimiter)
|
|
8
|
+
@parser = parser
|
|
9
|
+
@delimiter = delimiter
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse(input, position = 0)
|
|
13
|
+
first = @parser.parse(input, position)
|
|
14
|
+
return ParseResult.success([], input, position) if first.failure?
|
|
15
|
+
|
|
16
|
+
results = [first.value]
|
|
17
|
+
current_pos = first.position
|
|
18
|
+
|
|
19
|
+
loop do
|
|
20
|
+
delim_result = @delimiter.parse(input, current_pos)
|
|
21
|
+
break if delim_result.failure?
|
|
22
|
+
|
|
23
|
+
item_result = @parser.parse(input, delim_result.position)
|
|
24
|
+
break if item_result.failure?
|
|
25
|
+
|
|
26
|
+
results << item_result.value
|
|
27
|
+
current_pos = item_result.position
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
ParseResult.success(results, input, current_pos)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Separated by 1 (at least one)
|
|
6
|
+
class SepBy1 < Parser
|
|
7
|
+
def initialize(parser, delimiter)
|
|
8
|
+
@parser = parser
|
|
9
|
+
@delimiter = delimiter
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse(input, position = 0)
|
|
13
|
+
first = @parser.parse(input, position)
|
|
14
|
+
return first if first.failure?
|
|
15
|
+
|
|
16
|
+
results = [first.value]
|
|
17
|
+
current_pos = first.position
|
|
18
|
+
|
|
19
|
+
loop do
|
|
20
|
+
delim_result = @delimiter.parse(input, current_pos)
|
|
21
|
+
break if delim_result.failure?
|
|
22
|
+
|
|
23
|
+
item_result = @parser.parse(input, delim_result.position)
|
|
24
|
+
break if item_result.failure?
|
|
25
|
+
|
|
26
|
+
results << item_result.value
|
|
27
|
+
current_pos = item_result.position
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
ParseResult.success(results, input, current_pos)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module TRuby
|
|
4
|
+
module ParserCombinator
|
|
5
|
+
# Sequence two parsers
|
|
6
|
+
class Sequence < Parser
|
|
7
|
+
def initialize(left, right)
|
|
8
|
+
@left = left
|
|
9
|
+
@right = right
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def parse(input, position = 0)
|
|
13
|
+
result1 = @left.parse(input, position)
|
|
14
|
+
return result1 if result1.failure?
|
|
15
|
+
|
|
16
|
+
result2 = @right.parse(input, result1.position)
|
|
17
|
+
return result2 if result2.failure?
|
|
18
|
+
|
|
19
|
+
ParseResult.success([result1.value, result2.value], input, result2.position)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|