Class: AST::Node

Inherits:
Object
  • Object
show all
Defined in:
lib/ast/node.rb

Overview

Node is an immutable class, instances of which represent abstract syntax tree nodes. It combines semantic information (i.e. anything that affects the algorithmic properties of a program) with meta-information (line numbers or compiler intermediates).

Notes on inheritance

The distinction between semantics and metadata is important. Complete semantic information should be contained within just the #type and #children of a Node instance; in other words, if an AST was to be stripped of all meta-information, it should remain a valid AST which could be successfully processed to yield a result with the same algorithmic properties.

Thus, Node should never be inherited in order to define methods which affect or return semantic information, such as getters for class_name, superclass and body in the case of a hypothetical ClassNode. The correct solution is to use a generic Node with a #type of :class and three children. See also Processor for tips on working with such ASTs.

On the other hand, Node can and should be inherited to define application-specific metadata (see also #initialize) or customize the printing format. It is expected that an application would have one or two such classes and use them across the entire codebase.

The rationale for this pattern is extensibility and maintainability. Unlike static ones, dynamic languages do not require the presence of a predefined, rigid structure, nor does it improve dispatch efficiency, and while such a structure can certainly be defined, it does not add any value but incurs a maintaining cost. For example, extending the AST even with a transformation-local temporary node type requires making globally visible changes to the codebase.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(type, children = [], properties = {}) ⇒ Node

Constructs a new instance of Node.

The arguments type and children are converted with to_sym and to_a respectively. Additionally, the result of converting children is frozen. While mutating the arguments is generally considered harmful, the most common case is to pass an array literal to the constructor. If your code does not expect the argument to be frozen, use #dup.

The properties hash is passed to #assign_properties.



57
58
59
60
61
62
63
# File 'lib/ast/node.rb', line 57

def initialize(type, children=[], properties={})
  @type, @children = type.to_sym, children.to_a.freeze

  assign_properties(properties)

  freeze
end

Instance Attribute Details

#childrenArray (readonly)

Returns the children of this node. The returned value is frozen.

Returns:

  • (Array)


46
47
48
# File 'lib/ast/node.rb', line 46

def children
  @children
end

#typeSymbol (readonly)

Returns the type of this node.

Returns:

  • (Symbol)


41
42
43
# File 'lib/ast/node.rb', line 41

def type
  @type
end

Instance Method Details

#==(other) ⇒ Boolean

Compares self to other, possibly converting with to_ast. Only type and children are compared; metadata is deliberately ignored.

Returns:

  • (Boolean)


124
125
126
127
128
129
130
131
132
133
134
# File 'lib/ast/node.rb', line 124

def ==(other)
  if equal?(other)
    true
  elsif other.respond_to? :to_ast
    other = other.to_ast
    other.type == self.type &&
      other.children == self.children
  else
    false
  end
end

#append(element) ⇒ AST::Node Also known as: <<

Appends element to children and returns the resulting node.

Returns:



148
149
150
# File 'lib/ast/node.rb', line 148

def append(element)
  updated(nil, @children + [element])
end

#assign_properties(properties) ⇒ nil (protected)

By default, each entry in the properties hash is assigned to an instance variable in this instance of Node. A subclass should define attribute readers for such variables. The values passed in the hash are not frozen or whitelisted; such behavior can also be implemented\ by subclassing Node and overriding this method.

Returns:

  • (nil)


72
73
74
75
76
77
78
# File 'lib/ast/node.rb', line 72

def assign_properties(properties)
  properties.each do |name, value|
    instance_variable_set :"@#{name}", value
  end

  nil
end

#concat(array) ⇒ AST::Node Also known as: +

Concatenates array with children and returns the resulting node.

Returns:



139
140
141
# File 'lib/ast/node.rb', line 139

def concat(array)
  updated(nil, @children + array.to_a)
end

#dupObject

Nodes are already frozen, so there is no harm in returning the current node as opposed to initializing from scratch and freezing another one.

Returns:

  • self



89
90
91
# File 'lib/ast/node.rb', line 89

def dup
  self
end

#fancy_typeString (protected)

Returns @type with all underscores replaced by dashes. This allows to write symbol literals without quotes in Ruby sources and yet have nicely looking s-expressions.

Returns:

  • (String)


213
214
215
# File 'lib/ast/node.rb', line 213

def fancy_type
  @type.to_s.gsub('_', '-')
end

#to_aArray

Returns #children. This is very useful in order to decompose nodes concisely. For example:

node = s(:gasgn, :$foo, s(:integer, 1))
s
var_name, value = *node
p var_name # => :$foo
p value    # => (integer 1)

Returns:

  • (Array)


171
172
173
# File 'lib/ast/node.rb', line 171

def to_a
  children
end

#to_astAST::Node

Returns self.

Returns:



202
203
204
# File 'lib/ast/node.rb', line 202

def to_ast
  self
end

#to_sString

Converts self to a concise s-expression, omitting any children.

Returns:

  • (String)


157
158
159
# File 'lib/ast/node.rb', line 157

def to_s
  "(#{fancy_type} ...)"
end

#to_sexp(indent = 0) ⇒ String Also known as: inspect

Converts self to a pretty-printed s-expression.

Parameters:

  • indent (Integer) (defaults to: 0)

    Base indentation level.

Returns:

  • (String)


179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/ast/node.rb', line 179

def to_sexp(indent=0)
  indented = "  " * indent
  sexp = "#{indented}(#{fancy_type}"

  first_node_child = children.index do |child|
    child.is_a?(Node) || child.is_a?(Array)
  end || children.count

  children.each_with_index do |child, idx|
    if child.is_a?(Node) && idx >= first_node_child
      sexp << "\n#{child.to_sexp(indent + 1)}"
    else
      sexp << " #{child.inspect}"
    end
  end

  sexp << ")"

  sexp
end

#updated(type = nil, children = nil, properties = nil) ⇒ AST::Node

Returns a new instance of Node where non-nil arguments replace the corresponding fields of self.

For example, Node.new(:foo, [ 1, 2 ]).updated(:bar) would yield (bar 1 2), and Node.new(:foo, [ 1, 2 ]).updated(nil, []) would yield (foo).

If the resulting node would be identical to self, does nothing.

Parameters:

  • type (Symbol, nil) (defaults to: nil)
  • children (Array, nil) (defaults to: nil)
  • properties (Hash, nil) (defaults to: nil)

Returns:



106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/ast/node.rb', line 106

def updated(type=nil, children=nil, properties=nil)
  new_type       = type       || @type
  new_children   = children   || @children
  new_properties = properties || {}

  if @type == new_type &&
      @children == new_children &&
      properties.nil?
    self
  else
    original_dup.send :initialize, new_type, new_children, new_properties
  end
end