Last active
December 2, 2018 23:43
-
-
Save antimatter15/f679ccfc13577d16dab6450c5e9ce738 to your computer and use it in GitHub Desktop.
Dynamic Scoped Javascript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Part I: The Magic | |
// The crux of this are two methods: pushStackTokens and readStackTokens | |
// They form the primitives for manipulating the Javascript VM's call stack | |
// pushStackTokens allows us to inject information (tokens) into the call stack | |
// readStackTokens allows us to retrieve all the stack tokens in the | |
// current call stack. | |
function pushStackTokens(tokens, fn, ...args){ | |
tokens.forEach(tok => console.assert(/^\w+$/.test(tok), | |
'Stack tokens must be alphanumeric')); | |
let id = '_dyn_' + tokens.map(k => '$$' + k + '$$').join('_') | |
return eval('(function ' + id + '(fn){return fn.apply(this, [].slice.call(arguments, 1))})') | |
.apply(this, [fn, ...args]) | |
} | |
function readStackTokens(){ | |
let tokens = []; | |
(new Error()).stack.replace(/_dyn_\$\$(\w+)\$\$/g, (all, id) => | |
tokens.push(id)); | |
return tokens; | |
} | |
// Part II: The Basics | |
// Using the primitives in Part I, we define the high level methods | |
// for manipulating the dynamic scope and storing information outside | |
// of the VM call stack. | |
// withDynamic allows us to inject variables into the dynamically | |
// scoped environment. | |
// lookupDynamic allows us to look up a variable with a particular | |
// name in the dynamically scoped environment. It ascends the call | |
// stack until it finds the first matching scope and returns. | |
var dynamicScopes = {} | |
function withDynamic(vars, fn){ | |
let id = Math.random().toString(36).slice(3) | |
dynamicScopes[id] = vars; | |
return pushStackTokens.call(this, [ id ], fn) | |
} | |
function lookupDynamic(name){ | |
for(let tok of readStackTokens()) | |
if(dynamicScopes[tok][name]) | |
return dynamicScopes[tok][name]; | |
} | |
// Part III: Handling Asynchronous Primitives | |
// Native methods like setTimeout naturally destroy information about | |
// the call stack. That is, when the callback gets invoked, the call stack | |
// is fresh and contains no information about the function which invoked | |
// the original function. | |
// In this section we define a withDynamicCallbacks function which | |
// creates a wrapped version of any function that restores the stack | |
// tokens whenever a callback is finally invoked | |
function withDynamicCallbacks(fn){ | |
if(fn.__orig) fn = fn.__orig; | |
let wrapped = function (...args) { | |
let tokens = readStackTokens() | |
return fn.apply(this, args.map(cb => typeof cb !== 'function' ? cb : | |
function(...cbArgs) { | |
return pushStackTokens.call(this, tokens, cb, ...cbArgs) | |
})) | |
} | |
wrapped.__orig = fn; | |
return wrapped; | |
} | |
window.setTimeout = withDynamicCallbacks(setTimeout) | |
// Example Usage | |
withDynamic({hi: 'whats up dog'}, function(){ | |
setTimeout(function(){ | |
console.log('first thing', lookupDynamic('hi')) | |
}, 10) | |
}) | |
withDynamic({hi: 'not a dog'}, function(){ | |
setTimeout(function(){ | |
console.log('no conflict closure scope', lookupDynamic('hi')) | |
}, 10) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment