Class: Ibex::Frontend::Parser

Inherits:
Object
  • Object
show all
Defined in:
lib/ibex/frontend/parser.rb,
sig/ibex/frontend/parser.rbs

Overview

Public grammar parser backed by Ibex's generated LR frontend.

Constant Summary collapse

DEFAULT_MAX_DIAGNOSTICS =

Signature:

  • Integer

Returns:

  • (Integer)
20
STRICT_LEXICAL_DIAGNOSTIC_LIMIT =

Signature:

  • Integer

Returns:

  • (Integer)
1

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(source, file: "(grammar)", mode: :default) ⇒ Parser

Returns a new instance of Parser.

RBS:

  • (String | Array[Token] source, ?file: String, ?mode: Symbol) -> void

Parameters:

  • source (String, Array[Token])
  • file: (String) (defaults to: "(grammar)")
  • mode: (Symbol) (defaults to: :default)


23
24
25
26
27
28
29
30
31
32
33
# File 'lib/ibex/frontend/parser.rb', line 23

def initialize(source, file: "(grammar)", mode: :default)
  raise ArgumentError, "mode must be :default or :extended" unless %i[default extended].include?(mode)

  @mode = mode
  @lexical_diagnostics = [] #: Array[Diagnostic]
  @parse_error_message = nil
  @source_document_error_message = nil
  tokens = source.is_a?(Array) ? source : tokenize_source(source, file)
  @tokens = tokens
  @implementation = GeneratedParser.new(tokens, mode: mode)
end

Instance Attribute Details

#implementationGeneratedParser (readonly)

Signature:

  • GeneratedParser

Returns:



11
12
13
# File 'lib/ibex/frontend/parser.rb', line 11

def implementation
  @implementation
end

Instance Method Details

#diagnostic_key(diagnostic) ⇒ [ String, String, Integer, Integer, String ]

RBS:

  • (Diagnostic diagnostic) -> [String, String, Integer, Integer, String]

Parameters:

Returns:

  • ([ String, String, Integer, Integer, String ])


189
190
191
192
# File 'lib/ibex/frontend/parser.rb', line 189

def diagnostic_key(diagnostic)
  location = diagnostic.location
  [diagnostic.phase.to_s, location.file, location.line, location.column, diagnostic.message]
end

#diagnostic_sort_key(diagnostic) ⇒ [ String, Integer, Integer, String, String ]

RBS:

  • (Diagnostic diagnostic) -> [String, Integer, Integer, String, String]

Parameters:

Returns:

  • ([ String, Integer, Integer, String, String ])


195
196
197
198
# File 'lib/ibex/frontend/parser.rb', line 195

def diagnostic_sort_key(diagnostic)
  location = diagnostic.location
  [location.file, location.line, location.column, diagnostic.phase.to_s, diagnostic.code]
end

#enrich_rule_documentation(node) ⇒ AST::Root, AST::Fragment

RBS:

  • (AST::Root | AST::Fragment node) -> (AST::Root | AST::Fragment)

Parameters:

Returns:



130
131
132
133
134
135
# File 'lib/ibex/frontend/parser.rb', line 130

def enrich_rule_documentation(node)
  source_document = @source_document
  return node unless source_document

  RuleDocumentation.enrich(node, source_document)
end

#lexical_diagnostics_for(max_diagnostics) ⇒ Array[Diagnostic]

RBS:

  • (Integer max_diagnostics) -> Array[Diagnostic]

Parameters:

  • max_diagnostics (Integer)

Returns:



168
169
170
171
172
173
174
175
176
177
# File 'lib/ibex/frontend/parser.rb', line 168

def lexical_diagnostics_for(max_diagnostics)
  return @lexical_diagnostics if @lexical_diagnostics.empty? || max_diagnostics == 1

  source_document = @source_document
  return @lexical_diagnostics.first(max_diagnostics) unless source_document

  _, diagnostics = Lexer.new(source_document.source, file: source_document.file)
                        .tokenize_document_recovering(max_diagnostics: max_diagnostics)
  diagnostics
end

#merge_diagnostics(lexical, syntax, limit) ⇒ Array[Diagnostic]

RBS:

  • (Array[Diagnostic] lexical, Array[Diagnostic] syntax, Integer limit) -> Array[Diagnostic]

Parameters:

Returns:



180
181
182
183
184
185
186
# File 'lib/ibex/frontend/parser.rb', line 180

def merge_diagnostics(lexical, syntax, limit)
  (lexical + syntax)
    .uniq { |diagnostic| diagnostic_key(diagnostic) }
    .sort_by { |diagnostic| diagnostic_sort_key(diagnostic) }
    .first(limit)
    .freeze
end

#parseAST::Root

RBS:

  • () -> AST::Root

Returns:



36
37
38
39
40
41
# File 'lib/ibex/frontend/parser.rb', line 36

def parse
  node = parse_node
  return node if node.is_a?(AST::Root)

  raise Ibex::Error, "#{node.loc}: fragment input requires Parser#parse_fragment"
end

#parse_documentSourceDocument

Parse the grammar and return its lossless source model.

RBS:

  • () -> SourceDocument

Returns:



54
55
56
57
58
59
# File 'lib/ibex/frontend/parser.rb', line 54

def parse_document
  parsed_document = @parsed_document
  return parsed_document if parsed_document && parsed_document.ast.is_a?(AST::Root)

  @parsed_document = source_document!.with_ast(parse)
end

#parse_fragmentAST::Fragment

Parse an explicit fragment without resolving its includes.

RBS:

  • () -> AST::Fragment

Returns:



45
46
47
48
49
50
# File 'lib/ibex/frontend/parser.rb', line 45

def parse_fragment
  node = parse_node
  return node if node.is_a?(AST::Fragment)

  raise Ibex::Error, "#{node.loc}: root grammar input cannot be parsed as a fragment"
end

#parse_nodeAST::Root, AST::Fragment

RBS:

  • () -> (AST::Root | AST::Fragment)

Returns:



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/ibex/frontend/parser.rb', line 111

def parse_node
  lexical = @lexical_diagnostics.first
  raise Ibex::Error, lexical.to_s if lexical

  parse_error_message = @parse_error_message
  raise Ibex::Error, parse_error_message if parse_error_message

  node = @parsed_node
  return node if node

  begin
    @parsed_node = enrich_rule_documentation(@implementation.parse)
  rescue Ibex::Error => e
    @parse_error_message = e.message.dup.freeze
    raise
  end
end

#parse_source_documentSourceDocument

Parse either a root grammar or an explicit fragment and return its lossless source model.

RBS:

  • () -> SourceDocument

Returns:



63
64
65
66
67
68
# File 'lib/ibex/frontend/parser.rb', line 63

def parse_source_document
  parsed_document = @parsed_document
  return parsed_document if parsed_document

  @parsed_document = source_document!.with_ast(parse_node)
end

#parse_with_diagnostics(max_diagnostics: DEFAULT_MAX_DIAGNOSTICS) ⇒ ParseResult

Parse with conservative boundary recovery and collect multiple errors.

RBS:

  • (?max_diagnostics: Integer) -> ParseResult

Parameters:

  • max_diagnostics: (Integer) (defaults to: DEFAULT_MAX_DIAGNOSTICS)

Returns:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/ibex/frontend/parser.rb', line 72

def parse_with_diagnostics(max_diagnostics: DEFAULT_MAX_DIAGNOSTICS)
  unless max_diagnostics.is_a?(Integer) && max_diagnostics.positive?
    raise ArgumentError, "max_diagnostics must be a positive integer"
  end

  lexical_diagnostics = lexical_diagnostics_for(max_diagnostics)
  if @source_document.nil? && @lexical_diagnostics.any?
    return ParseResult.new(diagnostics: lexical_diagnostics, ast: nil, document: nil)
  end

  recovery = DiagnosticRecovery.new(
    @tokens, mode: @mode, max_diagnostics: max_diagnostics
  )
  node, syntax_diagnostics = recovery.parse
  node = enrich_rule_documentation(node) if node
  ast, syntax_diagnostics = root_diagnostic_result(node, syntax_diagnostics)
  diagnostics = merge_diagnostics(lexical_diagnostics, syntax_diagnostics, max_diagnostics)
  document = if diagnostics.empty? && ast
               @source_document&.with_ast(ast)
             else
               @source_document
             end
  ParseResult.new(diagnostics: diagnostics, ast: ast, document: document)
end

#root_diagnostic_result(node, diagnostics) ⇒ [ AST::Root?, Array[Diagnostic] ]

RBS:

  • (AST::Root | AST::Fragment | nil node, Array[Diagnostic] diagnostics) -> [AST::Root?, Array[Diagnostic]]

Parameters:

Returns:



139
140
141
142
143
144
145
146
147
# File 'lib/ibex/frontend/parser.rb', line 139

def root_diagnostic_result(node, diagnostics)
  return [node, diagnostics] unless node.is_a?(AST::Fragment)

  diagnostic = Diagnostic.new(
    code: "frontend.syntax_error", phase: :syntax,
    message: "fragment input requires Parser#parse_fragment", location: node.loc
  )
  [nil, diagnostics + [diagnostic]]
end

#source_document!SourceDocument

RBS:

  • () -> SourceDocument

Returns:



100
101
102
103
104
105
106
107
108
# File 'lib/ibex/frontend/parser.rb', line 100

def source_document!
  source_document_error_message = @source_document_error_message
  raise Ibex::Error, source_document_error_message if source_document_error_message

  source_document = @source_document
  raise ArgumentError, "source document parsing requires String source" unless source_document

  source_document
end

#tokenize_source(source, file) ⇒ Array[Token]

RBS:

  • (String source, String file) -> Array[Token]

Parameters:

  • source (String)
  • file (String)

Returns:



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/ibex/frontend/parser.rb', line 150

def tokenize_source(source, file)
  document, diagnostics = Lexer.new(source, file: file).tokenize_document_recovering(
    max_diagnostics: STRICT_LEXICAL_DIAGNOSTIC_LIMIT
  )
  @source_document = document
  @lexical_diagnostics = diagnostics
  document.tokens
rescue Ibex::Error => e
  location = Location.new(file: file, line: 1, column: 1)
  @lexical_diagnostics = [
    Diagnostic.new(code: "frontend.lexical_error", phase: :lexical, message: e.message,
                   location: location, rendered: e.message)
  ]
  @source_document_error_message = e.message.dup.freeze
  [Token.new(type: :eof, value: nil, location: location, span: nil)]
end