159 lines
4.8 KiB
JavaScript
159 lines
4.8 KiB
JavaScript
// scope.js
|
|
// MIT licensed, see LICENSE file
|
|
// Copyright (c) 2013-2016 Olov Lassus <olov.lassus@gmail.com>
|
|
|
|
"use strict";
|
|
|
|
const assert = require("assert");
|
|
const stringmap = require("stringmap");
|
|
const stringset = require("stringset");
|
|
const is = require("simple-is");
|
|
const fmt = require("simple-fmt");
|
|
|
|
function Scope(args) {
|
|
assert(is.someof(args.kind, ["hoist", "block", "catch-block"]));
|
|
assert(is.object(args.node));
|
|
assert(args.parent === null || is.object(args.parent));
|
|
|
|
// kind === "hoist": function scopes, program scope, injected globals
|
|
// kind === "block": ES6 block scopes
|
|
// kind === "catch-block": catch block scopes
|
|
this.kind = args.kind;
|
|
|
|
// the AST node the block corresponds to
|
|
this.node = args.node;
|
|
|
|
// parent scope
|
|
this.parent = args.parent;
|
|
|
|
// children scopes for easier traversal (populated internally)
|
|
this.children = [];
|
|
|
|
// scope declarations. decls[variable_name] = {
|
|
// kind: "fun" for functions,
|
|
// "param" for function parameters,
|
|
// "caught" for catch parameter
|
|
// "var",
|
|
// "const",
|
|
// "let"
|
|
// node: the AST node the declaration corresponds to
|
|
// from: source code index from which it is visible at earliest
|
|
// (only stored for "const", "let" [and "var"] nodes)
|
|
// }
|
|
this.decls = stringmap();
|
|
|
|
// names of all variables declared outside this hoist scope but
|
|
// referenced in this scope (immediately or in child).
|
|
// only stored on hoist scopes for efficiency
|
|
// (because we currently generate lots of empty block scopes)
|
|
this.propagates = (this.kind === "hoist" ? stringset() : null);
|
|
|
|
// scopes register themselves with their parents for easier traversal
|
|
if (this.parent) {
|
|
this.parent.children.push(this);
|
|
}
|
|
}
|
|
|
|
Scope.prototype.print = function(indent) {
|
|
indent = indent || 0;
|
|
const scope = this;
|
|
const names = this.decls.keys().map(function(name) {
|
|
return fmt("{0} [{1}]", name, scope.decls.get(name).kind);
|
|
}).join(", ");
|
|
const propagates = this.propagates ? this.propagates.items().join(", ") : "";
|
|
console.log(fmt("{0}{1}: {2}. propagates: {3}", fmt.repeat(" ", indent), this.node.type, names, propagates));
|
|
this.children.forEach(function(c) {
|
|
c.print(indent + 2);
|
|
});
|
|
};
|
|
|
|
Scope.prototype.add = function(name, kind, node, referableFromPos) {
|
|
assert(is.someof(kind, ["fun", "param", "var", "caught", "const", "let"]));
|
|
|
|
function isConstLet(kind) {
|
|
return is.someof(kind, ["const", "let"]);
|
|
}
|
|
|
|
let scope = this;
|
|
|
|
// search nearest hoist-scope for fun, param and var's
|
|
// const, let and caught variables go directly in the scope (which may be hoist, block or catch-block)
|
|
if (is.someof(kind, ["fun", "param", "var"])) {
|
|
while (scope.kind !== "hoist") {
|
|
// if (scope.decls.has(name) && isConstLet(scope.decls.get(name).kind)) { // could be caught
|
|
// return error(getline(node), "{0} is already declared", name);
|
|
// }
|
|
scope = scope.parent;
|
|
}
|
|
}
|
|
// name exists in scope and either new or existing kind is const|let => error
|
|
// if (scope.decls.has(name) && (isConstLet(scope.decls.get(name).kind) || isConstLet(kind))) {
|
|
// return error(getline(node), "{0} is already declared", name);
|
|
// }
|
|
|
|
const declaration = {
|
|
kind: kind,
|
|
node: node,
|
|
};
|
|
if (referableFromPos) {
|
|
assert(is.someof(kind, ["var", "const", "let"]));
|
|
declaration.from = referableFromPos;
|
|
}
|
|
scope.decls.set(name, declaration);
|
|
};
|
|
|
|
Scope.prototype.getKind = function(name) {
|
|
assert(is.string(name));
|
|
const decl = this.decls.get(name);
|
|
return decl ? decl.kind : null;
|
|
};
|
|
|
|
Scope.prototype.getNode = function(name) {
|
|
assert(is.string(name));
|
|
const decl = this.decls.get(name);
|
|
return decl ? decl.node : null;
|
|
};
|
|
|
|
Scope.prototype.getFromPos = function(name) {
|
|
assert(is.string(name));
|
|
const decl = this.decls.get(name);
|
|
return decl ? decl.from : null;
|
|
};
|
|
|
|
Scope.prototype.hasOwn = function(name) {
|
|
return this.decls.has(name);
|
|
};
|
|
|
|
Scope.prototype.remove = function(name) {
|
|
return this.decls.remove(name);
|
|
};
|
|
|
|
Scope.prototype.doesPropagate = function(name) {
|
|
return this.propagates.has(name);
|
|
};
|
|
|
|
Scope.prototype.markPropagates = function(name) {
|
|
this.propagates.add(name);
|
|
};
|
|
|
|
Scope.prototype.closestHoistScope = function() {
|
|
let scope = this;
|
|
while (scope.kind !== "hoist") {
|
|
scope = scope.parent;
|
|
}
|
|
return scope;
|
|
};
|
|
|
|
Scope.prototype.lookup = function(name) {
|
|
for (let scope = this; scope; scope = scope.parent) {
|
|
if (scope.decls.has(name)) {
|
|
return scope;
|
|
} else if (scope.kind === "hoist") {
|
|
scope.propagates.add(name);
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
module.exports = Scope;
|