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
@@ -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
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Skip right: parse both, return left
6
+ class SkipRight < 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, input, result2.position)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Declaration Parser - Parse T-Ruby declarations
6
+ class DeclarationParser
7
+ include DSL
8
+
9
+ def initialize
10
+ @type_parser = TypeParser.new
11
+ build_parsers
12
+ end
13
+
14
+ def parse(input)
15
+ result = @declaration.parse(input.strip)
16
+ if result.success?
17
+ { success: true, declarations: result.value }
18
+ else
19
+ { success: false, error: result.error, position: result.position }
20
+ end
21
+ end
22
+
23
+ def parse_file(input)
24
+ result = @program.parse(input)
25
+ if result.success?
26
+ { success: true, declarations: result.value.compact }
27
+ else
28
+ { success: false, error: result.error, position: result.position }
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def build_parsers
35
+ # Type expression (delegate to TypeParser)
36
+ lazy { parse_type_inline }
37
+
38
+ # Keywords
39
+ kw_type = lexeme(string("type"))
40
+ kw_interface = lexeme(string("interface"))
41
+ kw_def = lexeme(string("def"))
42
+ kw_end = lexeme(string("end"))
43
+ lexeme(string("class"))
44
+ lexeme(string("module"))
45
+
46
+ # Type alias: type Name = Definition
47
+ type_alias = (
48
+ kw_type >>
49
+ lexeme(identifier) <<
50
+ lexeme(char("=")) >>
51
+ regex(/[^\n]+/).map(&:strip)
52
+ ).map do |((_, name), definition)|
53
+ type_result = @type_parser.parse(definition)
54
+ if type_result[:success]
55
+ IR::TypeAlias.new(name: name, definition: type_result[:type])
56
+ end
57
+ end
58
+
59
+ # Interface member: name: Type
60
+ interface_member = (
61
+ lexeme(identifier) <<
62
+ lexeme(char(":")) >>
63
+ regex(/[^\n]+/).map(&:strip)
64
+ ).map do |(name, type_str)|
65
+ type_result = @type_parser.parse(type_str)
66
+ if type_result[:success]
67
+ IR::InterfaceMember.new(name: name, type_signature: type_result[:type])
68
+ end
69
+ end
70
+
71
+ # Interface: interface Name ... end
72
+ interface_body = (interface_member << (newline | spaces)).many
73
+
74
+ interface_decl = (
75
+ kw_interface >>
76
+ lexeme(identifier) <<
77
+ (newline | spaces) >>
78
+ interface_body <<
79
+ kw_end
80
+ ).map do |((_, name), members)|
81
+ IR::Interface.new(name: name, members: members.compact)
82
+ end
83
+
84
+ # Parameter: name: Type or name
85
+ param = (
86
+ identifier >>
87
+ (lexeme(char(":")) >> regex(/[^,)]+/).map(&:strip)).optional
88
+ ).map do |(name, type_str)|
89
+ type_node = if type_str
90
+ type_str_val = type_str.is_a?(Array) ? type_str.last : type_str
91
+ result = @type_parser.parse(type_str_val)
92
+ result[:success] ? result[:type] : nil
93
+ end
94
+ IR::Parameter.new(name: name, type_annotation: type_node)
95
+ end
96
+
97
+ # Parameters list
98
+ params_list = (
99
+ lexeme(char("(")) >>
100
+ param.sep_by(lexeme(char(","))) <<
101
+ lexeme(char(")"))
102
+ ).map { |(_, params)| params }
103
+
104
+ # Return type annotation
105
+ return_type = (
106
+ lexeme(char(":")) >>
107
+ regex(/[^\n]+/).map(&:strip)
108
+ ).map { |(_, type_str)| type_str }.optional
109
+
110
+ # Method definition: def name(params): ReturnType
111
+ method_def = (
112
+ kw_def >>
113
+ identifier >>
114
+ params_list.optional >>
115
+ return_type
116
+ ).map do |(((_, name), params), ret_str)|
117
+ ret_type = if ret_str
118
+ result = @type_parser.parse(ret_str)
119
+ result[:success] ? result[:type] : nil
120
+ end
121
+ IR::MethodDef.new(
122
+ name: name,
123
+ params: params || [],
124
+ return_type: ret_type
125
+ )
126
+ end
127
+
128
+ # Any declaration
129
+ @declaration = choice(
130
+ type_alias,
131
+ interface_decl,
132
+ method_def
133
+ )
134
+
135
+ # Line (declaration or empty)
136
+ line = (@declaration << (newline | eof)) | (spaces >> newline).map { nil }
137
+
138
+ # Program (multiple declarations)
139
+ @program = line.many
140
+ end
141
+
142
+ def parse_type_inline
143
+ Lazy.new { @type_parser.instance_variable_get(:@type_expr) }
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # DSL Module - Convenience methods
6
+ module DSL
7
+ def literal(str)
8
+ Literal.new(str)
9
+ end
10
+
11
+ def regex(pattern, description = nil)
12
+ Regex.new(pattern, description)
13
+ end
14
+
15
+ def satisfy(description = "character", &predicate)
16
+ Satisfy.new(predicate, description)
17
+ end
18
+
19
+ def char(c)
20
+ Literal.new(c)
21
+ end
22
+
23
+ def string(str)
24
+ Literal.new(str)
25
+ end
26
+
27
+ def eof
28
+ EndOfInput.new
29
+ end
30
+
31
+ def pure(value)
32
+ Pure.new(value)
33
+ end
34
+
35
+ def fail(message)
36
+ Fail.new(message)
37
+ end
38
+
39
+ def lazy(&)
40
+ Lazy.new(&)
41
+ end
42
+
43
+ def choice(*parsers)
44
+ Choice.new(*parsers)
45
+ end
46
+
47
+ def sequence(*parsers)
48
+ parsers.reduce { |acc, p| acc >> p }
49
+ end
50
+
51
+ # Common character parsers
52
+ def digit
53
+ satisfy("digit") { |c| c =~ /[0-9]/ }
54
+ end
55
+
56
+ def letter
57
+ satisfy("letter") { |c| c =~ /[a-zA-Z]/ }
58
+ end
59
+
60
+ def alphanumeric
61
+ satisfy("alphanumeric") { |c| c =~ /[a-zA-Z0-9]/ }
62
+ end
63
+
64
+ def whitespace
65
+ satisfy("whitespace") { |c| c =~ /\s/ }
66
+ end
67
+
68
+ def spaces
69
+ whitespace.many.map(&:join)
70
+ end
71
+
72
+ def spaces1
73
+ whitespace.many1.map(&:join)
74
+ end
75
+
76
+ def newline
77
+ char("\n") | string("\r\n")
78
+ end
79
+
80
+ def identifier
81
+ (letter >> (alphanumeric | char("_")).many).map do |(first, rest)|
82
+ first + rest.join
83
+ end
84
+ end
85
+
86
+ def integer
87
+ (char("-").optional >> digit.many1).map do |(sign, digits)|
88
+ num = digits.join.to_i
89
+ sign ? -num : num
90
+ end
91
+ end
92
+
93
+ def float
94
+ regex(/-?\d+\.\d+/, "float").map(&:to_f)
95
+ end
96
+
97
+ def quoted_string(quote = '"')
98
+ content = satisfy("string character") { |c| c != quote && c != "\\" }
99
+ escape = (char("\\") >> satisfy("escape char")).map { |(_bs, c)| c }
100
+
101
+ (char(quote) >> (content | escape).many.map(&:join) << char(quote)).map { |(_, str)| str }
102
+ end
103
+
104
+ # Skip whitespace around parser
105
+ def lexeme(parser)
106
+ (spaces >> parser << spaces).map { |(_, val)| val }
107
+ end
108
+
109
+ # Chain for left-associative operators
110
+ def chainl(term, op)
111
+ ChainLeft.new(term, op)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Error Reporting
6
+ class ParseError
7
+ attr_reader :message, :position, :line, :column, :input
8
+
9
+ def initialize(message:, position:, input:)
10
+ @message = message
11
+ @position = position
12
+ @input = input
13
+ @line, @column = calculate_line_column
14
+ end
15
+
16
+ def to_s
17
+ "Parse error at line #{@line}, column #{@column}: #{@message}"
18
+ end
19
+
20
+ def context(lines_before: 2, lines_after: 1)
21
+ input_lines = @input.split("\n")
22
+ start_line = [@line - lines_before - 1, 0].max
23
+ end_line = [@line + lines_after - 1, input_lines.length - 1].min
24
+
25
+ result = []
26
+ (start_line..end_line).each do |i|
27
+ prefix = i == @line - 1 ? ">>> " : " "
28
+ result << "#{prefix}#{i + 1}: #{input_lines[i]}"
29
+
30
+ if i == @line - 1
31
+ result << " #{" " * (@column + @line.to_s.length + 1)}^"
32
+ end
33
+ end
34
+
35
+ result.join("\n")
36
+ end
37
+
38
+ private
39
+
40
+ def calculate_line_column
41
+ lines = @input[0...@position].split("\n", -1)
42
+ line = lines.length
43
+ column = lines.last&.length || 0
44
+ [line, column + 1]
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Parse result - either success or failure
6
+ class ParseResult
7
+ attr_reader :value, :remaining, :position, :error
8
+
9
+ def initialize(success:, value: nil, remaining: "", position: 0, error: nil)
10
+ @success = success
11
+ @value = value
12
+ @remaining = remaining
13
+ @position = position
14
+ @error = error
15
+ end
16
+
17
+ def success?
18
+ @success
19
+ end
20
+
21
+ def failure?
22
+ !@success
23
+ end
24
+
25
+ def self.success(value, remaining, position)
26
+ new(success: true, value: value, remaining: remaining, position: position)
27
+ end
28
+
29
+ def self.failure(error, remaining, position)
30
+ new(success: false, error: error, remaining: remaining, position: position)
31
+ end
32
+
33
+ def map
34
+ return self if failure?
35
+
36
+ ParseResult.success(yield(value), remaining, position)
37
+ end
38
+
39
+ def flat_map
40
+ return self if failure?
41
+
42
+ yield(value, remaining, position)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Base parser class for string-based parsing
6
+ class Parser
7
+ def parse(input, position = 0)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ # Combinators as methods
12
+
13
+ # Sequence: run this parser, then the other
14
+ def >>(other)
15
+ Sequence.new(self, other)
16
+ end
17
+
18
+ # Alternative: try this parser, if it fails try the other
19
+ def |(other)
20
+ Alternative.new(self, other)
21
+ end
22
+
23
+ # Map: transform the result
24
+ def map(&block)
25
+ Map.new(self, block)
26
+ end
27
+
28
+ # FlatMap: transform with another parser
29
+ def flat_map(&block)
30
+ FlatMap.new(self, block)
31
+ end
32
+
33
+ # Many: zero or more repetitions
34
+ def many
35
+ Many.new(self)
36
+ end
37
+
38
+ # Many1: one or more repetitions
39
+ def many1
40
+ Many1.new(self)
41
+ end
42
+
43
+ # Optional: zero or one
44
+ def optional
45
+ Optional.new(self)
46
+ end
47
+
48
+ # Separated by: parse items separated by delimiter
49
+ def sep_by(delimiter)
50
+ SepBy.new(self, delimiter)
51
+ end
52
+
53
+ # Separated by 1: at least one item
54
+ def sep_by1(delimiter)
55
+ SepBy1.new(self, delimiter)
56
+ end
57
+
58
+ # Between: parse between left and right delimiters
59
+ def between(left, right)
60
+ (left >> self << right).map { |(_, val)| val }
61
+ end
62
+
63
+ # Skip right: parse both, keep left result
64
+ def <<(other)
65
+ SkipRight.new(self, other)
66
+ end
67
+
68
+ # Label: add a descriptive label for error messages
69
+ def label(name)
70
+ Label.new(self, name)
71
+ end
72
+
73
+ # Lookahead: check without consuming
74
+ def lookahead
75
+ Lookahead.new(self)
76
+ end
77
+
78
+ # Not: succeed only if parser fails
79
+ def not_followed_by
80
+ NotFollowedBy.new(self)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Parse end of input
6
+ class EndOfInput < Parser
7
+ def parse(input, position = 0)
8
+ if position >= input.length
9
+ ParseResult.success(nil, input, position)
10
+ else
11
+ ParseResult.failure("Expected end of input", input, position)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Always fail
6
+ class Fail < Parser
7
+ def initialize(message)
8
+ @message = message
9
+ end
10
+
11
+ def parse(input, position = 0)
12
+ ParseResult.failure(@message, input, position)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Lazy parser (for recursive grammars)
6
+ class Lazy < Parser
7
+ def initialize(&block)
8
+ @block = block
9
+ @parser = nil
10
+ end
11
+
12
+ def parse(input, position = 0)
13
+ @parser ||= @block.call
14
+ @parser.parse(input, position)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Parse a literal string
6
+ class Literal < Parser
7
+ def initialize(string)
8
+ @string = string
9
+ end
10
+
11
+ def parse(input, position = 0)
12
+ remaining = input[position..]
13
+ if remaining&.start_with?(@string)
14
+ ParseResult.success(@string, input, position + @string.length)
15
+ else
16
+ ParseResult.failure("Expected '#{@string}'", input, position)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Always succeed with a value
6
+ class Pure < Parser
7
+ def initialize(value)
8
+ @value = value
9
+ end
10
+
11
+ def parse(input, position = 0)
12
+ ParseResult.success(@value, input, position)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Parse using regex
6
+ class Regex < Parser
7
+ def initialize(pattern, description = nil)
8
+ @pattern = pattern.is_a?(Regexp) ? pattern : Regexp.new("^#{pattern}")
9
+ @description = description || @pattern.inspect
10
+ end
11
+
12
+ def parse(input, position = 0)
13
+ remaining = input[position..]
14
+ match = @pattern.match(remaining)
15
+
16
+ if match&.begin(0)&.zero?
17
+ matched = match[0]
18
+ ParseResult.success(matched, input, position + matched.length)
19
+ else
20
+ ParseResult.failure("Expected #{@description}", input, position)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TRuby
4
+ module ParserCombinator
5
+ # Parse a single character matching predicate
6
+ class Satisfy < Parser
7
+ def initialize(predicate, description = "character")
8
+ @predicate = predicate
9
+ @description = description
10
+ end
11
+
12
+ def parse(input, position = 0)
13
+ if position < input.length && @predicate.call(input[position])
14
+ ParseResult.success(input[position], input, position + 1)
15
+ else
16
+ ParseResult.failure("Expected #{@description}", input, position)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end