arpaka 0.1.0 → 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.
@@ -3,16 +3,14 @@
3
3
  require "lrama"
4
4
  require "json"
5
5
  require "digest"
6
- require "open3"
7
- require "rbconfig"
8
6
  require_relative "actions"
7
+ require_relative "profile/ruby"
9
8
 
10
9
  module Arpaka::RubyGrammar
11
10
  REVISION = "37d60dd3241d5fdb10ca43f36077f5d556ae1b8d"
12
11
  EXTENDED_RULES = {
13
12
  ["value_expr_command", ["command"]] => "tLBRACE_ARG"
14
13
  }.freeze
15
-
16
14
  def self.parse(source, filename)
17
15
  grammar = Lrama::Parser.new(source, filename).parse
18
16
  unless grammar.no_stdlib
@@ -32,14 +30,23 @@ module Arpaka::RubyGrammar
32
30
  actual = Digest::SHA256.file(File.join(vendor, path)).hexdigest
33
31
  next if actual == digest
34
32
 
35
- raise ::Arpaka::Error, "Unsupported Ruby source change: #{path}; " \
36
- "expected SHA256 #{digest}, got #{actual}. " \
37
- "Update the Action mappings and source profile before regenerating."
33
+ warn "Arpaka: Ruby source differs from the recorded profile: #{path}; " \
34
+ "expected SHA256 #{digest}, got #{actual}."
38
35
  end
39
- source, error, result = Open3.capture3(RbConfig.ruby,
40
- File.join(vendor, "tool/id2token.rb"), File.join(vendor, "parse.y"))
41
- raise ::Arpaka::Error, "Ruby preprocessing failed: #{error}" unless result.success?
36
+ source = preprocess(File.read(File.join(vendor, "parse.y")),
37
+ File.join(vendor, "defs/id.def"))
42
38
  grammar = parse(source, File.join(vendor, "parse.y"))
39
+ [grammar, inventory(grammar)]
40
+ end
41
+
42
+ def self.load_parse_y(parse_y)
43
+ path = File.expand_path(parse_y)
44
+ source = preprocess_names(File.read(path))
45
+ grammar = parse(source, path)
46
+ [grammar, inventory(grammar)]
47
+ end
48
+
49
+ def self.inventory(grammar)
43
50
  inventory = grammar.rules.reject(&:initial_rule?).map do |rule|
44
51
  { "id" => rule.id, "lhs" => rule.lhs.id.s_value,
45
52
  "rhs" => rule.rhs.map { |symbol| symbol.id.s_value },
@@ -48,20 +55,34 @@ module Arpaka::RubyGrammar
48
55
  "midrule_position" => rule.position_in_original_rule_rhs,
49
56
  "precedence" => rule.precedence_sym&.id&.s_value }
50
57
  end
51
- expected = JSON.parse(File.read(File.join(__dir__, "upstream_rules.json")))
52
- unless inventory == expected
53
- changed = (0...[inventory.length, expected.length].max).select { |i| inventory[i] != expected[i] }
54
- details = changed.first(5).map do |i|
55
- before, after = expected[i], inventory[i]
56
- "rule #{before&.fetch('id') || after.fetch('id')}: " \
57
- "#{before&.fetch('lhs') || '(absent)'} -> #{after&.fetch('lhs') || '(absent)'}"
58
- end
59
- raise ::Arpaka::Error, "Upstream rules changed (#{changed.length} entries); " \
60
- "review Action mappings: #{details.join('; ')}"
58
+ inventory
59
+ end
60
+
61
+ # Ruby's id.def is Ruby code. Evaluate it in a Box so its temporary locals,
62
+ # methods and constants cannot affect Arpaka or Lrama's process.
63
+ def self.preprocess(source, id_def)
64
+ box = ::Ruby::Box.new
65
+ ids = box.eval(File.read(id_def))
66
+ tokens = ids.fetch(:token_op).each_with_object({}) do |(_id, _op, token, id), result|
67
+ result[token] = id if token
61
68
  end
62
- unknown = ::Arpaka::RubyGrammarActions::ACTIONS.keys - inventory.map { |rule| rule.fetch("id") }
63
- raise ::Arpaka::Error, "Unknown action IDs: #{unknown.inspect}" unless unknown.empty?
64
- [grammar, inventory]
69
+ return source unless tokens.any?
70
+
71
+ names = tokens.keys.map { |name| Regexp.escape(name) }.join("|")
72
+ token_pattern = /\bRUBY_TOKEN\((#{names})\)\s*(?=\s)/
73
+ source.lines.map do |line|
74
+ if line.start_with?("%token")
75
+ line.gsub(token_pattern) { tokens.fetch(Regexp.last_match(1)).to_s }
76
+ else
77
+ line
78
+ end
79
+ end.join
80
+ rescue KeyError, TypeError, SyntaxError => error
81
+ raise ::Arpaka::Error, "Ruby id.def preprocessing failed: #{error.message}"
82
+ end
83
+
84
+ def self.preprocess_names(source)
85
+ source.gsub(/\bRUBY_TOKEN\(([A-Za-z_][A-Za-z0-9_]*)\)/, '\\1')
65
86
  end
66
87
 
67
88
  def self.symbol_names(grammar)
@@ -81,12 +102,18 @@ module Arpaka::RubyGrammar
81
102
  end
82
103
  end
83
104
 
84
- def self.artifacts(ruby_source:)
85
- grammar, inventory = load_upstream(ruby_source)
105
+ def self.artifacts(ruby_source: nil, parse_y: nil)
106
+ grammar, inventory = if ruby_source
107
+ load_upstream(ruby_source)
108
+ else
109
+ load_parse_y(parse_y)
110
+ end
86
111
  names = symbol_names(grammar)
87
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)
88
115
  lines = ["/* Generated from ruby/ruby #{REVISION}. See Arpaka Action mappings. */",
89
- "%no-stdlib", "%expect #{grammar.expect || 0}"]
116
+ "%no-stdlib", "%expect #{expected_conflicts}"]
90
117
  lines << "%define lr.type ielr" if grammar.ielr_defined?
91
118
  grammar.terms.each do |symbol|
92
119
  next if symbol.error_symbol? || symbol.undef_symbol?
@@ -112,7 +139,9 @@ module Arpaka::RubyGrammar
112
139
  else
113
140
  ""
114
141
  end
115
- expression = ::Arpaka::RubyGrammarActions::ACTIONS[rule.id]&.last || "@builder.unsupported(#{rule.id})"
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})"
116
145
  lines << "/* upstream parse.y:#{metadata.fetch(rule.id).fetch('line')}: #{rule.as_comment} */"
117
146
  lines << "#{name.call(rule.lhs)}: #{rhs}#{precedence} { $$ = #{expression} };"
118
147
  end
@@ -124,9 +153,13 @@ module Arpaka::RubyGrammar
124
153
  compact = rules.map do |rule|
125
154
  { id: rule.id, rule: rule.as_comment,
126
155
  line: metadata.fetch(rule.id).fetch("line"),
127
- status: ::Arpaka::RubyGrammarActions::ACTIONS[rule.id]&.first || "unsupported" }
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" }
128
159
  end
129
- { "parse.y" => source, "rules.json" => JSON.pretty_generate(compact) + "\n" }
160
+ { "parse.y" => source, "rules.json" => JSON.pretty_generate(compact) + "\n",
161
+ "runtime" => profile.fetch("runtime", {}),
162
+ "source_profile" => profile.fetch("source_tag", REVISION) }
130
163
  end
131
164
 
132
165
  def self.verify_structure(original, source, names)
@@ -139,7 +172,7 @@ module Arpaka::RubyGrammar
139
172
  end.sort_by(&:inspect)
140
173
  end
141
174
  expected_rules = signature.call(original, names).map do |rule|
142
- if rule[0] == names.fetch("value_expr_command") && rule[1] == [names.fetch("command")]
175
+ if names.key?("value_expr_command") && rule[0] == names.fetch("value_expr_command") && rule[1] == [names.fetch("command")]
143
176
  rule[0, 2] + ["tLBRACE_ARG"]
144
177
  else
145
178
  rule
@@ -6,6 +6,14 @@ module AST
6
6
  If = Data.define(:condition, :then_body, :else_body)
7
7
  LocalRead = Data.define(:name)
8
8
  LocalWrite = Data.define(:name, :value)
9
+ <% if runtime.fetch("ast_nodes", []).include?("assignment") -%>
10
+ Assignment = Data.define(:target, :value)
11
+ OpAssign = Data.define(:target, :operator, :value)
12
+ MultipleAssign = Data.define(:targets, :value)
13
+ Field = Data.define(:receiver, :operator, :name)
14
+ Alias = Data.define(:new_name, :old_name)
15
+ Undef = Data.define(:names)
16
+ <% end -%>
9
17
  BareCall = Data.define(:name)
10
18
  ArrayLiteral = Data.define(:elements)
11
19
  HashLiteral = Data.define(:pairs)
@@ -26,10 +34,23 @@ module AST
26
34
  XStringLiteral = Data.define(:value)
27
35
  For = Data.define(:variable, :enumerable, :body)
28
36
  Def = Data.define(:name, :arguments, :body)
37
+ <% if runtime.fetch("ast_nodes", []).include?("singleton_method") -%>
38
+ SingletonMethodDef = Data.define(:receiver, :name, :arguments, :body)
39
+ <% end -%>
29
40
  ClassDef = Data.define(:name, :superclass, :body)
30
41
  ModuleDef = Data.define(:name, :body)
42
+ <% if runtime.fetch("ast_nodes", []).include?("singleton_class") -%>
43
+ SingletonClassDef = Data.define(:receiver, :body)
44
+ <% end -%>
31
45
  When = Data.define(:patterns, :body)
32
46
  Case = Data.define(:expression, :whens, :else_body)
47
+ <% if runtime.fetch("ast_nodes", []).include?("pattern") -%>
48
+ Pattern = Data.define(:kind, :values)
49
+ RescueClause = Data.define(:exceptions, :variable, :body)
50
+ FullRescue = Data.define(:body, :clauses, :else_body, :ensure_body)
51
+ BeginBlock = Data.define(:body)
52
+ EndBlock = Data.define(:body)
53
+ <% end -%>
33
54
  Lambda = Data.define(:arguments, :body)
34
55
  Sequence = Data.define(:statements)
35
56
  end
@@ -84,6 +84,12 @@ class Builder
84
84
  def def_node(name, arguments, body)
85
85
  AST::Def.new(name.to_sym, (arguments || []).freeze, body || [].freeze)
86
86
  end
87
+ <% if runtime.fetch("builder_methods", []).include?("singleton_method") -%>
88
+ def singleton_method_node(receiver, name, arguments, body)
89
+ AST::SingletonMethodDef.new(receiver, name.to_sym, (arguments || []).freeze,
90
+ body_nodes(body))
91
+ end
92
+ <% end -%>
87
93
 
88
94
  def class_node(name, superclass, body)
89
95
  AST::ClassDef.new(name.to_sym, superclass, body_nodes(body))
@@ -92,6 +98,11 @@ class Builder
92
98
  def module_node(name, body)
93
99
  AST::ModuleDef.new(name.to_sym, body_nodes(body))
94
100
  end
101
+ <% if runtime.fetch("builder_methods", []).include?("singleton_class") -%>
102
+ def singleton_class_node(receiver, body)
103
+ AST::SingletonClassDef.new(receiver, body_nodes(body))
104
+ end
105
+ <% end -%>
95
106
 
96
107
  def body_nodes(body)
97
108
  value = body
@@ -170,6 +181,42 @@ class Builder
170
181
  def lambda_node(arguments, body)
171
182
  AST::Lambda.new((arguments || []).freeze, body_nodes(body))
172
183
  end
184
+ <% if runtime.fetch("builder_methods", []).include?("rescue") -%>
185
+ def body_statement(body, clauses, else_body, ensure_body)
186
+ clauses = clauses.nil? ? [] : Array(clauses)
187
+ if clauses.empty? && else_body.nil? && ensure_body.nil?
188
+ body_nodes(body)
189
+ else
190
+ full_rescue(body, clauses, else_body, ensure_body)
191
+ end
192
+ end
193
+
194
+ def rescue_chain(clause, rest)
195
+ [clause] + (rest.nil? ? [] : Array(rest))
196
+ end
197
+
198
+ def pattern(kind, values)
199
+ AST::Pattern.new(kind.to_sym, Array(values).freeze)
200
+ end
201
+
202
+ def rescue_clause(exceptions, variable, body)
203
+ AST::RescueClause.new(Array(exceptions).freeze, variable, body_nodes(body))
204
+ end
205
+
206
+ def full_rescue(body, clauses, else_body, ensure_body)
207
+ AST::FullRescue.new(body_nodes(body), Array(clauses).freeze,
208
+ else_body.nil? ? nil : body_nodes(else_body),
209
+ ensure_body.nil? ? nil : body_nodes(ensure_body))
210
+ end
211
+
212
+ def begin_block(body)
213
+ AST::BeginBlock.new(body_nodes(body))
214
+ end
215
+
216
+ def end_block(body)
217
+ AST::EndBlock.new(body_nodes(body))
218
+ end
219
+ <% end -%>
173
220
 
174
221
  def unary(operator, operand)
175
222
  AST::Unary.new(operator, operand)
@@ -212,6 +259,35 @@ class Builder
212
259
  def assign(name, value)
213
260
  AST::LocalWrite.new(name, value)
214
261
  end
262
+ <% if runtime.fetch("builder_methods", []).include?("assignment") -%>
263
+ def assignment(name, value)
264
+ if name.is_a?(AST::Field) || name.is_a?(AST::Index)
265
+ AST::Assignment.new(name, value)
266
+ else
267
+ AST::LocalWrite.new(name, value)
268
+ end
269
+ end
270
+
271
+ def opassign(target, operator, value)
272
+ AST::OpAssign.new(target, operator.to_sym, value)
273
+ end
274
+
275
+ def multiple_assign(targets, value)
276
+ AST::MultipleAssign.new(Array(targets).freeze, value)
277
+ end
278
+
279
+ def field(receiver, operator, name)
280
+ AST::Field.new(receiver, operator.to_sym, name.to_sym)
281
+ end
282
+
283
+ def alias_node(new_name, old_name)
284
+ AST::Alias.new(new_name, old_name)
285
+ end
286
+
287
+ def undef_node(names)
288
+ AST::Undef.new(Array(names).freeze)
289
+ end
290
+ <% end -%>
215
291
 
216
292
  def array(elements)
217
293
  AST::ArrayLiteral.new(elements.freeze)
@@ -1287,7 +1287,14 @@ class Lexer
1287
1287
  OP_TOKENS.fetch(text, :tOP_ASGN)
1288
1288
  end
1289
1289
  @context.lambda_pending = true if token == :tLAMBDA
1290
+ <% source_text_tokens = runtime.fetch("lexer", {}).fetch("source_text_tokens", []) -%>
1291
+ <% if source_text_tokens.empty? -%>
1290
1292
  return [token, nil]
1293
+ <% elsif source_text_tokens.one? -%>
1294
+ return [token, token == <%= source_text_tokens.first.to_sym.inspect %> ? text : nil]
1295
+ <% else -%>
1296
+ return [token, <%= source_text_tokens.map { |value| value.to_sym }.inspect %>.include?(token) ? text : nil]
1297
+ <% end -%>
1291
1298
  end
1292
1299
  value = byte.chr
1293
1300
  advance
data/lib/arpaka.rb CHANGED
@@ -5,12 +5,12 @@ require_relative "lrama/ruby"
5
5
  module Arpaka
6
6
  class Error < Lrama::Ruby::Error; end
7
7
 
8
- def self.generate(ruby_source:, class_name: "RubyParser")
9
- Generator.new.generate(ruby_source: ruby_source, class_name: class_name)
8
+ def self.generate(ruby_source: nil, parse_y: nil, class_name: "RubyParser")
9
+ Generator.new.generate(ruby_source: ruby_source, parse_y: parse_y, class_name: class_name)
10
10
  end
11
11
 
12
- def self.compile(ruby_source:, class_name: "RubyParser")
13
- source = generate(ruby_source: ruby_source, class_name: class_name)
12
+ def self.compile(ruby_source: nil, parse_y: nil, class_name: "RubyParser")
13
+ source = generate(ruby_source: ruby_source, parse_y: parse_y, class_name: class_name)
14
14
  box = ::Ruby::Box.new
15
15
  box.eval(source)
16
16
  box.const_get(class_name, false)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lrama
4
4
  module Ruby
5
- VERSION = "0.1.0"
5
+ VERSION = "1.0.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: arpaka
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - MATSUMOTO, Katsuyoshi
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
25
  version: 0.8.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: racc
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
26
40
  description: A Ruby parser frontend and Ruby output backend for Lrama.
27
41
  email:
28
42
  - github@katsyoshi.org
@@ -45,6 +59,9 @@ files:
45
59
  - lib/arpaka/actions.rb
46
60
  - lib/arpaka/cli.rb
47
61
  - lib/arpaka/generator.rb
62
+ - lib/arpaka/profile/ruby.rb
63
+ - lib/arpaka/profile/ruby_3_3.rb
64
+ - lib/arpaka/profile/ruby_3_4.rb
48
65
  - lib/arpaka/ruby_grammar.rb
49
66
  - lib/arpaka/runtime/ast.rb.erb
50
67
  - lib/arpaka/runtime/builder.rb.erb