arpaka 0.1.1 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9c4d3fb152bbd9bb8a5c00037371b5387d8d72027dfe9aacef3032364fff434
4
- data.tar.gz: 39037d6380101b5f89d24d2c397428377fe44180e77215c59c5967349a48256e
3
+ metadata.gz: 5a9e8ba3375a56453551778e540a98c57737d596ddd67d1121b040011d356c5c
4
+ data.tar.gz: af951a8d45255b4adb3531ccde464d0a20427fc58067bae9d9c2fb88595c0067
5
5
  SHA512:
6
- metadata.gz: c0505eecc22775c6bb8f84932382d7274a80372541c0a68d65c6dc2c1203c37251ef91c063f94d38313d934b1d6e9143a27d34640413d5428344721d6449b209
7
- data.tar.gz: 598e86005c31fbbd8cc98374c1c3dbedcb6feb1f97087dfaf3ab12461fc40b64e31c7b2f538eed382c726b19ab771f2edf2f1acfca25c1deef7f98fb74ef605c
6
+ metadata.gz: 84c9d1d486ce1dc6352c47f191396bd7d9b93d23c4938a0d62bdd0080583102ab13e2d7c363c99b72043607ef813c94a81fee842ea255c2ed6f2dc02f594e8ae
7
+ data.tar.gz: 86586ad0c9d3bba004a276c0691c4bb64c6d479f4a9330e7a1cd6bdf4bed17d7a545716fc5caf217e2497070ad6f7d0f5ecb4b23a9aa1cae88bcdbcd9d234939
data/README.md CHANGED
@@ -150,6 +150,20 @@ preserved unless `--force` is supplied. A failed generation leaves the previous
150
150
  file intact. The output includes its source profile, generator version and
151
151
  copyright/license notices.
152
152
 
153
+ When only `parse.y` is available, Arpaka can replace its `RUBY_TOKEN(NAME)`
154
+ declarations with token names and generate a frontend without the rest of the
155
+ Ruby source tree:
156
+
157
+ ```sh
158
+ RUBY_BOX=1 bundle exec arpaka generate \
159
+ --parse-y /path/to/parse.y \
160
+ --class-name MyRubyParser \
161
+ --output lib/my_library/ruby_parser.rb
162
+ ```
163
+
164
+ This mode embeds Arpaka's own license notices. The `--ruby-source` mode remains
165
+ the compatibility-preserving path when the Ruby source tree is available.
166
+
153
167
  The consumer only needs the generated file and Ruby 4.0 or later:
154
168
 
155
169
  ```ruby
data/lib/arpaka/cli.rb CHANGED
@@ -11,8 +11,9 @@ module Arpaka
11
11
  force = false
12
12
  help = false
13
13
  parser = OptionParser.new do |opts|
14
- opts.banner = "Usage: arpaka generate --ruby-source DIR --output FILE [--class-name NAME] [--force]"
14
+ opts.banner = "Usage: arpaka generate (--ruby-source DIR | --parse-y FILE) --output FILE [--class-name NAME] [--force]"
15
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 }
16
17
  opts.on("--output FILE", "Write a standalone Ruby frontend") { |v| options[:output] = v }
17
18
  opts.on("--class-name NAME", "Generated class name (default: RubyParser)") { |v| options[:class_name] = v }
18
19
  opts.on("--force", "Replace an existing output file") { force = true }
@@ -31,7 +32,9 @@ module Arpaka
31
32
  return 0
32
33
  end
33
34
  raise OptionParser::InvalidArgument, args.join(" ") unless args.empty?
34
- %i[ruby_source output].each do |key|
35
+ raise OptionParser::InvalidArgument, "specify exactly one of --ruby-source or --parse-y" if options[:ruby_source] == options[:parse_y]
36
+ raise OptionParser::MissingArgument, "--output" unless options[:output]
37
+ %i[output].each do |key|
35
38
  raise OptionParser::MissingArgument, "--#{key.to_s.tr('_', '-')}" unless options[key]
36
39
  end
37
40
  output = File.expand_path(options.delete(:output))
@@ -6,28 +6,36 @@ require_relative "ruby_grammar"
6
6
 
7
7
  module Arpaka
8
8
  class Generator
9
- def generate(ruby_source:, class_name:)
9
+ def generate(ruby_source: nil, parse_y: nil, class_name:)
10
+ if ruby_source.nil? == parse_y.nil?
11
+ raise Error, "specify exactly one of ruby_source or parse_y"
12
+ end
10
13
  unless /\A[A-Z][a-zA-Z0-9_]*\z/.match?(class_name)
11
14
  raise Error, "class_name must be a single Ruby constant name"
12
15
  end
13
16
  unless ::Ruby::Box.enabled?
14
17
  raise Error, "Ruby Box is required for generation. Start Ruby with RUBY_BOX=1."
15
18
  end
16
- artifacts = RubyGrammar.artifacts(ruby_source: ruby_source)
19
+ artifacts = RubyGrammar.artifacts(ruby_source: ruby_source, parse_y: parse_y)
20
+ grammar_filename = parse_y || File.join(ruby_source, "parse.y")
17
21
  parser = Lrama::Ruby.generate(artifacts.fetch("parse.y"),
18
- filename: File.join(ruby_source, "parse.y"), class_name: "Parser", allow_error_rules: true)
22
+ filename: grammar_filename, class_name: "Parser", allow_error_rules: true)
19
23
  parser = parser.delete_prefix("# frozen_string_literal: true\n")
20
24
  rules = JSON.parse(artifacts.fetch("rules.json"))
25
+ runtime_options = artifacts.fetch("runtime", {})
21
26
  runtime = %w[frontend ast builder lexer].map do |name|
22
27
  template = File.read(File.join(__dir__, "runtime", "#{name}.rb.erb"))
23
- name == "builder" ? ERB.new(template).result_with_hash(rules: rules) : template
28
+ ERB.new(template, trim_mode: "-").result_with_hash(rules: rules, runtime: runtime_options)
24
29
  end.join("\n")
25
30
  notices = [File.read(File.expand_path("../../LICENSE.txt", __dir__))]
26
- %w[COPYING BSDL].each { |name| notices << File.read(File.join(ruby_source, name)) }
31
+ %w[COPYING BSDL].each do |name|
32
+ path = ruby_source ? File.join(ruby_source, name) : File.join(__dir__, name)
33
+ notices << File.read(path)
34
+ end
27
35
  source = "# frozen_string_literal: true\n" \
28
36
  "# Generated by Arpaka #{Lrama::Ruby::VERSION}. Do not edit.\n" \
29
- "# Ruby source profile: #{RubyGrammar::REVISION}\n" \
30
- "# parse.y SHA256: #{Digest::SHA256.file(File.join(ruby_source, 'parse.y')).hexdigest}\n" \
37
+ "# Ruby source profile: #{ruby_source ? artifacts.fetch('source_profile') : 'parse.y only'}\n" \
38
+ "# parse.y SHA256: #{Digest::SHA256.file(grammar_filename).hexdigest}\n" \
31
39
  "#{notices.join("\n").lines.map { |line| "# #{line}" }.join}\n" \
32
40
  "class #{class_name}\n#{parser}\n#{runtime}\nend\n"
33
41
  # Compile in an isolated box to validate syntax without executing actions.
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Arpaka
6
+ module Profile
7
+ module RUBY
8
+ CURRENT = {
9
+ "name" => "ruby-4.0",
10
+ "actions" => ::Arpaka::RubyGrammarActions::ACTIONS,
11
+ "additional_expected_conflicts" => 0,
12
+ "runtime" => {}.freeze
13
+ }.freeze
14
+
15
+ def self.select(grammar)
16
+ if grammar.rules.any? { |rule| rule.lhs.id.s_value == "value_expr_command" }
17
+ CURRENT
18
+ elsif grammar.rules.any? { |rule| rule.lhs.id.s_value == "top_compstmt" &&
19
+ rule.rhs.any? { |symbol| symbol.id.s_value == "option_terms" } }
20
+ legacy
21
+ elsif grammar.rules.any? { |rule| rule.lhs.id.s_value == "top_compstmt" &&
22
+ rule.rhs.any? { |symbol| symbol.id.s_value == "opt_terms" } }
23
+ legacy_3_3
24
+ else
25
+ raise ::Arpaka::Error, "Unsupported Ruby parse.y profile"
26
+ end
27
+ end
28
+
29
+ def self.legacy
30
+ @legacy ||= load("ruby_3_4.rb", "ruby-3.4")
31
+ end
32
+
33
+ def self.legacy_3_3
34
+ @legacy_3_3 ||= load("ruby_3_3.rb", "ruby-3.3")
35
+ end
36
+
37
+ def self.load(filename, name)
38
+ box = ::Ruby::Box.new
39
+ profile = box.eval(File.read(File.join(__dir__, filename)))
40
+ unless profile.is_a?(Hash) && profile["name"] == name
41
+ raise ::Arpaka::Error, "Invalid #{name} profile"
42
+ end
43
+ profile.freeze
44
+ rescue SyntaxError, TypeError => error
45
+ raise ::Arpaka::Error, "Ruby profile loading failed: #{error.message}"
46
+ end
47
+ end
48
+ end
49
+ end