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 +4 -4
- data/README.md +14 -0
- data/lib/arpaka/cli.rb +5 -2
- data/lib/arpaka/generator.rb +15 -7
- data/lib/arpaka/profile/ruby.rb +49 -0
- data/lib/arpaka/profile/ruby_3_3.rb +800 -0
- data/lib/arpaka/profile/ruby_3_4.rb +802 -0
- data/lib/arpaka/ruby_grammar.rb +61 -16
- data/lib/arpaka/runtime/ast.rb.erb +21 -0
- data/lib/arpaka/runtime/builder.rb.erb +76 -0
- data/lib/arpaka/runtime/lexer.rb.erb +7 -0
- data/lib/arpaka.rb +4 -4
- data/lib/lrama/ruby/version.rb +1 -1
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5a9e8ba3375a56453551778e540a98c57737d596ddd67d1121b040011d356c5c
|
|
4
|
+
data.tar.gz: af951a8d45255b4adb3531ccde464d0a20427fc58067bae9d9c2fb88595c0067
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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))
|
data/lib/arpaka/generator.rb
CHANGED
|
@@ -6,28 +6,36 @@ require_relative "ruby_grammar"
|
|
|
6
6
|
|
|
7
7
|
module Arpaka
|
|
8
8
|
class Generator
|
|
9
|
-
def generate(ruby_source
|
|
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:
|
|
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
|
-
|
|
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
|
|
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: #{
|
|
30
|
-
"# parse.y SHA256: #{Digest::SHA256.file(
|
|
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
|