Skip to content

Instantly share code, notes, and snippets.

@jashkenas
Created September 26, 2012 19:44
Show Gist options
  • Save jashkenas/3790135 to your computer and use it in GitHub Desktop.
Save jashkenas/3790135 to your computer and use it in GitHub Desktop.

The Scope class regulates lexical scoping within CoffeeScript. As you generate code, you create a tree of scopes in the same shape as the nested function bodies. Each scope knows about the variables declared within it, and has a reference to its parent enclosing scope. In this way, we know which variables are new and need to be declared with var, and which are shared with the outside.

Import the helpers we plan to use.

{extend, last} = require './helpers'

exports.Scope = class Scope

The top-level Scope object.

  @root: null

Initialize a scope with its parent, for lookups up the chain, as well as a reference to the Block node it belongs to, which is where it should declare its variables, and a reference to the function that it wraps.

  constructor: (@parent, @expressions, @method) ->
    @variables = [{name: 'arguments', type: 'arguments'}]
    @positions = {}
    Scope.root = this unless @parent

Adds a new variable or overrides an existing one.

  add: (name, type, immediate) ->
    return @parent.add name, type, immediate if @shared and not immediate
    if Object::hasOwnProperty.call @positions, name
      @variables[@positions[name]].type = type
    else
      @positions[name] = @variables.push({name, type}) - 1

When super is called, we need to find the name of the current method we're in, so that we know how to invoke the same method of the parent class. This can get complicated if super is being called from an inner function. namedMethod will walk up the scope tree until it either finds the first function object that has a name filled in, or bottoms out.

  namedMethod: ->
    return @method if @method.name or !@parent
    @parent.namedMethod()

Look up a variable name in lexical scope, and declare it if it does not already exist.

  find: (name) ->
    return yes if @check name
    @add name, 'var'
    no

Reserve a variable name as originating from a function parameter for this scope. No var required for internal references.

  parameter: (name) ->
    return if @shared and @parent.check name, yes
    @add name, 'param'

Just check to see if a variable has already been declared, without reserving, walks up to the root scope.

  check: (name) ->
    !!(@type(name) or @parent?.check(name))

Generate a temporary variable name at the given index.

  temporary: (name, index) ->
    if name.length > 1
      '_' + name + if index > 1 then index - 1 else ''
    else
      '_' + (index + parseInt name, 36).toString(36).replace /\d/g, 'a'

Gets the type of a variable.

  type: (name) ->
    return v.type for v in @variables when v.name is name
    null

If we need to store an intermediate result, find an available name for a compiler-generated variable. _var, _var2, and so on...

  freeVariable: (name, reserve=true) ->
    index = 0
    index++ while @check((temp = @temporary name, index))
    @add temp, 'var', yes if reserve
    temp

Ensure that an assignment is made at the top of this scope (or at the top-level scope, if requested).

  assign: (name, value) ->
    @add name, {value, assigned: yes}, yes
    @hasAssignments = yes

Does this scope have any declared variables?

  hasDeclarations: ->
    !!@declaredVariables().length

Return the list of variables first declared in this scope.

  declaredVariables: ->
    realVars = []
    tempVars = []
    for v in @variables when v.type is 'var'
      (if v.name.charAt(0) is '_' then tempVars else realVars).push v.name
    realVars.sort().concat tempVars.sort()

Return the list of assignments that are supposed to be made at the top of this scope.

  assignedVariables: ->
    "#{v.name} = #{v.type.value}" for v in @variables when v.type.assigned
(function() {
var Scope, extend, last, _ref;
_ref = require('./helpers'), extend = _ref.extend, last = _ref.last;
exports.Scope = Scope = (function() {
Scope.root = null;
function Scope(parent, expressions, method) {
this.parent = parent;
this.expressions = expressions;
this.method = method;
this.variables = [
{
name: 'arguments',
type: 'arguments'
}
];
this.positions = {};
if (!this.parent) {
Scope.root = this;
}
}
Scope.prototype.add = function(name, type, immediate) {
if (this.shared && !immediate) {
return this.parent.add(name, type, immediate);
}
if (Object.prototype.hasOwnProperty.call(this.positions, name)) {
return this.variables[this.positions[name]].type = type;
} else {
return this.positions[name] = this.variables.push({
name: name,
type: type
}) - 1;
}
};
Scope.prototype.namedMethod = function() {
if (this.method.name || !this.parent) {
return this.method;
}
return this.parent.namedMethod();
};
Scope.prototype.find = function(name) {
if (this.check(name)) {
return true;
}
this.add(name, 'var');
return false;
};
Scope.prototype.parameter = function(name) {
if (this.shared && this.parent.check(name, true)) {
return;
}
return this.add(name, 'param');
};
Scope.prototype.check = function(name) {
var _ref1;
return !!(this.type(name) || ((_ref1 = this.parent) != null ? _ref1.check(name) : void 0));
};
Scope.prototype.temporary = function(name, index) {
if (name.length > 1) {
return '_' + name + (index > 1 ? index - 1 : '');
} else {
return '_' + (index + parseInt(name, 36)).toString(36).replace(/\d/g, 'a');
}
};
Scope.prototype.type = function(name) {
var v, _i, _len, _ref1;
_ref1 = this.variables;
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
v = _ref1[_i];
if (v.name === name) {
return v.type;
}
}
return null;
};
Scope.prototype.freeVariable = function(name, reserve) {
var index, temp;
if (reserve == null) {
reserve = true;
}
index = 0;
while (this.check((temp = this.temporary(name, index)))) {
index++;
}
if (reserve) {
this.add(temp, 'var', true);
}
return temp;
};
Scope.prototype.assign = function(name, value) {
this.add(name, {
value: value,
assigned: true
}, true);
return this.hasAssignments = true;
};
Scope.prototype.hasDeclarations = function() {
return !!this.declaredVariables().length;
};
Scope.prototype.declaredVariables = function() {
var realVars, tempVars, v, _i, _len, _ref1;
realVars = [];
tempVars = [];
_ref1 = this.variables;
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
v = _ref1[_i];
if (v.type === 'var') {
(v.name.charAt(0) === '_' ? tempVars : realVars).push(v.name);
}
}
return realVars.sort().concat(tempVars.sort());
};
Scope.prototype.assignedVariables = function() {
var v, _i, _len, _ref1, _results;
_ref1 = this.variables;
_results = [];
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
v = _ref1[_i];
if (v.type.assigned) {
_results.push("" + v.name + " = " + v.type.value);
}
}
return _results;
};
return Scope;
})();
}).call(this);
@jashkenas
Copy link
Author

Using the literate branch, the text at the top, when listed in a file with a .litcoffee extension, compiles into the JavaScript at the bottom.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment