lively.lang
Version:
JavaScript utils providing useful abstractions for working with collections, functions, objects.
201 lines (171 loc) • 6.22 kB
JavaScript
// A `Closure` is a representation of a JavaScript function that controls what
// values are bound to out-of-scope variables. By default JavaScript has no
// reflection capabilities over closed values in functions. When needing to
// serialize execution or when behavior should become part of the state of a
// system it is often necessary to have first-class control over this language
// aspect.
//
// Typically closures aren't created directly but with the help of [`asScriptOf`](#)
//
// Example:
// function func(a) { return a + b; }
// var closureFunc = Closure.fromFunction(func, {b: 3}).recreateFunc();
// closureFunc(4) // => 7
// var closure = closureFunc.livelyClosure // => {
// // varMapping: { b: 3 },
// // originalFunc: function func(a) {/*...*/}
// // }
// closure.lookup("b") // => 3
// closure.getFuncSource() // => "function func(a) { return a + b; }"
import { evalJS } from "./function.js";
const parameterRegex = /function[^\(]*\(([^\)]*)\)|\(?([^\)=]*)\)?\s*=>/;
export default class Closure {
static fromFunction(func, varMapping) {
/*show-in-doc*/
return new this(func, varMapping || {});
}
static fromSource(source, varMapping) {
/*show-in-doc*/
return new this(null, varMapping || {}, source);
}
constructor(func, varMapping, source, funcProperties) {
this.originalFunc = func;
this.varMapping = varMapping || {};
this.setFuncSource(source || func);
this.setFuncProperties(func || funcProperties);
}
get isLivelyClosure() { return true; }
// serialization
get doNotSerialize() { return ['originalFunc']; }
// accessing
setFuncSource(src) {
/*show-in-doc*/
src = typeof lively !== "undefined" && lively.sourceTransform
&& typeof lively.sourceTransform.stringifyFunctionWithoutToplevelRecorder === "function" ?
lively.sourceTransform.stringifyFunctionWithoutToplevelRecorder(src) :
String(src);
return this.source = src;
}
getFuncSource() {
/*show-in-doc*/
return this.source || this.setFuncSource(this.originalFunc);
}
hasFuncSource() {
/*show-in-doc*/
return this.source && true;
}
getFunc() {
/*show-in-doc*/
return this.originalFunc || this.recreateFunc();
}
getFuncProperties() {
// ignore-in-doc
// a function may have state attached
return this.funcProperties || (this.funcProperties = {});
}
setFuncProperties(obj) {
// ignore-in-doc
var props = this.getFuncProperties();
for (var name in obj) {
// The AST implementation assumes that Function objects are some
// kind of value object. When their identity changes cached state
// should not be carried over to new function instances. This is a
// pretty intransparent way to invalidate attributes that are used
// for caches.
// @cschuster, can you please fix this by making invalidation more
// explicit?
if (obj.hasOwnProperty(name)) props[name] = obj[name];
}
}
lookup(name) {
/*show-in-doc*/
return this.varMapping[name];
}
parameterNames(methodString) {
// ignore-in-doc
if (typeof lively !== "undefined" && lively.ast && lively.ast.parseFunction) {
return (lively.ast.parseFunction(methodString).params || []).map(function(ea) {
if (ea.type === "Identifier") return ea.name;
if (ea.left && ea.left.type === "Identifier") return ea.left.name;
return null;
}).filter(Boolean);
}
var paramsMatch = parameterRegex.exec(methodString);
if (!paramsMatch) return [];
var paramsString = paramsMatch[1] || paramsMatch[2] || "";
return paramsString.split(",").map(function(ea) { return ea.trim(); });
}
firstParameter(src) {
// ignore-in-doc
return this.parameterNames(src)[0] || null;
}
// -=-=-=-=-=-=-=-=-=-
// function creation
// -=-=-=-=-=-=-=-=-=-
recreateFunc() {
// Creates a real function object
return this.recreateFuncFromSource(this.getFuncSource(), this.originalFunc);
}
recreateFuncFromSource(funcSource, optFunc) {
// ignore-in-doc
// what about objects that are copied by value, e.g. numbers?
// when those are modified after the originalFunc we captured
// varMapping then we will have divergent state
var closureVars = [],
thisFound = false,
specificSuperHandling = this.firstParameter(funcSource) === '$super';
for (var name in this.varMapping) {
if (!this.varMapping.hasOwnProperty(name)) continue;
if (name == 'this') { thisFound = true; continue; }
// closureVars.push(`var ${name} = this.varMapping.${name};\n`);
closureVars.push("var " + name + " = this.varMapping." + name + ";\n");
}
var src = "";
if (closureVars.length > 0) src += closureVars.join("\n");
if (specificSuperHandling) src += '(function superWrapperForClosure() { return ';
src += "(" + funcSource + ")";
if (specificSuperHandling) src += '.apply(this, [$super.bind(this)]'
+ '.concat(Array.from(arguments))) })';
try {
var func = evalJS.call(this, src) || this.couldNotCreateFunc(src);
this.addFuncProperties(func);
this.originalFunc = func;
return func;
} catch (e) {
// var msg = `Cannot create function ${e} src: ${src}`;
var msg = "Cannot create function " + e + " src: " + src;
console.error(msg);
throw new Error(msg);
}
}
addFuncProperties(func) {
// ignore-in-doc
var props = this.getFuncProperties();
for (var name in props)
if (props.hasOwnProperty(name))
func[name] = props[name];
this.addClosureInformation(func);
}
couldNotCreateFunc(src) {
// ignore-in-doc
var msg = 'Could not recreate closure from source: \n' + src;
console.error(msg);
return function() { throw new Error(msg); };
}
// -=-=-=-=-=-
// conversion
// -=-=-=-=-=-
asFunction() {
/*ignore-in-doc*/
return this.recreateFunc();
}
// -=-=-=-=-=-=-=-=-=-=-=-
// function modification
// -=-=-=-=-=-=-=-=-=-=-=-
addClosureInformation(f) {
/*ignore-in-doc-in-doc*/
f.hasLivelyClosure = true;
f.livelyClosure = this;
return f;
}
}