Class: Riml::AST_Rewriter

Inherits:
Object
  • Object
show all
Includes:
Constants
Defined in:
lib/riml/ast_rewriter.rb

Defined Under Namespace

Classes: CallToExplicitCall, ClassDefinitionToFunctions, DefaultParamToIfNode, DeserializeVarAssignment, ObjectInstantiationToCall, RegisterClassDependencies, RegisterDefinedClasses, RegisterImportedClasses, SplatsToCallFunctionInCallingContext, StrictEqualsComparisonOperator, TopLevelDefMethodToDef, VarEqualsComparisonOperator

Constant Summary

Constants included from Constants

Constants::BUILTIN_COMMANDS, Constants::BUILTIN_FUNCTIONS, Constants::COMPARISON_OPERATORS, Constants::COMPILED_STRING_LOCATION, Constants::DEFINE_KEYWORDS, Constants::END_KEYWORDS, Constants::IGNORECASE_CAPABLE_OPERATORS, Constants::KEYWORDS, Constants::REGISTERS, Constants::RIML_CLASS_COMMANDS, Constants::RIML_COMMANDS, Constants::RIML_END_KEYWORDS, Constants::RIML_FILE_COMMANDS, Constants::RIML_KEYWORDS, Constants::SPECIAL_VARIABLE_PREFIXES, Constants::SPLAT_LITERAL, Constants::UNKNOWN_LOCATION_INFO, Constants::VIML_COMMANDS, Constants::VIML_END_KEYWORDS, Constants::VIML_KEYWORDS

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(ast = nil, classes = nil, class_dependency_graph = nil) ⇒ AST_Rewriter

Returns a new instance of AST_Rewriter.



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/riml/ast_rewriter.rb', line 14

def initialize(ast = nil, classes = nil, class_dependency_graph = nil)
  @ast = ast
  @classes = classes
  @class_dependency_graph = class_dependency_graph
  # only the top-most rewriter has these properties and defaults
  if self.instance_of?(Riml::AST_Rewriter)
    @classes ||= ClassMap.new
    # AST_Rewriter shares options with Parser. Parser set AST_Rewriter's
    # options before call to `rewrite`.
    @options = nil
    # Keeps track of filenames with their rewritten ASTs, to prevent rewriting
    # the same AST more than once.
    @rewritten_included_and_sourced_files = {}
    # Keeps track of which filenames included/sourced which.
    # ex: { nil => ["main.riml"], "main.riml" => ["lib1.riml", "lib2.riml"],
    # "lib1.riml" => [], "lib2.riml" => [] }
    @included_and_sourced_file_refs = Hash.new { |h, k| h[k] = [] }
    @class_dependency_graph ||= ClassDependencyGraph.new
    @resolving_class_dependencies = nil
  end
end

Instance Attribute Details

#astObject

Returns the value of attribute ast.



11
12
13
# File 'lib/riml/ast_rewriter.rb', line 11

def ast
  @ast
end

#classesObject (readonly)

Returns the value of attribute classes.



12
13
14
# File 'lib/riml/ast_rewriter.rb', line 12

def classes
  @classes
end

#optionsObject

Returns the value of attribute options.



11
12
13
# File 'lib/riml/ast_rewriter.rb', line 11

def options
  @options
end

Instance Method Details

#add_SID_function!Object

:h <SID>



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# File 'lib/riml/ast_rewriter.rb', line 289

def add_SID_function!
  fchild = ast.nodes.first
  return false if DefNode === fchild && fchild.name == 'SID' && fchild.scope_modifier == 's:'
  fn = DefNode.new('!', nil, 's:', 'SID', [], nil, Nodes.new([
      IfNode.new(
        CallNode.new(nil, 'exists', [StringNode.new('s:SID_VALUE', :s)]),
        Nodes.new([
          ReturnNode.new(GetVariableNode.new('s:', 'SID_VALUE'))
        ])
      ),
      AssignNode.new(
        '=',
        GetVariableNode.new('s:', 'SID_VALUE'),
        CallNode.new(nil, 'matchstr', [
        CallNode.new(nil, 'expand', [StringNode.new('<sfile>', :s)]),
        StringNode.new('<SNR>\zs\d\+\ze_SID$', :s)
      ]
      )),
      ReturnNode.new(GetVariableNode.new('s:', 'SID_VALUE'))
    ])
  )
  fn.parent = ast.nodes
  establish_parents(fn)
  ast.nodes.unshift fn
end

#add_SID_function?(filename) ⇒ Boolean

Add SID function if this is the main file and it has defined classes, or if it included other files and any one of those other files defined classes.

Returns:

  • (Boolean)


272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/riml/ast_rewriter.rb', line 272

def add_SID_function?(filename)
  return true if ast.children.grep(ClassDefinitionNode).any?
  included_files = @included_and_sourced_file_refs[filename]
  while included_files.any?
    incs = []
    included_files.each do |included_file|
      if (ast = @rewritten_included_and_sourced_files[included_file])
        return true if ast.children.grep(ClassDefinitionNode).any?
      end
      incs.concat @included_and_sourced_file_refs[included_file]
    end
    included_files = incs
  end
  false
end

#do_establish_parents(node) ⇒ Object



79
80
81
82
83
# File 'lib/riml/ast_rewriter.rb', line 79

def do_establish_parents(node)
  node.children.each do |child|
    child.parent_node = node if Visitable === child
  end if Visitable === node
end

#do_rewrite_on_match(node) ⇒ Object



89
90
91
# File 'lib/riml/ast_rewriter.rb', line 89

def do_rewrite_on_match(node)
  replace node if match?(node)
end

#establish_parents(node) ⇒ Object Also known as: reestablish_parents



74
75
76
# File 'lib/riml/ast_rewriter.rb', line 74

def establish_parents(node)
  Walker.walk_node(node, method(:do_establish_parents))
end

#max_recursion_lvlObject

recurse until no more children



266
267
268
# File 'lib/riml/ast_rewriter.rb', line 266

def max_recursion_lvl
  -1
end

#reorder_includes_based_on_class_dependencies!Object



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/riml/ast_rewriter.rb', line 132

def reorder_includes_based_on_class_dependencies!
  global_included_filename_order = @class_dependency_graph.filename_order
  asts = [ast]
  while (ast = asts.shift)
    include_nodes =
      ast.children.grep(RimlFileCommandNode).select do |n|
        n.name == 'riml_include'
      end
    included_filenames = include_nodes.map { |n| n.arguments.map(&:value) }.flatten
    new_order_filenames = global_included_filename_order & included_filenames
    add_to_head = included_filenames - new_order_filenames
    new_order_filenames = add_to_head + new_order_filenames
    include_nodes.each do |node|
      node.arguments.each do |arg|
        if (included_file_ast = @included_ASTs_by_include_file[arg.value])
          asts << included_file_ast
        end
        if new_order_filenames.first
          arg.value = new_order_filenames.shift
        else
          # for now, just to be cautious
          raise "Internal error in AST rewriting process. Please report bug!"
        end
      end
    end
  end
end

#resolve_class_dependencies!(filename) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/riml/ast_rewriter.rb', line 98

def resolve_class_dependencies!(filename)
  if @resolving_class_dependencies.nil?
    start_resolving = @resolving_class_dependencies = true
    @included_ASTs_by_include_file = {}
  end
  old_ast = ast
  RegisterClassDependencies.new(ast, classes, @class_dependency_graph, filename).rewrite_on_match
  ast.children.grep(RimlFileCommandNode).each do |node|
    next unless node.name == 'riml_include'
    node.each_existing_file! do |file, fullpath|
      if filename && @included_and_sourced_file_refs[file].include?(filename)
        msg = "#{filename.inspect} can't include #{file.inspect}, as " \
              " #{file.inspect} already included #{filename.inspect}"
        error = IncludeFileLoop.new(msg, node)
        raise error
      elsif filename == file
        error = UserArgumentError.new("#{file.inspect} can't include itself", node)
        raise error
      end
      @included_and_sourced_file_refs[filename] << file
      riml_src = File.read(fullpath)
      Parser.new.tap { |p| p.options = @options }.parse(riml_src, self, file, true)
      @included_ASTs_by_include_file[file] = Parser.ast_cache[file]
    end
  end
ensure
  self.ast = old_ast
  if start_resolving == true
    @resolving_class_dependencies = false
    @included_and_sourced_file_refs.clear
    reorder_includes_based_on_class_dependencies!
  end
end

#resolve_class_dependencies?Boolean

Returns:

  • (Boolean)


93
94
95
96
# File 'lib/riml/ast_rewriter.rb', line 93

def resolve_class_dependencies?
  @resolving_class_dependencies = false if @options[:include_reordering] != true
  @resolving_class_dependencies != false
end

#rewrite(filename = nil, included = false) ⇒ Object



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/riml/ast_rewriter.rb', line 36

def rewrite(filename = nil, included = false)
  if filename && (rewritten_ast = Riml.rewritten_ast_cache[filename])
    return rewritten_ast
  end

  establish_parents(ast)
  if @options && @options[:allow_undefined_global_classes] && !@classes.has_global_import?
    @classes.globbed_imports.unshift(ImportedClass.new('*'))
  end
  class_imports = RegisterImportedClasses.new(ast, classes)
  class_imports.rewrite_on_match
  if resolve_class_dependencies?
    resolve_class_dependencies!(filename)
    return if @resolving_class_dependencies == true
  end
  class_registry = RegisterDefinedClasses.new(ast, classes)
  class_registry.rewrite_on_match
  rewrite_included_and_sourced_files!(filename)
  if filename && !included && add_SID_function?(filename)
    add_SID_function!
  end
  rewriters = [
    StrictEqualsComparisonOperator.new(ast, classes),
    VarEqualsComparisonOperator.new(ast, classes),
    ClassDefinitionToFunctions.new(ast, classes),
    ObjectInstantiationToCall.new(ast, classes),
    CallToExplicitCall.new(ast, classes),
    DefaultParamToIfNode.new(ast, classes),
    DeserializeVarAssignment.new(ast, classes),
    TopLevelDefMethodToDef.new(ast, classes),
    SplatsToCallFunctionInCallingContext.new(ast, classes)
  ]
  rewriters.each do |rewriter|
    rewriter.rewrite_on_match
  end
  ast
end

#rewrite_included_and_sourced_files!(filename) ⇒ Object

We need to rewrite the included/sourced files before anything else. This is in order to keep track of any classes defined in the included and sourced files (and files included/sourced in those, etc…). We keep a cache of rewritten asts because the included/sourced files are parsed more than once. They’re parsed first in this step, plus whenever the compiler visits a ‘riml_include’/‘riml_source’ node in order to compile it on the spot.



209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/riml/ast_rewriter.rb', line 209

def rewrite_included_and_sourced_files!(filename)
  old_ast = ast
  ast.children.grep(RimlFileCommandNode).each do |node|
    action = node.name == 'riml_include' ? 'include' : 'source'

    node.each_existing_file! do |file, fullpath|
      if filename && @included_and_sourced_file_refs[file].include?(filename)
        msg = "#{filename.inspect} can't #{action} #{file.inspect}, as " \
              " #{file.inspect} already included/sourced #{filename.inspect}"
        # IncludeFileLoop/SourceFileLoop
        error = Riml.const_get("#{action.capitalize}FileLoop").new(msg, node)
        raise error
      elsif filename == file
        error = UserArgumentError.new("#{file.inspect} can't #{action} itself", node)
        raise error
      end
      @included_and_sourced_file_refs[filename] << file
      # recursively parse included files with this ast_rewriter in order
      # to pick up any classes that are defined there
      rewritten_ast = nil
      watch_for_class_pickup do
        rewritten_ast = Riml.rewritten_ast_cache.fetch(file) do
          riml_src = File.read(fullpath)
          Parser.new.tap { |p| p.options = @options }.
            parse(riml_src, self, file, action == 'include')
        end
      end
      @rewritten_included_and_sourced_files[file] ||= rewritten_ast
    end
  end
ensure
  self.ast = old_ast
end

#rewrite_on_match(node = ast) ⇒ Object



85
86
87
# File 'lib/riml/ast_rewriter.rb', line 85

def rewrite_on_match(node = ast)
  Walker.walk_node(node, method(:do_rewrite_on_match), max_recursion_lvl)
end

#watch_for_class_pickupObject



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/riml/ast_rewriter.rb', line 243

def watch_for_class_pickup
  before_class_names = @classes.class_names
  ast = yield
  after_class_names = @classes.class_names
  diff_class_names = after_class_names - before_class_names
  class_diff = diff_class_names.inject({}) do |hash, class_name|
    hash[class_name] = @classes[class_name]
    hash
  end
  # no classes were picked up, it could be that the cache was hit. Let's
  # register the cached classes for this ast, if there are any
  if class_diff.empty?
    real_diff = Riml.rewritten_ast_cache.fetch_classes_registered(ast)
    real_diff.each do |k,v|
      @classes[k] = v unless @classes.safe_fetch(k)
    end
  # new classes were picked up, let's save them with this ast as the key
  else
    Riml.rewritten_ast_cache.save_classes_registered(ast, class_diff)
  end
end