diff --git a/src/lips.js b/src/lips.js index 38b979b8e..18eb4c6c5 100644 --- a/src/lips.js +++ b/src/lips.js @@ -3394,7 +3394,9 @@ function macro_expand(single) { if (node instanceof Pair) { return node.car.valueOf(); } - throw new Error('macroexpand: Invalid let binding'); + const t = type(node); + const msg = `macroexpand: Invalid let binding expectig pair got ${t}`; + throw new Error(msg); })]; } function is_macro(name, value) { @@ -4295,7 +4297,7 @@ function is_promise(o) { if (o instanceof Promise) { return true; } - return o && is_function(o.then); + return !!o && is_function(o.then); } // ---------------------------------------------------------------------- function is_undef(value) { @@ -6687,6 +6689,11 @@ function LipsError(message, args) { LipsError.prototype = new Error(); LipsError.prototype.constructor = LipsError; // ------------------------------------------------------------------------- +// :: Fake exception to handle try catch to break the execution +// :: of body expression #163 +// ------------------------------------------------------------------------- +class IgnoreException extends Error { } +// ------------------------------------------------------------------------- // :: Environment constructor (parent and name arguments are optional) // ------------------------------------------------------------------------- function Environment(obj, parent, name) { @@ -9014,7 +9021,7 @@ var global_env = new Environment({ // ------------------------------------------------------------------ 'try': doc(new Macro('try', function(code, { use_dynamic, error }) { return new Promise((resolve, reject) => { - var catch_clause, finally_clause; + let catch_clause, finally_clause, body_error; if (LSymbol.is(code.cdr.car.car, 'catch')) { catch_clause = code.cdr.car; if (code.cdr.cdr instanceof Pair && @@ -9027,11 +9034,20 @@ var global_env = new Environment({ if (!(finally_clause || catch_clause)) { throw new Error('try: invalid syntax'); } - let next = resolve; + function finalize(result) { + resolve(result); + throw new IgnoreException('[CATCH]'); + } + let next = (result, next) => { + next(result); + } if (finally_clause) { next = function(result, cont) { // prevent infinite loop when finally throw exception next = reject; + args.error = (e) => { + throw e; + }; unpromise(evaluate(new Pair( new LSymbol('begin'), finally_clause.cdr @@ -9045,33 +9061,45 @@ var global_env = new Environment({ use_dynamic, dynamic_env: this, error: (e) => { + body_error = true; var env = this.inherit('try'); if (catch_clause) { - env.set(catch_clause.cdr.car.car, e); + const name = catch_clause.cdr.car.car; + if (!(name instanceof LSymbol)) { + throw new Error('try: invalid syntax: catch require variable name'); + } + env.set(name, e); + let catch_error; var args = { env, - error + use_dynamic, + dynamic_env: this, + error: (e) => { + catch_error = true; + reject(e); + throw new IgnoreException('[CATCH]'); + } }; - args.dynamic_env = this; - unpromise(evaluate(new Pair( + const value = evaluate(new Pair( new LSymbol('begin'), catch_clause.cdr.cdr - ), args), function(result) { - next(result, resolve); + ), args); + unpromise(value, function handler(result) { + if (!catch_error) { + next(result, finalize); + } }); } else { - next(e, error); + next(undefined, () => { + throw e; + }); } } }; - let result = evaluate(code.car, args); - if (is_promise(result)) { - result.then(result => { - next(result, resolve); - }).catch(args.error); - } else { + const value = evaluate(code.car, args); + unpromise(value, function(result) { next(result, resolve); - } + }, args.error); }); }), `(try expr (catch (e) code)) (try expr (catch (e) code) (finally code)) @@ -9731,7 +9759,8 @@ function nodeModuleFind(dir) { function is_node() { return typeof global !== 'undefined' && global.global === global; } - +// ------------------------------------------------------------------------- +const noop = () => {}; // ------------------------------------------------------------------------- async function node_specific() { const { createRequire } = await import('mod' + 'ule'); @@ -9780,6 +9809,15 @@ async function node_specific() { }, `(require module) Function used inside Node.js to import a module.`)); + + // ignore exceptions that are catched elsewhere. This is needed to fix AVA + // reporting unhandled rejections for try..catch + // see: https://github.com/avajs/ava/discussions/3289 + process.on('unhandledRejection', (reason, promise) => { + if (reason instanceof IgnoreException) { + promise.catch(noop); + } + }); } // ------------------------------------------------------------------------- /* c8 ignore next 11 */ @@ -10025,15 +10063,26 @@ function evaluate_macro(macro, code, eval_args) { } return quote(result); } - var value = macro.invoke(code, eval_args); - return unpromise(resolve_promises(value), function ret(value) { - if (!value || value && value[__data__] || self_evaluated(value)) { - return value; - } else { - return unpromise(evaluate(value, eval_args), finalize); + try { + var value = macro.invoke(code, eval_args); + return unpromise(resolve_promises(value), function ret(value) { + if (!value || value && value[__data__] || self_evaluated(value)) { + return value; + } else { + return unpromise(evaluate(value, eval_args), finalize); + } + }, error => { + if (!(error instanceof IgnoreException)) { + throw error; + } + }); + } catch (error) { + if (!(error instanceof IgnoreException)) { + throw error; } - }); + } } + // ------------------------------------------------------------------------- function prepare_fn_args(fn, args) { if (is_bound(fn) && !is_object_bound(fn) && @@ -10198,8 +10247,7 @@ class Continuation { } } } -// ------------------------------------------------------------------------- -const noop = () => {}; + // ------------------------------------------------------------------------- function evaluate(code, { env, dynamic_env, use_dynamic, error = noop, ...rest } = {}) { try { @@ -10335,7 +10383,9 @@ function exec_collect(collect_callback) { e.__code__.push(code.toString(true)); } } - throw e; + if (!(e instanceof IgnoreException)) { + throw e; + } } }); results.push(collect_callback(code, await value)); diff --git a/tests/core.scm b/tests/core.scm index d53261069..601f91d0f 100644 --- a/tests/core.scm +++ b/tests/core.scm @@ -281,14 +281,17 @@ (t.is result 10)))) (t.is (await p) 10)))) -(test "core: quoted promise repr" +(test "core: quoted resolved promise repr" (lambda (t) (let ((resolve)) (define promise '>(new Promise (lambda (r) (set! resolve r)))) (t.is (repr promise) "#") (resolve "xx") (t.is (await promise) "xx") - (t.is (repr promise) "#")) + (t.is (repr promise) "#")))) + +(test "core: quoted rejected promise repr" + (lambda (t) (let ((reject)) (define promise '>(new Promise (lambda (_ r) (set! reject r)))) (t.is (repr promise) "#") @@ -297,6 +300,30 @@ (t.is (repr promise) "#") (t.is (not (null? (promise.__reason__.message.match #/ZONK/))) true)))) +(test "core: quoted promise + lexical scope" + (lambda (t) + (let ((x (await (let ((x 2)) + (--> '>(Promise.resolve (let ((y 4)) + (+ x y))) + (then (lambda (x) + (* x x)))))))) + (t.is x 36)))) + +(test "core: resolving promises in quoted promise realm" + (lambda (t) + (t.is (await (let ((x 2)) + (--> '>(let ((y (Promise.resolve 4))) + (+ x y)) + (then (lambda (x) + (* x x)))))) + 36))) + +(test "core: promise + let" + (lambda (t) + (let ((x (Promise.resolve 2)) + (y (Promise.resolve 4))) + (t.is (* x y) (Promise.resolve 8))))) + (test "core: Promise.all on quoted promises" (lambda (t) (let ((expected #(10 20)) @@ -326,53 +353,58 @@ (test "core: try..catch" (lambda (t) - (let ((x)) - (t.is (try 10 (finally (set! x 10))) 10) - (t.is x 10)) + (begin + (let ((x)) + (t.is (try 10 (finally (set! x 10))) 10) + (t.is x 10)) + + (let ((x)) + (t.is (try aa (catch (e) false) (finally (set! x 10))) false) + (t.is x 10)) - (let ((x)) - (t.is (try aa (catch (e) false) (finally (set! x 10))) false) - (t.is x 10)) + (let ((x 10)) + (t.is (to.throw (try 10 (finally (throw "error") (set! x 20)))) true) + (t.is x 10)) - (t.is (to.throw (try bb (catch (e) (throw e)))) true) + (t.is (to.throw (try bb (catch (e) (throw e)))) true) - (let ((x)) - (t.is (to.throw (try cc (finally (set! x 10)))) true) - (t.is x 10)) + (let ((x)) + (t.is (to.throw (try cc (finally (set! x 10)))) true) + (t.is x 10)) - (let ((x)) - (t.is (try (new Promise (lambda (r) (r 10))) (finally (set! x 10))) 10) - (t.is x 10)) + (let ((x)) + (t.is (try (new Promise (lambda (r) (r 10))) (finally (set! x 10))) 10) + (t.is x 10)) - (let ((x)) - (t.is (to.throw (try (Promise.reject 10) (catch (e) (set! x 10) (throw e)))) true) - (t.is x 10)) + (let ((x)) + (t.is (to.throw (try (Promise.reject 10) (catch (e) (set! x 10) (throw e)))) true) + (t.is x 10)) - (t.is (try xx (catch (e) false)) false) + (t.is (try xx (catch (e) false)) false) - (let ((x)) - (t.is (try (Promise.reject 10) (catch (e) e) (finally (set! x 10))) 10) - (t.is x 10)) + (let ((x)) + (t.is (try (Promise.reject 10) (catch (e) e) (finally (set! x 10))) 10) + (t.is x 10)) - (t.is (try (Promise.reject 10) (catch (e) e)) 10) + (t.is (try (Promise.reject 10) (catch (e) e)) 10) - (t.is (to.throw (try (Promise.reject 10) (catch (e) (throw e)))) true) + (t.is (to.throw (try (Promise.reject 10) (catch (e) (throw e)))) true) - (let ((x)) - (t.is (to.throw (try (Promise.reject 10) (finally (set! x 10))))true) - (t.is x 10)))) + (let ((x)) + (t.is (to.throw (try (Promise.reject 10) (finally (set! x 10)))) true) + (t.is x 10))))) -(test.failing "core: try..catch should stop execution" - (lambda (t) - (let ((result #f)) - (try - (begin - (set! result 1) - (throw 'ZONK) - (set! result 2)) - (catch (e) - (set! result 3))) - (t.is result 3)))) +(test "core: try..catch should stop execution #163" + (lambda (t) + (let ((result #f)) + (try + (begin + (set! result 1) + (throw 'ZONK) + (set! result 2)) + (catch (e) + (set! result 3))) + (t.is result 3)))) (test "core: chain of promises" (lambda (t) @@ -469,6 +501,13 @@ (lambda (t) (t.is (to.throw ((Promise.resolve 'list) 1 2 3)) true))) +(test "core: should catch quoted promise rejection" + (lambda (t) + (t.is (await (--> '>(Promise.reject 10) + (catch (lambda (e) + #t)))) + #t))) + (test "core: should clone list" (lambda (t) (let* ((a '(1 2 3)) (b (clone a))) diff --git a/tests/test.js b/tests/test.js index 63beb483e..aa09af98a 100644 --- a/tests/test.js +++ b/tests/test.js @@ -35,16 +35,16 @@ get_files().then(filenames => { return Promise.all(filenames.map(function(file) { return readFile(`tests/${file}`, 'utf8'); })).then(async function (files) { - await exec(` - (let-env lips.env.__parent__ - (load "./dist/std.xcb") - (load "./tests/helpers/helpers.scm")) - (define test (require "ava")) - `); - return exec(files.join('\n\n')); - }).catch(e => { - console.error(e.message); - console.error(e.stack); - }); + await exec(` + (let-env lips.env.__parent__ + (load "./dist/std.xcb") + (load "./tests/helpers/helpers.scm")) + (define test (require "ava")) + `); + return exec(files.join('\n\n')); + }); +}).catch(e => { + console.error(e.message); + console.error(e.stack); });