Jump To …

scope.coffee

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 Expressions node is 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 = {}
    if @parent
      @garbage = @parent.garbage
    else
      @garbage   = []
      Scope.root = this

Adds a new variable or overrides an existing one.

  add: (name, type) ->
    if typeof (pos = @positions[name]) is 'number'
      @variables[pos].type = type
    else
      @positions[name] = @variables.push({name, type}) - 1

Create a new garbage level

  startLevel: ->
    @garbage.push []
    this

Return to the previous garbage level and erase referenced temporary variables in current level from scope.

  endLevel: ->
    @add name, 'reuse' for name in @garbage.pop() when @type(name) is 'var'
    this

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

  find: (name, options) ->
    return true if @check name, options
    @add name, 'var'
    false

Test variables and return true the first time fn(v) returns true

  any: (fn) ->
    return yes for v in @variables when fn v
    no

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

  parameter: (name) ->
    @add name, 'param'

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

  check: (name, immediate) ->
    found = !!@type(name)
    return found if found or immediate
    !!@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 else ''
    else
      '_' + (index + parseInt name, 36).toString(36).replace /\d/g, 'a'

Gets the type of a variable.

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

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

  freeVariable: (type) ->
    index = 0
    index++ while @check((temp = @temporary type, index), true) and @type(temp) isnt 'reuse'
    @add temp, 'var'
    last(@garbage)?.push temp
    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: value, assigned: true

Does this scope reference any variables that need to be declared in the given function body?

  hasDeclarations: (body) ->
    body is @expressions and @any (v) -> v.type in ['var', 'reuse']

Does this scope reference any assignments that need to be declared at the top of the given function body?

  hasAssignments: (body) ->
    body is @expressions and @any (v) -> v.type.assigned

Return the list of variables first declared in this scope.

  declaredVariables: ->
    usr = []
    tmp = []
    for v in @variables when v.type in ['var', 'reuse']
      (if v.name.charAt(0) is '_' then tmp else usr).push v.name
    usr.sort().concat tmp.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)

Compile the JavaScript for all of the variable declarations in this scope.

  compiledDeclarations: ->
    @declaredVariables().join ', '

Compile the JavaScript for all of the variable assignments in this scope.

  compiledAssignments: ->
    @assignedVariables().join ', '