mathjs/src/parser/Scope.js
2013-03-15 23:15:32 +01:00

350 lines
8.5 KiB
JavaScript

/**
* Scope
* A scope stores functions.
*
* @constructor mathnotepad.Scope
* @param {Scope} [parentScope]
*/
function Scope(parentScope) {
this.parentScope = parentScope;
this.nestedScopes = undefined;
this.symbols = {}; // the actual symbols
// the following objects are just used to test existence.
this.defs = {}; // definitions by name (for example "a = [1, 2; 3, 4]")
this.updates = {}; // updates by name (for example "a(2, 1) = 5.2")
this.links = {}; // links by name (for example "2 * a")
}
math.parser.node.Scope = Scope;
// TODO: rethink the whole scoping solution again. Try to simplify
/**
* Create a nested scope
* The variables in a nested scope are not accessible from the parent scope
* @return {Scope} nestedScope
*/
Scope.prototype.createNestedScope = function () {
var nestedScope = new Scope(this);
if (!this.nestedScopes) {
this.nestedScopes = [];
}
this.nestedScopes.push(nestedScope);
return nestedScope;
};
/**
* Clear all symbols in this scope and its nested scopes
* (parent scope will not be cleared)
*/
Scope.prototype.clear = function () {
this.symbols = {};
this.defs = {};
this.links = {};
this.updates = {};
if (this.nestedScopes) {
var nestedScopes = this.nestedScopes;
for (var i = 0, iMax = nestedScopes.length; i < iMax; i++) {
nestedScopes[i].clear();
}
}
};
/**
* create a symbol
* @param {String} name
* @return {function} symbol
* @private
*/
Scope.prototype.createSymbol = function (name) {
var symbol = this.symbols[name];
if (!symbol) {
// get a link to the last definition
var lastDef = this.findDef(name);
// create a new symbol
symbol = this.newSymbol(name, lastDef);
this.symbols[name] = symbol;
}
return symbol;
};
/**
* Create a new symbol
* @param {String} name
* @param {*} [value]
* @return {function} symbol
* @private
*/
Scope.prototype.newSymbol = function (name, value) {
// create a new symbol
var scope = this;
var symbol = function () {
if (!symbol.value) {
// try to resolve again
symbol.value = scope.findDef(name);
if (!symbol.value) {
throw new Error('Undefined symbol ' + name);
}
}
if (typeof symbol.value == 'function') {
return symbol.value.apply(null, arguments);
}
else {
// TODO: implement subset for all types
return symbol.value;
}
};
symbol.value = value;
symbol.toString = function () {
return symbol.value ? symbol.value.toString() : '';
};
return symbol;
};
/**
* create a link to a value.
* @param {String} name
* @return {function} symbol
*/
Scope.prototype.createLink = function (name) {
var symbol = this.links[name];
if (!symbol) {
symbol = this.createSymbol(name);
this.links[name] = symbol;
}
return symbol;
};
/**
* Create a variable definition
* Returns the created symbol
* @param {String} name
* @param {*} [value]
* @return {function} symbol
*/
Scope.prototype.createDef = function (name, value) {
var symbol = this.defs[name];
if (!symbol) {
symbol = this.createSymbol(name);
this.defs[name] = symbol;
}
if (symbol && value != undefined) {
symbol.value = value;
}
return symbol;
};
/**
* Create a variable update definition
* Returns the created symbol
* @param {String} name
* @return {function} symbol
*/
Scope.prototype.createUpdate = function (name) {
var symbol = this.updates[name];
if (!symbol) {
symbol = this.createLink(name);
this.updates[name] = symbol;
}
return symbol;
};
/**
* get the link to a symbol definition or update.
* If the symbol is not found in this scope, it will be looked up in its parent
* scope.
* @param {String} name
* @return {function | undefined} symbol, or undefined when not found
*/
Scope.prototype.findDef = function (name) {
var symbol;
// check scope
symbol = this.defs[name];
if (symbol) {
return symbol;
}
symbol = this.updates[name];
if (symbol) {
return symbol;
}
// check parent scope
if (this.parentScope) {
return this.parentScope.findDef(name);
}
else {
// this is the root scope (has no parent)
var newSymbol = this.newSymbol,
symbols = this.symbols,
defs = this.defs;
/**
* Store a symbol in the root scope
* @param {String} name
* @param {*} value
* @return {function} symbol
*/
function put(name, value) {
var symbol = newSymbol(name, value);
symbols[name] = symbol;
defs[name] = symbol;
return symbol;
}
// check constant (and load the constant)
if (name == 'pi') {
return put(name, math.PI);
}
if (name == 'e') {
return put(name, math.E);
}
if (name == 'i') {
return put(name, new Complex(0, 1));
}
// check function (and load the function), for example "sin" or "sqrt"
// search in the mathnotepad.math namespace for this symbol
var fn = math[name];
if (fn) {
return put(name, fn);
}
// Check if token is a unit
// Note: we do not check the upper case name, units are case sensitive!
if (Unit.isUnit(name)) {
var unit = new Unit(null, name);
return put(name, unit);
}
}
return undefined;
};
/**
* Remove a link to a symbol
* @param {String} name
*/
Scope.prototype.removeLink = function (name) {
delete this.links[name];
};
/**
* Remove a definition of a symbol
* @param {String} name
*/
Scope.prototype.removeDef = function (name) {
delete this.defs[name];
};
/**
* Remove an update definition of a symbol
* @param {String} name
*/
Scope.prototype.removeUpdate = function (name) {
delete this.updates[name];
};
/**
* initialize the scope and its nested scopes
*
* All functions are linked to their previous definition
* If there is no parentScope, or no definition of the func in the parent scope,
* the link will be set undefined
*/
Scope.prototype.init = function () {
var symbols = this.symbols;
var parentScope = this.parentScope;
for (var name in symbols) {
if (symbols.hasOwnProperty(name)) {
var symbol = symbols[name];
symbol.value = (parentScope ? parentScope.findDef(name) : undefined);
}
}
if (this.nestedScopes) {
this.nestedScopes.forEach(function (nestedScope) {
nestedScope.init();
});
}
};
/**
* Check whether this scope or any of its nested scopes contain a link to a
* symbol with given name
* @param {String} name
* @return {boolean} hasLink True if a link with given name is found
*/
Scope.prototype.hasLink = function (name) {
if (this.links[name]) {
return true;
}
if (this.nestedScopes) {
var nestedScopes = this.nestedScopes;
for (var i = 0, iMax = nestedScopes.length; i < iMax; i++) {
if (nestedScopes[i].hasLink(name)) {
return true;
}
}
}
return false;
};
/**
* Check whether this scope contains a definition of a symbol with given name
* @param {String} name
* @return {boolean} hasDef True if a definition with given name is found
*/
Scope.prototype.hasDef = function (name) {
return (this.defs[name] != undefined);
};
/**
* Check whether this scope contains an update definition of a symbol with
* given name
* @param {String} name
* @return {boolean} hasUpdate True if an update definition with given name is found
*/
Scope.prototype.hasUpdate = function (name) {
return (this.updates[name] != undefined);
};
/**
* Retrieve all undefined symbols
* @return {function[]} undefinedSymbols All symbols which are undefined
*/
Scope.prototype.getUndefinedSymbols = function () {
var symbols = this.symbols;
var undefinedSymbols = [];
for (var i in symbols) {
if (symbols.hasOwnProperty(i)) {
var symbol = symbols[i];
if (symbol.value == undefined) {
undefinedSymbols.push(symbol);
}
}
}
if (this.nestedScopes) {
this.nestedScopes.forEach(function (nestedScope) {
undefinedSymbols =
undefinedSymbols.concat(nestedScope.getUndefinedSymbols());
});
}
return undefinedSymbols;
};