diff --git a/lib/coffee_script/grammar.y b/lib/coffee_script/grammar.y index ea635e9663..d7ed1c3c5c 100644 --- a/lib/coffee_script/grammar.y +++ b/lib/coffee_script/grammar.y @@ -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. diff --git a/lib/coffee_script/lexer.rb b/lib/coffee_script/lexer.rb index b27fb72529..1d0cb83d1d 100644 --- a/lib/coffee_script/lexer.rb +++ b/lib/coffee_script/lexer.rb @@ -13,10 +13,11 @@ class Lexer "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|\$)*)/ diff --git a/lib/coffee_script/nodes.rb b/lib/coffee_script/nodes.rb index b0790e89ff..bc5c86b19a 100644 --- a/lib/coffee_script/nodes.rb +++ b/lib/coffee_script/nodes.rb @@ -24,6 +24,13 @@ def self.statement_only 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 @@ def compile(o={}) 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 @@ def idt(tabs=0) @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 @@ def top_sensitive?; false; end # 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 @@ def compile_expression(node, o) # 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 @@ class LiteralNode < Node # 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 @@ def compile_node(o) # 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 @@ def compile_node(o={}) # 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 @@ def compile_splat(o) # 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 @@ def compile_node(o={}) # 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 @@ def compile_node(o) # 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 @@ def compile_node(o) # 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 @@ def compile_node(o) 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 @@ def compile_array(o) # 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 @@ def compile_node(o) # 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 @@ def compile_splice(o) # 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 @@ class OpNode < Node 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 @@ def compile_unary(o) # 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 @@ def compile_node(o) 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 @@ def compile_node(o) # 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 @@ def compile_value(o, name, index) # An object literal. class ObjectNode < Node - attr_reader :properties + children :properties alias_method :objects, :properties def initialize(properties = []) @@ -639,7 +674,7 @@ def compile_node(o) # An array literal. class ArrayNode < Node - attr_reader :objects + children :objects def initialize(objects=[]) @objects = objects @@ -672,10 +707,9 @@ def self.wrap(array, expressions) # 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 @@ def compile_node(o) # 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 @@ def compile_node(o) # 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 @@ def compile_node(o) # Throw an exception. class ThrowNode < Node + children :expression statement_only - attr_reader :expression - def initialize(expression) @expression = expression end @@ -815,7 +848,7 @@ def compile_node(o) # 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 @@ def compile_node(o) # 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 @@ def compile_node(o) # 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 diff --git a/lib/coffee_script/value.rb b/lib/coffee_script/value.rb index 483ff8f8d3..0ceda17f66 100644 --- a/lib/coffee_script/value.rb +++ b/lib/coffee_script/value.rb @@ -45,6 +45,10 @@ def hash def match(regex) @value.match(regex) end + + def children + [] + end end end \ No newline at end of file diff --git a/test/fixtures/execution/test_functions.coffee b/test/fixtures/execution/test_functions.coffee index 6a3aeb798f..d64b4c2571 100644 --- a/test/fixtures/execution/test_functions.coffee +++ b/test/fixtures/execution/test_functions.coffee @@ -19,4 +19,22 @@ obj: { } obj.unbound() -obj.bound() \ No newline at end of file +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) \ No newline at end of file