arpaka 1.0.0 → 2.0.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 +4 -4
- data/CHANGELOG.md +46 -2
- data/README.md +104 -7
- data/Rakefile +15 -1
- data/docs/architecture.dot +30 -0
- data/docs/architecture.md +96 -0
- data/docs/architecture.webp +0 -0
- data/docs/contributes.md +115 -0
- data/docs/usage.md +497 -0
- data/lib/arpaka/cli.rb +5 -53
- data/lib/arpaka/plugins/cli.rb +61 -0
- data/lib/arpaka/plugins/sample.rb +14 -0
- data/lib/arpaka/plugins/sample_runner.rb +21 -0
- data/lib/arpaka/{actions.rb → ruby/actions.rb} +230 -151
- data/lib/arpaka/ruby/class_name_parser.rb +97 -0
- data/lib/arpaka/ruby/cli.rb +64 -0
- data/lib/arpaka/ruby/frontend_emitter.rb +59 -0
- data/lib/arpaka/ruby/generator.rb +41 -0
- data/lib/arpaka/{ruby_grammar.rb → ruby/grammar.rb} +5 -56
- data/lib/arpaka/ruby/grammar_emitter.rb +71 -0
- data/lib/arpaka/ruby/profile.rb +129 -0
- data/lib/arpaka/{profile → ruby/profiles}/ruby_3_3.rb +21 -21
- data/lib/arpaka/{profile → ruby/profiles}/ruby_3_4.rb +21 -21
- data/lib/arpaka/{runtime → ruby/runtime/detailed}/ast.rb.erb +41 -5
- data/lib/arpaka/ruby/runtime/detailed/builder.rb.erb +1177 -0
- data/lib/arpaka/{runtime → ruby/runtime}/frontend.rb.erb +17 -4
- data/lib/arpaka/{runtime → ruby/runtime}/lexer.rb.erb +576 -99
- data/lib/arpaka/ruby/runtime/structured.rb.erb +111 -0
- data/lib/arpaka/ruby.rb +23 -0
- data/lib/arpaka.rb +6 -9
- data/lib/lrama/ruby/backend.rb +4 -9
- data/lib/lrama/ruby/parser.rb.erb +35 -9
- data/lib/lrama/ruby/parser_emitter.rb +41 -0
- data/lib/lrama/ruby/table_codec.rb +50 -0
- data/lib/lrama/ruby/version.rb +1 -1
- data/lib/lrama/ruby.rb +1 -0
- data/sig/arpaka.rbs +8 -3
- metadata +32 -16
- data/lib/arpaka/generator.rb +0 -51
- data/lib/arpaka/profile/ruby.rb +0 -49
- data/lib/arpaka/runtime/builder.rb.erb +0 -323
- /data/lib/arpaka/{BSDL → ruby/BSDL} +0 -0
- /data/lib/arpaka/{COPYING → ruby/COPYING} +0 -0
- /data/lib/arpaka/{source.json → ruby/source.json} +0 -0
- /data/lib/arpaka/{upstream_rules.json → ruby/upstream_rules.json} +0 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Arpaka::Ruby
|
|
4
|
+
class ClassName
|
|
5
|
+
attr_reader :components
|
|
6
|
+
|
|
7
|
+
def initialize(components)
|
|
8
|
+
@components = components.freeze
|
|
9
|
+
freeze
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def namespaces
|
|
13
|
+
components[0...-1]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def constant
|
|
17
|
+
components.last
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def resolve(root)
|
|
21
|
+
components.reduce(root) { |namespace, name| namespace.const_get(name, false) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_s
|
|
25
|
+
components.join("::")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class ClassNameParser
|
|
30
|
+
def self.parse(value)
|
|
31
|
+
new(value).parse
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(value)
|
|
35
|
+
@value = value
|
|
36
|
+
@index = 0
|
|
37
|
+
@components = []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def parse
|
|
41
|
+
unless @value.is_a?(String)
|
|
42
|
+
invalid("must be a String")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
parse_component
|
|
46
|
+
until end_of_input?
|
|
47
|
+
parse_separator
|
|
48
|
+
parse_component
|
|
49
|
+
end
|
|
50
|
+
ClassName.new(@components)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def parse_component
|
|
56
|
+
component_number = @components.length + 1
|
|
57
|
+
if end_of_input? || current_byte == 58
|
|
58
|
+
invalid("component #{component_number} is empty")
|
|
59
|
+
end
|
|
60
|
+
unless uppercase_byte?(current_byte)
|
|
61
|
+
invalid("component #{component_number} must start with an uppercase letter")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
start = @index
|
|
65
|
+
@index += 1
|
|
66
|
+
@index += 1 while identifier_byte?(current_byte)
|
|
67
|
+
@components << @value.byteslice(start, @index - start)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parse_separator
|
|
71
|
+
unless current_byte == 58 && @value.getbyte(@index + 1) == 58
|
|
72
|
+
invalid("expected `::` after component #{@components.length}")
|
|
73
|
+
end
|
|
74
|
+
@index += 2
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def end_of_input?
|
|
78
|
+
@index >= @value.bytesize
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def current_byte
|
|
82
|
+
@value.getbyte(@index)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def uppercase_byte?(byte)
|
|
86
|
+
byte && byte.between?(65, 90)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def identifier_byte?(byte)
|
|
90
|
+
byte && (uppercase_byte?(byte) || byte.between?(97, 122) || byte.between?(48, 57) || byte == 95)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def invalid(message)
|
|
94
|
+
raise ::Arpaka::Error, "invalid class_name: #{message}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
require_relative "../../arpaka"
|
|
6
|
+
|
|
7
|
+
module Arpaka::Ruby
|
|
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 | --parse-y FILE) --output FILE [--class-name NAME] [--ast-type detailed|structured] [--force]"
|
|
15
|
+
opts.on("--ruby-source DIR", "Ruby source tree (provided by the user)") { |v| options[:ruby_source] = v }
|
|
16
|
+
opts.on("--parse-y FILE", "parse.y file (use Arpaka's token definitions)") { |v| options[:parse_y] = v }
|
|
17
|
+
opts.on("--output FILE", "Write a standalone Ruby frontend") { |v| options[:output] = v }
|
|
18
|
+
opts.on("--class-name NAME", "Generated class name (default: RubyParser)") { |v| options[:class_name] = v }
|
|
19
|
+
opts.on("--ast-type TYPE", %w[detailed structured], "AST output type (default: detailed)") { |v| options[:ast_type] = v }
|
|
20
|
+
opts.on("--force", "Replace an existing output file") { force = true }
|
|
21
|
+
opts.on("-h", "--help", "Show usage") { help = true }
|
|
22
|
+
end
|
|
23
|
+
args = argv.dup
|
|
24
|
+
command = args.shift
|
|
25
|
+
if ["--help", "-h"].include?(command)
|
|
26
|
+
out.puts(parser)
|
|
27
|
+
return 0
|
|
28
|
+
end
|
|
29
|
+
raise OptionParser::InvalidArgument, "expected generate" unless command == "generate"
|
|
30
|
+
parser.parse!(args)
|
|
31
|
+
if help
|
|
32
|
+
out.puts(parser)
|
|
33
|
+
return 0
|
|
34
|
+
end
|
|
35
|
+
raise OptionParser::InvalidArgument, args.join(" ") unless args.empty?
|
|
36
|
+
raise OptionParser::InvalidArgument, "specify exactly one of --ruby-source or --parse-y" if options[:ruby_source] == options[:parse_y]
|
|
37
|
+
raise OptionParser::MissingArgument, "--output" unless options[:output]
|
|
38
|
+
%i[output].each do |key|
|
|
39
|
+
raise OptionParser::MissingArgument, "--#{key.to_s.tr('_', '-')}" unless options[key]
|
|
40
|
+
end
|
|
41
|
+
output = File.expand_path(options.delete(:output))
|
|
42
|
+
if !force && (File.exist?(output) || File.symlink?(output))
|
|
43
|
+
raise ::Arpaka::Error, "Output already exists: #{output}; use --force to replace it"
|
|
44
|
+
end
|
|
45
|
+
source = Arpaka::Ruby.generate(**options)
|
|
46
|
+
Tempfile.create([".arpaka-", ".rb"], File.dirname(output)) do |file|
|
|
47
|
+
file.write(source)
|
|
48
|
+
file.flush
|
|
49
|
+
file.chmod(0o644)
|
|
50
|
+
if force
|
|
51
|
+
File.rename(file.path, output)
|
|
52
|
+
else
|
|
53
|
+
# Publish only the complete file, without overwriting a concurrent writer.
|
|
54
|
+
File.link(file.path, output)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
out.puts("Generated #{output}")
|
|
58
|
+
0
|
|
59
|
+
rescue OptionParser::ParseError, Lrama::Ruby::Error, SystemCallError => error
|
|
60
|
+
err.puts("arpaka: #{error.message}")
|
|
61
|
+
1
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "erb"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module Arpaka::Ruby
|
|
8
|
+
class FrontendEmitter
|
|
9
|
+
RuntimeBundle = Data.define(:templates)
|
|
10
|
+
DETAILED_RUNTIME_BUNDLE = RuntimeBundle.new(
|
|
11
|
+
%w[frontend detailed/ast detailed/builder lexer].freeze
|
|
12
|
+
).freeze
|
|
13
|
+
STRUCTURED_RUNTIME_BUNDLE = RuntimeBundle.new(
|
|
14
|
+
%w[frontend detailed/ast detailed/builder lexer structured].freeze
|
|
15
|
+
).freeze
|
|
16
|
+
|
|
17
|
+
def emit(parser:, artifacts:, class_name:, grammar_filename:, ruby_source:,
|
|
18
|
+
runtime_bundle: DETAILED_RUNTIME_BUNDLE, ast_type: :detailed)
|
|
19
|
+
rules = JSON.parse(artifacts.fetch("rules.json"))
|
|
20
|
+
runtime_options = artifacts.fetch("runtime", {})
|
|
21
|
+
runtime = runtime_bundle.templates.map do |template_name|
|
|
22
|
+
template = File.read(File.join(__dir__, "runtime", "#{template_name}.rb.erb"))
|
|
23
|
+
ERB.new(template, trim_mode: "-").result_with_hash(rules: rules, runtime: runtime_options,
|
|
24
|
+
ast_type: ast_type)
|
|
25
|
+
end.join("\n")
|
|
26
|
+
notices = [File.read(File.expand_path("../../../LICENSE.txt", __dir__))]
|
|
27
|
+
%w[COPYING BSDL].each do |name|
|
|
28
|
+
path = ruby_source ? File.join(ruby_source, name) : File.join(__dir__, name)
|
|
29
|
+
notices << File.read(path)
|
|
30
|
+
end
|
|
31
|
+
source = "# frozen_string_literal: true\n" \
|
|
32
|
+
"# Generated by Arpaka #{Lrama::Ruby::VERSION}. Do not edit.\n" \
|
|
33
|
+
"# Ruby source profile: #{ruby_source ? artifacts.fetch('source_profile') : 'parse.y only'}\n" \
|
|
34
|
+
"# parse.y SHA256: #{Digest::SHA256.file(grammar_filename).hexdigest}\n" \
|
|
35
|
+
"#{notices.join("\n").lines.map { |line| "# #{line}" }.join}\n"
|
|
36
|
+
source << wrap_class(class_name, parser, runtime)
|
|
37
|
+
validate(source)
|
|
38
|
+
source
|
|
39
|
+
rescue SyntaxError => error
|
|
40
|
+
raise ::Arpaka::Error, "Invalid generated frontend: #{error.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def validate(source)
|
|
46
|
+
# Compile in an isolated box to validate syntax without executing actions.
|
|
47
|
+
::Ruby::Box.new.eval(source)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def wrap_class(class_name, parser, runtime)
|
|
51
|
+
definition = "class #{class_name.constant}\n#{parser}\n#{runtime}\nend\n"
|
|
52
|
+
class_name.namespaces.reverse_each do |namespace|
|
|
53
|
+
body = definition.lines.map { |line| " #{line}" }.join
|
|
54
|
+
definition = "module #{namespace}\n#{body}end\n"
|
|
55
|
+
end
|
|
56
|
+
definition
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "grammar"
|
|
4
|
+
require_relative "class_name_parser"
|
|
5
|
+
require_relative "frontend_emitter"
|
|
6
|
+
|
|
7
|
+
module Arpaka::Ruby
|
|
8
|
+
class Generator
|
|
9
|
+
def generate(ruby_source: nil, parse_y: nil, class_name:, ast_type: :detailed,
|
|
10
|
+
runtime_bundle: nil)
|
|
11
|
+
if ruby_source.nil? == parse_y.nil?
|
|
12
|
+
raise ::Arpaka::Error, "specify exactly one of ruby_source or parse_y"
|
|
13
|
+
end
|
|
14
|
+
ast_type = case ast_type.to_s
|
|
15
|
+
when "detailed" then :detailed
|
|
16
|
+
when "structured" then :structured
|
|
17
|
+
else
|
|
18
|
+
raise ::Arpaka::Error, "ast_type must be detailed or structured"
|
|
19
|
+
end
|
|
20
|
+
runtime_bundle ||= ast_type == :structured ?
|
|
21
|
+
FrontendEmitter::STRUCTURED_RUNTIME_BUNDLE : FrontendEmitter::DETAILED_RUNTIME_BUNDLE
|
|
22
|
+
class_name = ClassNameParser.parse(class_name)
|
|
23
|
+
unless ::Ruby::Box.enabled?
|
|
24
|
+
raise ::Arpaka::Error, "Ruby Box is required for generation. Start Ruby with RUBY_BOX=1."
|
|
25
|
+
end
|
|
26
|
+
artifacts = Grammar.artifacts(ruby_source: ruby_source, parse_y: parse_y)
|
|
27
|
+
grammar_filename = parse_y || File.join(ruby_source, "parse.y")
|
|
28
|
+
parser = Lrama::Ruby.generate(artifacts.fetch("parse.y"),
|
|
29
|
+
filename: grammar_filename, class_name: "Parser", allow_error_rules: true)
|
|
30
|
+
parser = parser.delete_prefix("# frozen_string_literal: true\n")
|
|
31
|
+
FrontendEmitter.new.emit(parser: parser, artifacts: artifacts, class_name: class_name,
|
|
32
|
+
grammar_filename: grammar_filename, ruby_source: ruby_source, runtime_bundle: runtime_bundle,
|
|
33
|
+
ast_type: ast_type)
|
|
34
|
+
rescue SyntaxError => error
|
|
35
|
+
raise ::Arpaka::Error, "Invalid generated frontend: #{error.message}"
|
|
36
|
+
rescue SystemCallError => error
|
|
37
|
+
raise ::Arpaka::Error, "Cannot read Ruby frontend inputs: #{error.message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -3,14 +3,10 @@
|
|
|
3
3
|
require "lrama"
|
|
4
4
|
require "json"
|
|
5
5
|
require "digest"
|
|
6
|
-
require_relative "
|
|
7
|
-
require_relative "profile/ruby"
|
|
6
|
+
require_relative "grammar_emitter"
|
|
8
7
|
|
|
9
|
-
module Arpaka::
|
|
8
|
+
module Arpaka::Ruby::Grammar
|
|
10
9
|
REVISION = "37d60dd3241d5fdb10ca43f36077f5d556ae1b8d"
|
|
11
|
-
EXTENDED_RULES = {
|
|
12
|
-
["value_expr_command", ["command"]] => "tLBRACE_ARG"
|
|
13
|
-
}.freeze
|
|
14
10
|
def self.parse(source, filename)
|
|
15
11
|
grammar = Lrama::Parser.new(source, filename).parse
|
|
16
12
|
unless grammar.no_stdlib
|
|
@@ -109,57 +105,10 @@ module Arpaka::RubyGrammar
|
|
|
109
105
|
load_parse_y(parse_y)
|
|
110
106
|
end
|
|
111
107
|
names = symbol_names(grammar)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
expected_conflicts = (grammar.expect || 0) + profile.fetch("additional_expected_conflicts", 0)
|
|
115
|
-
lines = ["/* Generated from ruby/ruby #{REVISION}. See Arpaka Action mappings. */",
|
|
116
|
-
"%no-stdlib", "%expect #{expected_conflicts}"]
|
|
117
|
-
lines << "%define lr.type ielr" if grammar.ielr_defined?
|
|
118
|
-
grammar.terms.each do |symbol|
|
|
119
|
-
next if symbol.error_symbol? || symbol.undef_symbol?
|
|
120
|
-
declaration = "%token #{name.call(symbol)} #{symbol.token_id}"
|
|
121
|
-
declaration += " #{symbol.alias_name}" if symbol.alias_name
|
|
122
|
-
lines << declaration
|
|
123
|
-
end
|
|
124
|
-
grammar.precedences.group_by(&:precedence).sort.each do |_level, group|
|
|
125
|
-
lines << "%#{group.first.type} #{group.map(&:s_value).join(' ')}"
|
|
126
|
-
end
|
|
127
|
-
lines << "%left keyword_do_block"
|
|
128
|
-
lines << "%start #{name.call(grammar.rules.first.rhs.first)}"
|
|
129
|
-
lines << "%%"
|
|
130
|
-
metadata = inventory.to_h { |rule| [rule.fetch("id"), rule] }
|
|
131
|
-
rules = grammar.rules.reject(&:initial_rule?)
|
|
132
|
-
rules.each do |rule|
|
|
133
|
-
rhs = rule.rhs.empty? ? "%empty" : rule.rhs.map(&name).join(" ")
|
|
134
|
-
extended_precedence = EXTENDED_RULES[[rule.lhs.id.s_value, rule.rhs.map { |symbol| symbol.id.s_value }]]
|
|
135
|
-
precedence = if rule.precedence_sym
|
|
136
|
-
" %prec #{name.call(rule.precedence_sym)}"
|
|
137
|
-
elsif extended_precedence
|
|
138
|
-
" %prec #{extended_precedence}"
|
|
139
|
-
else
|
|
140
|
-
""
|
|
141
|
-
end
|
|
142
|
-
key = [rule.lhs.id.s_value, rule.rhs.map { |symbol| symbol.id.s_value }]
|
|
143
|
-
action = profile.fetch("actions")[rule.id] || profile.fetch("compatibility_actions", {})[key]
|
|
144
|
-
expression = action&.last || "@builder.unsupported(#{rule.id})"
|
|
145
|
-
lines << "/* upstream parse.y:#{metadata.fetch(rule.id).fetch('line')}: #{rule.as_comment} */"
|
|
146
|
-
lines << "#{name.call(rule.lhs)}: #{rhs}#{precedence} { $$ = #{expression} };"
|
|
147
|
-
end
|
|
148
|
-
lines << "/* arpaka extension: block call as a parenthesized argument */"
|
|
149
|
-
lines << "call_args: block_call { $$ = [$1].freeze };"
|
|
150
|
-
lines << "call_args: args ',' block_call %prec tLOWEST { $$ = ($1 + [$3]).freeze };"
|
|
151
|
-
source = lines.join("\n") + "\n"
|
|
108
|
+
artifacts = ::Arpaka::Ruby::GrammarEmitter.new(grammar: grammar, inventory: inventory, names: names).emit
|
|
109
|
+
source = artifacts.fetch("parse.y")
|
|
152
110
|
verify_structure(grammar, source, names)
|
|
153
|
-
|
|
154
|
-
{ id: rule.id, rule: rule.as_comment,
|
|
155
|
-
line: metadata.fetch(rule.id).fetch("line"),
|
|
156
|
-
status: (profile.fetch("actions")[rule.id] ||
|
|
157
|
-
profile.fetch("compatibility_actions", {})[[rule.lhs.id.s_value,
|
|
158
|
-
rule.rhs.map { |symbol| symbol.id.s_value }]])&.first || "unsupported" }
|
|
159
|
-
end
|
|
160
|
-
{ "parse.y" => source, "rules.json" => JSON.pretty_generate(compact) + "\n",
|
|
161
|
-
"runtime" => profile.fetch("runtime", {}),
|
|
162
|
-
"source_profile" => profile.fetch("source_tag", REVISION) }
|
|
111
|
+
artifacts
|
|
163
112
|
end
|
|
164
113
|
|
|
165
114
|
def self.verify_structure(original, source, names)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "actions"
|
|
5
|
+
require_relative "profile"
|
|
6
|
+
|
|
7
|
+
module Arpaka::Ruby
|
|
8
|
+
class GrammarEmitter
|
|
9
|
+
EXTENDED_RULES = {
|
|
10
|
+
["value_expr_command", ["command"]] => "tLBRACE_ARG"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
def initialize(grammar:, inventory:, names:)
|
|
14
|
+
@grammar = grammar
|
|
15
|
+
@inventory = inventory
|
|
16
|
+
@names = names
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def emit
|
|
20
|
+
name = ->(symbol) { symbol.term ? symbol.id.s_value : @names.fetch(symbol.id.s_value) }
|
|
21
|
+
profile = ::Arpaka::Ruby::Profile.select(@grammar)
|
|
22
|
+
expected_conflicts = (@grammar.expect || 0) + profile.fetch("additional_expected_conflicts", 0)
|
|
23
|
+
lines = ["/* Generated from ruby/ruby #{Arpaka::Ruby::Grammar::REVISION}. See Arpaka Action mappings. */",
|
|
24
|
+
"%no-stdlib", "%expect #{expected_conflicts}"]
|
|
25
|
+
lines << "%define lr.type ielr" if @grammar.ielr_defined?
|
|
26
|
+
@grammar.terms.each do |symbol|
|
|
27
|
+
next if symbol.error_symbol? || symbol.undef_symbol?
|
|
28
|
+
declaration = "%token #{name.call(symbol)} #{symbol.token_id}"
|
|
29
|
+
declaration += " #{symbol.alias_name}" if symbol.alias_name
|
|
30
|
+
lines << declaration
|
|
31
|
+
end
|
|
32
|
+
@grammar.precedences.group_by(&:precedence).sort.each do |_level, group|
|
|
33
|
+
lines << "%#{group.first.type} #{group.map(&:s_value).join(' ')}"
|
|
34
|
+
end
|
|
35
|
+
lines << "%left keyword_do_block"
|
|
36
|
+
lines << "%start #{name.call(@grammar.rules.first.rhs.first)}"
|
|
37
|
+
lines << "%%"
|
|
38
|
+
metadata = @inventory.to_h { |rule| [rule.fetch("id"), rule] }
|
|
39
|
+
rules = @grammar.rules.reject(&:initial_rule?)
|
|
40
|
+
rules.each do |rule|
|
|
41
|
+
rhs = rule.rhs.empty? ? "%empty" : rule.rhs.map(&name).join(" ")
|
|
42
|
+
extended_precedence = EXTENDED_RULES[[rule.lhs.id.s_value, rule.rhs.map { |symbol| symbol.id.s_value }]]
|
|
43
|
+
precedence = if rule.precedence_sym
|
|
44
|
+
" %prec #{name.call(rule.precedence_sym)}"
|
|
45
|
+
elsif extended_precedence
|
|
46
|
+
" %prec #{extended_precedence}"
|
|
47
|
+
else
|
|
48
|
+
""
|
|
49
|
+
end
|
|
50
|
+
key = [rule.lhs.id.s_value, rule.rhs.map { |symbol| symbol.id.s_value }]
|
|
51
|
+
action = profile.fetch("compatibility_actions", {})[key] || profile.fetch("actions")[rule.id]
|
|
52
|
+
expression = action&.last || "@builder.unsupported(#{rule.id})"
|
|
53
|
+
lines << "/* upstream parse.y:#{metadata.fetch(rule.id).fetch('line')}: #{rule.as_comment} */"
|
|
54
|
+
lines << "#{name.call(rule.lhs)}: #{rhs}#{precedence} { $$ = #{expression} };"
|
|
55
|
+
end
|
|
56
|
+
lines << "/* arpaka extension: block call as a parenthesized argument */"
|
|
57
|
+
lines << "call_args: block_call { $$ = [$1].freeze };"
|
|
58
|
+
lines << "call_args: args ',' block_call %prec tLOWEST { $$ = ($1 + [$3]).freeze };"
|
|
59
|
+
source = lines.join("\n") + "\n"
|
|
60
|
+
compact = rules.map do |rule|
|
|
61
|
+
{ id: rule.id, rule: rule.as_comment,
|
|
62
|
+
line: metadata.fetch(rule.id).fetch("line"),
|
|
63
|
+
status: (profile.fetch("compatibility_actions", {})[[rule.lhs.id.s_value,
|
|
64
|
+
rule.rhs.map { |symbol| symbol.id.s_value }]] || profile.fetch("actions")[rule.id])&.first || "unsupported" }
|
|
65
|
+
end
|
|
66
|
+
{ "parse.y" => source, "rules.json" => JSON.pretty_generate(compact) + "\n",
|
|
67
|
+
"runtime" => profile.fetch("runtime", {}),
|
|
68
|
+
"source_profile" => profile.fetch("source_tag", Arpaka::Ruby::Grammar::REVISION) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Arpaka
|
|
6
|
+
module Ruby
|
|
7
|
+
module Profile
|
|
8
|
+
CURRENT = {
|
|
9
|
+
"name" => "ruby-4.0",
|
|
10
|
+
"actions" => ::Arpaka::Ruby::Actions::ACTIONS,
|
|
11
|
+
# Rule numbers are stable only within one parse.y revision. Keep
|
|
12
|
+
# this Ruby 4.0-specific reduction keyed by its grammar signature so
|
|
13
|
+
# the shared action table cannot accidentally apply the old action to
|
|
14
|
+
# a shifted rule number.
|
|
15
|
+
"compatibility_actions" => {
|
|
16
|
+
["top_stmt", ["keyword_BEGIN", "begin_block"]] =>
|
|
17
|
+
["ported", "@builder.begin_block($2)"],
|
|
18
|
+
["stmt_or_begin", ["keyword_BEGIN", "$@5", "begin_block"]] =>
|
|
19
|
+
["ported", "@builder.begin_block($3)"],
|
|
20
|
+
["def_endless_method_endless_command", ["defn_head", "f_opt_paren_args", "'='", "endless_command"]] =>
|
|
21
|
+
["ported", "@builder.def_node($1, $2, @builder.body_nodes($4))"],
|
|
22
|
+
["def_endless_method_endless_command", ["defs_head", "f_opt_paren_args", "'='", "endless_command"]] =>
|
|
23
|
+
["ported", "@builder.singleton_method_node($1[0], $1[2], $2, @builder.body_nodes($4))"],
|
|
24
|
+
["def_endless_method_endless_arg", ["defn_head", "f_opt_paren_args", "'='", "endless_arg"]] =>
|
|
25
|
+
["ported", "@builder.def_node($1, $2, @builder.body_nodes($4))"],
|
|
26
|
+
["def_endless_method_endless_arg", ["defs_head", "f_opt_paren_args", "'='", "endless_arg"]] =>
|
|
27
|
+
["ported", "@builder.singleton_method_node($1[0], $1[2], $2, @builder.body_nodes($4))"],
|
|
28
|
+
["begin_block", ["block_open", "compstmt_top_stmts", "'}'"]] =>
|
|
29
|
+
["pass_through", "$2"],
|
|
30
|
+
["stmt", ["k_END", "block_open", "compstmt_stmts", "'}'"]] =>
|
|
31
|
+
["ported", "@builder.end_block($3)"],
|
|
32
|
+
["mlhs_basic", ["mlhs_head", "tSTAR", "mlhs_node", "','", "mlhs_items_mlhs_item"]] =>
|
|
33
|
+
["ported", "@builder.multiple_assign_post($1, $3, $5)"],
|
|
34
|
+
["mlhs_basic", ["mlhs_head", "tSTAR", "','", "mlhs_items_mlhs_item"]] =>
|
|
35
|
+
["ported", "@builder.multiple_assign_post($1, :NODE_SPECIAL_NO_NAME_REST, $4)"],
|
|
36
|
+
["mlhs_basic", ["tSTAR", "mlhs_node", "','", "mlhs_items_mlhs_item"]] =>
|
|
37
|
+
["ported", "@builder.multiple_assign_post([], $2, $4)"],
|
|
38
|
+
["mlhs_basic", ["tSTAR", "','", "mlhs_items_mlhs_item"]] =>
|
|
39
|
+
["ported", "@builder.multiple_assign_post([], :NODE_SPECIAL_NO_NAME_REST, $3)"],
|
|
40
|
+
["f_opt_primary_value", ["f_arg_asgn", "f_eq", "primary_value"]] =>
|
|
41
|
+
["ported", "@builder.parameter(:optional, $1, $3)"],
|
|
42
|
+
["f_opt_arg_primary_value", ["f_opt_arg_primary_value", "','", "f_opt_primary_value"]] =>
|
|
43
|
+
["ported", "@builder.parameter_list($1, $3)"],
|
|
44
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
45
|
+
["f_arg", "','", "f_opt_arg_primary_value", "','", "f_rest_arg", "opt_args_tail_block_args_tail_none"]] =>
|
|
46
|
+
["ported", "@builder.parameter_list(@builder.append_block_rest(@builder.parameter_list($1, $3), $5), $6)"],
|
|
47
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
48
|
+
["f_arg", "','", "f_opt_arg_primary_value", "','", "f_rest_arg", "','", "f_arg", "opt_args_tail_block_args_tail_none"]] =>
|
|
49
|
+
["ported", "@builder.parameter_list(@builder.append_block_rest(@builder.parameter_list($1, $3), $5, $7), $8)"],
|
|
50
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
51
|
+
["f_arg", "','", "f_opt_arg_primary_value", "opt_args_tail_block_args_tail_none"]] =>
|
|
52
|
+
["ported", "@builder.parameter_list($1, $3, $4)"],
|
|
53
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
54
|
+
["f_arg", "','", "f_opt_arg_primary_value", "','", "f_arg", "opt_args_tail_block_args_tail_none"]] =>
|
|
55
|
+
["ported", "@builder.parameter_list($1, $3, $5, $6)"],
|
|
56
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
57
|
+
["f_arg", "','", "f_rest_arg", "opt_args_tail_block_args_tail_none"]] =>
|
|
58
|
+
["ported", "@builder.parameter_list(@builder.append_block_rest($1, $3), $4)"],
|
|
59
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
60
|
+
["f_arg", "','", "f_rest_arg", "','", "f_arg", "opt_args_tail_block_args_tail_none"]] =>
|
|
61
|
+
["ported", "@builder.parameter_list(@builder.append_block_rest($1, $3, $5), $6)"],
|
|
62
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
63
|
+
["f_opt_arg_primary_value", "','", "f_rest_arg", "opt_args_tail_block_args_tail_none"]] =>
|
|
64
|
+
["ported", "@builder.parameter_list(@builder.append_block_rest([], $1), $3)"],
|
|
65
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
66
|
+
["f_opt_arg_primary_value", "','", "f_rest_arg", "','", "f_arg", "opt_args_tail_block_args_tail_none"]] =>
|
|
67
|
+
["ported", "@builder.parameter_list(@builder.append_block_rest([], $1, $5), $6)"],
|
|
68
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
69
|
+
["f_opt_arg_primary_value", "opt_args_tail_block_args_tail_none"]] =>
|
|
70
|
+
["ported", "@builder.parameter_list($1, $2)"],
|
|
71
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
72
|
+
["f_opt_arg_primary_value", "','", "f_arg", "opt_args_tail_block_args_tail_none"]] =>
|
|
73
|
+
["ported", "@builder.parameter_list($1, $3, $4)"],
|
|
74
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
75
|
+
["f_rest_arg", "opt_args_tail_block_args_tail_none"]] =>
|
|
76
|
+
["ported", "(@builder.append_block_rest([], $1) + Array($2)).freeze"],
|
|
77
|
+
["args-list_primary_value_opt_args_tail_block_args_tail_none",
|
|
78
|
+
["f_rest_arg", "','", "f_arg", "opt_args_tail_block_args_tail_none"]] =>
|
|
79
|
+
["ported", "@builder.parameter_list(@builder.append_block_rest([], $1, $3), $4)"],
|
|
80
|
+
["block_param", ["f_arg", "excessed_comma"]] =>
|
|
81
|
+
["ported", "@builder.parameter_list($1)"],
|
|
82
|
+
["block_param", ["f_arg", "opt_args_tail_block_args_tail_none"]] =>
|
|
83
|
+
["ported", "@builder.parameter_list($1, $2)"],
|
|
84
|
+
["block_param", ["tail-only-args_block_args_tail"]] =>
|
|
85
|
+
["ported", "@builder.parameter_list($1)"],
|
|
86
|
+
}.freeze,
|
|
87
|
+
"additional_expected_conflicts" => 0,
|
|
88
|
+
"runtime" => {
|
|
89
|
+
"ast_nodes" => %w[self assignment singleton_method singleton_class pattern keyword_arguments].freeze,
|
|
90
|
+
"builder_methods" => %w[self assignment singleton_method singleton_class pattern keyword_arguments rescue].freeze,
|
|
91
|
+
"lexer" => { "source_text_tokens" => %w[tOP_ASGN tUMINUS tUPLUS].freeze }.freeze
|
|
92
|
+
}.freeze
|
|
93
|
+
}.freeze
|
|
94
|
+
|
|
95
|
+
def self.select(grammar)
|
|
96
|
+
if grammar.rules.any? { |rule| rule.lhs.id.s_value == "value_expr_command" }
|
|
97
|
+
CURRENT
|
|
98
|
+
elsif grammar.rules.any? { |rule| rule.lhs.id.s_value == "top_compstmt" &&
|
|
99
|
+
rule.rhs.any? { |symbol| symbol.id.s_value == "option_terms" } }
|
|
100
|
+
legacy
|
|
101
|
+
elsif grammar.rules.any? { |rule| rule.lhs.id.s_value == "top_compstmt" &&
|
|
102
|
+
rule.rhs.any? { |symbol| symbol.id.s_value == "opt_terms" } }
|
|
103
|
+
legacy_3_3
|
|
104
|
+
else
|
|
105
|
+
raise ::Arpaka::Error, "Unsupported Ruby parse.y profile"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.legacy
|
|
110
|
+
@legacy ||= load("profiles/ruby_3_4.rb", "ruby-3.4")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.legacy_3_3
|
|
114
|
+
@legacy_3_3 ||= load("profiles/ruby_3_3.rb", "ruby-3.3")
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.load(filename, name)
|
|
118
|
+
box = ::Ruby::Box.new
|
|
119
|
+
profile = box.eval(File.read(File.join(__dir__, filename)))
|
|
120
|
+
unless profile.is_a?(Hash) && profile["name"] == name
|
|
121
|
+
raise ::Arpaka::Error, "Invalid #{name} profile"
|
|
122
|
+
end
|
|
123
|
+
profile.freeze
|
|
124
|
+
rescue SyntaxError, TypeError => error
|
|
125
|
+
raise ::Arpaka::Error, "Ruby profile loading failed: #{error.message}"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
"source_revision" => "343ea050023cfc0374fdea6fdf625b2f57b716a4",
|
|
8
8
|
"source_sha256" => "0347b145280f89b0052aaaa506aa2022c9abdfa2c37677203d53a1a2b3e35861",
|
|
9
9
|
"runtime" => {
|
|
10
|
-
"ast_nodes" => %w[assignment singleton_method singleton_class pattern].freeze,
|
|
11
|
-
"builder_methods" => %w[assignment singleton_method singleton_class rescue].freeze,
|
|
12
|
-
|
|
10
|
+
"ast_nodes" => %w[assignment singleton_method singleton_class pattern keyword_arguments].freeze,
|
|
11
|
+
"builder_methods" => %w[assignment singleton_method singleton_class rescue keyword_arguments].freeze,
|
|
12
|
+
"lexer" => {"source_text_tokens" => %w[tOP_ASGN tUMINUS tUPLUS].freeze}.freeze
|
|
13
13
|
}.freeze,
|
|
14
14
|
"actions" => {
|
|
15
15
|
1 => ["ported", "nil"],
|
|
@@ -37,15 +37,15 @@
|
|
|
37
37
|
23 => ["ported", "nil"],
|
|
38
38
|
24 => ["ported", "$1"],
|
|
39
39
|
25 => ["ported", "nil"],
|
|
40
|
-
26 => ["ported", "@builder.alias_node($2, $4)"],
|
|
41
|
-
27 => ["ported", "@builder.alias_node($2, $3)"],
|
|
42
|
-
28 => ["ported", "@builder.alias_node($2, $3)"],
|
|
43
|
-
29 => ["unsupported", "@builder.unsupported(
|
|
40
|
+
26 => ["ported", "@builder.alias_node(:method, $2, $4)"],
|
|
41
|
+
27 => ["ported", "@builder.alias_node(:variable, $2, $3)"],
|
|
42
|
+
28 => ["ported", "@builder.alias_node(:variable, $2, $3)"],
|
|
43
|
+
29 => ["unsupported", "@builder.unsupported(29)"],
|
|
44
44
|
30 => ["ported", "@builder.undef_node($2)"],
|
|
45
|
-
31 => ["ported", "@builder.
|
|
46
|
-
32 => ["ported", "@builder.
|
|
47
|
-
33 => ["ported", "@builder.
|
|
48
|
-
34 => ["ported", "@builder.
|
|
45
|
+
31 => ["ported", "@builder.modifier_if_node($3, [$1])"],
|
|
46
|
+
32 => ["ported", "@builder.modifier_unless_node($3, [$1])"],
|
|
47
|
+
33 => ["ported", "@builder.modifier_loop(:while, $3, [$1])"],
|
|
48
|
+
34 => ["ported", "@builder.modifier_loop(:until, $3, [$1])"],
|
|
49
49
|
35 => ["ported", "@builder.rescue_modifier($1, $4)"],
|
|
50
50
|
36 => ["ported", "@builder.end_block($4)"],
|
|
51
51
|
37 => ["ported", "$1"],
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
50 => ["ported", "@builder.opassign(@builder.field($1, $2, $3), $4, $6)"],
|
|
65
65
|
51 => ["ported", "@builder.def_node($1, $2, $4)"],
|
|
66
66
|
52 => ["ported", "@builder.singleton_method_node($1[0], $1[2], $2, $4)"],
|
|
67
|
-
53 => ["unsupported", "@builder.unsupported(
|
|
67
|
+
53 => ["unsupported", "@builder.unsupported(53)"],
|
|
68
68
|
54 => ["ported", "$1"],
|
|
69
69
|
55 => ["ported", "@builder.rescue_modifier($1, $4)"],
|
|
70
70
|
56 => ["ported", "@builder.unary(\":!\", $3)"],
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
89 => ["ported", "@builder.receiver_call($1, :\"::\", $3, $4)"],
|
|
104
104
|
90 => ["ported", "@builder.block_call(@builder.receiver_call($1, :\"::\", $3, $4), $5)"],
|
|
105
105
|
91 => ["ported", "@builder.block_call(@builder.receiver_call($1, :\"::\", $3, []), $5)"],
|
|
106
|
-
92 => ["ported", "@builder.
|
|
106
|
+
92 => ["ported", "@builder.super_call($2 || [])"],
|
|
107
107
|
93 => ["ported", "@builder.call(:yield, $2 || [])"],
|
|
108
108
|
94 => ["ported", "@builder.call(:return, $2 || [])"],
|
|
109
109
|
95 => ["ported", "@builder.call(:break, $2 || [])"],
|
|
@@ -186,8 +186,8 @@
|
|
|
186
186
|
172 => ["ported", "@builder.operator(\"**\")"],
|
|
187
187
|
173 => ["ported", "@builder.operator(\"!\")"],
|
|
188
188
|
174 => ["ported", "@builder.operator(\"~\")"],
|
|
189
|
-
175 => ["ported", "@builder.operator(\"+@\")"],
|
|
190
|
-
176 => ["ported", "@builder.operator(\"-@\")"],
|
|
189
|
+
175 => ["ported", "@builder.operator($1 || \"+@\")"],
|
|
190
|
+
176 => ["ported", "@builder.operator($1 || \"-@\")"],
|
|
191
191
|
177 => ["ported", "@builder.operator(\"[]\")"],
|
|
192
192
|
178 => ["ported", "@builder.operator(\"[]=\")"],
|
|
193
193
|
179 => ["ported", "@builder.operator(\"`\")"],
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
226 => ["ported", "@builder.opassign(@builder.field($1, $2, $3), $4, $6)"],
|
|
241
241
|
227 => ["ported", "@builder.opassign(@builder.field($1, :\"::\", $3), $4, $6)"],
|
|
242
242
|
228 => ["ported", "@builder.opassign(@builder.field(nil, :\"::\", $2), $3, $5)"],
|
|
243
|
-
229 => ["unsupported", "@builder.unsupported(
|
|
243
|
+
229 => ["unsupported", "@builder.unsupported(229)"],
|
|
244
244
|
230 => ["ported", "@builder.range(:\"..\", $1, $3)"],
|
|
245
245
|
231 => ["ported", "@builder.range(:\"...\", $1, $3)"],
|
|
246
246
|
232 => ["ported", "@builder.range(:\"..\", $1, nil)"],
|
|
@@ -484,8 +484,8 @@
|
|
|
484
484
|
470 => ["ported", "@builder.receiver_call($1, :\"::\", $3, [])"],
|
|
485
485
|
471 => ["ported", "$1"],
|
|
486
486
|
472 => ["ported", "$1"],
|
|
487
|
-
|
|
488
|
-
|
|
487
|
+
473 => ["ported", "@builder.super_call($2 || [])"],
|
|
488
|
+
474 => ["ported", "@builder.super_call(nil)"],
|
|
489
489
|
475 => ["ported", "@builder.index($1, $3 || [])"],
|
|
490
490
|
476 => ["ported", "$2"],
|
|
491
491
|
477 => ["ported", "$2"],
|
|
@@ -508,8 +508,8 @@
|
|
|
508
508
|
494 => ["ported", "$1"],
|
|
509
509
|
495 => ["ported", "$1"],
|
|
510
510
|
496 => ["ported", "$1"],
|
|
511
|
-
497 => ["ported", "@builder.
|
|
512
|
-
498 => ["ported", "@builder.
|
|
511
|
+
497 => ["ported", "@builder.modifier_if_node($3, [$1])"],
|
|
512
|
+
498 => ["ported", "@builder.modifier_unless_node($3, [$1])"],
|
|
513
513
|
499 => ["ported", "$1"],
|
|
514
514
|
500 => ["ported", "@builder.pattern(:array, [nil, [$1], nil, nil])"],
|
|
515
515
|
501 => ["ported", "@builder.pattern(:array, [[$1] + Array($3[0]), $3[1], $3[2]])"],
|
|
@@ -615,7 +615,7 @@
|
|
|
615
615
|
601 => ["ported", "@builder.concat_strings($1, $2)"],
|
|
616
616
|
602 => ["ported", "@builder.string(@builder.join_strings($2))"],
|
|
617
617
|
603 => ["ported", "@builder.xstring(@builder.join_strings($2))"],
|
|
618
|
-
604 => ["ported", "@builder.regexp(@builder.join_strings($2))"],
|
|
618
|
+
604 => ["ported", "@builder.regexp(@builder.join_strings($2), $3)"],
|
|
619
619
|
605 => ["ported", "$1"],
|
|
620
620
|
606 => ["ported", "$1"],
|
|
621
621
|
607 => ["ported", "@builder.word_array($3 || [])"],
|