Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do e += 1 and e = e + 1 compile differently in CoffeeScript?

I always assumed that <var> += 1 and <var> = <var> + 1 have the same semantics in JS.
Now, this CoffeeScript code compiles to different JavaScript when applied to the global variable e:

a: ->
  e = e + 1
b: ->
  e += 1

Note that b uses the global variable, whereas a defines a local variable:

({
  a: function() {
    var e;
    return e = e + 1;
  },
  b: function() {
    return e += 1;
  }
});

Try it yourself.
Is this a bug or is there a reason why this is so?

like image 570
Dan Abramov Avatar asked Nov 06 '12 19:11

Dan Abramov


2 Answers

I think I would call this a bug or at least an undocumented edge case or ambiguity. I don't see anything in the docs that explicitly specifies when a new local variable is created in CoffeeScript so it boils down to the usual

We do X when the current implementation does X and that happens because the current implementation does it that way.

sort of thing.

The condition that seems to trigger the creation of a new variable is assignment: it looks like CoffeeScript decides to create a new variable when you try to give it a value. So this:

a = ->
  e = e + 1

becomes

var a;
a = function() {
  var e;
  return e = e + 1;
};

with a local e variable because you are explicitly assigning e a value. If you simply refer to e in an expression:

b = ->
  e += 1

then CoffeeScript won't create a new variable because it doesn't recognize that there's an assignment to e in there. CS recognizes an expression but isn't smart enough to see e +=1 as equivalent to e = e + 1.

Interestingly enough, CS does recognize a problem when you use an op= form that is part of CoffeeScript but not JavaScript; for example:

c = ->
  e ||= 11

yields an error that:

the variable "e" can't be assigned with ||= because it has not been defined

I think making a similar complaint about e += 1 would be sensible and consistent. Or all a op= b expressions should expand to a = a op b and be treated equally.


If we look at the CoffeeScript source, we can see what's going on. If you poke around a bit you'll find that all the op= constructs end up going through Assign#compileNode:

compileNode: (o) ->
  if isValue = @variable instanceof Value
    return @compilePatternMatch o if @variable.isArray() or @variable.isObject()
    return @compileSplice       o if @variable.isSplice()
    return @compileConditional  o if @context in ['||=', '&&=', '?=']
  #...

so there is special handling for the CoffeeScript-specific op= conditional constructs as expected. A quick review suggests that a op= b for non-conditional op (i.e. ops other than ||, &&, and ?) pass straight on through to the JavaScript. So what's going on with compileCondtional? Well, as expected, it checks that you're not using undeclared variables:

compileConditional: (o) ->
  [left, right] = @variable.cacheReference o
  # Disallow conditional assignment of undefined variables.
  if not left.properties.length and left.base instanceof Literal and 
         left.base.value != "this" and not o.scope.check left.base.value
    throw new Error "the variable \"#{left.base.value}\" can't be assigned with #{@context} because it has not been defined."
  #...

There's the error message that we see from -> a ||= 11 and a comment noting that you're not allowed to a ||= b when a isn't defined somewhere.

like image 189
mu is too short Avatar answered Oct 14 '22 05:10

mu is too short


This can be pieced together from the documentation:

  • =: Assignment in Lexical scope

    The CoffeeScript compiler takes care to make sure that all of your variables are properly declared within lexical scope — you never need to write var yourself.

    inner within the function, on the other hand, should not be able to change the value of the external variable of the same name, and therefore has a declaration of its own.

    The example given in this section is precisely the same as your case.

  • += and ||=

    This is not a declaration, so the above does not apply. In its absence, += takes on its usual meaning, as does ||=.

    In fact, since these are not redefined by CoffeeScript, they take their meaning from ECMA-262 — the underlying target language — which yields the results you've observed.

    Unfortunately, this "fall-through" doesn't seem to be explicitly documented.

like image 21
Lightness Races in Orbit Avatar answered Oct 14 '22 05:10

Lightness Races in Orbit