ibex 0.1.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 +7 -0
- data/.rubocop.yml +43 -0
- data/CHANGELOG.md +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +194 -0
- data/Rakefile +25 -0
- data/Steepfile +10 -0
- data/docs/architecture.md +99 -0
- data/docs/compat-notes.md +37 -0
- data/docs/grammar-reference.md +127 -0
- data/docs/lexer-coverage.md +14 -0
- data/docs/phase10-extensions.md +27 -0
- data/docs/racc-migration.md +47 -0
- data/exe/ibex +6 -0
- data/gemfiles/Gemfile +7 -0
- data/gemfiles/Gemfile.lock +98 -0
- data/lib/ibex/analysis/sets.rb +194 -0
- data/lib/ibex/analysis.rb +8 -0
- data/lib/ibex/cli/counterexample_options.rb +38 -0
- data/lib/ibex/cli/outputs.rb +88 -0
- data/lib/ibex/cli.rb +333 -0
- data/lib/ibex/codegen/dot.rb +52 -0
- data/lib/ibex/codegen/html.rb +110 -0
- data/lib/ibex/codegen/rbs.rb +54 -0
- data/lib/ibex/codegen/report.rb +149 -0
- data/lib/ibex/codegen/ruby.rb +235 -0
- data/lib/ibex/codegen/symbol_labels.rb +18 -0
- data/lib/ibex/error.rb +7 -0
- data/lib/ibex/frontend/action_scanner.rb +266 -0
- data/lib/ibex/frontend/ast.rb +146 -0
- data/lib/ibex/frontend/bootstrap_parser.rb +163 -0
- data/lib/ibex/frontend/dsl.rb +196 -0
- data/lib/ibex/frontend/generated_parser.rb +434 -0
- data/lib/ibex/frontend/generated_parser_base.rb +273 -0
- data/lib/ibex/frontend/grammar.y +156 -0
- data/lib/ibex/frontend/lexer.rb +165 -0
- data/lib/ibex/frontend/parser/declarations.rb +163 -0
- data/lib/ibex/frontend/parser/rules.rb +154 -0
- data/lib/ibex/frontend/parser.rb +23 -0
- data/lib/ibex/frontend/regenerator.rb +41 -0
- data/lib/ibex/frontend/source_cursor.rb +94 -0
- data/lib/ibex/frontend/token_adapter/declaration_state.rb +248 -0
- data/lib/ibex/frontend/token_adapter/delimiter_tracker.rb +37 -0
- data/lib/ibex/frontend/token_adapter/rule_state.rb +162 -0
- data/lib/ibex/frontend/token_adapter.rb +108 -0
- data/lib/ibex/frontend.rb +28 -0
- data/lib/ibex/ir/automaton_ir.rb +109 -0
- data/lib/ibex/ir/grammar_ir.rb +192 -0
- data/lib/ibex/ir/serialize.rb +154 -0
- data/lib/ibex/ir.rb +53 -0
- data/lib/ibex/lalr/builder.rb +270 -0
- data/lib/ibex/lalr/conflict.rb +120 -0
- data/lib/ibex/lalr/conflict_search.rb +332 -0
- data/lib/ibex/lalr/conflict_search_limits.rb +39 -0
- data/lib/ibex/lalr/counterexample.rb +138 -0
- data/lib/ibex/lalr/default_reductions.rb +67 -0
- data/lib/ibex/lalr.rb +27 -0
- data/lib/ibex/normalize/declarations.rb +60 -0
- data/lib/ibex/normalize/diagnostics.rb +99 -0
- data/lib/ibex/normalize/expander.rb +218 -0
- data/lib/ibex/normalize/expression.rb +54 -0
- data/lib/ibex/normalize.rb +166 -0
- data/lib/ibex/runtime/parser.rb +360 -0
- data/lib/ibex/runtime.rb +8 -0
- data/lib/ibex/tables.rb +125 -0
- data/lib/ibex/version.rb +6 -0
- data/lib/ibex.rb +23 -0
- data/sig/ibex/analysis/sets.rbs +72 -0
- data/sig/ibex/analysis.rbs +6 -0
- data/sig/ibex/cli/counterexample_options.rbs +16 -0
- data/sig/ibex/cli/outputs.rbs +31 -0
- data/sig/ibex/cli.rbs +108 -0
- data/sig/ibex/codegen/dot.rbs +19 -0
- data/sig/ibex/codegen/html.rbs +35 -0
- data/sig/ibex/codegen/rbs.rbs +31 -0
- data/sig/ibex/codegen/report.rbs +39 -0
- data/sig/ibex/codegen/ruby.rbs +92 -0
- data/sig/ibex/codegen/symbol_labels.rbs +11 -0
- data/sig/ibex/error.rbs +7 -0
- data/sig/ibex/frontend/action_scanner.rbs +74 -0
- data/sig/ibex/frontend/ast.rbs +223 -0
- data/sig/ibex/frontend/bootstrap_parser.rbs +80 -0
- data/sig/ibex/frontend/dsl.rbs +124 -0
- data/sig/ibex/frontend/generated_parser.rbs +163 -0
- data/sig/ibex/frontend/generated_parser_base.rbs +114 -0
- data/sig/ibex/frontend/lexer.rbs +61 -0
- data/sig/ibex/frontend/parser/declarations.rbs +56 -0
- data/sig/ibex/frontend/parser/rules.rbs +48 -0
- data/sig/ibex/frontend/parser.rbs +16 -0
- data/sig/ibex/frontend/regenerator.rbs +16 -0
- data/sig/ibex/frontend/source_cursor.rbs +73 -0
- data/sig/ibex/frontend/token_adapter/declaration_state.rbs +97 -0
- data/sig/ibex/frontend/token_adapter/delimiter_tracker.rbs +25 -0
- data/sig/ibex/frontend/token_adapter/rule_state.rbs +77 -0
- data/sig/ibex/frontend/token_adapter.rbs +67 -0
- data/sig/ibex/frontend.rbs +15 -0
- data/sig/ibex/ir/automaton_ir.rbs +77 -0
- data/sig/ibex/ir/grammar_ir.rbs +143 -0
- data/sig/ibex/ir/serialize.rbs +50 -0
- data/sig/ibex/ir.rbs +43 -0
- data/sig/ibex/lalr/builder.rbs +85 -0
- data/sig/ibex/lalr/conflict.rbs +47 -0
- data/sig/ibex/lalr/conflict_search.rbs +131 -0
- data/sig/ibex/lalr/conflict_search_limits.rbs +29 -0
- data/sig/ibex/lalr/counterexample.rbs +53 -0
- data/sig/ibex/lalr/default_reductions.rbs +20 -0
- data/sig/ibex/lalr.rbs +23 -0
- data/sig/ibex/normalize/declarations.rbs +20 -0
- data/sig/ibex/normalize/diagnostics.rbs +32 -0
- data/sig/ibex/normalize/expander.rbs +66 -0
- data/sig/ibex/normalize/expression.rbs +21 -0
- data/sig/ibex/normalize.rbs +96 -0
- data/sig/ibex/runtime/parser.rbs +167 -0
- data/sig/ibex/runtime.rbs +6 -0
- data/sig/ibex/tables.rbs +50 -0
- data/sig/ibex/version.rbs +6 -0
- data/sig/ibex.rbs +6 -0
- metadata +161 -0
data/lib/ibex/cli.rb
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "../ibex"
|
|
5
|
+
require_relative "cli/counterexample_options"
|
|
6
|
+
require_relative "cli/outputs"
|
|
7
|
+
|
|
8
|
+
module Ibex
|
|
9
|
+
# @rbs!
|
|
10
|
+
# interface _CLIOutput
|
|
11
|
+
# def puts: (*untyped) -> untyped
|
|
12
|
+
# def write: (String) -> untyped
|
|
13
|
+
# end
|
|
14
|
+
# type cli_options = {
|
|
15
|
+
# emit: String,
|
|
16
|
+
# mode: Symbol,
|
|
17
|
+
# table: Symbol,
|
|
18
|
+
# line_convert: bool,
|
|
19
|
+
# ?line_convert_all: bool,
|
|
20
|
+
# counterexample_max_tokens: Integer,
|
|
21
|
+
# counterexample_max_configurations: Integer,
|
|
22
|
+
# ?from: String,
|
|
23
|
+
# ?algorithm: Symbol,
|
|
24
|
+
# ?warnings: Array[Symbol],
|
|
25
|
+
# ?output: String,
|
|
26
|
+
# ?embedded: bool,
|
|
27
|
+
# ?debug: bool,
|
|
28
|
+
# ?verbose: bool,
|
|
29
|
+
# ?rbs: String | true,
|
|
30
|
+
# ?dot: String,
|
|
31
|
+
# ?html: String,
|
|
32
|
+
# ?log_file: String,
|
|
33
|
+
# ?executable: String,
|
|
34
|
+
# ?frozen: bool,
|
|
35
|
+
# ?omit_actions: bool,
|
|
36
|
+
# ?superclass: String,
|
|
37
|
+
# ?check_only: bool,
|
|
38
|
+
# ?status: bool,
|
|
39
|
+
# ?profile: bool,
|
|
40
|
+
# ?debug_flags: String,
|
|
41
|
+
# ?version: bool,
|
|
42
|
+
# ?runtime_version: bool,
|
|
43
|
+
# ?copyright: bool,
|
|
44
|
+
# ?help: bool
|
|
45
|
+
# }
|
|
46
|
+
|
|
47
|
+
# Command-line pipeline coordinator.
|
|
48
|
+
# rubocop:disable Metrics/ClassLength -- inline type contracts add lines without adding runtime responsibilities.
|
|
49
|
+
class CLI
|
|
50
|
+
include CLICounterexampleOptions
|
|
51
|
+
include CLIOutputs
|
|
52
|
+
|
|
53
|
+
# @rbs @stdout: _CLIOutput
|
|
54
|
+
# @rbs @stderr: _CLIOutput
|
|
55
|
+
# @rbs @options: cli_options
|
|
56
|
+
|
|
57
|
+
# @rbs (Array[String] arguments, ?stdout: _CLIOutput, ?stderr: _CLIOutput) -> Integer
|
|
58
|
+
def self.start(arguments, stdout: $stdout, stderr: $stderr)
|
|
59
|
+
new(stdout: stdout, stderr: stderr).run(arguments)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @rbs (stdout: _CLIOutput, stderr: _CLIOutput) -> void
|
|
63
|
+
def initialize(stdout:, stderr:)
|
|
64
|
+
@stdout = stdout
|
|
65
|
+
@stderr = stderr
|
|
66
|
+
@options = { emit: "ruby", mode: :racc, table: :compact, line_convert: true }
|
|
67
|
+
.merge(CLICounterexampleOptions::DEFAULTS)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @rbs (Array[String] arguments) -> Integer
|
|
71
|
+
def run(arguments)
|
|
72
|
+
parser = option_parser
|
|
73
|
+
remaining = parser.parse(arguments)
|
|
74
|
+
information = informational_result(parser)
|
|
75
|
+
return information unless information.nil?
|
|
76
|
+
|
|
77
|
+
process_grammar(input_path(remaining))
|
|
78
|
+
rescue OptionParser::ParseError, Ibex::Error, Errno::ENOENT => e
|
|
79
|
+
@stderr.puts(e.message)
|
|
80
|
+
1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# @rbs () -> OptionParser
|
|
86
|
+
def option_parser
|
|
87
|
+
OptionParser.new do |options|
|
|
88
|
+
options.banner = "Usage: ibex [options] grammarfile"
|
|
89
|
+
add_pipeline_options(options)
|
|
90
|
+
add_output_options(options)
|
|
91
|
+
add_compatibility_options(options)
|
|
92
|
+
add_information_options(options)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @rbs (OptionParser options) -> void
|
|
97
|
+
def add_pipeline_options(options)
|
|
98
|
+
options.on("--emit=FORMAT", "ast, grammar-ir, automaton-ir, or ruby") { |value| @options[:emit] = value }
|
|
99
|
+
options.on("--from=FORMAT", %w[grammar-ir automaton-ir], "resume from IR JSON") do |value|
|
|
100
|
+
@options[:from] = value
|
|
101
|
+
end
|
|
102
|
+
options.on("--mode=MODE", %w[racc extended], "grammar mode") { |value| @options[:mode] = value.to_sym }
|
|
103
|
+
options.on("--table=FORMAT", %w[plain compact], "parser table format") do |value|
|
|
104
|
+
@options[:table] = value.to_sym
|
|
105
|
+
end
|
|
106
|
+
options.on("--algorithm=NAME", %w[slr lalr lr1], "parser construction algorithm") do |value|
|
|
107
|
+
@options[:algorithm] = value.to_sym
|
|
108
|
+
end
|
|
109
|
+
options.on("--warnings=CATEGORIES", "all, error, all,error, or none") do |value|
|
|
110
|
+
@options[:warnings] = warning_categories(value)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @rbs (OptionParser options) -> void
|
|
115
|
+
def add_output_options(options)
|
|
116
|
+
options.on("-o", "--output-file=FILE", "generated parser path") { |value| @options[:output] = value }
|
|
117
|
+
options.on("-E", "--embedded", "embed the Pure Ruby runtime") { @options[:embedded] = true }
|
|
118
|
+
options.on("-t", "--debug", "generate a debug-capable parser") { @options[:debug] = true }
|
|
119
|
+
options.on("-g", "obsolete alias for --debug") { @options[:debug] = true }
|
|
120
|
+
options.on("-v", "--verbose", "write an automaton report") { @options[:verbose] = true }
|
|
121
|
+
add_counterexample_options(options)
|
|
122
|
+
options.on("--rbs[=FILE]", "write an RBS signature (defaults beside parser)") do |value|
|
|
123
|
+
@options[:rbs] = value || true
|
|
124
|
+
end
|
|
125
|
+
options.on("--dot=FILE", "write Graphviz DOT") { |value| @options[:dot] = value }
|
|
126
|
+
options.on("--html=FILE", "write a self-contained HTML report") { |value| @options[:html] = value }
|
|
127
|
+
options.on("-O", "--log-file=FILE", "automaton report path") do |value|
|
|
128
|
+
@options[:verbose] = true
|
|
129
|
+
@options[:log_file] = value
|
|
130
|
+
end
|
|
131
|
+
options.on("-e", "--executable [RUBY]", "add a shebang") do |value|
|
|
132
|
+
@options[:executable] = value || "/usr/bin/env ruby"
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @rbs (OptionParser options) -> void
|
|
137
|
+
def add_compatibility_options(options)
|
|
138
|
+
options.on("-F", "--frozen", "emit frozen string literals") { @options[:frozen] = true }
|
|
139
|
+
options.on("--line-convert-all", "convert all source lines") do
|
|
140
|
+
@options[:line_convert] = true
|
|
141
|
+
@options[:line_convert_all] = true
|
|
142
|
+
end
|
|
143
|
+
options.on("-l", "--no-line-convert", "use generated-file action lines") do
|
|
144
|
+
@options[:line_convert] = false
|
|
145
|
+
@options[:line_convert_all] = false
|
|
146
|
+
end
|
|
147
|
+
options.on("-a", "--no-omit-actions", "generate implicit action methods") { @options[:omit_actions] = false }
|
|
148
|
+
options.on("--superclass=CLASS", "override parser superclass") { |value| @options[:superclass] = value }
|
|
149
|
+
options.on("-C", "--check-only", "check grammar and exit") { @options[:check_only] = true }
|
|
150
|
+
options.on("-S", "--output-status", "show pipeline status") { @options[:status] = true }
|
|
151
|
+
options.on("-P", "accept the compatibility profiling flag") { @options[:profile] = true }
|
|
152
|
+
options.on("-D FLAGS", "accept internal compatibility flags") { |value| @options[:debug_flags] = value }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @rbs (OptionParser options) -> void
|
|
156
|
+
def add_information_options(options)
|
|
157
|
+
options.on("--version", "show version") { @options[:version] = true }
|
|
158
|
+
options.on("--runtime-version", "show runtime version") { @options[:runtime_version] = true }
|
|
159
|
+
options.on("--copyright", "show copyright") { @options[:copyright] = true }
|
|
160
|
+
options.on("--help", "show help") { @options[:help] = true }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# @rbs (OptionParser parser) -> Integer?
|
|
164
|
+
def informational_result(parser)
|
|
165
|
+
return print_version if @options[:version] || @options[:runtime_version]
|
|
166
|
+
return print_copyright if @options[:copyright]
|
|
167
|
+
return print_help(parser) if @options[:help]
|
|
168
|
+
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @rbs (Array[String] remaining) -> String
|
|
173
|
+
def input_path(remaining)
|
|
174
|
+
path = remaining.first || raise(Ibex::Error, "(cli):1:1: grammar file is required")
|
|
175
|
+
raise Ibex::Error, "(cli):1:1: only one grammar file may be specified" if remaining.length > 1
|
|
176
|
+
|
|
177
|
+
path
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @rbs (String path) -> Integer
|
|
181
|
+
def process_grammar(path)
|
|
182
|
+
return process_ir(path) if @options[:from]
|
|
183
|
+
|
|
184
|
+
report_status("reading #{path}")
|
|
185
|
+
ast = Frontend::Parser.new(File.read(path), file: path, mode: @options[:mode]).parse
|
|
186
|
+
return emit_ast(ast) if @options[:emit] == "ast"
|
|
187
|
+
|
|
188
|
+
grammar = Normalizer.new(ast, mode: @options[:mode]).normalize
|
|
189
|
+
dispatch_grammar(grammar, path)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# @rbs (String path) -> Integer
|
|
193
|
+
def process_ir(path)
|
|
194
|
+
report_status("reading #{path}")
|
|
195
|
+
value = IR::Serialize.load(File.read(path))
|
|
196
|
+
expected = @options[:from] == "grammar-ir" ? IR::Grammar : IR::Automaton
|
|
197
|
+
raise Ibex::Error, "#{path}:1:1: expected #{@options[:from]} input" unless value.is_a?(expected)
|
|
198
|
+
|
|
199
|
+
return dispatch_grammar(value, path) if value.is_a?(IR::Grammar)
|
|
200
|
+
|
|
201
|
+
dispatch_automaton(value, path)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @rbs (IR::Grammar grammar, String path) -> Integer
|
|
205
|
+
def dispatch_grammar(grammar, path)
|
|
206
|
+
handle_grammar_warnings(grammar, path)
|
|
207
|
+
return 0 if @options[:check_only]
|
|
208
|
+
return emit_grammar(grammar) if @options[:emit] == "grammar-ir"
|
|
209
|
+
return emit_automaton(grammar, path) if @options[:emit] == "automaton-ir"
|
|
210
|
+
return emit_ruby(grammar, path) if @options[:emit] == "ruby"
|
|
211
|
+
|
|
212
|
+
raise Ibex::Error, "(cli):1:1: emit format #{@options[:emit].inspect} is not available yet"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# @rbs (IR::Automaton automaton, String path) -> Integer
|
|
216
|
+
def dispatch_automaton(automaton, path)
|
|
217
|
+
handle_grammar_warnings(automaton.grammar, path)
|
|
218
|
+
return 0 if @options[:check_only]
|
|
219
|
+
return emit_grammar(automaton.grammar) if @options[:emit] == "grammar-ir"
|
|
220
|
+
return emit_loaded_automaton(automaton, path) if @options[:emit] == "automaton-ir"
|
|
221
|
+
|
|
222
|
+
if @options[:emit] == "ruby"
|
|
223
|
+
prepare_loaded_automaton(automaton, path)
|
|
224
|
+
return generate_ruby(automaton, path)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
raise Ibex::Error, "(cli):1:1: AST cannot be reconstructed from Automaton IR"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# @rbs () -> Integer
|
|
231
|
+
def print_version
|
|
232
|
+
@stdout.puts("ibex #{VERSION}")
|
|
233
|
+
0
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# @rbs (OptionParser parser) -> Integer
|
|
237
|
+
def print_help(parser)
|
|
238
|
+
@stdout.puts(parser)
|
|
239
|
+
0
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# @rbs () -> Integer
|
|
243
|
+
def print_copyright
|
|
244
|
+
@stdout.puts("Ibex #{VERSION} Copyright (c) 2026 Yudai Takada")
|
|
245
|
+
0
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# @rbs (Frontend::AST::Root ast) -> Integer
|
|
249
|
+
def emit_ast(ast)
|
|
250
|
+
@stdout.puts(JSON.pretty_generate(ast.to_h))
|
|
251
|
+
0
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# @rbs (IR::Grammar grammar) -> Integer
|
|
255
|
+
def emit_grammar(grammar)
|
|
256
|
+
@stdout.write(IR::Serialize.dump(grammar))
|
|
257
|
+
0
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# @rbs (IR::Grammar grammar, String input_path) -> Integer
|
|
261
|
+
def emit_automaton(grammar, input_path)
|
|
262
|
+
@stdout.write(IR::Serialize.dump(build_automaton(grammar, input_path)))
|
|
263
|
+
0
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# @rbs (IR::Grammar grammar, String input_path) -> Integer
|
|
267
|
+
def emit_ruby(grammar, input_path)
|
|
268
|
+
automaton = build_automaton(grammar, input_path)
|
|
269
|
+
generate_ruby(automaton, input_path)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# @rbs (IR::Automaton automaton, String input_path) -> Integer
|
|
273
|
+
def generate_ruby(automaton, input_path)
|
|
274
|
+
source = Codegen::Ruby.new(
|
|
275
|
+
automaton, table: @options[:table], embedded: @options.fetch(:embedded, false),
|
|
276
|
+
line_convert: @options.fetch(:line_convert), debug: @options.fetch(:debug, false),
|
|
277
|
+
line_convert_all: @options.fetch(:line_convert_all, false),
|
|
278
|
+
omit_action_call: @options[:omit_actions], superclass: @options[:superclass],
|
|
279
|
+
executable: @options[:executable]
|
|
280
|
+
).generate
|
|
281
|
+
output_path = @options[:output] || default_output_path(input_path, ".rb")
|
|
282
|
+
File.write(output_path, source)
|
|
283
|
+
File.chmod(0o755, output_path) if @options[:executable]
|
|
284
|
+
report_status("wrote #{output_path}")
|
|
285
|
+
write_rbs(automaton, output_path) if @options[:rbs]
|
|
286
|
+
0
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# @rbs (IR::Automaton automaton, String output_path) -> void
|
|
290
|
+
def write_rbs(automaton, output_path)
|
|
291
|
+
configured_path = @options[:rbs]
|
|
292
|
+
path = configured_path == true ? default_output_path(output_path, ".rbs") : configured_path
|
|
293
|
+
raise ArgumentError, "RBS output path is required" unless path.is_a?(String)
|
|
294
|
+
|
|
295
|
+
source = Codegen::RBS.new(automaton, superclass: @options[:superclass]).generate
|
|
296
|
+
File.write(path, source)
|
|
297
|
+
report_status("wrote #{path}")
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# @rbs (IR::Automaton automaton, String input_path) -> Integer
|
|
301
|
+
def emit_loaded_automaton(automaton, input_path)
|
|
302
|
+
prepare_loaded_automaton(automaton, input_path)
|
|
303
|
+
@stdout.write(IR::Serialize.dump(automaton))
|
|
304
|
+
0
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# @rbs (IR::Grammar grammar, String input_path) -> IR::Automaton
|
|
308
|
+
def build_automaton(grammar, input_path)
|
|
309
|
+
report_status("building LALR automaton")
|
|
310
|
+
automaton = LALR::Builder.new(grammar, algorithm: @options[:algorithm] || :lalr).build
|
|
311
|
+
report_conflicts(automaton, input_path)
|
|
312
|
+
write_report(automaton, input_path) if @options[:verbose]
|
|
313
|
+
write_visualizations(automaton)
|
|
314
|
+
automaton
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# @rbs (IR::Automaton automaton, String input_path) -> void
|
|
318
|
+
def prepare_loaded_automaton(automaton, input_path)
|
|
319
|
+
report_conflicts(automaton, input_path)
|
|
320
|
+
write_report(automaton, input_path) if @options[:verbose]
|
|
321
|
+
write_visualizations(automaton)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# @rbs (IR::Automaton automaton) -> void
|
|
325
|
+
def write_visualizations(automaton)
|
|
326
|
+
dot_path = @options[:dot]
|
|
327
|
+
html_path = @options[:html]
|
|
328
|
+
File.write(dot_path, Codegen::Dot.render(automaton)) if dot_path
|
|
329
|
+
File.write(html_path, Codegen::HTML.render(automaton)) if html_path
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
# rubocop:enable Metrics/ClassLength
|
|
333
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "symbol_labels"
|
|
4
|
+
|
|
5
|
+
module Ibex
|
|
6
|
+
module Codegen
|
|
7
|
+
# Renders Graphviz DOT from Automaton IR.
|
|
8
|
+
module Dot
|
|
9
|
+
# @rbs!
|
|
10
|
+
# private def escape: (String value) -> String
|
|
11
|
+
# private def self.escape: (String value) -> String
|
|
12
|
+
# private def symbol_name: (Hash[Integer, String] labels, Integer id) -> String
|
|
13
|
+
# private def self.symbol_name: (Hash[Integer, String] labels, Integer id) -> String
|
|
14
|
+
|
|
15
|
+
# @rbs (IR::Automaton automaton) -> String
|
|
16
|
+
def render(automaton)
|
|
17
|
+
labels = SymbolLabels.build(automaton.grammar)
|
|
18
|
+
lines = ["digraph ibex_automaton {", " rankdir=LR;", " node [shape=box];"]
|
|
19
|
+
automaton.states.each do |state|
|
|
20
|
+
attributes = ["label=\"State #{state.id}\""]
|
|
21
|
+
attributes << "color=red" unless state.conflicts.empty?
|
|
22
|
+
lines << " state_#{state.id} [#{attributes.join(', ')}];"
|
|
23
|
+
state.transitions.each do |symbol_id, target|
|
|
24
|
+
label = escape(symbol_name(labels, symbol_id))
|
|
25
|
+
lines << " state_#{state.id} -> state_#{target} [label=\"#{label}\"];"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
lines << "}"
|
|
29
|
+
"#{lines.join("\n")}\n"
|
|
30
|
+
end
|
|
31
|
+
module_function :render
|
|
32
|
+
|
|
33
|
+
# @rbs skip
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# @rbs skip
|
|
37
|
+
def escape(value)
|
|
38
|
+
value.gsub("\\", "\\\\").gsub('"', '\\"')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @rbs skip
|
|
42
|
+
def symbol_name(labels, id)
|
|
43
|
+
labels.fetch(id) { raise Ibex::Error, "missing grammar symbol id #{id}" }
|
|
44
|
+
end
|
|
45
|
+
module_function :escape, :symbol_name
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
private :escape, :symbol_name
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "symbol_labels"
|
|
4
|
+
|
|
5
|
+
module Ibex
|
|
6
|
+
module Codegen
|
|
7
|
+
# Renders a self-contained navigable automaton report.
|
|
8
|
+
module HTML
|
|
9
|
+
# @rbs!
|
|
10
|
+
# private def state_sections: (IR::Automaton automaton, IR::Grammar grammar,
|
|
11
|
+
# Hash[Integer, String] labels) -> String
|
|
12
|
+
# private def self.state_sections: (IR::Automaton automaton, IR::Grammar grammar,
|
|
13
|
+
# Hash[Integer, String] labels) -> String
|
|
14
|
+
# private def item_html: (IR::AutomatonItem item, IR::Grammar grammar, Hash[Integer, String] labels) -> String
|
|
15
|
+
# private def self.item_html: (IR::AutomatonItem item, IR::Grammar grammar,
|
|
16
|
+
# Hash[Integer, String] labels) -> String
|
|
17
|
+
# private def rule_sections: (IR::Grammar grammar, Hash[Integer, String] labels) -> String
|
|
18
|
+
# private def self.rule_sections: (IR::Grammar grammar, Hash[Integer, String] labels) -> String
|
|
19
|
+
# private def conflict_sections: (IR::Automaton automaton) -> String
|
|
20
|
+
# private def self.conflict_sections: (IR::Automaton automaton) -> String
|
|
21
|
+
# private def escape: (String value) -> String
|
|
22
|
+
# private def self.escape: (String value) -> String
|
|
23
|
+
# private def symbol_name: (Hash[Integer, String] labels, Integer id) -> String
|
|
24
|
+
# private def self.symbol_name: (Hash[Integer, String] labels, Integer id) -> String
|
|
25
|
+
|
|
26
|
+
# @rbs (IR::Automaton automaton) -> String
|
|
27
|
+
def render(automaton)
|
|
28
|
+
grammar = automaton.grammar
|
|
29
|
+
labels = SymbolLabels.build(grammar)
|
|
30
|
+
<<~HTML
|
|
31
|
+
<!doctype html>
|
|
32
|
+
<html lang="en"><head><meta charset="utf-8"><title>Ibex automaton</title>
|
|
33
|
+
<style>body{font:14px system-ui;margin:2rem;max-width:90rem}code{white-space:pre-wrap}.state{border:1px solid #bbb;padding:1rem;margin:1rem 0}.conflict{color:#a00}a{color:#075985}</style>
|
|
34
|
+
</head><body><h1>Ibex #{escape(automaton.algorithm)} automaton</h1>
|
|
35
|
+
<nav><a href="#rules">Rules</a> · <a href="#conflicts">Conflicts</a></nav>
|
|
36
|
+
#{state_sections(automaton, grammar, labels)}
|
|
37
|
+
<h2 id="rules">Rules</h2>#{rule_sections(grammar, labels)}
|
|
38
|
+
<h2 id="conflicts">Conflicts</h2>#{conflict_sections(automaton)}
|
|
39
|
+
</body></html>
|
|
40
|
+
HTML
|
|
41
|
+
end
|
|
42
|
+
module_function :render
|
|
43
|
+
|
|
44
|
+
# @rbs skip
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# @rbs skip
|
|
48
|
+
def state_sections(automaton, grammar, labels)
|
|
49
|
+
automaton.states.map do |state|
|
|
50
|
+
transitions = state.transitions.map do |symbol_id, target|
|
|
51
|
+
symbol = escape(symbol_name(labels, symbol_id))
|
|
52
|
+
"<li>#{symbol} → <a href=\"#state-#{target}\">state #{target}</a></li>"
|
|
53
|
+
end.join
|
|
54
|
+
items = state.items.map { |item| item_html(item, grammar, labels) }.join
|
|
55
|
+
<<~HTML
|
|
56
|
+
<section class="state" id="state-#{state.id}"><h2>State #{state.id}</h2>
|
|
57
|
+
<ul>#{transitions}</ul><ul>#{items}</ul></section>
|
|
58
|
+
HTML
|
|
59
|
+
end.join
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# @rbs skip
|
|
63
|
+
def item_html(item, grammar, labels)
|
|
64
|
+
return "<li><code>$accept</code></li>" if item.production.negative?
|
|
65
|
+
|
|
66
|
+
production = grammar.productions.fetch(item.production)
|
|
67
|
+
lhs = symbol_name(labels, production.lhs)
|
|
68
|
+
rhs = production.rhs.map { |id| symbol_name(labels, id) }.insert(item.dot, "•").join(" ")
|
|
69
|
+
link = "<a href=\"#rule-#{production.id}\">rule #{production.id}</a>"
|
|
70
|
+
"<li>#{link} <code>#{escape(lhs)} → #{escape(rhs)}</code></li>"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @rbs skip
|
|
74
|
+
def rule_sections(grammar, labels)
|
|
75
|
+
grammar.productions.map do |production|
|
|
76
|
+
lhs = symbol_name(labels, production.lhs)
|
|
77
|
+
rhs = production.rhs.map { |id| symbol_name(labels, id) }.join(" ")
|
|
78
|
+
number = "<strong>#{production.id}</strong>"
|
|
79
|
+
"<p id=\"rule-#{production.id}\">#{number} <code>#{escape(lhs)} → #{escape(rhs)}</code></p>"
|
|
80
|
+
end.join
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @rbs skip
|
|
84
|
+
def conflict_sections(automaton)
|
|
85
|
+
conflicts = automaton.states.flat_map do |state|
|
|
86
|
+
state.conflicts.map do |conflict|
|
|
87
|
+
link = "<a href=\"#state-#{state.id}\">state #{state.id}</a>"
|
|
88
|
+
"<li class=\"conflict\">#{link}: #{escape(conflict.inspect)}</li>"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
conflicts.empty? ? "<p>None</p>" : "<ul>#{conflicts.join}</ul>"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @rbs skip
|
|
95
|
+
def escape(value)
|
|
96
|
+
value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @rbs skip
|
|
100
|
+
def symbol_name(labels, id)
|
|
101
|
+
labels.fetch(id) { raise Ibex::Error, "missing grammar symbol id #{id}" }
|
|
102
|
+
end
|
|
103
|
+
module_function :state_sections, :item_html, :rule_sections, :conflict_sections, :escape, :symbol_name
|
|
104
|
+
|
|
105
|
+
class << self
|
|
106
|
+
private :state_sections, :item_html, :rule_sections, :conflict_sections, :escape, :symbol_name
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
module Codegen
|
|
5
|
+
# Generates an RBS declaration for the public surface of a generated parser.
|
|
6
|
+
class RBS
|
|
7
|
+
# @rbs @automaton: IR::Automaton
|
|
8
|
+
# @rbs @grammar: IR::Grammar
|
|
9
|
+
# @rbs @superclass: String
|
|
10
|
+
|
|
11
|
+
# @rbs (IR::Automaton automaton, ?superclass: String?) -> void
|
|
12
|
+
def initialize(automaton, superclass: nil)
|
|
13
|
+
@automaton = automaton
|
|
14
|
+
@grammar = automaton.grammar
|
|
15
|
+
@superclass = superclass || @grammar.superclass || "Ibex::Runtime::Parser"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @rbs () -> String
|
|
19
|
+
def generate
|
|
20
|
+
lines = ["# Generated by Ibex #{grammar_digest_comment}", ""]
|
|
21
|
+
modules, class_name = class_parts
|
|
22
|
+
modules.each { |name| lines << "module #{name}" }
|
|
23
|
+
lines << "class #{class_name} < #{@superclass}"
|
|
24
|
+
append_contract(lines)
|
|
25
|
+
lines << "end"
|
|
26
|
+
modules.reverse_each { lines << "end" }
|
|
27
|
+
"#{lines.join("\n")}\n"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
# @rbs (Array[String] lines) -> void
|
|
33
|
+
def append_contract(lines)
|
|
34
|
+
lines.push(" PARSER_TABLE_FORMAT_VERSION: Integer",
|
|
35
|
+
" TOKEN_IDS: Hash[untyped, Integer]", " TOKEN_NAMES: Hash[Integer, String]",
|
|
36
|
+
" ACTIONS: untyped", " GOTOS: untyped", " DEFAULT_ACTIONS: Array[untyped]",
|
|
37
|
+
" PRODUCTIONS: Array[Hash[Symbol, untyped]]",
|
|
38
|
+
" PARSER_TABLES: Hash[Symbol, untyped]", " DEBUG_ENABLED: bool", "",
|
|
39
|
+
" def self.parser_tables: () -> Hash[Symbol, untyped]")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @rbs () -> [Array[String], String]
|
|
43
|
+
def class_parts
|
|
44
|
+
parts = @grammar.class_name.split("::")
|
|
45
|
+
[parts.take(parts.length - 1), parts.fetch(-1)]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @rbs () -> String
|
|
49
|
+
def grammar_digest_comment
|
|
50
|
+
@automaton.grammar_digest.delete_prefix("sha256:").chars.first(12).join
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|