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

Added error handling for loops, warps, etc. Fixes #50 #51

Merged
merged 8 commits into from
Jan 11, 2018
Merged
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
77 changes: 56 additions & 21 deletions src/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,14 @@ backend.bounceOffEdge = function(node) {
///////////////////// Control /////////////////////
backend.doWarp = function(node) {
let body = this.generateCode(node.inputs[0]);
let afterFn = `callback_${node.id}_${Date.now()}`;
return `new SPromise(${afterFn} => ${callStatementWithArgs(node.type, true)}
const afterFn = `callback_${node.id}_${Date.now()}`;
const reject = `reject_${node.id}_${Date.now()}`;

return `new SPromise((${afterFn}, ${reject}) => ${callStatementWithArgs(node.type, true)}
.then(() => ${body})
.then(() => ${callStatementWithArgs(node.type, false)})
.then(() => ${afterFn}())
.catch(${reject})
)`;

};
Expand All @@ -73,6 +76,8 @@ backend.doWait = function(node) {
var time = this.generateCode(node.inputs[0]),
afterFn = `afterWait_${node.id}`;

// TODO: handle rejections?
// This can only fail if the underlying implementation is faulty...
return [
`new SPromise(${afterFn} => {`,
callStatementWithArgs(node.type, time, afterFn),
Expand Down Expand Up @@ -108,6 +113,7 @@ backend.doReport = function(node) {
// Get the current callback name and call it!
var value = this.generateCode(node.inputs[0]);
let callback = getCallbackName(node);
//let reject = getRejectName(node);
if (!node.parent) throw 'no parent:' + node.type;

if (callback) {
Expand Down Expand Up @@ -168,22 +174,24 @@ backend.fork = function(node) {
};

backend.doRepeat = function(node) {
var count = node.inputs[0] ? this.generateCode(node.inputs[0]) : 0,
let count = node.inputs[0] ? this.generateCode(node.inputs[0]) : 0,
body = this.generateCode(node.inputs[1]),
next = this.generateCode(node.next),
iterVar = node.id,
recurse;

recurse = callStatementWithArgs('doYield', `doLoop_${node.id}`, node.id);

let cond = `${node.id}-- > 0`;
let doLoop = `() => {${body}.then(() => ${recurse})}`;
let callback = `resolve_${node.id}_${Date.now()}`;
let doElse = `() => {${next}.then(() => ${callback}());\n}`;
const cond = `${node.id}-- > 0`;
const callback = `resolve_${node.id}_${Date.now()}`;
const reject = `reject_${node.id}_${Date.now()}`;

let doLoop = `() => {${body}.then(() => ${recurse}).catch(${reject})}`;
let doElse = `() => {${next}.then(() => ${callback}()).then(${reject});\n}`;
loopControl = callStatementWithArgs('doIfElse', cond, doLoop, doElse);

return [
`new SPromise(${callback} => {`,
`new SPromise((${callback}, ${reject}) => {`,
`function doLoop_${node.id} (${node.id}) {`,
`return ${indent(loopControl)};`,
`}`,
Expand All @@ -195,16 +203,18 @@ backend.doRepeat.async = true;

backend.doForever = function(node) {
var recurse = callStatementWithArgs('doYield', `doForever_${node.id}`),
callback = `resolve_${node.id}_${Date.now()}`,
reject = `reject_${node.id}_${Date.now()}`,
body = recurse;

if (node.inputs[0]) {
body = `${this.generateCode(node.inputs[0])}.then(() => ${recurse})`;
body = `${this.generateCode(node.inputs[0])}.then(() => ${recurse}).catch(${reject})`;
} else {
body = recurse;
}

return [
`new SPromise(() => {`,
`new SPromise((${callback}, ${reject}) => {`,
`function doForever_${node.id} () {`,
indent(body),
`}`,
Expand All @@ -218,6 +228,7 @@ backend.doUntil = function(node) {
body = newPromise(),
exitBody = node.next ? this.generateCode(node.next) : newPromise(),
callback = `resolve_${node.id}_${Date.now()}`,
reject = `reject_${node.id}_${Date.now()}`,
iterVar = node.id,
recurse;

Expand All @@ -231,12 +242,12 @@ backend.doUntil = function(node) {
cond = this.generateCode(node.inputs[0]);
}

let execBody = `function() {\n${body}.then(() => ${recurse})}`;
let exitLoop = `function() {\n${exitBody}.then(() => \n${callback}());\n}`;
let execBody = `function() {\n${body}.then(() => ${recurse}).catch(${reject})}`;
let exitLoop = `function() {\n${exitBody}.then(() => \n${callback}()).then(${reject});\n}`;

loopControl = callStatementWithArgs('doIfElse', cond, exitLoop, execBody);
return [
`new SPromise(${callback} => {`,
`new SPromise((${callback}, ${reject}) => {`,
`function doLoop_${node.id} () {`,
`${indent(loopControl)};`,
`}`,
Expand Down Expand Up @@ -290,6 +301,8 @@ backend.doSayFor = function(node) {
msg = this.generateCode(node.inputs[0]),
afterFn = `afterSay_${node.id}`;

// TODO: handle rejections?
// This can only fail if the underlying implementation is faulty...
return [
`new SPromise(${afterFn} => {`,
callStatementWithArgs(node.type, msg, time, afterFn),
Expand All @@ -302,6 +315,8 @@ backend.doThinkFor = function(node) {
msg = this.generateCode(node.inputs[0]),
afterFn = `afterThink_${node.id}`;

// TODO: handle rejections?
// This can only fail if the underlying implementation is faulty...
return [
`new SPromise(${afterFn} => {`,
callStatementWithArgs(node.type, msg, time, afterFn),
Expand Down Expand Up @@ -478,40 +493,60 @@ const TYPES_WITH_CALLBACKS = [
let nodeIdCounter = 1;

// Get the name of the callback fn of the closest enclosing fn definition
const getCallbackName = node => {
const getFnName = (fn, node) => {
if (TYPES_WITH_CALLBACKS.includes(node.type)) {
if (!node.id) {
node.id = `anon_item__${nodeIdCounter++}`;
}
return `callback${node.id.replace(/-/g, '_')}`;
return `${fn}${node.id.replace(/-/g, '_')}`;
}

if (node.parent) return getCallbackName(node.parent);
if (node.parent) return getFnName(fn, node.parent);

return null;
};

const getCallbackName = node => getFnName('callback', node);
const getRejectName = node => getFnName('reject', node);


backend.reifyScript =
backend.reifyReporter =
backend.reifyPredicate = function(node) {
var body = '',
let body = '',
cb = getCallbackName(node),
reject = getRejectName(node),
args = node.inputs[1].inputs
.map(this.generateCode);

// Why is this missing the callback???
if (node.inputs[0]) {
body = this.generateCode(node.inputs[0]);

// Add error handling and callback
let lastChar = body[body.length-1];

let isMissingCallback = !(new RegExp('\\b' + cb + '\\b').test(body));
if (isMissingCallback) {
body = body.replace(/[;\n]*$/, `.then(${cb})`);
}

body = body.replace(/[;\n]*$/, `.catch(${reject})`);
if (lastChar === ';') body += ';';
} else {
body = `${cb}()`;
}

// TODO: add the callback name to the function...
// TODO: doReport should call this callback...
return [
`function(${args.map((e, i) => `a${i}`).join(', ')}${args.length ? ',' : ''}${cb}) {`,
indent(`var context = new VariableFrame(arguments[${args.length+1}] || __CONTEXT);`),
`function(${args.map((e, i) => `a${i}`).join(', ')}) {`,
indent(`return new SPromise((${cb}, ${reject}) => {`),
indent(`var context = new VariableFrame(arguments[${args.length}] || __CONTEXT);`),
indent(`var self = context.get('${CALLER}').value;`),
indent(args.map((arg, index) => `context.set(${arg}, a${index});`).join('\n')),
indent(`__CONTEXT = context;`),
indent(body),
indent(`});`),
`}`
].join('\n');
};
Expand All @@ -526,7 +561,7 @@ backend.autolambda = function(node) {
let callback = getCallbackName(node);

if (callback) {
body = `${callback}(${body});`;
body = `SPromise.resolve().then(() => ${body}).then(${callback});`;
}

return `return ${body};`;
Expand Down
14 changes: 2 additions & 12 deletions src/context/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,24 +463,14 @@ context.reportJoinWords = function() {

context.evaluate = function(fn) {
var args = Array.prototype.slice.call(arguments, 1);
return new SPromise(resolve => {
let cxt = args.pop();
args.push(resolve);
args.push(cxt);
return fn.apply(this, args);
});
return fn.apply(this, args);
};

context.evaluateCustomBlock = function(name, fnVar) {
var args = Array.prototype.slice.call(arguments, 2),
fn = fnVar.value;

return new SPromise(resolve => {
let cxt = args.pop();
args.push(resolve);
args.push(cxt);
return fn.apply(this, args);
});
return fn.apply(this, args);
};

module.exports = context;
1 change: 1 addition & 0 deletions test/compilation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ describe('compilation', function() {

utils.getProjectPaths()
.filter(filename => !filename.includes('all-control'))
//.filter(filename => filename.includes('custom-block-inputs'))
.forEach(filename => {
it(`should nop every operation in ${filename}`, function() {
var content = fs.readFileSync(filename, 'utf8');
Expand Down
2 changes: 1 addition & 1 deletion test/compile-fns.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ describe('functions', function() {
let factory = snap2js.compile(content);
let env = snap2js.newContext();
let fn = factory(env);
fn.call(null, result => {
fn().then(result => {
result.forEach((n, i) => assert.equal(n, i+1));
done();
});
Expand Down
8 changes: 3 additions & 5 deletions test/custom-blocks.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,9 @@ describe('custom blocks', function() {
assert(value(result[index]));
} else if (value instanceof Object) {
fn = result[index];
return fn(value.in, promise => {
promise.then(output => {
assert.equal(output, value.out);
done();
});
return fn(value.in).then(output => {
assert.equal(output, value.out);
done();
});
} else {
assert.equal(result[index], value);
Expand Down
74 changes: 74 additions & 0 deletions test/errors.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
describe('errors', function() {
const snap2js = require('..');
const assert = require('assert');
const utils = require('./utils');
const Q = require('q');

describe('basic', function() {
it('should be able to catch error', function(done) {
let cxt = snap2js.newContext();
let content = utils.getContextXml('list-err');
let bin = snap2js.compile(content);
let fn = bin(cxt);
fn().catch(err => done())
});

it.skip('should return the failing block id', function() {
let cxt = snap2js.newContext();

let content = utils.getContextXml('list-err');
console.log(snap2js.transpile(content));

let bin = snap2js.compile(content);
let fn = bin(cxt);
console.log(fn.toString());
try {
Q(fn())
.catch(err => console.error('error:', err));
console.log('no error');
} catch (e) {
console.log('err', e);
}
});
});

describe('in repeat loop', function() {
it('should be able to catch error', function(done) {
let cxt = snap2js.newContext();
let content = utils.getContextXml('list-err-loop');
let bin = snap2js.compile(content);
let fn = bin(cxt);
fn().catch(err => done())
});
});

describe('in repeat until', function() {
it('should be able to catch error', function(done) {
let cxt = snap2js.newContext();
let content = utils.getContextXml('list-err-until');
let bin = snap2js.compile(content);
let fn = bin(cxt);
fn().catch(err => done())
});
});

describe('in forever', function() {
it('should be able to catch error', function(done) {
let cxt = snap2js.newContext();
let content = utils.getContextXml('list-err-forever');
let bin = snap2js.compile(content);
let fn = bin(cxt);
fn().catch(err => done())
});
});

describe('in warp', function() {
it('should be able to catch error', function(done) {
let cxt = snap2js.newContext();
let content = utils.getContextXml('list-err-warp');
let bin = snap2js.compile(content);
let fn = bin(cxt);
fn().catch(err => done())
});
});
});
24 changes: 9 additions & 15 deletions test/operators.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,20 +177,16 @@ describe('operators', function() {
});

it('should eval true correctly (and+and+not)', function(done) {
result(true, true, false, result => {
result.then(value => {
assert.equal(value, true);
done();
});
result(true, true, false).then(value => {
assert.equal(value, true);
done();
});
});

it('should eval false correctly (and+and+not)', function(done) {
result(true, true, true, result => {
result.then(value => {
assert.equal(value, false);
done();
});
result(true, true, true).then(value => {
assert.equal(value, false);
done();
});
});
});
Expand All @@ -207,11 +203,9 @@ describe('operators', function() {
});

it('should eval correctly (sum inputs)', function(done) {
result(3, 5, promise => {
promise.then(value => {
assert.equal(value, 8);
done();
});
result(3, 5).then(value => {
assert.equal(value, 8);
done();
});
});
});
Expand Down
1 change: 1 addition & 0 deletions test/test-cases/contexts/list-err-forever.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<context id="1"><inputs></inputs><variables></variables><script><block collabId="item_25_2" s="doDeclareVariables"><list><l>a</l></list></block><block collabId="item_26" s="doForever"><script><block collabId="item_25_6" s="doChangeVar"><l>a</l><l>2</l></block><block collabId="item_25_7" s="doAddToList"><l>thing</l><block collabId="item_25_10" var="a"/></block></script></block></script><receiver><sprite name="Sprite" collabId="item_-1" idx="1" x="0" y="0" heading="90" scale="1" rotation="1" draggable="true" costume="0" color="80,80,80" pen="tip" id="17"><costumes><list collabId="" id="18"></list></costumes><sounds><list collabId="" id="19"></list></sounds><variables></variables><blocks></blocks><scripts><script x="183" y="197"><block collabId="item_2" s="getJSFromRPCStruct"><l>Execute</l><l>call</l><block collabId="item_6" s="reifyScript"><script><block collabId="item_8" s="doDeclareVariables"><list><l>a</l></list></block><block collabId="item_13" s="doRepeat"><l>10</l><script><block collabId="item_9" s="doChangeVar"><l>a</l><l>2</l></block><block collabId="item_18" s="doAddToList"><l>thing</l><block collabId="item_19" var="a"/></block></script></block><block collabId="item_7" s="doReport"><block collabId="item_12" var="a"/></block></script><list></list></block></block></script><script x="200" y="427"><block collabId="item_20" s="getJSFromRPCStruct"><l>Execute</l><l>call</l><block collabId="item_20_1" s="reifyScript"><script><block collabId="item_20_2" s="doDeclareVariables"><list><l>a</l></list></block><block collabId="item_20_3" s="doUntil"><block collabId="item_22" s="reportLessThan"><l>10</l><block collabId="item_23" var="a"/></block><script><block collabId="item_20_5" s="doChangeVar"><l>a</l><l>2</l></block><block collabId="item_20_6" s="doAddToList"><l>thing</l><block collabId="item_20_8" var="a"/></block></script></block><block collabId="item_20_4" s="doReport"><block collabId="item_20_7" var="a"/></block></script><list></list></block></block></script><script x="647" y="430"><block collabId="item_25" s="getJSFromRPCStruct"><l>Execute</l><l>call</l><block collabId="item_25_1" s="reifyScript"><script><block collabId="item_25_2" s="doDeclareVariables"><list><l>a</l></list></block><block collabId="item_26" s="doForever"><script><block collabId="item_25_6" s="doChangeVar"><l>a</l><l>2</l></block><block collabId="item_25_7" s="doAddToList"><l>thing</l><block collabId="item_25_10" var="a"/></block></script></block></script><list></list></block></block></script><script x="725.4580098125" y="612.000005"><block collabId="item_25_3" s="doUntil"><block collabId="item_25_5" s="reportLessThan"><l>10</l><block collabId="item_25_8" var="a"/></block><script></script></block><block collabId="item_25_4" s="doReport"><block collabId="item_25_9" var="a"/></block></script></scripts><history></history></sprite></receiver><context id="106"><inputs></inputs><variables></variables><receiver><ref id="17"></ref></receiver></context></context>
1 change: 1 addition & 0 deletions test/test-cases/contexts/list-err-loop.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<context id="1"><inputs></inputs><variables></variables><script><block collabId="item_8" s="doDeclareVariables"><list><l>a</l></list></block><block collabId="item_13" s="doRepeat"><l>10</l><script><block collabId="item_9" s="doChangeVar"><l>a</l><l>2</l></block><block collabId="item_18" s="doAddToList"><l>thing</l><block collabId="item_19" var="a"/></block></script></block><block collabId="item_7" s="doReport"><block collabId="item_12" var="a"/></block></script><receiver><sprite name="Sprite" collabId="item_-1" idx="1" x="0" y="0" heading="90" scale="1" rotation="1" draggable="true" costume="0" color="80,80,80" pen="tip" id="20"><costumes><list collabId="" id="21"></list></costumes><sounds><list collabId="" id="22"></list></sounds><variables></variables><blocks></blocks><scripts><script x="183" y="197"><block collabId="item_2" s="getJSFromRPCStruct"><l>Execute</l><l>call</l><block collabId="item_6" s="reifyScript"><script><block collabId="item_8" s="doDeclareVariables"><list><l>a</l></list></block><block collabId="item_13" s="doRepeat"><l>10</l><script><block collabId="item_9" s="doChangeVar"><l>a</l><l>2</l></block><block collabId="item_18" s="doAddToList"><l>thing</l><block collabId="item_19" var="a"/></block></script></block><block collabId="item_7" s="doReport"><block collabId="item_12" var="a"/></block></script><list></list></block></block></script></scripts><history></history></sprite></receiver><context id="51"><inputs></inputs><variables></variables><receiver><ref id="20"></ref></receiver></context></context>
Loading