arpaka 0.1.0

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.
data/lib/arpaka/cli.rb ADDED
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "tempfile"
5
+ require_relative "../arpaka"
6
+
7
+ module Arpaka
8
+ class CLI
9
+ def self.run(argv, out: $stdout, err: $stderr)
10
+ options = { class_name: "RubyParser" }
11
+ force = false
12
+ help = false
13
+ parser = OptionParser.new do |opts|
14
+ opts.banner = "Usage: arpaka generate --ruby-source DIR --output FILE [--class-name NAME] [--force]"
15
+ opts.on("--ruby-source DIR", "Ruby source tree (provided by the user)") { |v| options[:ruby_source] = v }
16
+ opts.on("--output FILE", "Write a standalone Ruby frontend") { |v| options[:output] = v }
17
+ opts.on("--class-name NAME", "Generated class name (default: RubyParser)") { |v| options[:class_name] = v }
18
+ opts.on("--force", "Replace an existing output file") { force = true }
19
+ opts.on("-h", "--help", "Show usage") { help = true }
20
+ end
21
+ args = argv.dup
22
+ command = args.shift
23
+ if ["--help", "-h"].include?(command)
24
+ out.puts(parser)
25
+ return 0
26
+ end
27
+ raise OptionParser::InvalidArgument, "expected generate" unless command == "generate"
28
+ parser.parse!(args)
29
+ if help
30
+ out.puts(parser)
31
+ return 0
32
+ end
33
+ raise OptionParser::InvalidArgument, args.join(" ") unless args.empty?
34
+ %i[ruby_source output].each do |key|
35
+ raise OptionParser::MissingArgument, "--#{key.to_s.tr('_', '-')}" unless options[key]
36
+ end
37
+ output = File.expand_path(options.delete(:output))
38
+ if !force && (File.exist?(output) || File.symlink?(output))
39
+ raise Error, "Output already exists: #{output}; use --force to replace it"
40
+ end
41
+ source = Arpaka.generate(**options)
42
+ Tempfile.create([".arpaka-", ".rb"], File.dirname(output)) do |file|
43
+ file.write(source)
44
+ file.flush
45
+ file.chmod(0o644)
46
+ if force
47
+ File.rename(file.path, output)
48
+ else
49
+ # Publish only the complete file, without overwriting a concurrent writer.
50
+ File.link(file.path, output)
51
+ end
52
+ end
53
+ out.puts("Generated #{output}")
54
+ 0
55
+ rescue OptionParser::ParseError, Lrama::Ruby::Error, SystemCallError => error
56
+ err.puts("arpaka: #{error.message}")
57
+ 1
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "json"
5
+ require_relative "ruby_grammar"
6
+
7
+ module Arpaka
8
+ class Generator
9
+ def generate(ruby_source:, class_name:)
10
+ unless /\A[A-Z][a-zA-Z0-9_]*\z/.match?(class_name)
11
+ raise Error, "class_name must be a single Ruby constant name"
12
+ end
13
+ unless ::Ruby::Box.enabled?
14
+ raise Error, "Ruby Box is required for generation. Start Ruby with RUBY_BOX=1."
15
+ end
16
+ artifacts = RubyGrammar.artifacts(ruby_source: ruby_source)
17
+ parser = Lrama::Ruby.generate(artifacts.fetch("parse.y"),
18
+ filename: File.join(ruby_source, "parse.y"), class_name: "Parser", allow_error_rules: true)
19
+ parser = parser.delete_prefix("# frozen_string_literal: true\n")
20
+ rules = JSON.parse(artifacts.fetch("rules.json"))
21
+ runtime = %w[frontend ast builder lexer].map do |name|
22
+ template = File.read(File.join(__dir__, "runtime", "#{name}.rb.erb"))
23
+ name == "builder" ? ERB.new(template).result_with_hash(rules: rules) : template
24
+ end.join("\n")
25
+ notices = [File.read(File.expand_path("../../LICENSE.txt", __dir__))]
26
+ %w[COPYING BSDL].each { |name| notices << File.read(File.join(ruby_source, name)) }
27
+ source = "# frozen_string_literal: true\n" \
28
+ "# Generated by Arpaka #{Lrama::Ruby::VERSION}. Do not edit.\n" \
29
+ "# Ruby source profile: #{RubyGrammar::REVISION}\n" \
30
+ "# parse.y SHA256: #{Digest::SHA256.file(File.join(ruby_source, 'parse.y')).hexdigest}\n" \
31
+ "#{notices.join("\n").lines.map { |line| "# #{line}" }.join}\n" \
32
+ "class #{class_name}\n#{parser}\n#{runtime}\nend\n"
33
+ # Compile in an isolated box to validate syntax without executing actions.
34
+ box = ::Ruby::Box.new
35
+ box.eval(source)
36
+ source
37
+ rescue SyntaxError => error
38
+ raise Error, "Invalid generated frontend: #{error.message}"
39
+ rescue SystemCallError => error
40
+ raise Error, "Cannot read Ruby frontend inputs: #{error.message}"
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "lrama"
4
+ require "json"
5
+ require "digest"
6
+ require "open3"
7
+ require "rbconfig"
8
+ require_relative "actions"
9
+
10
+ module Arpaka::RubyGrammar
11
+ REVISION = "37d60dd3241d5fdb10ca43f36077f5d556ae1b8d"
12
+ EXTENDED_RULES = {
13
+ ["value_expr_command", ["command"]] => "tLBRACE_ARG"
14
+ }.freeze
15
+
16
+ def self.parse(source, filename)
17
+ grammar = Lrama::Parser.new(source, filename).parse
18
+ unless grammar.no_stdlib
19
+ stdlib = Lrama::Command::STDLIB_FILE_PATH
20
+ grammar.prepend_parameterized_rules(Lrama::Parser.new(File.read(stdlib), stdlib).parse.parameterized_rules)
21
+ end
22
+ grammar.prepare
23
+ grammar.validate!
24
+ grammar
25
+ end
26
+
27
+ def self.load_upstream(ruby_source)
28
+ vendor = File.expand_path(ruby_source)
29
+ manifest = JSON.parse(File.read(File.join(__dir__, "source.json")))
30
+ raise ::Arpaka::Error, "Unexpected upstream revision" unless manifest.fetch("revision") == REVISION
31
+ manifest.fetch("files").each do |path, digest|
32
+ actual = Digest::SHA256.file(File.join(vendor, path)).hexdigest
33
+ next if actual == digest
34
+
35
+ raise ::Arpaka::Error, "Unsupported Ruby source change: #{path}; " \
36
+ "expected SHA256 #{digest}, got #{actual}. " \
37
+ "Update the Action mappings and source profile before regenerating."
38
+ end
39
+ source, error, result = Open3.capture3(RbConfig.ruby,
40
+ File.join(vendor, "tool/id2token.rb"), File.join(vendor, "parse.y"))
41
+ raise ::Arpaka::Error, "Ruby preprocessing failed: #{error}" unless result.success?
42
+ grammar = parse(source, File.join(vendor, "parse.y"))
43
+ inventory = grammar.rules.reject(&:initial_rule?).map do |rule|
44
+ { "id" => rule.id, "lhs" => rule.lhs.id.s_value,
45
+ "rhs" => rule.rhs.map { |symbol| symbol.id.s_value },
46
+ "line" => rule.token_code&.location&.first_line || rule.lineno,
47
+ "code" => rule.token_code&.s_value,
48
+ "midrule_position" => rule.position_in_original_rule_rhs,
49
+ "precedence" => rule.precedence_sym&.id&.s_value }
50
+ end
51
+ expected = JSON.parse(File.read(File.join(__dir__, "upstream_rules.json")))
52
+ unless inventory == expected
53
+ changed = (0...[inventory.length, expected.length].max).select { |i| inventory[i] != expected[i] }
54
+ details = changed.first(5).map do |i|
55
+ before, after = expected[i], inventory[i]
56
+ "rule #{before&.fetch('id') || after.fetch('id')}: " \
57
+ "#{before&.fetch('lhs') || '(absent)'} -> #{after&.fetch('lhs') || '(absent)'}"
58
+ end
59
+ raise ::Arpaka::Error, "Upstream rules changed (#{changed.length} entries); " \
60
+ "review Action mappings: #{details.join('; ')}"
61
+ end
62
+ unknown = ::Arpaka::RubyGrammarActions::ACTIONS.keys - inventory.map { |rule| rule.fetch("id") }
63
+ raise ::Arpaka::Error, "Unknown action IDs: #{unknown.inspect}" unless unknown.empty?
64
+ [grammar, inventory]
65
+ end
66
+
67
+ def self.symbol_names(grammar)
68
+ used = grammar.terms.map { |symbol| symbol.id.s_value }
69
+ grammar.nterms.to_h do |symbol|
70
+ original = symbol.id.s_value
71
+ candidate = if original.match?(/\A\$?@\d+\z/)
72
+ "midrule_#{original.delete('$@')}"
73
+ else
74
+ original.gsub("'\\n'", "newline").gsub(/[^a-zA-Z0-9_]/, "_")
75
+ end
76
+ candidate = "rule_#{candidate}" unless candidate.match?(/\A[a-zA-Z_]/)
77
+ candidate += "_#{symbol.number}" if used.include?(candidate)
78
+ raise ::Arpaka::Error, "Symbol name collision: #{candidate}" if used.include?(candidate)
79
+ used << candidate
80
+ [original, candidate]
81
+ end
82
+ end
83
+
84
+ def self.artifacts(ruby_source:)
85
+ grammar, inventory = load_upstream(ruby_source)
86
+ names = symbol_names(grammar)
87
+ name = ->(symbol) { symbol.term ? symbol.id.s_value : names.fetch(symbol.id.s_value) }
88
+ lines = ["/* Generated from ruby/ruby #{REVISION}. See Arpaka Action mappings. */",
89
+ "%no-stdlib", "%expect #{grammar.expect || 0}"]
90
+ lines << "%define lr.type ielr" if grammar.ielr_defined?
91
+ grammar.terms.each do |symbol|
92
+ next if symbol.error_symbol? || symbol.undef_symbol?
93
+ declaration = "%token #{name.call(symbol)} #{symbol.token_id}"
94
+ declaration += " #{symbol.alias_name}" if symbol.alias_name
95
+ lines << declaration
96
+ end
97
+ grammar.precedences.group_by(&:precedence).sort.each do |_level, group|
98
+ lines << "%#{group.first.type} #{group.map(&:s_value).join(' ')}"
99
+ end
100
+ lines << "%left keyword_do_block"
101
+ lines << "%start #{name.call(grammar.rules.first.rhs.first)}"
102
+ lines << "%%"
103
+ metadata = inventory.to_h { |rule| [rule.fetch("id"), rule] }
104
+ rules = grammar.rules.reject(&:initial_rule?)
105
+ rules.each do |rule|
106
+ rhs = rule.rhs.empty? ? "%empty" : rule.rhs.map(&name).join(" ")
107
+ extended_precedence = EXTENDED_RULES[[rule.lhs.id.s_value, rule.rhs.map { |symbol| symbol.id.s_value }]]
108
+ precedence = if rule.precedence_sym
109
+ " %prec #{name.call(rule.precedence_sym)}"
110
+ elsif extended_precedence
111
+ " %prec #{extended_precedence}"
112
+ else
113
+ ""
114
+ end
115
+ expression = ::Arpaka::RubyGrammarActions::ACTIONS[rule.id]&.last || "@builder.unsupported(#{rule.id})"
116
+ lines << "/* upstream parse.y:#{metadata.fetch(rule.id).fetch('line')}: #{rule.as_comment} */"
117
+ lines << "#{name.call(rule.lhs)}: #{rhs}#{precedence} { $$ = #{expression} };"
118
+ end
119
+ lines << "/* arpaka extension: block call as a parenthesized argument */"
120
+ lines << "call_args: block_call { $$ = [$1].freeze };"
121
+ lines << "call_args: args ',' block_call %prec tLOWEST { $$ = ($1 + [$3]).freeze };"
122
+ source = lines.join("\n") + "\n"
123
+ verify_structure(grammar, source, names)
124
+ compact = rules.map do |rule|
125
+ { id: rule.id, rule: rule.as_comment,
126
+ line: metadata.fetch(rule.id).fetch("line"),
127
+ status: ::Arpaka::RubyGrammarActions::ACTIONS[rule.id]&.first || "unsupported" }
128
+ end
129
+ { "parse.y" => source, "rules.json" => JSON.pretty_generate(compact) + "\n" }
130
+ end
131
+
132
+ def self.verify_structure(original, source, names)
133
+ # C-lexer inspection only: @builder is a Ruby ivar, not a yacc location.
134
+ port = parse(source.gsub("@builder.", "builder_"), "generated Ruby grammar")
135
+ signature = lambda do |grammar, rename|
136
+ grammar.rules.reject(&:initial_rule?).map do |rule|
137
+ name = ->(symbol) { rename.fetch(symbol.id.s_value, symbol.id.s_value) }
138
+ [name.call(rule.lhs), rule.rhs.map(&name), rule.precedence_sym && name.call(rule.precedence_sym)]
139
+ end.sort_by(&:inspect)
140
+ end
141
+ expected_rules = signature.call(original, names).map do |rule|
142
+ if rule[0] == names.fetch("value_expr_command") && rule[1] == [names.fetch("command")]
143
+ rule[0, 2] + ["tLBRACE_ARG"]
144
+ else
145
+ rule
146
+ end
147
+ end
148
+ expected_rules << ["call_args", ["block_call"], nil]
149
+ expected_rules << ["call_args", ["args", "','", "block_call"], "tLOWEST"]
150
+ actual_rules = signature.call(port, {})
151
+ unless expected_rules.sort_by(&:inspect) == actual_rules.sort_by(&:inspect)
152
+ raise ::Arpaka::Error, "Production structure changed"
153
+ end
154
+ terms = lambda do |grammar|
155
+ grammar.terms.map { |symbol| [symbol.id.s_value, symbol.token_id, symbol.precedence&.type, symbol.precedence&.precedence] }.sort_by(&:inspect)
156
+ end
157
+ expected_terms = terms.call(original).map do |term|
158
+ if term[0] == "keyword_do_block"
159
+ [term[0], term[1], :left, original.precedences.map(&:precedence).max + 1]
160
+ else
161
+ term
162
+ end
163
+ end
164
+ actual_terms = terms.call(port)
165
+ unless expected_terms.sort_by(&:inspect) == actual_terms
166
+ raise ::Arpaka::Error, "Tokens or precedence changed: expected=#{expected_terms.sort_by(&:inspect).inspect} actual=#{actual_terms.inspect}"
167
+ end
168
+ end
169
+
170
+ end
@@ -0,0 +1,35 @@
1
+ module AST
2
+ Program = Data.define(:statements)
3
+ Literal = Data.define(:value)
4
+ Binary = Data.define(:operator, :left, :right)
5
+ Unary = Data.define(:operator, :operand)
6
+ If = Data.define(:condition, :then_body, :else_body)
7
+ LocalRead = Data.define(:name)
8
+ LocalWrite = Data.define(:name, :value)
9
+ BareCall = Data.define(:name)
10
+ ArrayLiteral = Data.define(:elements)
11
+ HashLiteral = Data.define(:pairs)
12
+ Pair = Data.define(:key, :value)
13
+ RangeLiteral = Data.define(:operator, :begin_value, :end_value)
14
+ Ternary = Data.define(:condition, :then_value, :else_value)
15
+ Call = Data.define(:name, :arguments)
16
+ ReceiverCall = Data.define(:receiver, :operator, :name, :arguments)
17
+ BlockCall = Data.define(:call, :body)
18
+ StringLiteral = Data.define(:value)
19
+ InterpolatedString = Data.define(:parts)
20
+ Index = Data.define(:receiver, :arguments)
21
+ Loop = Data.define(:kind, :condition, :body)
22
+ Control = Data.define(:kind)
23
+ Variable = Data.define(:kind, :name)
24
+ Rescue = Data.define(:expression, :fallback)
25
+ RegexpLiteral = Data.define(:value)
26
+ XStringLiteral = Data.define(:value)
27
+ For = Data.define(:variable, :enumerable, :body)
28
+ Def = Data.define(:name, :arguments, :body)
29
+ ClassDef = Data.define(:name, :superclass, :body)
30
+ ModuleDef = Data.define(:name, :body)
31
+ When = Data.define(:patterns, :body)
32
+ Case = Data.define(:expression, :whens, :else_body)
33
+ Lambda = Data.define(:arguments, :body)
34
+ Sequence = Data.define(:statements)
35
+ end
@@ -0,0 +1,247 @@
1
+ class Builder
2
+ RULES = <%= rules.inspect %>.to_h { |rule| [rule.fetch("id"), rule.freeze] }.freeze
3
+ private_constant :RULES
4
+
5
+ def initialize
6
+ @locals = {}
7
+ end
8
+
9
+ def program(statements)
10
+ AST::Program.new(statements.freeze)
11
+ end
12
+
13
+ def integer(value)
14
+ raise ArgumentError, "tINTEGER requires an Integer value" unless value.is_a?(Integer)
15
+ AST::Literal.new(value)
16
+ end
17
+
18
+ def float(value)
19
+ raise ArgumentError, "tFLOAT requires a Float value" unless value.is_a?(Float)
20
+ AST::Literal.new(value)
21
+ end
22
+
23
+ def literal(value)
24
+ AST::Literal.new(value)
25
+ end
26
+
27
+ def reserved_word(value)
28
+ value.to_sym
29
+ end
30
+
31
+ def binary(operator, left, right)
32
+ AST::Binary.new(operator, left, right)
33
+ end
34
+
35
+ def operator(value)
36
+ value.to_sym
37
+ end
38
+
39
+ def range(operator, begin_value, end_value)
40
+ AST::RangeLiteral.new(operator, begin_value, end_value)
41
+ end
42
+
43
+ def ternary(condition, then_value, else_value)
44
+ AST::Ternary.new(condition, then_value, else_value)
45
+ end
46
+
47
+ def call(name, arguments)
48
+ AST::Call.new(name, arguments.freeze)
49
+ end
50
+
51
+ def receiver_call(receiver, operator, name, arguments)
52
+ AST::ReceiverCall.new(receiver, operator, name, (arguments || []).freeze)
53
+ end
54
+
55
+ def block_call(call, body)
56
+ AST::BlockCall.new(call, body_nodes(body))
57
+ end
58
+
59
+ def append_arguments(arguments, argument)
60
+ list = arguments.is_a?(Array) ? arguments : [arguments]
61
+ (list + [argument]).freeze
62
+ end
63
+
64
+ def string(value)
65
+ if value.is_a?(Array)
66
+ return AST::InterpolatedString.new(value.freeze) if value.any? { |part| !part.is_a?(String) }
67
+ value = value.join
68
+ end
69
+ AST::StringLiteral.new(value.to_s)
70
+ end
71
+
72
+ def regexp(value)
73
+ AST::RegexpLiteral.new(value.is_a?(Array) ? value.join : value.to_s)
74
+ end
75
+
76
+ def xstring(value)
77
+ AST::XStringLiteral.new(value.is_a?(Array) ? value.join : value.to_s)
78
+ end
79
+
80
+ def for_node(variable, enumerable, body)
81
+ AST::For.new(variable, enumerable, body)
82
+ end
83
+
84
+ def def_node(name, arguments, body)
85
+ AST::Def.new(name.to_sym, (arguments || []).freeze, body || [].freeze)
86
+ end
87
+
88
+ def class_node(name, superclass, body)
89
+ AST::ClassDef.new(name.to_sym, superclass, body_nodes(body))
90
+ end
91
+
92
+ def module_node(name, body)
93
+ AST::ModuleDef.new(name.to_sym, body_nodes(body))
94
+ end
95
+
96
+ def body_nodes(body)
97
+ value = body
98
+ while value.is_a?(Array) && value.length == 1 && value.first.is_a?(Array)
99
+ value = value.first
100
+ end
101
+ value || [].freeze
102
+ end
103
+
104
+ def symbol(value)
105
+ value = value.map { |part| part.is_a?(String) ? part : part.to_s }.join if value.is_a?(Array)
106
+ value = value.name if value.is_a?(AST::Variable)
107
+ AST::Literal.new(value.to_sym)
108
+ end
109
+
110
+ def join_strings(parts)
111
+ parts
112
+ end
113
+
114
+ def interpolation(value)
115
+ value.is_a?(Array) && value.length == 1 ? value.first : value
116
+ end
117
+
118
+ def concat_strings(left, right)
119
+ left_parts = left.is_a?(AST::InterpolatedString) ? left.parts : [left.value]
120
+ right_parts = right.is_a?(AST::InterpolatedString) ? right.parts : [right.value]
121
+ parts = left_parts + right_parts
122
+ parts.all? { |part| part.is_a?(String) } ? AST::StringLiteral.new(parts.join) : AST::InterpolatedString.new(parts.freeze)
123
+ end
124
+
125
+ def index(receiver, arguments)
126
+ AST::Index.new(receiver, arguments.freeze)
127
+ end
128
+
129
+ def loop(kind, condition, body)
130
+ AST::Loop.new(kind, condition, body)
131
+ end
132
+
133
+ def control(kind)
134
+ AST::Control.new(kind)
135
+ end
136
+
137
+ def variable(kind, name)
138
+ AST::Variable.new(kind, name.is_a?(String) ? name.to_sym : name)
139
+ end
140
+
141
+ def rescue_modifier(expression, fallback)
142
+ AST::Rescue.new(expression, fallback)
143
+ end
144
+
145
+ def when_node(patterns, body)
146
+ AST::When.new(patterns.freeze, body)
147
+ end
148
+
149
+ def case_node(expression, whens, else_body)
150
+ AST::Case.new(expression, whens.freeze, else_body)
151
+ end
152
+
153
+ def case_clauses(value)
154
+ value.is_a?(Array) ? value : [value]
155
+ end
156
+
157
+ def case_parts(value)
158
+ values = case_clauses(value)
159
+ if values.all? { |item| item.is_a?(AST::When) }
160
+ [values, nil]
161
+ else
162
+ [values.grep(AST::When), values.reject { |item| item.is_a?(AST::When) }]
163
+ end
164
+ end
165
+
166
+ def case_chain(when_node, rest)
167
+ [when_node] + (rest.nil? ? [] : case_clauses(rest))
168
+ end
169
+
170
+ def lambda_node(arguments, body)
171
+ AST::Lambda.new((arguments || []).freeze, body_nodes(body))
172
+ end
173
+
174
+ def unary(operator, operand)
175
+ AST::Unary.new(operator, operand)
176
+ end
177
+
178
+ def if_node(condition, then_body, else_body)
179
+ AST::If.new(condition, then_body, else_body)
180
+ end
181
+
182
+ def unless_node(condition, then_body, else_body)
183
+ AST::If.new(AST::Unary.new(:!, condition), then_body, else_body)
184
+ end
185
+
186
+ def elsif_node(condition, then_body, else_body)
187
+ AST::If.new(condition, then_body, else_body)
188
+ end
189
+
190
+ def parentheses(statements, rule_id)
191
+ return statements.first if statements.length == 1
192
+ AST::Sequence.new(statements.freeze)
193
+ end
194
+
195
+ def identifier(value)
196
+ unless value.is_a?(Symbol) || value.is_a?(String)
197
+ raise ArgumentError, "tIDENTIFIER requires a Symbol or String value"
198
+ end
199
+ value.to_sym
200
+ end
201
+
202
+ def declare_local(name)
203
+ @locals[name] = true
204
+ name
205
+ end
206
+
207
+ def read_local(name)
208
+ return name if name.is_a?(AST::Variable)
209
+ (@locals.key?(name) ? AST::LocalRead : AST::BareCall).new(name)
210
+ end
211
+
212
+ def assign(name, value)
213
+ AST::LocalWrite.new(name, value)
214
+ end
215
+
216
+ def array(elements)
217
+ AST::ArrayLiteral.new(elements.freeze)
218
+ end
219
+
220
+ def word_array(elements, symbols: false)
221
+ elements ||= []
222
+ elements = elements.elements if elements.is_a?(AST::ArrayLiteral)
223
+ values = elements.map do |value|
224
+ value = value.value if value.is_a?(AST::StringLiteral)
225
+ symbols ? value.to_sym : AST::StringLiteral.new(value.to_s)
226
+ end
227
+ AST::ArrayLiteral.new(values.freeze)
228
+ end
229
+
230
+ def hash(pairs)
231
+ AST::HashLiteral.new(pairs.freeze)
232
+ end
233
+
234
+ def pair(key, value)
235
+ AST::Pair.new(key, value)
236
+ end
237
+
238
+ def append(list, value)
239
+ value.nil? ? list : (list + [value]).freeze
240
+ end
241
+
242
+ def unsupported(id)
243
+ rule = RULES.fetch(id)
244
+ raise UnsupportedSyntax.new(rule.fetch("rule"), rule.fetch("line"))
245
+ end
246
+ end
247
+ private_constant :Builder
@@ -0,0 +1,37 @@
1
+ class Error < StandardError; end
2
+
3
+ class ParseError < Error
4
+ attr_reader :token, :state
5
+
6
+ def initialize(error)
7
+ @token, @state = error.token, error.state
8
+ super(error.message)
9
+ end
10
+ end
11
+
12
+ class UnsupportedSyntax < Error
13
+ attr_reader :rule, :line
14
+
15
+ def initialize(rule, line)
16
+ @rule, @line = rule, line
17
+ super("Unsupported syntax: #{rule} (upstream parse.y:#{line})")
18
+ end
19
+ end
20
+
21
+ class LexerError < Error; end
22
+
23
+ def self.parse(source, filename: "(ruby)")
24
+ lexer = Lexer.new(source, filename: filename)
25
+ parse_tokens(lexer.each, lexical_context: lexer.context)
26
+ end
27
+
28
+ def self.parse_tokens(tokens, lexical_context: nil)
29
+ parser = Parser.new
30
+ parser.instance_variable_set(:@builder, Builder.new)
31
+ parser.lexical_context = lexical_context
32
+ parser.parse(tokens)
33
+ rescue Parser::ParseError => error
34
+ raise ParseError.new(error)
35
+ end
36
+ private_class_method :parse_tokens
37
+ private_constant :Parser