Skip to content

Instantly share code, notes, and snippets.

@antimatter15
Last active December 2, 2018 23:43
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save antimatter15/f679ccfc13577d16dab6450c5e9ce738 to your computer and use it in GitHub Desktop.
Dynamic Scoped Javascript
// 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