1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 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 131 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 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 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 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
|
# frozen_string_literal: true
module AST
# 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.
#
class Node
# Returns the type of this node.
# @return [Symbol]
attr_reader :type
# Returns the children of this node.
# The returned value is frozen.
# The to_a alias is useful for decomposing nodes concisely.
# For example:
#
# node = s(:gasgn, :$foo, s(:integer, 1))
# var_name, value = *node
# p var_name # => :$foo
# p value # => (integer 1)
#
# @return [Array]
attr_reader :children
alias to_a children
# Returns the precomputed hash value for this node
# @return [Integer]
attr_reader :hash
# 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}.
def initialize(type, children=[], properties={})
@type, @children = type.to_sym, children.to_a.freeze
assign_properties(properties)
@hash = [@type, @children, self.class].hash
freeze
end
# Test if other object is equal to
# @param [Object] other
# @return [Boolean]
def eql?(other)
self.class.eql?(other.class) &&
@type.eql?(other.type) &&
@children.eql?(other.children)
end
# 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.
#
# @return [nil]
def assign_properties(properties)
properties.each do |name, value|
instance_variable_set :"@#{name}", value
end
nil
end
protected :assign_properties
alias :original_dup :dup
private :original_dup
# Nodes are already frozen, so there is no harm in returning the
# current node as opposed to initializing from scratch and freezing
# another one.
#
# @return self
def dup
self
end
alias :clone :dup
# 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.
#
# @param [Symbol, nil] type
# @param [Array, nil] children
# @param [Hash, nil] properties
# @return [AST::Node]
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
copy = original_dup
copy.send :initialize, new_type, new_children, new_properties
copy
end
end
# Compares `self` to `other`, possibly converting with `to_ast`. Only
# `type` and `children` are compared; metadata is deliberately ignored.
#
# @return [Boolean]
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
# Concatenates `array` with `children` and returns the resulting node.
#
# @return [AST::Node]
def concat(array)
updated(nil, @children + array.to_a)
end
alias + concat
# Appends `element` to `children` and returns the resulting node.
#
# @return [AST::Node]
def append(element)
updated(nil, @children + [element])
end
alias << append
# Converts `self` to a pretty-printed s-expression.
#
# @param [Integer] indent Base indentation level.
# @return [String]
def to_sexp(indent=0)
indented = " " * indent
sexp = "#{indented}(#{fancy_type}"
children.each do |child|
if child.is_a?(Node)
sexp += "\n#{child.to_sexp(indent + 1)}"
else
sexp += " #{child.inspect}"
end
end
sexp += ")"
sexp
end
alias to_s to_sexp
# Converts `self` to a s-expression ruby string.
# The code return will recreate the node, using the sexp module s()
#
# @param [Integer] indent Base indentation level.
# @return [String]
def inspect(indent=0)
indented = " " * indent
sexp = "#{indented}s(:#{@type}"
children.each do |child|
if child.is_a?(Node)
sexp += ",\n#{child.inspect(indent + 1)}"
else
sexp += ", #{child.inspect}"
end
end
sexp += ")"
sexp
end
# @return [AST::Node] self
def to_ast
self
end
# Converts `self` to an Array where the first element is the type as a Symbol,
# and subsequent elements are the same representation of its children.
#
# @return [Array<Symbol, [...Array]>]
def to_sexp_array
children_sexp_arrs = children.map do |child|
if child.is_a?(Node)
child.to_sexp_array
else
child
end
end
[type, *children_sexp_arrs]
end
# Enables matching for Node, where type is the first element
# and the children are remaining items.
#
# @return [Array]
def deconstruct
[type, *children]
end
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.
#
# @return [String]
def fancy_type
@type.to_s.gsub('_', '-')
end
end
end
|