Class: Ibex::Frontend::Resolver

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

Overview

Loads an explicit root grammar and resolves its extended-mode fragment graph.

Constant Summary collapse

GLOB_CHARACTERS =

Signature:

  • Regexp

Returns:

  • (Regexp)
/[*?\[\]{}]/
WINDOWS_ABSOLUTE =

Signature:

  • Regexp

Returns:

  • (Regexp)
%r{\A(?:[A-Za-z]:[\\/]|\\\\)}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path, mode: :default, loader: SourceLoader.new) ⇒ Resolver

Returns a new instance of Resolver.

RBS:

  • (String path, ?mode: Symbol, ?loader: SourceLoader) -> void

Parameters:

  • path (String)
  • mode: (Symbol) (defaults to: :default)
  • loader: (SourceLoader) (defaults to: SourceLoader.new)


27
28
29
30
31
32
33
34
# File 'lib/ibex/frontend/resolver.rb', line 27

def initialize(path, mode: :default, loader: SourceLoader.new)
  raise ArgumentError, "mode must be :default or :extended" unless %i[default extended].include?(mode)

  @input_path = path
  @mode = mode
  @loader = loader
  @attempted_paths = [File.expand_path(path)]
end

Instance Attribute Details

#attempted_pathsArray[String] (readonly)

RBS:

  • @input_path: String

  • @mode: Symbol

  • @loader: SourceLoader

  • @root_path: String

  • @root_directory: String

  • @files: Array[String]

  • @visiting: Array[String]

  • @loaded: Hash[String, bool]

  • @include_chains: Hash[AST::Rule, Array[IR::source_provenance]]

  • @attempted_paths: Array[String]

Returns:

  • (Array[String])


24
25
26
# File 'lib/ibex/frontend/resolver.rb', line 24

def attempted_paths
  @attempted_paths
end

Instance Method Details

#canonical_include(include_node) ⇒ String

RBS:

  • (AST::Include include_node) -> String

Parameters:

Returns:

  • (String)


146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/ibex/frontend/resolver.rb', line 146

def canonical_include(include_node)
  validate_include_path(include_node)
  candidate = File.expand_path(include_node.path, File.dirname(include_node.loc.file))
  @attempted_paths << candidate unless @attempted_paths.include?(candidate)
  canonical = @loader.canonical_path(candidate)
  unless @loader.file?(canonical)
    fail_include(include_node, "include path is not a file: #{include_node.path.inspect}")
  end

  unless inside_root?(canonical)
    message = "include resolves outside the root grammar directory: #{include_node.path.inspect}"
    fail_include(include_node, message)
  end
  @loader.record_access(canonical, candidate)
  canonical
rescue Errno::ENOENT, Errno::ENOTDIR
  fail_include(include_node, "include file does not exist: #{include_node.path.inspect}")
rescue SystemCallError => e
  message = "#{include_node.loc}: cannot read include #{include_node.path.inspect}: #{e.message}"
  raise ResolutionIOError, message
end

#canonical_rootString

RBS:

  • () -> String

Returns:

  • (String)


83
84
85
86
87
88
89
90
91
# File 'lib/ibex/frontend/resolver.rb', line 83

def canonical_root
  path = @loader.canonical_path(@input_path)
  raise Ibex::Error, "#{@input_path}:1:1: root grammar must be a file" unless @loader.file?(path)

  @loader.record_access(path, @input_path)
  path
rescue SystemCallError => e
  raise ResolutionIOError, "#{@input_path}:1:1: cannot read root grammar: #{e.message}"
end

#dependenciesArray[String]

RBS:

  • () -> Array[String]

Returns:

  • (Array[String])


58
59
60
# File 'lib/ibex/frontend/resolver.rb', line 58

def dependencies
  resolve.files
end

#expand_include(include_node, chain) ⇒ [ Array[AST::declaration], Array[AST::Rule] ]

RBS:

  • (AST::Include include_node, Array[IR::source_provenance] chain) -> [Array[AST::declaration], Array[AST::Rule]]

Parameters:

  • include_node (AST::Include)
  • chain (Array[IR::source_provenance])

Returns:

  • ([ Array[AST::declaration], Array[AST::Rule] ])


130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/ibex/frontend/resolver.rb', line 130

def expand_include(include_node, chain)
  target = canonical_include(include_node)
  reject_cycle(include_node, target)
  return [[], []] if @loaded[target]

  @files << target
  @visiting << target
  fragment = parse_fragment(target)
  next_chain = chain + [source_provenance(target)]
  declarations, rules = expand_node(fragment, next_chain)
  @visiting.pop
  @loaded[target] = true
  [declarations, rules]
end

#expand_node(node, chain) ⇒ [ Array[AST::declaration], Array[AST::Rule] ]

RBS:

  • (AST::Root | AST::Fragment node, Array[IR::source_provenance] chain) -> [Array[AST::declaration], Array[AST::Rule]]

Parameters:

Returns:

  • ([ Array[AST::declaration], Array[AST::Rule] ])


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

def expand_node(node, chain)
  declarations = [] #: Array[AST::declaration]
  rules = [] #: Array[AST::Rule]
  node.declarations.each do |declaration|
    if declaration.is_a?(AST::Include)
      included_declarations, included_rules = expand_include(declaration, chain)
      declarations.concat(included_declarations)
      rules.concat(included_rules)
    else
      declarations << declaration
    end
  end
  node.rules.each do |rule|
    @include_chains[rule] = chain
    rules << rule
  end
  [declarations, rules]
end

#fail_include(include_node, message) ⇒ bot

RBS:

  • (AST::Include include_node, String message) -> bot

Parameters:

Returns:

  • (bot)


212
213
214
# File 'lib/ibex/frontend/resolver.rb', line 212

def fail_include(include_node, message)
  raise Ibex::Error, "#{include_node.loc}: #{message}"
end

#inside_root?(path) ⇒ Boolean

RBS:

  • (String path) -> bool

Parameters:

  • path (String)

Returns:

  • (Boolean)


185
186
187
188
189
190
191
192
193
194
195
# File 'lib/ibex/frontend/resolver.rb', line 185

def inside_root?(path)
  directory = File.dirname(path)
  loop do
    return true if directory == @root_directory

    parent = File.dirname(directory)
    return false if parent == directory

    directory = parent
  end
end

#parse_fragment(path) ⇒ AST::Fragment

RBS:

  • (String path) -> AST::Fragment

Parameters:

  • path (String)

Returns:



101
102
103
104
105
# File 'lib/ibex/frontend/resolver.rb', line 101

def parse_fragment(path)
  Parser.new(@loader.read(path), file: path, mode: :extended).parse_fragment
rescue SystemCallError => e
  raise ResolutionIOError, "#{path}:1:1: cannot read fragment: #{e.message}"
end

#parse_root(path) ⇒ AST::Root

RBS:

  • (String path) -> AST::Root

Parameters:

  • path (String)

Returns:



94
95
96
97
98
# File 'lib/ibex/frontend/resolver.rb', line 94

def parse_root(path)
  Parser.new(@loader.read(path), file: path, mode: @mode).parse
rescue SystemCallError => e
  raise ResolutionIOError, "#{path}:1:1: cannot read grammar: #{e.message}"
end

#prepare_resolutionvoid

This method returns an undefined value.

RBS:

  • () -> void



71
72
73
74
75
76
77
78
79
80
# File 'lib/ibex/frontend/resolver.rb', line 71

def prepare_resolution
  @root_path = canonical_root
  @root_directory = File.dirname(@root_path)
  @files = [@root_path] #: Array[String]
  @visiting = [@root_path] #: Array[String]
  @loaded = {} #: Hash[String, bool]
  chains = {} #: Hash[AST::Rule, Array[IR::source_provenance]]
  @include_chains = chains.compare_by_identity
  @attempted_paths = [File.expand_path(@input_path)]
end

#reject_cycle(include_node, target) ⇒ void

This method returns an undefined value.

RBS:

  • (AST::Include include_node, String target) -> void

Parameters:



198
199
200
201
202
203
204
# File 'lib/ibex/frontend/resolver.rb', line 198

def reject_cycle(include_node, target)
  start = @visiting.index(target)
  return unless start

  cycle = @visiting.drop(start) + [target]
  fail_include(include_node, "include cycle: #{cycle.join(' -> ')}")
end

#resolveResolution

RBS:

  • () -> Resolution

Returns:



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/ibex/frontend/resolver.rb', line 37

def resolve
  cached = @resolution
  return cached if cached

  prepare_resolution
  parsed_root = parse_root(@root_path)
  declarations, rules = expand_node(parsed_root, [])
  root = AST::Root.new(
    class_name: parsed_root.class_name, superclass: parsed_root.superclass,
    declarations: declarations, rules: rules, user_code: parsed_root.user_code, loc: parsed_root.loc,
    extended: parsed_root.extended, cst: parsed_root.cst
  )
  @loaded[@root_path] = true
  @visiting.pop
  @resolution = Resolution.new(
    root: root, root_path: @root_path, root_directory: @root_directory,
    files: @files, include_chains: @include_chains
  )
end

#source_provenance(file) ⇒ IR::source_provenance

RBS:

  • (String file) -> IR::source_provenance

Parameters:

  • file (String)

Returns:

  • (IR::source_provenance)


207
208
209
# File 'lib/ibex/frontend/resolver.rb', line 207

def source_provenance(file)
  { file: file, root: @root_directory, byte_span: nil }
end

#source_recordsArray[GenerationInput]

Source bytes actually consumed by the parser, in resolution order.

RBS:

  • () -> Array[GenerationInput]

Returns:



64
65
66
# File 'lib/ibex/frontend/resolver.rb', line 64

def source_records
  @loader.read_records
end

#validate_include_path(include_node) ⇒ void

This method returns an undefined value.

RBS:

  • (AST::Include include_node) -> void

Parameters:



169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/ibex/frontend/resolver.rb', line 169

def validate_include_path(include_node)
  path = include_node.path
  fail_include(include_node, "include path must not be empty") if path.empty?
  fail_include(include_node, "include path must not contain NUL") if path.include?("\0")
  if path.start_with?("/") || path.match?(WINDOWS_ABSOLUTE)
    fail_include(include_node, "include path must be relative")
  end
  if path.split(%r{[\\/]}).include?("..")
    fail_include(include_node, "include path must not contain parent traversal")
  end
  return unless path.match?(GLOB_CHARACTERS)

  fail_include(include_node, "include path must not contain glob metacharacters")
end