diff --git a/docs/examples/baconjs/index.js b/docs/examples/baconjs/index.js new file mode 100644 index 0000000..0b84f10 --- /dev/null +++ b/docs/examples/baconjs/index.js @@ -0,0 +1,24 @@ +// external +import {} from 'https://unpkg.com/jquery' +// external +import {fromEvent} from 'https://unpkg.com/baconjs?module' + +const upEl = globalThis.document.createElement('button') +const downEl = globalThis.document.createElement('button') +const counterEl = globalThis.document.createElement('div') + +globalThis.document.body.appendChild(upEl) +globalThis.document.body.appendChild(downEl) +globalThis.document.body.appendChild(counterEl) + +const up = fromEvent(upEl, 'click'); +const down = fromEvent(downEl, 'click'); + +const counter = + // map up to 1, down to -1 + up.map(1).merge(down.map(-1)) + // accumulate sum + .scan(0, (x,y) => x + y); + +// assign observable value to jQuery property text +counter.onValue(text => Object.assign(counterEl, {innerText: text})); diff --git a/src/editor/value_explorer.js b/src/editor/value_explorer.js index 732b4e5..0bc684b 100644 --- a/src/editor/value_explorer.js +++ b/src/editor/value_explorer.js @@ -6,6 +6,10 @@ import {el, stringify, scrollIntoViewIfNeeded} from './domutils.js' + +// TODO only test for globalThis. because we only eval code in +// another window + const has_custom_toString = object => object.toString != globalThis.run_window.Object.prototype.toString && @@ -16,6 +20,11 @@ const isError = object => || object instanceof globalThis.run_window.Error +const isPromise = object => + object instanceof Promise + || + object instanceof globalThis.run_window.Promise + const displayed_entries = object => { if(Array.isArray(object)) { return object.map((v, i) => [i, v]) @@ -39,6 +48,8 @@ export const stringify_for_header = v => { } else if(type == 'function') { // TODO clickable link, 'fn', cursive return 'fn ' + v.name + } else if (isPromise(v)) { + return 'Promise<>' } else if(isError(v)) { return v.toString() } else if(type == 'object') { @@ -70,7 +81,9 @@ export const header = object => { } else if(object == null) { return 'null' } else if(typeof(object) == 'object') { - if(isError(object)) { + if(isPromise(object)) { + return 'Promise<>' + } else if(isError(object)) { return object.toString() } else if(Array.isArray(object)) { return '[' diff --git a/src/effects.js b/src/effects.js index 991cf9b..df25651 100644 --- a/src/effects.js +++ b/src/effects.js @@ -8,6 +8,7 @@ import { import {current_cursor_position} from './calltree.js' import {FLAGS} from './feature_flags.js' import {exec, FILES_ROOT} from './index.js' +import {unwrap_settled_promises} from './unwrap_promises.js' // Imports in the context of `run_window`, so global variables in loaded // modules refer to that window's context @@ -128,7 +129,7 @@ export const render_initial_state = (ui, state) => { ui.editor.switch_session(state.current_module) } -export const render_common_side_effects = (prev, next, command, ui) => { +export const render_common_side_effects = async (prev, next, command, ui) => { if( prev.project_dir != next.project_dir || @@ -187,6 +188,8 @@ export const render_common_side_effects = (prev, next, command, ui) => { } else { + await unwrap_settled_promises(next.calltree) + if( prev.calltree == null || diff --git a/src/eval.js b/src/eval.js index 1028ac9..bed1b17 100644 --- a/src/eval.js +++ b/src/eval.js @@ -73,7 +73,7 @@ const codegen_function_expr = (node, cxt) => { const args = node.function_args.children.map(do_codegen).join(',') - const call = `(${args}) => ` + ( + const call = (node.is_async ? 'async ' : '') + `(${args}) => ` + ( (node.body.type == 'do') ? '{' + do_codegen(node.body) + '}' : '(' + do_codegen(node.body) + ')' @@ -384,7 +384,17 @@ export const eval_modules = ( try { value = fn(...args) ok = true - return value + return value instanceof Promise.Original + ? value + .then(v => { + value.status = {ok: true, value: v} + return v + }) + .catch(e => { + value.status = {ok: false, error: e} + throw e + }) + : value } catch(_error) { ok = false error = _error @@ -917,6 +927,10 @@ const do_eval_frame_expr = (node, scope, callsleft) => { value = typeof(expr.result.value) } else if(node.operator == '-') { value = - expr.result.value + } else if(node.operator == 'await') { + log('expr', expr.result.value.status) + value = expr.result.value + //throw new Error('not implemented') } else { throw new Error('unknown op') } diff --git a/src/find_definitions.js b/src/find_definitions.js index 86f9521..5ce41d1 100644 --- a/src/find_definitions.js +++ b/src/find_definitions.js @@ -243,6 +243,7 @@ code analysis: - cannot import names that are not exported from modules - module can be imported either as external or regular - cannot return from modules (even inside toplevel if statements) +- await only in async fns */ export const analyze = (node, is_toplevel = true) => { // TODO remove diff --git a/src/parse_js.js b/src/parse_js.js index 4bc78c3..59d8b83 100644 --- a/src/parse_js.js +++ b/src/parse_js.js @@ -908,6 +908,8 @@ const object_literal = const function_expr = if_ok( seq([ + optional(literal('async')), + either( // arguments inside braces list_destructuring(['(', ')'], 'function_args'), @@ -934,7 +936,7 @@ const function_expr = ]), ({value, ...node}) => { - const [args, _, body] = value + const [is_async, args, _, body] = value const function_args = args.type == 'identifier' ? { ...args, @@ -950,6 +952,7 @@ const function_expr = return { ...node, type: 'function_expr', + is_async: is_async != null, body, children: [function_args, body] } @@ -1029,6 +1032,7 @@ const expr = unary('!'), unary('-'), unary('typeof'), + unary('await'), binary(['**']), binary(['*','/','%']), binary(['+','-']), diff --git a/src/patch_promise.js b/src/patch_promise.js new file mode 100644 index 0000000..1c1a4b6 --- /dev/null +++ b/src/patch_promise.js @@ -0,0 +1,40 @@ +export const patch_promise = window => { + + // TODO check that it is not already patched + if(window.Promise.Original != null) { + throw new Error('already patched') + } + + class PromiseWithStatus extends Promise { + constructor(fn) { + let status + let is_constructor_finished = false + super( + (resolve, reject) => { + fn( + (value) => { + status = {ok: true, value} + if(is_constructor_finished) { + this.status = status + } + resolve(value) + }, + (error) => { + status = {ok: false, error} + if(is_constructor_finished) { + this.status = status + } + reject(error) + }, + ) + } + ) + is_constructor_finished = true + this.status = status + } + } + + PromiseWithStatus.Original = Promise + + window.Promise = PromiseWithStatus +} diff --git a/src/unwrap_promises.js b/src/unwrap_promises.js new file mode 100644 index 0000000..3f140aa --- /dev/null +++ b/src/unwrap_promises.js @@ -0,0 +1,59 @@ +export const unwrap_settled_promises = calltree => { + let is_finished = false + + const unwrap = call => { + // TODO use run_window.Promise + + if(!call.ok) { + return + } + + if(call.value instanceof Promise) { + call.value + .then(value => { + if(is_finished) { + return + } + call.unwrapped_value = {ok: true, value} + }) + .catch(error => { + if(is_finished) { + return + } + call.unwrapped_value = {ok: false, error} + }) + } + } + + const unwrap_tree = call => { + unwrap(call) + if(call.children != null) { + for(let c of call.children) { + unwrap(c) + } + } + } + + unwrap_tree(calltree) + + return Promise.resolve().then(() => { + is_finished = true + return calltree + }) +} + +/* +const delay = new Promise(resolve => setTimeout(() => resolve('123'), 1000)) + +const tree = { + value: Promise.resolve('resolved'), + ok: true, + children: [ + {value: delay, ok: true} + ] +} + +await unwrap_settled_promises(tree) +console.log('tree', tree) +*/ + diff --git a/test/test.js b/test/test.js index d873518..afaf7d0 100644 --- a/test/test.js +++ b/test/test.js @@ -2588,4 +2588,16 @@ const y = x()` // must be discarded assert_equal(get_deferred_calls(result), null) }), + + test_only('async/await', () => { + const code = ` + const x = async () => 123 + const y = async () => await x() + y() + ` + const s = test_initial_state(code) + const move = COMMANDS.move_cursor(s, code.indexOf('await x()')).state + log('m', root_calltree_node(move).children[0].children[0].value) + //log(s.parse_result.modules['']) + }), ] diff --git a/test/utils.js b/test/utils.js index 78c2d77..796e5d9 100644 --- a/test/utils.js +++ b/test/utils.js @@ -2,6 +2,11 @@ import {parse, print_debug_node, load_modules} from '../src/parse_js.js' import {eval_tree, eval_frame} from '../src/eval.js' import {COMMANDS} from '../src/cmd.js' +// external +import {patch_promise} from '../src/patch_promise.js' + +patch_promise(globalThis) + Object.assign(globalThis, {log: console.log}) export const parse_modules = (entry, modules) => diff --git a/tt.js b/tt.js new file mode 100644 index 0000000..879eed0 --- /dev/null +++ b/tt.js @@ -0,0 +1,63 @@ +const x = async () => 1 + +const trace = fn => { + try { + const value = fn() + if(value instanceof Promise) { + return value + .then(v => { + value.status = {ok: true, value: v} + return v + }) + .catch(e => { + value.status = {ok: false, error: e} + throw e + }) + } else { + return value + } + } catch(e) { + + } finally { + } +} + +//trace(x) + +//const c = () => { +// return Promise.reject(1) +//} +// +//const b = () => { +// return c() +// .then(value => { +// console.log('v', value) +// return value +// }) +// .catch(e => { +// console.log('e') +// throw e +// }) +//} +// +// +//try { +// console.log(b()/*.catch(x => 1)*/) +//} catch(e) { +// console.log('error', e) +//} + +const throws_p = () => {throw Promise.reject('err')} + + +try { + await throws_p() +} catch(e) { + console.log('e', e) + try { + await e + } catch(e2) { + + console.log('e2', e2) + } +} diff --git a/x.js b/x.js new file mode 100644 index 0000000..1088838 --- /dev/null +++ b/x.js @@ -0,0 +1,29 @@ +/* +const delay = new Promise(resolve => { + setTimeout(resolve, 1000) +}) +await delay +console.log('x') +export const x = 1 +*/ + +const p = {then: y => y(3)} + +async function test() { + return await p +} + + // TODO remove + //const x = new Promise((resolve, reject) => resolve(10)) + //const x = Promise.reject(10) + //const x = Promise.resolve(10) + //console.log('x', x.status) + //x.catch(e => { + // console.log('x', x.status) + //}) + //const x = new Promise((resolve, reject) => setTimeout(() => resolve(10), 1000)) + //console.log('x', x.status) + //x.then(() => { + // console.log('x', x.status) + //}) +console.log(await test())