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
|
@@ -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
|