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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "symbol_labels"
|
|
4
|
+
|
|
5
|
+
module Ibex
|
|
6
|
+
module Codegen
|
|
7
|
+
# Renders a human-readable state and conflict report from Automaton IR.
|
|
8
|
+
module Report
|
|
9
|
+
# @rbs!
|
|
10
|
+
# private def append_state: (
|
|
11
|
+
# Array[String] lines,
|
|
12
|
+
# IR::AutomatonState state,
|
|
13
|
+
# IR::Grammar grammar,
|
|
14
|
+
# Array[IR::counterexample] examples,
|
|
15
|
+
# Hash[Integer, String] labels
|
|
16
|
+
# ) -> void
|
|
17
|
+
# private def self.append_state: (
|
|
18
|
+
# Array[String] lines,
|
|
19
|
+
# IR::AutomatonState state,
|
|
20
|
+
# IR::Grammar grammar,
|
|
21
|
+
# Array[IR::counterexample] examples,
|
|
22
|
+
# Hash[Integer, String] labels
|
|
23
|
+
# ) -> void
|
|
24
|
+
# private def append_counterexample: (Array[String] lines, IR::counterexample example, IR::Grammar grammar,
|
|
25
|
+
# Hash[Integer, String] labels) -> void
|
|
26
|
+
# private def self.append_counterexample: (Array[String] lines, IR::counterexample example, IR::Grammar grammar,
|
|
27
|
+
# Hash[Integer, String] labels) -> void
|
|
28
|
+
# private def append_tree: (Array[String] lines, untyped tree, String indentation, IR::Grammar grammar,
|
|
29
|
+
# Hash[Integer, String] labels) -> void
|
|
30
|
+
# private def self.append_tree: (Array[String] lines, untyped tree, String indentation, IR::Grammar grammar,
|
|
31
|
+
# Hash[Integer, String] labels) -> void
|
|
32
|
+
# private def format_item: (IR::AutomatonItem item, IR::Grammar grammar, Hash[Integer, String] labels) -> String
|
|
33
|
+
# private def self.format_item: (IR::AutomatonItem item, IR::Grammar grammar,
|
|
34
|
+
# Hash[Integer, String] labels) -> String
|
|
35
|
+
# private def format_action: (IR::parser_action action) -> String
|
|
36
|
+
# private def self.format_action: (IR::parser_action action) -> String
|
|
37
|
+
# private def symbol_name: (Hash[Integer, String] labels, Integer id) -> String
|
|
38
|
+
# private def self.symbol_name: (Hash[Integer, String] labels, Integer id) -> String
|
|
39
|
+
# private def tree_label: (IR::Grammar grammar, Hash[Integer, String] labels, untyped value) -> untyped
|
|
40
|
+
# private def self.tree_label: (IR::Grammar grammar, Hash[Integer, String] labels, untyped value) -> untyped
|
|
41
|
+
|
|
42
|
+
# @rbs (IR::Automaton automaton, ?max_tokens: Integer, ?max_configurations: Integer) -> String
|
|
43
|
+
def render(automaton, max_tokens: LALR::Counterexample::DEFAULT_MAX_TOKENS,
|
|
44
|
+
max_configurations: LALR::Counterexample::DEFAULT_MAX_CONFIGURATIONS)
|
|
45
|
+
grammar = automaton.grammar
|
|
46
|
+
labels = SymbolLabels.build(grammar)
|
|
47
|
+
lines = ["Algorithm: #{automaton.algorithm}", "States: #{automaton.states.length}", ""]
|
|
48
|
+
examples = LALR::Counterexample.new(
|
|
49
|
+
automaton, max_tokens: max_tokens, max_configurations: max_configurations
|
|
50
|
+
).all.group_by { |example| example[:state] }
|
|
51
|
+
automaton.states.each do |state|
|
|
52
|
+
append_state(lines, state, grammar, examples.fetch(state.id, Array.new(0)), labels)
|
|
53
|
+
end
|
|
54
|
+
summary = automaton.conflict_summary
|
|
55
|
+
lines << "Conflicts: #{summary[:sr]} shift/reduce, #{summary[:rr]} reduce/reduce"
|
|
56
|
+
"#{lines.join("\n")}\n"
|
|
57
|
+
end
|
|
58
|
+
module_function :render
|
|
59
|
+
|
|
60
|
+
# @rbs skip
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
# @rbs skip
|
|
64
|
+
def append_state(lines, state, grammar, examples, labels)
|
|
65
|
+
lines << "State #{state.id}"
|
|
66
|
+
state.items.each { |item| lines << " #{format_item(item, grammar, labels)}" }
|
|
67
|
+
state.actions.each do |token_id, action|
|
|
68
|
+
lines << " on #{symbol_name(labels, token_id)}: #{format_action(action)}"
|
|
69
|
+
end
|
|
70
|
+
lines << " default: #{format_action(state.default_action)}" if state.default_action
|
|
71
|
+
state.gotos.each { |symbol_id, target| lines << " goto #{symbol_name(labels, symbol_id)}: #{target}" }
|
|
72
|
+
state.conflicts.each { |conflict| lines << " conflict: #{conflict.inspect}" }
|
|
73
|
+
examples.each { |example| append_counterexample(lines, example, grammar, labels) }
|
|
74
|
+
lines << ""
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @rbs skip
|
|
78
|
+
def append_counterexample(lines, example, grammar, labels)
|
|
79
|
+
label = example[:unifying] ? "unifying counterexample" : "nonunifying witness"
|
|
80
|
+
sentence = example[:sentence].dup.insert(example[:lookahead_index], "•").join(" ")
|
|
81
|
+
lines << " #{label}: #{sentence}"
|
|
82
|
+
example[:interpretations].each do |interpretation|
|
|
83
|
+
lines << " #{interpretation[:kind]} derivation:"
|
|
84
|
+
append_tree(lines, interpretation[:tree], " ", grammar, labels)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @rbs skip
|
|
89
|
+
def append_tree(lines, tree, indentation, grammar, labels)
|
|
90
|
+
unless tree.is_a?(Hash)
|
|
91
|
+
lines << "#{indentation}#{tree_label(grammar, labels, tree)}"
|
|
92
|
+
return
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
symbol = tree_label(grammar, labels, tree[:symbol] || tree[:token])
|
|
96
|
+
unless tree[:children]
|
|
97
|
+
lines << "#{indentation}#{symbol}"
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
production = tree[:production] ? " (production #{tree[:production]})" : ""
|
|
102
|
+
lines << "#{indentation}#{symbol}#{production}"
|
|
103
|
+
tree[:children].each { |child| append_tree(lines, child, "#{indentation} ", grammar, labels) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @rbs skip
|
|
107
|
+
def format_item(item, grammar, labels)
|
|
108
|
+
if item.production == LALR::Builder::AUGMENTED_PRODUCTION
|
|
109
|
+
rhs = [grammar.start]
|
|
110
|
+
lhs = "$accept"
|
|
111
|
+
else
|
|
112
|
+
production = grammar.productions.fetch(item.production)
|
|
113
|
+
rhs = production.rhs.map { |id| symbol_name(labels, id) }
|
|
114
|
+
lhs = symbol_name(labels, production.lhs)
|
|
115
|
+
end
|
|
116
|
+
rhs = rhs.dup.insert(item.dot, "•")
|
|
117
|
+
lookaheads = item.lookaheads.map { |id| symbol_name(labels, id) }.join(", ")
|
|
118
|
+
"#{lhs} -> #{rhs.join(' ')} [#{lookaheads}]"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# @rbs skip
|
|
122
|
+
def format_action(action)
|
|
123
|
+
case action[:type]
|
|
124
|
+
when :shift then "shift #{action[:state]}"
|
|
125
|
+
when :reduce then "reduce #{action[:production]}"
|
|
126
|
+
else action[:type].to_s
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# @rbs skip
|
|
131
|
+
def symbol_name(labels, id)
|
|
132
|
+
labels.fetch(id) { raise Ibex::Error, "missing grammar symbol id #{id}" }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @rbs skip
|
|
136
|
+
def tree_label(grammar, labels, value)
|
|
137
|
+
symbol = grammar.symbol(value.to_s)
|
|
138
|
+
symbol ? symbol_name(labels, symbol.id) : value
|
|
139
|
+
end
|
|
140
|
+
module_function :append_state, :append_counterexample, :append_tree, :format_item, :format_action, :symbol_name,
|
|
141
|
+
:tree_label
|
|
142
|
+
|
|
143
|
+
class << self
|
|
144
|
+
private :append_state, :append_counterexample, :append_tree, :format_item, :format_action, :symbol_name,
|
|
145
|
+
:tree_label
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../runtime/parser"
|
|
4
|
+
|
|
5
|
+
module Ibex
|
|
6
|
+
module Codegen
|
|
7
|
+
# Generates a standalone Ruby parser class from Automaton IR.
|
|
8
|
+
class Ruby
|
|
9
|
+
# @rbs @automaton: IR::Automaton
|
|
10
|
+
# @rbs @grammar: IR::Grammar
|
|
11
|
+
# @rbs @table_format: Symbol
|
|
12
|
+
# @rbs @embedded: bool
|
|
13
|
+
# @rbs @line_convert: bool
|
|
14
|
+
# @rbs @line_convert_all: bool
|
|
15
|
+
# @rbs @debug: bool
|
|
16
|
+
# @rbs @omit_action_call: bool
|
|
17
|
+
# @rbs @superclass: String
|
|
18
|
+
# @rbs @executable: String?
|
|
19
|
+
|
|
20
|
+
# @rbs (IR::Automaton automaton, ?table: Symbol | String, ?embedded: bool, ?line_convert: bool,
|
|
21
|
+
# ?line_convert_all: bool, ?debug: bool, ?omit_action_call: bool?, ?superclass: String?,
|
|
22
|
+
# ?executable: String?) -> void
|
|
23
|
+
def initialize(automaton, table: :compact, embedded: false, line_convert: true, debug: false,
|
|
24
|
+
line_convert_all: false, omit_action_call: nil, superclass: nil, executable: nil)
|
|
25
|
+
@automaton = automaton
|
|
26
|
+
@grammar = automaton.grammar
|
|
27
|
+
@table_format = table.to_sym
|
|
28
|
+
@embedded = embedded
|
|
29
|
+
@line_convert = line_convert
|
|
30
|
+
@line_convert_all = line_convert_all
|
|
31
|
+
@debug = debug
|
|
32
|
+
@omit_action_call = omit_action_call.nil? ? @grammar.options[:omit_action_call] : omit_action_call
|
|
33
|
+
@superclass = superclass || @grammar.superclass || "Ibex::Runtime::Parser"
|
|
34
|
+
@executable = executable
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @rbs () -> String
|
|
38
|
+
def generate
|
|
39
|
+
lines = [] #: Array[String]
|
|
40
|
+
lines << "#!#{@executable}" if @executable
|
|
41
|
+
lines.push("# frozen_string_literal: true", "# Generated by Ibex #{grammar_digest_comment}", "")
|
|
42
|
+
append_user_code(lines, "header")
|
|
43
|
+
append_runtime(lines)
|
|
44
|
+
modules, class_name = class_parts
|
|
45
|
+
modules.each { |name| lines << "module #{name}" }
|
|
46
|
+
lines << "class #{class_name} < #{@superclass}"
|
|
47
|
+
append_tables(lines)
|
|
48
|
+
append_actions(lines)
|
|
49
|
+
append_user_code(lines, "inner", indent: 2)
|
|
50
|
+
lines << "end"
|
|
51
|
+
modules.reverse_each { lines << "end" }
|
|
52
|
+
append_user_code(lines, "footer")
|
|
53
|
+
"#{lines.join("\n")}\n"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# @rbs (Array[String] lines) -> void
|
|
59
|
+
def append_runtime(lines)
|
|
60
|
+
if @embedded
|
|
61
|
+
lines << embedded_source("../runtime/parser.rb")
|
|
62
|
+
lines << embedded_source("../../ibex/tables.rb")
|
|
63
|
+
else
|
|
64
|
+
lines << 'require "ibex/runtime"'
|
|
65
|
+
lines << 'require "ibex/tables"' if @table_format == :compact
|
|
66
|
+
end
|
|
67
|
+
lines << ""
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @rbs (String relative_path) -> String
|
|
71
|
+
def embedded_source(relative_path)
|
|
72
|
+
path = File.expand_path(relative_path, File.dirname(__FILE__))
|
|
73
|
+
File.read(path).lines.reject { |line| line.start_with?("# frozen_string_literal:") }.join.rstrip
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @rbs (Array[String] lines) -> void
|
|
77
|
+
def append_tables(lines)
|
|
78
|
+
table_set = Tables.build(@automaton, format: @table_format)
|
|
79
|
+
indent = " "
|
|
80
|
+
lines << "#{indent}PARSER_TABLE_FORMAT_VERSION = #{Runtime::PARSER_TABLE_FORMAT_VERSION}"
|
|
81
|
+
lines << "#{indent}TOKEN_IDS = #{token_ids_literal}.freeze"
|
|
82
|
+
lines << "#{indent}TOKEN_NAMES = #{token_names_literal}.freeze"
|
|
83
|
+
lines << "#{indent}ACTIONS = #{table_literal(table_set.actions)}"
|
|
84
|
+
lines << "#{indent}GOTOS = #{table_literal(table_set.gotos)}"
|
|
85
|
+
lines << "#{indent}DEFAULT_ACTIONS = #{table_set.default_actions.inspect}.freeze"
|
|
86
|
+
lines << "#{indent}PRODUCTIONS = #{productions_literal}.freeze"
|
|
87
|
+
lines << "#{indent}PARSER_TABLES = { format_version: PARSER_TABLE_FORMAT_VERSION,"
|
|
88
|
+
lines << "#{indent} tokens: TOKEN_IDS, token_names: TOKEN_NAMES, actions: ACTIONS,"
|
|
89
|
+
lines << "#{indent} gotos: GOTOS, default_actions: DEFAULT_ACTIONS,"
|
|
90
|
+
lines << "#{indent} productions: PRODUCTIONS }.freeze"
|
|
91
|
+
lines << "#{indent}def self.parser_tables = PARSER_TABLES"
|
|
92
|
+
lines << "#{indent}DEBUG_ENABLED = #{@debug}"
|
|
93
|
+
lines << ""
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# @rbs (untyped table) -> String
|
|
97
|
+
def table_literal(table)
|
|
98
|
+
return "#{table.inspect}.freeze" unless table.is_a?(Tables::Compact)
|
|
99
|
+
|
|
100
|
+
"Ibex::Tables::Compact.new(offsets: #{table.offsets.inspect}, values: #{table.values.inspect}, " \
|
|
101
|
+
"checks: #{table.checks.inspect}, row_count: #{table.row_count})"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @rbs () -> String
|
|
105
|
+
def token_ids_literal
|
|
106
|
+
pairs = @grammar.terminals.filter_map do |terminal|
|
|
107
|
+
next if terminal.id.zero? || terminal.name == "error"
|
|
108
|
+
|
|
109
|
+
"#{external_token_expression(terminal)} => #{terminal.id}"
|
|
110
|
+
end
|
|
111
|
+
"{ #{pairs.join(', ')} }"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @rbs (IR::GrammarSymbol terminal) -> String
|
|
115
|
+
def external_token_expression(terminal)
|
|
116
|
+
conversion = @grammar.conversions[terminal.name]
|
|
117
|
+
return "(#{conversion})" if conversion
|
|
118
|
+
return terminal.name if terminal.name.start_with?("'", '"')
|
|
119
|
+
|
|
120
|
+
":#{terminal.name}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# @rbs () -> String
|
|
124
|
+
def token_names_literal
|
|
125
|
+
entries = @grammar.terminals.map { |terminal| "#{terminal.id} => #{terminal.name.inspect}" }
|
|
126
|
+
"{ #{entries.join(', ')} }"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @rbs () -> String
|
|
130
|
+
def productions_literal
|
|
131
|
+
entries = @grammar.productions.map do |production|
|
|
132
|
+
action = action_method?(production) ? ":_ibex_action_#{production.id}" : "nil"
|
|
133
|
+
"{ lhs: #{production.lhs}, length: #{production.rhs.length}, action: #{action} }"
|
|
134
|
+
end
|
|
135
|
+
"[#{entries.join(', ')}]"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @rbs (Array[String] lines) -> void
|
|
139
|
+
def append_actions(lines)
|
|
140
|
+
@grammar.productions.each do |production|
|
|
141
|
+
next unless action_method?(production)
|
|
142
|
+
|
|
143
|
+
append_action_constant(lines, production)
|
|
144
|
+
append_action_method(lines, production)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @rbs (Array[String] lines, IR::Production production) -> void
|
|
149
|
+
def append_action_constant(lines, production)
|
|
150
|
+
return unless production.action && @line_convert
|
|
151
|
+
|
|
152
|
+
lines << " ACTION_CODE_#{production.id} = #{production.action.code.inspect}.freeze"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @rbs (Array[String] lines, IR::Production production) -> void
|
|
156
|
+
def append_action_method(lines, production)
|
|
157
|
+
action = production.action
|
|
158
|
+
lines << " private def _ibex_action_#{production.id}(val, _values)"
|
|
159
|
+
lines << " val = _values.last(#{action.context_length})" if action&.context_length&.positive?
|
|
160
|
+
append_named_bindings(lines, action)
|
|
161
|
+
if action
|
|
162
|
+
append_semantic_code(lines, production)
|
|
163
|
+
else
|
|
164
|
+
lines << " val[0]"
|
|
165
|
+
end
|
|
166
|
+
lines << " end"
|
|
167
|
+
lines << ""
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @rbs (Array[String] lines, IR::Action? action) -> void
|
|
171
|
+
def append_named_bindings(lines, action)
|
|
172
|
+
return unless action&.named_refs&.any?
|
|
173
|
+
|
|
174
|
+
action.named_refs.each { |reference| lines << " #{reference[:name]} = val[#{reference[:index]}]" }
|
|
175
|
+
names = action.named_refs.map { |reference| reference[:name] }
|
|
176
|
+
lines << " _ibex_named_values = [#{names.join(', ')}]"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @rbs (Array[String] lines, IR::Production production) -> void
|
|
180
|
+
def append_semantic_code(lines, production)
|
|
181
|
+
action = production.action || raise(Ibex::Error, "missing semantic action")
|
|
182
|
+
lines << " result = val[0]" if @grammar.options[:result_var]
|
|
183
|
+
if @line_convert
|
|
184
|
+
evaluation = "eval(ACTION_CODE_#{production.id}, binding, #{action.location[:file].inspect}, " \
|
|
185
|
+
"#{action.location[:line]})"
|
|
186
|
+
lines << " #{@grammar.options[:result_var] ? evaluation : "return #{evaluation}"}"
|
|
187
|
+
else
|
|
188
|
+
action.code.lines.each { |line| lines << " #{line.rstrip}" }
|
|
189
|
+
end
|
|
190
|
+
lines << " result" if @grammar.options[:result_var]
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# @rbs (IR::Production production) -> bool
|
|
194
|
+
def action_method?(production)
|
|
195
|
+
!!(production.action || !@omit_action_call)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @rbs (Array[String] lines, String name, ?indent: Integer) -> void
|
|
199
|
+
def append_user_code(lines, name, indent: 0)
|
|
200
|
+
code = @grammar.user_code[name]
|
|
201
|
+
return if code.nil? || code.empty?
|
|
202
|
+
|
|
203
|
+
prefix = " " * indent
|
|
204
|
+
chunks = @grammar.user_code_chunks.fetch(name, Array.new(0))
|
|
205
|
+
if map_user_code?(name) && !chunks.empty?
|
|
206
|
+
chunks.each do |chunk|
|
|
207
|
+
location = chunk.location
|
|
208
|
+
lines << "#{prefix}eval(#{chunk.code.dump}, binding, #{location[:file].inspect}, #{location[:line]})"
|
|
209
|
+
end
|
|
210
|
+
elsif @line_convert_all
|
|
211
|
+
raise Ibex::Error, "(codegen):1:1: source locations are required to convert #{name} user code"
|
|
212
|
+
else
|
|
213
|
+
code.lines.each { |line| lines << "#{prefix}#{line.rstrip}" }
|
|
214
|
+
end
|
|
215
|
+
lines << ""
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# @rbs (String name) -> bool
|
|
219
|
+
def map_user_code?(name)
|
|
220
|
+
@line_convert_all || (@line_convert && name == "inner")
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# @rbs () -> [Array[String], String]
|
|
224
|
+
def class_parts
|
|
225
|
+
parts = @grammar.class_name.split("::")
|
|
226
|
+
[parts.take(parts.length - 1), parts.fetch(-1)]
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# @rbs () -> String
|
|
230
|
+
def grammar_digest_comment
|
|
231
|
+
@automaton.grammar_digest.delete_prefix("sha256:").chars.first(12).join
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
module Codegen
|
|
5
|
+
# Builds human-facing symbol labels without changing Grammar IR identities.
|
|
6
|
+
module SymbolLabels
|
|
7
|
+
# @rbs (IR::Grammar grammar) -> Hash[Integer, String]
|
|
8
|
+
def self.build(grammar)
|
|
9
|
+
labels = grammar.symbols.to_h { |symbol| [symbol.id, symbol.name] }
|
|
10
|
+
grammar.productions.each do |production|
|
|
11
|
+
expression = production.origin[:expression]
|
|
12
|
+
labels[production.lhs] = expression if expression.is_a?(String)
|
|
13
|
+
end
|
|
14
|
+
labels
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/ibex/error.rb
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
module Frontend
|
|
5
|
+
# Extracts a balanced Ruby action while ignoring braces inside literals.
|
|
6
|
+
class ActionScanner
|
|
7
|
+
# @rbs!
|
|
8
|
+
# type heredoc = {
|
|
9
|
+
# identifier: String,
|
|
10
|
+
# indented: bool,
|
|
11
|
+
# length: Integer,
|
|
12
|
+
# location: Location
|
|
13
|
+
# }
|
|
14
|
+
|
|
15
|
+
PAIRED_DELIMITERS = { "(" => ")", "[" => "]", "{" => "}", "<" => ">" }.freeze #: Hash[String, String]
|
|
16
|
+
REGEX_PREFIXES = "=([{!,:;?&|+-*%^~<>" #: String
|
|
17
|
+
|
|
18
|
+
# @rbs @cursor: SourceCursor
|
|
19
|
+
# @rbs @pending_heredocs: Array[heredoc]
|
|
20
|
+
|
|
21
|
+
# @rbs (SourceCursor cursor) -> void
|
|
22
|
+
def initialize(cursor)
|
|
23
|
+
@cursor = cursor
|
|
24
|
+
@pending_heredocs = [] #: Array[heredoc]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @rbs () -> Token
|
|
28
|
+
def scan
|
|
29
|
+
location = @cursor.location
|
|
30
|
+
@cursor.advance
|
|
31
|
+
start = @cursor.index
|
|
32
|
+
scan_code(1)
|
|
33
|
+
finish = @cursor.index
|
|
34
|
+
@cursor.advance
|
|
35
|
+
Token.new(type: :action, value: @cursor.source[start...finish], location: location)
|
|
36
|
+
rescue Ibex::Error
|
|
37
|
+
raise
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
raise Ibex::Error, "#{location}: unable to scan action: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# @rbs (Integer depth) -> void
|
|
45
|
+
def scan_code(depth)
|
|
46
|
+
until @cursor.eof?
|
|
47
|
+
if @cursor.peek == "\n" && @pending_heredocs.any?
|
|
48
|
+
@cursor.advance
|
|
49
|
+
scan_pending_heredocs
|
|
50
|
+
next
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
index = @cursor.index
|
|
54
|
+
scan_special_character
|
|
55
|
+
next if @cursor.index != index
|
|
56
|
+
|
|
57
|
+
depth += 1 if @cursor.peek == "{"
|
|
58
|
+
depth -= 1 if @cursor.peek == "}"
|
|
59
|
+
return if depth.zero?
|
|
60
|
+
|
|
61
|
+
@cursor.advance
|
|
62
|
+
end
|
|
63
|
+
raise Ibex::Error, "#{@cursor.location}: unterminated action"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @rbs () -> void
|
|
67
|
+
def scan_special_character
|
|
68
|
+
character = @cursor.peek
|
|
69
|
+
case character
|
|
70
|
+
when "'", '"', "`" then scan_quoted(character)
|
|
71
|
+
when "%" then scan_percent_literal
|
|
72
|
+
when "/" then scan_regexp if regexp_start?
|
|
73
|
+
when "#" then scan_comment
|
|
74
|
+
when "?" then scan_character_literal
|
|
75
|
+
when "<" then scan_heredoc if @cursor.peek(1) == "<"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @rbs (String quote) -> void
|
|
80
|
+
def scan_quoted(quote)
|
|
81
|
+
start = @cursor.location
|
|
82
|
+
@cursor.advance
|
|
83
|
+
until @cursor.eof?
|
|
84
|
+
if @cursor.peek == "\\"
|
|
85
|
+
@cursor.advance(2)
|
|
86
|
+
elsif quote != "'" && @cursor.rest.start_with?("\#{")
|
|
87
|
+
@cursor.advance(2)
|
|
88
|
+
scan_interpolation
|
|
89
|
+
elsif @cursor.peek == quote
|
|
90
|
+
@cursor.advance
|
|
91
|
+
return
|
|
92
|
+
else
|
|
93
|
+
@cursor.advance
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
raise Ibex::Error, "#{start}: unterminated #{quote} string in action"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @rbs () -> void
|
|
100
|
+
def scan_interpolation
|
|
101
|
+
depth = 1
|
|
102
|
+
until @cursor.eof?
|
|
103
|
+
character = @cursor.peek
|
|
104
|
+
raise Ibex::Error, "#{@cursor.location}: unterminated string interpolation" unless character
|
|
105
|
+
|
|
106
|
+
if ["'", '"', "`"].include?(character)
|
|
107
|
+
scan_quoted(character)
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
if character == "{"
|
|
111
|
+
depth += 1
|
|
112
|
+
elsif character == "}"
|
|
113
|
+
depth -= 1
|
|
114
|
+
@cursor.advance
|
|
115
|
+
return if depth.zero?
|
|
116
|
+
|
|
117
|
+
next
|
|
118
|
+
end
|
|
119
|
+
@cursor.advance
|
|
120
|
+
end
|
|
121
|
+
raise Ibex::Error, "#{@cursor.location}: unterminated string interpolation"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @rbs () -> void
|
|
125
|
+
def scan_percent_literal
|
|
126
|
+
match = @cursor.rest.match(/\A%(?:[qQwWiIxrs])?([^\w\s])/)
|
|
127
|
+
return unless match
|
|
128
|
+
|
|
129
|
+
literal_prefix = match[0]
|
|
130
|
+
opener = match[1]
|
|
131
|
+
return unless literal_prefix && opener
|
|
132
|
+
|
|
133
|
+
closer = PAIRED_DELIMITERS.fetch(opener, opener)
|
|
134
|
+
@cursor.advance(literal_prefix.length)
|
|
135
|
+
scan_delimited(opener, closer)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @rbs (String opener, String closer) -> void
|
|
139
|
+
def scan_delimited(opener, closer)
|
|
140
|
+
start = @cursor.location
|
|
141
|
+
depth = 1
|
|
142
|
+
until @cursor.eof?
|
|
143
|
+
if @cursor.peek == "\\"
|
|
144
|
+
@cursor.advance(2)
|
|
145
|
+
next
|
|
146
|
+
end
|
|
147
|
+
depth += 1 if opener != closer && @cursor.peek == opener
|
|
148
|
+
depth -= 1 if @cursor.peek == closer
|
|
149
|
+
@cursor.advance
|
|
150
|
+
return if depth.zero?
|
|
151
|
+
end
|
|
152
|
+
raise Ibex::Error, "#{start}: unterminated percent literal"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @rbs () -> bool
|
|
156
|
+
def regexp_start?
|
|
157
|
+
prefix = @cursor.source.chars.take(@cursor.index).join.rstrip[-1]
|
|
158
|
+
prefix.nil? || REGEX_PREFIXES.include?(prefix)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# @rbs () -> void
|
|
162
|
+
def scan_regexp
|
|
163
|
+
start = @cursor.location
|
|
164
|
+
@cursor.advance
|
|
165
|
+
in_class = false
|
|
166
|
+
until @cursor.eof?
|
|
167
|
+
if @cursor.peek == "\\"
|
|
168
|
+
@cursor.advance(2)
|
|
169
|
+
next
|
|
170
|
+
end
|
|
171
|
+
in_class = true if @cursor.peek == "["
|
|
172
|
+
in_class = false if @cursor.peek == "]"
|
|
173
|
+
if @cursor.peek == "/" && !in_class
|
|
174
|
+
@cursor.advance
|
|
175
|
+
@cursor.advance while @cursor.peek&.match?(/[a-z]/i)
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
@cursor.advance
|
|
179
|
+
end
|
|
180
|
+
raise Ibex::Error, "#{start}: unterminated regular expression"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# @rbs () -> void
|
|
184
|
+
def scan_comment
|
|
185
|
+
@cursor.advance until @cursor.eof? || @cursor.peek == "\n"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @rbs () -> void
|
|
189
|
+
def scan_character_literal
|
|
190
|
+
@cursor.advance
|
|
191
|
+
@cursor.advance if @cursor.peek == "\\"
|
|
192
|
+
@cursor.advance unless @cursor.eof?
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# @rbs () -> void
|
|
196
|
+
def scan_heredoc
|
|
197
|
+
opener = heredoc_opener
|
|
198
|
+
return unless opener
|
|
199
|
+
|
|
200
|
+
@cursor.advance(opener[:length])
|
|
201
|
+
@pending_heredocs << opener
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @rbs () -> heredoc?
|
|
205
|
+
def heredoc_opener
|
|
206
|
+
prefix = @cursor.rest.match(/\A<<([~-]?)/)
|
|
207
|
+
return unless prefix
|
|
208
|
+
|
|
209
|
+
marker = prefix[0]
|
|
210
|
+
return unless marker
|
|
211
|
+
|
|
212
|
+
quote = @cursor.rest[marker.length]
|
|
213
|
+
identifier, length = if quote && ["'", '"', "`"].include?(quote)
|
|
214
|
+
quoted_heredoc_identifier(marker, quote)
|
|
215
|
+
else
|
|
216
|
+
bare_heredoc_identifier(marker)
|
|
217
|
+
end
|
|
218
|
+
return unless identifier
|
|
219
|
+
|
|
220
|
+
modifier = prefix[1] || ""
|
|
221
|
+
{ identifier: identifier, indented: !modifier.empty?, length: length,
|
|
222
|
+
location: @cursor.location } #: heredoc
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# @rbs (String prefix, String quote) -> [String, Integer]?
|
|
226
|
+
def quoted_heredoc_identifier(prefix, quote)
|
|
227
|
+
start = prefix.length + 1
|
|
228
|
+
finish = @cursor.rest.index(quote, start)
|
|
229
|
+
return unless finish
|
|
230
|
+
|
|
231
|
+
identifier = @cursor.rest[start...finish]
|
|
232
|
+
return unless identifier
|
|
233
|
+
return if identifier.match?(/[\r\n]/)
|
|
234
|
+
|
|
235
|
+
[identifier, finish + 1]
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# @rbs (String prefix) -> [String?, Integer]
|
|
239
|
+
def bare_heredoc_identifier(prefix)
|
|
240
|
+
suffix = @cursor.rest[prefix.length..] || ""
|
|
241
|
+
identifier = suffix.match(/\A[A-Za-z_]\w*/)&.[](0)
|
|
242
|
+
[identifier, prefix.length + identifier.to_s.length]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# @rbs () -> void
|
|
246
|
+
def scan_pending_heredocs
|
|
247
|
+
@pending_heredocs.shift.then { |heredoc| scan_heredoc_body(heredoc) } until @pending_heredocs.empty?
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# @rbs (heredoc heredoc) -> void
|
|
251
|
+
def scan_heredoc_body(heredoc)
|
|
252
|
+
identifier = heredoc[:identifier]
|
|
253
|
+
escaped_identifier = Regexp.escape(identifier)
|
|
254
|
+
prefix = heredoc[:indented] ? "[ \\t]*" : ""
|
|
255
|
+
terminator = /\A#{prefix}#{escaped_identifier}\r?\z/
|
|
256
|
+
until @cursor.eof?
|
|
257
|
+
line = @cursor.rest[/\A[^\n]*(?:\n|\z)/] || ""
|
|
258
|
+
content = line.delete_suffix("\n")
|
|
259
|
+
@cursor.advance(line.length)
|
|
260
|
+
return if content.match?(terminator)
|
|
261
|
+
end
|
|
262
|
+
raise Ibex::Error, "#{heredoc[:location]}: unterminated heredoc #{identifier}"
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|