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,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibex
4
+ module Frontend
5
+ # Parses the declaration section of a grammar.
6
+ module BootstrapParserDeclarations
7
+ DECLARATIONS = %w[token prechigh preclow options expect start convert pragma rule].freeze #: Array[String]
8
+ ASSOCIATIVITIES = %w[left right nonassoc].freeze #: Array[String]
9
+
10
+ private
11
+
12
+ # @rbs () -> Array[AST::declaration]
13
+ def parse_declarations
14
+ # @type self: BootstrapParser
15
+ declarations = [] #: Array[AST::declaration]
16
+ declarations << parse_declaration until keyword?("rule") || current.type == :eof
17
+ declarations
18
+ end
19
+
20
+ # @rbs () -> void
21
+ def parse_pragmas
22
+ # @type self: BootstrapParser
23
+ seen = false
24
+ while keyword?("pragma")
25
+ keyword = current
26
+ fail_at(keyword.location, "duplicate pragma extended") if seen
27
+
28
+ advance
29
+ value = expect(:identifier)
30
+ name = token_string(value)
31
+ fail_at(value.location, "unknown pragma #{name}") unless name == "extended"
32
+
33
+ @mode = :extended
34
+ seen = true
35
+ end
36
+ end
37
+
38
+ # @rbs () -> AST::declaration
39
+ def parse_declaration
40
+ # @type self: BootstrapParser
41
+ case current.value
42
+ when "token" then parse_tokens
43
+ when "prechigh", "preclow" then parse_precedence
44
+ when "options" then parse_options
45
+ when "expect" then parse_expect
46
+ when "start" then parse_start
47
+ when "convert" then parse_convert
48
+ when "pragma" then fail_at(current.location, "expected rule, got pragma")
49
+ else fail_expected("a declaration or rule")
50
+ end
51
+ end
52
+
53
+ # @rbs () -> AST::Tokens
54
+ def parse_tokens
55
+ # @type self: BootstrapParser
56
+ location = advance.location
57
+ names = [] #: Array[String]
58
+ names << parse_symbol_name until declaration_start?
59
+ AST::Tokens.new(names: names, loc: location)
60
+ end
61
+
62
+ # @rbs () -> AST::Precedence
63
+ def parse_precedence
64
+ # @type self: BootstrapParser
65
+ opening = advance
66
+ opening_name = token_string(opening)
67
+ closing = opening_name == "prechigh" ? "preclow" : "prechigh"
68
+ levels = precedence_levels(closing)
69
+ expect_keyword(closing)
70
+ AST::Precedence.new(direction: opening_name == "prechigh" ? :high_to_low : :low_to_high,
71
+ levels: levels, loc: opening.location)
72
+ end
73
+
74
+ # @rbs (String closing) -> Array[AST::PrecedenceLevel]
75
+ def precedence_levels(closing)
76
+ # @type self: BootstrapParser
77
+ levels = [] #: Array[AST::PrecedenceLevel]
78
+ until keyword?(closing) || current.type == :eof
79
+ association = expect_one_of(ASSOCIATIVITIES)
80
+ symbols = [] #: Array[String]
81
+ symbols << parse_symbol_name until association_start? || keyword?(closing) || current.type == :eof
82
+ fail_at(association.location, "expected at least one precedence symbol") if symbols.empty?
83
+ levels << AST::PrecedenceLevel.new(associativity: token_string(association).to_sym, symbols: symbols,
84
+ loc: association.location)
85
+ end
86
+ levels
87
+ end
88
+
89
+ # @rbs () -> AST::Options
90
+ def parse_options
91
+ # @type self: BootstrapParser
92
+ location = advance.location
93
+ names = [] #: Array[String]
94
+ names << token_string(expect(:identifier)) until declaration_start?
95
+ AST::Options.new(names: names, loc: location)
96
+ end
97
+
98
+ # @rbs () -> AST::Expect
99
+ def parse_expect
100
+ # @type self: BootstrapParser
101
+ location = advance.location
102
+ AST::Expect.new(conflicts: token_integer(expect(:integer)), loc: location)
103
+ end
104
+
105
+ # @rbs () -> AST::Start
106
+ def parse_start
107
+ # @type self: BootstrapParser
108
+ location = advance.location
109
+ AST::Start.new(name: parse_symbol_name, loc: location)
110
+ end
111
+
112
+ # @rbs () -> AST::Convert
113
+ def parse_convert
114
+ # @type self: BootstrapParser
115
+ location = advance.location
116
+ pairs = [] #: Array[AST::Conversion]
117
+ until keyword?("end") || current.type == :eof
118
+ name_token = current
119
+ name = parse_symbol_name
120
+ expression = decode_conversion(tokens_on_line(name_token.location.line), name_token.location)
121
+ pairs << AST::Conversion.new(name: name, expression: expression, loc: name_token.location)
122
+ end
123
+ expect_keyword("end")
124
+ AST::Convert.new(pairs: pairs, loc: location)
125
+ end
126
+
127
+ # @rbs (Integer line) -> Array[Token]
128
+ def tokens_on_line(line)
129
+ # @type self: BootstrapParser
130
+ tokens = [] #: Array[Token]
131
+ tokens << advance while current.type != :eof && current.location.line == line && !keyword?("end")
132
+ tokens
133
+ end
134
+
135
+ # @rbs (Array[Token] tokens, Location location) -> String
136
+ def decode_conversion(tokens, location)
137
+ # @type self: BootstrapParser
138
+ unless tokens.length == 1 && tokens.first.type == :literal
139
+ fail_at(location, "expected a quoted Ruby conversion expression")
140
+ end
141
+
142
+ literal = token_string(tokens.first)
143
+ return literal.undump if literal.start_with?('"')
144
+
145
+ (literal[1...-1] || "").gsub("\\'", "'").gsub("\\\\", "\\")
146
+ rescue RuntimeError => e
147
+ fail_at(location, "invalid conversion expression: #{e.message}")
148
+ end
149
+
150
+ # @rbs () -> bool
151
+ def declaration_start?
152
+ # @type self: BootstrapParser
153
+ current.type == :eof || (current.type == :identifier && DECLARATIONS.include?(current.value))
154
+ end
155
+
156
+ # @rbs () -> bool
157
+ def association_start?
158
+ # @type self: BootstrapParser
159
+ current.type == :identifier && ASSOCIATIVITIES.include?(current.value)
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibex
4
+ module Frontend
5
+ # Parses productions and extended RHS items.
6
+ module BootstrapParserRules
7
+ EXTENDED_SUFFIXES = {
8
+ "?": AST::Optional, "*": AST::Star, "+": AST::Plus
9
+ }.freeze #: Hash[Symbol, singleton(AST::Optional) | singleton(AST::Star) | singleton(AST::Plus)]
10
+
11
+ private
12
+
13
+ # @rbs () -> Array[AST::Rule]
14
+ def parse_rules
15
+ # @type self: BootstrapParser
16
+ rules = [] #: Array[AST::Rule]
17
+ rules << parse_rule until keyword?("end") || current.type == :eof
18
+ fail_expected("at least one rule") if rules.empty?
19
+ rules
20
+ end
21
+
22
+ # @rbs () -> AST::Rule
23
+ def parse_rule
24
+ # @type self: BootstrapParser
25
+ lhs = expect(:identifier)
26
+ expect(:":")
27
+ alternatives = [] #: Array[AST::Alternative]
28
+ loop do
29
+ alternatives << parse_alternative(lhs)
30
+ next if accept(:|)
31
+
32
+ accept(:";")
33
+ break
34
+ end
35
+ AST::Rule.new(lhs: token_string(lhs), alternatives: alternatives, loc: lhs.location)
36
+ end
37
+
38
+ # @rbs (Token lhs) -> AST::Alternative
39
+ def parse_alternative(lhs)
40
+ # @type self: BootstrapParser
41
+ location = current.location
42
+ items = [] #: Array[AST::item]
43
+ precedence = nil #: String?
44
+ until alternative_end?(lhs)
45
+ if accept(:"=")
46
+ precedence = parse_symbol_name
47
+ break
48
+ end
49
+ items << parse_item
50
+ end
51
+ action = nil #: AST::InlineAction?
52
+ last_item = items.last
53
+ if last_item.is_a?(AST::InlineAction)
54
+ items.pop
55
+ action = last_item
56
+ end
57
+ AST::Alternative.new(items: items, action: action, precedence: precedence, loc: location)
58
+ end
59
+
60
+ # @rbs () -> AST::item
61
+ def parse_item
62
+ # @type self: BootstrapParser
63
+ return parse_action if current.type == :action
64
+ return parse_separated_list if separated_list?
65
+ return parse_group if current.type == :"("
66
+
67
+ token = expect_symbol
68
+ named_reference = parse_named_reference
69
+ item = AST::SymbolReference.new(name: token_string(token), named_reference: named_reference,
70
+ loc: token.location)
71
+ parse_suffix(item)
72
+ end
73
+
74
+ # @rbs () -> AST::InlineAction
75
+ def parse_action
76
+ # @type self: BootstrapParser
77
+ token = advance
78
+ AST::InlineAction.new(code: token_string(token), loc: token.location)
79
+ end
80
+
81
+ # @rbs () -> String?
82
+ def parse_named_reference
83
+ # @type self: BootstrapParser
84
+ return nil unless current.type == :":"
85
+
86
+ extended_only!(current.location, "named references")
87
+ advance
88
+ token_string(expect(:identifier))
89
+ end
90
+
91
+ # @rbs (AST::item item) -> AST::item
92
+ def parse_suffix(item)
93
+ # @type self: BootstrapParser
94
+ while (wrapper = EXTENDED_SUFFIXES[current.type])
95
+ extended_only!(current.location, "EBNF suffixes")
96
+ item = wrapper.new(item: item, loc: advance.location)
97
+ end
98
+ item
99
+ end
100
+
101
+ # @rbs () -> AST::item
102
+ def parse_group
103
+ # @type self: BootstrapParser
104
+ opening = advance
105
+ extended_only!(opening.location, "EBNF groups")
106
+ alternatives = [[]] #: Array[Array[AST::item]]
107
+ until current.type == :")"
108
+ fail_at(opening.location, "unterminated EBNF group") if current.type == :eof || keyword?("end")
109
+ if accept(:|)
110
+ alternatives << []
111
+ next
112
+ end
113
+ fail_at(current.location, "actions inside EBNF groups are not supported") if current.type == :action
114
+
115
+ alternatives.last << parse_item
116
+ end
117
+ expect(:")")
118
+ parse_suffix(AST::Group.new(alternatives: alternatives, loc: opening.location))
119
+ end
120
+
121
+ # @rbs () -> AST::SeparatedList
122
+ def parse_separated_list
123
+ # @type self: BootstrapParser
124
+ function = advance
125
+ extended_only!(function.location, "separated lists")
126
+ expect(:"(")
127
+ item = parse_item
128
+ expect(:",")
129
+ separator = parse_item
130
+ expect(:")")
131
+ AST::SeparatedList.new(item: item, separator: separator,
132
+ nonempty: function.value == "separated_nonempty_list", loc: function.location)
133
+ end
134
+
135
+ # @rbs (Token lhs) -> bool
136
+ def alternative_end?(lhs)
137
+ # @type self: BootstrapParser
138
+ %i[| ; eof].include?(current.type) || keyword?("end") || rule_start?(lhs)
139
+ end
140
+
141
+ # @rbs (Token lhs) -> bool
142
+ def rule_start?(lhs)
143
+ # @type self: BootstrapParser
144
+ current.type == :identifier && lookahead.type == :":" && current.location.column <= lhs.location.column
145
+ end
146
+
147
+ # @rbs () -> bool
148
+ def separated_list?
149
+ # @type self: BootstrapParser
150
+ %w[separated_list separated_nonempty_list].include?(current.value) && lookahead.type == :"("
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibex
4
+ module Frontend
5
+ # Public grammar parser backed by Ibex's generated LR frontend.
6
+ class Parser
7
+ attr_reader :implementation #: GeneratedParser
8
+
9
+ # @rbs (String | Array[Token] source, ?file: String, ?mode: Symbol) -> void
10
+ def initialize(source, file: "(grammar)", mode: :racc)
11
+ raise ArgumentError, "mode must be :racc or :extended" unless %i[racc extended].include?(mode)
12
+
13
+ tokens = source.is_a?(Array) ? source : Lexer.new(source, file: file).tokenize
14
+ @implementation = GeneratedParser.new(tokens, mode: mode)
15
+ end
16
+
17
+ # @rbs () -> AST::Root
18
+ def parse
19
+ @implementation.parse
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../error"
4
+ require_relative "../tables"
5
+ require_relative "source_cursor"
6
+ require_relative "action_scanner"
7
+ require_relative "lexer"
8
+ require_relative "ast"
9
+ require_relative "parser/declarations"
10
+ require_relative "parser/rules"
11
+ require_relative "bootstrap_parser"
12
+ require_relative "../ir"
13
+ require_relative "../normalize"
14
+ require_relative "../analysis"
15
+ require_relative "../lalr"
16
+ require_relative "../codegen/ruby"
17
+
18
+ module Ibex
19
+ module Frontend
20
+ # Builds the committed frontend parser from its canonical Ibex grammar.
21
+ module Regenerator
22
+ GRAMMAR_PATH = File.expand_path("grammar.y", File.dirname(__FILE__)) #: String
23
+
24
+ module_function
25
+
26
+ # @rbs () -> String
27
+ def generate
28
+ source = File.read(GRAMMAR_PATH)
29
+ ast = BootstrapParser.new(source, file: relative_grammar_path).parse
30
+ grammar = Normalizer.new(ast).normalize
31
+ automaton = LALR::Builder.new(grammar).build
32
+ Codegen::Ruby.new(automaton, table: :compact, line_convert: false).generate
33
+ end
34
+
35
+ # @rbs () -> String
36
+ def relative_grammar_path
37
+ "lib/ibex/frontend/grammar.y"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ibex
4
+ module Frontend
5
+ # A source coordinate retained by frontend and IR objects.
6
+ Location = Struct.new(
7
+ :file, #: String
8
+ :line, #: Integer
9
+ :column, #: Integer
10
+ keyword_init: true
11
+ )
12
+
13
+ class Location
14
+ # @rbs () -> IR::location
15
+ def to_h
16
+ { file: file, line: line, column: column }
17
+ end
18
+
19
+ # @rbs () -> String
20
+ def to_s
21
+ "#{file}:#{line}:#{column}"
22
+ end
23
+ end
24
+
25
+ # A grammar token and its source coordinate.
26
+ Token = Struct.new(
27
+ :type, #: Symbol
28
+ :value, #: token_value
29
+ :location, #: Location
30
+ keyword_init: true
31
+ )
32
+
33
+ class Token
34
+ # @rbs () -> Hash[Symbol, untyped]
35
+ def to_h
36
+ { type: type, value: value, location: location.to_h }
37
+ end
38
+ end
39
+
40
+ # Advances through source text while maintaining one-based coordinates.
41
+ class SourceCursor
42
+ attr_reader :source #: String
43
+ attr_reader :file #: String
44
+ attr_reader :index #: Integer
45
+ attr_reader :line #: Integer
46
+ attr_reader :column #: Integer
47
+
48
+ # @rbs (String source, String file) -> void
49
+ def initialize(source, file)
50
+ @source = source
51
+ @file = file
52
+ @index = 0
53
+ @line = 1
54
+ @column = 1
55
+ end
56
+
57
+ # @rbs () -> bool
58
+ def eof?
59
+ @index >= @source.length
60
+ end
61
+
62
+ # @rbs (?Integer offset) -> String?
63
+ def peek(offset = 0)
64
+ @source[@index + offset]
65
+ end
66
+
67
+ # @rbs () -> String
68
+ def rest
69
+ @source[@index..] || ""
70
+ end
71
+
72
+ # @rbs () -> Location
73
+ def location
74
+ Location.new(file: @file, line: @line, column: @column)
75
+ end
76
+
77
+ # @rbs (?Integer count) -> void
78
+ def advance(count = 1)
79
+ count.times do
80
+ character = @source[@index]
81
+ break unless character
82
+
83
+ @index += 1
84
+ if character == "\n"
85
+ @line += 1
86
+ @column = 1
87
+ else
88
+ @column += 1
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end