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

View File

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

View File

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

View File

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

View File

@@ -19,4 +19,22 @@ obj: {
} }
obj.unbound() 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)