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.
Files changed (118) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +43 -0
  3. data/CHANGELOG.md +30 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +194 -0
  6. data/Rakefile +25 -0
  7. data/Steepfile +10 -0
  8. data/docs/architecture.md +99 -0
  9. data/docs/compat-notes.md +37 -0
  10. data/docs/grammar-reference.md +127 -0
  11. data/docs/lexer-coverage.md +14 -0
  12. data/docs/phase10-extensions.md +27 -0
  13. data/docs/racc-migration.md +47 -0
  14. data/exe/ibex +6 -0
  15. data/gemfiles/Gemfile +7 -0
  16. data/gemfiles/Gemfile.lock +98 -0
  17. data/lib/ibex/analysis/sets.rb +194 -0
  18. data/lib/ibex/analysis.rb +8 -0
  19. data/lib/ibex/cli/counterexample_options.rb +38 -0
  20. data/lib/ibex/cli/outputs.rb +88 -0
  21. data/lib/ibex/cli.rb +333 -0
  22. data/lib/ibex/codegen/dot.rb +52 -0
  23. data/lib/ibex/codegen/html.rb +110 -0
  24. data/lib/ibex/codegen/rbs.rb +54 -0
  25. data/lib/ibex/codegen/report.rb +149 -0
  26. data/lib/ibex/codegen/ruby.rb +235 -0
  27. data/lib/ibex/codegen/symbol_labels.rb +18 -0
  28. data/lib/ibex/error.rb +7 -0
  29. data/lib/ibex/frontend/action_scanner.rb +266 -0
  30. data/lib/ibex/frontend/ast.rb +146 -0
  31. data/lib/ibex/frontend/bootstrap_parser.rb +163 -0
  32. data/lib/ibex/frontend/dsl.rb +196 -0
  33. data/lib/ibex/frontend/generated_parser.rb +434 -0
  34. data/lib/ibex/frontend/generated_parser_base.rb +273 -0
  35. data/lib/ibex/frontend/grammar.y +156 -0
  36. data/lib/ibex/frontend/lexer.rb +165 -0
  37. data/lib/ibex/frontend/parser/declarations.rb +163 -0
  38. data/lib/ibex/frontend/parser/rules.rb +154 -0
  39. data/lib/ibex/frontend/parser.rb +23 -0
  40. data/lib/ibex/frontend/regenerator.rb +41 -0
  41. data/lib/ibex/frontend/source_cursor.rb +94 -0
  42. data/lib/ibex/frontend/token_adapter/declaration_state.rb +248 -0
  43. data/lib/ibex/frontend/token_adapter/delimiter_tracker.rb +37 -0
  44. data/lib/ibex/frontend/token_adapter/rule_state.rb +162 -0
  45. data/lib/ibex/frontend/token_adapter.rb +108 -0
  46. data/lib/ibex/frontend.rb +28 -0
  47. data/lib/ibex/ir/automaton_ir.rb +109 -0
  48. data/lib/ibex/ir/grammar_ir.rb +192 -0
  49. data/lib/ibex/ir/serialize.rb +154 -0
  50. data/lib/ibex/ir.rb +53 -0
  51. data/lib/ibex/lalr/builder.rb +270 -0
  52. data/lib/ibex/lalr/conflict.rb +120 -0
  53. data/lib/ibex/lalr/conflict_search.rb +332 -0
  54. data/lib/ibex/lalr/conflict_search_limits.rb +39 -0
  55. data/lib/ibex/lalr/counterexample.rb +138 -0
  56. data/lib/ibex/lalr/default_reductions.rb +67 -0
  57. data/lib/ibex/lalr.rb +27 -0
  58. data/lib/ibex/normalize/declarations.rb +60 -0
  59. data/lib/ibex/normalize/diagnostics.rb +99 -0
  60. data/lib/ibex/normalize/expander.rb +218 -0
  61. data/lib/ibex/normalize/expression.rb +54 -0
  62. data/lib/ibex/normalize.rb +166 -0
  63. data/lib/ibex/runtime/parser.rb +360 -0
  64. data/lib/ibex/runtime.rb +8 -0
  65. data/lib/ibex/tables.rb +125 -0
  66. data/lib/ibex/version.rb +6 -0
  67. data/lib/ibex.rb +23 -0
  68. data/sig/ibex/analysis/sets.rbs +72 -0
  69. data/sig/ibex/analysis.rbs +6 -0
  70. data/sig/ibex/cli/counterexample_options.rbs +16 -0
  71. data/sig/ibex/cli/outputs.rbs +31 -0
  72. data/sig/ibex/cli.rbs +108 -0
  73. data/sig/ibex/codegen/dot.rbs +19 -0
  74. data/sig/ibex/codegen/html.rbs +35 -0
  75. data/sig/ibex/codegen/rbs.rbs +31 -0
  76. data/sig/ibex/codegen/report.rbs +39 -0
  77. data/sig/ibex/codegen/ruby.rbs +92 -0
  78. data/sig/ibex/codegen/symbol_labels.rbs +11 -0
  79. data/sig/ibex/error.rbs +7 -0
  80. data/sig/ibex/frontend/action_scanner.rbs +74 -0
  81. data/sig/ibex/frontend/ast.rbs +223 -0
  82. data/sig/ibex/frontend/bootstrap_parser.rbs +80 -0
  83. data/sig/ibex/frontend/dsl.rbs +124 -0
  84. data/sig/ibex/frontend/generated_parser.rbs +163 -0
  85. data/sig/ibex/frontend/generated_parser_base.rbs +114 -0
  86. data/sig/ibex/frontend/lexer.rbs +61 -0
  87. data/sig/ibex/frontend/parser/declarations.rbs +56 -0
  88. data/sig/ibex/frontend/parser/rules.rbs +48 -0
  89. data/sig/ibex/frontend/parser.rbs +16 -0
  90. data/sig/ibex/frontend/regenerator.rbs +16 -0
  91. data/sig/ibex/frontend/source_cursor.rbs +73 -0
  92. data/sig/ibex/frontend/token_adapter/declaration_state.rbs +97 -0
  93. data/sig/ibex/frontend/token_adapter/delimiter_tracker.rbs +25 -0
  94. data/sig/ibex/frontend/token_adapter/rule_state.rbs +77 -0
  95. data/sig/ibex/frontend/token_adapter.rbs +67 -0
  96. data/sig/ibex/frontend.rbs +15 -0
  97. data/sig/ibex/ir/automaton_ir.rbs +77 -0
  98. data/sig/ibex/ir/grammar_ir.rbs +143 -0
  99. data/sig/ibex/ir/serialize.rbs +50 -0
  100. data/sig/ibex/ir.rbs +43 -0
  101. data/sig/ibex/lalr/builder.rbs +85 -0
  102. data/sig/ibex/lalr/conflict.rbs +47 -0
  103. data/sig/ibex/lalr/conflict_search.rbs +131 -0
  104. data/sig/ibex/lalr/conflict_search_limits.rbs +29 -0
  105. data/sig/ibex/lalr/counterexample.rbs +53 -0
  106. data/sig/ibex/lalr/default_reductions.rbs +20 -0
  107. data/sig/ibex/lalr.rbs +23 -0
  108. data/sig/ibex/normalize/declarations.rbs +20 -0
  109. data/sig/ibex/normalize/diagnostics.rbs +32 -0
  110. data/sig/ibex/normalize/expander.rbs +66 -0
  111. data/sig/ibex/normalize/expression.rbs +21 -0
  112. data/sig/ibex/normalize.rbs +96 -0
  113. data/sig/ibex/runtime/parser.rbs +167 -0
  114. data/sig/ibex/runtime.rbs +6 -0
  115. data/sig/ibex/tables.rbs +50 -0
  116. data/sig/ibex/version.rbs +6 -0
  117. data/sig/ibex.rbs +6 -0
  118. 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