Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add native callbacks. #102

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions demos/callback.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>JS-Interpreter Callback Demo</title>
<link href="style.css" rel="stylesheet" type="text/css">
<script src="../acorn.js"></script>
<script src="../interpreter.js"></script>
<script>
var myInterpreter;
function initAlert(interpreter, scope) {
var wrapper = function(text) {
text = text ? text.toString() : '';
return interpreter.createPrimitive(alert(text));
};
interpreter.setProperty(scope, 'alert',
interpreter.createNativeFunction(wrapper));

var wrapper = function(href, callback) {
href = href ? href.toString() : '';
var req = new XMLHttpRequest();
req.open('GET', href, true);
req.onreadystatechange = function() {
if (req.readyState == 4 && req.status == 200) {
interpreter.queueCall(callback, [interpreter.createPrimitive(req.responseText)]);
interpreter.run();
}
};
req.send(null);
};
interpreter.setProperty(scope, 'getXhr',
interpreter.createNativeFunction(wrapper));
};

function parseButton() {
var code = document.getElementById('code').value
myInterpreter = new Interpreter(code, initAlert);
disable('');
}

function stepButton() {
if (myInterpreter.stateStack.length) {
var node =
myInterpreter.stateStack[myInterpreter.stateStack.length - 1].node;
var start = node.start;
var end = node.end;
} else {
var start = 0;
var end = 0;
}
createSelection(start, end);
try {
var ok = myInterpreter.step();
} finally {
if (!ok) {
disable('disabled');
}
}
}

function runButton() {
disable('disabled');
myInterpreter.run();
}

function disable(disabled) {
document.getElementById('stepButton').disabled = disabled;
document.getElementById('runButton').disabled = disabled;
}

function createSelection(start, end) {
var field = document.getElementById('code')
if (field.createTextRange) {
var selRange = field.createTextRange();
selRange.collapse(true);
selRange.moveStart('character', start);
selRange.moveEnd('character', end);
selRange.select();
} else if (field.setSelectionRange) {
field.setSelectionRange(start, end);
} else if (field.selectionStart) {
field.selectionStart = start;
field.selectionEnd = end;
}
field.focus();
}
</script>
</head>
<body>
<h1>JS-Interpreter Callback Demo</h1>

<p>A native function can take a callback as an argument, and call it later
to create real asynchronous scripts. The example below (see this page's
source) creates a <code>getXhr</code> function that fetches the URL and
calls the given callback, returning the content.</p>

<p>This function is defined using <code>createNativeFunction</code> during
initialization. When called, the interpreter passes the provided callback
function as a pseudo object representing all the data needed to execute
it later. The Xhr is then intialized and waits for the targeted URL's content.
The execution of the interpreter's program continues normally. When the
Xhr responds, the callback is loaded in the interpeter, and all it's arguments
are provided using <code>queueCall</code>. Then the interpreter is re-started
by calling <code>run</code>, and the callback function is executed within the
interpreter.</p>

<p>Click <em>Parse</em>, then either click <em>Step</em> repeatedly,
or click <em>Run</em> once. Open your browser's console for errors.</p>

<p><textarea id="code">
getXhr('async.txt', function(value) {
alert(value);
});
</textarea><br>
<button onclick="parseButton()">Parse</button>
<button onclick="stepButton()" id="stepButton" disabled="disabled">Step</button>
<button onclick="runButton()" id="runButton" disabled="disabled">Run</button>
</p>

<p>Back to the <a href="../docs.html">JS-Interpreter documentation</a>.</p>

<script>
disable('disabled');
if (location.protocol == 'file:' &&
navigator.userAgent.indexOf('Firefox') == -1) {
alert('Warning: This page is loaded with the "file:" protocol.\n' +
'Your browser might prevent XHR from working here.')
}
</script>
</body>
</html>
22 changes: 22 additions & 0 deletions docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,28 @@ <h2>External API</h2>
<p>For a working example, see the
<a href="demos/async.html">async demo</a>.</p>

<p>Alternatively, a callback can be provided to make asychronous calls. The
<code>getXhr(url)</code> function can therefore be written like this :</p>

<pre>
var wrapper = function(href, callback) {
href = href ? href.toString() : '';
var req = new XMLHttpRequest();
req.open('GET', href, true);
req.onreadystatechange = function() {
if (req.readyState == 4 && req.status == 200) {
interpreter.queueCall(callback, [interpreter.createPrimitive(req.responseText)]);
interpreter.run();
}
};
req.send(null);
};
interpreter.setProperty(scope, 'getXhr',
interpreter.createNativeFunction(wrapper));
</pre>

<p>See the <a href="demos/callback.html">callback demo</a>.</p>

<h2>Limitations</h2>

<p>The version of JavaScript implemented by the interpreter has a few
Expand Down
35 changes: 35 additions & 0 deletions interpreter.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,40 @@ Interpreter.prototype.appendCode = function(code) {
state.done = false;
};

/**
* Shifts the given function at the bottom of the state stack, delaying the call.
* @param {Interpreter.Object} func Pseudo function to call.
* @param {Interpreter.Object[]} args Arguments to provide to the function.
*/
Interpreter.prototype.queueCall = function(func, args) {
var state = this.stateStack[0];
var interpreter = this;
if (!state || state.node.type != 'Program') {
throw Error('Expecting original AST to start with a Program node.');
}
state.done = false;
var scope = this.createScope(func.node.body, func.parentScope);
func.node.params.forEach(function(p, i) {
interpreter.setProperty(scope, interpreter.createPrimitive(p.name), args[i]);
})
var argsList = this.createObject(this.ARRAY);
args.forEach(function(arg, i) {
interpreter.setProperty(argsList, interpreter.createPrimitive(i), arg);
})
this.setProperty(scope, 'arguments', argsList);
var last = func.node.body.body[func.node.body.body.length - 1];
if(last.type == 'ReturnStatement') {
last.type = 'ExpressionStatement';
last.expression = last.argument;
delete last.argument;
}
this.stateStack.splice(1, 0, {
node: func.node.body,
scope: scope,
value: this.getScope().strict ? this.UNDEFINED : this.global
});
};

/**
* Execute one step of the interpreter.
* @return {boolean} True if a step was executed, false if no more instructions.
Expand Down Expand Up @@ -3738,3 +3772,4 @@ Interpreter.prototype['createAsyncFunction'] =
Interpreter.prototype.createAsyncFunction;
Interpreter.prototype['step'] = Interpreter.prototype.step;
Interpreter.prototype['run'] = Interpreter.prototype.run;
Interpreter.prototype['queueCall'] = Interpreter.prototype.queueCall;