arpaka 0.1.1 → 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 +118 -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 -50
- 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} +53 -59
- data/lib/arpaka/ruby/grammar_emitter.rb +71 -0
- data/lib/arpaka/ruby/profile.rb +129 -0
- data/lib/arpaka/ruby/profiles/ruby_3_3.rb +800 -0
- data/lib/arpaka/ruby/profiles/ruby_3_4.rb +802 -0
- data/lib/arpaka/ruby/runtime/detailed/ast.rb.erb +92 -0
- 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 +583 -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 +46 -13
- data/lib/arpaka/generator.rb +0 -43
- data/lib/arpaka/runtime/ast.rb.erb +0 -35
- data/lib/arpaka/runtime/builder.rb.erb +0 -247
- /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,16 +3,10 @@
|
|
|
3
3
|
require "lrama"
|
|
4
4
|
require "json"
|
|
5
5
|
require "digest"
|
|
6
|
-
|
|
7
|
-
require "rbconfig"
|
|
8
|
-
require_relative "actions"
|
|
6
|
+
require_relative "grammar_emitter"
|
|
9
7
|
|
|
10
|
-
module Arpaka::
|
|
8
|
+
module Arpaka::Ruby::Grammar
|
|
11
9
|
REVISION = "37d60dd3241d5fdb10ca43f36077f5d556ae1b8d"
|
|
12
|
-
EXTENDED_RULES = {
|
|
13
|
-
["value_expr_command", ["command"]] => "tLBRACE_ARG"
|
|
14
|
-
}.freeze
|
|
15
|
-
|
|
16
10
|
def self.parse(source, filename)
|
|
17
11
|
grammar = Lrama::Parser.new(source, filename).parse
|
|
18
12
|
unless grammar.no_stdlib
|
|
@@ -35,10 +29,20 @@ module Arpaka::RubyGrammar
|
|
|
35
29
|
warn "Arpaka: Ruby source differs from the recorded profile: #{path}; " \
|
|
36
30
|
"expected SHA256 #{digest}, got #{actual}."
|
|
37
31
|
end
|
|
38
|
-
source
|
|
39
|
-
File.join(vendor, "
|
|
40
|
-
raise ::Arpaka::Error, "Ruby preprocessing failed: #{error}" unless result.success?
|
|
32
|
+
source = preprocess(File.read(File.join(vendor, "parse.y")),
|
|
33
|
+
File.join(vendor, "defs/id.def"))
|
|
41
34
|
grammar = parse(source, File.join(vendor, "parse.y"))
|
|
35
|
+
[grammar, inventory(grammar)]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.load_parse_y(parse_y)
|
|
39
|
+
path = File.expand_path(parse_y)
|
|
40
|
+
source = preprocess_names(File.read(path))
|
|
41
|
+
grammar = parse(source, path)
|
|
42
|
+
[grammar, inventory(grammar)]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.inventory(grammar)
|
|
42
46
|
inventory = grammar.rules.reject(&:initial_rule?).map do |rule|
|
|
43
47
|
{ "id" => rule.id, "lhs" => rule.lhs.id.s_value,
|
|
44
48
|
"rhs" => rule.rhs.map { |symbol| symbol.id.s_value },
|
|
@@ -47,9 +51,34 @@ module Arpaka::RubyGrammar
|
|
|
47
51
|
"midrule_position" => rule.position_in_original_rule_rhs,
|
|
48
52
|
"precedence" => rule.precedence_sym&.id&.s_value }
|
|
49
53
|
end
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
inventory
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Ruby's id.def is Ruby code. Evaluate it in a Box so its temporary locals,
|
|
58
|
+
# methods and constants cannot affect Arpaka or Lrama's process.
|
|
59
|
+
def self.preprocess(source, id_def)
|
|
60
|
+
box = ::Ruby::Box.new
|
|
61
|
+
ids = box.eval(File.read(id_def))
|
|
62
|
+
tokens = ids.fetch(:token_op).each_with_object({}) do |(_id, _op, token, id), result|
|
|
63
|
+
result[token] = id if token
|
|
64
|
+
end
|
|
65
|
+
return source unless tokens.any?
|
|
66
|
+
|
|
67
|
+
names = tokens.keys.map { |name| Regexp.escape(name) }.join("|")
|
|
68
|
+
token_pattern = /\bRUBY_TOKEN\((#{names})\)\s*(?=\s)/
|
|
69
|
+
source.lines.map do |line|
|
|
70
|
+
if line.start_with?("%token")
|
|
71
|
+
line.gsub(token_pattern) { tokens.fetch(Regexp.last_match(1)).to_s }
|
|
72
|
+
else
|
|
73
|
+
line
|
|
74
|
+
end
|
|
75
|
+
end.join
|
|
76
|
+
rescue KeyError, TypeError, SyntaxError => error
|
|
77
|
+
raise ::Arpaka::Error, "Ruby id.def preprocessing failed: #{error.message}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.preprocess_names(source)
|
|
81
|
+
source.gsub(/\bRUBY_TOKEN\(([A-Za-z_][A-Za-z0-9_]*)\)/, '\\1')
|
|
53
82
|
end
|
|
54
83
|
|
|
55
84
|
def self.symbol_names(grammar)
|
|
@@ -69,52 +98,17 @@ module Arpaka::RubyGrammar
|
|
|
69
98
|
end
|
|
70
99
|
end
|
|
71
100
|
|
|
72
|
-
def self.artifacts(ruby_source:)
|
|
73
|
-
grammar, inventory =
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"%no-stdlib", "%expect #{grammar.expect || 0}"]
|
|
78
|
-
lines << "%define lr.type ielr" if grammar.ielr_defined?
|
|
79
|
-
grammar.terms.each do |symbol|
|
|
80
|
-
next if symbol.error_symbol? || symbol.undef_symbol?
|
|
81
|
-
declaration = "%token #{name.call(symbol)} #{symbol.token_id}"
|
|
82
|
-
declaration += " #{symbol.alias_name}" if symbol.alias_name
|
|
83
|
-
lines << declaration
|
|
84
|
-
end
|
|
85
|
-
grammar.precedences.group_by(&:precedence).sort.each do |_level, group|
|
|
86
|
-
lines << "%#{group.first.type} #{group.map(&:s_value).join(' ')}"
|
|
101
|
+
def self.artifacts(ruby_source: nil, parse_y: nil)
|
|
102
|
+
grammar, inventory = if ruby_source
|
|
103
|
+
load_upstream(ruby_source)
|
|
104
|
+
else
|
|
105
|
+
load_parse_y(parse_y)
|
|
87
106
|
end
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
metadata = inventory.to_h { |rule| [rule.fetch("id"), rule] }
|
|
92
|
-
rules = grammar.rules.reject(&:initial_rule?)
|
|
93
|
-
rules.each do |rule|
|
|
94
|
-
rhs = rule.rhs.empty? ? "%empty" : rule.rhs.map(&name).join(" ")
|
|
95
|
-
extended_precedence = EXTENDED_RULES[[rule.lhs.id.s_value, rule.rhs.map { |symbol| symbol.id.s_value }]]
|
|
96
|
-
precedence = if rule.precedence_sym
|
|
97
|
-
" %prec #{name.call(rule.precedence_sym)}"
|
|
98
|
-
elsif extended_precedence
|
|
99
|
-
" %prec #{extended_precedence}"
|
|
100
|
-
else
|
|
101
|
-
""
|
|
102
|
-
end
|
|
103
|
-
expression = ::Arpaka::RubyGrammarActions::ACTIONS[rule.id]&.last || "@builder.unsupported(#{rule.id})"
|
|
104
|
-
lines << "/* upstream parse.y:#{metadata.fetch(rule.id).fetch('line')}: #{rule.as_comment} */"
|
|
105
|
-
lines << "#{name.call(rule.lhs)}: #{rhs}#{precedence} { $$ = #{expression} };"
|
|
106
|
-
end
|
|
107
|
-
lines << "/* arpaka extension: block call as a parenthesized argument */"
|
|
108
|
-
lines << "call_args: block_call { $$ = [$1].freeze };"
|
|
109
|
-
lines << "call_args: args ',' block_call %prec tLOWEST { $$ = ($1 + [$3]).freeze };"
|
|
110
|
-
source = lines.join("\n") + "\n"
|
|
107
|
+
names = symbol_names(grammar)
|
|
108
|
+
artifacts = ::Arpaka::Ruby::GrammarEmitter.new(grammar: grammar, inventory: inventory, names: names).emit
|
|
109
|
+
source = artifacts.fetch("parse.y")
|
|
111
110
|
verify_structure(grammar, source, names)
|
|
112
|
-
|
|
113
|
-
{ id: rule.id, rule: rule.as_comment,
|
|
114
|
-
line: metadata.fetch(rule.id).fetch("line"),
|
|
115
|
-
status: ::Arpaka::RubyGrammarActions::ACTIONS[rule.id]&.first || "unsupported" }
|
|
116
|
-
end
|
|
117
|
-
{ "parse.y" => source, "rules.json" => JSON.pretty_generate(compact) + "\n" }
|
|
111
|
+
artifacts
|
|
118
112
|
end
|
|
119
113
|
|
|
120
114
|
def self.verify_structure(original, source, names)
|
|
@@ -127,7 +121,7 @@ module Arpaka::RubyGrammar
|
|
|
127
121
|
end.sort_by(&:inspect)
|
|
128
122
|
end
|
|
129
123
|
expected_rules = signature.call(original, names).map do |rule|
|
|
130
|
-
if rule[0] == names.fetch("value_expr_command") && rule[1] == [names.fetch("command")]
|
|
124
|
+
if names.key?("value_expr_command") && rule[0] == names.fetch("value_expr_command") && rule[1] == [names.fetch("command")]
|
|
131
125
|
rule[0, 2] + ["tLBRACE_ARG"]
|
|
132
126
|
else
|
|
133
127
|
rule
|
|
@@ -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
|