t-ruby 0.0.42 → 0.0.46

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -2
  3. data/bin/t-ruby +6 -0
  4. data/lib/t_ruby/ast_type_inferrer.rb +2 -0
  5. data/lib/t_ruby/benchmark.rb +1 -0
  6. data/lib/t_ruby/bundler_integration.rb +1 -0
  7. data/lib/t_ruby/cache.rb +40 -10
  8. data/lib/t_ruby/cli.rb +30 -8
  9. data/lib/t_ruby/compiler.rb +168 -0
  10. data/lib/t_ruby/diagnostic.rb +115 -0
  11. data/lib/t_ruby/diagnostic_formatter.rb +162 -0
  12. data/lib/t_ruby/doc_generator.rb +1 -0
  13. data/lib/t_ruby/docs_badge_generator.rb +1 -0
  14. data/lib/t_ruby/error_handler.rb +201 -35
  15. data/lib/t_ruby/error_reporter.rb +57 -0
  16. data/lib/t_ruby/ir.rb +53 -69
  17. data/lib/t_ruby/lsp_server.rb +40 -97
  18. data/lib/t_ruby/package_manager.rb +1 -0
  19. data/lib/t_ruby/parser.rb +18 -4
  20. data/lib/t_ruby/parser_combinator/combinators/alternative.rb +20 -0
  21. data/lib/t_ruby/parser_combinator/combinators/chain_left.rb +34 -0
  22. data/lib/t_ruby/parser_combinator/combinators/choice.rb +29 -0
  23. data/lib/t_ruby/parser_combinator/combinators/flat_map.rb +21 -0
  24. data/lib/t_ruby/parser_combinator/combinators/label.rb +22 -0
  25. data/lib/t_ruby/parser_combinator/combinators/lookahead.rb +21 -0
  26. data/lib/t_ruby/parser_combinator/combinators/many.rb +29 -0
  27. data/lib/t_ruby/parser_combinator/combinators/many1.rb +32 -0
  28. data/lib/t_ruby/parser_combinator/combinators/map.rb +17 -0
  29. data/lib/t_ruby/parser_combinator/combinators/not_followed_by.rb +21 -0
  30. data/lib/t_ruby/parser_combinator/combinators/optional.rb +21 -0
  31. data/lib/t_ruby/parser_combinator/combinators/sep_by.rb +34 -0
  32. data/lib/t_ruby/parser_combinator/combinators/sep_by1.rb +34 -0
  33. data/lib/t_ruby/parser_combinator/combinators/sequence.rb +23 -0
  34. data/lib/t_ruby/parser_combinator/combinators/skip_right.rb +23 -0
  35. data/lib/t_ruby/parser_combinator/declaration_parser.rb +147 -0
  36. data/lib/t_ruby/parser_combinator/dsl.rb +115 -0
  37. data/lib/t_ruby/parser_combinator/parse_error.rb +48 -0
  38. data/lib/t_ruby/parser_combinator/parse_result.rb +46 -0
  39. data/lib/t_ruby/parser_combinator/parser.rb +84 -0
  40. data/lib/t_ruby/parser_combinator/primitives/end_of_input.rb +16 -0
  41. data/lib/t_ruby/parser_combinator/primitives/fail.rb +16 -0
  42. data/lib/t_ruby/parser_combinator/primitives/lazy.rb +18 -0
  43. data/lib/t_ruby/parser_combinator/primitives/literal.rb +21 -0
  44. data/lib/t_ruby/parser_combinator/primitives/pure.rb +16 -0
  45. data/lib/t_ruby/parser_combinator/primitives/regex.rb +25 -0
  46. data/lib/t_ruby/parser_combinator/primitives/satisfy.rb +21 -0
  47. data/lib/t_ruby/parser_combinator/token/expression_parser.rb +541 -0
  48. data/lib/t_ruby/parser_combinator/token/statement_parser.rb +644 -0
  49. data/lib/t_ruby/parser_combinator/token/token_alternative.rb +20 -0
  50. data/lib/t_ruby/parser_combinator/token/token_body_parser.rb +54 -0
  51. data/lib/t_ruby/parser_combinator/token/token_declaration_parser.rb +962 -0
  52. data/lib/t_ruby/parser_combinator/token/token_dsl.rb +16 -0
  53. data/lib/t_ruby/parser_combinator/token/token_label.rb +22 -0
  54. data/lib/t_ruby/parser_combinator/token/token_many.rb +29 -0
  55. data/lib/t_ruby/parser_combinator/token/token_many1.rb +32 -0
  56. data/lib/t_ruby/parser_combinator/token/token_map.rb +17 -0
  57. data/lib/t_ruby/parser_combinator/token/token_matcher.rb +29 -0
  58. data/lib/t_ruby/parser_combinator/token/token_optional.rb +21 -0
  59. data/lib/t_ruby/parser_combinator/token/token_parse_result.rb +40 -0
  60. data/lib/t_ruby/parser_combinator/token/token_parser.rb +62 -0
  61. data/lib/t_ruby/parser_combinator/token/token_sep_by.rb +34 -0
  62. data/lib/t_ruby/parser_combinator/token/token_sep_by1.rb +34 -0
  63. data/lib/t_ruby/parser_combinator/token/token_sequence.rb +23 -0
  64. data/lib/t_ruby/parser_combinator/token/token_skip_right.rb +23 -0
  65. data/lib/t_ruby/parser_combinator/type_parser.rb +118 -0
  66. data/lib/t_ruby/parser_combinator.rb +64 -936
  67. data/lib/t_ruby/runner.rb +132 -0
  68. data/lib/t_ruby/scanner.rb +883 -0
  69. data/lib/t_ruby/version.rb +1 -1
  70. data/lib/t_ruby/watcher.rb +67 -75
  71. data/lib/t_ruby.rb +16 -1
  72. metadata +73 -4
  73. 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
@@ -632,7 +646,13 @@ module TRuby
632
646
  end
633
647
 
634
648
  def to_rbs
635
- "#{@inner_type.to_rbs}?"
649
+ inner_rbs = @inner_type.to_rbs
650
+ # Simple types can use ? suffix, complex types need (Type | nil) form
651
+ if @inner_type.is_a?(SimpleType)
652
+ "#{inner_rbs}?"
653
+ else
654
+ "(#{inner_rbs} | nil)"
655
+ end
636
656
  end
637
657
 
638
658
  def to_trb
@@ -658,6 +678,26 @@ module TRuby
658
678
  end
659
679
  end
660
680
 
681
+ # Hash literal type: { key: Type, key2: Type }
682
+ class HashLiteralType < TypeNode
683
+ attr_accessor :fields # Array of { name: String, type: TypeNode }
684
+
685
+ def initialize(fields:, **opts)
686
+ super(**opts)
687
+ @fields = fields
688
+ end
689
+
690
+ def to_rbs
691
+ # Hash literal types in RBS are represented as Hash[Symbol, untyped] or specific record types
692
+ "Hash[Symbol, untyped]"
693
+ end
694
+
695
+ def to_trb
696
+ field_strs = @fields.map { |f| "#{f[:name]}: #{f[:type].to_trb}" }
697
+ "{ #{field_strs.join(", ")} }"
698
+ end
699
+ end
700
+
661
701
  #==========================================================================
662
702
  # Visitor Pattern
663
703
  #==========================================================================
@@ -778,12 +818,16 @@ module TRuby
778
818
  # 본문 IR이 있으면 사용 (BodyParser에서 파싱됨)
779
819
  body = info[:body_ir]
780
820
 
821
+ # Build location string from line/column info
822
+ location = info[:line] && info[:column] ? "#{info[:line]}:#{info[:column]}" : nil
823
+
781
824
  MethodDef.new(
782
825
  name: info[:name],
783
826
  params: params,
784
827
  return_type: info[:return_type] ? parse_type(info[:return_type]) : nil,
785
828
  body: body,
786
- visibility: info[:visibility] || :public
829
+ visibility: info[:visibility] || :public,
830
+ location: location
787
831
  )
788
832
  end
789
833
 
@@ -791,77 +835,17 @@ module TRuby
791
835
  return nil unless type_str
792
836
 
793
837
  type_str = type_str.strip
838
+ return nil if type_str.empty?
794
839
 
795
- # Union type
796
- if type_str.include?("|")
797
- types = type_str.split("|").map { |t| parse_type(t.strip) }
798
- return UnionType.new(types: types)
799
- end
800
-
801
- # Intersection type
802
- if type_str.include?("&")
803
- types = type_str.split("&").map { |t| parse_type(t.strip) }
804
- return IntersectionType.new(types: types)
805
- end
806
-
807
- # Nullable type
808
- if type_str.end_with?("?")
809
- inner = parse_type(type_str[0..-2])
810
- return NullableType.new(inner_type: inner)
811
- end
812
-
813
- # Generic type
814
- if type_str.include?("<") && type_str.include?(">")
815
- match = type_str.match(/^(\w+)<(.+)>$/)
816
- if match
817
- base = match[1]
818
- args = parse_generic_args(match[2])
819
- return GenericType.new(base: base, type_args: args)
820
- end
821
- end
822
-
823
- # Function type
824
- if type_str.include?("->")
825
- match = type_str.match(/^\((.*)?\)\s*->\s*(.+)$/)
826
- if match
827
- param_types = match[1] ? match[1].split(",").map { |t| parse_type(t.strip) } : []
828
- return_type = parse_type(match[2])
829
- return FunctionType.new(param_types: param_types, return_type: return_type)
830
- end
831
- end
840
+ # Use ParserCombinator::TypeParser for all type parsing
841
+ # Supports: simple types, generics, array shorthand, union, intersection, function types
842
+ @type_parser ||= TRuby::ParserCombinator::TypeParser.new
843
+ result = @type_parser.parse(type_str)
844
+ return result[:type] if result[:success]
832
845
 
833
- # Simple type
846
+ # Fallback for unparseable types - return as SimpleType
834
847
  SimpleType.new(name: type_str)
835
848
  end
836
-
837
- def parse_generic_args(args_str)
838
- args = []
839
- current = ""
840
- depth = 0
841
-
842
- args_str.each_char do |char|
843
- case char
844
- when "<"
845
- depth += 1
846
- current += char
847
- when ">"
848
- depth -= 1
849
- current += char
850
- when ","
851
- if depth.zero?
852
- args << parse_type(current.strip)
853
- current = ""
854
- else
855
- current += char
856
- end
857
- else
858
- current += char
859
- end
860
- end
861
-
862
- args << parse_type(current.strip) unless current.empty?
863
- args
864
- end
865
849
  end
866
850
 
867
851
  #==========================================================================
@@ -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)
@@ -4,6 +4,7 @@ require "json"
4
4
  require "fileutils"
5
5
  require "net/http"
6
6
  require "uri"
7
+ require "time"
7
8
 
8
9
  module TRuby
9
10
  # Semantic version parsing and comparison
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