// http://jarrett.cs.ucla.edu/worlds/worlds.js DEBUG = false // some library functions Object.prototype.delegated = function(props) { var f = function() { } f.prototype = this var r = new f() for (var p in props) if (props.hasOwnProperty(p)) r[p] = props[p] return r } // object -> unique id // Note: one good thing about this funny object hashing scheme is that it makes worlds a bit like weak arrays, i.e., they // never get in the way of proper GC. Object.prototype.getID = (function() { var numIds = 0 return function() { return this._id_ === undefined ? this._id_ = "R" + numIds++ : this._id_ } })() Boolean.prototype.getID = function() { return this } String.prototype.getID = function() { return "S" + this } Number.prototype.getID = function() { return "N" + this } getID = function(x) { return x === null || x === undefined ? x : x.getID() } // implementation of possible worlds Array.prototype.parent = Object.prototype Boolean.prototype.parent = Object.prototype Number.prototype.parent = Object.prototype String.prototype.parent = Object.prototype $world = (function() { var deltas = {} return { deltas: deltas, get: function(r, p) { var id = getID(r) if (DEBUG) console.log("? parent world looking up " + id + "." + p) if (deltas[id] !== undefined && deltas[id][p] !== undefined) return deltas[id][p] else if (r === Object.prototype) return undefined else return this.get(r === null || r === undefined ? Object.prototype : r.parent, p) }, set: function(r, p, v) { var id = getID(r) if (DEBUG) console.log("! parent world assigning to " + id + "." + p) if (deltas[id] === undefined) deltas[id] = {} deltas[id][p] = v return v }, send: function(r, p) { var realR = r, realP = p arguments[0] = this arguments[1] = realR return this.get(realR, realP).apply(null, arguments) }, makeChild: function() { var parentWorld = this, deltas = {} return { deltas: deltas, get: function(r, p) { var id = getID(r) if (DEBUG) console.log("? child world looking up " + id + "." + p) return deltas[id] !== undefined && deltas[id][p] !== undefined ? deltas[id][p] : parentWorld.get.call(this, r, p) }, set: function(r, p, v) { var id = getID(r) if (DEBUG) console.log("! child world assigning to " + id + "." + p) if (deltas[id] === undefined) deltas[id] = {} deltas[id][p] = v return v }, commit: function() { for (var p in deltas) if (deltas.hasOwnProperty(p)) parentWorld.deltas[p] = deltas[p] }, // Note: worlds can be used to get something like the semantics of expanders... though sometimes it is useful to let // some side-effects leak to the "client" world. That's what "freeze" is for. // TODO: improve "freeze" semantics; is it possible to get world-specific fields? freeze: function() { this.set = parentWorld.set }, send: parentWorld.send, makeChild: parentWorld.makeChild, } } } })(); // function calls call = function($world, f) { var realF = f arguments[1] = undefined return realF.apply(null, arguments) } // "this" and lexical scoping // Note: I'm not very happy about using undefined to denote that a variable is not declared, since it allows programmers to // dynamically undeclare variables via assignment. One solution might be to make "undefined" only accessible at the impl. level, // but it would be even better to actually solve this problem. $this = undefined $scope = { get: function($world, n) { return $world.get(this, n) }, set: function($world, n, v) { return $world.set(this, n, v) }, decl: function($world, n, v) { return $world.set(this, n, v) }, makeChild: function() { var parent = this return { get: function($world, n) { return $world.get(this, n) === undefined ? parent.get($world, n) : $world.get(this, n) }, set: function($world, n, v) { return $world.get(this, n) === undefined ? parent.set($world, n, v) : $world.set(this, n, v) }, decl: parent.decl, makeChild: parent.makeChild } } } // hand-translated examples /* f = function(x) { return function(y) { return x + y } } f(1)(2) */ $scope.set($world, "f", (function() { var $staticScope = $scope return function($world, $this) { var $scope = $staticScope.makeChild() $scope.decl($world, "x", arguments[2]) return (function() { var $staticScope = $scope return function($world, $this) { var $scope = $staticScope.makeChild() $scope.decl($world, "y", arguments[2]) return $scope.get($world, "x") + $scope.get($world, "y") } })() } })()) console.log(call($world, call($world, $scope.get($world, "f"), 1), 2)) /* try { x = 1; y = 2; alert(x); alert(y); abort } else { alert(x); alert(y) } */ console.log(" ") console.log("before, x=" + $scope.get($world, "x") + ", y=" + $scope.get($world, "y")) try { var $world0 = $world.makeChild(); (function($world) { $scope.set($world, "x", 1) $scope.set($world, "y", 2) console.log("during, x=" + $scope.get($world, "x") + ", y=" + $scope.get($world, "y")) throw "abort" })($world0) } catch(_) { console.log("aborted") $world0 = null } finally { if ($world0 !== null) $world0.commit() } console.log("after, x=" + $scope.get($world, "x") + ", y=" + $scope.get($world, "y")) /* object.foo = function(a, b) { return "foo was called on " + this + ", a=" + a + ", b=" + b } null.foo() */ console.log(" ") $scope.decl($world, "object", Object.prototype) // TODO: have a bunch of these protos declared globally $world.set($scope.get($world, "object"), "foo", (function() { var $staticScope = $scope return function($world, $this) { var $scope = $staticScope.makeChild() $scope.decl($world, "a", arguments[2]) $scope.decl($world, "b", arguments[3]) return "foo was called on " + $this + ", a=" + $scope.get($world, "a") + ", b=" + $scope.get($world, "b") } })()) console.log($world.send(null, "foo", 5, 6)) /* app1 = new world app2 = new world in app1 { object.sign = function() { this.signature = "signed by app1" } } freeze app1 // make all subsequent changes go to the "parent" world in app2 { object.sign = function() { this.signature = "signed by app2" } } freeze app2 x = "foo" in app1 { x.sign() } console.log(x + ".signature = " + x.signature) in app2 { x.sign() } console.log(x + ".signature = " + x.signature) */ console.log(" ") $scope.set($world, "app1", $world.makeChild()) $scope.set($world, "app2", $world.makeChild()); (function($world) { $world.set($scope.get($world, "object"), "sign", (function() { var $staticScope = $scope return function($world, $this) { var $scope = $staticScope.makeChild() $world.set($this, "signature", "signed by app1") } })()) })($scope.get($world, "app1")); $scope.get($world, "app1").freeze(); (function($world) { $world.set($scope.get($world, "object"), "sign", (function() { var $staticScope = $scope return function($world, $this) { var $scope = $staticScope.makeChild() $world.set($this, "signature", "signed by app2") } })()) })($scope.get($world, "app2")); $scope.get($world, "app2").freeze(); $scope.set($world, "x", "foo"); (function($world) { $world.send($scope.get($world, "x"), "sign") })($scope.get($world, "app1")); console.log($world.get($scope.get($world, "x"), "signature")); (function($world) { $world.send($scope.get($world, "x"), "sign") })($scope.get($world, "app2")); console.log($world.get($scope.get($world, "x"), "signature")); /* // this may be an interesting form of generics app1 = new world app2 = new world doAll = function(xs) { for (var idx = 0; idx < xs.length; idx++) xs[idx].doYourThing() } in app1 { object.doYourThing = function() { console.log(this) } } in app2 { object.doYourThing = function() { alert(this) } } objs = {0: "first", 1: "second", 2: "third", length: 3} in app1 { doAll(objs) } in app2 { doAll(objs) } */ console.log(" ") $scope.set($world, "app1", $world.makeChild()) $scope.set($world, "app2", $world.makeChild()) $scope.set($world, "doAll", (function() { var $staticScope = $scope return function($world, $this) { var $scope = $staticScope.makeChild() $scope.decl($world, "xs", arguments[2]) $scope.decl($world, "idx", null) for ($scope.set($world, "idx", 0); $scope.get($world, "idx") < $world.get($scope.get($world, "xs"), "length"); $scope.set($world, "idx", $scope.get($world, "idx") + 1)) $world.send($world.get($scope.get($world, "xs"), $scope.get($world, "idx")), "doYourThing") } })()); (function($world) { $world.set($scope.get($world, "object"), "doYourThing", (function() { var $staticScope = $scope return function($world, $this) { var $scope = $staticScope.makeChild() console.log($this) } })()) })($scope.get($world, "app1")); (function($world) { $world.set($scope.get($world, "object"), "doYourThing", (function() { var $staticScope = $scope return function($world, $this) { var $scope = $staticScope.makeChild() alert($this) } })()) })($scope.get($world, "app2")); $scope.set($world, "objs", {}) $world.set($scope.get($world, "objs"), 0, "first") $world.set($scope.get($world, "objs"), 1, "second") $world.set($scope.get($world, "objs"), 2, "third") $world.set($scope.get($world, "objs"), "length", 3); (function($world) { call($world, $scope.get($world, "doAll"), $scope.get($world, "objs")) })($scope.get($world, "app1")); (function($world) { call($world, $scope.get($world, "doAll"), $scope.get($world, "objs")) })($scope.get($world, "app2")); // TODO: write better examples // TODO: automate this translation scheme