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,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
module LALR
|
|
5
|
+
# Selects size-reducing default actions without changing any terminal cell.
|
|
6
|
+
module DefaultReductions
|
|
7
|
+
# @rbs!
|
|
8
|
+
# private def select_default: (Hash[Integer, IR::parser_action] actions, Array[Integer] terminal_ids) -> IR::parser_action?
|
|
9
|
+
# private def self.select_default: (Hash[Integer, IR::parser_action] actions, Array[Integer] terminal_ids) -> IR::parser_action?
|
|
10
|
+
|
|
11
|
+
ERROR_ACTION = { type: :error }.freeze #: IR::error_action
|
|
12
|
+
|
|
13
|
+
# @rbs (Array[IR::AutomatonState] states, terminal_ids: Array[Integer]) -> Array[IR::AutomatonState]
|
|
14
|
+
def apply(states, terminal_ids:)
|
|
15
|
+
states.map { |state| optimize(state, terminal_ids: terminal_ids) }
|
|
16
|
+
end
|
|
17
|
+
module_function :apply
|
|
18
|
+
|
|
19
|
+
# @rbs (IR::AutomatonState state, terminal_ids: Array[Integer]) -> IR::AutomatonState
|
|
20
|
+
def optimize(state, terminal_ids:)
|
|
21
|
+
return state if state.default_action
|
|
22
|
+
|
|
23
|
+
default_action = select_default(state.actions, terminal_ids)
|
|
24
|
+
return state unless default_action
|
|
25
|
+
|
|
26
|
+
actions = {} #: Hash[Integer, IR::parser_action]
|
|
27
|
+
terminal_ids.each_with_object(actions) do |token_id, result|
|
|
28
|
+
action = state.actions[token_id]
|
|
29
|
+
result[token_id] = action || ERROR_ACTION unless action == default_action
|
|
30
|
+
end
|
|
31
|
+
IR::AutomatonState.new(id: state.id, items: state.items, transitions: state.transitions,
|
|
32
|
+
actions: actions, gotos: state.gotos, default_action: default_action,
|
|
33
|
+
conflicts: state.conflicts)
|
|
34
|
+
end
|
|
35
|
+
module_function :optimize
|
|
36
|
+
|
|
37
|
+
# @rbs skip
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# @rbs skip
|
|
41
|
+
def select_default(actions, terminal_ids)
|
|
42
|
+
candidates = [] #: Array[IR::reduce_action]
|
|
43
|
+
actions.each_value do |action|
|
|
44
|
+
next unless action[:type] == :reduce
|
|
45
|
+
|
|
46
|
+
reduction = action #: IR::reduce_action
|
|
47
|
+
candidates << reduction
|
|
48
|
+
end
|
|
49
|
+
candidates.uniq!
|
|
50
|
+
candidates.sort_by! { |action| action.fetch(:production) }
|
|
51
|
+
candidate = candidates.max_by do |action|
|
|
52
|
+
saved_entries = actions.count { |_token_id, candidate| candidate == action }
|
|
53
|
+
[saved_entries, -action.fetch(:production)]
|
|
54
|
+
end
|
|
55
|
+
return unless candidate
|
|
56
|
+
|
|
57
|
+
optimized_size = terminal_ids.count { |token_id| actions[token_id] != candidate } + 1
|
|
58
|
+
candidate if optimized_size < actions.length
|
|
59
|
+
end
|
|
60
|
+
module_function :select_default
|
|
61
|
+
|
|
62
|
+
class << self
|
|
63
|
+
private :select_default
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/ibex/lalr.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lalr/conflict"
|
|
4
|
+
require_relative "lalr/default_reductions"
|
|
5
|
+
require_relative "lalr/builder"
|
|
6
|
+
require_relative "lalr/conflict_search_limits"
|
|
7
|
+
require_relative "lalr/conflict_search"
|
|
8
|
+
require_relative "lalr/counterexample"
|
|
9
|
+
|
|
10
|
+
module Ibex
|
|
11
|
+
module LALR
|
|
12
|
+
# @rbs!
|
|
13
|
+
# type lr_item = [Integer, Integer, Integer]
|
|
14
|
+
# type item_core = [Integer, Integer]
|
|
15
|
+
# type item_set = Set[lr_item]
|
|
16
|
+
# type packed_items = Hash[item_core, Set[Integer]]
|
|
17
|
+
# type transitions = Array[Hash[Integer, Integer]]
|
|
18
|
+
# type derivation_node = Hash[Symbol, untyped]
|
|
19
|
+
# type search_status = :conflict | :shifted | :accepted
|
|
20
|
+
# type search_entry = [search_status, ConflictSearch::Configuration]
|
|
21
|
+
# type search_result = {
|
|
22
|
+
# sentence_ids: Array[Integer],
|
|
23
|
+
# lookahead_index: Integer,
|
|
24
|
+
# interpretations: Array[IR::interpretation]
|
|
25
|
+
# }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
# Declaration extraction used by Normalizer.
|
|
5
|
+
module NormalizeDeclarations
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# @rbs () -> void
|
|
9
|
+
def read_declarations
|
|
10
|
+
# @type self: Normalizer
|
|
11
|
+
@declared_tokens = {} #: Hash[String, IR::location]
|
|
12
|
+
@precedence = {} #: Hash[String, IR::precedence]
|
|
13
|
+
@precedence_locations = {} #: Hash[String, IR::location]
|
|
14
|
+
@options = { result_var: true, omit_action_call: true }
|
|
15
|
+
@expected_conflicts = 0
|
|
16
|
+
@conversions = {} #: Hash[String, String]
|
|
17
|
+
@ast.declarations.each { |declaration| read_declaration(declaration) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @rbs (Frontend::AST::declaration declaration) -> void
|
|
21
|
+
def read_declaration(declaration)
|
|
22
|
+
# @type self: Normalizer
|
|
23
|
+
case declaration
|
|
24
|
+
when Frontend::AST::Tokens then declaration.names.each { |name| @declared_tokens[name] = declaration.loc.to_h }
|
|
25
|
+
when Frontend::AST::Precedence then read_precedence(declaration)
|
|
26
|
+
when Frontend::AST::Options then declaration.names.each { |name| read_option(name, declaration.loc) }
|
|
27
|
+
when Frontend::AST::Expect then @expected_conflicts = declaration.conflicts
|
|
28
|
+
when Frontend::AST::Start
|
|
29
|
+
@explicit_start = declaration.name
|
|
30
|
+
@start_location = declaration.loc
|
|
31
|
+
when Frontend::AST::Convert then declaration.pairs.each { |pair| @conversions[pair.name] = pair.expression }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @rbs (Frontend::AST::Precedence declaration) -> void
|
|
36
|
+
def read_precedence(declaration)
|
|
37
|
+
# @type self: Normalizer
|
|
38
|
+
count = declaration.levels.length
|
|
39
|
+
declaration.levels.each_with_index do |level, index|
|
|
40
|
+
numeric_level = declaration.direction == :high_to_low ? count - index : index + 1
|
|
41
|
+
level.symbols.each do |name|
|
|
42
|
+
@precedence[name] = { associativity: level.associativity, level: numeric_level }
|
|
43
|
+
@precedence_locations[name] = level.loc.to_h
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @rbs (String name, Frontend::Location location) -> void
|
|
49
|
+
def read_option(name, location)
|
|
50
|
+
# @type self: Normalizer
|
|
51
|
+
case name
|
|
52
|
+
when "no_result_var" then @options[:result_var] = false
|
|
53
|
+
when "result_var" then @options[:result_var] = true
|
|
54
|
+
when "omit_action_call" then @options[:omit_action_call] = true
|
|
55
|
+
when "no_omit_action_call" then @options[:omit_action_call] = false
|
|
56
|
+
else fail_at(location, "unknown option #{name}")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Ibex
|
|
6
|
+
# Static grammar diagnostics used by Normalizer.
|
|
7
|
+
module NormalizeDiagnostics
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# @rbs () -> void
|
|
11
|
+
def validate_grammar
|
|
12
|
+
# @type self: Normalizer
|
|
13
|
+
warn_duplicate_productions
|
|
14
|
+
warn_unreachable_nonterminals
|
|
15
|
+
warn_unused_terminals
|
|
16
|
+
warn_empty_language
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @rbs () -> void
|
|
20
|
+
def warn_duplicate_productions
|
|
21
|
+
# @type self: Normalizer
|
|
22
|
+
seen = {} #: Hash[[Integer, Array[Integer]], Integer]
|
|
23
|
+
@productions.each do |production|
|
|
24
|
+
signature = [production.lhs, production.rhs] #: [Integer, Array[Integer]]
|
|
25
|
+
if seen.key?(signature)
|
|
26
|
+
@warnings << { type: :duplicate_production, production: production.id, original: seen[signature],
|
|
27
|
+
loc: production.origin[:loc] }
|
|
28
|
+
else
|
|
29
|
+
seen[signature] = production.id
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @rbs () -> void
|
|
35
|
+
def warn_unreachable_nonterminals
|
|
36
|
+
# @type self: Normalizer
|
|
37
|
+
reachable = reachable_symbol_ids
|
|
38
|
+
@symbols.select(&:nonterminal?).each do |grammar_symbol|
|
|
39
|
+
next if reachable.include?(grammar_symbol.id) || grammar_symbol.name.start_with?("$")
|
|
40
|
+
|
|
41
|
+
@warnings << { type: :unreachable_nonterminal, symbol: grammar_symbol.name, loc: grammar_symbol.location }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @rbs () -> Set[Integer]
|
|
46
|
+
def reachable_symbol_ids
|
|
47
|
+
# @type self: Normalizer
|
|
48
|
+
start = required_symbol(@start_name).id
|
|
49
|
+
reachable = Set[start]
|
|
50
|
+
loop do
|
|
51
|
+
before = reachable.length
|
|
52
|
+
@productions.select { |production| reachable.include?(production.lhs) }.each do |production|
|
|
53
|
+
production.rhs.each { |id| reachable << id }
|
|
54
|
+
end
|
|
55
|
+
return reachable if reachable.length == before
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @rbs () -> void
|
|
60
|
+
def warn_unused_terminals
|
|
61
|
+
# @type self: Normalizer
|
|
62
|
+
used = @productions.flat_map(&:rhs).to_set
|
|
63
|
+
@symbols.select(&:terminal?).each do |grammar_symbol|
|
|
64
|
+
next if grammar_symbol.reserved || used.include?(grammar_symbol.id) || @precedence.key?(grammar_symbol.name)
|
|
65
|
+
|
|
66
|
+
@warnings << { type: :unused_terminal, symbol: grammar_symbol.name, loc: grammar_symbol.location }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @rbs () -> void
|
|
71
|
+
def warn_empty_language
|
|
72
|
+
# @type self: Normalizer
|
|
73
|
+
productive = productive_terminal_ids
|
|
74
|
+
loop do
|
|
75
|
+
before = productive.length
|
|
76
|
+
@productions.each do |production|
|
|
77
|
+
productive << production.lhs if production.rhs.all? { |id| productive.include?(id) }
|
|
78
|
+
end
|
|
79
|
+
break if productive.length == before
|
|
80
|
+
end
|
|
81
|
+
return if productive.include?(required_symbol(@start_name).id)
|
|
82
|
+
|
|
83
|
+
start_symbol = required_symbol(@start_name)
|
|
84
|
+
@warnings << { type: :empty_language, symbol: @start_name, loc: start_symbol.location }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @rbs () -> Set[Integer]
|
|
88
|
+
def productive_terminal_ids
|
|
89
|
+
# @type self: Normalizer
|
|
90
|
+
@symbols.select { |grammar_symbol| productive_terminal?(grammar_symbol) }.to_set(&:id)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @rbs (IR::GrammarSymbol grammar_symbol) -> bool
|
|
94
|
+
def productive_terminal?(grammar_symbol)
|
|
95
|
+
# @type self: Normalizer
|
|
96
|
+
grammar_symbol.terminal? && !grammar_symbol.reserved
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
# Production and EBNF expansion used by Normalizer.
|
|
5
|
+
module NormalizeExpander
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
# @rbs () -> void
|
|
9
|
+
def normalize_user_productions
|
|
10
|
+
# @type self: Normalizer
|
|
11
|
+
@ast.rules.each do |rule|
|
|
12
|
+
rule.alternatives.each { |alternative| normalize_alternative(rule, alternative) }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @rbs (Frontend::AST::Rule rule, Frontend::AST::Alternative alternative) -> void
|
|
17
|
+
def normalize_alternative(rule, alternative)
|
|
18
|
+
# @type self: Normalizer
|
|
19
|
+
rhs = [] #: Array[String]
|
|
20
|
+
named_refs = [] #: Array[IR::named_ref]
|
|
21
|
+
alternative.items.each do |item|
|
|
22
|
+
if item.is_a?(Frontend::AST::InlineAction)
|
|
23
|
+
rhs << expand_inline_action(item, rhs.length, named_refs)
|
|
24
|
+
next
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
rhs << normalize_item(item)
|
|
28
|
+
add_named_reference(item, named_refs, rhs.length - 1)
|
|
29
|
+
end
|
|
30
|
+
action = normalize_action(alternative.action, named_refs)
|
|
31
|
+
add_production(rule.lhs, rhs, action, alternative.precedence,
|
|
32
|
+
{ kind: :user, loc: alternative.loc.to_h })
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @rbs (Frontend::AST::item item) -> String
|
|
36
|
+
def normalize_item(item)
|
|
37
|
+
# @type self: Normalizer
|
|
38
|
+
return symbol_for_reference(item).name if item.is_a?(Frontend::AST::SymbolReference)
|
|
39
|
+
return expand_group(item) if item.is_a?(Frontend::AST::Group)
|
|
40
|
+
return expand_optional(item) if item.is_a?(Frontend::AST::Optional)
|
|
41
|
+
return expand_star(item) if item.is_a?(Frontend::AST::Star)
|
|
42
|
+
return expand_plus(item) if item.is_a?(Frontend::AST::Plus)
|
|
43
|
+
return expand_separated_list(item) if item.is_a?(Frontend::AST::SeparatedList)
|
|
44
|
+
|
|
45
|
+
fail_at(item.loc, "unsupported nested EBNF expression")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @rbs (Frontend::AST::InlineAction item, Integer context_length, Array[IR::named_ref] named_refs) -> String
|
|
49
|
+
def expand_inline_action(item, context_length, named_refs)
|
|
50
|
+
# @type self: Normalizer
|
|
51
|
+
helper = new_helper("inline", item.loc)
|
|
52
|
+
action = IR::Action.new(code: item.code, location: item.loc.to_h, named_refs: named_refs.map(&:dup),
|
|
53
|
+
context_length: context_length)
|
|
54
|
+
add_production(helper, [], action, nil, { kind: :inline_action, loc: item.loc.to_h })
|
|
55
|
+
helper
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @rbs (Frontend::AST::Optional item) -> String
|
|
59
|
+
def expand_optional(item)
|
|
60
|
+
# @type self: Normalizer
|
|
61
|
+
base = normalize_item(item.item)
|
|
62
|
+
helper = new_helper("optional", item.loc)
|
|
63
|
+
add_production(helper, [], synthetic_action("nil", item.loc), nil, synthetic_origin(:optional, item))
|
|
64
|
+
add_production(helper, [base], synthetic_action("val[0]", item.loc), nil, synthetic_origin(:optional, item))
|
|
65
|
+
helper
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @rbs (Frontend::AST::Star item) -> String
|
|
69
|
+
def expand_star(item)
|
|
70
|
+
# @type self: Normalizer
|
|
71
|
+
base = normalize_item(item.item)
|
|
72
|
+
helper = new_helper("star", item.loc)
|
|
73
|
+
add_production(helper, [], synthetic_action("[]", item.loc), nil, synthetic_origin(:star, item))
|
|
74
|
+
add_production(helper, [helper, base], synthetic_action("val[0] + [val[1]]", item.loc), nil,
|
|
75
|
+
synthetic_origin(:star, item))
|
|
76
|
+
helper
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @rbs (Frontend::AST::Plus item) -> String
|
|
80
|
+
def expand_plus(item)
|
|
81
|
+
# @type self: Normalizer
|
|
82
|
+
base = normalize_item(item.item)
|
|
83
|
+
helper = new_helper("plus", item.loc)
|
|
84
|
+
add_production(helper, [base], synthetic_action("[val[0]]", item.loc), nil, synthetic_origin(:plus, item))
|
|
85
|
+
add_production(helper, [helper, base], synthetic_action("val[0] + [val[1]]", item.loc), nil,
|
|
86
|
+
synthetic_origin(:plus, item))
|
|
87
|
+
helper
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @rbs (Frontend::AST::SeparatedList item) -> String
|
|
91
|
+
def expand_separated_list(item)
|
|
92
|
+
# @type self: Normalizer
|
|
93
|
+
base = normalize_item(item.item)
|
|
94
|
+
separator = normalize_item(item.separator)
|
|
95
|
+
helper = new_helper("separated_list", item.loc)
|
|
96
|
+
unless item.nonempty
|
|
97
|
+
add_production(helper, [], synthetic_action("[]", item.loc), nil, synthetic_origin(:separated_list, item))
|
|
98
|
+
end
|
|
99
|
+
add_production(helper, [base], synthetic_action("[val[0]]", item.loc), nil,
|
|
100
|
+
synthetic_origin(:separated_list, item))
|
|
101
|
+
add_production(helper, [helper, separator, base], synthetic_action("val[0] + [val[2]]", item.loc), nil,
|
|
102
|
+
synthetic_origin(:separated_list, item))
|
|
103
|
+
helper
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @rbs (Frontend::AST::Group item) -> String
|
|
107
|
+
def expand_group(item)
|
|
108
|
+
# @type self: Normalizer
|
|
109
|
+
reject_group_named_references(item)
|
|
110
|
+
helper = new_helper("group", item.loc)
|
|
111
|
+
item.alternatives.each do |alternative|
|
|
112
|
+
rhs = alternative.map { |child| normalize_item(child) }
|
|
113
|
+
expression = group_value_expression(rhs.length)
|
|
114
|
+
add_production(helper, rhs, synthetic_action(expression, item.loc), nil, synthetic_origin(:group, item))
|
|
115
|
+
end
|
|
116
|
+
helper
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @rbs (Integer length) -> String
|
|
120
|
+
def group_value_expression(length)
|
|
121
|
+
# @type self: Normalizer
|
|
122
|
+
return "nil" if length.zero?
|
|
123
|
+
return "val[0]" if length == 1
|
|
124
|
+
|
|
125
|
+
"val"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# @rbs (Frontend::AST::Group group) -> void
|
|
129
|
+
def reject_group_named_references(group)
|
|
130
|
+
# @type self: Normalizer
|
|
131
|
+
reference = group.alternatives.flatten.filter_map { |item| named_reference_in(item) }.first
|
|
132
|
+
fail_at(reference.loc, "named references inside EBNF groups are not supported") if reference
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @rbs (Frontend::AST::item item) -> Frontend::AST::SymbolReference?
|
|
136
|
+
def named_reference_in(item)
|
|
137
|
+
# @type self: Normalizer
|
|
138
|
+
return item if item.is_a?(Frontend::AST::SymbolReference) && item.named_reference
|
|
139
|
+
if item.is_a?(Frontend::AST::Group)
|
|
140
|
+
return item.alternatives.flatten.filter_map { |child| named_reference_in(child) }.first
|
|
141
|
+
end
|
|
142
|
+
if item.is_a?(Frontend::AST::Optional) || item.is_a?(Frontend::AST::Star) ||
|
|
143
|
+
item.is_a?(Frontend::AST::Plus) || item.is_a?(Frontend::AST::SeparatedList)
|
|
144
|
+
return named_reference_in(item.item)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# @rbs (String kind, Frontend::Location location) -> String
|
|
151
|
+
def new_helper(kind, location)
|
|
152
|
+
# @type self: Normalizer
|
|
153
|
+
@helper_sequence += 1
|
|
154
|
+
name = "$#{kind}_#{@helper_sequence}"
|
|
155
|
+
intern(name, :nonterminal, location: location.to_h)
|
|
156
|
+
name
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# @rbs (String expression, Frontend::Location location) -> IR::Action
|
|
160
|
+
def synthetic_action(expression, location)
|
|
161
|
+
# @type self: Normalizer
|
|
162
|
+
code = @options[:result_var] ? " result = #{expression} " : " #{expression} "
|
|
163
|
+
IR::Action.new(code: code, location: location.to_h)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @rbs (Symbol kind, Frontend::AST::item item) -> Hash[Symbol, untyped]
|
|
167
|
+
def synthetic_origin(kind, item)
|
|
168
|
+
# @type self: Normalizer
|
|
169
|
+
{ kind: :"#{kind}_expansion", expression: NormalizeExpression.render(item), loc: item.loc.to_h }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# @rbs (Frontend::AST::InlineAction? action, Array[IR::named_ref] named_refs) -> IR::Action?
|
|
173
|
+
def normalize_action(action, named_refs)
|
|
174
|
+
# @type self: Normalizer
|
|
175
|
+
return nil unless action
|
|
176
|
+
|
|
177
|
+
IR::Action.new(code: action.code, location: action.loc.to_h, named_refs: named_refs)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @rbs (Frontend::AST::item item, Array[IR::named_ref] refs, Integer index) -> void
|
|
181
|
+
def add_named_reference(item, refs, index)
|
|
182
|
+
# @type self: Normalizer
|
|
183
|
+
reference = unwrap_reference(item)
|
|
184
|
+
return unless reference
|
|
185
|
+
|
|
186
|
+
name = reference.named_reference
|
|
187
|
+
return unless name
|
|
188
|
+
|
|
189
|
+
fail_at(reference.loc, "reserved named reference #{name}") if Normalizer::RESERVED_NAMES.include?(name)
|
|
190
|
+
fail_at(reference.loc, "duplicate named reference #{name}") if refs.any? { |entry| entry[:name] == name }
|
|
191
|
+
refs << { name: name, index: index }
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# @rbs (Frontend::AST::item item) -> Frontend::AST::SymbolReference?
|
|
195
|
+
def unwrap_reference(item)
|
|
196
|
+
# @type self: Normalizer
|
|
197
|
+
return item if item.is_a?(Frontend::AST::SymbolReference)
|
|
198
|
+
if item.is_a?(Frontend::AST::Optional) || item.is_a?(Frontend::AST::Star) ||
|
|
199
|
+
item.is_a?(Frontend::AST::Plus) || item.is_a?(Frontend::AST::SeparatedList)
|
|
200
|
+
return unwrap_reference(item.item)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# @rbs (String lhs_name, Array[String] rhs_names, IR::Action? action, String? precedence_name,
|
|
207
|
+
# Hash[Symbol, untyped] origin) -> void
|
|
208
|
+
def add_production(lhs_name, rhs_names, action, precedence_name, origin)
|
|
209
|
+
# @type self: Normalizer
|
|
210
|
+
lhs = symbol(lhs_name) || intern(lhs_name, :nonterminal, location: origin[:loc])
|
|
211
|
+
rhs = rhs_names.map { |name| symbol(name)&.id || fail_hash(origin[:loc], "undefined symbol #{name}") }
|
|
212
|
+
precedence = precedence_name && symbol(precedence_name)
|
|
213
|
+
fail_hash(origin[:loc], "undefined precedence symbol #{precedence_name}") if precedence_name && !precedence
|
|
214
|
+
@productions << IR::Production.new(id: @productions.length, lhs: lhs.id, rhs: rhs, action: action,
|
|
215
|
+
precedence_override: precedence&.id, origin: origin)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ibex
|
|
4
|
+
# Deterministically renders frontend EBNF items for production origin metadata.
|
|
5
|
+
module NormalizeExpression
|
|
6
|
+
# @rbs!
|
|
7
|
+
# private def render_reference: (Frontend::AST::SymbolReference item) -> String
|
|
8
|
+
# private def self.render_reference: (Frontend::AST::SymbolReference item) -> String
|
|
9
|
+
# private def render_group: (Frontend::AST::Group item) -> String
|
|
10
|
+
# private def self.render_group: (Frontend::AST::Group item) -> String
|
|
11
|
+
# private def render_separated_list: (Frontend::AST::SeparatedList item) -> String
|
|
12
|
+
# private def self.render_separated_list: (Frontend::AST::SeparatedList item) -> String
|
|
13
|
+
|
|
14
|
+
# @rbs (Frontend::AST::item item) -> String
|
|
15
|
+
def render(item)
|
|
16
|
+
case item
|
|
17
|
+
when Frontend::AST::SymbolReference then render_reference(item)
|
|
18
|
+
when Frontend::AST::Group then render_group(item)
|
|
19
|
+
when Frontend::AST::Optional then "#{render(item.item)}?"
|
|
20
|
+
when Frontend::AST::Star then "#{render(item.item)}*"
|
|
21
|
+
when Frontend::AST::Plus then "#{render(item.item)}+"
|
|
22
|
+
when Frontend::AST::SeparatedList then render_separated_list(item)
|
|
23
|
+
else raise Ibex::Error, "#{item.loc}: cannot render unsupported EBNF expression"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
module_function :render
|
|
27
|
+
|
|
28
|
+
# @rbs skip
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# @rbs skip
|
|
32
|
+
def render_reference(item)
|
|
33
|
+
suffix = item.named_reference ? ":#{item.named_reference}" : ""
|
|
34
|
+
"#{item.name}#{suffix}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @rbs skip
|
|
38
|
+
def render_group(item)
|
|
39
|
+
alternatives = item.alternatives.map { |items| items.map { |child| render(child) }.join(" ") }
|
|
40
|
+
"(#{alternatives.join(' | ')})"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @rbs skip
|
|
44
|
+
def render_separated_list(item)
|
|
45
|
+
name = item.nonempty ? "separated_nonempty_list" : "separated_list"
|
|
46
|
+
"#{name}(#{render(item.item)}, #{render(item.separator)})"
|
|
47
|
+
end
|
|
48
|
+
module_function :render_reference, :render_group, :render_separated_list
|
|
49
|
+
|
|
50
|
+
class << self
|
|
51
|
+
private :render_reference, :render_group, :render_separated_list
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "normalize/declarations"
|
|
4
|
+
require_relative "normalize/expression"
|
|
5
|
+
require_relative "normalize/expander"
|
|
6
|
+
require_relative "normalize/diagnostics"
|
|
7
|
+
|
|
8
|
+
module Ibex
|
|
9
|
+
# Converts a frontend AST into immutable Grammar IR.
|
|
10
|
+
class Normalizer
|
|
11
|
+
include NormalizeDeclarations
|
|
12
|
+
include NormalizeExpander
|
|
13
|
+
include NormalizeDiagnostics
|
|
14
|
+
|
|
15
|
+
RESERVED_NAMES = %w[result val _values].freeze #: Array[String]
|
|
16
|
+
|
|
17
|
+
# @rbs @ast: Frontend::AST::Root
|
|
18
|
+
# @rbs @mode: Symbol
|
|
19
|
+
# @rbs @symbols: Array[IR::GrammarSymbol]
|
|
20
|
+
# @rbs @symbols_by_name: Hash[String, IR::GrammarSymbol]
|
|
21
|
+
# @rbs @productions: Array[IR::Production]
|
|
22
|
+
# @rbs @warnings: Array[IR::grammar_warning]
|
|
23
|
+
# @rbs @helper_sequence: Integer
|
|
24
|
+
# @rbs @declared_tokens: Hash[String, IR::location]
|
|
25
|
+
# @rbs @precedence: Hash[String, IR::precedence]
|
|
26
|
+
# @rbs @precedence_locations: Hash[String, IR::location]
|
|
27
|
+
# @rbs @options: IR::grammar_options
|
|
28
|
+
# @rbs @expected_conflicts: Integer
|
|
29
|
+
# @rbs @conversions: Hash[String, String]
|
|
30
|
+
# @rbs @explicit_start: String?
|
|
31
|
+
# @rbs @start_name: String
|
|
32
|
+
# @rbs @start_location: Frontend::Location?
|
|
33
|
+
|
|
34
|
+
# @rbs (Frontend::AST::Root ast, ?mode: Symbol | String) -> void
|
|
35
|
+
def initialize(ast, mode: :racc)
|
|
36
|
+
@ast = ast
|
|
37
|
+
@mode = mode
|
|
38
|
+
@symbols = [] #: Array[IR::GrammarSymbol]
|
|
39
|
+
@symbols_by_name = {} #: Hash[String, IR::GrammarSymbol]
|
|
40
|
+
@productions = [] #: Array[IR::Production]
|
|
41
|
+
@warnings = [] #: Array[IR::grammar_warning]
|
|
42
|
+
@helper_sequence = 0
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @rbs () -> IR::Grammar
|
|
46
|
+
def normalize
|
|
47
|
+
read_declarations
|
|
48
|
+
intern_reserved_symbols
|
|
49
|
+
intern_declared_terminals
|
|
50
|
+
intern_user_nonterminals
|
|
51
|
+
normalize_user_productions
|
|
52
|
+
validate_grammar
|
|
53
|
+
IR::Grammar.new(class_name: @ast.class_name, superclass: @ast.superclass, start: @start_name,
|
|
54
|
+
expect: @expected_conflicts, options: @options, symbols: @symbols,
|
|
55
|
+
productions: @productions, user_code: normalized_user_code,
|
|
56
|
+
conversions: @conversions, warnings: @warnings, user_code_chunks: normalized_user_code_chunks)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
# @rbs () -> void
|
|
62
|
+
def intern_reserved_symbols
|
|
63
|
+
intern("$eof", :terminal, reserved: true)
|
|
64
|
+
intern("error", :terminal, reserved: true)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# @rbs () -> void
|
|
68
|
+
def intern_declared_terminals
|
|
69
|
+
@declared_tokens.each { |name, loc| intern(name, :terminal, location: loc) }
|
|
70
|
+
@precedence.each_key { |name| intern(name, :terminal, location: @precedence_locations[name]) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @rbs () -> void
|
|
74
|
+
def intern_user_nonterminals
|
|
75
|
+
@ast.rules.each { |rule| intern(rule.lhs, :nonterminal, location: rule.loc.to_h) }
|
|
76
|
+
@start_name = @explicit_start || @ast.rules.first&.lhs
|
|
77
|
+
fail_at(@ast.loc, "grammar has no start rule") unless @start_name
|
|
78
|
+
return if symbol(@start_name)&.nonterminal?
|
|
79
|
+
|
|
80
|
+
fail_at(@start_location || @ast.loc, "undefined start symbol #{@start_name}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @rbs (String name, Symbol kind, ?reserved: bool, ?location: IR::location?) -> IR::GrammarSymbol
|
|
84
|
+
def intern(name, kind, reserved: false, location: nil)
|
|
85
|
+
existing = symbol(name)
|
|
86
|
+
if existing
|
|
87
|
+
fail_hash(location, "symbol #{name} is both terminal and nonterminal") if existing.kind != kind
|
|
88
|
+
return existing
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
precedence = @precedence[name]
|
|
92
|
+
definition = IR::GrammarSymbol.new(id: @symbols.length, name: name, kind: kind, reserved: reserved,
|
|
93
|
+
precedence: precedence, location: location)
|
|
94
|
+
@symbols << definition
|
|
95
|
+
@symbols_by_name[name] = definition
|
|
96
|
+
definition
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @rbs (String name) -> IR::GrammarSymbol?
|
|
100
|
+
def symbol(name)
|
|
101
|
+
@symbols_by_name[name]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @rbs (String name) -> IR::GrammarSymbol
|
|
105
|
+
def required_symbol(name)
|
|
106
|
+
symbol(name) || raise(Ibex::Error, "missing normalized symbol #{name}")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# @rbs (Frontend::AST::SymbolReference reference) -> IR::GrammarSymbol
|
|
110
|
+
def symbol_for_reference(reference)
|
|
111
|
+
existing = symbol(reference.name)
|
|
112
|
+
return existing if existing
|
|
113
|
+
return undefined_nonterminal(reference) if nonterminal_name?(reference.name)
|
|
114
|
+
|
|
115
|
+
warn_undeclared_terminal(reference)
|
|
116
|
+
intern(reference.name, :terminal, location: reference.loc.to_h)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @rbs (String name) -> bool
|
|
120
|
+
def nonterminal_name?(name)
|
|
121
|
+
name.match?(/\A[a-z_]/) && name != "error"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @rbs (Frontend::AST::SymbolReference reference) -> bot
|
|
125
|
+
def undefined_nonterminal(reference)
|
|
126
|
+
fail_at(reference.loc, "undefined nonterminal #{reference.name}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @rbs (Frontend::AST::SymbolReference reference) -> void
|
|
130
|
+
def warn_undeclared_terminal(reference)
|
|
131
|
+
return if @declared_tokens.empty? || reference.name.start_with?("'", '"')
|
|
132
|
+
|
|
133
|
+
@warnings << { type: :undeclared_terminal, symbol: reference.name, loc: reference.loc.to_h }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# @rbs () -> Hash[String, String]
|
|
137
|
+
def normalized_user_code
|
|
138
|
+
%w[header inner footer].to_h do |name|
|
|
139
|
+
[name, @ast.user_code.fetch(name, Array.new(0)).map(&:code).join]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @rbs () -> IR::user_code_chunks
|
|
144
|
+
def normalized_user_code_chunks
|
|
145
|
+
chunks_by_name = %w[header inner footer].to_h do |name|
|
|
146
|
+
chunks = @ast.user_code.fetch(name, Array.new(0)).map do |block|
|
|
147
|
+
location = block.loc.to_h.merge(line: block.loc.line + 1, column: 1)
|
|
148
|
+
IR::UserCodeChunk.new(code: block.code, location: location)
|
|
149
|
+
end
|
|
150
|
+
[name, chunks]
|
|
151
|
+
end
|
|
152
|
+
chunks_by_name.reject { |_name, chunks| chunks.empty? }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @rbs (Frontend::Location location, String message) -> bot
|
|
156
|
+
def fail_at(location, message)
|
|
157
|
+
raise Ibex::Error, "#{location}: #{message}"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# @rbs (IR::location? location, String message) -> bot
|
|
161
|
+
def fail_hash(location, message)
|
|
162
|
+
rendered = location ? "#{location[:file]}:#{location[:line]}:#{location[:column]}" : "(grammar):1:1"
|
|
163
|
+
raise Ibex::Error, "#{rendered}: #{message}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|