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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -2
  3. data/README.md +104 -7
  4. data/Rakefile +15 -1
  5. data/docs/architecture.dot +30 -0
  6. data/docs/architecture.md +96 -0
  7. data/docs/architecture.webp +0 -0
  8. data/docs/contributes.md +115 -0
  9. data/docs/usage.md +497 -0
  10. data/lib/arpaka/cli.rb +5 -53
  11. data/lib/arpaka/plugins/cli.rb +61 -0
  12. data/lib/arpaka/plugins/sample.rb +14 -0
  13. data/lib/arpaka/plugins/sample_runner.rb +21 -0
  14. data/lib/arpaka/{actions.rb → ruby/actions.rb} +230 -151
  15. data/lib/arpaka/ruby/class_name_parser.rb +97 -0
  16. data/lib/arpaka/ruby/cli.rb +64 -0
  17. data/lib/arpaka/ruby/frontend_emitter.rb +59 -0
  18. data/lib/arpaka/ruby/generator.rb +41 -0
  19. data/lib/arpaka/{ruby_grammar.rb → ruby/grammar.rb} +5 -56
  20. data/lib/arpaka/ruby/grammar_emitter.rb +71 -0
  21. data/lib/arpaka/ruby/profile.rb +129 -0
  22. data/lib/arpaka/{profile → ruby/profiles}/ruby_3_3.rb +21 -21
  23. data/lib/arpaka/{profile → ruby/profiles}/ruby_3_4.rb +21 -21
  24. data/lib/arpaka/{runtime → ruby/runtime/detailed}/ast.rb.erb +41 -5
  25. data/lib/arpaka/ruby/runtime/detailed/builder.rb.erb +1177 -0
  26. data/lib/arpaka/{runtime → ruby/runtime}/frontend.rb.erb +17 -4
  27. data/lib/arpaka/{runtime → ruby/runtime}/lexer.rb.erb +576 -99
  28. data/lib/arpaka/ruby/runtime/structured.rb.erb +111 -0
  29. data/lib/arpaka/ruby.rb +23 -0
  30. data/lib/arpaka.rb +6 -9
  31. data/lib/lrama/ruby/backend.rb +4 -9
  32. data/lib/lrama/ruby/parser.rb.erb +35 -9
  33. data/lib/lrama/ruby/parser_emitter.rb +41 -0
  34. data/lib/lrama/ruby/table_codec.rb +50 -0
  35. data/lib/lrama/ruby/version.rb +1 -1
  36. data/lib/lrama/ruby.rb +1 -0
  37. data/sig/arpaka.rbs +8 -3
  38. metadata +32 -16
  39. data/lib/arpaka/generator.rb +0 -51
  40. data/lib/arpaka/profile/ruby.rb +0 -49
  41. data/lib/arpaka/runtime/builder.rb.erb +0 -323
  42. /data/lib/arpaka/{BSDL → ruby/BSDL} +0 -0
  43. /data/lib/arpaka/{COPYING → ruby/COPYING} +0 -0
  44. /data/lib/arpaka/{source.json → ruby/source.json} +0 -0
  45. /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 "actions"
7
- require_relative "profile/ruby"
6
+ require_relative "grammar_emitter"
8
7
 
9
- module Arpaka::RubyGrammar
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
- name = ->(symbol) { symbol.term ? symbol.id.s_value : names.fetch(symbol.id.s_value) }
113
- profile = ::Arpaka::Profile::RUBY.select(grammar)
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
- compact = rules.map do |rule|
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
- "lexer" => {"source_text_tokens" => ["tOP_ASGN"].freeze}.freeze
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(31)"],
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.if_node($3, [$1], nil)"],
46
- 32 => ["ported", "@builder.unless_node($3, [$1], nil)"],
47
- 33 => ["ported", "@builder.loop(:while, $3, [$1])"],
48
- 34 => ["ported", "@builder.loop(:until, $3, [$1])"],
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(56)"],
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.call(:super, $2 || [])"],
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(234)"],
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
- 473 => ["ported", "@builder.call(:super, $2 || [])"],
488
- 474 => ["ported", "@builder.call(:super, [])"],
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.if_node($3, [$1], nil)"],
512
- 498 => ["ported", "@builder.unless_node($3, [$1], nil)"],
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 || [])"],