Metacircular dirty checking

We’ve been to few places trying to observe JavaScript values changes automatically. The examples are:

It seems that Object.observe will clear up this field for a good, but why don’t try to test if it’s possible to take different approach using MetaES interceptors.

The idea is to create a function or many functions that will be responsible for updates. For example:

array = [1,2,3];
array.push(4); // here we'd like to be notified that '4' was added
array.splice(0 ,1); // we're of course interested also in removing the element from the array

observedObject.proprerty = 10 // in that case we're interested in getting the triple (observedObject, 'property', 10) as the information about change

delete observedObject.property // we want the (observedObject, 'property') here and some information that it was deleted

I said funcition(s), so let’s wrap the code into one of them preparing the variables upfront:

var array = [1,2,3], observedObject = {};
function observer(){
    array.push(4); 
    array.splice(0 ,1);
    observedObject.proprerty = 10;
    delete observedObject.property;
}

Calling observer(); in standard JavaScript execution will make the changes, but we won’t be notified about them in any way. Let’s make a use of MetaES now. At first use the toMetacircular function:

function toMetacircular(fn) {
    return metaes.evaluate(fn, {});
}

and convert the observer function:

var metacircularObserver = toMetacircular(observer);

Calling metacircularObserver() will throw ReferenceError of course, because the code is executed in context that doesn’t know about existence of array and observedObject. Let’s change it a bit:

function toMetacircular(fn, env) {
    return metaes.evaluate(fn, env);
}

var metacircularObserver = toMetacircular(observer, {array: array, observedObject: observedObject});

metacircularObserver();

Now it works, but still we have no informations about changes. As you remember, there is additional param interceptor, let’s use it:

function toMetacircular(fn, env) {
    function interceptor(e, value, env) {
        console.log(e.type, value);
    }
    return metaes.evaluate(fn, env, {interceptor: interceptor});
}

Just to recall , the result is:

FunctionExpression function observer(){
    array.push(4); 
    array.splice(0 ,1);
    observedObject.proprerty = 10;
    delete observedObject.property;
} VM630:13
ExpressionStatement function observer(){
    array.push(4); 
    array.splice(0 ,1);
    observedObject.proprerty = 10;
    delete observedObject.property;
} VM630:13
Program function observer(){
    array.push(4); 
    array.splice(0 ,1);
    observedObject.proprerty = 10;
    delete observedObject.property;
} VM630:13
Identifier [1, 2, 3] VM630:13
Identifier function push() { [native code] } VM630:13
MemberExpression function push() { [native code] } VM630:13
Literal 4 VM630:13
CallExpression function push() { [native code] } VM630:13
CallExpression 4 VM630:13
ExpressionStatement 4 VM630:13
Identifier [1, 2, 3, 4] VM630:13
Identifier function splice() { [native code] } VM630:13
MemberExpression function splice() { [native code] } VM630:13
Literal 0 VM630:13
Literal 1 VM630:13
CallExpression function splice() { [native code] } VM630:13
CallExpression [1] VM630:13
ExpressionStatement [1] VM630:13
Literal 10 VM630:13
Identifier Object {} VM630:13
Identifier undefined VM630:13
MemberExpression undefined VM630:13
AssignmentExpression 10 VM630:13
ExpressionStatement 10 VM630:13
Identifier Object {proprerty: 10} VM630:13
Identifier undefined VM630:13
MemberExpression undefined VM630:13
UnaryExpression true VM630:13
ExpressionStatement true VM630:13
BlockStatement true VM630:13
FunctionExpression function observer(){
    array.push(4); 
    array.splice(0 ,1);
    observedObject.proprerty = 10;
    delete observedObject.property;
} 

At first it looks messy, but after a while it is easy to see some patterns. The first expression to watch was

array.push(4);

and evaluation of it is here (with my comments):

// array
    Identifier [1, 2, 3] VM630:13 
// push
    Identifier function push() { [native code] } VM630:13 
// array.push
    MemberExpression function push() { [native code] } VM630:13 
// 4    
    Literal 4 VM630:13 
// array.push(...) - the fact of function being called    
    CallExpression function push() { [native code] } VM630:13 
// the result of pushing    
    CallExpression 4 VM630:13 
// the result of whole expression    
    ExpressionStatement 4 VM630:13

I almost forgot about logging e.subProgram property:

(...)
function interceptor(e, value, env) {
    console.log("["+e.type+"]\n\t",e.subProgram, value)
}
(...)

and that gives:

[Identifier]
     array [1, 2, 3] VM881:13
[Identifier]
     undefined function push() { [native code] } VM881:13
[MemberExpression]
     array.push function push() { [native code] } VM881:13
[Literal]
     4 4 VM881:13
[CallExpression]
     array.push(4) function push() { [native code] } VM881:13
[CallExpression]
     array.push(4) 4 VM881:13
[ExpressionStatement]
     array.push(4); 4 VM881:13
[Identifier]
     array [1, 2, 3, 4] VM881:13
[Identifier]
     undefined function splice() { [native code] } VM881:13
[MemberExpression]
     array.splice function splice() { [native code] } VM881:13
[Literal]
     0 0 VM881:13
[Literal]
     1 1 VM881:13
[CallExpression]
     array.splice(0 ,1) function splice() { [native code] } VM881:13
[CallExpression]
     array.splice(0 ,1) [1] VM881:13
[ExpressionStatement]
     array.splice(0 ,1); [1] VM881:13
[Literal]
     10 10 VM881:13
[Identifier]
     observedObject Object {} VM881:13
[Identifier]
     undefined undefined VM881:13
[MemberExpression]
     observedObject.proprerty undefined VM881:13
[AssignmentExpression]
     observedObject.proprerty = 10 10 VM881:13
[ExpressionStatement]
     observedObject.proprerty = 10; 10 VM881:13
[Identifier]
     observedObject Object {proprerty: 10} VM881:13
[Identifier]
     undefined undefined VM881:13
[MemberExpression]
     observedObject.property undefined VM881:13
[UnaryExpression]
     delete observedObject.property true VM881:13
[ExpressionStatement]
     delete observedObject.property; true 

So in case of array.push(4), we’re looking for chain of Identifier, Identifier, MemberExpression, Literal, CallExpression and the array reference will be at index 0 or the found chain. Just for the sake of clarity:

function toMetacircular(fn, env) {
    var chainToLookFor = ['Identifier', 'Identifier', 'MemberExpression', 'Literal', 'CallExpression'],
        currentChain = [],
        indexInChain = 0;
    function interceptor(e, value, env) {
        if(e.type === chainToLookFor[index]){
            currentChain.push({ e: e, value: value });
            indexInChain++;
        }
        if(indexInChain === chainToLookFor.length){
            console.log(currentChain[0].value); // here we have `array` in `array.push`
        }
    }
    return metaes.evaluate(fn, env, {interceptor: interceptor});
}

Probably the implementation of this task could be different and use other observations, but this one is most naive and required the least thinking.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: