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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/t_ruby/ast_type_inferrer.rb +2 -0
  3. data/lib/t_ruby/cache.rb +40 -10
  4. data/lib/t_ruby/cli.rb +14 -9
  5. data/lib/t_ruby/code_emitter.rb +254 -0
  6. data/lib/t_ruby/compiler.rb +186 -3
  7. data/lib/t_ruby/config.rb +18 -3
  8. data/lib/t_ruby/diagnostic.rb +115 -0
  9. data/lib/t_ruby/diagnostic_formatter.rb +162 -0
  10. data/lib/t_ruby/error_handler.rb +201 -35
  11. data/lib/t_ruby/error_reporter.rb +57 -0
  12. data/lib/t_ruby/ir.rb +39 -1
  13. data/lib/t_ruby/lsp_server.rb +40 -97
  14. data/lib/t_ruby/parser.rb +18 -4
  15. data/lib/t_ruby/parser_combinator/combinators/alternative.rb +20 -0
  16. data/lib/t_ruby/parser_combinator/combinators/chain_left.rb +34 -0
  17. data/lib/t_ruby/parser_combinator/combinators/choice.rb +29 -0
  18. data/lib/t_ruby/parser_combinator/combinators/flat_map.rb +21 -0
  19. data/lib/t_ruby/parser_combinator/combinators/label.rb +22 -0
  20. data/lib/t_ruby/parser_combinator/combinators/lookahead.rb +21 -0
  21. data/lib/t_ruby/parser_combinator/combinators/many.rb +29 -0
  22. data/lib/t_ruby/parser_combinator/combinators/many1.rb +32 -0
  23. data/lib/t_ruby/parser_combinator/combinators/map.rb +17 -0
  24. data/lib/t_ruby/parser_combinator/combinators/not_followed_by.rb +21 -0
  25. data/lib/t_ruby/parser_combinator/combinators/optional.rb +21 -0
  26. data/lib/t_ruby/parser_combinator/combinators/sep_by.rb +34 -0
  27. data/lib/t_ruby/parser_combinator/combinators/sep_by1.rb +34 -0
  28. data/lib/t_ruby/parser_combinator/combinators/sequence.rb +23 -0
  29. data/lib/t_ruby/parser_combinator/combinators/skip_right.rb +23 -0
  30. data/lib/t_ruby/parser_combinator/declaration_parser.rb +147 -0
  31. data/lib/t_ruby/parser_combinator/dsl.rb +115 -0
  32. data/lib/t_ruby/parser_combinator/parse_error.rb +48 -0
  33. data/lib/t_ruby/parser_combinator/parse_result.rb +46 -0
  34. data/lib/t_ruby/parser_combinator/parser.rb +84 -0
  35. data/lib/t_ruby/parser_combinator/primitives/end_of_input.rb +16 -0
  36. data/lib/t_ruby/parser_combinator/primitives/fail.rb +16 -0
  37. data/lib/t_ruby/parser_combinator/primitives/lazy.rb +18 -0
  38. data/lib/t_ruby/parser_combinator/primitives/literal.rb +21 -0
  39. data/lib/t_ruby/parser_combinator/primitives/pure.rb +16 -0
  40. data/lib/t_ruby/parser_combinator/primitives/regex.rb +25 -0
  41. data/lib/t_ruby/parser_combinator/primitives/satisfy.rb +21 -0
  42. data/lib/t_ruby/parser_combinator/token/expression_parser.rb +541 -0
  43. data/lib/t_ruby/parser_combinator/token/statement_parser.rb +644 -0
  44. data/lib/t_ruby/parser_combinator/token/token_alternative.rb +20 -0
  45. data/lib/t_ruby/parser_combinator/token/token_body_parser.rb +54 -0
  46. data/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb +920 -0
  47. data/lib/t_ruby/parser_combinator/token/token_dsl.rb +16 -0
  48. data/lib/t_ruby/parser_combinator/token/token_label.rb +22 -0
  49. data/lib/t_ruby/parser_combinator/token/token_many.rb +29 -0
  50. data/lib/t_ruby/parser_combinator/token/token_many1.rb +32 -0
  51. data/lib/t_ruby/parser_combinator/token/token_map.rb +17 -0
  52. data/lib/t_ruby/parser_combinator/token/token_matcher.rb +29 -0
  53. data/lib/t_ruby/parser_combinator/token/token_optional.rb +21 -0
  54. data/lib/t_ruby/parser_combinator/token/token_parse_result.rb +40 -0
  55. data/lib/t_ruby/parser_combinator/token/token_parser.rb +62 -0
  56. data/lib/t_ruby/parser_combinator/token/token_sep_by.rb +34 -0
  57. data/lib/t_ruby/parser_combinator/token/token_sep_by1.rb +34 -0
  58. data/lib/t_ruby/parser_combinator/token/token_sequence.rb +23 -0
  59. data/lib/t_ruby/parser_combinator/token/token_skip_right.rb +23 -0
  60. data/lib/t_ruby/parser_combinator/type_parser.rb +103 -0
  61. data/lib/t_ruby/parser_combinator.rb +64 -936
  62. data/lib/t_ruby/ruby_version.rb +112 -0
  63. data/lib/t_ruby/scanner.rb +883 -0
  64. data/lib/t_ruby/version.rb +1 -1
  65. data/lib/t_ruby/watcher.rb +83 -76
  66. data/lib/t_ruby.rb +17 -1
  67. metadata +58 -7
  68. 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
 
@@ -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
- diagnostics = []
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
- # Use ErrorHandler to check for errors
375
- error_handler = ErrorHandler.new(text)
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
- def validate_type_aliases(type_aliases, diagnostics, text)
406
- lines = text.split("\n")
407
- registry = TypeAliasRegistry.new
408
-
409
- type_aliases.each do |alias_info|
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
- def validate_functions(functions, diagnostics, text)
424
- lines = text.split("\n")
389
+ col = (diagnostic.column || 1) - 1
390
+ col = 0 if col.negative?
425
391
 
426
- functions.each do |func|
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
- def find_line_number(lines, pattern)
453
- lines.each_with_index do |line, idx|
454
- return idx if line.match?(pattern)
455
- end
456
- nil
457
- end
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
- def valid_type?(type_str)
460
- return true if type_str.nil?
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
- # Handle union types
463
- if type_str.include?("|")
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
- # Handle intersection types
468
- if type_str.include?("&")
469
- return type_str.split("&").map(&:strip).all? { |t| valid_type?(t) }
470
- end
414
+ lsp_diag
415
+ end
471
416
 
472
- # Handle generic types
473
- if type_str.include?("<")
474
- base_type = type_str.split("<").first
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
- BUILT_IN_TYPES.include?(type_str) || @type_alias_registry.valid_type?(type_str)
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 = BodyParser.new if parse_body
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