/* Copyright (c) 2008 Alessandro Warth */ // TODO: make "new world()" and "new scope()" work //$("workspaceForm").source.rows = 42 // THE IDEA: try s1 else s2 // - creates a "possible world" w' that's a "fork" of w, the "current world" (may itself be a possible world) // - evaluates s1 in w' // - if no aborts in s1, commit changes (i.e., merge w and w') // - otherwise, discard possible world and evaluate s2 in current world // (abort only makes sense inside a try) DEBUG = false // 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 // don't get in the way of proper GC. Object.prototype.getID = (function() { var numIds = 0 return function() { return this.hasOwnProperty("_id_") ? this._id_ : this._id_ = "R" + numIds++ } })() 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 abort = {} nothing = {} $worldProto = {} $world = (function() { var deltas = {} return { parent: $worldProto, deltas: deltas, hasOwn: function(r, p) { var id = getID(r) return deltas.hasOwnProperty(id) && deltas[id].hasOwnProperty(p) }, has: function(r, p) { var id = getID(r) return deltas.hasOwnProperty(id) && deltas[id].hasOwnProperty(p) }, get: function(r, p) { if (typeof r === "string" && (typeof p === "number" || p === "length")) return r[p] var id = getID(r) if (DEBUG) console.log("? parent world looking up " + id + "." + p) if (deltas.hasOwnProperty(id) && deltas[id].hasOwnProperty(p)) 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.hasOwnProperty(id)) deltas[id] = {} deltas[id][p] = v return v }, call: function(_, f) { var realF = f arguments[0] = this arguments[1] = undefined return realF.apply(null, arguments) }, send: function(r, p) { var realR = r, realP = p arguments[0] = this arguments[1] = realR if (DEBUG) console.log("send, receiver=" + realR + ", selector=" + realP) return this.get(realR, realP).apply(realR, arguments) }, freeze: function() { this.set = function(r, p, v) { return v } }, commit: function() { }, makeChild: function() { var parentWorld = this, deltas = {} return { parent: this, deltas: deltas, hasOwn: function(r, p) { var id = getID(r) return deltas.hasOwnProperty(id) && deltas[id].hasOwnProperty(p) }, has: function(r, p) { return this.hasOwn(r, p) || parentWorld.has(r, p) }, get: function(r, p) { if (typeof r === "string" && (typeof p === "number" || p === "length")) return r[p] var id = getID(r) if (DEBUG) console.log("? child world looking up " + id + "." + p) return deltas.hasOwnProperty(id) && deltas[id].hasOwnProperty(p) ? 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.hasOwnProperty(id)) deltas[id] = {} deltas[id][p] = v return v }, commit: function() { for (var i in deltas) { if (!deltas.hasOwnProperty(i)) continue for (var p in deltas[i]) { if (!deltas[i].hasOwnProperty(p)) continue if (!parentWorld.deltas.hasOwnProperty(i)) parentWorld.deltas[i] = {} if (DEBUG) console.log("committing " + i + "." + p) parentWorld.deltas[i][p] = deltas[i][p] } } deltas = {} }, // 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 the semantics of freeze; is it possible to get world-specific fields? freeze: function() { this.set = parentWorld.set }, call: parentWorld.call, send: parentWorld.send, makeChild: parentWorld.makeChild, tryElse: parentWorld.tryElse } }, tryElse: function(tb, eb) { var w = this.makeChild(), aborted = false try { return tb(w) } catch (e) { aborted = true if (e === abort) return eb(this) else throw e } finally { if (!aborted) w.commit() } } } })() // "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 $scopeProto = {} $scope = { parent: $scopeProto, hasOwn: function($world, n) { return $world.hasOwn(this, n) }, has: function($world, n) { return $world.has(this, n) }, 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 { parent: this, hasOwn: parent.hasOwn, has: parent.has, get: function($world, n) { return $world.has(this, n) ? $world.get(this, n) : parent.get($world, n) }, set: function($world, n, v) { return $world.has(this, n) ? $world.set(this, n, v) : parent.set($world, n, v) }, decl: parent.decl, makeChild: parent.makeChild } } } // support for "new" // NOTE: these are not implemented as methods in order to be compatible with undefined and null makeChild = function(x) { return {parent: x} } newAndInitialize = function($world, x) { var orig = x arguments[0] = $world arguments[1] = {parent: x} $world.get(orig, "initialize").apply(null, arguments) return arguments[1] } // some globals, etc. $scope.decl($world, "object", Object.prototype) $world.set(Object.prototype, "initialize", function($world, $this) { }) $scope.decl($world, "null", null) $scope.decl($world, "undefined", undefined) $scope.decl($world, "true", true) $scope.decl($world, "false", false) $scope.decl($world, "array", {}); Array.prototype.parent = $scope.get($world, "array") $scope.decl($world, "string", {}); String.prototype.parent = $scope.get($world, "string") $scope.decl($world, "bool", {}); Boolean.prototype.parent = $scope.get($world, "bool") $scope.decl($world, "number", {}); Number.prototype.parent = $scope.get($world, "number") $scope.decl($world, "alert", function($world, $this) { alert(arguments[2]) }) $scope.decl($world, "console", {}) $world.set($scope.get($world, "console"), "log", function($world, $this) { console.log(arguments[2]) }) $scope.decl($world, "worldProto", $worldProto) $world.set($worldProto, "hasOwn", function($world, $this, x, p) { return this.hasOwn(x, p) }) $world.set($worldProto, "has", function($world, $this, x, p) { return this.has(x, p) }) $world.set($worldProto, "get", function($world, $this, x, p) { return this.get(x, p) }) $world.set($worldProto, "set", function($world, $this, x, p, v) { return this.set(x, p, v) }) $world.set($worldProto, "call", function($world, $this, f) { var args = [null] for (var idx = 2; idx < arguments.length; idx++) args.push(arguments[idx]) return this.call.apply(this, args) }) $world.set($worldProto, "send", function($world, $this, r, p) { var args = [] for (var idx = 2; idx < arguments.length; idx++) args.push(arguments[idx]) return this.send.apply(this, args) }) $world.set($worldProto, "commit", function($world, $this) { return this.commit() }) $world.set($worldProto, "freeze", function($world, $this) { return this.freeze() }) $scope.decl($world, "scopeProto", $scopeProto) $world.set($scopeProto, "hasOwn", function($world, $this, x) { return $world.hasOwn(this, x) }) $world.set($scopeProto, "has", function($world, $this, x) { return $world.has(this, x) }) $world.set($scopeProto, "get", function($world, $this, x) { return $world.get(this, x) }) $world.set($scopeProto, "set", function($world, $this, x, v) { return $world.set(this, x, v) }) $world.set($scopeProto, "decl", function($world, $this, x, v) { return this.decl($world, x, v) }) Array.prototype.toWJSArray = function() { var r = newAndInitialize($world, $scope.get($world, "array")) for (var idx = 0; idx < this.length; idx++) $world.set(r, idx, this[idx]) $world.set(r, "length", this.length) return r } Object.prototype.toWJSObject = function() { var r = newAndInitialize($world, $scope.get($world, "object")) for (var p in this) if (this.hasOwnProperty(p)) $world.set(r, p, this[p]) return r }