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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +302 -0
- data/Rakefile +39 -0
- data/assets/arpaka-logo.svg +209 -0
- data/examples/calculator.y +13 -0
- data/examples/calculator_ast.y +13 -0
- data/exe/arpaka +6 -0
- data/lib/arpaka/BSDL +22 -0
- data/lib/arpaka/COPYING +58 -0
- data/lib/arpaka/actions.rb +656 -0
- data/lib/arpaka/cli.rb +60 -0
- data/lib/arpaka/generator.rb +43 -0
- data/lib/arpaka/ruby_grammar.rb +170 -0
- data/lib/arpaka/runtime/ast.rb.erb +35 -0
- data/lib/arpaka/runtime/builder.rb.erb +247 -0
- data/lib/arpaka/runtime/frontend.rb.erb +37 -0
- data/lib/arpaka/runtime/lexer.rb.erb +1371 -0
- data/lib/arpaka/source.json +11 -0
- data/lib/arpaka/upstream_rules.json +10202 -0
- data/lib/arpaka.rb +20 -0
- data/lib/lrama/ruby/action_code.rb +55 -0
- data/lib/lrama/ruby/action_scanner.rb +353 -0
- data/lib/lrama/ruby/backend.rb +103 -0
- data/lib/lrama/ruby/generator.rb +38 -0
- data/lib/lrama/ruby/parser.rb.erb +127 -0
- data/lib/lrama/ruby/version.rb +7 -0
- data/lib/lrama/ruby.rb +28 -0
- data/sig/arpaka.rbs +15 -0
- data/sig/lrama/ruby.rbs +17 -0
- metadata +86 -0
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
|