added children macro to Node, using it so that all nodes now have a 'children' method -- used for safe references to 'this' within closure wrappers

This commit is contained in:
Jeremy Ashkenas 2010-01-16 11:24:10 -05:00
parent 701cdb4c13
commit 1cd7fa8ebe
5 changed files with 100 additions and 42 deletions

View File

@ -16,6 +16,7 @@ token ARGUMENTS
token NEWLINE
token COMMENT
token JS
token THIS
token INDENT OUTDENT
# Declare order of operations.
@ -102,12 +103,12 @@ rule
| BREAK { result = LiteralNode.new(val[0]) }
| CONTINUE { result = LiteralNode.new(val[0]) }
| ARGUMENTS { result = LiteralNode.new(val[0]) }
| TRUE { result = LiteralNode.new(true) }
| FALSE { result = LiteralNode.new(false) }
| YES { result = LiteralNode.new(true) }
| NO { result = LiteralNode.new(false) }
| ON { result = LiteralNode.new(true) }
| OFF { result = LiteralNode.new(false) }
| TRUE { result = LiteralNode.new(Value.new(true)) }
| FALSE { result = LiteralNode.new(Value.new(false)) }
| YES { result = LiteralNode.new(Value.new(true)) }
| NO { result = LiteralNode.new(Value.new(false)) }
| ON { result = LiteralNode.new(Value.new(true)) }
| OFF { result = LiteralNode.new(Value.new(false)) }
;
# Assignment to a variable (or index).
@ -235,6 +236,7 @@ rule
| Range { result = ValueNode.new(val[0]) }
| Value Accessor { result = val[0] << val[1] }
| Invocation Accessor { result = ValueNode.new(val[0], [val[1]]) }
| THIS { result = ValueNode.new(ThisNode.new) }
;
# Accessing into an object or array, through dot or index notation.

View File

@ -13,10 +13,11 @@ module CoffeeScript
"try", "catch", "finally", "throw",
"break", "continue",
"for", "in", "of", "by", "where", "while",
"delete", "instanceof", "typeof",
"switch", "when",
"super", "extends",
"arguments",
"delete", "instanceof", "typeof"]
"this"]
# Token matching regexes.
IDENTIFIER = /\A([a-zA-Z$_](\w|\$)*)/

View File

@ -24,6 +24,13 @@ module CoffeeScript
class_eval "def statement_only?; true; end"
end
# Provide a quick implementation of a children method.
def self.children(*attributes)
attr_reader *attributes
attrs = attributes.map {|a| "[@#{a}]" }.join(', ')
class_eval "def children; [#{attrs}].flatten.compact; end"
end
def write(code)
puts "#{self.class.to_s}:\n#{@options.inspect}\n#{code}\n\n" if ENV['VERBOSE']
code
@ -42,9 +49,11 @@ module CoffeeScript
end
def compile_closure(o={})
indent = o[:indent]
@indent = (o[:indent] = idt(1))
"(function() {\n#{compile_node(o.merge(:return => true))}\n#{indent}})()"
indent = o[:indent]
@indent = (o[:indent] = idt(1))
pass_this = !o[:closure] && contains? {|node| node.is_a?(ThisNode) }
param = pass_this ? '__this' : ''
"(function(#{param}) {\n#{compile_node(o.merge(:return => true, :closure => true))}\n#{indent}})(#{pass_this ? 'this' : ''})"
end
# Quick short method for the current indentation level, plus tabbing in.
@ -52,6 +61,20 @@ module CoffeeScript
@indent + (TAB * tabs)
end
# Does this node, or any of it's children, contain a node of a certain kind?
def contains?(&block)
children.each do |node|
return true if yield(node)
node.is_a?(Node) && node.contains?(&block)
false
end
end
# All Nodes must implement a "children" method that returns child nodes.
def children
raise NotImplementedError, "#{self.class} is missing a 'children' method"
end
# Default implementations of the common node methods.
def unwrap; self; end
def statement?; false; end
@ -62,7 +85,7 @@ module CoffeeScript
# A collection of nodes, each one representing an expression.
class Expressions < Node
statement
attr_reader :expressions
children :expressions
TRAILING_WHITESPACE = /\s+$/
UPPERCASE = /[A-Z]/
@ -156,6 +179,7 @@ module CoffeeScript
# Literals are static values that have a Ruby representation, eg.: a string, a number,
# true, false, nil, etc.
class LiteralNode < Node
children :value
# Values of a literal node that much be treated as a statement -- no
# sense returning or assigning them.
@ -165,8 +189,6 @@ module CoffeeScript
# it to an array.
ARG_ARRAY = 'Array.prototype.slice.call(arguments, 0)'
attr_reader :value
# Wrap up a compiler-generated string as a LiteralNode.
def self.wrap(string)
self.new(Value.new(string))
@ -192,8 +214,7 @@ module CoffeeScript
# Return an expression, or wrap it in a closure and return it.
class ReturnNode < Node
statement_only
attr_reader :expression
children :expression
def initialize(expression)
@expression = expression
@ -225,7 +246,7 @@ module CoffeeScript
# Node for a function invocation. Takes care of converting super() calls into
# calls against the prototype's function of the same name.
class CallNode < Node
attr_reader :variable, :arguments
children :variable, :arguments
def initialize(variable, arguments=[])
@variable, @arguments = variable, arguments
@ -285,8 +306,8 @@ module CoffeeScript
# Node to extend an object's prototype with an ancestor object.
# After goog.inherits from the Closure Library.
class ExtendsNode < Node
children :sub_object, :super_object
statement
attr_reader :sub_object, :super_object
def initialize(sub_object, super_object)
@sub_object, @super_object = sub_object, super_object
@ -307,7 +328,8 @@ module CoffeeScript
# A value, indexed or dotted into, or vanilla.
class ValueNode < Node
attr_reader :base, :properties, :last, :source
children :base, :properties
attr_reader :last, :source
def initialize(base, properties=[])
@base, @properties = base, properties
@ -352,7 +374,7 @@ module CoffeeScript
# A dotted accessor into a part of a value, or the :: shorthand for
# an accessor into the object's prototype.
class AccessorNode < Node
attr_reader :name
children :name
def initialize(name, prototype=false)
@name, @prototype = name, prototype
@ -366,7 +388,7 @@ module CoffeeScript
# An indexed accessor into a part of an array or object.
class IndexNode < Node
attr_reader :index
children :index
def initialize(index)
@index = index
@ -377,10 +399,20 @@ module CoffeeScript
end
end
# A node to represent a reference to "this". Needs to be transformed into a
# reference to the correct value of "this", when used within a closure wrapper.
class ThisNode < Node
def compile_node(o)
write(o[:closure] ? "__this" : "this")
end
end
# A range literal. Ranges can be used to extract portions (slices) of arrays,
# or to specify a range for array comprehensions.
class RangeNode < Node
attr_reader :from, :to
children :from, :to
def initialize(from, to, exclusive=false)
@from, @to, @exclusive = from, to, exclusive
@ -423,7 +455,7 @@ module CoffeeScript
# specifies the index of the end of the slice (just like the first parameter)
# is the index of the beginning.
class SliceNode < Node
attr_reader :range
children :range
def initialize(range)
@range = range
@ -439,11 +471,11 @@ module CoffeeScript
# Setting the value of a local variable, or the value of an object property.
class AssignNode < Node
children :variable, :value
PROTO_ASSIGN = /\A(\S+)\.prototype/
LEADING_DOT = /\A\.(prototype\.)?/
attr_reader :variable, :value, :context
def initialize(variable, value, context=nil)
@variable, @value, @context = variable, value, context
end
@ -505,6 +537,9 @@ module CoffeeScript
# Simple Arithmetic and logical operations. Performs some conversion from
# CoffeeScript operations into their JavaScript equivalents.
class OpNode < Node
children :first, :second
attr_reader :operator
CONVERSIONS = {
:== => "===",
:'!=' => "!==",
@ -517,8 +552,6 @@ module CoffeeScript
CONDITIONALS = [:'||=', :'&&=']
PREFIX_OPERATORS = [:typeof, :delete]
attr_reader :operator, :first, :second
def initialize(operator, first, second=nil, flip=false)
@first, @second, @flip = first, second, flip
@operator = CONVERSIONS[operator.to_sym] || operator
@ -550,7 +583,8 @@ module CoffeeScript
# A function definition. The only node that creates a new Scope.
class CodeNode < Node
attr_reader :params, :body, :bound
children :params, :body
attr_reader :bound
def initialize(params, body, tag=nil)
@params = params
@ -566,6 +600,7 @@ module CoffeeScript
o[:indent] = idt(@bound ? 2 : 1)
o.delete(:no_wrap)
o.delete(:globals)
o.delete(:closure)
name = o.delete(:immediate_assign)
if @params.last.is_a?(SplatNode)
splat = @params.pop
@ -584,8 +619,8 @@ module CoffeeScript
# A splat, either as a parameter to a function, an argument to a call,
# or in a destructuring assignment.
class SplatNode < Node
children :name
attr_accessor :index
attr_reader :name
def initialize(name)
@name = name
@ -612,7 +647,7 @@ module CoffeeScript
# An object literal.
class ObjectNode < Node
attr_reader :properties
children :properties
alias_method :objects, :properties
def initialize(properties = [])
@ -639,7 +674,7 @@ module CoffeeScript
# An array literal.
class ArrayNode < Node
attr_reader :objects
children :objects
def initialize(objects=[])
@objects = objects
@ -672,10 +707,9 @@ module CoffeeScript
# A while loop, the only sort of low-level loop exposed by CoffeeScript. From
# it, all other loops can be manufactured.
class WhileNode < Node
children :condition, :body
statement
attr_reader :condition, :body
def initialize(condition, body)
@condition, @body = condition, body
end
@ -707,10 +741,10 @@ module CoffeeScript
# of the comprehenion. Unlike Python array comprehensions, it's able to pass
# the current index of the loop as a second parameter.
class ForNode < Node
children :body, :source, :filter
attr_reader :name, :index, :step
statement
attr_reader :body, :source, :name, :index, :filter, :step
def initialize(body, source, name, index=nil)
@body, @name, @index = body, name, index
@source = source[:source]
@ -780,10 +814,10 @@ module CoffeeScript
# A try/catch/finally block.
class TryNode < Node
children :try, :recovery, :finally
attr_reader :error
statement
attr_reader :try, :error, :recovery, :finally
def initialize(try, error, recovery, finally=nil)
@try, @error, @recovery, @finally = try, error, recovery, finally
end
@ -800,10 +834,9 @@ module CoffeeScript
# Throw an exception.
class ThrowNode < Node
children :expression
statement_only
attr_reader :expression
def initialize(expression)
@expression = expression
end
@ -815,7 +848,7 @@ module CoffeeScript
# Check an expression for existence (meaning not null or undefined).
class ExistenceNode < Node
attr_reader :expression
children :expression
def initialize(expression)
@expression = expression
@ -831,7 +864,7 @@ module CoffeeScript
# You can't wrap parentheses around bits that get compiled into JS statements,
# unfortunately.
class ParentheticalNode < Node
attr_reader :expressions
children :expressions
def initialize(expressions, line=nil)
@expressions = expressions.unwrap
@ -850,7 +883,7 @@ module CoffeeScript
# Single-expression IfNodes are compiled into ternary operators if possible,
# because ternaries are first-class returnable assignable expressions.
class IfNode < Node
attr_reader :condition, :body, :else_body
children :condition, :body, :else_body
def initialize(condition, body, else_body=nil, tags={})
@condition = condition

View File

@ -45,6 +45,10 @@ module CoffeeScript
def match(regex)
@value.match(regex)
end
def children
[]
end
end
end

View File

@ -19,4 +19,22 @@ obj: {
}
obj.unbound()
obj.bound()
obj.bound()
# When when a closure wrapper is generated for expression conversion, make sure
# that references to "this" within the wrapper are safely converted as well.
obj: {
num: 5
func: =>
this.result: if false
10
else
"a"
"b"
this.num
}
print(obj.num is obj.func())
print(obj.num is obj.result)