From f03d97ee1e82a6bd7d93e01f0e3c836223e675cc Mon Sep 17 00:00:00 2001 From: dmitry-vsl Date: Thu, 12 Dec 2024 14:54:01 +0000 Subject: [PATCH] deploy: leporello-js/app@4dc1f7d21acba7423e8dc66e19cd2b0d57b95747 --- .eslintrc.json | 22 - .github/FUNDING.yml | 3 - .github/workflows/static.yml | 44 - .github/workflows/test.yml | 25 - .gitignore | 2 - .nojekyll | 0 README.md | 4 +- docs/examples/domevents.js | 35 + gen_module_preload.sh | 6 - index.html | 66 +- package.json | 3 - service_worker.js | 2 +- src/analyze_versioned_let_vars.js | 153 - src/ast_utils.js | 208 -- src/calltree.js | 973 ------ src/canvas.js | 144 - src/cmd.js | 938 ----- src/color.js | 255 -- src/editor/calltree.js | 242 -- src/editor/domutils.js | 118 - src/editor/editor.js | 641 ---- src/editor/files.js | 262 -- src/editor/io_trace.js | 66 - src/editor/logs.js | 98 - src/editor/share_dialog.js | 92 - src/editor/ui.js | 363 -- src/editor/value_explorer.js | 304 -- src/effects.js | 352 -- src/eval.js | 1589 --------- src/examples.js | 97 - src/filesystem.js | 174 - src/find_definitions.js | 365 -- src/index.js | 335 -- src/launch.js | 6 - src/parse_js.js | 1888 ---------- src/reserved.js | 48 - src/runtime/array.js | 142 - src/runtime/let_multiversion.js | 61 - src/runtime/map.js | 45 - src/runtime/multiversion.js | 91 - src/runtime/object.js | 68 - src/runtime/record_io.js | 355 -- src/runtime/runtime.js | 575 --- src/runtime/set.js | 44 - src/share.js | 74 - src/utils.js | 118 - src/value_explorer_utils.js | 201 -- test/run.js | 6 - test/run_utils.js | 54 - test/self_hosted_test.js | 108 - test/test.js | 5394 ----------------------------- test/utils.js | 323 -- 52 files changed, 99 insertions(+), 17483 deletions(-) delete mode 100644 .eslintrc.json delete mode 100644 .github/FUNDING.yml delete mode 100644 .github/workflows/static.yml delete mode 100644 .github/workflows/test.yml delete mode 100644 .gitignore create mode 100644 .nojekyll create mode 100644 docs/examples/domevents.js delete mode 100755 gen_module_preload.sh delete mode 100644 package.json delete mode 100644 src/analyze_versioned_let_vars.js delete mode 100644 src/ast_utils.js delete mode 100644 src/calltree.js delete mode 100644 src/canvas.js delete mode 100644 src/cmd.js delete mode 100644 src/color.js delete mode 100644 src/editor/calltree.js delete mode 100644 src/editor/domutils.js delete mode 100644 src/editor/editor.js delete mode 100644 src/editor/files.js delete mode 100644 src/editor/io_trace.js delete mode 100644 src/editor/logs.js delete mode 100644 src/editor/share_dialog.js delete mode 100644 src/editor/ui.js delete mode 100644 src/editor/value_explorer.js delete mode 100644 src/effects.js delete mode 100644 src/eval.js delete mode 100644 src/examples.js delete mode 100644 src/filesystem.js delete mode 100644 src/find_definitions.js delete mode 100644 src/index.js delete mode 100644 src/launch.js delete mode 100644 src/parse_js.js delete mode 100644 src/reserved.js delete mode 100644 src/runtime/array.js delete mode 100644 src/runtime/let_multiversion.js delete mode 100644 src/runtime/map.js delete mode 100644 src/runtime/multiversion.js delete mode 100644 src/runtime/object.js delete mode 100644 src/runtime/record_io.js delete mode 100644 src/runtime/runtime.js delete mode 100644 src/runtime/set.js delete mode 100644 src/share.js delete mode 100644 src/utils.js delete mode 100644 src/value_explorer_utils.js delete mode 100644 test/run.js delete mode 100644 test/run_utils.js delete mode 100644 test/self_hosted_test.js delete mode 100644 test/test.js delete mode 100644 test/utils.js diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index eff21c6..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - - "env": { - "node": true, - "browser": true, - "es6": true - }, - "rules": { - "no-unsafe-finally": "off", - "no-unused-vars": "off", - "no-use-before-define": [2, { "functions": false , "classes": false, "variables": false}], - "no-console": "off" - }, - "globals": { - "globalThis": true - }, - "extends": "eslint:recommended" -} diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index d55b0d9..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms - -github: [leporello-js] diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml deleted file mode 100644 index 04105ed..0000000 --- a/.github/workflows/static.yml +++ /dev/null @@ -1,44 +0,0 @@ -# Simple workflow for deploying static content to GitHub Pages -name: Deploy static content to Pages - -on: - # Runs on pushes targeting the default branch - push: - branches: ["master"] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: "pages" - cancel-in-progress: false - -jobs: - # Single deploy job since we're just deploying - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Pages - uses: actions/configure-pages@v4 - - name: Build - run: ./gen_module_preload.sh - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: '.' - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index f1523a7..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Test - -on: - push: - branches: '*' - -jobs: - - test: - runs-on: ubuntu-latest - - steps: - - - uses: actions/checkout@v2 - - uses: actions/setup-node@v3 - with: - node-version: 'latest' - - - - name: test - run: | - npm install eslint - npx eslint src test - node test/run.js - node test/self_hosted_test.js diff --git a/.gitignore b/.gitignore deleted file mode 100644 index d6f13d5..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -README.html -.DS_Store diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index dc57586..509c601 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Leporello.js -Leporello.js is an interactive JavaScript environment with a time-travel debugger +Leporello.js is a next-level online JavaScript debugger and REPL [](https://vimeo.com/845773267) @@ -32,7 +32,7 @@ Leporello.js source code is developed within Leporello.js itself Variables are declared using the `const` or 'let' declaration. The use of `var` is not supported. -Loops of any kind are not supported. Instead, consider using recursion or array functions as alternatives. +Currently the only supported loop type is `while`. Other loop types (for loop, for-of loop, for-in loop, do-while loop) will be supported in future. The `if` / `else` statements can only contain blocks of code and not single statements (TODO). diff --git a/docs/examples/domevents.js b/docs/examples/domevents.js new file mode 100644 index 0000000..8d615cc --- /dev/null +++ b/docs/examples/domevents.js @@ -0,0 +1,35 @@ +window.addEventListener('load', () => { + const text = document.createElement('input') + + const checkbox = document.createElement('input') + checkbox.setAttribute('type', 'checkbox') + + const radio = document.createElement('input') + radio.setAttribute('type', 'radio') + + const range = document.createElement('input') + range.setAttribute('type', 'range') + + const select = document.createElement('select') + Array.from({length: 5}, (_, i) => i).forEach(i => { + const option = document.createElement('option') + option.setAttribute('value', i) + option.innerText = i + select.appendChild(option) + }) + + const div = document.createElement('div') + + const elements = { text, checkbox, range, select, radio, div} + + Object.entries(elements).forEach(([name, el]) => { + document.body.appendChild(el); + ['click', 'input', 'change'].forEach(type => { + el.addEventListener(type, e => { + const row = document.createElement('div') + div.appendChild(row) + row.innerText = [name, type, e.target.value, e.target.checked, e.target.selectedIndex].join(', ') + }) + }) + }) +}) diff --git a/gen_module_preload.sh b/gen_module_preload.sh deleted file mode 100755 index df056d2..0000000 --- a/gen_module_preload.sh +++ /dev/null @@ -1,6 +0,0 @@ -PRELOADS="" -for f in `find src -name '*.js'`; do - PRELOADS=$PRELOADS"" -done - -sed -i.bak "s#.*PRELOADS_PLACEHOLDER.*#$PRELOADS#" index.html diff --git a/index.html b/index.html index d02480e..bf9ff25 100644 --- a/index.html +++ b/index.html @@ -5,8 +5,6 @@ Leporello.js - - @@ -18,6 +16,8 @@ :root { --shadow_color: rgb(171 200 214); --active_color: rgb(173, 228, 253); + --error-color: #ff000024; + --warn-color: #fff6d5; } html, body, .app { @@ -28,6 +28,25 @@ margin: 0px; } + .spinner { + display: inline-block; + height: 0.8em; + width: 0.8em; + min-width: 0.8em; + border-radius: 50%; + border-top: none !important; + border: 2px solid; + animation: rotate 0.6s linear infinite; + } + @keyframes rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + .app { /* same as ace editor */ font-family: Monaco, Menlo, "Ubuntu Mono", Consolas, source-code-pro, monospace; @@ -81,6 +100,7 @@ .selection { position: absolute; background-color: #ff00ff; + z-index: 1; /* make it on top of evaluated_ok and evaluated_error */ } .evaluated_ok { @@ -89,7 +109,7 @@ } .evaluated_error { position: absolute; - background-color: #ff000024; + background-color: var(--error-color); } .error-code { /* @@ -157,7 +177,19 @@ } .logs .log.active { - background-color: var(--active_color); + background-color: var(--active_color) !important; + } + + .logs .log.error { + background-color: var(--error-color); + color: black !important; /* override red color that is set for calltree */ + &.native { + color: grey !important; + } + } + + .logs .log.warn { + background-color: var(--warn-color); } .tab_content { @@ -166,12 +198,25 @@ } .callnode { + /* This makes every callnode be the size of the the longest one, so + * every callnode is clickable anywhere in the calltree view, and + * background for active callnodes is as wide as the entire container. + * Useful when scrolling very wide call trees */ + min-width: fit-content; margin-left: 1em; } .callnode .active { background-color: var(--active_color); } .call_el { + /* + Make active callnode background start from the left of the calltree + view + */ + margin-left: -1000vw; + padding-left: 1000vw; + width: 100%; + cursor: pointer; display: inline-block; } @@ -179,6 +224,7 @@ padding-left: 5px; padding-right: 2px; } + .call_header { white-space: nowrap; } @@ -187,7 +233,6 @@ } .call_header.error.native { color: red; - opacity: 0.5; } .call_header.native { font-style: italic; @@ -318,9 +363,15 @@ } .value_explorer_header { + display: inline-block; + padding-right: 1em; cursor: pointer; } + .value_explorer_header .expand_icon { + padding: 5px; + } + .value_explorer_header.active { background-color: rgb(148, 227, 191); } @@ -340,6 +391,11 @@ align-items: center; justify-content: flex-start; } + + .statusbar .spinner { + margin-right: 0.5em; + } + .status, .current_file { font-size: 1.5em; } diff --git a/package.json b/package.json deleted file mode 100644 index 4efd107..0000000 --- a/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type" : "module" -} diff --git a/service_worker.js b/service_worker.js index 9c204fd..cfe2026 100644 --- a/service_worker.js +++ b/service_worker.js @@ -50,7 +50,7 @@ const serve_response_from_dir = async event => { let file if(path == '__leporello_blank.html') { - file = '' + file = '' } else if(dir_handle != null) { file = await read_file(dir_handle, path) } else { diff --git a/src/analyze_versioned_let_vars.js b/src/analyze_versioned_let_vars.js deleted file mode 100644 index 5a5e628..0000000 --- a/src/analyze_versioned_let_vars.js +++ /dev/null @@ -1,153 +0,0 @@ -import {collect_destructuring_identifiers} from './ast_utils.js' - -const set_versioned_let_vars = (node, closed_let_vars, assigned_vars) => { - if( - node.type == 'identifier' - && closed_let_vars.find(index => node.index == index) != null - && assigned_vars.find(index => node.index == index) != null - ) { - return {...node, is_versioned_let_var: true} - } else if(node.children != null) { - return { - ...node, - children: node.children.map(c => - set_versioned_let_vars(c, closed_let_vars, assigned_vars) - ) - } - } else { - return node - } -} - -const do_find_versioned_let_vars = (node, current_fn) => { - const children_result = node - .children - .map(c => find_versioned_let_vars(c, current_fn)) - const children = children_result.map(r => r.node) - const closed_let_vars = children_result - .flatMap(r => r.closed_let_vars) - .filter(r => r != null) - const assigned_vars = children_result - .flatMap(r => r.assigned_vars) - .filter(r => r != null) - return { - node: {...node, children}, - closed_let_vars, - assigned_vars, - } -} - -const has_versioned_let_vars = (node, is_root = true) => { - if(node.type == 'identifier' && node.is_versioned_let_var) { - return true - } else if(node.type == 'function_expr' && !is_root) { - return false - } else if(node.children != null) { - return node.children.find(c => has_versioned_let_vars(c, false)) != null - } else { - return false - } -} - -// TODO function args -export const find_versioned_let_vars = (node, current_fn = node) => { - /* - Assigns 'is_versioned_let_var: true' to let variables that are - - assigned after declaration - - closed in nested function - and sets 'has_versioned_let_vars: true' for functions that have versioned - let vars. - - - Traverse AST - - collects closed_let_vars and assigned_vars going from AST bottom to root: - - For every assignment, add assigned var to assigned_vars - - For every use of identifier, check if it used in function where it - declared, and populate assigned_vars otherwise - - for 'do' node, find 'let' declarations and set is_versioned_let_var. - */ - if(node.type == 'do') { - const {node: result, closed_let_vars, assigned_vars} - = do_find_versioned_let_vars(node, current_fn) - const next_node = { - ...result, - children: result.children.map(c => { - if(c.type != 'let') { - return c - } else { - const children = c.children.map(decl => { - if(decl.type == 'identifier') { - return set_versioned_let_vars(decl, closed_let_vars, assigned_vars) - } else if(decl.type == 'decl_pair') { - const [left, right] = decl.children - return { - ...decl, - children: [ - set_versioned_let_vars(left, closed_let_vars, assigned_vars), - right - ] - } - } else { - throw new Error('illegal state') - } - }) - return {...c, children} - } - }) - } - return { - node: node == current_fn - // toplevel - ? {...next_node, has_versioned_let_vars: has_versioned_let_vars(next_node)} - : next_node, - closed_let_vars, - assigned_vars, - } - } else if(node.type == 'assignment') { - const {node: next_node, closed_let_vars, assigned_vars} - = do_find_versioned_let_vars(node, current_fn) - const next_assigned_vars = node - .children - .filter(c => c.type == 'decl_pair') - .flatMap(decl_pair => - collect_destructuring_identifiers(decl_pair).map(id => { - if(id.definition.index == null) { - throw new Error('illegal state') - } - return id.definition.index - }) - ) - return { - node: next_node, - closed_let_vars, - assigned_vars: [...(assigned_vars ?? []), ...next_assigned_vars], - } - } else if(node.type == 'function_expr') { - const result = do_find_versioned_let_vars(node, node) - return { - ...result, - node: { - ...result.node, - has_versioned_let_vars: has_versioned_let_vars(result.node) - } - } - } else if(node.children != null) { - return do_find_versioned_let_vars(node, current_fn) - } else if(node.type == 'identifier') { - if(node.definition == 'self') { - return {node, closed_let_vars: null, assigned_vars: null} - } - const index = node.definition.index - if(!(index >= current_fn.index && index < current_fn.index + current_fn.length)) { - // used let var from parent function scope - return { - node, - closed_let_vars: [index], - assigned_vars: null, - } - } else { - return {node, closed_let_vars: null, assigned_vars: null} - } - } else if(node.children == null) { - return {node, closed_let_vars: null, assigned_vars: null} - } -} diff --git a/src/ast_utils.js b/src/ast_utils.js deleted file mode 100644 index b9e0610..0000000 --- a/src/ast_utils.js +++ /dev/null @@ -1,208 +0,0 @@ -import {uniq, map_find} from './utils.js' - -export const collect_destructuring_identifiers = node => { - if(Array.isArray(node)) { - return node.map(collect_destructuring_identifiers).flat() - } else if(node.type == 'identifier') { - return [node] - } else if(['destructuring_default', 'destructuring_rest'].includes(node.type)){ - return collect_destructuring_identifiers(node.name_node) - } else if(node.type == 'destructuring_pair') { - return collect_destructuring_identifiers(node.value) - } else if( - ['array_destructuring', 'object_destructuring', 'function_args'] - .includes(node.type) - ) { - return node.children - .map(collect_destructuring_identifiers) - .flat() - } else if (node.type == 'decl_pair') { - return collect_destructuring_identifiers(node.children[0]) - } else { - console.error(node) - throw new Error('not implemented') - } -} - -export const map_destructuring_identifiers = (node, mapper) => { - const map = node => map_destructuring_identifiers(node, mapper) - if(node.type == 'identifier') { - return mapper(node) - } else if(node.type == 'destructuring_default') { - return {...node, children: [map(node.children[0]), node.children[1]]} - } else if(node.type == 'destructuring_rest') { - return {...node, children: [mapper(node.children[0])]} - } else if(node.type == 'destructuring_pair') { - return {...node, children: [node.children[0], map(node.children[1])]} - } else if(node.type == 'array_destructuring' || node.type == 'object_destructuring') { - return {...node, children: node.children.map(map)} - } else { - console.error(node) - throw new Error('not implemented') - } -} - -export const collect_imports = module => { - return uniq( - module.children - .filter(n => n.type == 'import') - .filter(n => !n.is_external) - .map(n => n.full_import_path) - ) -} - -export const collect_external_imports = modules => - Object - .entries(modules) - .map(([module_name, node]) => - node - .children - .filter(c => c.type == 'import' && c.is_external) - .map(node => ({node, module_name})) - ) - .flat() - -export const find_leaf = (node, index) => { - if(!(node.index <= index && node.index + node.length > index)){ - return null - } else { - if(node.children == null){ - return node - } else { - const children = node.children.map(n => find_leaf(n, index)) - const child = children.find(c => c != null) - return child || node - } - } -} - -// Finds node declaring identifier with given index -export const find_declaration = (node, index) => { - if(node.type == 'let' || node.type == 'const') { - return node - } - if(node.type == 'function_decl' && node.index == index) { - return node - } - if(node.type == 'function_args') { - return node - } - if(node.type == 'import') { - return node - } - if(node.children == null) { - return null - } - const child = node.children.find(node => is_index_within_node(node, index)) - if(child == null) { - return null - } - return find_declaration(child, index) -} - -export const is_index_within_node = (node, index) => - node.index <= index && node.index + node.length > index - -export const is_child = (child, parent) => { - return parent.index <= child.index && - (parent.index + parent.length) >= child.index + child.length -} - -// TODO inconsistency. Sometimes we compare by identity, sometimes by this -// function -export const is_eq = (a, b) => { - return a.index == b.index && a.length == b.length - // Two different nodes can have the same index and length. Currently there - // is only one case: single-child do statement and its only child. So we - // add `type` to comparison. Better refactor it and add unique id to every - // node? Maybe also include `module` to id? - && a.type == b.type -} - -export const ancestry = (child, parent) => { - if(is_eq(parent, child)){ - return [] - } else { - if(parent.children == null){ - return null - } else { - const c = parent.children.find(c => is_child(child, c)) - if(c == null){ - return null - } else { - return ancestry(child, c).concat([parent]) - } - } - } -} - -export const ancestry_inc = (child, parent) => - [child, ...ancestry(child, parent)] - -export const find_fn_by_location = (node, loc) => { - if( - node.index == loc.index && node.length == loc.length - // see comment for is_eq - && node.type == 'function_expr' - ) { - return node - } else if(node.children == null){ - throw new Error('illegal state') - } else { - const c = node.children.find(c => is_child(loc, c)) - if(c == null){ - throw new Error('illegal state') - } else { - return find_fn_by_location(c, loc) - } - } -} - -export const find_node = (node, pred) => { - if(pred(node)) { - return node - } - if(node.children == null) { - return null - } - return node - .children - .reduce( - (result, c) => result ?? find_node(c, pred), - null - ) -} - -// TODO result is ok, but value is rejected promise -export const find_error_origin_node = (node, is_root = true) => { - if(node.result == null) { - return null - } - if(node.type == 'function_expr' && !is_root) { - return null - } - if(node.result.is_error_origin) { - return node - } - if(node.children == null) { - return null - } - return node - .children - .reduce( - (result, c) => result ?? find_error_origin_node(c, false), - null - ) -} - -/* Maps tree nodes, discarding mapped children, so maps only node contents, not - * allowing to modify structure */ -export const map_tree = (node, mapper) => { - const mapped = mapper(node) - if(node.children == null) { - return mapped - } - return {...mapped, - children: node.children.map(c => map_tree(c, mapper)) - } -} diff --git a/src/calltree.js b/src/calltree.js deleted file mode 100644 index d6ae230..0000000 --- a/src/calltree.js +++ /dev/null @@ -1,973 +0,0 @@ -import {update_children} from './parse_js.js' -import {map_accum, map_find, map_object, stringify, findLast} from './utils.js' -import {is_eq, find_error_origin_node} from './ast_utils.js' -import {find_node, find_leaf, ancestry_inc} from './ast_utils.js' -import {color} from './color.js' -import {eval_frame, eval_expand_calltree_node, get_after_if_path} from './eval.js' - -export const version_number_symbol = Symbol('version_number') -export const is_versioned_object = o => o?.[version_number_symbol] != null -export const get_version_number = o => o[version_number_symbol] - -export const pp_calltree = tree => ({ - id: tree.id, - ok: tree.ok, - args: tree.args, - value: tree.value, - is_log: tree.is_log, - has_more_children: tree.has_more_children, - string: tree.code?.string, - children: tree.children && tree.children.map(pp_calltree) -}) - -export const current_cursor_position = state => - state.cursor_position_by_file[state.current_module] - // When we open file for the first time, cursor set to the beginning - ?? 0 - -export const set_cursor_position = (state, cursor_position) => ( - { - ...state, - cursor_position_by_file: { - ...state.cursor_position_by_file, [state.current_module]: cursor_position - } - } -) - -export const set_location = (state, location) => set_cursor_position( - {...state, current_module: location.module}, - location.index -) - -const is_stackoverflow = node => - // Chrome - node.error?.message == 'Maximum call stack size exceeded' - || - // Firefox - node.error?.message == "too much recursion" - -export const has_error = n => - !n.ok - || - ( - n.value instanceof globalThis.app_window.Promise - && - n.value.status != null - && - !n.value.status.ok - ) - -export const calltree_node_loc = node => node.toplevel - ? {module: node.module} - : node.fn.__location - -export const get_deferred_calls = state => state.calltree.children[1].children - -export const root_calltree_node = state => - // Returns calltree node for toplevel - // It is either toplevel for entrypoint module, or for module that throw - // error and prevent entrypoint module from executing. - // state.calltree.children[1] is deferred calls - state.calltree.children[0] - -export const root_calltree_module = state => - root_calltree_node(state).module - -export const make_calltree = (root_calltree_node, deferred_calls) => ({ - id: 'calltree', - children: [ - root_calltree_node, - {id: 'deferred_calls', children: deferred_calls}, - ] -}) - -export const is_native_fn = calltree_node => - !calltree_node.toplevel && calltree_node.fn.__location == null - -export const active_frame = state => - state.frames[state.active_calltree_node.id] - -export const get_calltree_node_by_loc = (state, index) => { - const nodes_of_module = state.calltree_node_by_loc.get(state.current_module) - if(nodes_of_module == null) { - return null - } else { - return nodes_of_module.get(index) - } -} - -const get_selected_calltree_node_by_loc = ( - state, - node, - module = state.current_module -) => - state.selected_calltree_node_by_loc - ?.[module] - ?.[ - state.parse_result.modules[module] == node - // identify toplevel by index `-1`, because function and toplevel can - // have the same index (in case when module starts with function_expr) - ? -1 - : node.index - ] - -const set_node_by_loc = (node_by_loc, loc, node_id) => { - return {...node_by_loc, - [loc.module]: { - ...node_by_loc?.[loc.module], - [loc.index ?? -1]: node_id - } - } -} - -const set_selected_calltree_node_by_loc = (state, loc, node_id) => { - return { - ...state, - selected_calltree_node_by_loc: set_node_by_loc( - state.selected_calltree_node_by_loc, - loc, - node_id, - ) - } -} - -export const set_active_calltree_node = ( - state, - active_calltree_node, - current_calltree_node = state.current_calltree_node, -) => { - return { - ...state, - active_calltree_node, - current_calltree_node, - } -} - -export const add_frame = ( - state, - active_calltree_node, - current_calltree_node = active_calltree_node, -) => { - let with_frame - let frame - frame = state.frames?.[active_calltree_node.id] - if(frame == null) { - frame = update_children(eval_frame(active_calltree_node, state.modules, state.rt_cxt)) - const execution_paths = active_calltree_node.toplevel - ? null - : get_execution_paths(frame) - const coloring = color(frame) - with_frame = {...state, - frames: {...state.frames, - [active_calltree_node.id]: {...frame, coloring, execution_paths} - } - } - } else { - with_frame = state - } - - const loc = calltree_node_loc(active_calltree_node) - - const with_colored_frames = {...with_frame, - colored_frames: set_node_by_loc( - with_frame.colored_frames, - loc, - active_calltree_node.id, - ) - } - - return set_active_calltree_node( - with_colored_frames, - active_calltree_node, - current_calltree_node - ) -} - -const replace_calltree_node = (root, node, replacement) => { - const do_replace = root => { - if(root.id == node.id) { - return [true, replacement] - } - - if(root.children == null) { - return [false, root] - } - - const [replaced, children] = map_accum( - (replaced, c) => replaced - // Already replaced, do not look for replacement - ? [true, c] - : do_replace(c), - false, - root.children, - ) - - if(replaced) { - return [true, {...root, children}] - } else { - return [false, root] - } - } - - const [replaced, result] = do_replace(root) - - if(!replaced) { - throw new Error('illegal state') - } - - return result -} - -const expand_calltree_node = (state, node) => { - if(node.has_more_children) { - const next_node = eval_expand_calltree_node( - state.rt_cxt, - state.parse_result, - node - ) - return { - state: {...state, - calltree: replace_calltree_node(state.calltree, node, next_node) - }, - node: next_node - } - } else { - return {state, node} - } -} - -const jump_calltree_node = (_state, _current_calltree_node, do_not_set_location = false) => { - const {state, node: current_calltree_node} = expand_calltree_node( - _state, _current_calltree_node - ) - - /* - When node is selected or expanded/collapsed - If native, goto call site - If hosted - If parent is native - goto inside fn - If parent is hosted - If expanded, goto inside fn - If collapsed, goto call site - */ - - /* Whether to show fn body (true) or callsite (false) */ - let show_body - - const [parent] = path_to_root(state.calltree, current_calltree_node) - - if( - current_calltree_node.toplevel - || - parent.id == 'deferred_calls' - ) { - show_body = true - } else if(is_native_fn(current_calltree_node)) { - show_body = false - } else { - if(is_native_fn(parent)) { - show_body = true - } else { - const is_expanded = state.calltree_node_is_expanded[current_calltree_node.id] - show_body = is_expanded - } - } - - const active_calltree_node = show_body ? current_calltree_node : parent - - const next = add_frame(state, active_calltree_node, current_calltree_node) - - let loc, callsite_node - - if(show_body) { - loc = calltree_node_loc(next.active_calltree_node) - } else { - const frame = next.frames[active_calltree_node.id] - callsite_node = find_node(frame, n => - (n.type == 'function_call' || n.type == 'new') - && - n.result?.call?.id == current_calltree_node.id - ) - loc = { - module: calltree_node_loc(active_calltree_node).module, - index: callsite_node.index - } - } - - const with_location = do_not_set_location - ? next - : next.current_calltree_node.toplevel - ? {...next, current_module: loc.module} - // TODO: better jump not start of function (arguments), but start - // of body? - : set_location(next, loc) - - const with_selected_calltree_node = set_selected_calltree_node_by_loc( - with_location, - calltree_node_loc(active_calltree_node), - with_location.active_calltree_node.id, - ) - - let value_explorer - if(next.current_calltree_node.toplevel) { - value_explorer = null - } else { - const _arguments = { - value: - show_body - ? active_frame(with_selected_calltree_node) - // function args node - .children[0] - .result - .value - : current_calltree_node.args, - [version_number_symbol]: current_calltree_node.version_number, - } - value_explorer = { - index: loc.index, - result: { - ok: true, - is_calltree_node_explorer: true, - value: current_calltree_node.ok - ? { - '*arguments*': _arguments, - '*return*': { - value: current_calltree_node.value, - [version_number_symbol]: current_calltree_node.last_version_number, - } - } - : { - '*arguments*': _arguments, - '*throws*': { - value: current_calltree_node.error, - [version_number_symbol]: current_calltree_node.last_version_number, - } - } - } - } - } - - return {...with_selected_calltree_node, - value_explorer, - selection_state: show_body - ? null - : {node: callsite_node} - } -} - -const show_value_explorer = state => { - return jump_calltree_node(state, state.current_calltree_node, true) -} - -export const path_to_root = (root, child) => { - const do_path = (root) => { - if(root.id == child.id) { - return [] - } - if(root.children == null) { - return null - } - return root.children.reduce( - (result, c) => { - if(result != null) { - return result - } - const path = do_path(c) - if(path == null) { - return null - } - return [...path, root] - }, - null - ) - } - - const result = do_path(root) - - if(result == null) { - throw new Error('illegal state') - } - - return result -} - -export const is_expandable = node => - is_native_fn(node) - ? (node.children != null && node.children.length != 0) - || node.has_more_children - // Hosted node always can be expanded, even if has not children - // Toplevel cannot be expanded if has no children - : node.toplevel - ? (node.children != null && node.children.length != 0) - : true - -/* - Right - - - does not has children - nothing - - has children - first click expands, second jumps to first element - - Left - - - root - nothing - - not root collapse node, goes to parent if already collapsed - - Up - goes to prev visible element - Down - goes to next visible element - - Click - select and toggle expand - - step_into - select and expand -*/ - -const arrow_down = state => { - const current = state.current_calltree_node - let next_node - - if( - is_expandable(current) - && state.calltree_node_is_expanded[current.id] - && current.children != null - ) { - - next_node = current.children[0] - - } else { - - const next = (n, path) => { - if(n.id == 'calltree') { - return null - } - const [parent, ...grandparents] = path - const child_index = parent.children.findIndex(c => - c == n - ) - const next_child = parent.children[child_index + 1] - if(next_child == null) { - return next(parent, grandparents) - } else { - return next_child - } - } - - next_node = next( - current, - path_to_root(state.calltree, current) - ) - } - - if(next_node?.id == 'deferred_calls') { - if(next_node.children == null) { - next_node = null - } else { - next_node = next_node.children[0] - } - } - - return next_node == null - ? state - : jump_calltree_node(state, next_node) -} - -const arrow_up = state => { - const current = state.current_calltree_node - if(current == root_calltree_node(state)) { - return state - } - const [parent] = path_to_root(state.calltree, current) - const child_index = parent.children.findIndex(c => - c == current - ) - const next_child = parent.children[child_index - 1] - const last = node => { - if( - !is_expandable(node) - || !state.calltree_node_is_expanded[node.id] - || node.children == null - ) { - return node - } else { - return last(node.children[node.children.length - 1]) - } - } - let next_node - if(next_child == null) { - next_node = parent.id == 'deferred_calls' - ? last(root_calltree_node(state)) - : parent - } else { - next_node = last(next_child) - } - return jump_calltree_node(state, next_node) -} - -const arrow_left = state => { - const current = state.current_calltree_node - const is_expanded = state.calltree_node_is_expanded[current.id] - if(!is_expandable(current) || !is_expanded) { - const [parent] = path_to_root(state.calltree, current) - if(parent.id == 'calltree' || parent.id == 'deferred_calls') { - return state - } else { - return jump_calltree_node(state, parent) - } - } else { - return toggle_expanded(state) - } -} - -const arrow_right = state => { - const current = state.current_calltree_node - if(is_expandable(current)) { - const is_expanded = state.calltree_node_is_expanded[current.id] - if(!is_expanded) { - return toggle_expanded(state) - } else { - if(current.children != null) { - return jump_calltree_node(state, current.children[0]) - } else { - return state - } - } - } else { - return state - } -} - - -export const toggle_expanded = (state, is_exp) => { - const node_id = state.current_calltree_node.id - const prev = state.calltree_node_is_expanded[node_id] - const next_is_exp = is_exp ?? !prev - const expanded_state = { - ...state, - calltree_node_is_expanded: { - ...state.calltree_node_is_expanded, - [node_id]: next_is_exp, - } - } - return jump_calltree_node( - expanded_state, - state.current_calltree_node, - ) -} - -const select_node = (state, id) => { - const node = find_node(state.calltree, n => n.id == id) - return jump_calltree_node(state, node) -} - -const select_and_toggle_expanded = (state, id) => { - const node = find_node(state.calltree, n => n.id == id) - const nextstate = jump_calltree_node(state, node) - if(is_expandable(node)) { - return toggle_expanded(nextstate) - } else { - return nextstate - } -} - -export const expand_path = (state, node) => { - if(state.calltree_node_is_expanded?.[node.id]) { - return state - } - - - return { - ...state, - calltree_node_is_expanded: { - ...state.calltree_node_is_expanded, - ...Object.fromEntries( - path_to_root(state.calltree, node) - .map(n => [n.id, true]) - ), - // Also expand node, since it is not included in - // path_to_root - [node.id]: true, - } - } -} - -export const initial_calltree_node = state => { - const root = root_calltree_node(state) - if( - root.ok - || - // Not looking for error origin, stack too deep - is_stackoverflow(root) - ) { - return { - state: expand_path(state, root), - node: root, - } - } else { - // Find error origin - const node = find_node(root, - n => !n.ok && ( - // All children are ok - n.children == null - || - n.children.find(c => !c.ok) == null - ) - ) - return {state: expand_path(state, node), node} - } -} - -export const default_expand_path = state => initial_calltree_node(state).state - -export const get_execution_paths = frame => { - /* - - depth-first search tree. if 'result == null', then stop. Do not descend - into function_expr - - look for 'if' and ternary - - Get executed branch (branch.result.ok != null). There may be no executed - branch if cond executed with error - - for 'if' statement we also add 'get_after_if_path(if_node.index)' - */ - const do_get = (node, next_node) => { - if(node.type == 'function_expr' || node.result == null) { - return [] - } - let after_if, branch - if(node.type == 'if' || node.type == 'ternary') { - const [cond, ...branches] = node.children - branch = branches.find(b => b.result != null) - } - if(node.type == 'if' && next_node != null && next_node.result != null) { - after_if = get_after_if_path(node) - } - const paths = [branch?.index, after_if].filter(i => i != null) - const children = node.children ?? [] - const child_paths = children - .map((c, i) => do_get(c, children[i + 1])) - .flat() - return [...paths, ...child_paths] - } - - const [args, body] = frame.children - - return new Set(do_get(body)) -} - -const find_execution_path = (node, index) => { - if(node.children == null) { - return [] - } - - const child = node.children.find(c => - c.index <= index && c.index + c.length > index - ) - - if(child == null) { - return [] - } - - const prev_ifs = node - .children - .filter(c => - c.index < child.index && c.type == 'if' - ) - .map(c => get_after_if_path(c)) - - const child_path = find_execution_path(child, index) - - // TODO other conditionals, like &&, ||, ??, ?., ?.[] - if(node.type == 'if' || node.type == 'ternary') { - const [cond, left, right] = node.children - if(child == left || child == right) { - return [...prev_ifs, child.index, ...child_path] - } - } - - return [...prev_ifs, ...child_path] -} - -export const find_call_node = (state, index) => { - const module = state.parse_result.modules[state.current_module] - - if(module == null) { - // Module is not executed - return {node: null, path: null} - } - - let node, path - - if(index < module.index || index >= module.index + module.length) { - // index is outside of module, it can happen because of whitespace and - // comments in the beginning and the end - node = module - path = null - } else { - const leaf = find_leaf(module, index) - const anc = ancestry_inc(leaf, module) - const fn = anc.find(n => n.type == 'function_expr') - node = fn == null - ? module - : fn - path = find_execution_path(node, index) - } - - return {node, path} -} - -export const find_call = (state, index) => { - const {node, path} = find_call_node(state, index) - - if(node == null) { - return state - } - - if(node == state.parse_result.modules[root_calltree_module(state)]) { - const toplevel = root_calltree_node(state) - return add_frame( - expand_path( - state, - toplevel - ), - toplevel, - ) - } else if(node.type == 'do') { - // Currently we only allow to eval in toplevel of entrypoint module - return state - } - - const selected_ct_node_id = get_selected_calltree_node_by_loc(state, node) - const execution_paths = selected_ct_node_id == null - ? null - : state.frames[selected_ct_node_id].execution_paths - - const ct_node_id = get_calltree_node_by_loc(state, node.index) - - if(ct_node_id == null) { - return set_active_calltree_node(state, null) - } - - const path_ct_node_id = [node.index, ...path] - .map(path_elem => { - const is_selected_node_hits_path = - selected_ct_node_id != null - && - (execution_paths.has(path_elem) || node.index == path_elem) - - if(is_selected_node_hits_path) { - return selected_ct_node_id - } - - // If there is no node selected for this fn, or it did not hit the path - // when executed, try to find node calltree_node_by_loc - return get_calltree_node_by_loc(state, path_elem) - }) - .findLast(node_id => node_id != null) - - const ct_node = find_node( - state.calltree, - n => n.id == path_ct_node_id - ) - if(ct_node == null) { - throw new Error('illegal state') - } - - return add_frame( - expand_path(state, ct_node), - ct_node, - ) -} - -const select_return_value = state => { - if(state.current_calltree_node.toplevel) { - return {state} - } - - const code = state.active_calltree_node.code - const loc = calltree_node_loc(state.active_calltree_node) - const frame = active_frame(state) - - let node, result_node - - if(state.current_calltree_node == state.active_calltree_node) { - if(frame.result.ok) { - if(code.body.type == 'do') { - const return_statement = find_node(frame, n => - n.type == 'return' && n.result?.ok - ) - - if(return_statement == null) { - // Fn has no return statement - return { - state: set_location(state, {module: loc.module, index: code.body.index}), - effects: {type: 'set_focus'} - } - } else { - result_node = return_statement.children[0] - } - - } else { - // Last children is function body expr - result_node = frame.children[frame.children.length - 1] - } - } else { - result_node = find_error_origin_node(frame) - } - - node = find_node(code, n => is_eq(result_node, n)) - - } else { - result_node = find_node(frame, n => - (n.type == 'function_call' || n.type == 'new') - && n.result != null - && n.result.call.id == state.current_calltree_node.id - ) - node = find_node(code, n => is_eq(result_node, n)) - } - - return { - state: { - ...set_location(state, {module: loc.module, index: node.index}), - selection_state: { - node, - }, - value_explorer: { - index: node.index, - length: node.length, - result: result_node.result, - }, - }, - effects: {type: 'set_focus'} - } - -} - -const select_arguments = (state, with_focus = true) => { - if(state.current_calltree_node.toplevel) { - return {state} - } - - const loc = calltree_node_loc(state.active_calltree_node) - const frame = active_frame(state) - - let node, result - - if(state.current_calltree_node == state.active_calltree_node) { - if(state.active_calltree_node.toplevel) { - return {state} - } - node = state.active_calltree_node.code.children[0] // function_args - result = frame.children[0].result - - } else { - const call = find_node(frame, n => - (n.type == 'function_call' || n.type == 'new') - && n.result != null - && n.result.call.id == state.current_calltree_node.id - ) - const call_node = find_node(state.active_calltree_node.code, n => is_eq(n, call)) - node = call_node.children[1] // call_args - result = call.children[1].result - } - - return { - state: { - ...set_location(state, {module: loc.module, index: node.index}), - selection_state: { - node, - }, - value_explorer: { - index: node.index, - length: node.length, - result, - }, - }, - effects: with_focus - ? {type: 'set_focus'} - : null, - } -} - -const select_error = state => { - const node = [ - state.current_calltree_node, - state.active_calltree_node, - root_calltree_node(state), - // TODO deferred calls??? - ].find(n => has_error(n)) - - if(node == null) { - return {state, effects: [{type: 'set_status', args: ['no error found']}]} - } - - const error_origin = find_node(node, n => - has_error(n) - && ( - n.children == null - || - n.children.every(c => - !has_error(c) - || - // Error in native fn - is_native_fn(c) && c.children == null - ) - ) - ) - - if(error_origin == null) { - throw new Error('illegal state: error origin not found') - } - - const next = expand_path(jump_calltree_node(state, error_origin), error_origin) - const frame = active_frame(next) - const error_node = find_error_origin_node(frame) - - return { - state: set_location(next, { - module: calltree_node_loc(error_origin).module, - index: error_node.index - }), - effects: {type: 'set_focus'} - } -} - -const navigate_logs_increment = (state, increment) => { - if(state.logs.logs.length == 0) { - return {state} - } - const index = - Math.max( - Math.min( - state.logs.log_position == null - ? 0 - : state.logs.log_position + increment, - state.logs.logs.length - 1 - ), - 0 - ) - return navigate_logs_position(state, index) -} - -const navigate_logs_position = (state, log_position) => { - const node = find_node(state.calltree, n => - n.id == state.logs.logs[log_position].id - ) - const {state: next, effects} = select_arguments( - expand_path(jump_calltree_node(state, node), node), - false, - ) - if(effects != null) { - throw new Error('illegal state') - } - return {...next, logs: {...state.logs, log_position}} -} - -export const calltree_commands = { - arrow_down, - arrow_up, - arrow_left, - arrow_right, - select_node, - select_and_toggle_expanded, - select_return_value, - select_arguments, - select_error, - navigate_logs_position, - navigate_logs_increment, - show_value_explorer, -} diff --git a/src/canvas.js b/src/canvas.js deleted file mode 100644 index 609bc5c..0000000 --- a/src/canvas.js +++ /dev/null @@ -1,144 +0,0 @@ -// TODO time-travel for canvas ImageData - -import {abort_replay} from './runtime/record_io.js' -import {set_record_call} from './runtime/runtime.js' -import {is_expandable} from './calltree.js' - -const context_reset = globalThis?.CanvasRenderingContext2D?.prototype?.reset - -function reset(context) { - if(context_reset != null) { - context_reset.call(context) - } else { - // For older browsers, `reset` may be not available - // changing width does the same as `reset` - // see https://stackoverflow.com/a/45871243/795038 - context.canvas.width = context.canvas.width + 0 - } -} - -function canvas_reset(canvas_ops) { - for(let context of canvas_ops.contexts) { - reset(context) - } -} - -export function apply_canvas_patches(window) { - const proto = window?.CanvasRenderingContext2D?.prototype - - if(proto == null) { - return - } - - const props = Object.getOwnPropertyDescriptors(proto) - - Object.entries(props).forEach(([name, p]) => { - if(p.value != null) { - if(typeof(p.value) != 'function') { - // At the moment this was written, all canvas values were functions - return - } - const method = p.value - proto[name] = { - // declare function like this so it has `name` property set - [name]() { - const cxt = window.__cxt - - set_record_call(cxt) - - /* - abort replay, because otherwise animated_fractal_tree would replay - instantly (because setTimeout is in io_trace) - */ - if(!cxt.io_trace_is_recording && !cxt.is_recording_deferred_calls) { - abort_replay(cxt) - } - - const version_number = ++cxt.version_counter - - try { - return method.apply(this, arguments) - } finally { - cxt.canvas_ops.contexts.add(this) - cxt.canvas_ops.ops.push({ - canvas_context: this, - method, - version_number, - args: arguments, - }) - } - } - }[name] - } - - if(p.set != null) { - const set_op = p.set - Object.defineProperty(proto, name, { - set(prop_value) { - const cxt = window.__cxt - - set_record_call(cxt) - - if(!cxt.io_trace_is_recording && !cxt.is_recording_deferred_calls) { - abort_replay(cxt) - } - - const version_number = ++cxt.version_counter - - try { - set_op.call(this, prop_value) - } finally { - cxt.canvas_ops.contexts.add(this) - cxt.canvas_ops.ops.push({ - canvas_context: this, - version_number, - set_op, - prop_value, - }) - } - } - }) - } - }) -} - -function replay_op(op) { - if(op.method != null) { - op.method.apply(op.canvas_context, op.args) - } else if(op.set_op != null) { - op.set_op.call(op.canvas_context, op.prop_value) - } else { - throw new Error('illegal op') - } -} - -export function redraw_canvas(state, is_replay_all_canvs_ops) { - if(state.calltree == null) { - // code is invalid or not executed yet - return - } - - const cxt = state.rt_cxt - - if(cxt.canvas_ops.ops == null) { - return - } - - canvas_reset(cxt.canvas_ops) - - if(is_replay_all_canvs_ops) { - for(let op of cxt.canvas_ops.ops) { - replay_op(op) - } - } else { - const current = state.current_calltree_node - // replay all ops up to current_calltree_node, including - const version_number = state.current_calltree_node.last_version_number - for(let op of cxt.canvas_ops.ops) { - if(op.version_number > version_number) { - break - } - replay_op(op) - } - } -} diff --git a/src/cmd.js b/src/cmd.js deleted file mode 100644 index 16b63d9..0000000 --- a/src/cmd.js +++ /dev/null @@ -1,938 +0,0 @@ -import {map_object, map_find, filter_object, collect_nodes_with_parents, uniq, - set_is_eq} from './utils.js' -import { - is_eq, is_child, ancestry, ancestry_inc, map_tree, - find_leaf, find_fn_by_location, find_node, find_error_origin_node, - collect_external_imports, collect_destructuring_identifiers -} from './ast_utils.js' -import {load_modules} from './parse_js.js' -import {eval_modules} from './eval.js' -import { - root_calltree_node, root_calltree_module, make_calltree, - get_deferred_calls, - calltree_commands, - add_frame, calltree_node_loc, expand_path, - initial_calltree_node, default_expand_path, toggle_expanded, active_frame, - find_call, set_active_calltree_node, - set_cursor_position, current_cursor_position, set_location, - is_native_fn, -} from './calltree.js' - -// external -import {with_version_number} from './runtime/runtime.js' - -const collect_logs = (logs, call) => { - const id_to_log = new Map( - collect_nodes_with_parents(call, n => n.is_log) - .map(({parent, node}) => ( - [ - node.id, - { - id: node.id, - version_number: node.version_number, - toplevel: parent.toplevel, - module: parent.toplevel - ? parent.module - : parent.fn.__location.module, - parent_name: parent.fn?.name, - args: node.args, - log_fn_name: node.fn.name, - } - ] - )) - ) - return logs.map(l => id_to_log.get(l.id)) -} - -export const with_version_number_of_log = (state, log_item, action) => - with_version_number(state.rt_cxt, log_item.version_number, action) - -const apply_eval_result = (state, eval_result) => { - // TODO what if console.log called from native fn (like Array::map)? - // Currently it is not recorded. Maybe we should monkey patch `console`? - return { - ...state, - calltree: make_calltree(eval_result.calltree, null), - calltree_node_by_loc: eval_result.calltree_node_by_loc, - // TODO copy rt_cxt? - rt_cxt: eval_result.rt_cxt, - logs: { - logs: collect_logs(eval_result.logs, eval_result.calltree), - log_position: null - }, - modules: eval_result.modules, - io_trace: - (eval_result.io_trace == null || eval_result.io_trace.length == 0) - // If new trace is empty, reuse previous trace - ? state.io_trace - : eval_result.io_trace - } -} - -const run_code = (s, globals) => { - const is_globals_eq = s.globals == null - ? globals == null - : set_is_eq(s.globals, globals) - - // If globals change, then errors for using undeclared identifiers may be - // no longer valid. Do not use cache - const parse_cache = is_globals_eq ? s.parse_result.cache : {} - const loader = module => s.files[module] - const parse_result = load_modules(s.entrypoint, loader, parse_cache, globals) - - const state = { - ...s, - globals, - parse_result, - calltree: null, - modules: null, - - // Shows that calltree is brand new and requires entire rerender - calltree_changed_token: {}, - - rt_cxt: null, - logs: null, - current_calltree_node: null, - active_calltree_node: null, - calltree_node_is_expanded: null, - frames: null, - colored_frames: null, - calltree_node_by_loc: null, - selected_calltree_node_by_loc: null, - selection_state: null, - loading_external_imports_state: null, - value_explorer: null, - } - - if(!state.parse_result.ok) { - return state - } - - const external_imports = uniq( - collect_external_imports(state.parse_result.modules) - .map(i => i.node.full_import_path) - ) - - if(external_imports.length != 0) { - // Trigger loading of external modules - return {...state, - loading_external_imports_state: { - external_imports, - } - } - } else { - // Modules were loaded and cached, proceed - return external_imports_loaded(state, state) - } -} - -const external_imports_loaded = ( - s, - prev_state, - external_imports, -) => { - if( - s.loading_external_imports_state - != - prev_state.loading_external_imports_state - ) { - // code was modified after loading started, discard - return s - } - - const state = { - ...s, - loading_external_imports_state: null - } - - if(external_imports != null) { - const errors = new Set( - Object - .entries(external_imports) - .filter(([url, result]) => !result.ok) - .map(([url, result]) => url) - ) - if(errors.size != 0) { - const problems = collect_external_imports(state.parse_result.modules) - .filter(({node}) => errors.has(node.full_import_path)) - .map(({node, module_name}) => ({ - index: node.index, - message: external_imports[node.full_import_path].error.message, - module: module_name, - })) - return {...state, - parse_result: { - ok: false, - cache: state.parse_result.cache, - problems, - } - } - } - } - - // TODO if module not imported, then do not run code on edit at all - const result = eval_modules( - state.parse_result, - external_imports, - state.on_deferred_call, - state.calltree_changed_token, - state.io_trace, - state.storage, - ) - - if(result.then != null) { - return {...state, - eval_modules_state: { promise: result } - } - } else { - return eval_modules_finished(state, state, result) - } -} - -const eval_modules_finished = (state, prev_state, result) => { - if(state.calltree_changed_token != prev_state.calltree_changed_token) { - // code was modified after prev vesion of code was executed, discard - return state - } - - if(result.rt_cxt.io_trace_is_replay_aborted) { - // execution was discarded, return state to execute `run_code` without io_trace - return clear_io_trace({...state, rt_cxt: result.rt_cxt}) - } - - const next = find_call( - apply_eval_result(state, result), - current_cursor_position(state) - ) - - let result_state - - if(next.active_calltree_node == null) { - const {node, state: next2} = initial_calltree_node(next) - result_state = set_active_calltree_node(next2, null, node) - } else { - result_state = default_expand_path( - expand_path( - next, - next.active_calltree_node - ) - ) - } - - const eval_state_clear = result_state.eval_modules_state == null - ? result_state - : {...result_state, eval_modules_state: null} - - return do_move_cursor( - eval_state_clear, - current_cursor_position(eval_state_clear) - ) -} - -const input = (state, code, index) => { - const files = {...state.files, [state.current_module]: code} - const with_files = { - ...state, - files, - parse_result: state.parse_result == null - ? null - : { - ...state.parse_result, - cache: filter_object(state.parse_result.cache, module => - module != state.current_module - ) - } - } - const next = set_cursor_position(with_files, index) - const effect_save = { - type: 'write', - args: [ - next.current_module, - next.files[next.current_module], - ] - } - return {state: next, effects: [effect_save]} -} - -const can_evaluate_node = (parent, node) => { - const anc = ancestry(node, parent) - if(anc == null){ - return {ok: false, message: 'out of scope'} - } - - const intermediate_fn = anc.find(n => - !is_eq(n, parent) && !is_eq(n, node) && n.type == 'function_expr' - ) - - if(intermediate_fn != null){ - // TODO check if identifier is defined in current scope, and eval - return {ok: false, message: 'code was not reached during program execution'} - } - - return {ok: true} -} - -const validate_index_action = state => { - if(!state.parse_result.ok){ - return {state, effects: {type: 'set_status', args: ['invalid syntax']}} - } - - if( - state.loading_external_imports_state != null - || - state.eval_modules_state != null - ) { - return {state, effects: {type: 'set_status', args: ['loading']}} - } - - if( - state.active_calltree_node == null - || - calltree_node_loc(state.active_calltree_node).module != state.current_module - ) { - return { - state, - effects: { - type: 'set_status', - args: ['code was not reached during program execution'] - } - } - } -} - -const get_step_into_node = (ast, frame, index) => { - // TODO step into from toplevel (must be fixed by frame follows cursor) - - const node = find_leaf(ast, index) - - // Find parent node with function call - const call = ancestry_inc(node, ast).find(n => n.type == 'function_call') - - if(call == null){ - return {ok: false, message: 'no function call to step into'} - } - - const can_eval = can_evaluate_node(frame, call) - if(!can_eval.ok){ - return {ok: false, message: can_eval.message} - } - - const callnode = find_node(frame, n => is_eq(n, call)) - if(callnode.result == null) { - return {ok: false, message: 'call was not reached during program execution'} - } else { - return {ok: true, calltree_node: callnode.result.call} - } -} - -const step_into = (state, index) => { - - const validate_result = validate_index_action(state) - if(validate_result != null) { - return validate_result - } - - const {ok, message, calltree_node} = get_step_into_node( - state.parse_result.modules[state.current_module], - active_frame(state), - index - ) - - if(is_native_fn(calltree_node)) { - return { - state, - effects: { - type: 'set_status', - args: ['Cannot step into: function is either builtin or from external lib'] - } - } - } - - if(!ok){ - return {state, effects: {type: 'set_status', args: [message]}} - } else { - const expanded = { - ...state, calltree_node_is_expanded: { - ...state.calltree_node_is_expanded, [calltree_node.id]: true - } - } - return toggle_expanded( - {...expanded, current_calltree_node: calltree_node}, - true - ) - } -} - -const get_next_selection_state = (selection_state, frame, is_expand, index) => { - if(selection_state != null && selection_state.index == index){ - // Expanding/collapsing selection - let next_node - const effective_is_expand = selection_state.initial_is_expand == is_expand - if(effective_is_expand){ - if(is_eq(selection_state.node, frame)) { - next_node = selection_state.node - } else { - next_node = ancestry(selection_state.node, frame).find(n => !n.not_evaluatable) - if(next_node.is_statement) { - next_node = selection_state.node - } - } - } else { - // collapse - if(selection_state.node.children != null){ - const leaf = find_leaf(selection_state.node, index) - next_node = ancestry_inc(leaf, selection_state.node) - .findLast(n => !n.not_evaluatable && n != selection_state.node) - } else { - // no children, cannot collapse - next_node = selection_state.node - } - } - return { - ok: true, - initial_is_expand: selection_state.initial_is_expand, - node: next_node, - index, - } - } else { - // Creating new selection - const leaf = find_leaf(frame, index); - const a = ancestry_inc(leaf, frame); - const node = a.find(n => !n.not_evaluatable); - if(node.is_statement) { - return { - ok: false, - message: 'can only evaluate expression, not statement', - } - } - return { - ok: true, - index, - node, - initial_is_expand: is_expand, - } - } -} - -export const selection = (selection_state, frame, is_expand, index) => { - const leaf = find_leaf(frame, index) - if(leaf == null) { - return { - selection_state: { - ok: false, - message: 'out of scope', - } - } - } - - const next_selection_state = get_next_selection_state(selection_state, frame, is_expand, index) - - if(!next_selection_state.ok) { - return {selection_state: next_selection_state} - } - - const {ok, message} = can_evaluate_node(frame, next_selection_state.node) - if(ok){ - const node = find_node(frame, n => is_eq(n, next_selection_state.node)) - if(node.result == null) { - return { - selection_state: { - ...next_selection_state, - ok: false, - message: 'expression was not reached during program execution', - } - } - } else { - let result - if(node.result.ok) { - result = node.result - } else { - const error_node = find_error_origin_node(node) - result = error_node.result - } - return { - selection_state: {...next_selection_state, ok: true}, - result - } - } - } else { - return { - selection_state: {...next_selection_state, ok: false, message} - } - } -} - -const eval_selection = (state, index, is_expand) => { - const validate_result = validate_index_action(state) - if(validate_result != null) { - return validate_result - } - - const {selection_state, result} = selection( - state.selection_state, - active_frame(state), - is_expand, - index - ) - - const nextstate = {...state, - selection_state, - value_explorer: selection_state.ok - ? { - node: selection_state.node, - index: selection_state.node.index, - length: selection_state.node.length, - result, - } - : null - } - - if(!selection_state.ok) { - return {state: nextstate, effects: {type: 'set_status', args: [selection_state.message]}} - } - - return {state: nextstate} -} - - -const change_current_module = (state, current_module) => { - if(state.files[current_module] == null) { - return { - state, - effects: {type: 'set_status', args: ['File not found']} - } - } else { - return {...state, current_module} - } -} - -const change_entrypoint = (state, entrypoint, current_module = entrypoint) => { - return {...state, - entrypoint, - current_module, - } -} - -const change_html_file = (state, html_file) => { - return {...state, html_file} -} - -const goto_location = (state, loc) => { - return { - state: move_cursor(set_location(state, loc), loc.index), - effects: {type: 'set_focus'}, - } -} - -const goto_definition = (state, index) => { - if(!state.parse_result.ok){ - return {state, effects: {type: 'set_status', args: ['unresolved syntax errors']}} - } else { - const module = state.parse_result.modules[state.current_module] - const node = find_leaf(module, index) - if(node == null || node.type != 'identifier') { - return {state, effects: {type: 'set_status', args: ['not an identifier']}} - } else { - const d = node.definition - if(d == 'global') { - return {state, effects: {type: 'set_status', args: ['global variable']}} - } else if (d == 'self') { - // place where identifier is declared, nothing to do - return {state} - } else { - let loc - if(d.module != null) { - const exp = map_find(state.parse_result.modules[d.module].children, n => { - if(n.type != 'export') { - return null - } - if(n.is_default && d.is_default) { - return n.children[0] - } else if(!n.is_default && !d.is_default) { - const ids = n.binding.children.flatMap(c => - collect_destructuring_identifiers(c.name_node) - ) - return ids.find(i => i.value == node.value) - } - }) - loc = {module: d.module, index: exp.index} - } else { - loc = {module: state.current_module, index: d.index} - } - return goto_location(state, loc) - } - } - } -} - -const goto_problem = (state, p) => { - return { - state: set_location(state, p), - effects: {type: 'set_focus'} - } -} - - -// TODO remove? -// TODO: to every child, add displayed_children property -/* -const filter_calltree = (calltree, pred) => { - const do_filter_calltree = calltree => { - const children = calltree.children && calltree.children - .map(c => do_filter_calltree(c)) - .flat() - - if(pred(calltree)) { - return [{...calltree, children}] - } else { - return children - } - } - - const result = do_filter_calltree(calltree) - - if(result.length == 1 && result[0].toplevel) { - return result[0] - } else { - return {...calltree, children: result} - } -} -*/ - -const get_stmt_value_explorer = (state, stmt) => { - if(stmt.result == null) { - // statement was not evaluated - return null - } - - let result - - if(stmt.result.ok) { - if(stmt.type == 'return') { - if(stmt.expr == null) { - // add fake version number - result = {ok: true, value: undefined, version_number: 0} - } else { - result = stmt.children[0].result - } - } else if(['let', 'const', 'assignment'].includes(stmt.type)) { - - if(stmt.children.find(c => c.type == 'assignment_pair') != null) { - if(stmt.children.length != 1) { - // Multiple assignments, not clear what value to show in value - // explorer, show nothing - return null - } - // get result of first assignment - result = stmt.children[0].result - } else { - const identifiers = stmt - .children - .flatMap( - collect_destructuring_identifiers - ) - .filter(id => id.result != null) - .map(id => [id.value, id.result.value]) - let value - if( - stmt.children.length == 1 - && - ( - stmt.children[0].type == 'identifier' - || - stmt.children[0].type == 'decl_pair' - && - stmt.children[0].name_node.type == 'identifier' - ) - ) { - // Just a single declaration - if(identifiers.length != 1) { - throw new Error('illegal state') - } - value = identifiers[0][1] - } else { - value = Object.fromEntries(identifiers) - } - - // TODO different identifiers may have different version_number, - // because there may be function calls and assignments in between fix - // it - const version_number = stmt.children[0].result.version_number - return { - index: stmt.index, - length: stmt.length, - result: {ok: true, value, version_number}, - } - } - } else if(stmt.type == 'if'){ - return null - } else if(stmt.type == 'import'){ - result = { - ok: true, - value: state.modules[stmt.full_import_path], - // For imports, we show version for the moment of module toplevel - // starts execution - version_number: state.active_calltree_node.version_number, - } - } else if (stmt.type == 'export') { - return get_stmt_value_explorer(state, stmt.children[0]) - } else { - result = stmt.result - } - } else { - result = find_error_origin_node(stmt).result - } - - return {index: stmt.index, length: stmt.length, result} -} - - -const get_value_explorer = (state, index) => { - if( - state.active_calltree_node == null - || - ( - state.current_module - != - calltree_node_loc(state.active_calltree_node).module - ) - ) { - return null - } - - const frame = active_frame(state) - - if( - true - // not toplevel, function expr - && frame.type == 'function_expr' - && index >= frame.children[0].index - && index < frame.children[0].index + frame.children[0].length - ) { - if(frame.children[0].children.length == 0) { - // Zero args - return null - } else { - // cursor in args, show args - return { - index: frame.children[0].index, - length: frame.children[0].length, - result: frame.children[0].result, - } - } - } - - if(frame.type == 'function_expr' && frame.body.type != 'do') { - const result = frame.children[1].result - if(result == null) { - // Error in arguments, body not evaluated - return null - } - return { - index: frame.children[1].index, - length: frame.children[1].length, - result: result.ok - ? result - : find_error_origin_node(frame.children[1]).result - } - } - - const leaf = find_leaf(frame, index) - const adjusted_leaf = ( - // We are in the whitespace at the beginning or at the end of the file - leaf == null - || - // Empty body or cursor between statements - leaf.type == 'do' && index > frame.index - ) - // Try find statement one symbol before, in case we are typing at the end - // of current statement - ? find_leaf(frame, index - 1) - : leaf - - if( - adjusted_leaf == null - || - adjusted_leaf.type == 'do' - || - /* between body and args*/ - is_eq(frame, adjusted_leaf) - ) { - return null - } - - const anc = ancestry_inc(adjusted_leaf, frame) - const intermediate_fn = anc.find(n => - !is_eq(n, frame) && !is_eq(n, adjusted_leaf) && n.type == 'function_expr' - ) - if(intermediate_fn != null) { - // TODO maybe cut `anc` from frame to intermediate fn, so we do not look - // inside intermediate fn. But it should be fixed by frame follows cursor - return null - } - - // Find inner do - const do_index = anc.findIndex(n => n.type == 'do') - const do_node = anc[do_index] - const stmt = anc[do_index - 1] - - return get_stmt_value_explorer(state, stmt) - -} - -const do_move_cursor = (state, index) => { - // TODO: if value explorer is null, show current fn return value and args? - - const value_explorer = get_value_explorer(state, index) - if( - value_explorer != null - && value_explorer.result.ok - && value_explorer.result.version_number == null - ) { - console.error('no version_number found', value_explorer) - throw new Error('illegal state') - } - return { ...state, value_explorer} -} - -const move_cursor = (s, index) => { - - const with_cursor = set_cursor_position(s, index) - - if(!s.parse_result.ok){ - return {state: with_cursor} - } - - if(s.loading_external_imports_state != null || s.eval_modules_state != null) { - // Code will be executed when imports will load, do not do it right now - return {state: with_cursor} - } - - // Remove selection on move cursor - const state_sel_removed = {...with_cursor, selection_state: null} - - const state = find_call(state_sel_removed, index) - - const validate_result = validate_index_action(state) - if(validate_result != null) { - return { ...state, value_explorer: null } - } - - return do_move_cursor(state, index) -} - -const on_deferred_call = (state, call, calltree_changed_token, logs) => { - if(state.calltree_changed_token != calltree_changed_token) { - return state - } - return {...state, - calltree: make_calltree( - root_calltree_node(state), - [...(get_deferred_calls(state) ?? []), call], - ), - logs: { - ...state.logs, - logs: state.logs.logs.concat(collect_logs(logs, call)) - }, - } -} - -const clear_io_trace = state => { - return {...state, io_trace: null} -} - -const load_files = (state, dir) => { - const collect_files = dir => dir.kind == 'file' - ? [dir] - : dir.children.map(collect_files).flat() - - const files = Object.fromEntries( - collect_files(dir).map(f => [f.path, f.contents]) - ) - - return { - ...state, - project_dir: dir, - files: {...files, '': state.files['']}, - } -} - -const apply_entrypoint_settings = (state, entrypoint_settings) => { - const blank_if_not_exists = key => - state.files[entrypoint_settings[key]] == null - ? '' - : entrypoint_settings[key] - - const entrypoint = blank_if_not_exists('entrypoint') - const current_module = blank_if_not_exists('current_module') - const html_file = blank_if_not_exists('html_file') - - return { - ...state, - entrypoint, - current_module, - html_file, - } -} - -const load_dir = (state, dir, has_file_system_access, entrypoint_settings) => { - // Clear parse cache and rerun code - const with_dir = load_files(state, dir) - return { - ...( - entrypoint_settings == null - ? with_dir - : apply_entrypoint_settings(with_dir, entrypoint_settings) - ), - - has_file_system_access, - - // remove cache. We have to clear cache because imports of modules that are - // not available because project_dir is not available have errors and the - // errors are cached - parse_result: {...state.parse_result, cache: {}}, - } -} - -const create_file = (state, dir, current_module) => { - return {...load_dir(state, dir, true), current_module} -} - -const open_app_window = state => ({...state, storage: new Map()}) - -const get_initial_state = (state, entrypoint_settings, cursor_pos = 0) => { - const with_files = state.project_dir == null - ? state - : load_files(state, state.project_dir) - - const with_settings = apply_entrypoint_settings(with_files, entrypoint_settings) - - return { - ...with_settings, - storage: new Map(), - cursor_position_by_file: {[with_settings.current_module]: cursor_pos}, - } -} - -export const COMMANDS = { - get_initial_state, - input, - run_code, - open_app_window, - load_dir, - create_file, - step_into, - change_current_module, - change_entrypoint, - change_html_file, - goto_location, - goto_definition, - goto_problem, - move_cursor, - eval_selection, - external_imports_loaded, - eval_modules_finished, - on_deferred_call, - clear_io_trace, - calltree: calltree_commands, -} diff --git a/src/color.js b/src/color.js deleted file mode 100644 index eabac31..0000000 --- a/src/color.js +++ /dev/null @@ -1,255 +0,0 @@ -const collect_function_exprs = new Function('root', ` - const result = [] - function do_collect(n) { - if(n.type == 'function_expr') { - result.push(n) - return - } - if(n.children == null) { - return - } - for(let c of n.children) { - do_collect(c) - } - } - do_collect(root) - return result -`) - -const is_result_eq = (a,b) => a.result == null - ? b.result == null - : b.result != null - && a.result.ok == b.result.ok - && !(!a.result.is_error_origin) == !(!b.result.is_error_origin) - -const node_to_color = node => ({ - index: node.index, - length: node.length, - result: node.result == null - ? null - : node.type == 'function_expr' - ? null - : node.result.ok - ? {ok: true} - : {ok: false, is_error_origin: !(!node.result.is_error_origin)} -}) - -const is_short_circuit = node => - node.type == 'binary' || node.type == 'ternary' - -const color_children = (node, is_root) => { - const coloring = node.children.map(n => do_color(n)).reduce( - (coloring, [range, ...rest]) => { - if(coloring.length == 0) { - return [range, ...rest] - } else { - const prev_range = coloring[coloring.length - 1] - if(is_result_eq(prev_range, range)) { - // Merge ranges - return [ - ...coloring.slice(0, coloring.length - 1), - { - index: prev_range.index, - length: range.index - prev_range.index + range.length, - result: range.result == null ? null : {ok: range.result.ok} - }, - ...rest - ] - } else if(!is_short_circuit(node) && prev_range.result == null && range.result?.ok){ - // Expand range back to the end of prev range - const index = prev_range.index + prev_range.length - return [ - ...coloring, - {...range, - index, - length: range.index - index + range.length, - }, - ...rest, - ] - } else if(!is_short_circuit(node) && prev_range.result?.ok && range.result == null) { - // Expand prev_range until beginning of range - const index = prev_range.index + prev_range.length - return [ - ...coloring.slice(0, coloring.length - 1), - {...prev_range, - length: range.index - prev_range.index - }, - range, - ...rest, - ] - } else { - // Append range - return [ - ...coloring, - range, - ...rest, - ] - } - } - }, - [] - ) - - if( - node.result == null || node.result?.ok - && - // All colors the same - coloring.reduce( - (result, c) => result && is_result_eq(coloring[0], c), - true - ) - ) { - - if(is_result_eq(node, coloring[0])) { - if(is_root && node.type == 'function_expr') { - // Override null result for function expr - return [{...node_to_color(node), result: {ok: node.result.ok}}] - } else { - return [node_to_color(node)] - } - } else { - const node_color = node_to_color(node) - const last = coloring[coloring.length - 1] - const index = coloring[0].index + coloring[0].length - return [ - { - ...node_color, - length: coloring[0].index - node_color.index, - }, - ...coloring, - { - ...node_color, - index, - length: node.index + node.length - index, - }, - ] - } - - } - - if(coloring.length == 0) { - throw new Error('illegal state') - } - - // if first child is ok, then expand it to the beginning of parent - const first = coloring[0] - const adj_left = is_result_eq(first, node) && node.result?.ok - ? [ - {...first, - index: node.index, - length: first.length + first.index - node.index - }, - ...coloring.slice(1), - ] - : coloring - - // if last child is ok, then expand it to the end of parent - const last = adj_left[adj_left.length - 1] - const adj_right = is_result_eq(last, node) && node.result?.ok - ? [ - ...adj_left.slice(0, adj_left.length - 1), - {...last, - index: last.index, - length: node.index + node.length - last.index, - }, - ] - : adj_left - - return adj_right -} - -const do_color = (node, is_root = false) => { - if(node.type == 'function_expr' && !is_root) { - return [{...node_to_color(node), result: null}] - } - - if( - false - || node.children == null - || node.children.length == 0 - ) { - return [node_to_color(node)] - } - - if(node.result?.is_error_origin) { - const color = node_to_color(node) - const exprs = collect_function_exprs(node) - if(exprs.length == 0) { - return [color] - } - // Color node in red, but make holes for function exprs - return exprs - .map((e, i) => { - let prev_index - if(i == 0) { - prev_index = node.index - } else { - const prev_expr = exprs[i - 1] - prev_index = prev_expr.index + prev_expr.length - } - return { - index: prev_index, - length: e.index - prev_index, - result: color.result - } - }) - .concat([{ - index: exprs.at(-1).index + exprs.at(-1).length, - length: node.index + node.length - - (exprs.at(-1).index + exprs.at(-1).length), - result: color.result, - }]) - } - - const result = color_children(node, is_root) - return node.result != null && !node.result.ok - ? result.map(c => c.result == null - ? {...c, result: {ok: false, is_error_origin: false}} - : c - ) - : result -} - -export const color = frame => { - const coloring = do_color(frame, true) - .filter(c => - // Previously we colored nodes that were not reach to grey color, now we - // just skip them - c.result != null - && - // Parts that were not error origins - (c.result.ok || c.result.is_error_origin) - ) - - // Sanity-check result - const {ok} = coloring.reduce( - ({ok, prev}, c) => { - if(!ok) { - return {ok} - } - if(prev == null) { - return {ok, prev: c} - } else { - // Check that prev is before next - // TODO check that next is right after prev, ie change > to == - if(prev.index + prev.length > c.index) { - return {ok: false} - } else { - return {ok: true, prev: c} - } - } - }, - {ok: true, prev: null} - ) - if(!ok) { - throw new Error('illegal state') - } - return coloring -} - -export const color_file = (state, file) => - Object - .values(state.colored_frames?.[file] ?? {}) - .filter(node_id => node_id != null) - .map(node_id => state.frames[node_id].coloring) - .flat() diff --git a/src/editor/calltree.js b/src/editor/calltree.js deleted file mode 100644 index caf48c3..0000000 --- a/src/editor/calltree.js +++ /dev/null @@ -1,242 +0,0 @@ -import {exec} from '../index.js' -import {el, scrollIntoViewIfNeeded, value_to_dom_el, join} from './domutils.js' -import {stringify_for_header} from '../value_explorer_utils.js' -import {find_node} from '../ast_utils.js' -import {with_version_number} from '../runtime/runtime.js' -import {is_expandable, root_calltree_node, get_deferred_calls, has_error} - from '../calltree.js' - -export class CallTree { - constructor(ui, container) { - this.ui = ui - this.container = container - - this.container.addEventListener('keydown', (e) => { - - if(e.key == 'Escape') { - this.ui.editor.focus() - } - - if(e.key == 'F1') { - this.ui.editor.focus_value_explorer(this.container) - } - - if(e.key == 'F2') { - this.ui.editor.focus() - } - - if(e.key == 'a') { - exec('calltree.select_arguments') - } - - if(e.key == 'e') { - exec('calltree.select_error') - } - - if(e.key == 'r' || e.key == 'Enter') { - exec('calltree.select_return_value') - } - - if(e.key == 'ArrowDown' || e.key == 'j'){ - // Do not scroll - e.preventDefault() - exec('calltree.arrow_down') - } - - if(e.key == 'ArrowUp' || e.key == 'k'){ - // Do not scroll - e.preventDefault() - exec('calltree.arrow_up') - } - - if(e.key == 'ArrowLeft' || e.key == 'h'){ - // Do not scroll - e.preventDefault() - exec('calltree.arrow_left') - } - - if(e.key == 'ArrowRight' || e.key == 'l'){ - // Do not scroll - e.preventDefault() - exec('calltree.arrow_right') - } - }) - - } - - on_click_node(ev, id) { - if(ev.target.classList.contains('expand_icon')) { - exec('calltree.select_and_toggle_expanded', id) - } else { - exec('calltree.select_node', id) - } - } - - clear_calltree(){ - this.container.innerHTML = '' - this.node_to_el = new Map() - this.state = null - } - - render_node(n){ - const is_expanded = this.state.calltree_node_is_expanded[n.id] - - const result = el('div', 'callnode', - el('div', { - 'class': 'call_el', - click: e => this.on_click_node(e, n.id), - }, - !is_expandable(n) - ? el('span', 'expand_icon_placeholder', '\xa0') - : el('span', 'expand_icon', is_expanded ? '▼' : '▶'), - n.toplevel - ? el('span', '', - el('i', '', - 'toplevel: ' + (n.module == '' ? '*scratch*' : n.module), - ), - n.ok ? '' : el('span', 'call_header error', '\xa0', stringify_for_header(n.error)), - ) - : el('span', - 'call_header ' - + (has_error(n) ? 'error' : '') - + (n.fn.__location == null ? ' native' : '') - , - // TODO show `this` argument - (n.is_new ? 'new ' : ''), - n.fn.name, - '(' , - ...join( - // for arguments, use n.version_number - last version before call - with_version_number(this.state.rt_cxt, n.version_number, () => - n.args.map(a => value_to_dom_el(a)) - ) - ), - ')' , - // TODO: show error message only where it was thrown, not every frame? - ': ', - // for return value, use n.last_version_number - last version that was - // created during call - with_version_number(this.state.rt_cxt, n.last_version_number, () => - n.ok - ? value_to_dom_el(n.value) - : stringify_for_header(n.error) - ) - ), - ), - (n.children == null || !is_expanded) - ? null - : n.children.map(c => this.render_node(c)) - ) - - this.node_to_el.set(n.id, result) - - result.is_expanded = is_expanded - - return result - } - - render_active(node, is_active) { - const dom = this.node_to_el.get(node.id).getElementsByClassName('call_el')[0] - if(is_active) { - dom.classList.add('active') - } else { - dom.classList.remove('active') - } - } - - render_select_node(prev, state) { - if(prev != null) { - this.render_active(prev.current_calltree_node, false) - } - this.state = state - this.render_active(this.state.current_calltree_node, true) - if(prev?.current_calltree_node != state.current_calltree_node) { - // prevent scroll on adding deferred call - scrollIntoViewIfNeeded( - this.container, - this.node_to_el.get(this.state.current_calltree_node.id).getElementsByClassName('call_el')[0] - ) - } - } - - render_expand_node(prev_state, state) { - this.state = state - - this.do_render_expand_node( - prev_state.calltree_node_is_expanded, - state.calltree_node_is_expanded, - root_calltree_node(prev_state), - root_calltree_node(state), - ) - - const prev_deferred_calls = get_deferred_calls(prev_state) - const deferred_calls = get_deferred_calls(state) - - if(prev_deferred_calls != null) { - // Expand already existing deferred calls - for(let i = 0; i < prev_deferred_calls.length; i++) { - this.do_render_expand_node( - prev_state.calltree_node_is_expanded, - state.calltree_node_is_expanded, - prev_deferred_calls[i], - deferred_calls[i], - ) - } - // Add new deferred calls - for(let i = prev_deferred_calls.length; i < deferred_calls.length; i++) { - this.deferred_calls_root.appendChild( - this.render_node(deferred_calls[i]) - ) - } - } - - this.render_select_node(prev_state, state) - } - - do_render_expand_node(prev_exp, next_exp, prev_node, next_node) { - if(prev_node.id != next_node.id) { - throw new Error() - } - if(!!prev_exp[prev_node.id] != !!next_exp[next_node.id]) { - const prev_dom_node = this.node_to_el.get(prev_node.id) - const next = this.render_node(next_node) - prev_dom_node.parentNode.replaceChild(next, prev_dom_node) - } else { - if(prev_node.children == null) { - return - } - for(let i = 0; i < prev_node.children.length; i++) { - this.do_render_expand_node( - prev_exp, - next_exp, - prev_node.children[i], - next_node.children[i], - ) - } - } - } - - // TODO on hover highlight line where function defined - // TODO hover ? - render_calltree(state){ - this.clear_calltree() - this.state = state - const root = root_calltree_node(this.state) - this.container.appendChild(this.render_node(root)) - this.render_select_node(null, state) - } - - render_deferred_calls(state) { - this.state = state - this.container.appendChild( - el('div', 'callnode', - el('div', 'call_el', - el('i', '', 'deferred calls'), - this.deferred_calls_root = el('div', 'callnode', - get_deferred_calls(state).map(call => this.render_node(call)) - ) - ) - ) - ) - } -} diff --git a/src/editor/domutils.js b/src/editor/domutils.js deleted file mode 100644 index a37b0ae..0000000 --- a/src/editor/domutils.js +++ /dev/null @@ -1,118 +0,0 @@ -import {exec} from '../index.js' -import {stringify_for_header} from '../value_explorer_utils.js' - -export function el(tag, className, ...children){ - const result = document.createElement(tag) - if(typeof(className) == 'string'){ - result.setAttribute('class', className) - } else { - const attrs = className - for(let attrName in attrs){ - const value = attrs[attrName] - if(['change','click'].includes(attrName)){ - result.addEventListener(attrName, value) - } else if(attrName == 'checked') { - if(attrs[attrName]){ - result.setAttribute(attrName, "checked") - } - } else { - result.setAttribute(attrName, value) - } - } - } - children.forEach(child => { - const append = child => { - if(typeof(child) == 'undefined') { - throw new Error('illegal state') - } else if(child !== null && child !== false) { - result.appendChild( - typeof(child) == 'string' - ? document.createTextNode(child) - : child - ) - } - } - if(Array.isArray(child)) { - child.forEach(append) - } else { - append(child) - } - }) - return result -} - -function fn_link(fn){ - // TODO if name is empty or 'anonymous', then show its source code instead - // of name - const str = fn.__location == null - ? `${fn.name}` - : `fn ${fn.name}` - const c = document.createElement('div') - c.innerHTML = str - const el = c.children[0] - if(fn.__location != null) { - el.addEventListener('click', e => { - e.stopPropagation() - exec('goto_location',fn.__location) - }) - } - return el -} - -export function value_to_dom_el(value) { - return typeof(value) == 'function' - ? fn_link(value) - : stringify_for_header(value) -} - -export function join(arr, separator = ', ') { - const result = [] - for(let i = 0; i < arr.length; i++) { - result.push(arr[i]) - if(i != arr.length - 1) { - result.push(separator) - } - } - return result -} - - -// Idea is borrowed from: -// https://mhk-bit.medium.com/scroll-into-view-if-needed-10a96e0bdb61 -// https://stackoverflow.com/questions/37137450/scroll-all-nested-scrollbars-to-bring-an-html-element-into-view -export const scrollIntoViewIfNeeded = (container, target) => { - - // Target is outside the viewport from the top - if(target.offsetTop - container.scrollTop - container.offsetTop < 0){ - // The top of the target will be aligned to the top of the visible area of the scrollable ancestor - target.scrollIntoView(true); - } - - // Target is outside the view from the bottom - if(target.offsetTop - container.scrollTop - container.offsetTop - container.clientHeight + target.clientHeight > 0) { - // The bottom of the target will be aligned to the bottom of the visible area of the scrollable ancestor. - target.scrollIntoView(false); - } - - /* - Also works - - // Target is outside the view from the top - if (target.getBoundingClientRect().y < container.getBoundingClientRect().y) { - // The top of the target will be aligned to the top of the visible area of the scrollable ancestor - target.scrollIntoView(); - } - - // Target is outside the view from the bottom - if ( - target.getBoundingClientRect().bottom - container.getBoundingClientRect().bottom + - // Adjust for scrollbar size - container.offsetHeight - container.clientHeight - > 0 - ) { - // The bottom of the target will be aligned to the bottom of the visible area of the scrollable ancestor. - target.scrollIntoView(false); - } - */ - -}; diff --git a/src/editor/editor.js b/src/editor/editor.js deleted file mode 100644 index 14a8c2e..0000000 --- a/src/editor/editor.js +++ /dev/null @@ -1,641 +0,0 @@ -import {exec, get_state, exec_and_reload_app_window} from '../index.js' -import {ValueExplorer} from './value_explorer.js' -import {stringify_for_header} from '../value_explorer_utils.js' -import {el} from './domutils.js' -import {version_number_symbol} from '../calltree.js' - -/* - normalize events 'change' and 'changeSelection': - - change is debounced - - changeSelection must not fire if 'change' is fired. So for every keystroke, - either change or changeSelection should be fired, not both - - changeSelection fired only once (ace fires it multiple times for single - keystroke) -*/ -const normalize_events = (ace_editor, { - on_change, - on_change_selection, - is_change_selection_supressed, - on_change_immediate, -}) => { - const TIMEOUT = 1000 - - let state - - const set_initial_state = () => { - state = {} - } - - set_initial_state() - - const flush = () => { - if(state.change_args != null) { - on_change(...state.change_args) - } else if(state.change_selection_args != null) { - on_change_selection(...state.change_selection_args) - } - set_initial_state() - } - - ace_editor.on('change', (...args) => { - on_change_immediate() - - if(state.tid != null) { - clearTimeout(state.tid) - } - - state.change_args = args - - state.tid = setTimeout(() => { - state.tid = null - flush() - }, TIMEOUT) - }) - - ace_editor.on('changeSelection', (...args) => { - // TODO debounce changeSelection? - if(is_change_selection_supressed()) { - return - } - if(state.tid != null) { - // flush is already by `change`, skip `changeSelection` - return - } - state.change_selection_args = args - if(!state.is_flush_set) { - state.is_flush_set = true - Promise.resolve().then(() => { - if(state.tid == null) { - flush() - } - }) - } - }) -} - -export class Editor { - - constructor(ui, editor_container){ - this.ui = ui - this.editor_container = editor_container - - this.make_resizable() - - this.markers = {} - this.sessions = {} - - this.ace_editor = globalThis.ace.edit(this.editor_container) - - this.ace_editor.setOptions({ - behavioursEnabled: false, - // Scroll past end for value explorer - scrollPastEnd: 100 /* Allows to scroll 100* */, - - enableLiveAutocompletion: false, - enableBasicAutocompletion: true, - }) - - normalize_events(this.ace_editor, { - on_change: () => { - try { - exec_and_reload_app_window('input', this.ace_editor.getValue(), this.get_cursor_position()) - } catch(e) { - // Do not throw Error to ACE because it breaks typing - console.error(e) - this.ui.set_status(e.message) - } - }, - - on_change_immediate: () => { - this.unembed_value_explorer() - }, - - on_change_selection: () => { - try { - if(!this.is_change_selection_supressed) { - exec('move_cursor', this.get_cursor_position()) - } - } catch(e) { - // Do not throw Error to ACE because it breaks typing - console.error(e) - this.ui.set_status(e.message) - } - }, - - is_change_selection_supressed: () => { - return this.is_change_selection_supressed - } - }) - - this.focus() - - this.init_keyboard() - } - - focus() { - this.ace_editor.focus() - } - - supress_change_selection(action) { - try { - this.is_change_selection_supressed = true - action() - } finally { - this.is_change_selection_supressed = false - } - } - - ensure_session(file, code) { - let session = this.sessions[file] - if(session == null) { - session = globalThis.ace.createEditSession(code) - this.sessions[file] = session - session.setUseWorker(false) - session.setOptions({ - mode: "ace/mode/javascript", - tabSize: 2, - useSoftTabs: true, - }) - } - return session - } - - get_session(file) { - return this.sessions[file] - } - - switch_session(file) { - // Supress selection change triggered by switching sessions - this.supress_change_selection(() => { - this.ace_editor.setSession(this.get_session(file)) - }) - } - - has_value_explorer() { - return this.value_explorer != null - } - - unembed_value_explorer() { - if(!this.has_value_explorer()) { - return null - } - - const session = this.ace_editor.getSession() - const widget_bottom = this.value_explorer.el.getBoundingClientRect().bottom - session.widgetManager.removeLineWidget(this.value_explorer) - - if(this.value_explorer.is_dom_el) { - /* - if cursor moves below value_explorer, then ace editor first adjusts scroll, - and then value_explorer gets remove, so scroll jerks. We have to set scroll - back - */ - // distance travelled by cursor - const distance = session.selection.getCursor().row - this.value_explorer.row - if(distance > 0) { - const line_height = this.ace_editor.renderer.lineHeight - const scroll = widget_bottom - this.editor_container.getBoundingClientRect().bottom - if(scroll > 0) { - const scrollTop = session.getScrollTop() - session.setScrollTop(session.getScrollTop() - scroll - line_height*distance) - } - } - } - - this.value_explorer = null - } - - update_value_explorer_margin() { - if(this.value_explorer != null) { - const session = this.ace_editor.getSession() - - // Calculate left margin in such way that value explorer does not cover - // code. It has sufficient left margin so all visible code is to the left - // of it - const lines_count = session.getLength() - let margin = 0 - for( - let i = this.value_explorer.row; - i <= this.ace_editor.renderer.getLastVisibleRow(); - i++ - ) { - margin = Math.max(margin, session.getLine(i).length) - } - - // Next line sets margin based on whole file - //const margin = this.ace_editor.getSession().getScreenWidth() - - this.value_explorer.content.style.marginLeft = (margin + 1) + 'ch' - } - } - - embed_value_explorer( - state, - { - node, - index, - length, - result: { - ok, - value, - error, - version_number, - is_calltree_node_explorer - } - } - ) { - this.unembed_value_explorer() - - const session = this.ace_editor.getSession() - - let content - const container = el('div', {'class': 'embed_value_explorer_container'}, - el('div', {'class': 'embed_value_explorer_wrapper'}, - content = el('div', { - // Ace editor cannot render value_explorer before the first line. So we - // render in on the next line and apply translate - 'class': 'embed_value_explorer_content', - tabindex: 0 - }) - ) - ) - - let initial_scroll_top - - const escape = () => { - if(initial_scroll_top != null) { - // restore scroll - session.setScrollTop(initial_scroll_top) - } - if(this.value_explorer.return_to == null) { - this.focus() - } else { - this.value_explorer.return_to.focus() - } - // TODO select root in value explorer - } - - container.addEventListener('keydown', e => { - if(e.key == 'Escape') { - escape() - } - }) - - if(node != null && node.type == 'function_call') { - content.append(el('a', { - href: 'javascript: void(0)', - 'class': 'embed_value_explorer_control', - click: () => exec('step_into', index), - }, 'Step into call (Enter)')) - } - - let is_dom_el - - if(ok) { - if(value instanceof globalThis.app_window.Element && !value.isConnected) { - is_dom_el = true - if(value instanceof globalThis.app_window.SVGElement) { - // Create svg context - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') - svg.appendChild(value) - content.appendChild(svg) - } else { - content.appendChild(value) - } - } else { - is_dom_el = false - const exp = new ValueExplorer({ - container: content, - event_target: container, - on_escape: escape, - scroll_to_element: t => { - if(initial_scroll_top == null) { - initial_scroll_top = session.getScrollTop() - } - let scroll - const out_of_bottom = t.getBoundingClientRect().bottom - this.editor_container.getBoundingClientRect().bottom - if(out_of_bottom > 0) { - session.setScrollTop(session.getScrollTop() + out_of_bottom) - } - const out_of_top = this.editor_container.getBoundingClientRect().top - t.getBoundingClientRect().top - if(out_of_top > 0) { - session.setScrollTop(session.getScrollTop() - out_of_top) - } - }, - }) - - if(is_calltree_node_explorer) { - exp.render(state, value, { - is_expanded: true, - children: { - '*arguments*': {is_expanded: true}, - } - }) - } else { - exp.render( - state, - {value, [version_number_symbol]: version_number}, - {is_expanded: true}, - ) - } - - } - } else { - is_dom_el = false - content.appendChild(el('span', 'eval_error', stringify_for_header(error))) - } - - const value_explorer = this.value_explorer = { - node, - row: is_dom_el - ? session.doc.indexToPosition(index + length).row - : session.doc.indexToPosition(index).row, - fixedWidth: true, - el: container, - content, - is_dom_el, - } - - - if (!session.widgetManager) { - const LineWidgets = require("ace/line_widgets").LineWidgets; - session.widgetManager = new LineWidgets(session); - session.widgetManager.attach(this.ace_editor); - } - - if(is_dom_el) { - container.classList.add('is_dom_el') - session.widgetManager.addLineWidget(value_explorer) - } else { - container.classList.add('is_not_dom_el') - const line_height = this.ace_editor.renderer.lineHeight - content.style.transform = `translate(0px, -${line_height}px)` - // hide element before margin applied to avoid jitter - container.style.display = 'none' - session.widgetManager.addLineWidget(value_explorer) - // update_value_explorer_margin relies on getLastVisibleRow which can be - // incorrect because it may be executed right after set_cursor_position - // which is async in ace_editor. Use setTimeout - setTimeout(() => { - this.update_value_explorer_margin() - container.style.display = '' - }, 0) - } - - } - - focus_value_explorer(return_to) { - if(this.value_explorer != null) { - this.value_explorer.return_to = return_to - this.value_explorer.content.focus({preventScroll: true}) - } - } - - set_keyboard_handler(type) { - if(type != null) { - localStorage.keyboard = type - } - this.ace_editor.setKeyboardHandler( - type == 'vim' ? "ace/keyboard/vim" : null - ) - } - - init_keyboard(){ - this.set_keyboard_handler(localStorage.keyboard) - - // Intercept Enter to execute step_into if function_call selected - this.ace_editor.keyBinding.addKeyboardHandler(($data, hashId, keyString, keyCode, e) => { - if(keyString == 'return') { - if(this.value_explorer?.node?.type == 'function_call') { - exec('step_into', this.value_explorer.node.index) - return {command: "null"} // to stop other handlers - } - } - }) - - const VimApi = require("ace/keyboard/vim").CodeMirror.Vim - - // Remove commands binded to function keys that we are going to redefine - this.ace_editor.commands.removeCommand('openCommandPallete') - this.ace_editor.commands.removeCommand('toggleFoldWidget') - this.ace_editor.commands.removeCommand('goToNextError') - - - this.ace_editor.commands.bindKey("F5", "goto_definition"); - VimApi._mapCommand({ - keys: 'gd', - type: 'action', - action: 'aceCommand', - actionArgs: { name: "goto_definition" } - }) - this.ace_editor.commands.addCommand({ - name: 'goto_definition', - exec: (editor) => { - this.goto_definition() - } - }) - - - this.ace_editor.commands.bindKey("F1", "focus_value_explorer"); - this.ace_editor.commands.addCommand({ - name: 'focus_value_explorer', - exec: (editor) => { - this.focus_value_explorer() - } - }) - - - this.ace_editor.commands.bindKey("ctrl-i", 'step_into') - VimApi._mapCommand({ - keys: '\\i', - type: 'action', - action: 'aceCommand', - actionArgs: { name: "step_into" } - }) - this.ace_editor.commands.addCommand({ - name: 'step_into', - exec: (editor) => { - exec('step_into', this.get_cursor_position()) - } - }) - - - this.ace_editor.commands.bindKey("ctrl-o", 'step_out') - VimApi._mapCommand({ - keys: '\\o', - type: 'action', - action: 'aceCommand', - actionArgs: { name: "step_out" } - }) - this.ace_editor.commands.addCommand({ - name: 'step_out', - exec: (editor) => { - exec('calltree.arrow_left') - } - }) - - - this.ace_editor.commands.addCommand({ - name: 'expand_selection', - exec: () => { - exec('eval_selection', this.get_cursor_position(), true) - } - }) - this.ace_editor.commands.addCommand({ - name: 'collapse_selection', - exec: () => { - exec('eval_selection', this.get_cursor_position(), false) - } - }) - this.ace_editor.commands.bindKey("ctrl-j", 'expand_selection') - this.ace_editor.commands.bindKey("ctrl-down", 'expand_selection') - this.ace_editor.commands.bindKey("ctrl-k", 'collapse_selection') - this.ace_editor.commands.bindKey("ctrl-up", 'collapse_selection') - - - this.ace_editor.commands.addCommand({ - name: 'edit', - exec: (editor, input) => { - const module = input.args == null ? '' : input.args[0] - exec('change_current_module', module) - } - }) - VimApi.defineEx("edit", "e", function(cm, input) { - cm.ace.execCommand("edit", input) - }) - - this.ace_editor.commands.addCommand({ - name: 'buffer', - exec: (editor, input) => { - const search_query = input.args == null ? '' : input.args[0] - // TODO move to cmd.js - const module = search_query == '' - ? '' - : Object.keys(get_state().files).find(name => name.includes(search_query)) - if(module != null) { - exec('change_current_module', module) - } - } - }) - VimApi.defineEx("buffer", "b", function(cm, input) { - cm.ace.execCommand("buffer", input) - }) - - // TODO remove my custom binding - VimApi.map('jj', '', 'insert') - } - - add_marker(file, className, from, to){ - const session = this.get_session(file) - const from_pos = session.doc.indexToPosition(from) - const to_pos = session.doc.indexToPosition(to) - const markerId = session.addMarker( - new globalThis.ace.Range(from_pos.row,from_pos.column,to_pos.row,to_pos.column), - className - ) - if(this.markers[file] == null){ - this.markers[file] = [] - } - this.markers[file].push({className, from, to, markerId}) - } - - remove_markers_of_type(file, type){ - if(this.markers[file] == null){ - this.markers[file] = [] - } - const for_removal = this.markers[file].filter(h => h.className == type) - const session = this.get_session(file) - for(let marker of for_removal){ - session.removeMarker(marker.markerId) - } - this.markers[file] = this.markers[file].filter(h => h.className != type) - } - - - get_cursor_position(file){ - const session = file == null - ? this.ace_editor.getSession() - : this.get_session(file) - - if(session == null) { - // Session was not created for file - throw new Error('illegal state') - } - - return session.doc.positionToIndex(session.selection.getCursor()) - } - - set_cursor_position(index){ - if(index == null) { - throw new Error('illegal state') - } - - const pos = this.ace_editor.session.doc.indexToPosition(index) - - this.supress_change_selection(() => { - const pos = this.ace_editor.session.doc.indexToPosition(index) - this.ace_editor.moveCursorToPosition(pos) - // Moving cursor performs selection, clear it - this.ace_editor.clearSelection() - const first = this.ace_editor.renderer.getFirstVisibleRow() - const last = this.ace_editor.renderer.getLastVisibleRow() - if(pos.row < first || pos.row > last) { - this.ace_editor.scrollToLine(pos.row) - } - }) - } - - goto_definition(){ - const index = this.get_cursor_position() - exec('goto_definition', index) - } - - for_each_session(cb) { - for(let file in this.sessions) { - cb(file, this.sessions[file]) - } - } - - make_resizable() { - - const apply_height = () => { - this.editor_container.style.height = localStorage.editor_height + 'vh' - } - - let last_resize_time = new Date().getTime() - - window.addEventListener('resize', () => { - last_resize_time = new Date().getTime() - }) - - // Save editor_height on resize and restore it on reopen - if(localStorage.editor_height != null) { - apply_height() - } - - let is_first_run = true - - new ResizeObserver((e) => { - if(is_first_run) { - // Resize observer callback seems to fire immediately on create - is_first_run = false - return - } - if(new Date().getTime() - last_resize_time < 100) { - // Resize observer triggered by window resize, skip - return - } - - // See https://stackoverflow.com/a/57166828/795038 - // ace editor must be updated based on container size change - this.ace_editor.resize() - - const height = this.editor_container.offsetHeight / window.innerHeight * 100 - localStorage.editor_height = height - // resize applies height in pixels. Wait for it and apply height in vh - setTimeout(apply_height, 0) - - }).observe(this.editor_container) - } -} - diff --git a/src/editor/files.js b/src/editor/files.js deleted file mode 100644 index 1b8480b..0000000 --- a/src/editor/files.js +++ /dev/null @@ -1,262 +0,0 @@ -import {el} from './domutils.js' -import {map_find} from '../utils.js' -import {open_dir, create_file} from '../filesystem.js' -import {examples} from '../examples.js' -import { - exec, - get_state, - open_directory, - exec_and_reload_app_window, - close_directory, -} from '../index.js' - -const is_html = path => path.endsWith('.htm') || path.endsWith('.html') -const is_js = path => path == '' || path.endsWith('.js') || path.endsWith('.mjs') - -export class Files { - constructor(ui) { - this.ui = ui - this.el = el('div', 'files_container') - this.render(get_state()) - } - - change_entrypoint(entrypoint, current_module) { - exec_and_reload_app_window('change_entrypoint', entrypoint, current_module) - this.ui.editor.focus() - } - - change_html_file(e) { - const html_file = e.target.value - exec_and_reload_app_window('change_html_file', html_file) - } - - - render(state) { - this.file_to_el = new Map() - const file_actions = state.has_file_system_access - ? el('div', 'file_actions', - el('a', { - 'class': 'file_action', - href: 'javascript: void(0)', - click: this.create_file.bind(this, false), - }, - 'New file' - ), - - el('a', { - 'class': 'file_action', - href: 'javascript: void(0)', - click: this.create_file.bind(this, true), - }, - 'New dir' - ), - - el('a', { - 'class': 'file_action', - href: 'javascript: void(0)', - click: close_directory, - }, - 'Revoke access' - ), - - el('a', { - href: 'https://github.com/leporello-js/leporello-js#selecting-entrypoint-module', - target: '__blank', - "class": 'select_entrypoint_title', - title: 'Select entrypoint', - }, - 'Entry point' - ), - ) - : el('div', 'file_actions', - el('div', 'file_action allow_file_access', - el('a', { - href: 'javascript: void(0)', - click: open_directory, - }, 'Allow access to local project folder'), - el('span', 'subtitle', `Your files will never leave your device`) - ), - ) - - - - const file_elements = [ - this.render_file({name: '*scratch*', path: ''}, state), - this.render_file(state.project_dir, state), - ] - - const files = this.el.querySelector('.files') - - if(files == null) { - this.el.innerHTML = '' - this.el.appendChild(file_actions) - this.el.appendChild( - el('div', 'files', file_elements) - ) - } else { - // Replace to preserve scroll position - this.el.replaceChild(file_actions, this.el.children[0]) - files.replaceChildren(...file_elements) - } - } - - render_current_module(current_module) { - this.current_file = current_module - this.active_el.querySelector('.file_title').classList.remove('active') - this.active_el = this.file_to_el.get(current_module) - this.active_el.querySelector('.file_title').classList.add('active') - } - - render_select_entrypoint(file, state) { - if(!state.has_file_system_access || file.kind == 'directory') { - return null - } else if(is_js(file.path)) { - return el('span', 'select_entrypoint', - el('input', { - type: 'radio', - name: 'js_entrypoint', - value: file.path, - checked: state.entrypoint == file.path, - change: e => this.change_entrypoint(e.target.value), - click: e => e.stopPropagation(), - }) - ) - } else if(is_html(file.path)) { - return el('span', 'select_entrypoint', - el('input', { - type: 'radio', - name: 'html_file', - value: file.path, - checked: state.html_file == file.path, - change: e => this.change_html_file(e), - click: e => e.stopPropagation(), - }) - ) - } else { - return null - } - } - - render_file(file, state) { - const result = el('div', 'file', - el('div', { - 'class': 'file_title' + (file.path == state.current_module ? ' active' : ''), - click: e => this.on_click(e, file) - }, - el('span', 'icon', - file.kind == 'directory' - ? '\u{1F4C1}' // folder icon - : '\xa0', - ), - file.name, - this.render_select_entrypoint(file, state), - ), - file.children == null - ? null - : file.children.map(c => this.render_file(c, state)) - ) - - this.file_to_el.set(file.path, result) - - if(file.path == state.current_module) { - this.active_el = result - this.current_file = file.path - } - - return result - } - - async create_file(is_dir) { - - let name = prompt(`Enter ${is_dir ? 'directory' : 'file'} name`) - if(name == null) { - return - } - - const root = get_state().project_dir - - let dir, file - - if(this.current_file == '' /* scratch */) { - // Create in root directory - dir = root - } else { - const find_file_with_parent = (dir, parent) => { - if(dir.path == this.current_file) { - return [dir, parent] - } - if(dir.children == null) { - return null - } - return map_find(dir.children, c => find_file_with_parent(c, dir)) - } - - ([file, dir] = find_file_with_parent(root)) - - if(file.kind == 'directory') { - dir = file - } else { - if(dir == null) { - throw new Error('illegal state') - } - } - } - - const path = dir == root ? name : dir.path + '/' + name - await create_file(path, is_dir) - - // Reload all files for simplicity - open_dir(false).then(dir => { - if(is_dir) { - exec_and_reload_app_window('load_dir', dir, true) - } else { - exec_and_reload_app_window('create_file', dir, path) - } - }) - } - - - on_click(e, file) { - e.stopPropagation() - - if(get_state().has_file_system_access) { - if(file.kind != 'directory') { - exec('change_current_module', file.path) - } - } else { - if(file.path == null) { - // root of examples dir, do nothing - } else if(file.path == '') { - this.change_entrypoint('') - } else { - const find_node = n => - n.path == file.path - || - n.children != null && n.children.find(find_node) - - // find example dir - const example_dir = get_state().project_dir.children.find( - c => find_node(c) != null - ) - - // in examples mode, on click file we also change entrypoint for - // simplicity - const example = examples.find(e => e.path == example_dir.path) - this.change_entrypoint( - example.entrypoint, - file.kind == 'directory' ? undefined : file.path - ) - if(example.with_app_window && !localStorage.onboarding_open_app_window) { - this.ui.toggle_open_app_window_tooltip(true) - } - } - } - - // Note that we call render_current_module AFTER exec('change_entrypoint'), - // because in case we clicked to example dir, entrypoint would be set, and - // rendered active, so file with entrypoint would be active and not dir we - // clicked, which is weird - this.render_current_module(file.path) - - } -} diff --git a/src/editor/io_trace.js b/src/editor/io_trace.js deleted file mode 100644 index f05d026..0000000 --- a/src/editor/io_trace.js +++ /dev/null @@ -1,66 +0,0 @@ -import {header, stringify_for_header} from '../value_explorer_utils.js' -import {el} from './domutils.js' -import {has_error} from '../calltree.js' - -export class IO_Trace { - constructor(ui, el) { - this.el = el - this.ui = ui - - this.el.addEventListener('keydown', (e) => { - - if(e.key == 'Escape') { - this.ui.editor.focus() - } - - if(e.key == 'F4') { - this.ui.editor.focus() - } - - }) - } - - clear() { - this.el.innerHTML = '' - this.is_rendered = false - } - - render_io_trace(state, force) { - if(force) { - this.is_rendered = false - } - - if(this.is_rendered) { - return - } - - this.is_rendered = true - - this.el.innerHTML = '' - - const items = state.io_trace ?? [] - // Number of items that were used during execution - const used_count = state.rt_cxt.io_trace_index ?? items.length - - for(let i = 0; i < items.length; i++) { - const item = items[i] - if(item.type == 'resolution') { - continue - } - const is_used = i < used_count - this.el.appendChild( - el('div', - 'call_header ' - + (has_error(item) ? 'error ' : '') - + (is_used ? '' : 'native '), - item.name, - '(' , - item.args.map(a => header(a)).join(', '), - '): ' , - (item.ok ? stringify_for_header(item.value) : item.error.toString()) - ) - ) - } - } - -} diff --git a/src/editor/logs.js b/src/editor/logs.js deleted file mode 100644 index 9a0965f..0000000 --- a/src/editor/logs.js +++ /dev/null @@ -1,98 +0,0 @@ -import {el, scrollIntoViewIfNeeded, value_to_dom_el, join} from './domutils.js' -import {exec} from '../index.js' -import {header} from '../value_explorer_utils.js' -import {with_version_number_of_log} from '../cmd.js' - -export class Logs { - constructor(ui, el) { - this.el = el - this.ui = ui - this.el.addEventListener('keydown', (e) => { - - if(e.key == 'Escape') { - this.ui.editor.focus() - } - - if(e.key == 'Enter') { - // TODO reselect call node that was selected previously by calling - // 'calltree.navigate_logs_position' - this.ui.editor.focus() - } - - if(e.key == 'F1') { - this.ui.editor.focus_value_explorer(this.el) - } - - if(e.key == 'F3') { - this.ui.editor.focus() - } - - if(e.key == 'ArrowDown' || e.key == 'j'){ - // prevent scroll - e.preventDefault() - exec('calltree.navigate_logs_increment', 1) - } - - if(e.key == 'ArrowUp' || e.key == 'k'){ - // prevent scroll - e.preventDefault() - exec('calltree.navigate_logs_increment', -1) - } - }) - } - - rerender_logs(state, logs) { - this.el.innerHTML = '' - this.render_logs(state, null, logs) - } - - render_logs(state, prev_logs, logs) { - for( - let i = prev_logs == null ? 0 : prev_logs.logs.length ; - i < logs.logs.length; - i++ - ) - { - const log = logs.logs[i] - this.el.appendChild( - el('div', - 'log call_header ' - + (log.log_fn_name == 'error' ? 'error' : '') - // Currently console.log calls from native fns (like Array::map) - // are not recorded, so next line is dead code - + (log.module == null ? ' native' : '') - , - el('a', { - href: 'javascript: void(0)', - click: () => exec('calltree.navigate_logs_position', i), - }, - (log.module == '' ? '*scratch*' : log.module) - + ': ' - + ( - log.toplevel - ? 'toplevel' - : 'fn ' + (log.parent_name == '' ? 'anonymous' : log.parent_name) - ) - + ':' - ), - ' ', - ...join(with_version_number_of_log(state, log, () => - log.args.map(a => value_to_dom_el(a)) - )) - ) - ) - } - - if(prev_logs?.log_position != logs.log_position) { - if(prev_logs?.logs == logs.logs && prev_logs?.log_position != null) { - this.el.children[prev_logs.log_position].classList.remove('active') - } - if(logs.log_position != null) { - const active_child = this.el.children[logs.log_position] - active_child.classList.add('active') - scrollIntoViewIfNeeded(this.el, active_child) - } - } - } - -} diff --git a/src/editor/share_dialog.js b/src/editor/share_dialog.js deleted file mode 100644 index 41830e8..0000000 --- a/src/editor/share_dialog.js +++ /dev/null @@ -1,92 +0,0 @@ -import {el} from './domutils.js' -import {save_share} from '../share.js' -import {get_state} from '../index.js' - -export class ShareDialog { - constructor() { - this.el = el('dialog', 'share_dialog', - this.upload_begin = el('p', '', - el('p', '', - 'This button will upload your scratch file to the cloud for sharing with others.'), - el('ul', '', - el('li', '', - 'Please ensure that no personal data or confidential information is included.' - ), - el('li', '', - 'Avoid including copyrighted materials.' - ), - ), - el('span', {style: 'color: red'}, - 'Caution: Once shared, files cannot be deleted.' - ), - this.upload_buttons = el('p', {style: 'text-align: center'}, - el('button', { - 'class': 'upload_button', - click: () => this.upload() - }, - "Upload" - ), - this.cancel_button = el('button', { - style: 'margin-left: 1em', - click: () => this.cancel() - }, - "Cancel" - ) - ), - ), - this.uploading = el('span', {style: 'display: none'}, - "Uploading..." - ), - this.upload_finish = el('p', {style: 'display: none'}, - el('p', '', - el('p', {style: ` - text-align: center; - margin-bottom: 1em; - font-size: 1.2em - `}, 'Upload successful'), - this.url_share = el('input', { - type: 'text', - readonly: true, - style: 'min-width: 30em', - }), - this.copy_button = el('button', { - click: () => this.copy(), - style: 'margin-left: 1em', - }, 'Copy URL') - ), - this.close_button = el('button', { - style: 'display: block; margin: auto', - click: () => this.cancel(), - }, 'Close'), - ) - ) - } - - async upload() { - this.uploading.style.display = '' - this.upload_begin.style.display = 'none' - try { - await save_share(get_state().files['']) - // window location was changed inside save_share, now it points out to a - // new share - this.url_share.value = window.location.toString() - this.upload_finish.style.display = '' - } catch(e) { - alert(e.message) - this.upload_begin.style.display = '' - } finally { - this.uploading.style.display = 'none' - } - } - - copy() { - this.url_share.select() - document.execCommand('copy') - } - - cancel() { - this.upload_finish.style.display = 'none' - this.upload_begin.style.display = '' - this.el.close() - } -} diff --git a/src/editor/ui.js b/src/editor/ui.js deleted file mode 100644 index bf28cc6..0000000 --- a/src/editor/ui.js +++ /dev/null @@ -1,363 +0,0 @@ -import {exec, get_state, open_app_window, exec_and_reload_app_window} from '../index.js' -import {Editor} from './editor.js' -import {Files} from './files.js' -import {CallTree} from './calltree.js' -import {Logs} from './logs.js' -import {IO_Trace} from './io_trace.js' -import {ShareDialog} from './share_dialog.js' -import {el} from './domutils.js' -import {redraw_canvas} from '../canvas.js' - -export class UI { - constructor(container, state){ - this.open_app_window = this.open_app_window.bind(this) - - this.files = new Files(this) - - this.tabs = {} - this.debugger = {} - - container.appendChild( - (this.root = el('div', 'root', - this.editor_container = el('div', 'editor_container'), - el('div', 'bottom', - this.debugger_container = el('div', 'debugger', - this.debugger_loaded = el('div', 'debugger_wrapper', - el('div', 'tabs', - this.tabs.calltree = el('div', 'tab', - el('a', { - click: () => this.set_active_tab('calltree'), - href: 'javascript: void(0)', - }, 'Call tree (F2)') - ), - this.tabs.logs = el('div', 'tab', - el('a', { - click: () => this.set_active_tab('logs'), - href: 'javascript: void(0)', - }, 'Logs (F3)') - ), - this.tabs.io_trace = el('div', 'tab', - el('a', { - click: () => this.set_active_tab('io_trace'), - href: 'javascript: void(0)', - }, 'IO trace (F4)') - ), - ), - this.debugger.calltree = el('div', { - 'class': 'tab_content', - tabindex: 0, - }), - this.debugger.logs = el('div', { - 'class': 'tab_content logs', - tabindex: 0, - }), - this.debugger.io_trace = el('div', { - 'class': 'tab_content io_trace', - tabindex: 0, - }), - ), - this.debugger_loading = el('div', 'debugger_wrapper', - this.debugger_loading_message = el('div'), - ), - ), - this.problems_container = el('div', {"class": 'problems_container', tabindex: 0}, - this.problems = el('div'), - ) - ), - - this.files.el, - - this.statusbar = el('div', 'statusbar', - this.status = el('div', 'status'), - this.current_module = el('div', 'current_module'), - - el('a', { - 'class': 'statusbar_action first', - href: 'javascript: void(0)', - click: () => this.clear_io_trace(), - }, - 'Clear IO trace (F6)' - ), - - el('a', { - 'class': 'statusbar_action open_app_window_button', - href: 'javascript: void(0)', - click: this.open_app_window, - }, - '(Re)open app window (F7)', - this.open_app_window_tooltip = el('div', { - 'class': 'open_app_window_tooltip', - }, - 'Click here to open app window' - ) - ), - - this.options = el('div', 'options', - el('label', {'for': 'standard'}, - el('input', { - id: 'standard', - type: 'radio', - name: 'keyboard', - checked: localStorage.keyboard == 'standard' - || localStorage.keyboard == null, - change: () => { - this.editor.set_keyboard_handler('standard') - } - }), - 'Standard' - ), - el('label', {'for': 'vim'}, - el('input', { - id: 'vim', - type: 'radio', - name: 'keyboard', - checked: localStorage.keyboard == 'vim', - change: () => { - this.editor.set_keyboard_handler('vim') - } - }), - 'VIM' - ) - ), - el('a', { - 'class': 'show_help', - href: 'javascript: void(0)', - click: () => this.help_dialog.showModal(), - }, - 'Help', - ), - el('a', { - 'class': 'github', - href: 'https://github.com/leporello-js/leporello-js', - target: '__blank', - }, 'Github'), - el('button', { - 'class': 'share_button', - 'click': () => this.share_dialog.showModal(), - }, 'Share'), - this.help_dialog = this.render_help(), - this.share_dialog = new ShareDialog().el, - ) - )) - ) - - window.addEventListener('keydown', () => this.clear_status(), true) - window.addEventListener('click', () => this.clear_status(), true) - - window.addEventListener('keydown', e => { - if(e.key == 'F2') { - this.set_active_tab('calltree') - } - - if(e.key == 'F3'){ - this.set_active_tab('logs') - } - - if(e.key == 'F4'){ - this.set_active_tab('io_trace') - } - - if(e.key == 'F6'){ - this.clear_io_trace() - } - - if(e.key == 'F7'){ - this.open_app_window() - } - }) - - this.editor = new Editor(this, this.editor_container) - - this.calltree = new CallTree(this, this.debugger.calltree) - this.logs = new Logs(this, this.debugger.logs) - this.io_trace = new IO_Trace(this, this.debugger.io_trace) - - this.render_current_module(state.current_module) - - this.set_active_tab('calltree', true) - - container.addEventListener('focusin', e => { - const active = document.activeElement - let is_focus_in_editor - if(this.editor_container.contains(document.activeElement)) { - if(this.editor.has_value_explorer()) { - // depends on if we come to value explorer from editor or from debugger - is_focus_in_editor = !this.debugger_container.contains( - this.editor.value_explorer.return_to - ) - } else { - is_focus_in_editor = true - } - } else { - is_focus_in_editor = false - } - if(this.prev_is_focus_in_editor != is_focus_in_editor) { - this.prev_is_focus_in_editor= is_focus_in_editor - redraw_canvas(get_state(), is_focus_in_editor) - } - }) - } - - set_active_tab(tab_id, skip_focus = false) { - this.active_tab = tab_id - Object.values(this.tabs).forEach(el => el.classList.remove('active')) - this.tabs[tab_id].classList.add('active') - Object.values(this.debugger).forEach(el => el.style.display = 'none') - this.debugger[tab_id].style.display = 'block' - - if(tab_id == 'io_trace') { - this.io_trace.render_io_trace(get_state(), false) - } - - if(!skip_focus) { - this.debugger[tab_id].focus() - } - - if(tab_id == 'calltree' && !skip_focus) { - exec('calltree.show_value_explorer') - } - } - - clear_io_trace() { - exec_and_reload_app_window('clear_io_trace') - } - - open_app_window() { - this.toggle_open_app_window_tooltip(false) - localStorage.onboarding_open_app_window = true - open_app_window(get_state()) - } - - render_debugger_loading(state) { - this.debugger_container.style = '' - this.problems_container.style = 'display: none' - - this.debugger_loaded.style = 'display: none' - this.debugger_loading.style = '' - - this.debugger_loading_message.innerText = - state.loading_external_imports_state != null - ? 'Loading external modules...' - : 'Executing code...' - } - - render_debugger(state) { - this.debugger_container.style = '' - this.problems_container.style = 'display: none' - - this.debugger_loading.style = 'display: none' - this.debugger_loaded.style = '' - - this.calltree.render_calltree(state) - this.logs.render_logs(state, null, state.logs) - } - - render_io_trace(state) { - // render lazily, only if selected - if(this.active_tab == 'io_trace') { - this.io_trace.render_io_trace(state, true) - } else { - // Do not render until user switch to the tab - this.io_trace.clear() - } - } - - render_problems(problems) { - this.debugger_container.style = 'display: none' - this.problems_container.style = '' - this.problems.innerHTML = '' - problems.forEach(p => { - const s = this.editor.get_session(p.module) - const pos = s.doc.indexToPosition(p.index) - const module = p.module == '' ? "*scratch*" : p.module - this.problems.appendChild( - el('div', 'problem', - el('a', { - href: 'javascript:void(0)', - click: () => exec('goto_problem', p) - }, - `${module}:${pos.row + 1}:${pos.column} - ${p.message}` - ) - ) - ) - }) - } - - set_status(text){ - this.current_module.style = 'display: none' - this.status.style = '' - this.status.innerText = text - } - - clear_status(){ - this.render_current_module(get_state().current_module) - } - - render_current_module(current_module) { - this.status.style = 'display: none' - this.current_module.innerText = - current_module == '' - ? '*scratch*' - : current_module - this.current_module.style = '' - } - - render_help() { - const options = [ - ['Focus value explorer', 'F1'], - ['Navigate value explorer', '← → ↑ ↓ or hjkl'], - ['Leave value explorer', 'F1 or Esc'], - ['Focus call tree view', 'F2'], - ['Navigate call tree view', '← → ↑ ↓ or hjkl'], - ['Leave call tree view', 'F2 or Esc'], - ['Focus console logs', 'F3'], - ['Navigate console logs', '↑ ↓ or jk'], - ['Leave console logs', 'F3 or Esc'], - ['Focus IO trace', 'F4'], - ['Leave IO trace', 'F4 or Esc'], - ['Jump to definition', 'F5', 'gd'], - ['Expand selection to eval expression', 'Ctrl-↓ or Ctrl-j'], - ['Collapse selection', 'Ctrl-↑ or Ctrl-k'], - ['Step into call', 'Ctrl-i', '\\i'], - ['Step out of call', 'Ctrl-o', '\\o'], - ['When in call tree view, jump to return statement', 'Enter'], - ['When in call tree view, jump to function arguments', 'a'], - ['When in call tree view, jump to error origin', 'e'], - ['Clear IO trace', 'F6'], - ['(Re)open run window (F7)', 'F7'], - ] - return el('dialog', 'help_dialog', - el('table', 'help', - el('thead', '', - el('th', '', 'Action'), - el('th', 'key', 'Standard'), - el('th', 'key', 'VIM'), - ), - el('tbody', '', - options.map(([text, standard, vim]) => - el('tr', '', - el('td', '', text), - el('td', - vim == null - ? {'class': 'key spanned', colspan: 2} - : {'class': 'key'}, - standard - ), - vim == null - ? null - : el('td', 'key', vim), - ) - ) - ) - ), - el('form', {method: 'dialog'}, - el('button', null, 'Close'), - ), - ) - } - - toggle_open_app_window_tooltip(on) { - this.open_app_window_tooltip.classList.toggle('on', on) - } - -} diff --git a/src/editor/value_explorer.js b/src/editor/value_explorer.js deleted file mode 100644 index d186add..0000000 --- a/src/editor/value_explorer.js +++ /dev/null @@ -1,304 +0,0 @@ -// TODO paging for large arrays/objects -// TODO show Errors in red -// TODO fns as clickable links (jump to definition), both for header and for -// content - -import {el, scrollIntoViewIfNeeded} from './domutils.js' -import {with_code_execution} from '../index.js' -// TODO remove is_expandble, join with displayed entries -import {header, short_header, is_expandable, displayed_entries} from '../value_explorer_utils.js' -import {with_version_number} from '../runtime/runtime.js' -import {is_versioned_object, get_version_number} from '../calltree.js' - -const node_props_by_path = (state, o, path) => { - if(is_versioned_object(o)) { - return with_version_number( - state.rt_cxt, - get_version_number(o), - () => node_props_by_path(state, o.value, path), - ) - } - if(path.length != 0) { - const [start, ...rest] = path - const value = displayed_entries(o).find(([k,v]) => k == start)[1] - return node_props_by_path(state, value, rest) - } else { - return { - displayed_entries: displayed_entries(o), - header: header(o), - short_header: short_header(o), - is_exp: is_expandable(o), - } - } -} - -export class ValueExplorer { - - constructor({ - container, - event_target = container, - scroll_to_element, - on_escape = () => {}, - } = {} - ) { - this.container = container - this.scroll_to_element = scroll_to_element - this.on_escape = on_escape - - event_target.addEventListener('keydown', e => with_code_execution(() => { - - /* - Right - - - does not has children - nothing - - has children - first click expands, second jumps to first element - - Left - - - root - nothing - - not root collapse node, goes to parent if already collapsed - - Up - goes to prev visible element - Down - goes to next visible element - - Click - select and toggles expand - */ - - if(e.key == 'F1') { - this.on_escape() - return - } - - const current_node = node_props_by_path(this.state, this.value, this.current_path) - - if(e.key == 'ArrowDown' || e.key == 'j'){ - // Do not scroll - e.preventDefault() - - if(current_node.is_exp && this.is_expanded(this.current_path)) { - this.select_path(this.current_path.concat( - current_node.displayed_entries[0][0] - )) - } else { - const next = p => { - if(p.length == 0) { - return null - } - const parent = p.slice(0, p.length - 1) - const children = node_props_by_path(this.state, this.value, parent) - .displayed_entries - const child_index = children.findIndex(([k,v]) => - k == p[p.length - 1] - ) - const next_child = children[child_index + 1] - if(next_child == null) { - return next(parent) - } else { - return [...parent, next_child[0]] - } - } - - const next_path = next(this.current_path) - if(next_path != null) { - this.select_path(next_path) - } - } - } - - if(e.key == 'ArrowUp' || e.key == 'k'){ - // Do not scroll - e.preventDefault() - - if(this.current_path.length == 0) { - this.on_escape() - return - } - const parent = this.current_path.slice(0, this.current_path.length - 1) - const children = node_props_by_path(this.state, this.value, parent).displayed_entries - const child_index = children.findIndex(([k,v]) => - k == this.current_path[this.current_path.length - 1] - ) - const next_child = children[child_index - 1] - if(next_child == null) { - this.select_path(parent) - } else { - const last = p => { - const node_props = node_props_by_path(this.state, this.value, p) - if(!node_props.is_exp || !this.is_expanded(p)) { - return p - } else { - const children = node_props - .displayed_entries - .map(([k,v]) => k) - return last([...p, children[children.length - 1]]) - - } - } - this.select_path(last([...parent, next_child[0]])) - } - } - - if(e.key == 'ArrowLeft' || e.key == 'h'){ - // Do not scroll - e.preventDefault() - - const is_expanded = this.is_expanded(this.current_path) - if(!current_node.is_exp || !is_expanded) { - if(this.current_path.length != 0) { - const parent = this.current_path.slice(0, this.current_path.length - 1) - this.select_path(parent) - } else { - this.on_escape() - } - } else { - this.toggle_expanded() - } - } - - if(e.key == 'ArrowRight' || e.key == 'l'){ - // Do not scroll - e.preventDefault() - - if(current_node.is_exp) { - const is_expanded = this.is_expanded(this.current_path) - if(!is_expanded) { - this.toggle_expanded() - } else { - const children = node_props_by_path(this.state, this.value, this.current_path) - .displayed_entries - this.select_path( - [ - ...this.current_path, - children[0][0], - ] - ) - } - } - } - })) - } - - get_node_data(path, node_data = this.node_data) { - if(path.length == 0) { - return node_data - } else { - const [start, ...rest] = path - return this.get_node_data(rest, node_data.children[start]) - } - } - - is_expanded(path) { - return this.get_node_data(path).is_expanded - } - - on_click(path) { - this.select_path(path) - this.toggle_expanded() - } - - render(state, value, node_data) { - this.state = state - this.value = value - this.node_data = node_data - const path = [] - this.container.appendChild(this.render_value_explorer_node(path, this.node_data)) - this.select_path(path) - } - - select_path(current_path) { - if(this.current_path != null) { - this.set_active(this.current_path, false) - } - this.current_path = current_path - this.set_active(this.current_path, true) - // Check that was already added to document - if(document.contains(this.container)) { - const target = this.get_node_data(current_path).el.getElementsByClassName('value_explorer_header')[0] - if(this.scroll_to_element == null) { - scrollIntoViewIfNeeded(this.container.parentNode, target) - } else { - this.scroll_to_element(target) - } - } - } - - set_active(path, is_active) { - const el = this.get_node_data(path).el.getElementsByClassName('value_explorer_header')[0] - if(is_active) { - el.classList.add('active') - } else { - el.classList.remove('active') - } - } - - set_expanded(fn) { - if(typeof(fn) == 'boolean') { - return this.set_expanded(() => fn) - } - const val = this.is_expanded(this.current_path) - const data = this.get_node_data(this.current_path) - data.is_expanded = fn(data.is_expanded) - const prev_dom_node = data.el - const next = this.render_value_explorer_node(this.current_path, data) - prev_dom_node.parentNode.replaceChild(next, prev_dom_node) - } - - toggle_expanded() { - this.set_expanded(e => !e) - this.set_active(this.current_path, true) - } - - render_value_explorer_node(path, node_data) { - return with_code_execution(() => ( - this.do_render_value_explorer_node(path, node_data) - ), this.state) - } - - do_render_value_explorer_node(path, node_data) { - const key = path.length == 0 - ? null - : path[path.length - 1] - - const {displayed_entries, header, short_header, is_exp} - = node_props_by_path(this.state, this.value, path) - - const is_expanded = is_exp && node_data.is_expanded - - node_data.children ??= {} - - const result = el('div', 'value_explorer_node', - - el('span', { - class: 'value_explorer_header', - click: this.on_click.bind(this, path), - }, - is_exp - ? (is_expanded ? '▼' : '▶') - : '\xa0', - - key == null - ? null - : el('span', 'value_explorer_key', key.toString(), ': '), - - key == null || !is_exp || !is_expanded - // Full header - ? header - // Short header - : key == '*arguments*' - ? '' - : short_header - ), - - (is_exp && is_expanded) - ? displayed_entries.map(([k,v]) => { - node_data.children[k] ??= {} - return this.do_render_value_explorer_node([...path, k], node_data.children[k]) - }) - : [] - ) - - node_data.el = result - - return result - } - - -} diff --git a/src/effects.js b/src/effects.js deleted file mode 100644 index 25c2732..0000000 --- a/src/effects.js +++ /dev/null @@ -1,352 +0,0 @@ -import {write_file} from './filesystem.js' -import {write_example} from './examples.js' -import {color_file} from './color.js' -import { - root_calltree_node, - calltree_node_loc, - get_deferred_calls -} from './calltree.js' -import {current_cursor_position} from './calltree.js' -import {exec, reload_app_window, FILES_ROOT} from './index.js' -import {redraw_canvas} from './canvas.js' - -// Imports in the context of `app_window`, so global variables in loaded -// modules refer to that window's context -const import_in_app_window = url => { - return new globalThis.app_window.Function('url', ` - return import(url) - `)(url) -} - -const load_external_imports = async state => { - if(state.loading_external_imports_state == null) { - return - } - const urls = state.loading_external_imports_state.external_imports - const results = await Promise.allSettled( - urls.map(u => import_in_app_window( - /^\w+:\/\//.test(u) - ? // starts with protocol, import as is - u - : // local path, load using File System Access API, see service_worker.js - // Append special URL segment that will be intercepted in service worker - // Note that we use the same origin as current page (where Leporello - // is hosted), so Leporello can access window object for custom - // `html_file` - FILES_ROOT + '/' + u - )) - ) - const modules = Object.fromEntries( - results.map((r, i) => ( - [ - urls[i], - { - ok: r.status == 'fulfilled', - error: r.reason, - module: r.value, - } - ] - )) - ) - exec('external_imports_loaded', state /* becomes prev_state */, modules) -} - -const ensure_session = (ui, state, file = state.current_module) => { - ui.editor.ensure_session(file, state.files[file]) -} - -const clear_file_coloring = (ui, file) => { - ui.editor.remove_markers_of_type(file, 'evaluated_ok') - ui.editor.remove_markers_of_type(file, 'evaluated_error') -} - -const clear_coloring = ui => { - ui.editor.for_each_session((file, session) => clear_file_coloring(ui, file)) -} - -const render_coloring = (ui, state) => { - const file = state.current_module - - clear_file_coloring(ui, file) - - color_file(state, file).forEach(c => { - ui.editor.add_marker( - file, - c.result.ok - ? 'evaluated_ok' - : 'evaluated_error', - c.index, - c.index + c.length - ) - }) -} - -const render_parse_result = (ui, state) => { - ui.editor.for_each_session((file, session) => { - ui.editor.remove_markers_of_type(file, 'error-code') - session.clearAnnotations() - }) - - if(!state.parse_result.ok){ - - ui.editor.for_each_session((file, session) => { - session.setAnnotations( - state.parse_result.problems - .filter(p => p.module == file) - .map(p => { - const pos = session.doc.indexToPosition(p.index) - return { - row: pos.row, - column: pos.column, - text: p.message, - type: "error", - } - }) - ) - }) - - state.parse_result.problems.forEach(problem => { - ensure_session(ui, state, problem.module) - // TODO unexpected end of input - ui.editor.add_marker( - problem.module, - 'error-code', - problem.index, - // TODO check if we can show token - problem.token == null - ? problem.index + 1 - : problem.index + problem.token.length - ) - }) - - ui.render_problems(state.parse_result.problems) - } else { - // Ensure session for each loaded module - Object.keys(state.parse_result.modules).forEach(file => { - ensure_session(ui, state, file) - }) - } -} - -export const render_initial_state = (ui, state, example) => { - ensure_session(ui, state) - ui.editor.switch_session(state.current_module) - if( - example != null - && example.with_app_window - && !localStorage.onboarding_open_app_window - ) { - ui.toggle_open_app_window_tooltip(true) - } -} - -export const apply_side_effects = (prev, next, ui, cmd) => { - if(prev.project_dir != next.project_dir) { - ui.files.render(next) - } - - if(prev.current_module != next.current_module) { - ui.files.render_current_module(next.current_module) - } - - if(prev.current_module != next.current_module) { - localStorage.current_module = next.current_module - ui.render_current_module(next.current_module) - } - - if(prev.entrypoint != next.entrypoint) { - localStorage.entrypoint = next.entrypoint - } - if(prev.html_file != next.html_file) { - localStorage.html_file = next.html_file - } - - if(prev.current_module != next.current_module) { - ensure_session(ui, next) - ui.editor.unembed_value_explorer() - ui.editor.switch_session(next.current_module) - } - - // Do not set cursor position on_deferred_call, because editor may be in the middle of the edition operation - if(current_cursor_position(next) != ui.editor.get_cursor_position() && cmd != 'on_deferred_call') { - ui.editor.set_cursor_position(current_cursor_position(next)) - } - - if(prev.loading_external_imports_state != next.loading_external_imports_state) { - load_external_imports(next) - } - - if( - prev.eval_modules_state != next.eval_modules_state - && - next.eval_modules_state != null - ) { - const s = next.eval_modules_state - s.promise.then(result => { - exec('eval_modules_finished', - next, /* becomes prev_state */ - result, - ) - }) - } - - if(prev.parse_result != next.parse_result) { - render_parse_result(ui, next) - } - - if(!next.parse_result.ok) { - - ui.calltree.clear_calltree() - clear_coloring(ui) - - } else { - - if( - prev.calltree == null - || - prev.calltree_changed_token != next.calltree_changed_token - ) { - - // code finished executing - - const is_loading = - next.loading_external_imports_state != null - || - next.eval_modules_state != null - if(next.rt_cxt?.io_trace_is_replay_aborted) { - reload_app_window() - } else if(is_loading) { - ui.calltree.clear_calltree() - clear_coloring(ui) - ui.render_debugger_loading(next) - } else { - // Rerender entire calltree - ui.render_debugger(next) - clear_coloring(ui) - render_coloring(ui, next) - ui.logs.rerender_logs(next, next.logs) - - if( - prev.io_trace != next.io_trace - || - prev.rt_cxt?.io_trace_index != next.rt_cxt.io_trace_index - ) { - ui.render_io_trace(next) - } - } - - } else { - - // code was already executed before current action - - if(get_deferred_calls(prev) == null && get_deferred_calls(next) != null) { - ui.calltree.render_deferred_calls(next) - } - - if( - prev.calltree != next.calltree - || - prev.calltree_node_is_expanded != next.calltree_node_is_expanded - ) { - ui.calltree.render_expand_node(prev, next) - } - - const node_changed = next.current_calltree_node != prev.current_calltree_node - - if(node_changed) { - ui.calltree.render_select_node(prev, next) - } - - if(prev.colored_frames != next.colored_frames) { - render_coloring(ui, next) - } - - ui.logs.render_logs(next, prev.logs, next.logs) - - - // Redraw canvas - if( - prev.current_calltree_node != next.current_calltree_node - || - prev.calltree_node_is_expanded != next.calltree_node_is_expanded - ) { - redraw_canvas(next, ui.is_focus_in_editor) - } - } - } - - // Render - - /* Eval selection */ - - if(prev.selection_state != next.selection_state) { - ui.editor.remove_markers_of_type(next.current_module, 'selection') - const node = next.selection_state?.node - if(node != null) { - ui.editor.add_marker( - next.current_module, - 'selection', - node.index, - node.index + node.length - ) - } - } - - - // Value explorer - if(prev.value_explorer != next.value_explorer) { - if(next.value_explorer == null) { - ui.editor.unembed_value_explorer() - } else { - ui.editor.embed_value_explorer(next, next.value_explorer) - } - } -} - - -export const EFFECTS = { - set_focus: (_state, _args, ui) => { - ui.editor.focus() - }, - - set_status: (state, [msg], ui) => { - ui.set_status(msg) - }, - - save_to_localstorage(state, [key, value]){ - localStorage[key] = value - }, - - write: (state, [name, contents], ui, prev_state) => { - if(name == '') { - const share_id = new URL(window.location).searchParams.get('share_id') - if(share_id == null) { - localStorage['code'] = contents - } else { - const key = 'share_' + share_id - if(localStorage['code'] == prev_state.files['']) { - /* - If scratch code is the same with share code, then update both - - Imagine the following scenario: - - - User shares code. URL is replaced with ?share_id=XXX - - He keeps working on code - - He closes browser tab and on the next day he opens app.leporello.tech - - His work is lost (actually, he can still access it with - ?share_id=XXX, but that not obvious - - To prevent that, we keep updating scratch code after sharing - */ - localStorage['code'] = contents - } - localStorage[key] = contents - } - } else if(state.has_file_system_access) { - write_file(name, contents) - } else { - write_example(name, contents) - } - } -} - diff --git a/src/eval.js b/src/eval.js deleted file mode 100644 index fec708e..0000000 --- a/src/eval.js +++ /dev/null @@ -1,1589 +0,0 @@ -import { - zip, - stringify, - map_object, - filter_object, -} from './utils.js' - -import { - find_fn_by_location, - find_declaration, - find_leaf, - collect_destructuring_identifiers, - map_destructuring_identifiers, - map_tree, -} from './ast_utils.js' - -import {has_toplevel_await} from './find_definitions.js' -// external -import {with_version_number} from './runtime/runtime.js' - -// import runtime as external because it has non-functional code -// external -import {run, do_eval_expand_calltree_node} from './runtime/runtime.js' -// external -import {LetMultiversion} from './runtime/let_multiversion.js' - -// TODO: fix error messages. For example, "__fn is not a function" - -/* -Generate code that records all function invocations. - -for each invokation, record - - function that was called with its closed variables - - args - - return value (or exception) - - child invocations (deeper in the stack) - -When calling function, we check if it is native or not (call it hosted). If -it is native, we record invocation at call site. If it is hosted, we dont -record invocation at call site, but function expression was wrapped in code -that records invocation. So its call will be recorded. - -Note that it is not enough to record all invocation at call site, because -hosted function can be called by native functions (for example Array::map). - -For each invocation, we can replay function body with metacircular interpreter, -collecting information for editor -*/ - -/* -type ToplevelCall = { - toplevel: true, - code, - ok, - value, - error, - children -} -type Call = { - args, - code, - fn, - ok, - value, - error, - children, -} -type Node = ToplevelCall | Call -*/ - -const codegen_function_expr = (node, node_cxt) => { - const do_codegen = n => codegen(n, node_cxt) - - const args = node.function_args.children.map(do_codegen).join(',') - - const decl = node.is_arrow - ? `(${args}) => ` - : `function(${args})` - - // TODO gensym __obj, __fn, __call_id, __let_vars, __literals - const prolog = - '{const __call_id = __cxt.call_counter;' - + ( - node.is_async - ? 'let __await_state;' - : '' - ) - + ( - node.has_versioned_let_vars - ? 'const __let_vars = __cxt.let_vars;' - : '' - ) - + 'const __literals = __cxt.literals;'; - - const call = (node.is_async ? 'async ' : '') + decl + ( - (node.body.type == 'do') - ? prolog + do_codegen(node.body) + '}' - : prolog + 'return ' + do_codegen(node.body) + '}' - ) - - const argscount = node - .function_args - .children - .find(a => a.type == 'destructuring_rest') == null - ? node.function_args.children.length - : null - - const location = `{index: ${node.index}, length: ${node.length}, module: '${node_cxt.module}'}` - - // TODO first create all functions, then assign __closure, after everything - // is declared. See 'out of order decl' test. Currently we assign __closure - // on first call (see `__trace`) - const get_closure = `() => ({${[...node.closed].join(',')}})` - - return `__trace(__cxt, ${call}, "${node.name}", ${argscount}, ${location}, \ -${get_closure}, ${node.has_versioned_let_vars})` -} - -/* - in v8 `foo().bar().baz()` gives error `foo(...).bar(...).baz is not a function` -*/ -const not_a_function_error = node => node.string.replaceAll( - new RegExp('\\(.*\\)', 'g'), - '(...)' -) - -// TODO if statically can prove that function is hosted, then do not codegen -// __trace -const codegen_function_call = (node, node_cxt) => { - - const do_codegen = n => codegen(n, node_cxt) - - const args = `[${node.args.children.map(do_codegen).join(',')}]` - - const errormessage = not_a_function_error(node.fn) - - let call - if(node.fn.type == 'member_access') { - const op = node.fn.is_optional_chaining ? '?.' : '' - - // TODO gensym __obj, __fn - // We cant do `codegen(obj)[prop].bind(codegen(obj))` because codegen(obj) - // can be expr we dont want to eval twice. Use comma operator to perform - // assignments in expression context - return `( - __obj = ${do_codegen(node.fn.object)}, - __fn = __obj${op}[${do_codegen(node.fn.property)}], - __trace_call(__cxt, __fn, __obj, ${args}, ${JSON.stringify(errormessage)}) - )` - } else { - return `__trace_call(__cxt, ${do_codegen(node.fn)}, null, ${args}, \ -${JSON.stringify(errormessage)})` - } - -} - -// Note that we use 'node.index + 1' as index, which -// does not correspond to any ast node, we just use it as a convenient -// marker -export const get_after_if_path = node => node.index + 1 - -const codegen = (node, node_cxt) => { - - const do_codegen = n => codegen(n, node_cxt) - - if(node.type == 'identifier') { - - if(node.definition == 'self' || node.definition == 'global') { - return node.value - } - - if(node.definition.index == null) { - throw new Error('illegal state') - } - if( - !node_cxt.literal_identifiers && - find_leaf(node_cxt.toplevel, node.definition.index).is_versioned_let_var - ) { - return node.value + '.get()' - } - - return node.value - } else if([ - 'number', - 'string_literal', - 'builtin_identifier', - 'backtick_string', - ].includes(node.type)){ - return node.value - } else if(node.type == 'do'){ - return [ - // hoist function decls to the top - ...node.children.filter(s => s.type == 'function_decl'), - ...node.children.filter(s => s.type != 'function_decl'), - ].reduce( - (result, stmt) => result + (do_codegen(stmt)) + ';\n', - '' - ) - } else if(node.type == 'return') { - if(node.expr == null) { - return 'return ;' - } else { - return 'return ' + do_codegen(node.expr) + ';' - } - } else if(node.type == 'throw') { - return 'throw ' + do_codegen(node.expr) + ';' - } else if(node.type == 'if') { - const codegen_branch = branch => - `{ __save_ct_node_for_path(__cxt, __calltree_node_by_loc, ${branch.index}, __call_id);` - + do_codegen(branch) - + '}' - const left = 'if(' + do_codegen(node.cond) + ')' - + codegen_branch(node.branches[0]) - const result = node.branches[1] == null - ? left - : left + ' else ' + codegen_branch(node.branches[1]) - // add path also for point after if statement, in case there was a return - // inside if statement. - return result + - `__save_ct_node_for_path(__cxt, __calltree_node_by_loc, ` - + `${get_after_if_path(node)}, __call_id);` - } else if(node.type == 'array_literal'){ - return '__create_array([' - + node.elements.map(c => do_codegen(c)).join(', ') - + `], __cxt, ${node.index}, __literals)` - } else if(node.type == 'object_literal'){ - const elements = - node.elements.map(el => { - if(el.type == 'object_spread'){ - return do_codegen(el) - } else if(el.type == 'identifier') { - const value = do_codegen(el) - return el.value + ': ' + value - } else if(el.type == 'key_value_pair') { - return '[' + do_codegen(el.key.type == 'computed_property' ? el.key.expr : el.key) + ']' - + ': (' + do_codegen(el.value, el) + ')' - } else { - throw new Error('unknown node type ' + el.type) - } - }) - .join(',') - return `__create_object({${elements}}, __cxt, ${node.index}, __literals)` - } else if(node.type == 'function_call'){ - return codegen_function_call(node, node_cxt) - } else if(node.type == 'function_expr'){ - return codegen_function_expr(node, node_cxt) - } else if(node.type == 'ternary'){ - const branches = node.branches.map(branch => - `(__save_ct_node_for_path(__cxt, __calltree_node_by_loc, ${branch.index}, __call_id), ` - + do_codegen(branch) - + ')' - ) - return '' - + '(' - + do_codegen(node.cond) - + ')\n? ' - + branches[0] - +'\n: ' - + branches[1] - } else if(['const', 'let', 'assignment'].includes(node.type)) { - const prefix = node.type == 'assignment' - ? '' - : node.type + ' ' - return prefix + node - .children - .map(c => { - if(node.type == 'let') { - let lefthand, righthand - if(c.type == 'identifier') { - // let decl without assignment, like 'let x' - lefthand = c - if(!lefthand.is_versioned_let_var) { - return lefthand.value - } - } else if(c.type == 'decl_pair') { - lefthand = c.children[0], righthand = c.children[1] - if(lefthand.type != 'identifier') { - // TODO - // See comment for 'simple_decl_pair' in 'src/parse_js.js' - // Currently it is unreachable - throw new Error('illegal state') - } - } else { - throw new Error('illegal state') - } - if(lefthand.is_versioned_let_var) { - const name = lefthand.value - const symbol = symbol_for_let_var(lefthand) - return name + ( - righthand == null - ? ` = __let_vars['${symbol}'] = new __Multiversion(__cxt)` - : ` = __let_vars['${symbol}'] = new __Multiversion(__cxt, ${do_codegen(righthand)})` - ) - } // Otherwise goes to the end of the func - } - - if(node.type == 'assignment' && c.children[0].type != 'member_access') { - const [lefthand, righthand] = c.children - if(lefthand.type != 'identifier') { - // TODO - // See comment for 'simple_decl_pair' in 'src/parse_js.js' - // Currently it is unreachable - throw new Error('TODO: illegal state') - } - const let_var = find_leaf(node_cxt.toplevel, lefthand.definition.index) - if(let_var.is_versioned_let_var){ - return lefthand.value + '.set(' + do_codegen(righthand, node) + ')' - } - } - - return do_codegen(c.children[0]) + ' = ' + do_codegen(c.children[1]) - }) - .join(',') - + ';' - + node.children.map(decl => { - if( - node.type != 'assignment' - && - decl.type != 'identifier' - && - decl.name_node.type == 'identifier' - && - decl.expr.type == 'function_call' - ) { - // deduce function name from variable it was assigned to if anonymous - // works for point-free programming, like - // const parse_statement = either(parse_import, parse_assignment, ...) - return ` - if( - typeof(${decl.name_node.value}) == 'function' - && - ${decl.name_node.value}.name == 'anonymous' - ) { - Object.defineProperty( - ${decl.name_node.value}, - "name", - {value: "${decl.name_node.value}"} - ); - } - ` - } else { - return '' - } - }) - .join('') - } else if(node.type == 'member_access'){ - return '(' - + do_codegen(node.object) - + (node.is_optional_chaining ? ')?.[' : ')[') - + do_codegen(node.property) - + ']' - } else if(node.type == 'unary') { - if(node.operator == 'await') { - return `(__await_state = __await_start(__cxt, ${do_codegen(node.expr)}),` + - `await __await_state.promise,` + - `__await_finish(__cxt, __await_state))` - } else { - return '(' + node.operator + ' ' + do_codegen(node.expr) + ')' - } - } else if(node.type == 'binary'){ - return '' - + do_codegen(node.args[0]) - + ' ' - + node.operator - + ' ' - + do_codegen(node.args[1]) - } else if(node.type == 'array_spread' || node.type == 'object_spread'){ - return '...(' + do_codegen(node.expr) + ')' - } else if(node.type == 'new') { - const args = `[${node.args.children.map(do_codegen).join(',')}]` - const errormessage = not_a_function_error(node.constructor) - return `__trace_call(__cxt, ${do_codegen(node.constructor)}, null, ${args},\ -${JSON.stringify(errormessage)}, true)` - } else if(node.type == 'grouping'){ - return '(' + do_codegen(node.expr) + ')' - } else if(node.type == 'array_destructuring') { - return '[' + node.elements.map(n => do_codegen(n)).join(', ') + ']' - } else if(node.type == 'object_destructuring') { - return '{' + node.elements.map(n => do_codegen(n)).join(', ') + '}' - } else if(node.type == 'destructuring_rest') { - return '...' + do_codegen(node.name_node) - } else if(node.type == 'destructuring_default') { - return do_codegen(node.name_node) + ' = ' + do_codegen(node.expr); - } else if(node.type == 'destructuring_pair') { - return do_codegen(node.key) + ' : ' + do_codegen(node.value); - } else if(node.type == 'import') { - let names, def - if(node.default_import != null) { - def = `default: ${node.default_import},` - names = node.children.slice(1).map(n => n.value) - } else { - def = '' - names = node.children.map(n => n.value) - } - return `const {${def} ${names.join(',')}} = __cxt.modules['${node.full_import_path}'];`; - } else if(node.type == 'export') { - if(node.is_default) { - return `__cxt.modules['${node_cxt.module}'].default = ${do_codegen(node.children[0])};` - } else { - const identifiers = node - .binding - .children - .flatMap(n => collect_destructuring_identifiers(n.name_node)) - .map(i => i.value) - return do_codegen(node.binding) - + - `Object.assign(__cxt.modules['${node_cxt.module}'], {${identifiers.join(',')}});` - } - } else if(node.type == 'function_decl') { - const expr = node.children[0] - return `const ${expr.name} = ${codegen_function_expr(expr, node_cxt)};` - } else { - console.error(node) - throw new Error('unknown node type: ' + node.type) - } -} - -export const eval_modules = ( - parse_result, - external_imports, - on_deferred_call, - calltree_changed_token, - io_trace, - storage, -) => { - // TODO gensym __cxt, __trace, __trace_call, __calltree_node_by_loc, - // __await_start, __await_finish, __Multiversion, __create_array, __create_object - - // TODO bug if module imported twice, once as external and as regular - - const is_async = has_toplevel_await(parse_result.modules) - - const Function = is_async - ? globalThis.app_window.eval('(async function(){})').constructor - : globalThis.app_window.Function - - const module_fns = parse_result.sorted.map(module => { - const code = codegen( - parse_result.modules[module], - { - module, - toplevel: parse_result.modules[module], - } - ) - return { - module, - // TODO refactor, instead of multiple args prefixed with '__', pass - // single arg called `runtime` - - fn: new Function( - '__cxt', - '__let_vars', - '__literals', - '__calltree_node_by_loc', - '__trace', - '__trace_call', - '__await_start', - '__await_finish', - '__save_ct_node_for_path', - '__Multiversion', - '__create_array', - '__create_object', - - '"use strict";\n' + - - /* Add dummy __call_id for toplevel. It does not make any sence - * (toplevel is executed only once unlike function), we only add it - * because we dont want to codegen differently for if statements in - * toplevel and if statements within functions*/ - - 'const __call_id = __cxt.call_counter;' + - 'let __await_state, __obj, __fn;' + - code - ) - } - }) - - const cxt = { - modules: external_imports == null - ? {} - : map_object(external_imports, (name, {module}) => module), - - call_counter: 0, - version_counter: 0, - children: null, - prev_children: null, - // TODO use native array for stack for perf? stack contains booleans - stack: new Array(), - - is_recording_deferred_calls: false, - on_deferred_call: (call, calltree_changed_token, logs) => { - return on_deferred_call( - assign_code(parse_result.modules, call), - calltree_changed_token, - logs, - ) - }, - calltree_changed_token, - is_toplevel_call: true, - - window: globalThis.app_window, - - storage, - - canvas_ops: {ops: [], contexts: new Set()}, - } - - const result = run(module_fns, cxt, io_trace) - - const make_result = result => { - const calltree = assign_code(parse_result.modules, result.calltree) - return { - modules: result.modules, - logs: result.logs, - rt_cxt: result.rt_cxt, - calltree, - calltree_node_by_loc: result.calltree_node_by_loc, - io_trace: result.rt_cxt.io_trace, - } - } - - if(is_async) { - // convert app_window.Promise to host Promise - return Promise.resolve(result).then(make_result) - } else { - return make_result(result) - } -} - - -export const eval_expand_calltree_node = (cxt, parse_result, node) => { - return assign_code( - parse_result.modules, - do_eval_expand_calltree_node(cxt, node) - ) -} - -// TODO: assign_code: benchmark and use imperative version for perf? -const assign_code = (modules, call) => { - if(call.toplevel) { - return {...call, - code: modules[call.module], - children: call.children && call.children.map(call => assign_code(modules, call)), - } - } else { - return {...call, - code: call.fn == null || call.fn.__location == null - ? null - // TODO cache find_fn_by_location calls? - : find_fn_by_location(modules[call.fn.__location.module], call.fn.__location), - children: call.children && call.children.map(call => assign_code(modules, call)), - } - } -} - - -/* ------------- Metacircular interpreter ---------------------------- */ - -/* -Evaluate single function call - -For each statement or expression, calculate if it was executed or not. - -Add evaluation result to each statement or expression and put it to `result` -prop. Evaluate expressions from leaves to root, substituting function calls for -already recorded results. - -Add `result` prop to each local variable. - -Eval statements from top to bottom, selecting effective if branch and stopping -on `return` and `throw`. When descending to nested blocks, take scope into -account -*/ - -// Workaround with statement forbidden in strict mode (imposed by ES6 modules) -// Also currently try/catch is not implemented TODO -export const eval_codestring = (codestring, scope) => - // Note that we eval code in context of app_window - (new (globalThis.app_window.Function)('codestring', 'scope', - // Make a copy of `scope` to not mutate it with assignments - ` - try { - return {ok: true, value: eval('with({...scope}){' + codestring + '}')} - } catch(error) { - return {ok: false, error, is_error_origin: true} - } - ` - ))(codestring, scope) - -const get_args_scope = (fn_node, args, closure) => { - const arg_names = - collect_destructuring_identifiers(fn_node.function_args) - .map(i => i.value) - - const destructuring = fn_node - .function_args - .children.map(n => codegen(n, {literal_identifiers: true})) - .join(',') - - /* - // TODO gensym __args. Or - new Function(` - return ({foo, bar}) => ({foo, bar}) - `)(args) - - to avoid gensym - */ - const codestring = `(([${destructuring}]) => [${arg_names.join(',')}])(__args)` - - const {ok, value, error} = eval_codestring(codestring, { - ...closure, - __args: args, - }) - - if(!ok) { - // TODO show exact destructuring error - return {ok, error, is_error_origin: true} - } else { - return { - ok, - value: Object.fromEntries( - zip( - arg_names, - value, - ) - ), - } - } -} - -const eval_binary_expr = (node, eval_cxt, frame_cxt) => { - const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt) - if(!ok) { - return {ok, children, eval_cxt: next_eval_cxt} - } - - const op = node.operator - const a = children[0].result.value - const b = children[1].result.value - const value = (new Function('a', 'b', ' return a ' + op + ' b'))(a, b) - return {ok, children, value, eval_cxt: next_eval_cxt} -} - -const is_symbol_for_let_var = symbol => - symbol.startsWith('!') - -const symbol_for_let_var = let_var_node => - '!' + let_var_node.value + '_' + let_var_node.index - -const symbol_for_closed_let_var = name => - '!' + name + '_' + 'closed' - -/* - For versioned let vars, any function call within expr can mutate let vars, so - it can change current scope and outer scopes. Since current scope and outer - scope can have let var with the same name, we need to address them with - different names. Prepend '!' symbol because it is not a valid js identifier, - so it will not be mixed with other variables -*/ -const symbol_for_identifier = (node, frame_cxt) => { - if(node.definition == 'global') { - return node.value - } - - const index = node.definition == 'self' ? node.index : node.definition.index - const declaration = find_declaration(frame_cxt.calltree_node.code, index) - - const variable = node.definition == 'self' - ? node - : find_leaf(frame_cxt.calltree_node.code, node.definition.index) - if(declaration == null) { - /* - Variable was declared in outer scope. Since there can be only one variable - with given name in outer scope, generate a name for it - */ - return symbol_for_closed_let_var(node.value) - } - if(declaration.type == 'let'){ - return symbol_for_let_var(variable) - } else { - return node.value - } -} - -const do_eval_frame_expr = (node, eval_cxt, frame_cxt) => { - if(node.type == 'identifier') { - let value - if(node.definition == 'global') { - value = globalThis.app_window[node.value] - } else { - value = eval_cxt.scope[symbol_for_identifier(node, frame_cxt)] - if(value instanceof LetMultiversion) { - value = value.get_version(eval_cxt.version_number) - } - } - return { - ok: true, - value, - eval_cxt, - } - } else if([ - 'builtin_identifier', - 'number', - 'string_literal', - 'backtick_string', - ].includes(node.type)){ - // TODO exprs inside backtick string - // TODO for string literal and number, do not use eval - return {...eval_codestring(node.value, eval_cxt.scope), eval_cxt} - } else if(node.type == 'array_spread') { - const result = eval_children(node, eval_cxt, frame_cxt) - if(!result.ok) { - return result - } - const child = result.children[0] - if((typeof(child.result.value?.[Symbol.iterator])) == 'function') { - return result - } else { - return { - ok: false, - children: result.children, - eval_cxt: result.eval_cxt, - error: new TypeError(child.string + ' is not iterable'), - is_error_origin: true, - } - } - } else if([ - 'object_spread', - 'key_value_pair', - 'computed_property' - ].includes(node.type)) { - return eval_children(node, eval_cxt, frame_cxt) - } else if(node.type == 'array_literal'){ - const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt) - if(!ok) { - return {ok, children, eval_cxt: next_eval_cxt} - } - const value = frame_cxt.calltree_node.literals.get(node.index) - return {ok, children, value, eval_cxt: next_eval_cxt} - } else if(node.type == 'call_args'){ - const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt) - if(!ok) { - return {ok, children, eval_cxt: next_eval_cxt} - } - const value = children.reduce( - (arr, el) => { - if(el.type == 'array_spread') { - return [...arr, ...el.children[0].result.value] - } else { - return [...arr, el.result.value] - } - }, - [], - ) - return {ok, children, value, eval_cxt: next_eval_cxt} - } else if(node.type == 'object_literal'){ - const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt) - if(!ok) { - return {ok, children, eval_cxt: next_eval_cxt} - } - const value = frame_cxt.calltree_node.literals.get(node.index) - return {ok, children, value, eval_cxt: next_eval_cxt} - } else if(node.type == 'function_call' || node.type == 'new'){ - const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt) - if(!ok) { - return {ok: false, children, eval_cxt: next_eval_cxt} - } else { - if(typeof(children[0].result.value) != 'function') { - return { - ok: false, - error: frame_cxt.calltree_node.error, - is_error_origin: true, - children, - eval_cxt: next_eval_cxt, - } - } - const calls = frame_cxt.calltree_node.children - const call = calls == null ? null : calls[next_eval_cxt.call_index] - if(call == null) { - throw new Error('illegal state') - } - - return { - ok: call.ok, - call, - value: call.value, - error: call.error, - is_error_origin: !call.ok, - children, - eval_cxt: { - ...next_eval_cxt, - call_index: next_eval_cxt.call_index + 1, - version_number: call.last_version_number, - }, - } - } - } else if(node.type == 'function_expr'){ - // It will never be called, create empty function - // TODO use new Function constructor with code? - const fn_placeholder = Object.defineProperty( - () => {}, - 'name', - {value: node.name} - ) - return { - ok: true, - value: fn_placeholder, - eval_cxt, - children: node.children, - } - } else if(node.type == 'ternary') { - const {node: cond_evaled, eval_cxt: eval_cxt_after_cond} = eval_frame_expr( - node.cond, - eval_cxt, - frame_cxt, - ) - const {ok, value} = cond_evaled.result - const branches = node.branches - if(!ok) { - return { - ok: false, - children: [cond_evaled, branches[0], branches[1]], - eval_cxt: eval_cxt_after_cond, - } - } else { - const {node: branch_evaled, eval_cxt: eval_cxt_after_branch} - = eval_frame_expr( - branches[value ? 0 : 1], - eval_cxt_after_cond, - frame_cxt - ) - const children = value - ? [cond_evaled, branch_evaled, branches[1]] - : [cond_evaled, branches[0], branch_evaled] - const ok = branch_evaled.result.ok - if(ok) { - return {ok, children, eval_cxt: eval_cxt_after_branch, - value: branch_evaled.result.value} - } else { - return {ok, children, eval_cxt: eval_cxt_after_branch} - } - } - } else if(node.type == 'member_access'){ - // TODO prop should only be evaluated if obj is not null, if optional - // chaining - const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt) - if(!ok) { - return {ok: false, children, eval_cxt: next_eval_cxt} - } - - const [obj, prop] = children - - - let value - - if(obj.result.value == null) { - if(!node.is_optional_chaining) { - return { - ok: false, - error: new TypeError('Cannot read properties of ' - + obj.result.value - + ` (reading '${prop.result.value}')` - ), - is_error_origin: true, - children, - eval_cxt: next_eval_cxt, - } - } else { - value = undefined - } - } else { - value = with_version_number(frame_cxt.rt_cxt, obj.result.version_number, () => - obj.result.value[prop.result.value] - ) - } - return { - ok: true, - value, - children, - eval_cxt: next_eval_cxt, - } - } else if(node.type == 'unary') { - const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt) - if(!ok) { - return {ok: false, children, eval_cxt: next_eval_cxt} - } else { - const expr = children[0] - let ok, value, error, is_error_origin - if(node.operator == '!') { - ok = true - value = !expr.result.value - } else if(node.operator == 'typeof') { - ok = true - value = typeof(expr.result.value) - } else if(node.operator == '-') { - ok = true - value = - expr.result.value - } else if(node.operator == 'await') { - if(expr.result.value instanceof globalThis.app_window.Promise) { - const status = expr.result.value.status - if(status == null) { - // Promise must be already resolved - throw new Error('illegal state') - } else { - ok = status.ok - error = status.error - value = status.value - is_error_origin = !ok - } - } else { - ok = true - value = expr.result.value - } - } else { - throw new Error('unknown op') - } - return {ok, children, value, error, is_error_origin, eval_cxt: next_eval_cxt} - } - } else if(node.type == 'binary' && !['&&', '||', '??'].includes(node.operator)){ - - return eval_binary_expr(node, eval_cxt, frame_cxt) - - } else if(node.type == 'binary' && ['&&', '||', '??'].includes(node.operator)){ - const {node: left_evaled, eval_cxt: next_eval_cxt} = eval_frame_expr( - node.children[0], - eval_cxt, - frame_cxt - ) - - const {ok, value} = left_evaled.result - if( - !ok - || - (node.operator == '&&' && !value) - || - (node.operator == '||' && value) - || - (node.operator == '??' && value != null) - ) { - return { - ok, - value, - children: [left_evaled, node.children[1]], - eval_cxt: next_eval_cxt, - } - } else { - return eval_binary_expr(node, eval_cxt, frame_cxt) - } - - } else if(node.type == 'grouping'){ - const {ok, children, eval_cxt: next_eval_cxt} = eval_children(node, eval_cxt, frame_cxt) - if(!ok) { - return {ok, children, eval_cxt: next_eval_cxt} - } else { - return {ok: true, children, value: children[0].result.value, eval_cxt: next_eval_cxt} - } - } else { - console.error(node) - throw new Error('unknown node type: ' + node.type) - } -} - -const eval_children = (node, eval_cxt, frame_cxt) => { - return node.children.reduce( - ({ok, children, eval_cxt}, child) => { - let next_child, next_ok, next_eval_cxt - if(!ok) { - next_child = child - next_ok = false - next_eval_cxt = eval_cxt - } else { - const result = eval_frame_expr(child, eval_cxt, frame_cxt) - next_child = result.node - next_ok = next_child.result.ok - next_eval_cxt = result.eval_cxt - } - return { - ok: next_ok, - children: [...children, next_child], - eval_cxt: next_eval_cxt, - } - }, - {ok: true, children: [], eval_cxt} - ) -} - -const eval_frame_expr = (node, eval_cxt, frame_cxt) => { - const {ok, error, is_error_origin, value, call, children, eval_cxt: next_eval_cxt} - = do_eval_frame_expr(node, eval_cxt, frame_cxt) - - return { - node: { - ...node, - children, - // Add `call` for step_into - result: { - ok, - error, - value, - call, - is_error_origin, - version_number: next_eval_cxt.version_number, - } - }, - eval_cxt: next_eval_cxt, - } -} - -const eval_decl_pair = (s, eval_cxt, frame_cxt, is_assignment) => { - // TODO default values for destructuring can be function calls - - const {node, eval_cxt: next_eval_cxt} - = eval_frame_expr(s.expr, eval_cxt, frame_cxt) - const s_expr_evaled = {...s, children: [s.name_node, node]} - if(!node.result.ok) { - return { - ok: false, - node: {...s_expr_evaled, result: {ok: false}}, - eval_cxt: next_eval_cxt, - } - } - - const name_nodes = collect_destructuring_identifiers(s.name_node) - const names = name_nodes.map(n => n.value) - const destructuring = codegen(s.name_node, {literal_identifiers: true}) - - // TODO if destructuring is just one id, then do not use eval_codestring - - // TODO unique name for __value (gensym) - const codestring = ` - const ${destructuring} = __value; - ({${names.join(',')}}); - ` - const {ok, value: values, error} = eval_codestring( - codestring, - {...next_eval_cxt.scope, __value: node.result.value} - ) - - const next_scope = Object.fromEntries( - name_nodes.map(n => { - const prev_value = next_eval_cxt.scope[symbol_for_identifier(n, frame_cxt)] - const next_value = prev_value instanceof LetMultiversion - ? prev_value - : values[n.value] - return [ - symbol_for_identifier(n, frame_cxt), - next_value, - ] - }) - ) - - // Currently assignment_count is always zero or one, because we do not allow - // destructuring assignment of let vars like `({foo, bar} = something)` - const assignment_count = name_nodes.filter(n => { - const value = next_eval_cxt.scope[symbol_for_identifier(n, frame_cxt)] - return value instanceof LetMultiversion - }).length - - const next_version_number = is_assignment - ? next_eval_cxt.version_number + assignment_count - : next_eval_cxt.version_number - - // TODO fine-grained destructuring error, only for identifiers that failed - // destructuring - const name_node_with_result = map_tree( - map_destructuring_identifiers( - s.name_node, - node => { - let result - if(!ok) { - result = { - ok, - error, - } - } else { - let value = next_scope[symbol_for_identifier(node, frame_cxt)] - if(value instanceof LetMultiversion) { - value = value.get_version(next_version_number) - } - result = { - ok, - value, - version_number: next_version_number, - } - } - return {...node, result} - } - ), - n => - // TODO this should set result for default values in destructuring - // Currently not implemented - // TODO version_number - n.result == null - ? {...n, result: {ok}} - : n - ) - - const s_evaled = {...s_expr_evaled, children: [ - name_node_with_result, - s_expr_evaled.children[1], - ]} - - if(!ok) { - return { - ok: false, - // TODO assign error to node where destructuring failed, not to every node - node: {...s_evaled, result: {ok, error, is_error_origin: true}}, - eval_cxt: next_eval_cxt, - } - } - - return { - ok: true, - node: {...s_evaled, result: {...node.result, call: null}}, - eval_cxt: { - ...next_eval_cxt, - scope: {...next_eval_cxt.scope, ...next_scope}, - version_number: next_version_number, - } - } -} - -const eval_assignment_pair = (s, eval_cxt, context) => { - const [lefthand, righthand] = s.children - if(lefthand.type != 'member_access') { - throw new Error('illegal state') - } - - // TODO it also evals value of member access which we dont need. We should - // eval obj, prop, and then righthand - // TODO prop and righthand only evaluated if obj is not null??? - const {node: lefthand_evaled, eval_cxt: lefthand_eval_cxt} = - eval_frame_expr(lefthand, eval_cxt, context) - if(!lefthand_evaled.result.ok) { - return { - ok: false, - eval_cxt: lefthand_eval_cxt, - node: {...s, result: {ok: false}, children: [lefthand_evaled, righthand]}, - } - } - - const {node: righthand_evaled, eval_cxt: righthand_eval_cxt} = - eval_frame_expr(righthand, lefthand_eval_cxt, context) - - if(!righthand_evaled.result.ok) { - return { - ok: false, - eval_cxt: righthand_eval_cxt, - node: { - ...s, - result: {ok: false}, - children: [lefthand_evaled, righthand_evaled], - }, - } - } - - const next_version_number = righthand_eval_cxt.version_number + 1 - - return { - ok: true, - eval_cxt: { - ...righthand_eval_cxt, - version_number: next_version_number, - }, - node: {...s, - result: righthand_evaled.result, - children: [ - { - ...lefthand_evaled, - result: { - ...righthand_evaled.result, - }, - }, - righthand_evaled, - ] - }, - } -} - -const eval_statement = (s, eval_cxt, frame_cxt) => { - if(s.type == 'do') { - const stmt = s - // hoist function decls to the top - const function_decls = s.children - .filter(s => s.type == 'function_decl') - .map(s => { - const {ok, children, eval_cxt: next_eval_cxt} = eval_children(s, eval_cxt, frame_cxt) - if(!ok) { - // Function decl can never fail - throw new Error('illegal state') - } - if(eval_cxt != next_eval_cxt) { - throw new Error('illegal state') - } - return {...s, children, result: {ok: true}} - }) - - const hoisted_functions_scope = Object.fromEntries( - function_decls.map(decl => - [decl.children[0].name, decl.children[0].result.value] - ) - ) - - const initial_scope = {...eval_cxt.scope, ...hoisted_functions_scope} - const initial = { - ok: true, - returned: false, - children: [], - eval_cxt: {...eval_cxt, scope: initial_scope} - } - - const {ok, returned, children, eval_cxt: next_eval_cxt} = - s.children.reduce( ({ok, returned, children, eval_cxt}, s) => { - if(returned || !ok) { - return {ok, returned, eval_cxt, children: [...children, s]} - } else if(s.type == 'function_decl') { - const node = function_decls.find(decl => decl.index == s.index) - return { - ok: true, - returned: false, - node, - eval_cxt, - children: [...children, node], - } - } else { - const { - ok, - returned, - node, - eval_cxt: next_eval_cxt, - } = eval_statement(s, eval_cxt, frame_cxt) - return { - ok, - returned, - eval_cxt: next_eval_cxt, - children: [...children, node], - } - } - }, - initial, - ) - const let_vars_scope = filter_object(next_eval_cxt.scope, (k, v) => - is_symbol_for_let_var(k) - ) - return { - ok, - node: {...s, children: children, result: {ok}}, - returned, - eval_cxt: { - ...next_eval_cxt, - scope: {...eval_cxt.scope, ...let_vars_scope}, - } - } - } else if(['let', 'const', 'assignment'].includes(s.type)) { - const stmt = s - - const initial = {ok: true, children: [], eval_cxt} - - const {ok, children, eval_cxt: next_eval_cxt} = s.children.reduce( - ({ok, children, eval_cxt}, s) => { - if(!ok) { - return {ok, eval_cxt, children: [...children, s]} - } - if(stmt.type == 'let' && s.type == 'identifier') { - // Just bare let declaration without initial value - const value = undefined - const node = {...s, result: {ok: true, value, version_number: eval_cxt.version_number}} - return { - ok, - children: [...children, node], - eval_cxt, - } - } - let result - if(s.type == 'decl_pair') { - result = eval_decl_pair(s, eval_cxt, frame_cxt, stmt.type == 'assignment') - } else if(s.type == 'assignment_pair') { - result = eval_assignment_pair(s, eval_cxt, frame_cxt) - } else { - throw new Error('illegal state') - } - const { - ok: next_ok, - node, - eval_cxt: next_eval_cxt, - } = result - return { - ok: next_ok, - eval_cxt: next_eval_cxt, - children: [...children, node], - } - }, - initial - ) - return { - ok, - node: {...s, children, result: {ok}}, - eval_cxt: next_eval_cxt, - } - } else if(s.type == 'return') { - - if(s.expr == null) { - return { - ok: true, - returned: true, - node: {...s, result: {ok: true}}, - eval_cxt, - } - } - - const {node, eval_cxt: next_eval_cxt} = - eval_frame_expr(s.expr, eval_cxt, frame_cxt) - - return { - ok: node.result.ok, - returned: node.result.ok, - node: {...s, children: [node], result: {ok: node.result.ok}}, - eval_cxt: next_eval_cxt, - } - - } else if(s.type == 'export') { - const {ok, eval_cxt: next_eval_cxt, node} - = eval_statement(s.binding, eval_cxt, frame_cxt) - return { - ok, - eval_cxt: next_eval_cxt, - node: {...s, children: [node], result: {ok: node.result.ok}} - } - } else if(s.type == 'import') { - const module = frame_cxt.modules[s.full_import_path] - const children = s.children.map((imp, i) => ( - {...imp, - result: { - ok: true, - value: imp.definition.is_default - ? module['default'] - : module[imp.value], - // For imports, we show version for the moment of module toplevel - // starts execution - version_number: frame_cxt.calltree_node.version_number, - } - } - )) - const imported_scope = Object.fromEntries( - children.map(imp => [imp.value, imp.result.value]) - ) - return { - ok: true, - eval_cxt: { - ...eval_cxt, - scope: {...eval_cxt.scope, ...imported_scope} - }, - node: {...s, children, result: {ok: true}} - } - } else if(s.type == 'if') { - - const {node, eval_cxt: eval_cxt_after_cond} = - eval_frame_expr(s.cond, eval_cxt, frame_cxt) - - if(!node.result.ok) { - return { - ok: false, - node: {...s, children: [node, ...s.branches], result: {ok: false}}, - eval_cxt: eval_cxt_after_cond, - } - } - - if(s.branches.length == 1) { - // if without else - if(node.result.value) { - // Execute branch - const { - node: evaled_branch, - returned, - eval_cxt: eval_cxt_after_branch, - } = eval_statement( - s.branches[0], - eval_cxt_after_cond, - frame_cxt - ) - return { - ok: evaled_branch.result.ok, - returned, - node: {...s, - children: [node, evaled_branch], - result: {ok: evaled_branch.result.ok} - }, - eval_cxt: eval_cxt_after_branch, - } - } else { - // Branch is not executed - return { - ok: true, - node: {...s, children: [node, s.branches[0]], result: {ok: true}}, - eval_cxt: eval_cxt_after_cond, - } - } - } else { - // if with else - const active_branch = node.result.value ? s.branches[0] : s.branches[1] - - const { - node: evaled_branch, - returned, - eval_cxt: eval_cxt_after_branch, - } = eval_statement( - active_branch, - eval_cxt_after_cond, - frame_cxt, - ) - - const children = node.result.value - ? [node, evaled_branch, s.branches[1]] - : [node, s.branches[0], evaled_branch] - - return { - ok: evaled_branch.result.ok, - returned, - node: {...s, children, result: {ok: evaled_branch.result.ok}}, - eval_cxt: eval_cxt_after_branch, - } - } - - } else if(s.type == 'throw') { - - const {node, eval_cxt: next_eval_cxt} = - eval_frame_expr(s.expr, eval_cxt, frame_cxt) - - return { - ok: false, - node: {...s, - children: [node], - result: { - ok: false, - is_error_origin: node.result.ok, - error: node.result.ok ? node.result.value : null, - } - }, - eval_cxt: next_eval_cxt, - } - - } else { - // stmt type is expression - const {node, eval_cxt: next_eval_cxt} = eval_frame_expr(s, eval_cxt, frame_cxt) - return { - ok: node.result.ok, - node, - eval_cxt: next_eval_cxt, - } - } -} - -const check_eval_result = result => { - // Ensure that eval_cxt is only constructed within eval_frame - if(!result.eval_cxt.__eval_cxt_marker) { - throw new Error('illegal state') - } - - // Commented for performance - - // Check that every node with result has version_number property - - //function check_version_number(node) { - // if(node.result != null && node.result.ok && node.result.value != null) { - // if(node.result.version_number == null) { - // console.error(node) - // throw new Error('bad node') - // } - // } - // if(node.children != null) { - // node.children.forEach(c => check_version_number(c)) - // } - //} - - //check_version_number(result.node) -} - -export const eval_frame = (calltree_node, modules, rt_cxt) => { - if(calltree_node.has_more_children) { - throw new Error('illegal state') - } - const node = calltree_node.code - const frame_cxt = {calltree_node, modules, rt_cxt} - if(node.type == 'do') { - // eval module toplevel - const eval_result = eval_statement( - node, - { - __eval_cxt_marker: true, - scope: calltree_node.let_vars, - call_index: 0, - version_number: calltree_node.version_number, - }, - frame_cxt, - ) - - check_eval_result(eval_result) - - return eval_result.node - } else { - // TODO default values for destructuring can be function calls - - const version_number = calltree_node.version_number - - const args_scope_result = { - ...get_args_scope( - node, - calltree_node.args, - calltree_node.fn.__closure - ), - version_number, - } - - // TODO fine-grained destructuring error, only for identifiers that - // failed destructuring - const function_args_with_result = { - ...node.function_args, - result: args_scope_result, - children: node.function_args.children.map(arg => - map_tree( - map_destructuring_identifiers( - arg, - a => ({...a, - result: { - ok: args_scope_result.ok, - error: args_scope_result.ok ? null : args_scope_result.error, - is_error_origin: !args_scope_result.ok, - value: !args_scope_result.ok ? null : args_scope_result.value[a.value], - version_number, - } - }) - ), - n => n.result == null - // TODO this should set result for default values in destructuring - // Currently not implemented - // TODO version_number - ? {...n, result: {ok: args_scope_result.ok}} - : n - ) - ) - } - - const body = node.body - - if(!args_scope_result.ok) { - return {...node, - result: {ok: false}, - children: [function_args_with_result, body], - } - } - - const closure_scope = { - ...Object.fromEntries( - Object.entries(calltree_node.fn.__closure).map(([key, value]) => { - return [ - symbol_for_closed_let_var(key), - value, - ] - }) - ), - ...calltree_node.let_vars, - } - - const scope = {...closure_scope, ...args_scope_result.value} - - let nextbody - - const eval_cxt = { - __eval_cxt_marker: true, - scope, - call_index: 0, - version_number: calltree_node.version_number, - } - - let eval_result - if(body.type == 'do') { - eval_result = eval_statement(body, eval_cxt, frame_cxt) - } else { - eval_result = eval_frame_expr(body, eval_cxt, frame_cxt) - } - - check_eval_result(eval_result) - - return {...node, - result: {ok: eval_result.node.result.ok}, - children: [function_args_with_result, eval_result.node], - } - } -} diff --git a/src/examples.js b/src/examples.js deleted file mode 100644 index 24aac2d..0000000 --- a/src/examples.js +++ /dev/null @@ -1,97 +0,0 @@ -export const write_example = (name, contents) => { - localStorage['examples_' + name] = contents -} - -const read_example = name => { - return localStorage['examples_' + name] -} - -export const examples = [ - { - path: 'github_api', - entrypoint: 'github_api/index.js', - }, - { - path: 'fibonacci', - entrypoint: 'fibonacci/index.js', - }, - { - path: 'todos-preact', - entrypoint: 'todos-preact/index.js', - with_app_window: true, - files: [ - 'todos-preact/app.js', - ] - }, - { - path: 'ethers', - entrypoint: 'ethers/block_by_timestamp.js', - }, - { - path: 'plot', - entrypoint: 'plot/index.js', - }, - - { - path: 'fractal_tree', - entrypoint: 'fractal_tree/fractal_tree.js', - with_app_window: true, - }, - { - path: 'animated_fractal_tree', - entrypoint: 'animated_fractal_tree/animated_fractal_tree.js', - with_app_window: true, - }, - { - path: 'canvas_animation_bubbles', - entrypoint: 'canvas_animation_bubbles/bubbles.js', - with_app_window: true, - }, -].map(e => ({...e, entrypoint: e.entrypoint ?? e.path})) - -const files_list = examples - .map(e => { - return (e.files ?? []).concat([e.entrypoint]) - }) - .flat() - .map(l => l.split('/')) - -const get_children = path => { - const children = files_list.filter(l => path.every((elem, i) => elem == l[i] )) - const files = children.filter(c => c.length == path.length + 1) - const dirs = [...new Set(children - .filter(c => c.length != path.length + 1) - .map(c => c[path.length]) - )] - return Promise.all(files.map(async f => { - const name = f[path.length] - const filepath = f.slice(0, path.length + 1).join('/') - return { - name, - path: filepath, - kind: 'file', - contents: - read_example(filepath) ?? - await fetch(globalThis.location.origin + '/docs/examples/'+ filepath) - .then(r => r.text()), - } - }) - .concat(dirs.map(async d => { - const p = [...path, d] - return { - name: d, - path: p.join('/'), - kind: 'directory', - children: await get_children(p), - } - }))) -} - -export const examples_dir_promise = get_children([]).then(children => { - return { - kind: 'directory', - name: 'examples', - path: null, - children, - } -}) diff --git a/src/filesystem.js b/src/filesystem.js deleted file mode 100644 index 2f9e0b0..0000000 --- a/src/filesystem.js +++ /dev/null @@ -1,174 +0,0 @@ -// code is borrowed from -// https://googlechrome.github.io/samples/service-worker/post-message/ -const send_message = (message) => { - return new Promise(function(resolve) { - const messageChannel = new MessageChannel(); - messageChannel.port1.onmessage = function(event) { - resolve(event.data) - }; - if(navigator.serviceWorker.controller == null) { - // Service worker will be available after reload - window.location.reload() - } - navigator.serviceWorker.controller.postMessage(message, - [messageChannel.port2]); - }); -} - -export const close_dir = () => { - send_message({type: 'SET_DIR_HANDLE', data: null}) - clearInterval(keepalive_interval_id) - keepalive_interval_id = null -} - -let dir_handle - -let keepalive_interval_id - -/* -Service worker is killed by the browser after 40 seconds of inactivity see -https://github.com/mswjs/msw/issues/367 - -There is hard 5 minute limit on service worker lifetime See -https://chromium.googlesource.com/chromium/src/+/master/docs/security/service-worker-security-faq.md#do-service-workers-live-forever - -Keep reviving serivce worker, so when user reloads page, dir_handle is picked -up from service worker -*/ -const keep_service_worker_alive = () => { - if(keepalive_interval_id != null) { - return - } - keepalive_interval_id = setInterval(() => { - send_message({type: 'SET_DIR_HANDLE', data: dir_handle}) - }, 10_000) -} - -const request_directory_handle = async () => { - dir_handle = await globalThis.showDirectoryPicker() - await send_message({type: 'SET_DIR_HANDLE', data: dir_handle}) - return dir_handle -} - -export const init_window_service_worker = window => { - window.navigator.serviceWorker.ready.then(() => { - window.navigator.serviceWorker.addEventListener('message', e => { - if(e.data.type == 'GET_DIR_HANDLE') { - e.ports[0].postMessage(dir_handle) - } - }) - }) -} - -const load_persisted_directory_handle = () => { - return navigator.serviceWorker.register('service_worker.js') - .then(() => navigator.serviceWorker.ready) - /* - Main window also provides dir_handle to service worker, together with - app_window. app_window provides dir_handle to service worker when it - issues fetch event. If clientId is '' then service worker will try to get - dir_handle from main window - */ - .then(() => init_window_service_worker(globalThis)) - .then(() => send_message({type: 'GET_DIR_HANDLE'})) - .then(async h => { - if(h == null || (await h.queryPermission()) != 'granted') { - return null - } - // test if directory handle is valid - try { - await h.entries().next() - } catch(e) { - return null - } - dir_handle = h - return dir_handle - }) -} - -const file_handle = async (dir_handle, filename, is_directory = false, options) => { - if(typeof(filename) == 'string') { - filename = filename.split('/') - } - const [first, ...rest] = filename - if(rest.length == 0) { - return is_directory - ? await dir_handle.getDirectoryHandle(first, options) - : await dir_handle.getFileHandle(first, options) - } else { - const nested_dir_handle = await dir_handle.getDirectoryHandle(first) - return file_handle(nested_dir_handle, rest, is_directory, options) - } -} - -export const write_file = async (name, contents) => { - const f_hanlde = await file_handle(dir_handle, name) - // Create a FileSystemWritableFileStream to write to. - const writable = await f_hanlde.createWritable() - // Write the contents of the file to the stream. - await writable.write(contents) - // Close the file and write the contents to disk. - await writable.close() -} - -// Blacklist hidden dirs and node_modules -const is_blacklisted = h => h.name == 'node_modules' || h.name.startsWith('.') - -const read_file = async handle => { - const file_data = await handle.getFile() - return await file_data.text() -} - -const do_open_dir = async (handle, path) => { - if(handle.kind == 'directory') { - const children = [] - for await (let [name, h] of handle) { - if(!is_blacklisted(h)) { - children.push(h) - } - } - return { - name: handle.name, - path, - kind: 'directory', - children: (await Promise.all( - children.map(c => - do_open_dir(c, path == null ? c.name : path + '/' + c.name) - ) - )).sort((a, b) => a.name.localeCompare(b.name)) - } - } else if(handle.kind == 'file') { - return { - name: handle.name, - path, - kind: 'file', - contents: await read_file(handle) - } - } else { - throw new Error('unknown kind') - } -} - -export const create_file = (path, is_dir) => { - return file_handle( - dir_handle, - path, - is_dir, - {create: true} - ) -} - -export const open_dir = async (should_request_access) => { - let handle - if(should_request_access) { - handle = await request_directory_handle() - } else { - handle = await load_persisted_directory_handle() - } - if(handle == null) { - return null - } else { - keep_service_worker_alive() - } - return do_open_dir(handle, null) -} diff --git a/src/find_definitions.js b/src/find_definitions.js deleted file mode 100644 index 86137ac..0000000 --- a/src/find_definitions.js +++ /dev/null @@ -1,365 +0,0 @@ -// TODO rename to analyze.js - -import {set_push, set_diff, set_union, map_object, map_find, uniq, uniq_by} - from './utils.js' -import {collect_destructuring_identifiers, collect_imports, ancestry, find_leaf} - from './ast_utils.js' - -const map_find_definitions = (nodes, mapper) => { - const result = nodes.map(mapper) - const undeclared = result.reduce( - (acc, el) => el.undeclared == null ? acc : acc.concat(el.undeclared), - [] - ) - const closed = result.map(r => r.closed).reduce(set_union, new Set()) - return { - nodes: result.map(r => r.node), - undeclared, - closed, - } -} - -const scope_from_node = n => { - if(n.type == 'import') { - return Object.fromEntries( - n.children.map(i => [i.value, i]) - ) - } else if(n.type == 'export'){ - return scope_from_node(n.binding) - } else if(n.type == 'let' || n.type == 'const') { - return Object.fromEntries( - n.children - .flatMap(collect_destructuring_identifiers) - .map(node => [ - node.value, node - ]) - ) - } else if(n.type == 'function_decl') { - // Return null because of hoisting. We take function decls into account - // first before processing statements one by one - return null - } else { - return null - } -} - -const add_trivial_definition = node => { - if(node.type == 'identifier') { - return {...node, definition: 'self'} - } else if(['destructuring_default', 'destructuring_rest'].includes(node.type)){ - return {...node, - children: [add_trivial_definition(node.name_node), ...node.children.slice(1)] - } - } else if(node.type == 'destructuring_pair') { - return {...node, children: [ - node.children[0], // key - add_trivial_definition(node.children[1]), // value - ]} - } else if(['array_destructuring', 'object_destructuring'].includes(node.type)) { - return {...node, children: node.children.map(add_trivial_definition)} - } else if (node.type == 'decl_pair') { - return { - ...node, - children: [add_trivial_definition(node.children[0]), ...node.children.slice(1)] - } - } else { - console.error(node) - throw new Error('not implemented') - } -} - -/* - * The function does these things: - * - For each occurence of identifier, attaches definition to this identifier - * - For each closure, attaches 'closed` property with set of vars it closes - * over - * - Finds undeclared identifiers - * - * `scope` is names that are already defined and can be used immediately. - * `closure_scope` is names that are defined but not yet assigned, but they - * will be assigned by the time the closures would be called - */ - -const DEFAULT_GLOBALS = new Set(['leporello']) // Leporello.js API - -export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, module_name) => { - - // sanity check - if(!(globals instanceof Set)) { - throw new Error('not a set') - } - - if(ast.type == 'identifier'){ - if(ast.definition != null) { - // Definition previously added by add_trivial_definition - return {node: ast, undeclared: null, closed: new Set([ast.value])} - } else { - const definition = scope[ast.value] - if(definition == null){ - if(globals.has(ast.value) || DEFAULT_GLOBALS.has(ast.value)) { - return {node: {...ast, definition: 'global'}, undeclared: null, closed: new Set()} - } else { - return {node: ast, undeclared: [ast], closed: new Set()} - } - } else { - return { - node: {...ast, definition: {index: definition.index}}, - undeclared: null, - closed: new Set([ast.value]) - } - } - } - } else if(ast.type == 'do'){ - const hoisted_functions_scope = Object.fromEntries( - ast.children - .filter(s => s.type == 'function_decl') - .map(s => [s.children[0].name, s.children[0]]) - ) - const children_with_scope = ast.children - .reduce( - ({scope, children}, node) => ({ - scope: {...scope, ...scope_from_node(node)}, - children: children.concat([{node, scope}]), - }) - , - {scope: hoisted_functions_scope, children: []} - ) - const local_scope = children_with_scope.scope - const {nodes, undeclared, closed} = map_find_definitions(children_with_scope.children, cs => - find_definitions(cs.node, globals, {...scope, ...cs.scope}, local_scope, module_name) - ) - return { - node: {...ast, children: nodes}, - undeclared, - closed: set_diff(closed, new Set(Object.keys(local_scope))), - } - } else if (ast.type == 'function_expr'){ - const args_identifiers = collect_destructuring_identifiers(ast.function_args) - const args_scope = Object.fromEntries(args_identifiers.map(a => [ - a.value, a - ])) - const {nodes, undeclared, closed} = map_find_definitions(ast.children, - node => find_definitions(node, globals, {...scope, ...closure_scope, ...args_scope}) - ) - const next_closed = set_diff(closed, new Set(args_identifiers.map(a => a.value))) - return { - node: {...ast, children: nodes, closed: next_closed}, - undeclared, - closed: new Set(), - } - } else if(ast.children != null){ - let children, full_import_path - if(ast.type == 'import') { - full_import_path = concat_path(module_name, ast.module) - children = ast.children.map((c, i) => ({ - ...c, - definition: { - module: full_import_path, - is_default: i == 0 && ast.default_import != null, - } - })) - } else if(ast.type == 'const' || ast.type == 'let') { - children = ast.children.map(add_trivial_definition) - } else { - children = ast.children - } - - const {nodes, undeclared, closed} = map_find_definitions(children, - c => find_definitions(c, globals, scope, closure_scope) - ) - - return { - node: ast.type == 'import' - ? {...ast, children: nodes, full_import_path} - : {...ast, children: nodes}, - undeclared, - closed - } - } else { - return {node: ast, undeclared: null, closed: new Set()} - } -} - -const BASE = 'dummy://dummy/' -const concat_path = (base, i) => { - const result = new URL(i, BASE + base).toString() - if(result.lastIndexOf(BASE) == 0) { - return result.replace(BASE, '') - } else { - return result - } -} - -export const topsort_modules = (modules) => { - const sort_module_deps = (module) => { - return collect_imports(modules[module]) - .map(m => sort_module_deps(m)) - .flat() - .concat(module) - } - - const sorted = Object.keys(modules).reduce( - (result, module) => result.concat(sort_module_deps(module)), - [] - ) - - // now remove duplicates - // quadratic, but N is supposed to be small - return sorted.reduce( - (result, elem) => - result.includes(elem) - ? result - : [...result, elem] - , - [] - ) -} - -export const has_toplevel_await = modules => - Object.values(modules).some(m => node_has_toplevel_await(m)) - -const node_has_toplevel_await = node => { - if(node.type == 'unary' && node.operator == 'await') { - return true - } - if(node.type == 'function_expr') { - return false - } - if(node.children == null) { - return false - } - return node.children.find(c => node_has_toplevel_await(c)) != null -} - -// TODO not implemented -// TODO detect cycles when loading modules -export const check_imports = modules => { - // TODO allow circular imports - return map_object(modules, (module, node) => { - const imports = node.children - .filter(n => n.type == 'import') - .reduce( - (imports, n) => [ - ...imports, - // TODO imports - // TODO use flatmap - ...(n.imports.map(i => ({name: i.value, from: n.module}))) - ], - [] - ) - const exports = node.statement - .filter(n => n.type == 'export') - .map(n => collect_destructuring_identifiers(n.binding.name_node)) - .reduce((all, current) => [...all, ...current], []) - - return {imports, exports} - }) -} - -/* -code analysis: -- name is declared once and only once (including function args). Name can be imported once -- every assignment can only be to identifier is earlier declared by let -- cannot import names that are not exported from modules.If there is default import from module, there should be default export -- module can be imported either as external or regular -- cannot return from modules toplevel -- await only in async fns -- import only from toplevel -*/ -export const analyze = (node, is_toplevel = true) => { - return [ - ...analyze_await(node, true), - ...named_declared_once(node), - ] -} - -const collect_problems = (node, context, collector) => { - const {context: next_context, problems: node_problems} = collector(node, context) - if(node.children == null) { - return node_problems - } - return node.children.reduce( - (problems, c) => { - const ps = collect_problems(c, next_context, collector) - if(ps == null) { - return problems - } - if(problems == null) { - return ps - } - return problems.concat(ps) - }, - node_problems - ) -} - -const analyze_await = (node, is_async_context = true) => { - const result = collect_problems(node, is_async_context, (node, is_async_context) => { - if(node.type == 'function_expr') { - return {problems: null, context: node.is_async} - } - if(node.type == 'unary' && node.operator == 'await' && !is_async_context) { - const _await = node.children[0] - const problem = { - index: _await.index, - length: _await.length, - message: 'await is only valid in async functions and the top level bodies of modules', - } - return {problems: [problem], context: is_async_context} - } - return {problems: null, context: is_async_context} - }) - - return result ?? [] -} - -const find_duplicates = names => { - const duplicates = names.filter((n, i) => - names.find((name, j) => name.value == n.value && j < i) != null - ) - const problems = duplicates.map(d => ({ - index: d.index, - length: d.length, - message: `Identifier '${d.value}' has already been declared`, - })) - return problems -} - -const named_declared_once = node => { - return collect_problems(node, null, (node, cxt) => { - if(node.type == 'function_expr') { - const names = collect_destructuring_identifiers(node.function_args) - return { - context: uniq_by(names, n => n.value), - problems: find_duplicates(names), - } - } else if(node.type == 'do') { - const names = node - .children - .map(c => { - if(c.type == 'function_decl') { - const function_expr = c.children[0] - return { - value: function_expr.name, - index: function_expr.index, - length: function_expr.name.length - } - } else { - const scope = scope_from_node(c) - return scope == null - ? null - : Object.values(scope) - } - }) - .flat() - .filter(n => n != null) - const problems = find_duplicates( - [...(cxt ?? []), ...names] - ) - return {context: null, problems} - } else { - return {context: null, problems: null} - } - }) - -} diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 64ce394..0000000 --- a/src/index.js +++ /dev/null @@ -1,335 +0,0 @@ -import {UI} from './editor/ui.js' -import {EFFECTS, render_initial_state, apply_side_effects} from './effects.js' -import { - open_dir, - close_dir, - init_window_service_worker -} from './filesystem.js' -import {examples, examples_dir_promise} from './examples.js' -import {get_share} from './share.js' - -const EXAMPLE = `function fib(n) { - if(n == 0 || n == 1) { - return n - } else { - return fib(n - 1) + fib(n - 2) - } -} - -fib(6) -` - - -const set_error_handler = (w, with_unhandled_rejection = true) => { - // TODO err.message - w.onerror = (msg, src, lineNum, colNum, err) => { - if(err?.__ignore) { - return - } - ui.set_status(msg) - } - if(with_unhandled_rejection) { - w.addEventListener('unhandledrejection', (event) => { - const error = event.reason - if(error.__ignore) { - return - } - ui.set_status(error) - }) - } -} - -// Fake directory, http requests to this directory intercepted by service_worker -export const FILES_ROOT = new URL('./__leporello_files', globalThis.location) - -const get_html_url = state => { - const base = FILES_ROOT + '/' - return state.html_file == '' - ? base + '__leporello_blank.html' - : base + state.html_file + '?leporello' -} - -const on_window_load = w => { - init_window_service_worker(w) - exec( - 'run_code', - new Set(Object.getOwnPropertyNames(w)) - ) -} - - -// By default run code in hidden iframe, until user explicitly opens visible -// window -let iframe -const open_app_iframe = (state) => { - iframe = document.createElement('iframe') - iframe.src = get_html_url(state) - iframe.setAttribute('hidden', '') - document.body.appendChild(iframe) - // for app_window, do not set unhandled rejection, because having rejected - // promises in user code is normal condition - set_error_handler(iframe.contentWindow, false) - globalThis.app_window = iframe.contentWindow - init_app_window(globalThis.app_window) -} - -// Open another browser window so user can interact with application -// TODO test in another browsers -export const open_app_window = state => { - // TODO set_error_handler? Or we dont need to set_error_handler for child - // window because error is always caught by parent window handler? - exec('open_app_window') - globalThis.app_window.close() - globalThis.app_window = open(get_html_url(state)) - init_app_window(globalThis.app_window) -} - -const init_app_window = w => { - - const is_loaded = () => { - const nav = w.performance.getEntriesByType("navigation")[0] - return nav != null && nav.loadEventEnd > 0 - } - - const add_load_handler = () => { - /* - Wait until 'load event', then set unload handler. The page after - window.open seems to go through these steps: - - - about:blank gets opened - - Real URL get opened - - 'unload' event for about:blank page - - 'load event for real URL - - if we set unload handler right now, then it will be fired for unload - event for about:blank page - */ - if(is_loaded()) { - // Already loaded - add_unload_handler() - on_window_load(w) - } else { - w.addEventListener('load', () => { - add_unload_handler() - // Wait until `load` event before executing code, because service worker that - // is responsible for loading external modules seems not working until `load` - // event fired. TODO: better register SW explicitly and don't rely on - // already registered SW? - on_window_load(w) - }) - } - } - - const add_unload_handler = () => { - w.addEventListener('unload', (e) => { - // Set timeout to 100ms because it takes some time for page to get closed - // after triggering 'unload' event - setTimeout(() => { - if(w.closed && w == globalThis.app_window) { - // If by that time w.closed was set to true, then page was - // closed. Get back to using iframe - globalThis.app_window = iframe.contentWindow - reload_app_window() - } else { - add_load_handler() - } - }, 100) - }) - } - - add_load_handler() -} - -export const reload_app_window = (state = get_state()) => { - // after window location reload, `run_code` command will be fired. - globalThis.app_window.location = get_html_url(state) -} - -const get_entrypoint_settings = () => { - return { - current_module: localStorage.current_module ?? '', - entrypoint: localStorage.entrypoint ?? '', - html_file: localStorage.html_file ?? '', - } -} - -export const exec_and_reload_app_window = (...exec_args) => { - exec(...exec_args) - reload_app_window() -} - -export const open_directory = () => { - if(globalThis.showDirectoryPicker == null) { - throw new Error('Your browser is not supporting File System Access API') - } - open_dir(true).then(dir => { - exec_and_reload_app_window('load_dir', dir, true, get_entrypoint_settings()) - }) -} - -export const close_directory = async () => { - close_dir() - exec_and_reload_app_window('load_dir', await examples_dir_promise, false, get_entrypoint_settings()) -} - - -let COMMANDS -let ui -let state - -export const init = async (container, _COMMANDS) => { - COMMANDS = _COMMANDS - - set_error_handler(window) - - let files = {'': localStorage.code || EXAMPLE} - let initial_state, entrypoint_settings - const project_dir = await open_dir(false) - let example - if(project_dir == null) { - /* - extract example_id from URL params and delete it (because we dont want to - persist in on refresh) - */ - const params = new URLSearchParams(window.location.search) - const example_path = params.get('example') - const nextURL = new URL(window.location) - nextURL.searchParams.delete('example') - history.replaceState(null, null, nextURL.href) - - example = examples.find(e => e.path == example_path) - - if(example == null) { - const shared_code = await get_share() - if(shared_code == null) { - entrypoint_settings = get_entrypoint_settings() - } else { - files = {'': shared_code} - entrypoint_settings = { - current_module: '', - entrypoint: '', - } - } - } else { - entrypoint_settings = { - current_module: example.entrypoint, - entrypoint: example.entrypoint, - } - } - - initial_state = { - project_dir: await examples_dir_promise, - files, - has_file_system_access: false, - } - } else { - entrypoint_settings = get_entrypoint_settings() - initial_state = { - project_dir, - files, - has_file_system_access: true, - } - } - - state = COMMANDS.get_initial_state( - { - ...initial_state, - on_deferred_call: (...args) => exec('on_deferred_call', ...args) - }, - entrypoint_settings, - ) - - // Expose state for debugging - globalThis.__state = state - ui = new UI(container, state) - // Expose for debugging - globalThis.__ui = ui - - render_initial_state(ui, state, example) - - open_app_iframe(state) -} - -export const get_state = () => state - -export const with_code_execution = (action, state = get_state()) => { - /* - supress is_recording_deferred_calls while rendering, because rendering may - call toJSON(), which can call trigger deferred call (see lodash.js lazy - chaining) - */ - if(state.rt_cxt != null) { - state.rt_cxt.is_recording_deferred_calls = false - state.rt_cxt.skip_save_ct_node_for_path = true - } - - try { - return action() - } finally { - if(state.rt_cxt != null) { - state.rt_cxt.is_recording_deferred_calls = true - state.rt_cxt.skip_save_ct_node_for_path = false - } - } -} - -export const exec = (cmd, ...args) => { - if(cmd == 'input' || cmd == 'write') { - // Do not print file to console - console.log('exec', cmd) - } else { - console.log('exec', cmd, ...args) - } - - const comm = cmd.split('.').reduce( - (comm, segment) => comm?.[segment], - COMMANDS - ) - if(comm == null) { - throw new Error('command ' + cmd + ' + not found') - } - - const result = comm(state, ...args) - console.log('nextstate', result) - - let nextstate, effects - if(result.state != null) { - ({state: nextstate, effects} = result) - } else { - nextstate = result - effects = null - } - - // Sanity check - if(state?.current_module == null) { - console.error('command did not return state, returned', result) - throw new Error('illegal state') - } - - - // Wrap with_code_execution, because rendering values can trigger execution - // of code by toString() and toJSON() methods - - with_code_execution(() => { - apply_side_effects(state, nextstate, ui, cmd) - - if(effects != null) { - (Array.isArray(effects) ? effects : [effects]).forEach(e => { - if(e.type == 'write' || e.type == 'save_to_localstorage') { - // do not spam to console - console.log('apply effect', e.type) - } else { - console.log('apply effect', e.type, ...(e.args ?? [])) - } - EFFECTS[e.type](nextstate, e.args, ui, state) - }) - } - }, nextstate) - - - - // Expose for debugging - globalThis.__prev_state = state - globalThis.__state = nextstate - state = nextstate -} diff --git a/src/launch.js b/src/launch.js deleted file mode 100644 index 239f4e1..0000000 --- a/src/launch.js +++ /dev/null @@ -1,6 +0,0 @@ -// external -import {init} from './index.js' - -import {COMMANDS} from './cmd.js' - -init(globalThis.document.getElementById('app'), COMMANDS) diff --git a/src/parse_js.js b/src/parse_js.js deleted file mode 100644 index 18461a0..0000000 --- a/src/parse_js.js +++ /dev/null @@ -1,1888 +0,0 @@ -import {stringify, zip, uniq, map_object} from './utils.js' - -import { - find_definitions, - topsort_modules, - check_imports, - analyze, -} from './find_definitions.js' - -import { find_versioned_let_vars } from './analyze_versioned_let_vars.js' - -import {reserved} from './reserved.js' - -import {collect_imports} from './ast_utils.js' - -const builtin_identifiers = ['true', 'false', 'null'] - -// Workaround that regexp cannot be drained with imperative code -// TODO foreign pragma -const drain_regexp = new Function('regexp', 'str', ` - const result = [] - let match - while((match = regexp.exec(str)) != null) { - result.push(match) - } - return result -`) - -// https://deplinenoise.wordpress.com/2012/01/04/python-tip-regex-based-tokenizer/ -const tokenize_js = (str) => { - - const arithmetic_ops = [ - '+', '-', '*', '/', '%', '**', - ] - - const logic_ops = [ - '===', '==', '!==', '!=', '!', '&&', '||', '??', - ] - - const punctuation = [ - // Braces - '(', ')', - - // Array literal, property access - '[', ']', - - // Map literal, blocks - '{', '}', - - // Spread - '...', - - // Property access - '.', - - // Optional chaining - '?.', - - ';', - - ',', - - // function expression, must be before `=` to take precedence - '=>', - - ...arithmetic_ops, - - ...logic_ops, - - - '=', - - // Comparison - '<=', '>=', '>', '<', - - // Ternary - '?', ':', - - // TODO bit operations - ] - - const TOKENS = [ - {name: 'pragma_external' , re: '//[\\s]*external[\\s]*[\r\n]'}, - {name: 'pragma_external' , re: '\\/\\*[\\s]*external[\\s]*\\*\\/'}, - - {name: 'comment' , re: '//[^\n]*'}, - {name: 'comment' , re: '\\/\\*[\\s\\S]*?\\*\\/'}, - {name: 'newline' , re: '[\r\n]+'}, - - - // whitespace except newline - //https://stackoverflow.com/a/3469155 - {name: 'whitespace' , re: '[^\\S\\r\\n]+'}, - - {name: 'string_literal' , re: "'.*?'"}, - {name: 'string_literal' , re: '".*?"'}, - - // TODO parse vars inside backtick string - {name: 'backtick_string' , re: '`[\\s\\S]*?`'}, - - {name: 'builtin_identifier' , re: builtin_identifiers - .map(i => '\\b' + i + '\\b') - .join('|')}, - {name: 'keyword' , re: reserved.map(r => '\\b' + r + '\\b').join('|')}, - // TODO all possible notatins for js numbers - {name: 'number' , re: '\\d*\\.?\\d+'}, - {name: 'identifier' , re: '[A-Za-z_$][A-Za-z0-9_$]*'}, - - {name: 'punctuation' , - re: '(' + - punctuation.map( - str => [...str].map(symbol => '\\' + symbol).join('') - ).join('|') - + ')'}, - ] - - // TODO test unbalanced quotes - const regexp_str = TOKENS.map(({re}) => '(' + re + ')').join('|') - const r = new RegExp(regexp_str, 'mg') - - const matches = drain_regexp(r, str) - - const tokens = matches.map(m => { - const type = TOKENS - .find((t,i) => - m[i + 1] != null - ) - .name - - return { - type, - index: m.index, - string: m[0], - length: m[0].length, - } - }) - - if(tokens.length == 0) { - return {ok: true, tokens} - } else { - const unexpected_token = - zip( - [{index: 0, length: 0}, ...tokens], - [...tokens, {index: str.length}], - ) - .find(([prev, current]) => prev.index + prev.length != current.index) - - if(unexpected_token != null) { - const prev = unexpected_token[0] - return { - ok: false, - index: prev.index + prev.length, - message: 'unexpected lexical token', - } - } else { - return {ok: true, tokens} - } - } -} - -/* - Parser combinators -*/ - -/* -let log_level = 1 -const log = (label, fn) => { - return (...args) => { - const cxt = args[0]; - const prefix = '-'.repeat(log_level) - console.log(prefix, label, 'args', cxt.str.slice(cxt.tokens[cxt.current].index)); - log_level++ - const result = fn(...args); - log_level-- - const {ok, value, error, cxt: cxterr} = result; - if(ok) { - console.log(prefix, label, 'ok', stringify(value)); - } else { - console.log(prefix, label, 'error', error, cxterr.str.slice(cxterr.tokens[cxterr.current].index)); - } - return result - } -} -const log_mute = (label, fn) => { - return fn -} -*/ - -const current = cxt => cxt.current < cxt.tokens.length - ? cxt.tokens[cxt.current] - : null - -const literal = str => by_pred(token => token.string == str, 'expected ' + str) - -const insignificant_types = new Set(['newline', 'pragma_external']) - -const by_pred = (pred, error) => { - const result = cxt => { - const token = current(cxt) - - if(token == null) { - return {ok: false, error, cxt} - } - - if(pred(token)) { - return { - ok: true, - value: {...token, value: token.string, string: undefined}, - cxt: {...cxt, current: cxt.current + 1} - } - } - - // Skip non-significant tokens if they were not asked for explicitly - if(insignificant_types.has(token.type)) { - return result({...cxt, current: cxt.current + 1}) - } - - return {ok: false, error, cxt} - } - - return result -} - -const by_type = type => by_pred( - token => token.type == type, - 'expected ' + type -) - -const newline = by_type('newline') - -export const eof = cxt => { - const c = current(cxt) - if(c == null) { - return {ok: true, cxt} - } - if(insignificant_types.has(c.type)) { - return eof({...cxt, current: cxt.current + 1}) - } - return {ok: false, error: 'unexpected token, expected eof', cxt} -} - -const either = (...parsers) => cxt => { - return parsers.reduce( - (result, p) => { - if(result.ok) { - return result - } else { - const other = p(cxt) - if(other.ok) { - return other - } else { - // Select error that matched more tokens - return result.cxt.current > other.cxt.current - ? result - : other - } - } - }, - {ok: false, cxt: {current: 0}}, - ) -} - -const optional = parser => cxt => { - const result = parser(cxt) - return result.ok - ? result - : {ok: true, value: null, cxt} -} - -const apply_location = result => { - if(result.ok) { - const first = result.value.find(v => v != null) - const last = [...result.value].reverse().find(v => v != null) - const value = { - value: result.value, - index: first.index, - length: (last.index + last.length) - first.index, - } - return {...result, value} - } else { - return result - } -} - -const seq = parsers => cxt => { - const seqresult = parsers.reduce( - (result, parser) => { - if(result.ok) { - const nextresult = parser(result.cxt) - if(nextresult.ok) { - return {...nextresult, value: result.value.concat([nextresult.value])} - } else { - return nextresult - } - } else { - return result - } - }, - {cxt, ok: true, value: []} - ) - if(seqresult.ok) { - return apply_location(seqresult) - } else { - return seqresult - } -} - -const if_ok = (parser, fn) => cxt => { - const result = parser(cxt) - if(!result.ok) { - return result - } else { - return {...result, value: fn(result.value)} - } -} - -const check_if_valid = (parser, check) => cxt => { - const result = parser(cxt) - if(!result.ok) { - return result - } else { - const {ok, error} = check(result.value) - if(!ok) { - return {ok: false, error, cxt} - } else { - return result - } - } -} - -const if_fail = (parser, error) => cxt => { - const result = parser(cxt) - if(result.ok) { - return result - } else { - return {...result, error} - } -} - -const if_ok_then = (parser, fn) => cxt => { - const result = parser(cxt) - return !result.ok - ? result - : fn(result.value)(result.cxt) -} - - -const seq_select = (index, parsers) => - if_ok( - seq(parsers), - node => ({...node, value: node.value[index]}) - ) - -const repeat = parser => cxt => { - const dorepeat = (cxt, values) => { - const result = parser(cxt) - if(result.ok) { - return dorepeat(result.cxt, values.concat([result.value])) - } else { - return values.length == 0 - ? result - : {ok: true, value: values, cxt} - } - } - const result = dorepeat(cxt, []) - if(!result.ok) { - return result - } else { - return apply_location(result) - } -} - -const repeat_until = (parser, stop) => cxt => { - const dorepeat = (cxt, values) => { - const result_stop = stop(cxt) - if(result_stop.ok) { - return {ok: true, cxt, value: values} - } else { - const result = parser(cxt) - if(result.ok) { - return dorepeat(result.cxt, values.concat([result.value])) - } else { - return result - } - } - } - const result = dorepeat(cxt, []) - if(!result.ok) { - return result - } else { - if(result.value.length == 0) { - return {...result, value: {value: result.value}} - } else { - return apply_location(result) - } - } -} - -const lookahead = parser => cxt => { - const result = parser(cxt) - if(result.ok) { - return {...result, cxt} - } else { - return result - } -} - -const finished = parser => - if_ok( - seq_select(0, [ - parser, - eof - ]), - ({value}) => value - ) - -/* - End parser combinators -*/ - - - -////////////////////////////////////////////////////////////// - // PARSER -////////////////////////////////////////////////////////////// - -const not_followed_by = (followed, follower) => cxt => { - const result = followed(cxt) - if(!result.ok) { - return result - } else { - const nextresult = follower(result.cxt) - if(nextresult.ok) { - return {ok: false, cxt, error: 'not_followed_by'} - } else { - return result - } - } -} - -/* ret from return */ -const ret = value => cxt => ({ok: true, value, cxt}) - -const attach_or_pass = (nested, attachment, add_attachment) => - if_ok( - seq([ - nested, - optional(attachment), - ]), - ({value, ...node}) => { - const [item, attachment] = value - if(attachment == null) { - return item - } else { - return {...node, ...add_attachment(item, attachment)} - } - } - ) - -const identifier = by_type('identifier') - -const builtin_identifier = if_ok( - by_type('builtin_identifier'), - ({...token}) => ({...token, type: 'builtin_identifier'}), -) - -const string_literal = by_type('string_literal') - -const unary = operator => nested => - if_ok( - seq([ - optional( - literal(operator) - ), - nested, - ]), - ({value, ...node}) => ( - value[0] == null - ? value[1] - : { - ...node, - type: 'unary', - operator, - children: [value[1]], - } - ) - ) - -const binary = ops => nested => - attach_or_pass( - nested, - - repeat( - seq([ - by_pred(token => ops.includes(token.string), 'expected ' + ops.join(',')), - nested, - ]) - ), - - (item, repetitions) => - repetitions.value.reduce( - (prev_node, rep) => { - const children = [ - prev_node, - rep.value[1], - ] - return { - // TODO refactor. This code is copypasted to other places that use 'repeat' - index: item.index, - length: rep.index - item.index + rep.length, - type: 'binary', - operator: rep.value[0].value, - children, - } - }, - item, - ) - ) - - -/* - // TODO check how ternary work - (foo ? bar : baz) ? qux : quux - foo ? bar : (baz ? qux : quux) -*/ -const ternary = nested => - attach_or_pass( - nested, - if_ok( - seq([ - literal('?'), - cxt => expr(cxt), - literal(':'), - cxt => expr(cxt), - ]), - value => { - const [_, left, __, right] = value.value; - return {...value, value: [left, right]} - }, - ), - (cond, {value: branches}) => { - return { - type: 'ternary', - cond, - branches, - children: [cond, ...branches], - } - } - ) - -const list = (separators, element_parser) => cxt => { - const cs = if_ok_then( - optional(lookahead(literal(separators[1]))), - value => - value != null - ? ret([]) - : if_ok_then( - element_parser, - value => if_ok_then( - optional(literal(',')), - comma => - comma == null - ? ret([value]) - : if_ok_then( - cs, - values => ret([value, ...values]) - ) - ) - ) - ) - - return seq_select(1, [ - literal(separators[0]), - cs, - literal(separators[1]), - ])(cxt) - -} - -const comma_separated_1 = element => cxt => { - - const do_comma_separated_1 = cxt => { - - const result = element(cxt) - if(!result.ok) { - return result - } - - const comma_result = literal(',')(result.cxt) - if(!comma_result.ok) { - return {...result, value: [result.value]} - } - - const rest = do_comma_separated_1(comma_result.cxt) - if(!rest.ok) { - return rest - } - - return {...rest, value: [result.value, ...rest.value]} - } - - const result = do_comma_separated_1(cxt) - if(!result.ok) { - return result - } else { - return apply_location(result) - } - -} - -const list_destructuring = (separators, node_type) => if_ok( - - list( - separators, - either( - // identifier = value - if_ok( - seq([ - cxt => destructuring(cxt), - literal('='), - cxt => expr(cxt), - ]), - ({value, ...node}) => ({ - ...node, - not_evaluatable: true, - type: 'destructuring_default', - children: [value[0], value[2]], - }) - ), - - // just identifier - cxt => destructuring(cxt), - - if_ok( - seq_select(1, [ - literal('...'), - cxt => destructuring(cxt), - ]), - ({value, ...node}) => ({ - ...node, - type: 'destructuring_rest', - not_evaluatable: true, - children: [value], - }) - ) - ) - ), - - ({value, ...node}) => ({ - // TODO check that rest is last element - ...node, - type: node_type, - not_evaluatable: true, - children: value, - }), - -) - -const array_destructuring = - list_destructuring(['[', ']'], 'array_destructuring') - -const object_destructuring = if_ok( - - // TODO computed property names, like `const {[x]: y} = {}` - // TODO default values, like `const {x = 1} = {}` - // TODO string keys `const {'x': x} = {x: 2}` - - list( - ['{', '}'], - either( - // identifier: destructuring - if_ok( - seq([ - // Normalize key without quotes to quoted key - if_ok( - identifier, - iden => ({...iden, type: 'string_literal', value: '"' + iden.value + '"'}), - ), - literal(':'), - cxt => destructuring(cxt), - ]), - ({value, ...node}) => ({ - ...node, - not_evaluatable: true, - type: 'destructuring_pair', - children: [value[0], value[2]], - }) - ), - - // just identifier - identifier, - - // rest - if_ok( - seq_select(1, [ - literal('...'), - identifier, - ]), - ({value, ...node}) => ({ - ...node, - type: 'destructuring_rest', - not_evaluatable: true, - children: [value], - }) - ) - ), - ), - - ({value, ...node}) => ({ - // TODO check that rest is last element - ...node, - type: 'object_destructuring', - not_evaluatable: true, - children: value, - }), - -) - -const destructuring = - either(identifier, array_destructuring, object_destructuring) - -/* Parse function_call, member_access or computed_member_access which are of - * the same priority - */ -const function_call_or_member_access = nested => - attach_or_pass( - nested, - - repeat( - either( - - // Member access - if_ok( - seq([ - either( - literal('?.'), - literal('.'), - ), - // Adjust identifier to string literal - if_ok( - either( - identifier, - by_type('keyword'), - ), - iden => ({...iden, - type: 'string_literal', - value: '"' + iden.value + '"', - not_evaluatable: true, - }), - ), - ]), - - ({value: [op, id], ...node}) => ({ - ...node, - value: id, - type: 'member_access', - is_optional_chaining: op.value == '?.', - }) - ), - - // Computed member access - if_ok( - seq([ - optional(literal('?.')), - literal('['), - cxt => expr(cxt), - literal(']'), - ]), - ({value: [optional_chaining, _1, value, _3], ...node}) => ( - {...node, - value, - type: 'computed_member_access', - is_optional_chaining: optional_chaining != null, - } - ) - ), - - // Function call - if_ok( - list( - ['(', ')'], - array_element, - ), - node => ({...node, type: 'function_call'}) - ) - ) - ), - - (object, repetitions) => repetitions.value.reduce( - (object, rep) => { - // TODO refactor. This code is copypasted to other places that use 'repeat' - const index = object.index - const length = rep.index - object.index + rep.length - let result - if(rep.type == 'member_access' || rep.type == 'computed_member_access') { - result = { - type: 'member_access', - is_optional_chaining: rep.is_optional_chaining, - children: [object, rep.value], - } - } else if(rep.type == 'function_call') { - const fn = object - - const {value, ...rest} = rep - const call_args = { - ...rest, - children: value, - not_evaluatable: value.length == 0, - type: 'call_args' - } - result = { - type: 'function_call', - children: [fn, call_args] - } - } else { - throw new Error() - } - return {...result, index, length} - }, - object - ) - ) - - -const grouping = nested => either( - if_ok( - not_followed_by( - seq_select(1, [ - literal('('), - nested, - literal(')'), - ]), - literal('=>') - ), - ({value, ...node}) => ({ - ...node, - type: 'grouping', - children: [value], - }) - ), - primary, -) - -const array_element = either( - if_ok( - seq_select(1, [ - literal('...'), - cxt => expr(cxt), - ]), - ({value, ...node}) => ({ - ...node, - type: 'array_spread', - not_evaluatable: true, - children: [value] - }) - ), - cxt => expr(cxt), -) - -const array_literal = - if_ok( - // TODO array literal can have several commas in a row, like that: - // `[,,,]` - // Each comma creates empty array element - list( - ['[', ']'], - array_element, - ), - ({value, ...node}) => ({...node, type: 'array_literal', children: value}) - ) - -const object_literal = - if_ok( - list( - ['{', '}'], - - either( - // Either object spread - if_ok( - seq_select(1, [ - literal('...'), - cxt => expr(cxt), - ]), - ({value, ...node}) => ({ - ...node, - type: 'object_spread', - children: [value], - not_evaluatable: true - }) - ), - - // Or key-value pair - if_ok( - seq([ - - // key is one of - either( - - // Normalize key without quotes to quoted key - if_ok( - identifier, - iden => ({...iden, type: 'string_literal', value: '"' + iden.value + '"'}), - ), - - string_literal, - - // Computed property name - if_ok( - seq_select(1, [ - literal('['), - cxt => expr(cxt), - literal(']'), - ]), - ({value, ...node}) => ({...node, type: 'computed_property', not_evaluatable: true, children: [value]}) - ) - ), - literal(':'), - cxt => expr(cxt), - ]), - - ({value: [key, _colon, value], ...node}) => ( - {...node, type: 'key_value_pair', not_evaluatable: true, children: [key, value]} - ), - ), - - // Or shorthand property - identifier, - - ), - ), - - ({value, ...node}) => ( - ({...node, type: 'object_literal', children: value}) - ) - ) - -const block_function_body = if_ok( - seq_select(1, [ - literal('{'), - cxt => parse_do(cxt), - literal('}'), - ]), - - ({value, ...node}) => ({...value, ...node}), -) - -const function_expr = must_have_name => - if_ok( - seq([ - optional(literal('async')), - literal('function'), - must_have_name ? identifier : optional(identifier), - list_destructuring(['(', ')'], 'function_args'), - block_function_body, - ]), - ({value, ...node}) => { - const [is_async, _fn, name, args, body] = value - const function_args = {...args, - not_evaluatable: args.children.length == 0 - } - return { - ...node, - type: 'function_expr', - is_async: is_async != null, - is_arrow: false, - name: name?.value, - body, - children: [function_args, body] - } - }, - ) - -const arrow_function_expr = - if_ok( - seq([ - optional(literal('async')), - - either( - // arguments inside braces - list_destructuring(['(', ')'], 'function_args'), - identifier, - ), - - literal('=>'), - - either( - // With curly braces - block_function_body, - // Just expression - cxt => expr(cxt), - ) - ]), - - ({value, ...node}) => { - const [is_async, args, _, body] = value - const function_args = args.type == 'identifier' - ? { - ...args, - not_evaluatable: true, - type: 'function_args', - children: [args] - } - : // args.type == 'function_args' - { - ...args, - not_evaluatable: args.children.length == 0 - } - return { - ...node, - type: 'function_expr', - is_async: is_async != null, - is_arrow: true, - body, - children: [function_args, body] - } - }, - ) - -/* - new is actually is operator with same precedence as function call and member access. - So it allows to parse expressions like `new x.y.z()` as `(new x.y.z)()`. - - Currently we only allow new only in form of `new (args)` - or `new(expr)(args)`, where expr is closed in braces - - TODO implement complete new operator support -*/ -const new_expr = if_ok( - seq([ - literal('new'), - either( - identifier, - if_ok( - seq_select(1, [ - literal('('), - cxt => expr(cxt), - literal(')'), - ]), - ({value}) => value, - ), - ), - list( - ['(', ')'], - array_element, - ) - ]), - ({value, ...node}) => { - const {value: args, ..._call_args} = value[2] - const call_args = { - ..._call_args, - children: args, - not_evaluatable: args.length == 0, - type: 'call_args', - } - return { - ...node, - type: 'new', - children: [value[1], call_args], - } - } -) - -const primary = if_fail( - either( - new_expr, - object_literal, - array_literal, - function_expr(false), - arrow_function_expr, - - // not_followed_by for better error messages - // y => { } must parse as function expr, not as identifier `y` - // followed by `=>` - not_followed_by(builtin_identifier, literal('=>')), - not_followed_by(identifier, literal('=>')), - - string_literal, - by_type('backtick_string'), - by_type('number'), - ), - 'expected expression' -) - -// operator precedence https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence -// TODO instanceof -const expr = - [ - grouping, - function_call_or_member_access, - unary('!'), - unary('-'), - unary('typeof'), - unary('await'), - // TODO 'delete' operator - binary(['**']), - binary(['*','/','%']), - binary(['+','-']), - binary(['<','>','<=','>=', 'instanceof']), - binary(['===', '==', '!==', '!=']), - binary(['&&']), - binary(['||', '??']), - ternary, - ].reduce( - (prev, next) => next(prev), - cxt => expr(cxt) - ) - -const function_decl = if_ok( - function_expr(true), - // wrap function_expr with function_decl - node => ({...node, type: 'function_decl', children: [node]}) -) - -const decl_pair = if_ok( - seq([destructuring, literal('='), expr]), - ({value, ...node}) => { - const [lefthand, _eq, expr] = value - return { - ...node, - type: 'decl_pair', - children: [lefthand, expr], - } - } -) - -/* -Like decl_pair, but lefthand can be only an identifier. - -The reason for having this is that currently we don't compile correctly code -like this: - - let {x} = ... - -If we have just - - let x = ... - -Then we compile it into - - const x = new Multiversion(cxt, ...) - -For 'let {x} = ...' we should compile it to something like - - const {x} = ... - const __x_multiversion = x; - -And then inside eval.js access x value only through __x_multiversion - -See branch 'let_vars_destructuring' - -Same for assignment -*/ -const simple_decl_pair = if_ok( - seq([identifier, literal('='), expr]), - ({value, ...node}) => { - const [lefthand, _eq, expr] = value - return { - ...node, - type: 'decl_pair', - children: [lefthand, expr], - } - } -) - -const const_or_let = is_const => if_ok( - seq_select(1, [ - literal(is_const ? 'const' : 'let'), - comma_separated_1( - is_const - ? decl_pair - : either(simple_decl_pair, identifier) - ) - ]), - ({value, ...node}) => ({ - ...node, - type: is_const ? 'const' : 'let', - children: value.value, - }) -) - -const const_statement = const_or_let(true) - -const let_declaration = const_or_let(false) - -// TODO object assignment required braces, like ({foo} = {foo: 1}) -// require assignment cannot start with '{' by not_followed_by -// TODO chaining assignment, like 'foo = bar = baz' -// TODO +=, *= etc -// TODO make assignment an expression, not a statement. Add comma operator to -// allow multiple assignments -const assignment = if_ok( - comma_separated_1( - either( - simple_decl_pair, - check_if_valid( - if_ok( - seq([ - expr, - literal('='), - expr, - ]), - ({value: [lefthand, _, righthand], ...node}) => { - return {...node, - type: 'assignment_pair', - children: [lefthand, righthand], - } - } - ), - (node) => { - const [lefthand, righthand] = node.children - if(lefthand.type != 'member_access' || lefthand.is_optional_chaining) { - return {ok: false, error: 'Invalid left-hand side in assignment'} - } else { - return {ok: true} - } - } - ) - ) - ), - ({value, ...node}) => ({ - ...node, - type: 'assignment', - children: value, - }) -) - - -const return_statement = either( - // return expr - if_ok( - seq_select(1, [ - not_followed_by( - literal('return'), - newline, - ), - expr, - ]), - ({value, ...node}) => ({ - ...node, - type: 'return', - children: [value], - }) - ), - - // bare return statement - if_ok( - literal('return'), - node => ({ - ...node, - type: 'return', - children: [], - value: null, - }) - ), -) - -const if_branch = if_ok( - seq_select(1, [ - literal('{'), - cxt => parse_do(cxt), - literal('}'), - ]), - ({value, ...node}) => ({...value, ...node}) -) - -const if_statement = - // TODO allow single statement without curly braces? - if_ok( - seq([ - literal('if'), - literal('('), - expr, - literal(')'), - if_branch, - if_ok_then( - optional(literal('else')), - value => value == null - ? ret(null) - : either( - if_branch, - - // else if - cxt => if_statement(cxt), - ) - ) - ]), - - ({value, ...node}) => { - const cond = value[2] - const left = value[4] - const else_ = value[5] - if(else_ == null) { - return { - ...node, - type: 'if', - children: [cond, left], - } - } else { - return { - ...node, - type: 'if', - children: [cond, left, else_], - } - } - }, - ) - -const throw_statement = if_ok( - seq_select(1, [ - not_followed_by( - literal('throw'), - newline, - ), - expr, - ]), - ({value, ...node}) => ({ - ...node, - type: 'throw', - children: [value], - }) -) - -const import_statement = - // TODO import *, import as - if_ok( - seq([ - optional(by_type('pragma_external')), - seq([ - literal('import'), - // TODO import can have both named import and default import, - // like 'import foo, {bar} from "module"' - optional( - seq_select(0, [ - either( - list( - ['{', '}'], - identifier, - ), - identifier, - ), - literal('from'), - ]), - ), - string_literal - ]) - ]), - ({value: [pragma_external, imp]}) => { - const {value: [_import, imports, module], ...node} = imp - let default_import, children - if(imports == null) { - children = [] - } else if(imports.value.type == 'identifier') { - default_import = imports.value.value - children = [imports.value] - } else { - children = imports.value.value - } - // remove quotes - const module_string = module.value.slice(1, module.value.length - 1) - // if url starts with protocol, then it is always external - const is_external_url = new RegExp('^\\w+://').test(module_string) - return { - ...node, - not_evaluatable: true, - type: 'import', - is_external: is_external_url || pragma_external != null, - // TODO refactor hanlding of string literals. Drop quotes from value and - // fix codegen for string_literal - module: module_string, - default_import, - children, - } - } - ) - -const export_statement = - either( - if_ok( - seq_select(1, [ - literal('export'), - // TODO export let statement, export function, export default, etc. - // Should we allow let statement? (it is difficult to transpile) - const_statement, - ]), - ({value, ...node}) => ({ - ...node, - not_evaluatable: true, - type: 'export', - is_default: false, - children: [value], - }) - ), - if_ok( - seq_select(2, [ - literal('export'), - literal('default'), - expr, - ]), - ({value, ...node}) => ({ - ...node, - not_evaluatable: true, - type: 'export', - is_default: true, - children: [value], - }) - ), - - ) - - -const do_statement = either( - const_statement, - let_declaration, - assignment, - if_statement, - throw_statement, - return_statement, - function_decl, -) - -const module_statement = either( - const_statement, - let_declaration, - assignment, - if_statement, - throw_statement, - import_statement, - export_statement, - function_decl, -) - -const parse_do_or_module = (is_module) => - if_ok( - repeat_until( - either( - // allows to repeat semicolons - literal(';'), - - // expr or statement - if_ok( - seq_select(0, [ - either( - is_module - ? module_statement - : do_statement, - expr, - ), - if_fail( - either( - literal(';'), - // Parse newline as semicolon (automatic semicolon insertion) - newline, - eof, - lookahead(literal('}')), - ), - 'unexpected token' - ) - ]), - // TODO fix that here we drop ';' from string. use node.length and node.index - node => node.value - ) - ), - - // Until - either( - eof, - lookahead(literal('}')), - ), - ), - ({value, ...node}) => ({ - ...node, - type: 'do', - children: value - // drop semicolons - .filter(n => n.type != 'punctuation'), - }) - ) - -const parse_do = parse_do_or_module(false) - -const program = (is_module) => either( - // either empty program - if_ok(eof, _ => ({type: 'do', index: 0, length: 0, children: []})), - - is_module ? parse_do_or_module(true) : parse_do -) - -const finished_program = (is_module) => finished(program(is_module)) - -const update_children_not_rec = (node, children = node.children) => { - if(node.type == 'object_literal'){ - return { ...node, elements: children} - } else if(node.type == 'array_literal'){ - return {...node, elements: children} - } else if([ - 'identifier', - 'number', - 'string_literal', - 'builtin_identifier', - 'backtick_string', - ].includes(node.type)) - { - return node - } else if(node.type == 'function_expr'){ - return {...node, - function_args: children[0], - body: children[children.length - 1], - } - } else if(node.type == 'ternary'){ - return {...node, - cond: children[0], - branches: children.slice(1), - } - } else if(node.type == 'const'){ - return {...node, - is_statement: true, - } - } else if(node.type == 'let'){ - return {...node, is_statement: true } - } else if(node.type == 'decl_pair') { - return {...node, expr: children[1], name_node: children[0]} - } else if(node.type == 'assignment_pair') { - return {...node, children} - } else if(node.type == 'assignment'){ - return {...node, is_statement: true} - } else if(node.type == 'do'){ - return {...node, is_statement: true} - } else if(node.type == 'function_decl'){ - return {...node, - is_statement: true, - } - } else if(node.type == 'unary') { - return {...node, - expr: children[0], - } - } else if(node.type == 'binary'){ - return {...node, - args: children, - } - } else if(node.type == 'member_access'){ - return {...node, - object: children[0], - property: children[1], - } - } else if(node.type == 'function_call'){ - return {...node, - fn: children[0], - args: children[1], - } - } else if(node.type == 'call_args') { - return node - } else if(node.type == 'array_spread' || node.type == 'object_spread') { - return {...node, - expr: children[0], - } - } else if(node.type == 'key_value_pair') { - return {...node, - key: children[0], - value: children[1], - } - } else if(node.type == 'computed_property') { - return {...node, - expr: children[0] - } - } else if(node.type == 'new') { - return {...node, constructor: children[0], args: children[1]} - } else if(node.type == 'grouping') { - return {...node, expr: children[0]} - } else if(node.type == 'return') { - return {...node, - expr: children[0], - is_statement: true, - } - } else if(node.type == 'throw') { - return {...node, - expr: children[0], - is_statement: true, - } - } else if(node.type == 'if'){ - return {...node, - cond: children[0], - branches: children.slice(1), - is_statement: true, - } - } else if( - ['array_destructuring', 'object_destructuring', 'function_args'] - .includes(node.type) - ) { - return {...node, - elements: children, - } - } else if(node.type == 'destructuring_default') { - return {...node, - name_node: children[0], - expr: children[1], - } - } else if(node.type == 'destructuring_rest') { - return {...node, - name_node: children[0], - } - } else if(node.type == 'destructuring_pair') { - return {...node, - key: children[0], - value: children[1], - } - } else if(node.type == 'import') { - return {...node, - is_statement: true, - } - } else if(node.type == 'export') { - return {...node, - binding: children[0], - is_statement: true, - } - } else { - console.error('invalid node', node) - throw new Error('unknown node type ' + node.type) - } -} - -export const update_children = node => { - if(Array.isArray(node)) { - return node.map(update_children) - } else { - const children = node.children == null - ? null - : update_children(node.children); - - return {...update_children_not_rec(node, children), children} - } -} - -const do_deduce_fn_names = (node, parent) => { - let changed, node_result - if(node.children == null) { - node_result = node - changed = false - } else { - const children_results = node - .children - .map(c => do_deduce_fn_names(c, node)) - changed = children_results.some(c => c[1]) - if(changed) { - node_result = {...node, children: children_results.map(c => c[0])} - } else { - node_result = node - } - } - - if( - node_result.type == 'function_expr' - && - // not a named function - node_result.name == null - ) { - let name - if(parent?.type == 'decl_pair') { - if(parent.name_node.type == 'identifier') { - name = parent.name_node.value - } - } else if(parent?.type == 'key_value_pair') { - const str = parent.key.value - // unwrap quotes - name = str.substr(1, str.length - 2) - } else { - name = 'anonymous' - } - changed = true - node_result = {...node_result, name} - } - - return [node_result, changed] -} - -const deduce_fn_names = node => { - return do_deduce_fn_names(node, null)[0] -} - -export const parse = (str, globals, is_module = false, module_name) => { - - // Add string to node for debugging - // TODO remove, only need for debug - const populate_string = node => { - if( - (node.index == null || node.length == null || isNaN(node.index) || isNaN(node.length)) - ) { - console.error(node) - throw new Error('invalid node') - } else { - const withstring = {...node, string: str.slice(node.index, node.index + node.length)} - return withstring.children == null - ? withstring - : {...withstring, children: withstring.children.map(populate_string)} - } - } - - const {tokens, ok, index, message} = tokenize_js(str) - if(!ok) { - return {ok: false, problems: [{message, index}]} - } else { - const significant_tokens = tokens.filter(token => - token.type != 'whitespace' && token.type != 'comment' - ) - - const cxt = { - tokens: significant_tokens, - current: 0, - // TODO remove, str here is only for debug (see `log` function) - str - } - const result = finished_program(is_module)(cxt) - if(!result.ok) { - const token = current(result.cxt) - const index = token == null ? str.length - 1 : token.index - return { - ok: false, - problems: [{ - message: result.error, - token, - index, - // Only for debugging - errorlocation: str.slice(index, 20) - }], - } - } else { - const {node, undeclared} = find_definitions( - update_children(result.value), - globals, - null, - null, - module_name - ) - if(undeclared.length != 0){ - return { - ok: false, - problems: undeclared.map(u => ({ - index: u.index, - length: u.length, - message: 'undeclared identifier: ' + u.value, - })) - } - } else { - // TODO remove populate_string (it is left for debug) - // - // call update_children, becase find_definitions adds `definition` - // property to some nodes, and children no more equal to other properties - // of nodes by idenitity, which somehow breaks code (i dont remember how - // exactly). Refactor it? - const fixed_node = update_children( - find_versioned_let_vars(deduce_fn_names(populate_string(node))).node - ) - const problems = analyze(fixed_node) - if(problems.length != 0) { - return {ok: false, problems} - } else { - return {ok: true, node: fixed_node} - } - } - } - } -} - -export const print_debug_node = node => { - const do_print_debug_node = node => { - const {index, length, string, type, children} = node - const res = {index, length, string, type} - if(children == null) { - return res - } else { - const next_children = children.map(do_print_debug_node) - return {...res, children: next_children} - } - } - return stringify(do_print_debug_node(node)) -} - -const do_load_modules = (module_names, loader, already_loaded, parse_cache, globals) => { - const parsed = module_names - .filter(module_name => already_loaded[module_name] == null) - .map(module_name => { - if(parse_cache[module_name] != null) { - return [module_name, parse_cache[module_name]] - } - const m = loader(module_name) - if(m == null) { - return [module_name, {ok: false, problems: [{is_load_error: true}]}] - } else if(typeof(m) == 'string') { - return [module_name, parse(m, globals, true, module_name)] - } else { - throw new Error('illegal state') - } - }) - - const {ok, problems} = parsed.reduce( - ({ok, problems}, [module_name, parsed]) => ({ - ok: ok && parsed.ok, - problems: [ - ...problems, - ...(parsed.problems ?? []).map(p => ({...p, module: module_name})) - ], - }), - {ok: true, problems: []} - ) - - const cache = Object.fromEntries(parsed) - - if(!ok) { - return {ok: false, problems, cache} - } - - const modules = Object.fromEntries( - parsed.map(([module_name, parsed]) => - [module_name, parsed.node] - ) - ) - - const deps = uniq( - Object.values(modules) - .map(m => collect_imports(m)) - .flat() - ) - - if(deps.length == 0) { - return {ok: true, modules, cache} - } - - // TODO when refactor this function to async, do not forget that different - // deps can be loaded independently. So dont just put `await Promise.all(` - // here - const loaded_deps = do_load_modules( - deps, - loader, - {...already_loaded, ...modules}, - parse_cache, - globals - ) - if(loaded_deps.ok) { - return { - ok: true, - modules: {...modules, ...loaded_deps.modules}, - cache: {...cache, ...loaded_deps.cache}, - } - } else { - // Match modules failed to load with import statements and generate - // problems for this import statements - - const failed_to_load = loaded_deps.problems - .filter(p => p.is_load_error) - .map(p => p.module) - - const load_problems = Object.entries(modules) - .map(([module, node]) => - node.children - .filter(n => n.type == 'import') - .map(i => [module, i]) - ) - .flat() - .filter(([module, i]) => - failed_to_load.find(m => m == i.full_import_path) != null - ) - .map(([module, i]) => ({ - message: 'failed lo load module ' + i.full_import_path, - module, - index: i.index - })) - - return { - ok: false, - problems: [ - ...load_problems, - ...loaded_deps.problems.filter(p => !p.is_load_error), - ], - cache: {...cache, ...loaded_deps.cache}, - } - } -} - -export const load_modules = (entry_module, loader, parse_cache, globals) => { - // TODO check_imports. detect cycles while modules are loading, in - // do_load_modules - - const result = do_load_modules([entry_module], loader, {}, parse_cache, globals) - if(!result.ok) { - return result - } else { - return {...result, sorted: topsort_modules(result.modules)} - } -} diff --git a/src/reserved.js b/src/reserved.js deleted file mode 100644 index 2c76d9f..0000000 --- a/src/reserved.js +++ /dev/null @@ -1,48 +0,0 @@ -// See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#keywords - -export const reserved = [ -'break', -'case', -'catch', -'class', -'const', -'continue', -'debugger', -'default', -'delete', -'do', -'else', -'export', -'extends', -'finally', -'for', -'function', -'if', -'import', -'in', -'instanceof', -'new', -'return', -'super', -'switch', -'this', -'throw', -'try', -'typeof', -'var', -'void', -'while', -'with', -'yield', -'enum', -'implements', -'interface', -'let', -'package', -'private', -'protected', -'public', -'static', -'yield', -'await', -] diff --git a/src/runtime/array.js b/src/runtime/array.js deleted file mode 100644 index 3c54a94..0000000 --- a/src/runtime/array.js +++ /dev/null @@ -1,142 +0,0 @@ -import {Multiversion, rollback_if_needed, wrap_methods, mutate} from './multiversion.js' - -function set(prop, value) { - this[prop] = value -} - -export const defineMultiversionArray = window => { - // We declare class in such a weird name to have its displayed name to be - // exactly 'Array' - window.MultiversionArray = class Array extends window.Array { - - constructor(initial, cxt) { - super() - this.multiversion = new Multiversion(cxt) - this.initial = [...initial] - this.redo_log = [] - this.apply_initial() - } - - apply_initial() { - super.length = this.initial.length - for(let i = 0; i < this.initial.length; i++) { - this[i] = this.initial[i] - } - } - - static get [Symbol.species]() { - return globalThis.Array - } - - } - - wrap_methods( - window.MultiversionArray, - - [ - 'at', - 'concat', - 'copyWithin', - 'entries', - 'every', - 'fill', - 'filter', - 'find', - 'findIndex', - 'findLast', - 'findLastIndex', - 'flat', - 'flatMap', - 'forEach', - 'includes', - 'indexOf', - 'join', - 'keys', - 'lastIndexOf', - 'map', - 'pop', - 'push', - 'reduce', - 'reduceRight', - 'reverse', - 'shift', - 'slice', - 'some', - 'sort', - 'splice', - 'toLocaleString', - 'toReversed', - 'toSorted', - 'toSpliced', - 'toString', - 'unshift', - 'values', - 'with', - Symbol.iterator, - ], - - [ - 'copyWithin', - 'fill', - 'pop', - 'push', - 'reverse', - 'shift', - 'sort', - 'splice', - 'unshift', - ] - ) -} - -const methods_that_return_self = new Set([ - 'copyWithin', - 'fill', - 'reverse', - 'sort', -]) - -export function wrap_array(initial, cxt) { - const array = new cxt.window.MultiversionArray(initial, cxt) - const handler = { - get(target, prop, receiver) { - rollback_if_needed(target) - const result = target[prop] - if( - typeof(prop) == 'string' - && isNaN(Number(prop)) - && typeof(result) == 'function' - ) { - if(methods_that_return_self.has(prop)) { - // declare object with key prop for function to have a name - return { - [prop]() { - result.apply(target, arguments) - return receiver - } - }[prop] - } else { - return { - [prop]() { - return result.apply(target, arguments) - } - }[prop] - } - } else { - return result - } - }, - - set(obj, prop, val) { - mutate(obj, set, [prop, val]) - return true - }, - } - return new Proxy(array, handler) -} - -export function create_array(initial, cxt, index, literals) { - const result = wrap_array(initial, cxt) - literals.set(index, result) - return result -} diff --git a/src/runtime/let_multiversion.js b/src/runtime/let_multiversion.js deleted file mode 100644 index c22816f..0000000 --- a/src/runtime/let_multiversion.js +++ /dev/null @@ -1,61 +0,0 @@ -import {Multiversion} from './multiversion.js' - -// https://stackoverflow.com/a/29018745 -function binarySearch(arr, el, compare_fn) { - let m = 0; - let n = arr.length - 1; - while (m <= n) { - let k = (n + m) >> 1; - let cmp = compare_fn(el, arr[k]); - if (cmp > 0) { - m = k + 1; - } else if(cmp < 0) { - n = k - 1; - } else { - return k; - } - } - return ~m; -} - -export class LetMultiversion extends Multiversion { - constructor(cxt, initial) { - super(cxt) - this.latest = initial - this.versions = [{version_number: cxt.version_counter, value: initial}] - } - - rollback_if_needed() { - if(this.needs_rollback()) { - this.latest = this.get_version(this.cxt.version_counter) - } - } - - get() { - this.rollback_if_needed() - return this.latest - } - - set(value) { - this.rollback_if_needed() - const version_number = ++this.cxt.version_counter - if(this.is_created_during_current_expansion()) { - this.versions.push({version_number, value}) - } - this.latest = value - } - - get_version(version_number) { - if(version_number == null) { - throw new Error('illegal state') - } - const idx = binarySearch(this.versions, version_number, (id, el) => id - el.version_number) - if(idx >= 0) { - return this.versions[idx].value - } else if(idx == -1) { - throw new Error('illegal state') - } else { - return this.versions[-idx - 2].value - } - } -} diff --git a/src/runtime/map.js b/src/runtime/map.js deleted file mode 100644 index f435657..0000000 --- a/src/runtime/map.js +++ /dev/null @@ -1,45 +0,0 @@ -import {Multiversion, wrap_methods, rollback_if_needed} from './multiversion.js' - -export const defineMultiversionMap = window => { - - // We declare class in such a weird name to have its displayed name to be - // exactly 'Map' - window.MultiversionMap = class Map extends window.Map { - - constructor(initial, cxt) { - super() - this.multiversion = new Multiversion(cxt) - this.initial = new globalThis.Map(initial) - this.redo_log = [] - this.apply_initial() - } - - apply_initial() { - super.clear() - for(let [k,v] of this.initial) { - super.set(k,v) - } - } - - get size() { - rollback_if_needed(this) - return super.size - } - - } - - - wrap_methods( - window.MultiversionMap, - - // all methods - [ - 'clear', 'delete', 'entries', 'forEach', 'get', 'has', 'keys', 'set', 'values', - Symbol.iterator, - ], - - // mutation methods - ['set', 'delete', 'clear'], - ) - -} diff --git a/src/runtime/multiversion.js b/src/runtime/multiversion.js deleted file mode 100644 index b56a13e..0000000 --- a/src/runtime/multiversion.js +++ /dev/null @@ -1,91 +0,0 @@ -export class Multiversion { - constructor(cxt) { - this.cxt = cxt - this.ct_expansion_id = cxt.ct_expansion_id - } - - is_created_during_current_expansion() { - return this.ct_expansion_id == this.cxt.ct_expansion_id - } - - needs_rollback() { - if(this.cxt.is_expanding_calltree_node) { - if(this.is_created_during_current_expansion()) { - // do nothing, keep using current version - } else { - if(this.rollback_expansion_id == this.cxt.ct_expansion_id) { - // do nothing, keep using current version - // We are in the same expansion rollback was done, keep using current version - } else { - this.rollback_expansion_id = this.cxt.ct_expansion_id - return true - } - } - } else { - if(this.rollback_expansion_id != null) { - this.rollback_expansion_id = null - return true - } else { - // do nothing - } - } - } -} - - -export function rollback_if_needed(object) { - if(object.multiversion.needs_rollback()) { - // Rollback to initial value - object.apply_initial() - // Replay redo log - for(let i = 0; i < object.redo_log.length; i++) { - const log_item = object.redo_log[i] - if(log_item.version_number > object.multiversion.cxt.version_counter) { - break - } - log_item.method.apply(object, log_item.args) - } - } -} - -function wrap_readonly_method(clazz, method) { - const original = clazz.__proto__.prototype[method] - clazz.prototype[method] = { - [method](){ - rollback_if_needed(this) - return original.apply(this, arguments) - } - }[method] -} - -export function mutate(object, method, args) { - rollback_if_needed(object) - const version_number = ++object.multiversion.cxt.version_counter - if(object.multiversion.is_created_during_current_expansion()) { - object.redo_log.push({ - method, - args, - version_number, - }) - } - return method.apply(object, args) -} - -function wrap_mutating_method(clazz, method) { - const original = clazz.__proto__.prototype[method] - clazz.prototype[method] = { - [method]() { - return mutate(this, original, arguments) - } - }[method] -} - -export function wrap_methods(clazz, all_methods, mutating_methods) { - for (let method of all_methods) { - if(mutating_methods.includes(method)) { - wrap_mutating_method(clazz, method) - } else { - wrap_readonly_method(clazz, method) - } - } -} diff --git a/src/runtime/object.js b/src/runtime/object.js deleted file mode 100644 index 33d6d96..0000000 --- a/src/runtime/object.js +++ /dev/null @@ -1,68 +0,0 @@ -import {Multiversion, rollback_if_needed, wrap_methods, mutate} from './multiversion.js' - -export function create_object(initial, cxt, index, literals) { - const multiversion = new Multiversion(cxt) - - let latest = {...initial} - const redo_log = [] - - function rollback_if_needed() { - if(multiversion.needs_rollback()) { - latest = {...initial} - for(let i = 0; i < redo_log.length; i++) { - const log_item = redo_log[i] - if(log_item.version_number > multiversion.cxt.version_counter) { - break - } - if(log_item.type == 'set') { - latest[log_item.prop] = log_item.value - } else if(log_item.type == 'delete') { - delete latest[log_item.prop] - } else { - throw new Error('illegal type') - } - } - } - } - - const handler = { - get(target, prop, receiver) { - rollback_if_needed() - return latest[prop] - }, - - has(target, prop) { - rollback_if_needed() - return prop in latest - }, - - set(obj, prop, value) { - rollback_if_needed() - const version_number = ++multiversion.cxt.version_counter - if(multiversion.is_created_during_current_expansion()) { - redo_log.push({ type: 'set', prop, value, version_number }) - } - latest[prop] = value - return true - }, - - ownKeys(target) { - rollback_if_needed() - return Object.keys(latest) - }, - - getOwnPropertyDescriptor(target, prop) { - rollback_if_needed() - return { - configurable: true, - enumerable: true, - value: latest[prop], - }; - }, - - // TODO delete property handler - } - const result = new Proxy(initial, handler) - literals.set(index, result) - return result -} diff --git a/src/runtime/record_io.js b/src/runtime/record_io.js deleted file mode 100644 index 7b172a2..0000000 --- a/src/runtime/record_io.js +++ /dev/null @@ -1,355 +0,0 @@ -import {set_record_call} from './runtime.js' - -const io_patch = (window, path, use_context = false) => { - let obj = window - for(let i = 0; i < path.length - 1; i++) { - obj = obj[path[i]] - } - const method = path.at(-1) - if(obj == null || obj[method] == null) { - // Method is absent in current env, skip patching - return - } - const name = path.join('.') - - const original = obj[method] - - obj[method] = make_patched_method(window, original, name, use_context) - - obj[method].__original = original -} - -export const abort_replay = (cxt) => { - cxt.io_trace_is_replay_aborted = true - cxt.io_trace_abort_replay() - // throw error to prevent further code execution. It - // is not necesseary, because execution would not have - // any effects anyway - const error = new Error('io replay aborted') - error.__ignore = true - throw error -} - -const patched_method_prolog = (window, original, that, has_new_target, args) => { - const cxt = window.__cxt - - if(cxt.io_trace_is_replay_aborted) { - // Try to finish fast - const error = new Error('io replay was aborted') - error.__ignore = true - throw error - } - - // save call, so on expand_call and find_call IO functions would not be - // called. - // TODO: we have a problem when IO function is called from third-party - // lib and async context is lost - set_record_call(cxt) - - if(cxt.is_recording_deferred_calls) { - // TODO record trace on deferred calls? - const value = has_new_target - ? new original(...args) - : original.apply(that, args) - return {is_return: true, value} - } - - return {is_return: false} -} - -const make_patched_method = (window, original, name, use_context) => { - const method = function(...args) { - const has_new_target = new.target != null - - const {value, is_return} = patched_method_prolog( - window, - original, - this, - has_new_target, - args - ) - if(is_return) { - return value - } - - const cxt = window.__cxt - - const cxt_copy = cxt - - if(cxt.io_trace_is_recording) { - let ok, value, error - try { - const index = cxt.io_trace.length - - if(name == 'setTimeout') { - args = args.slice() - // Patch callback - const cb = args[0] - args[0] = Object.defineProperty(function() { - if(cxt_copy != cxt) { - // If code execution was cancelled, then never call callback - return - } - if(cxt.io_trace_is_replay_aborted) { - // Non necessary - return - } - cxt.io_trace.push({type: 'resolution', index}) - cb() - }, 'name', {value: cb.name}) - } - - value = has_new_target - ? new original(...args) - : original.apply(this, args) - - if(value?.[Symbol.toStringTag] == 'Promise') { - value = value - .then(val => { - value.status = {ok: true, value: val} - return val - }) - .catch(error => { - value.status = {ok: true, error} - throw error - }) - .finally(() => { - if(cxt_copy != cxt) { - return - } - if(cxt.io_trace_is_replay_aborted) { - // Non necessary - return - } - cxt.io_trace.push({type: 'resolution', index}) - }) - } - - ok = true - return value - } catch(e) { - error = e - ok = false - throw e - } finally { - cxt.io_trace.push({ - type: 'call', - name, - ok, - value, - error, - args, - // To discern calls with and without 'new' keyword, primary for - // Date that can be called with and without new - has_new_target, - use_context, - context: use_context ? this : undefined, - }) - } - } else { - // IO trace replay - - const call = cxt.io_trace[cxt.io_trace_index] - - // TODO if call == null or call.type == 'resolution', then do not discard - // trace, instead switch to record mode and append new calls to the - // trace? - if( - call == null - || call.type != 'call' - || call.has_new_target != has_new_target - || call.use_context && (call.context != this) - || call.name != name - || ( - (name == 'setTimeout' && (args[1] != call.args[1])) /* compares timeout*/ - || - ( - name != 'setTimeout' - && - JSON.stringify(call.args) != JSON.stringify(args) - ) - ) - ){ - abort_replay(cxt) - } else { - - const next_resolution = cxt.io_trace.find((e, i) => - e.type == 'resolution' && i > cxt.io_trace_index - ) - - if(next_resolution != null && !cxt.io_trace_resolver_is_set) { - cxt.io_trace_resolver_is_set = true - - // use setTimeout function from host window (because this module was - // loaded as `external` by host window) - setTimeout(() => { - if(cxt_copy != cxt) { - return - } - - if(cxt.io_trace_is_replay_aborted) { - return - } - - cxt.io_trace_resolver_is_set = false - - // Sanity check - if(cxt.io_trace_index >= cxt.io_trace.length) { - throw new Error('illegal state') - } - - const next_event = cxt.io_trace[cxt.io_trace_index] - if(next_event.type == 'call') { - // TODO use abort_replay - cxt.io_trace_is_replay_aborted = true - cxt.io_trace_abort_replay() - } else { - while( - cxt.io_trace_index < cxt.io_trace.length - && - cxt.io_trace[cxt.io_trace_index].type == 'resolution' - ) { - const resolution = cxt.io_trace[cxt.io_trace_index] - const {resolve, reject} = cxt.io_trace_resolvers.get(resolution.index) - - cxt.io_trace_index++ - - if(cxt.io_trace[resolution.index].name == 'setTimeout') { - resolve() - } else { - const promise = cxt.io_trace[resolution.index].value - if(promise.status == null) { - throw new Error('illegal state') - } - if(promise.status.ok) { - resolve(promise.status.value) - } else { - reject(promise.status.error) - } - } - } - } - - }, 0) - } - - cxt.io_trace_index++ - - if(call.ok) { - // Use Symbol.toStringTag for comparison because Promise may - // originate from another window (if window was reopened after record - // trace) and instanceof would not work - if(call.value?.[Symbol.toStringTag] == 'Promise') { - // Always make promise originate from app_window - return new cxt.window.Promise((resolve, reject) => { - cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, {resolve, reject}) - }) - } else if(name == 'setTimeout') { - const timeout_cb = args[0] - cxt.io_trace_resolvers.set(cxt.io_trace_index - 1, {resolve: timeout_cb}) - return call.value - } else { - return call.value - } - } else { - throw call.error - } - } - } - } - - Object.defineProperty(method, 'name', {value: original.name}) - - return method -} - -const patch_Date = (window) => { - const Date_original = window.Date - const Date_patched = make_patched_method(window, Date_original, 'Date', false) - window.Date = function Date(...args) { - if(args.length == 0) { - // return current Date, IO operation - if(new.target != null) { - return new Date_patched(...args) - } else { - return Date_patched(...args) - } - } else { - // pure function - if(new.target != null) { - return new Date_original(...args) - } else { - return Date_original(...args) - } - } - } - window.Date.parse = Date.parse - window.Date.now = Date.now - window.Date.UTC = Date.UTC - io_patch(window, ['Date', 'now']) -} - -const patch_interval = (window, name) => { - const original = window[name] - - window[name] = function(...args) { - const has_new_target = new.target != null - const {value, is_return} = patched_method_prolog( - window, - original, - this, - has_new_target, - args - ) - if(is_return) { - return value - } - - const cxt = window.__cxt - - if(!cxt.io_trace_is_recording) { - /* - Discard io_trace. Run code without IO trace because it is not clear - how it should work with io trace - */ - abort_replay(cxt) - } - - return has_new_target - ? new original(...args) - : original.apply(this, args) - } - - Object.defineProperty(window[name], 'name', {value: name}) -} - - -export const apply_io_patches = (window) => { - io_patch(window, ['Math', 'random']) - - io_patch(window, ['setTimeout']) - // TODO if call setTimeout and then clearTimeout, trace it and remove call of - // clearTimeout, and make only setTimeout, then it would never be called when - // replaying from trace - io_patch(window, ['clearTimeout']) - - patch_interval(window, 'setInterval') - patch_interval(window, 'clearInterval') - - patch_Date(window) - - io_patch(window, ['fetch']) - // Check if Response is defined, for node.js - if(window.Response != null) { - const Response_methods = [ - 'arrayBuffer', - 'blob', - 'formData', - 'json', - 'text', - ] - for(let key of Response_methods) { - io_patch(window, ['Response', 'prototype', key], true) - } - } -} diff --git a/src/runtime/runtime.js b/src/runtime/runtime.js deleted file mode 100644 index 77e5f42..0000000 --- a/src/runtime/runtime.js +++ /dev/null @@ -1,575 +0,0 @@ -import {apply_io_patches} from './record_io.js' -import {LetMultiversion} from './let_multiversion.js' -import {defineMultiversionArray, create_array, wrap_array} from './array.js' -import {create_object} from './object.js' -import {defineMultiversionSet} from './set.js' -import {defineMultiversionMap} from './map.js' -import {apply_canvas_patches} from '../canvas.js' - -/* -Converts generator-returning function to promise-returning function. Allows to -have the same code both for sync and async. If we have only sync modules (no -toplevel awaits), then code executes synchronously, and if there are async -modules, then code executes asynchronoulsy, but we have syntactic niceties of -'yield', 'try', 'catch' -*/ -const gen_to_promise = gen_fn => { - return (...args) => { - const gen = gen_fn(...args) - const next = result => { - if(result.done){ - return result.value - } else { - if(result.value?.[Symbol.toStringTag] == 'Promise') { - return result.value.then( - value => next(gen.next(value)), - error => next(gen.throw(error)), - ) - } else { - return next(gen.next(result.value)) - } - } - } - return next(gen.next()) - } -} - -const make_promise_with_rejector = cxt => { - let rejector - const p = new Promise(r => rejector = r) - return [p, rejector] -} - -export const run = gen_to_promise(function*(module_fns, cxt, io_trace) { - if(!cxt.window.__is_initialized) { - defineMultiversion(cxt.window) - apply_io_patches(cxt.window) - inject_leporello_api(cxt) - apply_canvas_patches(cxt.window) - cxt.window.__is_initialized = true - } else { - throw new Error('illegal state') - } - - let calltree - - const calltree_node_by_loc = new Map( - module_fns.map(({module}) => [module, new Map()]) - ) - - const [replay_aborted_promise, io_trace_abort_replay] = - make_promise_with_rejector(cxt) - - cxt = (io_trace == null || io_trace.length == 0) - // TODO group all io_trace_ properties to single object? - ? {...cxt, - calltree_node_by_loc, - logs: [], - io_trace_is_recording: true, - io_trace: [], - } - : {...cxt, - calltree_node_by_loc, - logs: [], - io_trace_is_recording: false, - io_trace, - io_trace_is_replay_aborted: false, - io_trace_resolver_is_set: false, - // Map of (index in io_trace) -> resolve - io_trace_resolvers: new Map(), - io_trace_index: 0, - io_trace_abort_replay, - } - - // Set current context - cxt.window.__cxt = cxt - - apply_promise_patch(cxt) - - for(let i = 0; i < module_fns.length; i++) { - const {module, fn} = module_fns[i] - - cxt.is_entrypoint = i == module_fns.length - 1 - - cxt.children = null - calltree = { - toplevel: true, - module, - id: ++cxt.call_counter, - version_number: cxt.version_counter, - let_vars: {}, - literals: new Map(), - } - - try { - cxt.modules[module] = {} - const result = fn( - cxt, - calltree.let_vars, - calltree.literals, - calltree_node_by_loc.get(module), - __trace, - __trace_call, - __await_start, - __await_finish, - __save_ct_node_for_path, - LetMultiversion, - create_array, - create_object, - ) - if(result?.[Symbol.toStringTag] == 'Promise') { - yield Promise.race([replay_aborted_promise, result]) - } else { - yield result - } - calltree.ok = true - } catch(error) { - calltree.ok = false - calltree.error = error - } - calltree.children = cxt.children - calltree.last_version_number = cxt.version_counter - if(!calltree.ok) { - break - } - } - - cxt.is_recording_deferred_calls = true - const _logs = cxt.logs - cxt.logs = [] - cxt.children = null - - remove_promise_patch(cxt) - - return { - modules: cxt.modules, - calltree, - logs: _logs, - rt_cxt: cxt, - calltree_node_by_loc, - } -}) - -const inject_leporello_api = cxt => { - cxt.window.leporello = { storage: cxt.storage } -} - -const apply_promise_patch = cxt => { - if(cxt.window.Promise.prototype.__original_then != null) { - throw new Error('illegal state') - } - const original_then = cxt.window.Promise.prototype.then - cxt.window.Promise.prototype.__original_then = cxt.window.Promise.prototype.then - - cxt.window.Promise.prototype.then = function then(on_resolve, on_reject) { - - if(cxt.children == null) { - cxt.children = [] - } - let children_copy = cxt.children - - const make_callback = (cb, ok) => typeof(cb) != 'function' - ? cb - : value => { - if(this.status == null) { - this.status = ok ? {ok, value} : {ok, error: value} - } - const current = cxt.children - cxt.children = children_copy - try { - return cb(value) - } finally { - cxt.children = current - } - } - - return original_then.call( - this, - make_callback(on_resolve, true), - make_callback(on_reject, false), - ) - } -} - -const remove_promise_patch = cxt => { - cxt.window.Promise.prototype.then = cxt.window.Promise.prototype.__original_then - delete cxt.window.Promise.prototype.__original_then -} - -export const set_record_call = cxt => { - for(let i = 0; i < cxt.stack.length; i++) { - cxt.stack[i] = true - } -} - -export const do_eval_expand_calltree_node = (cxt, node) => { - cxt.is_recording_deferred_calls = false - - // Save call counter and set it to the value it had when executed 'fn' for - // the first time - const call_counter = cxt.call_counter - cxt.call_counter = node.fn.__location == null - // Function is native, set call_counter to node.id - ? node.id - // call_counter will be incremented inside __trace and produce the same id - // as node.id - : node.id - 1 - - - cxt.children = null - try { - with_version_number(cxt, node.version_number, () => { - if(node.is_new) { - new node.fn(...node.args) - } else { - node.fn.apply(node.context, node.args) - } - }) - } catch(e) { - // do nothing. Exception was caught and recorded inside '__trace' - } - - // Restore call counter - cxt.call_counter = call_counter - - - cxt.is_recording_deferred_calls = true - const children = cxt.children - cxt.children = null - - if(node.fn.__location != null) { - // fn is hosted, it created call, this time with children - const result = children[0] - if(result.id != node.id) { - throw new Error('illegal state') - } - result.children = cxt.prev_children - result.has_more_children = false - return result - } else { - // fn is native, it did not created call, only its child did - return {...node, - children: children, - has_more_children: false, - } - } -} - -const __await_start = (cxt, promise) => { - // children is an array of child calls for current function call. But it - // can be null to save one empty array allocation in case it has no child - // calls. Allocate array now, so we can have a reference to this array - // which will be used after await - if(cxt.children == null) { - cxt.children = [] - } - const children_copy = cxt.children - const result = {children_copy, promise} - - if(promise?.[Symbol.toStringTag] == 'Promise') { - result.promise = promise.then( - (value) => { - result.status = {ok: true, value} - // We do not return value on purpose - it will be return in - // __await_finish - }, - (error) => { - result.status = {ok: false, error} - // We do not throw error on purpose - }, - ) - } else { - result.status = {ok: true, value: promise} - } - - return result -} - -const __await_finish = (__cxt, await_state) => { - __cxt.children = await_state.children_copy - if(await_state.status.ok) { - return await_state.status.value - } else { - throw await_state.status.error - } -} - -const __trace = (cxt, fn, name, argscount, __location, get_closure, has_versioned_let_vars) => { - const result = (...args) => { - if(result.__closure == null) { - result.__closure = get_closure() - } - - const children_copy = cxt.children - cxt.children = null - cxt.stack.push(false) - - const call_id = ++cxt.call_counter - const version_number = cxt.version_counter - - // populate calltree_node_by_loc only for entrypoint module - if(cxt.is_entrypoint && !cxt.skip_save_ct_node_for_path) { - let nodes_of_module = cxt.calltree_node_by_loc.get(__location.module) - if(nodes_of_module == null) { - nodes_of_module = new Map() - cxt.calltree_node_by_loc.set(__location.module, nodes_of_module) - } - if(nodes_of_module.get(__location.index) == null) { - set_record_call(cxt) - nodes_of_module.set(__location.index, call_id) - } - } - - let let_vars - if(has_versioned_let_vars) { - let_vars = cxt.let_vars = {} - } - - // TODO only allocate map if has literals - const literals = cxt.literals = new Map() - - let ok, value, error - - const is_toplevel_call_copy = cxt.is_toplevel_call - cxt.is_toplevel_call = false - - try { - value = fn(...args) - ok = true - if(value?.[Symbol.toStringTag] == 'Promise') { - set_record_call(cxt) - } - return value - } catch(_error) { - ok = false - error = _error - set_record_call(cxt) - if(cxt.is_recording_deferred_calls && is_toplevel_call_copy) { - if(error instanceof cxt.window.Error) { - error.__ignore = true - } - } - throw error - } finally { - - cxt.prev_children = cxt.children - - const call = { - id: call_id, - version_number, - last_version_number: cxt.version_counter, - let_vars, - literals, - ok, - value, - error, - fn: result, - args: argscount == null - ? args - // Do not capture unused args - : args.slice(0, argscount), - } - - const should_record_call = cxt.stack.pop() - - if(should_record_call) { - call.children = cxt.children - } else { - call.has_more_children = cxt.children != null && cxt.children.length != 0 - } - cxt.children = children_copy - if(cxt.children == null) { - cxt.children = [] - } - cxt.children.push(call) - - cxt.is_toplevel_call = is_toplevel_call_copy - - if(cxt.is_recording_deferred_calls && cxt.is_toplevel_call) { - if(cxt.children.length != 1) { - throw new Error('illegal state') - } - const call = cxt.children[0] - cxt.children = null - const _logs = cxt.logs - cxt.logs = [] - cxt.on_deferred_call(call, cxt.calltree_changed_token, _logs) - } - } - } - - Object.defineProperty(result, 'name', {value: name}) - result.__location = __location - return result -} - -const defineMultiversion = window => { - defineMultiversionArray(window) - defineMultiversionSet(window) - defineMultiversionMap(window) -} - -const wrap_multiversion_value = (value, cxt) => { - - // TODO use a WeakMap value => wrapper ??? - - if(value instanceof cxt.window.Set) { - if(!(value instanceof cxt.window.MultiversionSet)) { - return new cxt.window.MultiversionSet(value, cxt) - } else { - return value - } - } - - if(value instanceof cxt.window.Map) { - if(!(value instanceof cxt.window.MultiversionMap)) { - return new cxt.window.MultiversionMap(value, cxt) - } else { - return value - } - } - - if(value instanceof cxt.window.Array) { - if(!(value instanceof cxt.window.MultiversionArray)) { - return wrap_array(value, cxt) - } else { - return value - } - } - - return value -} - -const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => { - if(fn != null && fn.__location != null && !is_new) { - // Call will be traced, because tracing code is already embedded inside - // fn - return fn(...args) - } - - if(typeof(fn) != 'function') { - throw new TypeError( - errormessage - + ' is not a ' - + (is_new ? 'constructor' : 'function') - ) - } - - const children_copy = cxt.children - cxt.children = null - cxt.stack.push(false) - - const call_id = ++cxt.call_counter - const version_number = cxt.version_counter - - // TODO: other console fns - const is_log = fn == cxt.window.console.log || fn == cxt.window.console.error - - if(is_log) { - set_record_call(cxt) - } - - let ok, value, error - - try { - if(!is_log) { - if(is_new) { - value = new fn(...args) - } else { - value = fn.apply(context, args) - } - } else { - value = undefined - } - ok = true - if(value?.[Symbol.toStringTag] == 'Promise') { - set_record_call(cxt) - } - - value = wrap_multiversion_value(value, cxt) - - return value - - } catch(_error) { - ok = false - error = _error - set_record_call(cxt) - throw error - } finally { - - cxt.prev_children = cxt.children - - const call = { - id: call_id, - version_number, - last_version_number: cxt.version_counter, - ok, - value, - error, - fn, - args, - context, - is_log, - is_new, - } - - if(is_log) { - cxt.logs.push(call) - } - - const should_record_call = cxt.stack.pop() - - if(should_record_call) { - call.children = cxt.children - } else { - call.has_more_children = cxt.children != null && cxt.children.length != 0 - } - - cxt.children = children_copy - if(cxt.children == null) { - cxt.children = [] - } - cxt.children.push(call) - } -} - -const __save_ct_node_for_path = (cxt, __calltree_node_by_loc, index, __call_id) => { - if(!cxt.is_entrypoint) { - return - } - - if(cxt.skip_save_ct_node_for_path) { - return - } - if(__calltree_node_by_loc.get(index) == null) { - __calltree_node_by_loc.set(index, __call_id) - set_record_call(cxt) - } -} - -let ct_expansion_id_gen = 0 - -export const with_version_number = (rt_cxt, version_number, action) => { - if(rt_cxt.logs == null) { - // check that argument is rt_cxt - throw new Error('illegal state') - } - if(version_number == null) { - throw new Error('illegal state') - } - if(rt_cxt.is_expanding_calltree_node) { - throw new Error('illegal state') - } - rt_cxt.is_expanding_calltree_node = true - const version_counter_copy = rt_cxt.version_counter - rt_cxt.version_counter = version_number - const ct_expansion_id = rt_cxt.ct_expansion_id - rt_cxt.ct_expansion_id = ct_expansion_id_gen++ - try { - return action() - } finally { - rt_cxt.ct_expansion_id = ct_expansion_id - rt_cxt.is_expanding_calltree_node = false - rt_cxt.version_counter = version_counter_copy - } -} diff --git a/src/runtime/set.js b/src/runtime/set.js deleted file mode 100644 index bffd994..0000000 --- a/src/runtime/set.js +++ /dev/null @@ -1,44 +0,0 @@ -import {Multiversion, wrap_methods, rollback_if_needed} from './multiversion.js' - -export const defineMultiversionSet = window => { - - // We declare class in such a weird name to have its displayed name to be - // exactly 'Set' - window.MultiversionSet = class Set extends window.Set { - - constructor(initial, cxt) { - super() - this.multiversion = new Multiversion(cxt) - this.initial = new globalThis.Set(initial) - this.redo_log = [] - this.apply_initial() - } - - apply_initial() { - super.clear() - for (const item of this.initial) { - super.add(item) - } - } - - get size() { - rollback_if_needed(this) - return super.size - } - - } - - wrap_methods( - window.MultiversionSet, - - // all methods - [ - 'has', 'add', 'delete', 'clear', 'entries', 'forEach', 'values', 'keys', - Symbol.iterator, - ], - - // mutation methods - ['add', 'delete', 'clear'], - ) - -} diff --git a/src/share.js b/src/share.js deleted file mode 100644 index 4078d65..0000000 --- a/src/share.js +++ /dev/null @@ -1,74 +0,0 @@ -const PROJECT_ID = 'leporello-js' -const URL_BASE = `https://firebasestorage.googleapis.com/v0/b/${PROJECT_ID}.appspot.com/o/` - -// see https://stackoverflow.com/a/48161723/795038 -async function sha256(message) { - // encode as UTF-8 - const msgBuffer = new TextEncoder().encode(message); - - // hash the message - const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer); - - // convert ArrayBuffer to Array - const hashArray = Array.from(new Uint8Array(hashBuffer)); - - // convert bytes to hex string - const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); - return hashHex; -} - -async function upload_share(text) { - const id = (await sha256(text)) - // Truncate to 20 bytes, like in git - .slice(0, 40) - const blob = new Blob([text], { type: 'text/plain' }) - const formData = new FormData() - formData.append('file', blob) - const response = await fetch(URL_BASE + id, { - method: 'POST', - body: formData - }) - if(!response.ok) { - const json = await response.json() - const message = json?.error?.message - throw new Error('Failed to upload: ' + message) - } - return id -} - -async function download_share(id) { - const response = await fetch(URL_BASE + id + '?alt=media') - if(!response.ok) { - const json = await response.json() - const message = json?.error?.message - throw new Error('Failed to fetch: ' + message) - } - return response.text() -} - -export async function get_share() { - const params = new URLSearchParams(window.location.search) - const share_id = params.get('share_id') - if(share_id == null) { - return null - } - - const shared_code = localStorage['share_' + share_id] - if(shared_code != null) { - return shared_code - } - - try { - return await download_share(share_id) - } catch(e) { - alert(e.message) - return null - } -} - -export async function save_share(text) { - const share_id = await upload_share(text) - const nextURL = new URL(window.location) - nextURL.searchParams.set('share_id', share_id) - history.replaceState(null, null, nextURL.href) -} diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index 4637606..0000000 --- a/src/utils.js +++ /dev/null @@ -1,118 +0,0 @@ -export const findLast = new Function('arr', 'pred', ` - for(let i = arr.length - 1; i >= 0; i--) { - if(pred(arr[i])) { - return arr[i] - } - } -`) - -export const set_push = (x,y) => new Set([...x, y]) - -export const set_union = (x,y) => new Set([...x, ...y]) - -export const set_is_eq = (a, b) => - a.size === b.size && [...a].every(value => b.has(value)) - -export const set_diff = (x,y) => { - return new Set([...x].filter(el => !y.has(el))) -} - -export const map_object = (obj, mapper) => Object.fromEntries( - Object.entries(obj).map(([k, v]) => [k, mapper(k,v)]) -) - -export const filter_object = (obj, pred) => Object.fromEntries( - Object.entries(obj).filter(([k, v]) => pred(k,v)) -) - -// https://bit.cloud/ramda/ramda/map-accum/~code -export const map_accum = new Function('fn', 'acc', 'arr', ` - let idx = 0; - const len = arr.length; - const result = []; - let tuple = [acc]; - while (idx < len) { - tuple = fn(tuple[0], arr[idx], idx); - result[idx] = tuple[1]; - idx += 1; - } - return [tuple[0], result]; -`) - -export const map_find = (arr, mapper) => arr.reduce( - (result, curr, i) => result ?? mapper(curr, i), - null -) - -export const stringify = val => JSON.stringify(val, null, 2) - -export const zip = (x,y) => { - if(x.length != y.length){ - throw new Error('zipped arrays must have same length') - } else { - return x.map((el, i) => [el, y[i]]) - } -} - -export const uniq = arr => [...new Set(arr)] - -export const uniq_by = (arr, mapper) => [ - ...new Map( - arr.map(e => [mapper(e), e]) - ) - .values() -] - -export const collect_nodes_with_parents = new Function('node', 'pred', ` - const result = [] - - const do_collect = (node, parent) => { - if(node.children != null) { - for(let c of node.children) { - do_collect(c, node) - } - } - if(pred(node)) { - result.push({node, parent}) - } - } - - do_collect(node, null) - - return result -`) - -// TODO remove -/* -function object_diff(a,b){ - function do_object_diff(a,b, context=[]) { - if(a == b){ - return - } - if(a == null && b == null){ - return - } - if(typeof(a) != 'object' || typeof(b) != 'object'){ - throw new Error(`not an object ${a} ${b}`) - } - for(let key in a) { - if(b[key] == null) { - throw new Error(`missing ${key} in right object ${context.join('.')}`) - } - } - for(let key in b) { - if(a[key] == null) { - throw new Error(`missing ${key} in left object ${context.join('.')}`) - } - do_object_diff(a[key], b[key], context.concat([key])) - } - } - try { - do_object_diff(a,b) - } catch(e){ - return e.message - } -} -*/ - - diff --git a/src/value_explorer_utils.js b/src/value_explorer_utils.js deleted file mode 100644 index 797c479..0000000 --- a/src/value_explorer_utils.js +++ /dev/null @@ -1,201 +0,0 @@ -// We test both for Object and globalThis.app_window.Object because objects may -// come both from app_window and current window (where they are created in -// metacircular interpreter -const has_custom_toString = object => - typeof(object.toString) == 'function' - && object.toString != globalThis.app_window.Object.prototype.toString - && object.toString != Object.prototype.toString - -const isError = object => - object instanceof Error - || - object instanceof globalThis.app_window.Error - -const isPromise = object => object?.[Symbol.toStringTag] == 'Promise' - -// Workaround try/catch is not implemented currently -const toJSON_safe = new Function('object', ` - try { - return object.toJSON() - } catch(e) { - return object - } -`) - -export const displayed_entries = object => { - if(object == null || typeof(object) != 'object') { - return [] - } else if((object[Symbol.toStringTag]) == 'Module') { - return Object.entries(object) - } else if(isPromise(object)) { - if(object.status == null) { - return [] - } - return displayed_entries( - object.status.ok ? object.status.value : object.status.error - ) - } else if(Array.isArray(object)) { - return object.map((v, i) => [i, v]) - } else if(object[Symbol.toStringTag] == 'Set') { - // TODO display set as list without keys as indexes, because Set in JS are - // not ordered and it would be incorrect to imply ordering - return [...object.values()].map((entry, i) => [i, entry]) - } else if(object[Symbol.toStringTag] == 'Map') { - return [...object.entries()] - } else if(typeof(object.toJSON) == 'function') { - const result = toJSON_safe(object) - if(result == object) { - // avoid infinite recursion when toJSON returns itself - return Object.entries(object) - } else { - return displayed_entries(result) - } - } else { - return Object.entries(object) - } -} - -export const is_expandable = v => - isPromise(v) - ? ( - v.status != null - && is_expandable(v.status.ok ? v.status.value : v.status.error) - ) - : ( - typeof(v) == 'object' - && v != null - && displayed_entries(v).length != 0 - ) - - -export const stringify_for_header_object = v => { - if(displayed_entries(v).length == 0) { - return '{}' - } else { - return '{…}' - } -} - -export const stringify_for_header = (v, no_toJSON = false) => { - const type = typeof(v) - - if(v === null) { - return 'null' - } else if(v === undefined) { - return 'undefined' - } else if(type == 'function') { - // TODO clickable link, 'fn', cursive - return 'fn ' + v.name - } else if(type == 'string') { - return JSON.stringify(v) - } else if(type == 'object') { - if((v[Symbol.toStringTag]) == 'Module') { - // protect against lodash module contains toJSON function - return stringify_for_header_object(v) - } else if (isPromise(v)) { - if(v.status == null) { - return `Promise` - } else { - if(v.status.ok) { - return `Promise` - } else { - return `Promise` - } - } - } else if(isError(v)) { - return v.toString() - } else if(Array.isArray(v)) { - if(v.length == 0) { - return '[]' - } else { - return '[…]' - } - } else if(typeof(v.toJSON) == 'function' && !no_toJSON) { - const json = toJSON_safe(v) - if(json == v) { - // prevent infinite recursion - return stringify_for_header(json, true) - } else { - return stringify_for_header(json) - } - } else if(has_custom_toString(v)) { - return v.toString() - } else { - return stringify_for_header_object(v) - } - } else { - return v.toString() - } -} - -export const short_header = value => - Array.isArray(value) - ? 'Array(' + value.length + ')' - : '' - -const header_object = object => { - const prefix = - (object.constructor?.name == null || object.constructor?.name == 'Object') - ? '' - : object.constructor.name + ' ' - const inner = displayed_entries(object) - .map(([k,v]) => { - const value = stringify_for_header(v) - return `${k}: ${value}` - }) - .join(', ') - return `${prefix}{${inner}}` -} - -export const header = (object, no_toJSON = false) => { - const type = typeof(object) - - if(object === null) { - return 'null' - } else if(object === undefined) { - return 'undefined' - } else if(type == 'function') { - // TODO clickable link, 'fn', cursive - return 'fn ' + object.name - } else if(type == 'string') { - return JSON.stringify(object) - } else if(type == 'object') { - if((object[Symbol.toStringTag]) == 'Module') { - // protect against lodash module contains toJSON function - return header_object(object) - } else if(isPromise(object)) { - if(object.status == null) { - return `Promise` - } else { - if(object.status.ok) { - return `Promise` - } else { - return `Promise` - } - } - } else if(isError(object)) { - return object.toString() - } else if(Array.isArray(object)) { - return '[' - + object - .map(stringify_for_header) - .join(', ') - + ']' - } else if(typeof(object.toJSON) == 'function' && !no_toJSON) { - const json = toJSON_safe(object) - if(json == object) { - // prevent infinite recursion - return header(object, true) - } else { - return header(json) - } - } else if(has_custom_toString(object)) { - return object.toString() - } else { - return header_object(object) - } - } else { - return object.toString() - } -} - diff --git a/test/run.js b/test/run.js deleted file mode 100644 index c6be90d..0000000 --- a/test/run.js +++ /dev/null @@ -1,6 +0,0 @@ -import {tests} from './test.js' - -// external -import {run} from './utils.js' - -await run(tests) diff --git a/test/run_utils.js b/test/run_utils.js deleted file mode 100644 index c84dd26..0000000 --- a/test/run_utils.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - For node.js tests - - It forces node.js to load Response (which is loaded lazily) - - Without this, `Response` loading code would be executed in record_io.js and - break test by calling `now()` -*/ -globalThis.Response - -if(globalThis.process != null) { - globalThis.NodeVM = await import('node:vm') -} - -let iframe - -export function create_app_window() { - if(globalThis.process != null) { - // We are in node.js - // `NodeVM` was preloaded earlier - - const context = globalThis.NodeVM.createContext({ - - process, - - // for some reason URL is not available inside VM - URL, - - console, - setTimeout, - // break fetch because we dont want it to be accidentally called in unit test - fetch: () => { - console.error('Error! fetch called') - }, - }) - const get_global_object = globalThis.NodeVM.compileFunction( - 'return this', - [], // args - {parsingContext: context} - ) - - return get_global_object() - - } else { - // We are in browser - if(iframe != null) { - globalThis.document.body.removeChild(iframe) - } - iframe = globalThis.document.createElement('iframe') - document.body.appendChild(iframe) - return iframe.contentWindow - } -} - diff --git a/test/self_hosted_test.js b/test/self_hosted_test.js deleted file mode 100644 index d3f4dca..0000000 --- a/test/self_hosted_test.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - Loads Leporello then runs tests inside Leporello. - Benchmarks how fast test suite is executed inside leporello -*/ - -import fs from 'fs' -import * as pathlib from 'path' -import {COMMANDS} from '../src/cmd.js' -import {root_calltree_node} from '../src/calltree.js' -import { assert_equal, test_initial_state, } from './utils.js' -import {tests} from './test.js' - -// Should work same as src/filesystem.js:load_dir -const load_dir = path => { - const kind = fs.statSync(path).isDirectory() ? 'directory' : 'file' - - const props = { - path, - name: pathlib.basename(path), - kind, - } - - if(kind == 'file') { - return {...props, contents: fs.readFileSync(path, 'utf8')} - } else { - return { - ...props, - children: fs.readdirSync(path) - .filter(f => !f.startsWith('.')) - .map(file => - load_dir(pathlib.join(path, file)) - ) - } - } -} - -// Convert path to modules relative to '.' into path relative to this file -const adjust_path = path => { - return pathlib.join( - pathlib.relative( - pathlib.dirname(import.meta.url.replace('file://', '')), - pathlib.resolve('.'), - ), - path - ) -} - -const load_external_modules = async state => { - const urls = state.loading_external_imports_state.external_imports - const results = await Promise.all( - urls.map(u => import(adjust_path(u))) - ) - return Object.fromEntries( - results.map((module, i) => ( - [ - urls[i], - { - ok: true, - module, - } - ] - )) - ) -} - -const dir = load_dir('.') - -console.time('run') - -const i = test_initial_state( - {}, // files - undefined, - { - project_dir: dir, - entrypoint: 'test/run.js', - } -) - -if(!i.parse_result.ok) { - console.error('Parse errors:', i.parse_result.problems) - i.parse_result.problems.forEach(p => { - if(p.index != null) { - console.error(p.module + ': ' + p.message + ' at ' + i.files[p.module].slice(p.index, p.index + 80)) - } - }) - throw new Error('parse error') -} - -assert_equal(i.loading_external_imports_state != null, true) -const external_imports = await load_external_modules(i) -const loaded = COMMANDS.external_imports_loaded(i, i, external_imports) - -assert_equal(loaded.eval_modules_state != null, true) -const s = loaded.eval_modules_state -const result = await s.promise -const state = COMMANDS.eval_modules_finished(loaded, loaded, result, s.node, s.toplevel) -const root = root_calltree_node(state) -const run = root.children[0] - -if(!root_calltree_node(state).ok) { - console.error(root_calltree_node(state).error) -} -assert_equal(root_calltree_node(state).ok, true) - -// Assert that run children are tests -assert_equal(run.children.length, tests.length) - -console.timeEnd('run') diff --git a/test/test.js b/test/test.js deleted file mode 100644 index ac96c59..0000000 --- a/test/test.js +++ /dev/null @@ -1,5394 +0,0 @@ -import {find_leaf, ancestry, find_node} from '../src/ast_utils.js' -import {print_debug_node} from '../src/parse_js.js' -import {eval_frame} from '../src/eval.js' -import {COMMANDS, with_version_number_of_log} from '../src/cmd.js' -import {header} from '../src/value_explorer_utils.js' -import { - root_calltree_node, - active_frame, - pp_calltree, - get_deferred_calls, - current_cursor_position, - get_execution_paths, -} from '../src/calltree.js' - -import {color_file} from '../src/color.js' -import { - test, - test_only, - assert_equal, - stringify, - do_parse, - assert_code_evals_to, assert_code_evals_to_async, - assert_code_error, assert_code_error_async, - assert_versioned_value, assert_value_explorer, assert_selection, - parse_modules, - run_code, - test_initial_state, test_initial_state_async, - test_deferred_calls_state, - print_debug_ct_node, - input, - input_async, -} from './utils.js' - -export const tests = [ - - test('reserved words', () => { - const result = do_parse('let catch') - assert_equal(result.ok, false) - assert_equal(result.problems[0].index, 4) - }), - - test('invalid token in the beginning', () => { - const result = do_parse('# import') - assert_equal(result, { - ok: false, - problems: [ { message: 'unexpected lexical token', index: 0 } ] - }) - }), - - test('invalid token in the middle', () => { - const result = do_parse(': # import') - assert_equal(result, { - ok: false, - problems: [ { message: 'unexpected lexical token', index: 2 } ] - }) - }), - - test('invalid token in the end', () => { - const result = do_parse(': ^') - assert_equal(result, { - ok: false, - problems: [ { message: 'unexpected lexical token', index: 2 } ] - }) - }), - - test('empty program', () => { - const i = test_initial_state('') - const frame = active_frame(i) - assert_equal(frame.children, []) - assert_equal(frame.result, {ok: true}) - }), - - test('empty if branch', () => { - const r = do_parse(` - if(true) { - } else { - } - `) - assert_equal(r.ok, true) - }), - - test('Must be finished by eof', () => { - const result = do_parse('}') - assert_equal(result.ok, false) - }), - - test('Only semicolons', () => { - const i = test_initial_state(';;;;') - const frame = active_frame(i) - assert_equal(frame.children, []) - assert_equal(frame.result, {ok: true}) - }), - - test('Comments', () => { - assert_code_evals_to(` - /*Qux - - */ - // Foo - 1 //Bar - /* Baz */ - - `, - 1 - ) - }), - - test('backtick_string', () => { - assert_code_evals_to( - 'const x = `b`; `a${x}a`', - 'aba', - ) - }), - - // TODO - // test('backtick_string let vars', () => { - // assert_code_evals_to( - // 'let x = `b`; `a${x}a`', - // 'aba', - // ) - // }), - - test('Simple expression', () => { - return assert_code_evals_to('1+1;', 2) - }), - - test('Logical not', () => { - return assert_code_evals_to('!false', true) - }), - - test('function expr', () => { - assert_code_evals_to( - ` - const x = function(){} - x.name - `, - 'x' - ) - assert_code_evals_to( - ` - const x = function foo(){} - x.name - `, - 'foo' - ) - assert_code_evals_to( - ` - (function foo(x) { - return x*2 - }).name - `, - 'foo' - ) - assert_code_evals_to( - ` - (function foo(x) { - return x*2 - })(1) - `, - 2 - ) - }), - - test('function declaration', () => { - assert_code_evals_to( - ` - function x() {return 1} - x() - `, - 1 - ) - }), - - test('More complex expression', () => { - assert_code_evals_to( - ` - const plusone = x => x + 1; - plusone(3); - `, - 4 - ) - }), - - test('closure', () => { - const code = ` - const x = 1 - const y = () => x; - y() - ` - const i = test_initial_state(code, code.indexOf('x;')) - const frame = active_frame(i) - assert_equal(frame.children[1].result.value, 1) - }), - - - // foo() call fails when tries to get closed variables, because - // NOT_INITIALIZED is not initialized at the moment `foo` is called - // TODO fix later - - // test('closure bug', () => { - // test_initial_state(` - // foo() - // const NOT_INITIALIZED = 1 - // function foo(){ - // return NOT_INITIALIZED - // } - // `) - // }), - - - test('member access', () => { - assert_code_evals_to( - 'const foo = {bar: {baz: 2}};foo.bar.baz;', - 2 - ) - }), - - test('optional chaining', () => { - assert_code_evals_to(`null?.foo`, undefined) - assert_code_evals_to(`{foo:1}?.foo`, 1) - }), - - test('optional chaining computed', () => { - assert_code_evals_to(`null?.['foo']`, undefined) - assert_code_evals_to(`{foo: 1}?.['foo']`, 1) - }), - - test('factorial', () => { - assert_code_evals_to( - ` - const fac = x => x == 0 ? 1 : x * fac(x - 1); - fac(10); - `, - 3628800 - ) - }), - - test('sort_1', () => { - assert_code_evals_to( - ` - const sort = x => x.length == 0 - ? [] - : [ - ...sort(x.slice(1).filter(y => y < x[0])), - x[0], - ...sort(x.slice(1).filter(y => x[0] <= y)), - ]; - sort([4, 7, 8, 9, 15, 1, 3, 2, 1]); - `, - [1, 1, 2, 3, 4, 7, 8, 9, 15] - ) - }), - - test('sort_2', () => { - assert_code_evals_to( - ` - const sort = x => { - return x.length == 0 - ? [] - : [ - ...sort(x.slice(1).filter(y => y < x[0])), - x[0], - ...sort(x.slice(1).filter(y => x[0] <= y)), - ] - }; - sort([4, 7, 8, 9, 15, 1, 3, 2, 1]); - `, - [1, 1, 2, 3, 4, 7, 8, 9, 15] - ) - }), - - test('chaining', () => { - assert_code_evals_to( - 'const foo = () => ({bar: 42}); foo().bar;', - 42 - ) - }), - - test('logic ops', () => { - assert_code_evals_to( - ` - const foo = false; - const bar = true; - const baz = false; - const foo2 = false; - foo || bar && baz || (foo2); - `, - false - ) - }), - - test('strict eq', () => { - assert_code_evals_to( - `null === undefined`, - false - ) - }), - - test('ternary', () => { - assert_code_evals_to(`true ? 1 : 2;`, 1) - }), - - test('nested ternary', () => { - assert_code_evals_to(`false ? 0 : true ? 1 : 2`, 1) - }), - - test('complex expression', () => { - assert_code_evals_to('(x => 2*x)(({foo: 1}).foo + 2 + 3)*10;', 120) - }), - - test('function_call spread', () => { - assert_code_evals_to( - ` - const test = (...args) => args[0] + args[1]; - const data = [1,2]; - const result = test(...data); - result - `, - 3 - ) - }), - - test('destructuring array', () => { - assert_code_evals_to( - ` - const [a,b=2,...c] = [1, undefined, 3,4]; - [a,b,...c]; - `, - [1,2,3,4] - ) - }), - - test('destructuring object', () => { - assert_code_evals_to( - ` - const {a, b: [b], ...c} = {a: 1, b: [2], c: 3, d: 4}; - [a,b,c]; - `, - [1, 2, {c:3, d: 4}] - ) - }), - - test('destructuring function arguments', () => { - assert_code_evals_to( - ` - const test = (first, ...others) => [first, others]; - test(1,2,3); - `, - [1, [2,3]] - ) - }), - - /* - test('let variable', () => { - const code = ` - let x, y = 2, unused, [z,q] = [3,4] - x = 1 - ` - const i = test_initial_state(code, code.indexOf('x')) - assert_equal(i.value_explorer.result.value, {y: 2, z: 3, q: 4}) - }), - */ - - test('let variable not initialized bug', () => { - const code = ` - let x - x /*label*/ - ` - const i = test_initial_state(code, code.indexOf('x /*label')) - assert_equal(i.value_explorer.result.ok, true) - assert_equal(i.value_explorer.result.value === undefined, true) - }), - - test('else if', () => { - const code = ` - let x - if(false) { - let x - x = 0 - } else if(true) { - x = 1 - } else { - x = 2 - }; - x - ` - assert_code_evals_to( - code, - 1 - ) - }), - - test('if without else', () => { - assert_code_evals_to( - ` - let x - if(true) { - x = 1 - } - if(false) { - throw new Error() - } - x - `, - 1 - ) - }), - - test('out of order decl', () => { - const i = test_initial_state( ` - const y = () => x; - const x = 1; - y(); - `) - assert_equal(root_calltree_node(i).children[0].value, 1) - }), - - test('nested closure', () => { - assert_code_evals_to( - ` - const x = () => () => y - const y = 1 - x()() - `, - 1 - ) - }), - - test('Simple expression ASI', () => { - return assert_code_evals_to('1+1', 2) - }), - - test('Closing bracket ASI', () => { - return assert_code_evals_to( - ` - let x - if(true) { - x = 1 - } else { - x = 2 - }; - x - `, - 1 - ) - }), - - test('parse assignment error', () => { - const code = ` - const x = [0] - x[0] = 1, x?.[0] = 2 - ` - const parse_result = do_parse(code) - assert_equal(parse_result.ok, false) - }), - - test('parse assignment ok', () => { - const code = ` - const x = [0] - x[0] = 1 - ` - const parse_result = do_parse(code) - assert_equal(parse_result.ok, true) - }), - - test('ASI_1', () => { - const parse_result = do_parse(` - 1 - const y = 2; - `) - assert_equal(parse_result.ok, true) - assert_equal( - parse_result.node.children.map(c => c.type), - ['number', 'const'] - ) - }), - - test('ASI_2', () => { - const parse_result = do_parse(` - 1 - 2 - `) - assert_equal(parse_result.ok, true) - assert_equal( - parse_result.node.children.map(c => c.type), - ['number', 'number'] - ) - }), - - test('ASI_restricted', () => { - assert_equal( - do_parse(` - return - 1 - `).ok, - true - ) - assert_equal( - do_parse(` - throw - 1 - `).ok, - false - ) - }), - - test('throw', () => { - assert_code_error(` - const x = () => { throw 1 }; - x() - `, - 1 - ) - }), - - test('throw null', () => { - assert_code_error(`throw null`, null) - }), - - test('throw null from function', () => { - const code = ` - const throws = () => { throw null } - throws() - ` - const s = test_initial_state(code) - const moved = COMMANDS.move_cursor(s, code.indexOf('throws()')) - assert_equal(moved.value_explorer.result.ok, false) - assert_equal(moved.value_explorer.result.error, null) - }), - - test('new', () => { - assert_code_evals_to('new Error("test").message', 'test') - }), - - test('new constructor expr', () => { - assert_code_evals_to(` - const x = {Error}; - new (x.Error)('test').message - `, 'test') - }), - - test('new calls are recorded in calltree', () => { - const code = ` - const make_class = new Function("return class { constructor(x) { x() } }") - const clazz = make_class() - const x = () => 1 - new clazz(x) - ` - const i = test_initial_state(code) - const find_call = COMMANDS.move_cursor(i, code.indexOf('1')) - assert_equal(root_calltree_node(find_call).children.length, 3) - const x_call = root_calltree_node(find_call).children[2].children[0] - assert_equal(x_call.fn.name, 'x') - }), - - test('new calls step into', () => { - const code = `new Set()` - const i = test_initial_state(code) - const into = COMMANDS.calltree.arrow_down(i) - assert_equal(into.current_calltree_node.fn.name, 'Set') - assert_equal(into.current_calltree_node.is_new, true) - }), - - test('new call non-constructor', () => { - assert_code_error( - `const x = () => 1; new x()`, - 'TypeError: fn is not a constructor' - ) - }), - - test('method chaining', () => { - assert_code_evals_to( - ` - const x = [1,2,3,4]; - x.slice(1).slice(1).slice(1); - `, - [4] - ) - }), - - test('error is not a function', () => { - assert_code_error( - ` - const x = null - x() - `, - 'TypeError: x is not a function' - ) - assert_code_error( - ` - const foo = () => ([{bar: {}}]) - foo()[0].bar.baz() - `, - 'TypeError: foo(...)[0].bar.baz is not a function' - ) - }), - - test('native throws', () => { - const s1 = test_initial_state( - ` - const throws = new Function('throw new Error("sorry")') - throws() - ` - ) - assert_equal( - root_calltree_node(s1).error.message, - "sorry" - ) - }), - - test('function name from object literal', () => { - const code = ` - const fns = {x: () => 1} - fns.x() - fns.x.name - ` - const i = test_initial_state(code) - assert_equal(root_calltree_node(i).children[0].fn.name, 'x') - assert_code_evals_to(code, 'x') - }), - - test('function name from const decl', () => { - const code = ` - const x = () => 1 - x() - x.name - ` - const i = test_initial_state(code) - assert_equal(root_calltree_node(i).children[0].fn.name, 'x') - assert_code_evals_to( - code, - 'x', - ) - }), - - test('function name deduce', () => { - const code = ` - const make_fn = () => () => 1 - const x = make_fn() - x() - x.name - ` - const i = test_initial_state(code) - assert_equal(root_calltree_node(i).children[1].fn.name, 'x') - assert_code_evals_to( - code, - 'x', - ) - }), - - test('function name dont deduce if already has name', () => { - const code = ` - const make_fn = () => { - const y = () => 1 - return y - } - const x = make_fn() - x() - x.name - ` - const i = test_initial_state(code) - assert_equal(root_calltree_node(i).children[1].fn.name, 'y') - assert_code_evals_to( - code, - 'y', - ) - }), - - /* TODO - test('named function scope', () => { - const code = 'const x = function y() { y }' - const parse_result = do_parse(code) - assert_equal(parse_result.ok, true) - }), - */ - - test('record call chain', () => { - const code = ` - const x = () => ({ - y: () => 1, - }) - x().y() - ` - const s1 = test_initial_state(code) - assert_equal(s1.current_calltree_node.children.length, 2) - }), - - test('record native call chain', () => { - const code = ` Object.entries({}).map(() => null) ` - const s1 = test_initial_state(code) - assert_equal(s1.current_calltree_node.children.length, 2) - }), - - test('eval_frame logical short circuit', () => { - assert_code_evals_to( - `true || false`, - true, - ) - }), - - test('eval_frame array_literal', () => { - assert_code_evals_to( - `[1,2,3,...[4,5]];`, - [1,2,3,4,5] - ) - }), - - test('eval_frame object_literal', () => { - assert_code_evals_to( - `{foo: 1, ...{bar: 2}, ['baz']: 3};`, - {foo:1, bar:2, baz: 3} - ) - }), - - test('eval_frame ternary', () => { - assert_code_evals_to(`false ? 1 : 2`, 2) - }), - - test('eval_frame unary', () => { - assert_code_evals_to(`! false`, true) - }), - - test('typeof', () => { - assert_code_evals_to('typeof 1', 'number') - }), - - test('eval_frame unary minus', () => { - assert_code_evals_to(`-(1)`, -1) - assert_code_evals_to(`-1`, -1) - assert_code_evals_to(`-(-1)`, 1) - }), - - test('eval_frame binary', () => { - const i = test_initial_state(` - 1 + 1 - `) - assert_equal(active_frame(i).children[0].result.value, 2) - }), - - test('eval_frame instanceof', () => { - assert_code_evals_to('1 instanceof Object', false) - assert_code_evals_to('{} instanceof Object', true) - }), - - test('eval_frame grouping', () => { - const i = test_initial_state('(1+1)') - assert_equal(active_frame(i).children[0].result.value, 2) - }), - - test('eval_frame member_access', () => { - const i = test_initial_state('{foo: "bar"}["foo"]') - assert_equal(active_frame(i).children[0].result.value, 'bar') - }), - - test('eval_frame member_access null', () => { - const frame = active_frame(test_initial_state('null["foo"]')) - const result = frame.children[0].result - assert_equal(result.ok, false) - assert_equal( - result.error, - new TypeError("Cannot read properties of null (reading 'foo')") - ) - }), - - test('eval_frame new', () => { - const i = test_initial_state('new Error("foobar")') - assert_equal(active_frame(i).children[0].result.value.message, 'foobar') - }), - - test('eval_frame function_call', () => { - const i = test_initial_state(` - const x = () => 1; - 2 * x(); - `) - assert_equal(active_frame(i).children[1].result.value, 2) - }), - - test('eval_frame function_body_expr', () => { - const code = ` - const x = y => y; - x(2); - ` - const i = test_initial_state(code, code.indexOf('y;')) - const result = active_frame(i).children[1].result - assert_equal(result.ok, true) - assert_equal(result.value, 2) - }), - - test('eval_frame function_body_do', () => { - const code = ` - const x = y => { - return y; - const z = 1; - }; - x(2); - ` - const i = test_initial_state(code, code.indexOf('return y')) - const frame = active_frame(i) - const ret = frame.children[1].children[0] - const z_after_ret = frame.children[1].children[1] - assert_equal(ret.result, {ok: true}) - assert_equal(z_after_ret.result, null) - }), - - test('eval_frame if', () => { - const i = test_initial_state(` - if(1) { - const x = 1; - } else { - const x = 1; - } - `) - const frame = active_frame(i) - const _if = frame.children[0] - assert_equal(_if.children[0].result.ok, true) - assert_equal(_if.children[0].result.value, 1) - assert_equal(_if.children[1].result, {ok: true}) - assert_equal(_if.children[2].result, null) - }), - - test('eval_frame if without else', () => { - const i = test_initial_state(` - if(1) { - const x = 1; - } - `) - const frame = active_frame(i) - const _if = frame.children[0] - assert_equal(_if.children.length, 2) - assert_equal(_if.children[0].result.ok, true) - assert_equal(_if.children[0].result.value, 1) - assert_equal(_if.children[1].result, {ok: true}) - }), - - test('eval_frame modules', () => { - const i = test_initial_state({ - '' : 'import {a} from "a"; export const b = a*2;', - 'a' : 'export const a = 1;', - }) - const frame = active_frame(i) - assert_equal(frame.children[1].result, {ok: true}) - assert_equal( - find_node(frame, n => n.string == 'b').result.value, - 2 - ) - }), - - test('eval_frame error', () => { - const code = ` - const x = ({a}) => 0; - x(null); - ` - const frame = active_frame( - test_initial_state(code, code.indexOf('0')) - ) - assert_equal(frame.result, {ok: false}) - }), - - test('eval_frame binary &&', () => { - const frame = active_frame(test_initial_state(` - const x = () => 1; - const y = () => 2; - false && x(); - y(); - `)) - assert_equal(frame.children[3].result.value, 2) - }), - - test('eval_frame binary ||', () => { - const frame = active_frame(test_initial_state(` - const x = () => 1; - const y = () => 2; - true || x(); - y(); - `)) - assert_equal(frame.children[3].result.value, 2) - }), - - test('eval_frame binary ??', () => { - const frame = active_frame(test_initial_state(` - const x = () => 1; - const y = () => 2; - 1 ?? x(); - y(); - `)) - assert_equal(frame.children[3].result.value, 2) - }), - - test('eval_frame null call', () => { - const frame = active_frame(test_initial_state(`null()`)) - assert_equal(frame.children[0].result.ok, false) - }), - - test('eval_frame non-function call bug', () => { - const frame = active_frame(test_initial_state(`Object.assign({}, {}); null()`)) - assert_equal(frame.children[frame.children.length - 1].result.ok, false) - }), - - test('eval_frame destructuring args', () => { - const code = ` - const x = (...a) => a; - x(1,2,3); - ` - const i = test_initial_state(code, code.indexOf('a;')) - const frame = active_frame(i) - assert_equal(frame.children[0].children[0].children[0].result.value, [1,2,3]) - }), - - test('eval_frame default arg', () => { - const code = ` - const x = 1 - function y(z = x) { - return z - } - y() - ` - const i = test_initial_state(code, code.indexOf('return z')) - const frame = active_frame(i) - assert_equal( - // value for z in return statement - find_node(frame.children[1], n => n.value == 'z').result.value, - 1 - ) - // TODO not implemented - //assert_equal( - // // value for x in arguments - // find_node(frame.children[0], n => n.value == 'x').result.value, - // 1 - //) - }), - - test('eval_frame const lefthand', () => { - const code = ` - const x = 1 - ` - const initial = test_initial_state(code) - const frame = active_frame(initial) - const x = find_node(frame, n => n.string == 'x') - assert_equal(x.result.value, 1) - assert_equal(x.result.version_number, 0) - }), - - test('bare return statement', () => { - const code = ` - function test() { - return - } - test() /*call*/ - ` - assert_value_explorer( - test_initial_state(code, code.indexOf('test() /*call*/')), - undefined, - ) - assert_value_explorer( - test_initial_state(code, code.indexOf('return')), - undefined, - ) - }), - - test('array spread not iterable', () => { - assert_code_error( - `[...null]`, - new Error('null is not iterable'), - ) - }), - - test('args spread not iterable', () => { - assert_code_error( - ` - function x() {} - x(...null) - `, - new Error('null is not iterable'), - ) - }), - - test('module not found', () => { - const parsed = parse_modules( - 'a', - { - 'a' : 'import {b} from "b"; import {c} from "c"', - 'b' : 'for' - } - ) - assert_equal(parsed.ok, false) - assert_equal( - parsed.problems.map(p => ({message: p.message, index: p.index, module: p.module})), - [ - { - message: "failed lo load module c", - index: 21, - module: "a", - }, - { - message: 'expected expression', - index: 0, - module: "b" - } - ] - ) - }), - - test('module parse cache', () => { - const s = test_initial_state({ - '' : `import {b} from 'b'`, - 'b' : `import {c} from 'c'`, - 'c' : `export const c = 1`, - }) - - // Break file c. If parse result is cached then the file will not be parsed - // and the code would not break - const spoil_file = {...s, files: {...s.files, 'c': ',,,'}} - - // change module '' - const {state: s2} = input(spoil_file, 'import {c} from "c"', 0) - - assert_equal(s2.parse_result.ok, true) - }), - - test('modules', () => { - const i = test_initial_state( - { - 'a' : 'export const a = 1;', - 'b' : 'import {a} from "a"; export const b = a*2;', - 'c1' : 'import {b} from "b"; import {a} from "a"; export const c1 = b*2;', - 'c2' : 'import {b} from "b"; import {a} from "a"; export const c2 = b*2;', - '' : 'import {c1} from "c1"; import {c2} from "c2"; export const result = c1 + c2;', - } - ) - assert_equal(i.parse_result.sorted, ['a', 'b', 'c1', 'c2', '']) - assert_equal(i.modules[''].result, 8) - }), - - test('module loaded just once', () => { - /* - root -> intermediate1 -> leaf - root -> intermediate2 -> leaf - */ - const i = test_initial_state( - { - '' : ` - import {l1} from "intermediate1"; - import {l2} from "intermediate2"; - export const is_eq = l1 == l2; - `, - 'intermediate1' : 'import {leaf} from "leaf"; export const l1 = leaf;', - 'intermediate2' : 'import {leaf} from "leaf"; export const l2 = leaf;', - 'leaf' : 'export const leaf = {}', - } - ) - // Check that the same symbol improted through different paths gives the - // same result - assert_equal(i.modules[''].is_eq, true) - }), - - test('modules empty import', () => { - const i = test_initial_state({ - '': 'import {} from "a"', - 'a': 'Object.assign(globalThis, {test_import: true})', - }) - assert_equal(i.active_calltree_node.ok, true) - assert_equal(globalThis.app_window.test_import, true) - }), - - test('modules bare import', () => { - const i = test_initial_state({ - '': 'import "a"', - 'a': 'Object.assign(globalThis, {test_import: true})', - }) - assert_equal(i.active_calltree_node.ok, true) - assert_equal(globalThis.app_window.test_import, true) - }), - - test('bug parser pragma external', () => { - const result = do_parse(` - // external - `) - assert_equal(result.ok, true) - }), - - test('module external', () => { - const code = ` - // external - import {foo_var} from 'foo.js' - console.log(foo_var) - ` - const s1 = test_initial_state(code) - assert_equal(s1.loading_external_imports_state.external_imports, ['foo.js']) - - const state = COMMANDS.external_imports_loaded(s1, s1, { - 'foo.js': { - ok: true, - module: { - 'foo_var': 'foo_value' - }, - } - }) - assert_equal(state.logs.logs[0].args, ['foo_value']) - assert_equal(state.loading_external_imports_state, null) - }), - - test('module external input', () => { - const initial_code = `` - const initial = test_initial_state(initial_code) - const edited = ` - // external - import {foo_var} from 'foo.js' - console.log(foo_var) - ` - - const index = edited.indexOf('foo_var') - - const {state, effects} = input( - initial, - edited, - index - ) - // embed_value_explorer suspended until external imports resolved - assert_equal(effects.length, 1) - assert_equal(effects[0].type, 'write') - assert_equal( - state.loading_external_imports_state.external_imports, - ['foo.js'], - ) - - // TODO must have effect embed_value_explorer - const next = COMMANDS.external_imports_loaded(state, state, { - 'foo.js': { - ok: true, - module: { - 'foo_var': 'foo_value' - }, - } - }) - assert_equal(next.loading_external_imports_state, null) - assert_equal(next.logs.logs[0].args, ['foo_value']) - }), - - test('module external load error', () => { - const code = ` - // external - import {foo_var} from 'foo.js' - console.log(foo_var) - ` - const initial = test_initial_state(code) - - const next = COMMANDS.external_imports_loaded(initial, initial, { - 'foo.js': { - ok: false, - error: new Error('Failed to resolve module'), - } - }) - - assert_equal(next.parse_result.ok, false) - assert_equal( - next.parse_result.problems, - [ - { - index: code.indexOf('import'), - message: 'Failed to resolve module', - module: '', - } - ] - ) - }), - - test('module external cache error bug', () => { - const code = ` - // external - import {foo_var} from 'foo.js' - console.log(foo_var) - ` - const initial = test_initial_state(code) - - // simulate module load error - const next = COMMANDS.external_imports_loaded(initial, initial, { - 'foo.js': { - ok: false, - error: new Error('Failed to resolve module'), - } - }) - - const edited = ` - // external - import {foo_var} from 'foo.js' - // edit - console.log(foo_var) - ` - - // edit code - const {state} = input( - next, - edited, - edited.lastIndexOf('foo_var'), - ) - - // Error must preserve after error - assert_equal(next.parse_result.ok, false) - assert_equal( - next.parse_result.problems, - [ - { - index: code.indexOf('import'), - message: 'Failed to resolve module', - module: '', - } - ] - ) - }), - - test('module external cache invalidation bug', () => { - const code = ` - // external - import {foo_var} from 'foo.js' - ` - const initial = test_initial_state(code) - - // simulate module load error - const next = COMMANDS.external_imports_loaded(initial, initial, { - 'foo.js': { - ok: false, - error: new Error('Failed to resolve module'), - } - }) - - const edited = `` - - // edit code - const {state, effects} = input( - next, - edited, - 0, - ) - - assert_equal(state.parse_result.ok, true) - }), - - test('modules default export', () => { - const modules = { - '' : "import foo from 'foo'; foo", - 'foo': `export default 1` - } - assert_code_evals_to(modules , 1) - - const i = test_initial_state(modules) - const s = COMMANDS.goto_definition(i, modules[''].indexOf('foo')).state - assert_equal(current_cursor_position(s), modules['foo'].indexOf('1')) - assert_equal(s.current_module, 'foo') - }), - - test('modules default import', () => { - const code = ` - // external - import foo from 'foo.js' - foo - ` - const initial = test_initial_state(code) - - const next = COMMANDS.external_imports_loaded(initial, initial, { - 'foo.js': { - ok: true, - module: { - 'default': 'foo_value' - }, - } - }) - assert_equal(active_frame(next).children.at(-1).result.value, 'foo_value') - }), - - test('export value explorer', () => { - const code = 'export const x = 1' - const i = test_initial_state(code) - assert_equal(i.value_explorer.result.value, 1) - }), - - // Static analysis - - test('undeclared', () => { - const undeclared_test = ` - const foo = 1; - const bar = baz => qux(foo, bar, baz, quux); - const qux = 3; - ` - const result = do_parse(undeclared_test) - assert_equal(result.problems.length, 1) - assert_equal(result.problems[0].message, 'undeclared identifier: quux') - }), - - test('name reuse', () => { - assert_code_evals_to( - ` - const f = f => f; - f(x => x + 1)(10); - `, - 11 - ) - }), - - test('assign to itself', () => { - const code = ` - const x = x; - ` - return assert_equal(do_parse(code).problems[0].message, 'undeclared identifier: x') - }), - - test('function hoisting', () => { - assert_code_evals_to(` - function x() { - return 1 - } - x() - `, - 1 - ) - assert_code_evals_to(` - const y = x() - function x() { - return 1 - } - y - `, - 1 - ) - }), - - test('await only inside async fns', () => { - const parse_result = do_parse('function x() { await 1 }') - assert_equal(parse_result.ok, false) - }), - - test('identifier has already been declared', () => { - const code = ` - const x = 1 - const x = 2 - ` - const i = test_initial_state(code) - assert_equal(i.parse_result.ok, false) - assert_equal( - i.parse_result.problems, - [ - { - index: code.indexOf('x = 2'), - length: 1, - message: "Identifier 'x' has already been declared", - module: '', - } - ] - ) - }), - - test('identifier has already been declared in fn arg', () => { - const code = ` - function foo(x) { - const x = 1 - } - - ` - const i = test_initial_state(code) - assert_equal(i.parse_result.ok, false) - assert_equal( - i.parse_result.problems, - [ - { - index: code.indexOf('x = 1'), - length: 1, - message: "Identifier 'x' has already been declared", - module: '', - } - ] - ) - }), - - test('identifier has been declared twice in args', () => { - const code = ` - function foo({x,x}) { - } - - ` - const i = test_initial_state(code) - assert_equal(i.parse_result.ok, false) - assert_equal( - i.parse_result.problems, - [ - { - index: code.indexOf('x}'), - length: 1, - message: "Identifier 'x' has already been declared", - module: '', - } - ] - ) - }), - - test('identifier has already been declared fn decl', () => { - const code = ` - const x = 1 - function x() { - } - ` - const i = test_initial_state(code) - assert_equal(i.parse_result.ok, false) - assert_equal( - i.parse_result.problems, - [ - { - index: code.indexOf('function x()'), - length: 1, - message: "Identifier 'x' has already been declared", - module: '', - } - ] - ) - }), - - test('identifier has already been declared export', () => { - const code = ` - export const x = 1 - function x() { - } - ` - const i = test_initial_state(code) - assert_equal(i.parse_result.ok, false) - assert_equal( - i.parse_result.problems, - [ - { - index: code.indexOf('function x()'), - length: 1, - message: "Identifier 'x' has already been declared", - module: '', - } - ] - ) - }), - - test('identifier has already been declared import', () => { - const code = { - '': ` - import {x} from 'x.js' - function x() { - } - `, - 'x.js': ` - export const x = 1 - ` - } - const i = test_initial_state(code) - assert_equal(i.parse_result.ok, false) - assert_equal( - i.parse_result.problems, - [ - { - index: code[''].indexOf('function x()'), - length: 1, - message: "Identifier 'x' has already been declared", - module: '', - } - ] - ) - }), - - test('function decl', () => { - const code = ` - function fib(n) { - if(n == 0 || n == 1) { - return n - } else { - return fib(n - 1) + fib(n - 2) - } - } - - fib(6) - ` - const i = test_initial_state(code) - const s = COMMANDS.calltree.arrow_right(COMMANDS.calltree.arrow_down( - COMMANDS.calltree.arrow_right(COMMANDS.calltree.arrow_down(i)) - )) - const s2 = COMMANDS.calltree.arrow_down(s) - assert_equal(s2.active_calltree_node.value, 5) - }), - - - /* - TODO use before assignment - test('no use before assignment', () => { - const test = ` - let x; - x; - ` - return assert_equal(do_parse(test).problems[0].message, 'undeclared identifier: x') - }), - */ - - test('goto_definition', () => { - const entry = ` - import {x} from 'a' - const y = x*x - ` - const a = `export const x = 2` - const s = test_initial_state({ - '' : entry, - a, - }) - const y_result = COMMANDS.goto_definition(s, entry.indexOf('y')) - assert_equal(y_result.effects, null) - - const x_result_1 = COMMANDS.goto_definition(s, entry.indexOf('x*x')) - assert_equal(x_result_1.state.current_module, '') - assert_equal(current_cursor_position(x_result_1.state), entry.indexOf('x')) - - const x_result_2 = COMMANDS.goto_definition(s, entry.indexOf('x')) - assert_equal(x_result_2.state.current_module, 'a') - assert_equal(current_cursor_position(x_result_2.state), a.indexOf('x = 2')) - }), - - test('assignment', () => { - const frame = assert_code_evals_to( - ` - let x; - x = 1; - x; - `, - 1 - ) - // assert let has result - assert_equal(frame.children[0].result, {ok: true}) - }), - - test('multiple assignments', () => { - assert_code_evals_to( - ` - let x, y - x = 1, y = 2 - {x,y} - `, - {x: 1, y: 2} - ) - }), - - /* TODO assignments destructuring - test('multiple assignments destructuring', () => { - assert_code_evals_to( - ` - let x, y - x = 1, {y} = {y: 2} - {x,y} - `, - {x: 1, y: 2} - ) - }), - */ - - test('assigments value explorer', () => { - const code = ` - let x - x = 1 - ` - const i = test_initial_state(code, code.indexOf('x = 1')) - assert_equal(i.value_explorer.result.value, 1) - }), - - test('multiple assigments value explorer', () => { - const code = ` - let x, y - x = 1, y = 2 - ` - const i = test_initial_state(code, code.indexOf('x = 1')) - assert_equal(i.value_explorer.result.value, {x: 1, y: 2}) - }), - - /* TODO - test('assignments destructuring value explorer', () => { - const code = ` - let x, y - x = 1, {y} = {y:2} - ` - const i = test_initial_state(code, code.indexOf('x = 1')) - assert_equal(i.value_explorer.result.value, {x: 1, y: 2}) - }), - */ - - test('assigments error', () => { - const code = ` - let x, y - x = 1, y = null.foo - ` - const i = test_initial_state(code, code.indexOf('x = 1')) - assert_equal(i.value_explorer.result.ok, false) - }), - - test('block scoping const', () => { - assert_code_evals_to( - ` - const x = 0 - if(true) { - const x = 1 - } - x - `, - 0 - ) - }), - - test('block scoping', () => { - assert_code_evals_to( - ` - const x = 10 - let y - if(true) { - const x = 1 - y = x - } else { - const x = 2 - y = x - } - y - `, - 1 - ) - }), - - test('block scoping shadow', () => { - assert_code_evals_to( - ` - let y - y = 1 - if(true) { - let y - y = 2 - } - y - `, - 1 - ) - }), - - test('block scoping shadow bug', () => { - assert_code_evals_to( - ` - let y = 3 - if(true) { - let y - y = 1 - if(true) { - let y - y = 2 - } - y - } - y - `, - 3 - ) - }), - - test('step_into', () => { - const code = ` - const x = () => 1; - const y = () => 1; - - if(1) { - x(); - } else { - y(); - } - ` - const initial = test_initial_state(code) - const state = COMMANDS.step_into(initial, code.indexOf('x()')) - const call_code = state.current_calltree_node.code - assert_equal(call_code.index, code.indexOf('() =>')) - assert_equal(current_cursor_position(state), code.indexOf('() =>')) - assert_equal(state.value_explorer.index, code.indexOf('() =>')) - }), - - test('step_into deepest', () => { - const code = ` - const x = () => () => 1; - x(2)(3); - ` - const initial = test_initial_state(code) - const next = COMMANDS.step_into(initial, code.indexOf('3')) - const cn = next.current_calltree_node.code - assert_equal(cn.index, code.indexOf('() => 1')) - }), - - test('step_into expand_calltree_node', () => { - const code = ` - const x = () => 1 - const y = () => x() - y() - - ` - const initial = test_initial_state(code) - const next = COMMANDS.step_into(initial, code.indexOf('y()')) - const cn = next.current_calltree_node.code - assert_equal(cn.index, code.indexOf('() => x()')) - }), - - test('step_into native bug', () => { - const code = `Object()` - const initial = test_initial_state(code) - const {state, effects} = COMMANDS.step_into(initial, 0) - assert_equal(initial == state, true) - assert_equal(effects, { - "type": "set_status", - "args": [ - "Cannot step into: function is either builtin or from external lib" - ] - }) - }), - - test('coloring', () => { - const code = ` - const x = () => { - throw new Error() - } - const y = x() - ` - - const initial = test_initial_state(code) - // only `throw new Error()` colored - assert_equal( - color_file(initial, ''), - [ - { - index: code.indexOf('const x'), - length: 'const x = '.length, - result: { ok: true } - }, - { - index: code.indexOf('x()'), - length: 'x()'.length, - result: { ok: false, is_error_origin: true } - } - ] - ) - - const x_call = root_calltree_node(initial).children[0] - const step_into = COMMANDS.calltree.select_and_toggle_expanded(initial, x_call.id) - assert_equal( - color_file(step_into, '').sort((a,b) => a.index - b.index), - [ - { - index: code.indexOf('const x'), - length: 'const x = '.length, - result: { ok: true } - }, - { - index: code.indexOf('() =>'), - length: '()'.length, - result: { ok: true } - }, - { - index: code.indexOf('throw'), - length: 'throw new Error()'.length, - result: { ok: false, is_error_origin: true } - }, - { - index: code.indexOf('x()'), - length: "x()".length, - result: { ok: false, is_error_origin: true } - } - ] - ) - }), - - test('coloring failed member access', () => { - const code = '(null[1])'; - const initial = test_initial_state(code) - // Color only index access, not grouping braces - assert_equal( - color_file(initial, ''), - [ { index: 1, length: 7, result: { ok: false, is_error_origin: true } } ], - ) - }), - - test('coloring if', () => { - const code = ` - const x = () => { - if(false) {/*m1*/ - if(true) { - 1 - } - 2 - } else { - 3 - } - }/*end*/ - - x()` - const initial = test_initial_state(code) - const x_call = root_calltree_node(initial).children[0] - const step_into = COMMANDS.calltree.select_and_toggle_expanded(initial, x_call.id) - - assert_equal( - color_file(step_into, '').sort((c1, c2) => c1.index - c2.index), - [ - { - index: code.indexOf('const x'), - length: code.indexOf('() =>') - code.indexOf('const x'), - result: { ok: true } - }, - { - index: code.indexOf('() =>'), - length: code.indexOf(' {/*m1*/') - code.indexOf('() =>') + 1, - result: { ok: true } - }, - { - index: code.indexOf(' else'), - length: code.indexOf('/*end*/') - code.indexOf(' else'), - result: { ok: true } - }, - { - index: code.indexOf('/*end*/'), - length: code.length - code.indexOf('/*end*/'), - result: { ok: true } - }, - ] - ) - }), - - - test('coloring failed toplevel', () => { - const code = `throw new Error()` - const initial = test_initial_state(code) - assert_equal( - color_file(initial, ''), - [ - { - index: 0, - length: code.length, - result: { ok: false, is_error_origin: true } - } - ] - ) - }), - - test('coloring short circuit', () => { - const code = `true || false` - const initial = test_initial_state(code) - assert_equal( - color_file(initial, ''), - [ - { - index: 0, - length: "true".length, - result: { ok: true } - } - ] - ) - }), - - test('coloring nested', () => { - const code = - // TODO reformat using .trim() - `const x = () => { - return () => { - return 123 - } -} -const y = x()` - const initial = test_initial_state(code) - const s = COMMANDS.move_cursor(initial, code.indexOf('return')) - const coloring = color_file(s, '').sort((c1, c2) => c1.index - c2.index) - // Checked by eye, test for regression - assert_equal( - coloring, - [ - { index: 0, length: 10, result: { ok: true } }, - { index: 10, length: 18, result: { ok: true } }, - { index: 56, length: 2, result: { ok: true } }, - { index: 58, length: 14, result: { ok: true } } - ] - ) - }), - - test('coloring function body after move inside', () => { - const code = ` - const x = () => { - 1 - } - x() - ` - const i = test_initial_state(code) - const moved = COMMANDS.move_cursor(i, code.indexOf('1')) - const coloring = color_file(moved, '') - const color_body = coloring.find(c => c.index == code.indexOf('(')) - assert_equal(color_body.result.ok, true) - }), - - test('coloring error with nested fns', () => { - const code = `[1].map(_ => {throw new Error()}).map(x => x + 1)` - const i = test_initial_state(code) - const coloring = color_file(i, '') - - const result = {ok: false, is_error_origin: true} - assert_equal( - coloring, - [ - { - index: 0, - length: code.indexOf('_ =>'), - result - }, - { - index: code.indexOf(').map(x =>'), - length: 1, - result - }, - ] - - ) - }), - - test('better parse errors', () => { - const code = ` - const x = z => { - 1 2 - } - ` - const r = do_parse(code) - assert_equal(r.ok, false) - const p = r.problems[0] - assert_equal(p.index, code.indexOf('2')) - }), - - test('better parse errors 2', () => { - const code = ` - if(true) { - const x = 1 - } else { - , - } - ` - const r = do_parse(code) - assert_equal(r.ok, false) - const p = r.problems[0] - assert_equal(p.index, code.indexOf(',')) - }), - - test('better parse errors 3', () => { - const code = `[() => { , }] ` - const r = do_parse(code) - const p = r.problems[0] - assert_equal(p.index, code.indexOf(',')) - }), - - test('edit function', () => { - const s = test_initial_state(` - const x = foo => { - return foo*2 - }; - - x(2); - `) - - const s2 = COMMANDS.calltree.select_and_toggle_expanded( - s, - root_calltree_node(s).children[0].id, - ) - - // Make code invalid - const invalid = ` - const x = foo => { - return - }; - - x(2); - ` - const s3 = input(s2, invalid, invalid.indexOf('return')).state - - const edited = ` - const x = foo => { - return foo*3 - }; - - x(2); - ` - - const n = input(s3, edited, edited.indexOf('return')).state - - const res = find_leaf(active_frame(n), edited.indexOf('*')) - - assert_equal(res.result.value, 6) - assert_equal( - n.calltree_node_by_loc.get('').get(edited.indexOf('foo =>')) == null, - false - ) - }), - - test('edit function 2', () => { - const code = ` - const x = () => { - return 1 - } - [1,2,3].map(x) - ` - const s1 = test_initial_state(code) - - // Go into first call of `x` - const s2 = COMMANDS.calltree.arrow_right(s1) - const s3 = COMMANDS.calltree.arrow_right(s2) - const s4 = COMMANDS.calltree.arrow_right(s3) - - assert_equal(s4.current_calltree_node.code.index, code.indexOf('() =>')) - - const edited = ` - const x = () => { - return 2 - } - [1,2,3].map(x) - ` - - const e = input(s4, edited, edited.indexOf('2')).state - - const active = active_frame(e) - - assert_equal(active.index, edited.indexOf('() =>')) - }), - - test('edit function modules bug', () => { - const s1 = test_initial_state({ - '' : ` - import {x} from 'x.js' - const fn = () => { - } - `, - 'x.js': ` - export const x = 1 - ` - }) - - const edited = ` - import {x} from 'x.js' - const fn = () => { - 1 - } - ` - - const {state: s2} = input(s1, edited, edited.indexOf('1')) - const s3 = COMMANDS.move_cursor(s2, edited.indexOf('import')) - assert_equal(s3.value_explorer.result.value.x, 1) - }), - - test('edit toplevel', () => { - const code = ` - const x = () => { - return 1 - } - x() - ` - const s1 = test_initial_state(code) - - // Go into call of `x` - const s2 = COMMANDS.calltree.arrow_right(s1) - const s3 = COMMANDS.calltree.arrow_right(s2) - - assert_equal(s3.current_calltree_node.code.index, code.indexOf('() =>')) - - const edited = ` - const y = 123 - const x = () => { - return 1 - } - x() - ` - - const e = input(s3, edited, edited.indexOf('123')).state - - assert_equal(e.active_calltree_node.toplevel, true) - }), - - test('edit module not_loaded', () => { - const s1 = COMMANDS.change_current_module( - test_initial_state({ - '' : '', - "x": 'export const x = 1', - }), - 'x' - ) - const e = input(s1, 'export const x = 2', 0).state - assert_equal(e.current_calltree_node.module, '') - assert_equal(e.active_calltree_node, null) - }), - - test('edit function unreachable', () => { - const code = ` - const x = () => { - return 1 - } - const y = () => { - return 2 - } - x() - ` - const s1 = test_initial_state(code) - - // Go into call of `x` - const s2 = COMMANDS.calltree.arrow_right(s1) - const s3 = COMMANDS.calltree.arrow_right(s2) - - const edited = ` - const x = () => { - return 1 - } - const y = () => { - return 3 - } - x() - ` - - const moved = COMMANDS.move_cursor(s3, code.indexOf('2')) - const e = input(moved, edited, edited.indexOf('3')).state - assert_equal(e.active_calltree_node, null) - assert_equal(e.current_calltree_node.toplevel, true) - }), - - test('edit function step out', () => { - const code = ` - const x = () => { - return 1 - } - x() - ` - const i = test_initial_state(code) - const edited = input(i, code.replace('1', '100'), code.indexOf('1')).state - const left = COMMANDS.calltree.arrow_left(edited) - assert_equal(left.active_calltree_node.toplevel, true) - }), - - test('expand_calltree_node', () => { - // Test expecting MAX_DEPTH = 1 - const s = test_initial_state(` - const countdown = c => c == 0 ? 0 : 1 + countdown(c - 1); - countdown(10) - `) - const first = root_calltree_node(s).children[0] - assert_equal(first.value, 10) - const s2 = COMMANDS.calltree.select_and_toggle_expanded(s, first.id) - const first2 = root_calltree_node(s2).children[0] - assert_equal(first2.children[0].value, 9) - assert_equal(first2.code, first2.children[0].code) - }), - - test('expand_calltree_node new', () => { - const code = ` - const make_class = new Function("return class { constructor(x) { x() } }") - const clazz = make_class() - const x = () => 1 - new clazz(x) - ` - const s = test_initial_state(code) - const new_call = root_calltree_node(s).children.at(-1) - const expanded_new_call = COMMANDS.calltree.select_and_toggle_expanded(s, new_call.id) - const x_call = root_calltree_node(expanded_new_call) - .children.at(-1) - .children[0] - assert_equal(x_call.fn.name, 'x') - }), - - test('expand_calltree_node native', () => { - const s = test_initial_state(`[1,2,3].map(x => x + 1)`) - const map = root_calltree_node(s).children[0] - const s2 = COMMANDS.calltree.select_and_toggle_expanded(s, map.id) - const map_expanded = root_calltree_node(s2).children[0] - assert_equal(map_expanded.children.length, 3) - }), - - test('value_explorer arguments', () => { - const i = test_initial_state(` - function foo(x, {y}) { - } - - foo(1, {y: 2}) - `) - const expanded = COMMANDS.calltree.select_and_toggle_expanded(i, root_calltree_node(i).children[0].id) - const args = expanded.value_explorer.result.value['*arguments*'] - assert_equal(args, {value: {x: 1, y: 2}}) - }), - - test('click native calltree node', () => { - const s = test_initial_state(`Object.fromEntries([])`) - const index = 0 // Where call starts - const call = root_calltree_node(s).children[0] - const state = COMMANDS.calltree.select_and_toggle_expanded(s, call.id) - assert_equal(current_cursor_position(state), index) - assert_equal( - state.value_explorer, - { - index, - result: { - "ok": true, - "is_calltree_node_explorer": true, - "value": { - "*arguments*": { - value: [ - [] - ], - }, - "*return*": { - value: {}, - } - } - } - } - ) - }), - - test('jump_calltree_location' , () => { - const code = ` - const x = foo => foo + 1; - const y = arr => { - return arr.map(x) - } - y([1,2,3]) - ` - - const assert_loc = (s, substring) => { - const state = COMMANDS.calltree.arrow_right(s) - const index = code.indexOf(substring) - assert_equal(current_cursor_position(state), index) - assert_equal(active_frame(state) != null, true) - return state - } - - - const s1 = test_initial_state(code) - - // Select call of `y()` - const s2 = assert_loc(s1, 'y([') - - // Expand call of `y()` - const s3 = assert_loc(s2, 'arr =>') - - // Select call of arr.map - const s4 = assert_loc(s3, 'arr.map') - - // Expand call of arr.map - // native call is not expandable - const s5 = assert_loc(s4, 'arr.map') - - // Select call of x - const s6 = assert_loc(s5, 'foo =>') - }), - - test('jump_calltree select callsite', () => { - const code = ` - function x(y) {} - x() - ` - const i = test_initial_state(code) - const call_selected = COMMANDS.calltree.arrow_right(i) - const node = call_selected.selection_state.node - assert_equal(node.index, code.indexOf('x()')) - assert_equal(node.length, 'x()'.length) - }), - - // Test very specific case - test('jump_calltree_location after error', () => { - const code = ` - const fail = () => { - throw new Error('fail') - } - const good = () => {/*good*/} - [good, fail].forEach(fn => fn()) - ` - const s = test_initial_state(code) - const call_fn = root_calltree_node(s).children[0].children[0] - const s2 = COMMANDS.calltree.select_and_toggle_expanded(s, call_fn.id) - const good = s2.current_calltree_node.children[0] - assert_equal(good.code.index, code.indexOf('() => {/*good')) - }), - - test('jump_calltree select another call of the same fn', () => { - const code = '[1,2].map(x => x*10)' - const i = test_initial_state(code, code.indexOf('10')) - assert_equal(i.value_explorer.result.value, 10) - const second_iter = COMMANDS.calltree.arrow_down(i) - const moved = COMMANDS.move_cursor(second_iter, code.indexOf('x*10')) - assert_equal(moved.value_explorer.result.value, 20) - }), - - test('unwind_stack', () => { - const s = test_initial_state(` - const y = () => 1 - const deep_error = x => { - if(x == 10) { - throw 'deep_error' - } else { - y() - deep_error(x + 1) - } - } - deep_error(0) - `) - - assert_equal(s.active_calltree_node.toplevel, true) - assert_equal(s.current_calltree_node.id, s.active_calltree_node.id) - - const first = root_calltree_node(s).children[0] - - const depth = (node, i = 0) => { - if(node.children == null) { - return i - } - assert_equal(s.calltree_node_is_expanded[node.id], true) - assert_equal(node.children.length, 2) - return depth(node.children[1], i + 1) - } - - assert_equal(depth(first), 10) - assert_equal(first.ok, false) - assert_equal(first.error, 'deep_error') - }), - - /* Test when node where error occured has subcalls */ - test('unwind_stack 2', () => { - const code = ` - const x = () => 1 - const error = () => { - x() - null.y - } - error() - ` - const s = test_initial_state(code) - assert_equal(s.current_calltree_node.toplevel, true) - }), - - //TODO this test is fine standalone, but it breaks self-hosted test - /* - test('unwind_stack overflow', () => { - const s = test_initial_state(` - const overflow = x => overflow(x + 1); - overflow(0) - `) - assert_equal( - s.current_calltree_node.error.message, - 'Maximum call stack size exceeded' - ) - assert_equal(s.current_calltree_node.toplevel, true) - assert_equal(s.calltree_node_is_expanded[s.current_calltree_node.id], true) - }), - */ - - test('eval_selection', () => { - const code = ` - const x = () => () => 1 - x() - 2*2 - false && 4 - if(true) { - } - ` - const s0 = test_initial_state(code) - const s1 = COMMANDS.eval_selection(s0, code.indexOf('2'), true).state - assert_equal(s1.value_explorer.result.value, 2) - - // Expand selection - const s2 = COMMANDS.eval_selection(s1, code.indexOf('2'), true).state - assert_equal(s2.value_explorer.result.value, 4) - - const s3 = COMMANDS.eval_selection(s2, code.indexOf('2'), true).state - // Selection is not expanded beyond expression to statement - assert_equal(s3.value_explorer.result.value, 4) - assert_equal(s3.selection_state.node.index, code.indexOf('2')) - assert_equal(s3.selection_state.node.length, 3) - - const s4 = COMMANDS.step_into(s0, code.indexOf('x()')) - const s5 = COMMANDS.eval_selection(s4, code.indexOf('2')) - assert_equal(s5.effects, {type: 'set_status', args: ['out of scope']}) - - const s6 = COMMANDS.eval_selection(s4, code.indexOf('1')) - assert_equal( - s6.effects, - { - type: 'set_status', - args: ['code was not reached during program execution'] - } - ) - - const s7 = COMMANDS.eval_selection(s0, code.indexOf('4')) - assert_equal( - s7.effects, - { - type: 'set_status', - args: ['expression was not reached during program execution'], - } - ) - - const s8 = COMMANDS.eval_selection(s0, code.indexOf('if')) - assert_equal( - s8.effects, - { - type: 'set_status', - args: ['can only evaluate expression, not statement'], - } - ) - }), - - test('eval_selection bug', () => { - const code = `{foo: 1}` - const i = test_initial_state(code) - const index = code.indexOf('1') - const moved = COMMANDS.move_cursor(i, index) - const selection = COMMANDS.eval_selection(moved, index, true).state - const selection2 = COMMANDS.eval_selection(selection, index, true).state - const selection3 = COMMANDS.eval_selection(selection2, index, false).state - assert_equal(selection3.selection_state.node.value, '1') - }), - - test('find_call', () => { - const code = ` - const y = () => y2() - const z = () => z2() - const y2 = () => 1 - const z2 = () => 2 - const target = (x) => target2(x) - const target2 = (x) => target3(x) - const target3 = (x) => 3 - const deep_call = x => { - if(x == 10) { - target(x) - } else { - y() - deep_call(x + 1) - z() - } - } - deep_call(0) - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('target2(x)')) - - assert_equal(s2.current_calltree_node.id, s2.active_calltree_node.id) - - assert_equal(s2.current_calltree_node.args, [10]) - assert_equal(s2.current_calltree_node.code.index, code.indexOf('(x) => target2')) - - const root = root_calltree_node(s2) - const first = root.children[0] - - assert_equal(first.ok, true) - - const find_target = (node, i = 0) => { - if(node.children.length == 1) { - return [i, node.children[0]] - } - - assert_equal(s2.calltree_node_is_expanded[node.id], true) - assert_equal(node.children.length, 3) - assert_equal(node.code != null, true) - - return find_target(node.children[1], i + 1) - } - - const [depth, target] = find_target(first) - assert_equal(depth, 10) - assert_equal(target.args, [10]) - - const target2 = target.children[0] - }), - - test('find_call error', () => { - const code = ` - const unreachable = () => { - 1 - } - - const throws = () => { - throw new Error('bad') - } - - throws() - ` - - const s1 = test_initial_state(code) - const state = COMMANDS.move_cursor(s1, code.indexOf('1')) - assert_equal(state.active_calltree_node, null) - assert_equal(state.current_calltree_node.toplevel, true) - assert_equal(state.value_explorer === null, true) - }), - - test('find_call with native call', () => { - const code = ` - [1,2,3].map(x => x + 1) - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('x + 1')) - assert_equal(s2.current_calltree_node.code.index, code.indexOf('x =>')) - }), - - test('find_call should find first call', () => { - const code = ` - const rec = i => i == 0 ? 0 : rec(i - 1) - rec(10) - ` - const s1 = test_initial_state(code) - const state = COMMANDS.move_cursor(s1, code.indexOf('i == 0')) - assert_equal(state.current_calltree_node.args, [10]) - }), - - test('select_return_value not expanded', () => { - const code = ` - const x = (a) => 1 - x() - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.calltree.arrow_right(s1) - const {state: s3, effects} = COMMANDS.calltree.select_return_value(s2) - assert_equal(s3.value_explorer.result.value, 1) - assert_equal(s3.selection_state.node.index, code.indexOf('x()')) - assert_equal(current_cursor_position(s3), code.indexOf('x()')) - assert_equal(effects, {type: 'set_focus'}) - }), - - test('select_return_value expanded', () => { - const code = ` - const x = (a) => 1 - x() - ` - const s1 = test_initial_state(code) - const s2_0 = COMMANDS.calltree.arrow_right(s1) - // Expand - const s2 = COMMANDS.calltree.arrow_right(s2_0) - const {state: s3, effects} = COMMANDS.calltree.select_return_value(s2) - assert_equal(s3.value_explorer.result.value, 1) - assert_equal(s3.selection_state.node.index, code.indexOf('1')) - assert_equal(current_cursor_position(s3), code.indexOf('1')) - assert_equal(effects, {type: 'set_focus'}) - }), - - test('select_return_value fn curly braces', () => { - const code = ` - const x = (a) => {return 1} - x() - ` - const s1 = test_initial_state(code) - const s2_0 = COMMANDS.calltree.arrow_right(s1) - // Expand - const s2 = COMMANDS.calltree.arrow_right(s2_0) - const {state: s3, effects} = COMMANDS.calltree.select_return_value(s2) - assert_equal(s3.value_explorer.result.value, 1) - assert_equal(s3.selection_state.node.index, code.indexOf('1')) - assert_equal(current_cursor_position(s3), code.indexOf('1')) - assert_equal(effects, {type: 'set_focus'}) - }), - - test('select_return_value fn curly braces no return', () => { - const code = ` - const x = (a) => { 1 } - x() - ` - const s1 = test_initial_state(code) - const s2_0 = COMMANDS.calltree.arrow_right(s1) - // Expand - const s2 = COMMANDS.calltree.arrow_right(s2_0) - const {state: s3, effects} = COMMANDS.calltree.select_return_value(s2) - assert_equal(s3.selection_state, null) - assert_equal(current_cursor_position(s3), code.indexOf('{')) - assert_equal(effects, {type: 'set_focus'}) - }), - - test('select_return_value native', () => { - const code = ` - [1,2,3].map(() => 1) - ` - const s1 = test_initial_state(code) - // Select map - const s2 = COMMANDS.calltree.arrow_right(s1) - const {state: s3, effects} = COMMANDS.calltree.select_return_value(s2) - assert_equal(s3.value_explorer.result.value, [1, 1, 1]) - }), - - test('select_return_value new call', () => { - const code = `new String('1')` - const s1 = test_initial_state(code) - const s2 = COMMANDS.calltree.arrow_right(s1) - const {state: s3, effects} = COMMANDS.calltree.select_return_value(s2) - assert_equal(s3.value_explorer.result.value, '1') - }), - - test('select_arguments not_expanded', () => { - const code = ` - const x = (a) => { 1 } - x(1) - ` - const s1 = test_initial_state(code) - // focus call - const s2 = COMMANDS.calltree.arrow_right(s1) - const s3 = COMMANDS.calltree.select_arguments(s2) - assert_equal(s3.state.value_explorer.result.ok, true) - assert_equal(s3.state.value_explorer.result.value, [1]) - assert_equal(current_cursor_position(s3.state), code.indexOf('(1)')) - assert_equal(s3.effects, {type: 'set_focus'}) - }), - - test('select_arguments expanded', () => { - const code = ` - const x = (a) => { 1 } - x(1) - ` - const s1 = test_initial_state(code) - // focus call - const s2_0 = COMMANDS.calltree.arrow_right(s1) - // expand call - const s2 = COMMANDS.calltree.arrow_right(s2_0) - const s3 = COMMANDS.calltree.select_arguments(s2) - assert_equal( - s3.state.value_explorer.result, - { - ok: true, - value: {a: 1}, - version_number: 0, - } - ) - assert_equal(current_cursor_position(s3.state), code.indexOf('(a)')) - assert_equal(s3.effects, {type: 'set_focus'}) - }), - - test('select_arguments new call', () => { - const code = `new String("1")` - const s1 = test_initial_state(code) - const s2 = COMMANDS.calltree.arrow_right(s1) - const s3 = COMMANDS.calltree.select_arguments(s2).state - assert_equal(s3.value_explorer.result.ok, true) - assert_equal(s3.value_explorer.result.value, ["1"]) - }), - - test('select_error', () => { - const code = ` - const deep = x => { - if(x == 10) { - throw new Error() - } else { - deep(x + 1) - } - } - - deep(0) - ` - const i = test_initial_state(code, code.indexOf('deep(x + 1)')) - const {state: found_err_state, effects} = COMMANDS.calltree.select_error(i) - assert_equal(found_err_state.active_calltree_node.args, [10]) - assert_equal(current_cursor_position(found_err_state), code.indexOf('throw')) - }), - - test('select_error in native fn', () => { - const code = ` - function x() { - Object.entries(null) - } - - x() - ` - const i = test_initial_state(code) - const {state: found_err_state} = COMMANDS.calltree.select_error(i) - assert_equal(found_err_state.active_calltree_node.fn.name, 'x') - assert_equal( - current_cursor_position(found_err_state), - code.indexOf('Object.entries') - ) - }), - - test('move_cursor arguments', () => { - const code = ` - const x = (a, b) => { } - x(1, 2) - ` - const s1 = test_initial_state(code) - // focus call - const s2 = COMMANDS.calltree.arrow_right(s1) - // expand call - const s3 = COMMANDS.calltree.arrow_right(s2) - const s4 = COMMANDS.move_cursor(s3, code.indexOf('a')) - const selected = '(a, b)' - assert_equal(s4.value_explorer, { - index: code.indexOf(selected), - length: selected.length, - result: {ok: true, value: {a: 1, b: 2}, version_number: 0}, - }) - }), - - test('move_cursor concise fn', () => { - const code = ` - const x = y => y*2 - x(2) - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('2')) - assert_equal(s2.value_explorer.index, code.indexOf('y*2')) - assert_equal(s2.value_explorer.length, 3) - assert_equal(s2.value_explorer.result.ok, true) - assert_equal(s2.value_explorer.result.value, 4) - }), - - test('move_cursor let', () => { - const code = ` - let x = 1 - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('x')) - const lettext = 'let x = 1' - assert_equal(s2.value_explorer, { - index: code.indexOf(lettext), - length: lettext.length, - result: {ok: true, value: 1, version_number: 0}, - }) - }), - - test('move_cursor destructuring default', () => { - const code = `const [x = 1, y] = [undefined, 2]` - const s = test_initial_state(code) - assert_equal(s.value_explorer.result.value, {x: 1, y: 2}) - }), - - test('move_cursor after type toplevel', () => { - const code = `1` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('1') + 1) - assert_equal(s2.value_explorer.result.value, 1) - }), - - test('move_cursor after type fn', () => { - const code = ` - const x = () => { 1 } - x() - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.step_into(s1, code.indexOf('x()')) - const s3 = COMMANDS.move_cursor(s2, code.indexOf('1') + 1) - assert_equal(s3.value_explorer.result.value, 1) - }), - - test('move_cursor between statements', () => { - const code = ` - 1 - - /*marker*/ - 1 - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('/') - 1) - assert_equal(s2.value_explorer === null, true) - }), - - test('move_cursor step_into fn', () => { - const code = ` - const x = () => { - 1 - } - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('1')) - assert_equal(s2.value_explorer === null, true) - }), - - test('move_cursor brace', () => { - const code = ` - if(true) { - 1 - } - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('{')) - assert_equal(s2.value_explorer === null, true) - }), - - test('move_cursor concise fn throws', () => { - const code = ` - const throws = () => { - throw new Error('boom') - } - - const x = () => 2 * (throws() + 1) - - x() - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('throws()')) - assert_equal(s2.value_explorer.result.error.message, 'boom') - }), - - test('move_cursor error in fn args bug', () => { - const code = ` - function x() {} - x(null.foo) - ` - const i = test_initial_state(code) - - const m = COMMANDS.move_cursor(i, code.indexOf('x(null')) - assert_equal( - m.value_explorer.result.error, - new TypeError("Cannot read properties of null (reading 'foo')") - ) - }), - - test('frame follows cursor toplevel', () => { - const code = ` - const x = () => { - 1 - } - x() - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('const')) - assert_equal(s2.current_calltree_node.toplevel, true) - assert_equal(s2.active_calltree_node.toplevel, true) - }), - - test('frame follows cursor fn', () => { - const code = ` - const x = () => { - 1 - 2 - } - x() - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('1')) - assert_equal(s2.current_calltree_node.code.index, code.indexOf('() =>')) - // Move within current node - const s3 = COMMANDS.move_cursor(s2, code.indexOf('2')) - assert_equal(s3.current_calltree_node.code.index, code.indexOf('() =>')) - }), - - test('frame follows cursor return back to fn', () => { - const code = ` - const x = () => { - 1 - } - x() - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('1')) - - // Go back toplevel - const s3 = COMMANDS.move_cursor(s2, code.indexOf('const')) - assert_equal(s3.current_calltree_node.toplevel, true) - - // Go back to fn - assert_equal(s3.rt_cxt == null, false) - const s4 = COMMANDS.move_cursor( - {...s3, - // Set rt_cxt to null, ensure eval would not be called again - rt_cxt: null - }, - code.indexOf('1') - ) - assert_equal(s4.current_calltree_node.code.index, code.indexOf('() =>')) - }), - - // Tests for one specific bug - test('frame follows cursor change fn', () => { - const code = ` - const x = () => { - 1 - } - const y = () => {/*y*/ - 2 - z() - } - const z = () => { - 3 - } - x() - y() - ` - const s1 = test_initial_state(code) - - // goto x() - const s2 = COMMANDS.move_cursor(s1, code.indexOf('1')) - - // goto y() - const s3 = COMMANDS.move_cursor(s2, code.indexOf('2')) - - assert_equal(s3.active_calltree_node.code.index, code.indexOf('() => {/*y')) - }), - - test('frame follows cursor deep nested fn', () => { - const code = ` - const y = () => { - 1 - } - const x = i => i == 0 ? y() : x(i - 1) - x(5) - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('1')) - assert_equal(s2.current_calltree_node.code.index, code.indexOf('() =>')) - }), - - test('frame follows cursor intermediate fn', () => { - const code = ` - const y = () => { - z() - } - const z = () => { - 1 - } - const x = i => i == 0 ? y() : x(i - 1) - x(5) - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('1')) - const s3 = COMMANDS.move_cursor(s2, code.indexOf('z()')) - assert_equal(s3.current_calltree_node.code.index, code.indexOf('() =>')) - // Check that node for `y` call was reused - assert_equal( - find_node(root_calltree_node(s2), n => n == s3.current_calltree_node) - == null, - false - ) - }), - - test('frame follows cursor unreachable fn', () => { - const code = ` - const x = () => { - 1 - 2 - } - ` - const s1 = test_initial_state(code) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('1')) - assert_equal(s2.current_calltree_node.toplevel, true) - assert_equal(s2.active_calltree_node, null) - - // Check that when we move cursor inside unreachable function, find_call - // not called again - assert_equal(s2.rt_cxt != null, true) - const s3 = COMMANDS.move_cursor( - // Set rt_cxt to null, ensure it would not be called again - {...s2, rt_cxt: null}, - code.indexOf('2') - ) - assert_equal(s3.active_calltree_node, null) - }), - - test('frame follows cursor only find_call in entrypoint module', () => { - const scratch = `import {x} from 'x'; x()` - const x_code = `export const x = () => 1; x()` - const s1 = test_initial_state({ - '' : scratch, - 'x' : x_code, - }) - const s2 = COMMANDS.move_cursor( - {...s1, current_module: 'x'}, - x_code.indexOf('1') - ) - assert_equal(root_calltree_node(s2).module, '') - }), - - test('find branch initial', () => { - const code = ` - function x(cond) { - if(cond) { - return true - } else { - return false - } - } - - x(true) - x(false) - ` - const i = test_initial_state(code, code.indexOf('return false')) - assert_equal(i.value_explorer.result.value, false) - }), - - test('find branch empty branch', () => { - const code = ` - function x(cond) { - if(cond) { - /* label */ - } - } - - x(false) - x(true) - ` - const i = test_initial_state(code, code.indexOf('label')) - assert_equal(i.active_calltree_node.args[0], true) - }), - - test('find branch move_cursor', () => { - const code = ` - function x(cond) { - if(cond) { - return true - } else { - return false - } - } - - x(true) - x(false) - ` - const i = test_initial_state(code) - const moved = COMMANDS.move_cursor(i, code.indexOf('return false')) - assert_equal(moved.value_explorer.result.value, false) - assert_equal( - i.colored_frames != moved.colored_frames, - true - ) - }), - - test('find branch ternary', () => { - const code = ` - function x(cond) { - return cond ? true : false - } - - x(true) - x(false) - ` - const i = test_initial_state(code, code.indexOf('false')) - assert_equal(i.value_explorer.result.value, false) - }), - - test('find branch move cursor within fn', () => { - const code = ` - function x(cond) { - if(cond) { - return true - } else { - return false - } - } - - x(true) - x(false) - ` - const i = test_initial_state(code) - const s1 = COMMANDS.move_cursor(i, code.indexOf('return false')) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('return true')) - assert_equal(s2.value_explorer.result.value, true) - assert_equal( - s1.colored_frames != s2.colored_frames, - true - ) - }), - - test('find branch fibonacci', () => { - const code = ` - function fib(n) { - if(n == 0 || n == 1) { - return n - } else { - return fib(n - 1) + fib(n - 2) - } - } - - fib(6) - ` - const i = test_initial_state(code) - const moved = COMMANDS.move_cursor(i, code.indexOf('return n')) - assert_equal(moved.value_explorer.result.value, 1) - }), - - test('find branch after if with return', () => { - const code = ` - function x(cond) { - if(cond) { - return true - } - 1 - } - x(true) - x(false) - ` - const i = test_initial_state(code, code.indexOf('1')) - assert_equal(i.value_explorer.result.value, 1) - }), - - test('find branch after if with return complex', () => { - const code = ` - function x(a, b) { - if(a) { - return true - } - if(a) { - return true - } - if(b) { - return true - } else { - if(false) { - return null - } - 1 - } - - } - x(true) - x(false, true) - x(false, false) - ` - const i = test_initial_state(code, code.indexOf('1')) - assert_equal(i.value_explorer.result.value, 1) - assert_equal(i.active_calltree_node.args, [false, false]) - }), - - test('find branch get_execution_paths', () => { - const code = ` - function x() { - if(true) {/*1*/ - } - if(false) { - } else {/*2*/ - if(true) {/*3*/ - true ? 4 : 5 - } - return null - } - // not executed - if(true) { - } - // not executed - true ? 6 : 7 - } - x() - ` - const i = test_initial_state(code, code.indexOf('if')) - assert_equal( - [...get_execution_paths(active_frame(i))].toSorted((a,b) => a - b), - [ - code.indexOf('if(true)') + 1, - code.indexOf('/*1*/') - 1, - code.indexOf('/*2*/') - 1, - code.indexOf('if(true) {/*3*/') + 1, - code.indexOf('/*3*/') - 1, - code.indexOf('4'), - ] - ) - }), - - test('find branch get_execution_paths consice body', () => { - const code = ` - const x = () => true ? 1 : 2 - x() - ` - const i = test_initial_state(code, code.indexOf('true')) - assert_equal( - get_execution_paths(active_frame(i)), - [code.indexOf('1')], - ) - }), - - test('find branch get_execution_paths nested fn', () => { - const code = ` - function x() { - function y() { - true ? 1 : 2 - } - } - x() - ` - const i = test_initial_state(code, code.indexOf('{')) - assert_equal( - get_execution_paths(active_frame(i)), - [], - ) - }), - - test('find branch jump_calltree_node', () => { - const code = ` - function test(x) { - if(x > 0) { - 'label' - } - } - test(1) - test(2) - ` - const i = test_initial_state(code, code.indexOf('label')) - assert_equal(i.active_calltree_node.args[0], 1) - // select second call - const second = COMMANDS.calltree.select_and_toggle_expanded(i, root_calltree_node(i).children[1].id) - assert_equal(second.active_calltree_node.args[0], 2) - }), - - test('find branch preserve selected calltree node when moving inside fn', () => { - const code = ` - function x(cond) { - if(cond) { - true - } else { - false - } - 'finish' - } - x(true) - x(false) - ` - const i = test_initial_state(code) - const first_call_id = root_calltree_node(i).children[0].id - // explicitly select first call - const selected = COMMANDS.calltree.select_and_toggle_expanded(i, first_call_id) - // implicitly select second call by moving cursor - const moved = COMMANDS.move_cursor(selected, code.indexOf('false')) - const finish = COMMANDS.move_cursor(moved, code.indexOf('finish')) - assert_equal(finish.active_calltree_node.id, first_call_id) - }), - - test('find branch select calltree node from logs', () => { - const code = ` - function f(x) { - if(x > 1) { - console.log(x) - } else { - console.log(x) - } - } - f(5) - f(10) - ` - const i = test_initial_state(code) - const log_selected = COMMANDS.calltree.navigate_logs_position(i, 1) - const moved = COMMANDS.move_cursor( - log_selected, - code.indexOf('console.log') - ) - assert_equal(moved.active_calltree_node.args, [10]) - }), - - test('find branch deferred calls', () => { - const code = ` - export const foo = arg => { - return arg - } - foo(1) - ` - const {state: i, on_deferred_call} = test_deferred_calls_state(code) - - // Make deferred call - i.modules[''].foo(2) - - const state = on_deferred_call(i) - const call = get_deferred_calls(state)[0] - assert_equal(call.value, 2) - - // Expand call - const expanded = COMMANDS.calltree.select_and_toggle_expanded(state, call.id) - const moved = COMMANDS.move_cursor(expanded, code.indexOf('return arg')) - assert_equal(moved.active_calltree_node.value, 2) - }), - - - test('stale id in frame function_call.result.calls bug', () => { - const code = ` - const x = () => {/*x*/ - y() - } - - const y = () => { - 1 - } - - x() - ` - - // Eval toplevel frame, id of call (x) will be saved in frame - const s1 = test_initial_state(code) - - // Expand call of x(), id will be changed (bug) - const s2 = COMMANDS.move_cursor(s1, code.indexOf('y()')) - - // Step into from toplevel to call of x(), the stale id will be used - const s3 = COMMANDS.move_cursor(s2, code.indexOf('x()')) - const s4 = COMMANDS.step_into(s3, code.indexOf('x()')) - - assert_equal(s4.active_calltree_node.code.index, code.indexOf('() => {/*x')) - }), - - test('get_initial_state toplevel not entrypoint', () => { - const s = test_initial_state( - { - '' : `import {x} from 'x'; x()`, - 'x' : `export const x = () => 1; x()`, - }, - undefined, - { - current_module: 'x', - } - ) - assert_equal(s.current_calltree_node.toplevel, true) - assert_equal(s.active_calltree_node, null) - }), - - test('module not evaluated because of error in module it depends on', () => { - const s = test_initial_state({ - '' : `import {x} from 'x'`, - 'x' : ` - const has_child_calls = i => i == 0 ? 0 : has_child_calls(i - 1) - has_child_calls(10) - console.log('log') - throw new Error('fail') - `, - }) - assert_equal(root_calltree_node(s).module, 'x') - - // Must collect logs from failed module - assert_equal(s.logs.logs.length, 1) - - const s2 = COMMANDS.move_cursor( - COMMANDS.change_current_module(s, 'x'), - s.files['x'].indexOf('throw') - ) - assert_equal(s2.value_explorer.index, s.files['x'].indexOf('throw')) - - const s3 = COMMANDS.calltree.arrow_right(s) - assert_equal(s3.current_calltree_node.fn.name, 'has_child_calls') - - }), - - test('logs simple', () => { - const code = `console.log(10)` - const i = test_initial_state(code) - assert_equal(i.logs.logs.length, 1) - assert_equal(i.logs.logs[0].args, [10]) - }), - - test('logs', () => { - const code = ` - const deep = x => { - if(x == 10) { - console.log(x) - } else { - deep(x + 1) - } - } - - deep(0) - ` - - const i = test_initial_state(code) - assert_equal(i.logs.logs.length, 1) - assert_equal(i.logs.logs[0].args, [10]) - const state = COMMANDS.calltree.navigate_logs_position(i, 0) - assert_equal(state.logs.log_position, 0) - assert_equal(state.value_explorer.result.value, [10]) - assert_equal(current_cursor_position(state), code.indexOf('(x)')) - }), - - test('deferred calls', () => { - const code = ` - export const fn = (x) => { - fn2(x) - } - - const fn2 = () => { - console.log(1) - } - ` - - const {state: i, on_deferred_call} = test_deferred_calls_state(code) - - // Make deferred call - i.modules[''].fn(10) - - const state = on_deferred_call(i) - assert_equal(state.logs.logs.length, 1) - - const call = get_deferred_calls(state)[0] - assert_equal(call.fn.name, 'fn') - assert_equal(call.code.index, code.indexOf('(x) => {')) - assert_equal(call.args, [10]) - - // Expand call - const expanded = COMMANDS.calltree.select_and_toggle_expanded(state, call.id) - assert_equal(get_deferred_calls(expanded)[0].children[0].fn.name, 'fn2') - - // Navigate logs - const nav = COMMANDS.calltree.navigate_logs_position(expanded, 0) - assert_equal(nav.current_calltree_node.is_log, true) - - const nav2 = COMMANDS.calltree.arrow_left(nav) - assert_equal(nav2.current_calltree_node.fn.name, 'fn2') - }), - - test('deferred calls calltree nav', () => { - const code = ` - const normal_call = (x) => { - } - - normal_call(0) - - export const deferred_call = (x) => { - } - ` - - const {state: i, on_deferred_call} = test_deferred_calls_state(code) - - // When there are no deferred calls, and we press arrow down, nothing should - // happen - const no_deferred_down = - COMMANDS.calltree.arrow_down( - COMMANDS.calltree.arrow_down(i) - ) - - assert_equal(no_deferred_down.current_calltree_node.fn.name, 'normal_call') - - const after_deferred_calls = [1, 2, 3].reduce( - (s, a) => { - // Make deferred calls - i.modules[''].deferred_call(a) - return on_deferred_call(s) - }, - i - ) - - assert_equal( - get_deferred_calls(after_deferred_calls).map(c => c.args[0]), - [1,2,3] - ) - - assert_equal(after_deferred_calls.current_calltree_node.toplevel, true) - - const down = COMMANDS.calltree.arrow_down(after_deferred_calls) - - const first_deferred_call_selected = COMMANDS.calltree.arrow_down( - COMMANDS.calltree.arrow_down(after_deferred_calls) - ) - - // After we press arrow down, first deferred call gets selected - assert_equal( - first_deferred_call_selected.current_calltree_node.args[0], - 1, - ) - - // One more arrow down, second deferred call gets selected - assert_equal( - COMMANDS.calltree.arrow_down(first_deferred_call_selected) - .current_calltree_node - .args[0], - 2 - ) - - // After we press arrow up when first deferred call selected, we select last - // visible non deferred call - assert_equal( - COMMANDS.calltree.arrow_up(first_deferred_call_selected) - .current_calltree_node - .args[0], - 0 - ) - - // After we press arrow left when first deferred call selected, we stay on - // this call - assert_equal( - COMMANDS.calltree.arrow_left(first_deferred_call_selected) - .current_calltree_node - .args[0], - 1 - ) - - - }), - - test('deferred_calls find_call', () => { - const code = ` - export const fn = () => { - fn2() - } - - const fn2 = () => { - console.log(1) - } - ` - - const {state: i, on_deferred_call} = test_deferred_calls_state(code) - - // Make deferred call - i.modules[''].fn() - - const state = on_deferred_call(i) - - const moved = COMMANDS.move_cursor(state, code.indexOf('fn2')) - assert_equal(moved.active_calltree_node.fn.name, 'fn') - - // Move cursor to toplevel and back, find cached (calltree_node_by_loc) call - const move_back = COMMANDS.move_cursor( - COMMANDS.move_cursor(moved, 0), - code.indexOf('fn2') - ) - - assert_equal(move_back.active_calltree_node.fn.name, 'fn') - }), - - test('deferred_calls find_call then deferred_call bug', () => { - const code = ` - export const fn = (x) => { /* label */ } - ` - - const {state: i, on_deferred_call} = test_deferred_calls_state(code) - - // Make deferred call - i.modules[''].fn(1) - - const state = on_deferred_call(i) - - // find call - const moved = COMMANDS.move_cursor(state, code.indexOf('label')) - - // Make deferred call - i.modules[''].fn(2) - - const result = on_deferred_call(moved) - - // there was a bug throwing error when added second deferred call - assert_equal(get_deferred_calls(result).map(c => c.args), [[1], [2]]) - }), - - test('deferred_calls discard on code rerun', () => { - const code = ` - export const fn = () => { /* label */ } - ` - const {state: i, on_deferred_call} = test_deferred_calls_state(code) - - const state = input(i, code, 0).state - - // Make deferred call, calling fn from previous code - i.modules[''].fn(1) - - const result = on_deferred_call(state) - - // deferred calls must be null, because deferred calls from previous executions - // must be discarded - assert_equal(get_deferred_calls(result), null) - }), - - test('deferred_calls several calls bug', () => { - const code = ` - export const fn = i => i == 0 ? 0 : fn(i - 1) - ` - - const {state: i, on_deferred_call} = test_deferred_calls_state(code) - - // Make deferred call - i.modules[''].fn(10) - - const state = on_deferred_call(i) - const call = get_deferred_calls(state)[0] - const expanded = COMMANDS.calltree.select_and_toggle_expanded(state, call.id) - // Make deferred call again. There was a runtime error - expanded.modules[''].fn(10) - }), - - test('deferred_calls find call bug', () => { - const code = ` - export const fn = () => 1 - ` - - const {state: i, on_deferred_call} = test_deferred_calls_state(code) - - const moved = COMMANDS.move_cursor(i, code.indexOf('1')) - assert_equal(moved.active_calltree_node, null) - - // Make deferred call - moved.modules[''].fn(10) - - const after_call = on_deferred_call(moved) - const moved2 = COMMANDS.move_cursor(after_call, code.indexOf('1')) - - assert_equal(moved2.active_calltree_node.value, 1) - }), - - test('async/await await non promise', async () => { - await assert_code_evals_to_async( - ` - await 1 - `, - 1 - ) - }), - - test('async/await await Promise resolved immediately', async () => { - await assert_code_evals_to_async( - ` - await new Promise(resolve => resolve(1)) - `, - 1 - ) - }), - - test('async/await return from async function', async () => { - await assert_code_evals_to_async( - ` - const x = async () => 123 - const y = async () => await x() - await y() - `, - 123 - ) - }), - - test('async/await await resolved Promise', async () => { - await assert_code_evals_to_async( - ` - await Promise.resolve(123) - `, - 123 - ) - }), - - test('async/await await Promise resolved with resolved Promise', async () => { - await assert_code_evals_to_async( - ` - await Promise.resolve(Promise.resolve(123)) - `, - 123 - ) - }), - - test('async/await await Promise resolved with async', async () => { - await assert_code_evals_to_async( - ` - const x = async () => 1 - await Promise.resolve(x()) - `, - 1 - ) - }), - - test('async/await await Promise resolved with rejected Promise', async () => { - await assert_code_error_async( - ` - await Promise.resolve(Promise.reject('boom')) - `, - 'boom', - ) - }), - - test('async/await await Promise returned from async function', async () => { - await assert_code_evals_to_async( - ` - const x = async () => { - return Promise.resolve(123) - } - await x() - `, - 123 - ) - }), - - test('async/await throw from async function', async () => { - await assert_code_error_async( - ` - const x = async () => { throw 'boom' } - await x() - `, - 'boom' - ) - }), - - test('async/await await rejected Promise', async () => { - await assert_code_error_async( - ` - await Promise.reject('boom') - `, - 'boom' - ) - }), - - test('async/await await rejected Promise fn call', async () => { - await assert_code_error_async( - ` - async function test() { - await Promise.reject('boom') - } - await test() - `, - 'boom' - ) - }), - - test('async/await promise rejected with null', async () => { - await assert_code_error_async( - `await Promise.reject()`, - undefined - ) - }), - - test('async/await await rejected Promise returned from async', async () => { - await assert_code_error_async( - ` - const x = async () => Promise.reject('boom') - await x() - `, - 'boom' - ) - }), - - test('async/await Promise.all', async () => { - await assert_code_evals_to_async( - ` - const x = async i => i - await Promise.all([x(0), x(1), x(2)]) - `, - [0,1,2] - ) - }), - - test('async/await calltree', async () => { - const i = await test_initial_state_async(` - const x = () => 1 - const delay = async time => { - await 1 - x() - } - await delay(3) - `) - const root = root_calltree_node(i) - assert_equal(root.children.length, 1) - const call_delay = root.children[0] - assert_equal(call_delay.fn.name, 'delay') - assert_equal(call_delay.fn.name, 'delay') - }), - - test('async/await Promise.all set child promises status ok', async () => { - const i = await test_initial_state_async(` - const async_fn = async () => 1 - await Promise.all([1,2,3].map(async_fn)) - `) - const async_fn_call = - root_calltree_node(i) - .children[0] // map - .children[0] // first call of async_fn - assert_equal(async_fn_call.value.status.ok, true) - assert_equal(async_fn_call.value.status.value, 1) - }), - - test('async/await Promise.all set child promises status error', - async () => { - const i = await test_initial_state_async(` - const async_fn = async () => { throw 1 } - await Promise.all([1,2,3].map(async_fn)) - `) - const async_fn_call = - root_calltree_node(i) - .children[0] // map - .children[0] // first call of async_fn - assert_equal(async_fn_call.value.status.ok, false) - assert_equal(async_fn_call.value.status.error, 1) - }), - - test('async/await logs out of order', async () => { - const i = await test_initial_state_async(` - // Init promises p1 and p2 that are resolved in different order (p2 then - // p1) - const p2 = Promise.resolve(2) - const p1 = p2.then(() => 1) - - const log = async p => { - const v = await p - console.log(v) - } - - await Promise.all([log(p1), log(p2)]) - `) - const logs = i.logs.logs.map(l => l.args[0]) - assert_equal(logs, [2, 1]) - }), - - test('async/await logs out of order timeout', async () => { - const i = await test_initial_state_async(` - const delay = async time => { - await new Promise(res => setTimeout(res, time*10)) - console.log(time) - } - - await Promise.all([delay(2), delay(1)]) - `) - const logs = i.logs.logs.map(l => l.args[0]) - assert_equal(logs, [1, 2]) - }), - - test('async/await external async fn', async () => { - await assert_code_evals_to_async( - ` - const AsyncFunction = - new Function('return (async () => {}).constructor')() - const async_fn = new AsyncFunction('return 1') - await async_fn() - `, - 1 - ) - }), - - test('async/await then bug', async () => { - await assert_code_evals_to_async( - ` - const p2 = Promise.resolve(2) - const p1 = p2.then(() => 1) - const x = () => 1 - await x() - `, - 1 - ) - }), - - test('async/await then non-function', async () => { - await assert_code_evals_to_async( - ` - await Promise.resolve(1).then(2) - `, - 1 - ) - }), - - test('async/await Promise.then creates subcall', async () => { - const i = await test_initial_state_async(` - const x = () => 1 - await Promise.resolve(1).then(x) - `) - const root = root_calltree_node(i) - assert_equal(root.children.at(-1).fn.name, 'then') - assert_equal(root.children.at(-1).children[0].fn.name, 'x') - }), - - test('async/await Promise.catch creates subcall', async () => { - const i = await test_initial_state_async(` - const x = () => 1 - await Promise.reject(1).catch(x) - `) - const root = root_calltree_node(i) - assert_equal(root.children.at(-1).fn.name, 'catch') - assert_equal(root.children.at(-1).children[0].fn.name, 'x') - }), - - test('async/await native Promise.then creates subcall', async () => { - const i = await test_initial_state_async(` - const x = () => 1 - const async_fn = async () => 1 - await async_fn().then(x) - `) - const root = root_calltree_node(i) - assert_equal(root.children.at(-1).children[0].fn.name, 'x') - }), - - test('async/await await promise wrapped to some data structure', async () => { - const i = await assert_code_evals_to_async( - ` - const async_fn = async () => 1 - const x = () => { - return {promise: async_fn()} - } - await x().promise - `, - 1 - ) - }), - - test('async/await await bug', async () => { - const code = ` - const x = () => {} - const test = async () => { - await 1 - x() - } - await Promise.all([test(), test()]) - ` - const i = await test_initial_state_async(code, code.indexOf('await 1')) - assert_value_explorer(i ,1) - }), - - test('async/await edit', async () => { - const code = ` - const f = async () => { - - } - await f() - ` - const i = await test_initial_state_async(code) - const code2 = ` - const f = async () => { - 1 - } - await f() - ` - const next = await input_async(i, code2, code2.indexOf('1')) - assert_equal(next.active_calltree_node.fn.name, 'f') - assert_equal(next.value_explorer.result.value, 1) - }), - - test('async/await move_cursor', async () => { - const code = ` - const f = async () => { - 1 - } - await f() - ` - const i = await test_initial_state_async(code) - const after_move = await COMMANDS.move_cursor(i, code.indexOf('1')) - assert_equal(after_move.active_calltree_node.fn.name, 'f') - }), - - test('async/await move_cursor deferred call', async () => { - const code = ` - export const fn = async () => { - await fn2() - } - - const fn2 = async () => { - return 1 - } - ` - const {state: i, on_deferred_call} = test_deferred_calls_state(code) - - // Make deferred call - i.modules[''].fn() - - const state = on_deferred_call(i) - const moved_state = COMMANDS.move_cursor(state, code.indexOf('1')) - assert_equal(moved_state.active_calltree_node.fn.name, 'fn2') - }), - - test('async/await async deferred call', async () => { - const code = ` - await new Object() - export const fn = () => 1 - ` - const {state: i, on_deferred_call} = test_deferred_calls_state(code) - - const result = await i.eval_modules_state.promise - - const s = COMMANDS.eval_modules_finished( - i, - i, - result, - i.eval_modules_state.node, - i.eval_modules_state.toplevel - ) - - // Make deferred call - s.modules[''].fn() - const state = on_deferred_call(s) - assert_equal(get_deferred_calls(state).length, 1) - assert_equal(get_deferred_calls(state)[0].value, 1) - }), - - test('async/await await argument bug', async () => { - await assert_code_evals_to_async( - ` - Object.assign({}, await {foo: 1}) - `, - {foo: 1} - ) - }), - - test('async/await move_cursor before code evaluated', async () => { - const i = test_initial_state(` - await new Promise(resolve => null) - `) - const moved = COMMANDS.move_cursor(i, 0) - // No assertion, must not throw - }), - - test('record io', () => { - let app_window_patches - - // Patch Math.random to always return 1 - app_window_patches = {'Math.random': () => 1} - - const initial = test_initial_state(`const x = Math.random()`, undefined, { - app_window_patches - }) - - // Now call to Math.random is cached, break it to ensure it was not called - // on next run - app_window_patches = {'Math.random': () => { throw 'fail' }} - - const next = input(initial, `const x = Math.random()*2`, 0, {app_window_patches}).state - assert_equal(next.value_explorer.result.value, 2) - assert_equal(next.rt_cxt.io_trace_index, 1) - - // Patch Math.random to return 2. - // TODO The first call to Math.random() is cached with value 1, and the - // second shoud return 2 - app_window_patches = {'Math.random': () => 2} - const replay_failed = input( - initial, - `const x = Math.random() + Math.random()`, - 0, - {app_window_patches} - ).state - - // TODO must reuse first cached call? - assert_equal(replay_failed.value_explorer.result.value, 4) - }), - - test('record io trace discarded if args does not match', async () => { - // Patch fetch - let app_window_patches - app_window_patches = {fetch: async () => 'first'} - - const initial = await test_initial_state_async(` - console.log(await fetch('url', {method: 'GET'})) - `, undefined, {app_window_patches}) - assert_equal(initial.logs.logs[0].args[0], 'first') - - // Patch fetch again - app_window_patches = {fetch: async () => 'second'} - - const cache_discarded = await input_async(initial, ` - console.log(await fetch('url', {method: 'POST'})) - `, 0, {app_window_patches}) - assert_equal(cache_discarded.logs.logs[0].args[0], 'second') - }), - - test('record io fetch rejects', async () => { - // Patch fetch - let app_window_patches - app_window_patches = {fetch: () => globalThis.app_window.Promise.reject('fail')} - - const initial = await test_initial_state_async(` - await fetch('url', {method: 'GET'}) - `, undefined, {app_window_patches}) - assert_equal(root_calltree_node(initial).error, 'fail') - - // Patch fetch again - app_window_patches = {fetch: () => async () => 'result'} - - const with_cache = await input_async(initial, ` - await fetch('url', {method: 'GET'}) - `, 0, {app_window_patches}) - assert_equal(root_calltree_node(initial).error, 'fail') - }), - - test('record io preserve promise resolution order', async () => { - // Generate fetch function which calls get resolved in reverse order - const calls = [] - function fetch(...args) { - let resolver - const promise = new (globalThis.app_window.Promise)(r => {resolver = r}) - calls.push({resolver, promise, args}) - return promise - } - - function resolve() { - [...calls].reverse().forEach(call => call.resolver(...call.args)) - } - - let app_window_patches - - // Patch fetch - app_window_patches = {fetch} - - const code = ` - await Promise.all( - [1, 2, 3].map(async v => { - const result = await fetch(v) - console.log(result) - }) - ) - ` - - const initial_promise = test_initial_state_async(code, undefined, {app_window_patches}) - - resolve() - - const initial = await initial_promise - - // calls to fetch are resolved in reverse order - assert_equal(initial.logs.logs.map(l => l.args[0]), [3,2,1]) - - // Break fetch to ensure it is not get called anymore - app_window_patches = {fetch: () => {throw 'broken'}} - - const with_cache = await input_async( - initial, - code, - 0, - {app_window_patches} - ) - - // cached calls to fetch should be resolved in the same (reverse) order as - // on the first run, so first call wins - assert_equal(with_cache.logs.logs.map(l => l.args[0]), [3,2,1]) - }), - - test('record io setTimeout', async () => { - let app_window_patches - // Patch fetch to return result in 10ms - app_window_patches = { - fetch: () => new (globalThis.app_window.Promise)(resolve => setTimeout(resolve, 10)) - } - - const code = ` - setTimeout(() => console.log('timeout'), 0) - await fetch().then(() => console.log('fetch')) - ` - - const i = await test_initial_state_async(code, undefined, {app_window_patches}) - - // First executed setTimeout, then fetch - assert_equal(i.logs.logs.map(l => l.args[0]), ['timeout', 'fetch']) - - // Break fetch to ensure it would not be called - app_window_patches = { - fetch: async () => {throw 'break'} - } - - const with_cache = await input_async(i, code, 0, {app_window_patches}) - - // Cache must preserve resolution order - assert_equal(with_cache.logs.logs.map(l => l.args[0]), ['timeout', 'fetch']) - }), - - test('record io clear io trace', async () => { - const s1 = test_initial_state(`Math.random()`) - const rnd = s1.value_explorer.result.value - const s2 = input(s1, `Math.random() + 1`, 0).state - assert_equal(s2.value_explorer.result.value, rnd + 1) - const cleared = input(COMMANDS.clear_io_trace(s2), `Math.random() + 1`).state - assert_equal( - cleared.value_explorer.result.value == rnd + 1, - false - ) - }), - - test('record io no io trace on deferred calls', async () => { - const code = ` - const x = Math.random - export const fn = () => x() - ` - - const {state: i, on_deferred_call} = test_deferred_calls_state(code) - - // Make deferred call - i.modules[''].fn() - - const state = on_deferred_call(i) - - // Deferred calls should not be record in cache - assert_equal(state.rt_cxt.io_trace.length, 0) - }), - - test('record io discard prev execution', () => { - // Populate cache - const i = test_initial_state(`Math.random(0)`) - const rnd = i.active_calltree_node.children[0].value - - // Run two versions of code in parallel - const next = input(i, `await Promise.resolve()`, 0) - const next2 = input(i, `Math.random(1)`, 0).state - const next_rnd = i.active_calltree_node.children[0].value - assert_equal(rnd, next_rnd) - }), - - test('record io Date', () => { - assert_equal( - test_initial_state('new Date()').io_trace.length, - 1 - ) - assert_equal( - test_initial_state('new Date("2020-01-01")').io_trace, - undefined, - ) - assert_equal( - typeof(test_initial_state('Date()').io_trace[0].value), - 'string', - ) - assert_equal( - typeof(test_initial_state('new Date()').io_trace[0].value), - 'object', - ) - }), - - test('record io hangs bug', async () => { - let app_window_patches - app_window_patches = { - fetch: () => new (globalThis.app_window.Promise)(resolve => setTimeout(resolve, 0)) - } - - const code = ` - const p = fetch('') - Math.random() - await p - ` - - const i = await test_initial_state_async(code, undefined, {app_window_patches}) - - assert_equal(i.io_trace.length, 3) - - const next_code = `await fetch('')` - - const state = await input_async(i, next_code, 0, {app_window_patches}) - assert_equal(state.io_trace.length, 2) - }), - - test('record io logs recorded twice bug', () => { - const code = `Math.random()` - const i = test_initial_state(code) - const second = input( - i, - `console.log(1); Math.random(); Math.random()`, - 0 - ) - assert_equal(second.state.logs.logs.length, 1) - }), - - test('record io expand_calltree_node bug', () => { - const code = ` - function x(i) { - return i == 0 ? Math.random() : x(i - 1) - } - x(2) - x(2) - ` - const i = test_initial_state(code) - const with_trace = run_code(i) - const second_call = root_calltree_node(with_trace).children[1] - assert_equal(second_call.fn.name, 'x') - const expanded = COMMANDS.calltree.select_and_toggle_expanded(with_trace, second_call.id) - const second_call_2 = root_calltree_node(expanded).children[1] - assert_equal(second_call_2.ok, true) - assert_equal(second_call.value, second_call_2.value) - }), - - test('value_explorer Set', () => { - assert_equal( - header(new Set(['foo', 'bar'])), - 'Set {0: "foo", 1: "bar"}' - ) - }), - - test('value_explorer Map', () => { - assert_equal( - header(new Map([['foo', 'bar'], ['baz', 'qux']])), - 'Map {foo: "bar", baz: "qux"}' - ) - }), - - test('let_versions find_versioned_lets toplevel', () => { - const result = do_parse(` - let x - x = 1 - function foo() { - x - } - `) - assert_equal(result.node.has_versioned_let_vars, true) - }), - - test('let_versions find_versioned_lets', () => { - function assert_is_versioned_let(code, is_versioned) { - const result = do_parse(code) - const root = find_node(result.node, - n => n.name == 'root' && n.type == 'function_expr' - ) - assert_equal(root.has_versioned_let_vars, is_versioned) - const node = find_node(result.node, n => n.index == code.indexOf('x')) - assert_equal(!(!node.is_versioned_let_var), is_versioned) - } - - assert_is_versioned_let( - ` - function root() { - let x - x = 1 - function foo() { - x - } - } - `, - true - ) - - // closed but constant - assert_is_versioned_let( - ` - function root() { - let x - function foo() { - x - } - } - `, - false - ) - - // assigned but not closed - assert_is_versioned_let( - ` - function root() { - let x - x = 1 - } - `, - false - ) - - // not closed, var has the same name - assert_is_versioned_let( - ` - function root() { - let x - x = 1 - function foo() { - let x - x - } - } - `, - false - ) - - // not closed, var has the same name - assert_is_versioned_let( - ` - function root() { - let x - x = 1 - if(true) { - let x - function foo() { - x - } - } - } - `, - false - ) - }), - - test('let_versions assign to let variable', () => { - const code = ` - let result = 0 - function unused() { - result = 2 - } - result = 1 - ` - const i = test_initial_state(code, code.indexOf('result = 1')) - assert_value_explorer(i, 1) - }), - - test('let_versions', () => { - const code = ` - let x - [1,2].forEach(y => { - x /*x*/ - x = y - }) - ` - const x_pos = code.indexOf('x /*x*/') - const i = test_initial_state(code, x_pos) - const second_iter = COMMANDS.calltree.arrow_down(i) - const select_x = COMMANDS.move_cursor(second_iter, x_pos) - assert_equal(select_x.value_explorer.result.value, 1) - }), - - test('let_versions close let var bug', () => { - const code = ` - let x - x = 1 - function y() { - return {x} - } - y() /*y()*/ - ` - const i = test_initial_state(code, code.indexOf('y() /*y()*/')) - assert_equal(i.value_explorer.result.value, {x: 1}) - }), - - test('let_versions initial let value', () => { - const code = ` - let x - function y() { - x /*x*/ - } - y() - ` - const x_pos = code.indexOf('x /*x*/') - const i = test_initial_state(code, x_pos) - assert_equal(i.value_explorer.result.ok, true) - assert_equal(i.value_explorer.result.value, undefined) - }), - - test('let_versions save version bug', () => { - const code = ` - let x = 0 - - function set_x(value) { - x = value - } - - function get_x() { - x /* result */ - } - - get_x() - - set_x(10) - x = 10 - set_x(10) - x = 10 - ` - const i = test_initial_state(code, code.indexOf('x /* result */')) - assert_equal(i.value_explorer.result.value, 0) - }), - - test('let_versions expand_calltree_node', () => { - const code = ` - let y - - function foo(x) { - y /*y*/ - bar(y) - } - - function bar(arg) { - } - - foo(0) - y = 11 - foo(0) - y = 12 - ` - const i = test_initial_state(code) - const second_foo_call = root_calltree_node(i).children[1] - assert_equal(second_foo_call.has_more_children, true) - const expanded = COMMANDS.calltree.select_and_toggle_expanded(i, second_foo_call.id) - const bar_call = root_calltree_node(expanded).children[1].children[0] - assert_equal(bar_call.fn.name, 'bar') - assert_equal(bar_call.args, [11]) - const moved = COMMANDS.move_cursor(expanded, code.indexOf('y /*y*/')) - assert_equal(moved.value_explorer.result.value, 11) - }), - - test('let_versions expand_calltree_node 2', () => { - const code = ` - let y - - function deep(x) { - if(x < 10) { - y /*y*/ - y = x - deep(x + 1) - } - } - - deep(0) - y = 11 - deep(0) - y = 12 - ` - const i = test_initial_state(code) - const second_deep_call = root_calltree_node(i).children[1] - assert_equal(second_deep_call.has_more_children, true) - const expanded = COMMANDS.calltree.select_and_toggle_expanded(i, second_deep_call.id) - const moved = COMMANDS.move_cursor(expanded, code.indexOf('y /*y*/')) - assert_equal(moved.value_explorer.result.value, 11) - }), - - test('let_versions create multiversion within expand_calltree_node', () => { - const code = ` - function x() { - let y - function set(value) { - y = value - } - set(1) - y /*result*/ - set(2) - } - - x() - x() - - ` - const i = test_initial_state(code) - const second_x_call = root_calltree_node(i).children[1] - assert_equal(second_x_call.has_more_children, true) - const expanded = COMMANDS.calltree.select_and_toggle_expanded(i, second_x_call.id) - const moved = COMMANDS.move_cursor(expanded, code.indexOf('y /*result*/')) - assert_equal(moved.value_explorer.result.value, 1) - }), - - test('let_versions mutable closure', () => { - const code = ` - const holder = (function() { - let value - return { - get: () => value, - set: (v) => { - value /*value*/ - value = v - } - } - })() - Array.from({length: 10}).map((_, i) => { - holder.set(i) - }) - holder.get() - ` - const i = test_initial_state(code, code.indexOf('holder.get')) - assert_equal(i.value_explorer.result.value, 9) - - const map_expanded = COMMANDS.calltree.select_and_toggle_expanded( - i, - root_calltree_node(i).children[2].id - ) - const expanded = COMMANDS.calltree.select_and_toggle_expanded( - map_expanded, - root_calltree_node(map_expanded).children[2].children[5].id - ) - const set_call = COMMANDS.calltree.arrow_right( - COMMANDS.calltree.arrow_right( - expanded - ) - ) - assert_equal( - set_call.active_calltree_node.code.index, - code.indexOf('(v) =>') - ) - const moved = COMMANDS.move_cursor(set_call, code.indexOf('value /*value*/')) - assert_equal(moved.value_explorer.result.value, 4) - }), - - test('let_versions forEach', () => { - const code = ` - let sum = 0 - [1,2,3].forEach(v => { - sum = sum + v - }) - sum /*first*/ - [1,2,3].forEach(v => { - sum = sum + v - }) - sum /*second*/ - ` - const i = test_initial_state(code, code.indexOf('sum /*first*/')) - assert_equal(i.value_explorer.result.value, 6) - const second = COMMANDS.move_cursor(i, code.indexOf('sum /*second*/')) - assert_equal(second.value_explorer.result.value, 12) - }), - - test('let_versions scope', () => { - assert_code_evals_to(` - let x = 1 - let y = 1 - function change_x() { - x = 2 - } - function change_y() { - y = 2 - } - function unused() { - return {} - } - if(false) { - } else { - if((change_y() || true) ? true : null) { - const a = [...[{...{ - y: unused()[!(1 + (true ? {y: [change_x()]} : null))] - }}]] - } - } - {x,y} /*result*/ - `, - {x: 2, y: 2} - ) - }), - - test('let_versions expr', () => { - assert_code_evals_to(` - let x = 0 - function inc() { - x = x + 1 - return 0 - } - x + inc() + x + inc() + x - `, - 3 - ) - }), - - test('let_versions update in assignment', () => { - assert_code_evals_to(` - let x - function set(value) { - x = 1 - return 0 - } - x = set() - x - `, - 0 - ) - }), - - test('let_versions update in assignment closed', () => { - const code = ` - function test() { - let x - function set(value) { - x = 1 - return 0 - } - x = set() - return x - } - test() - ` - const i = test_initial_state(code, code.indexOf('return x')) - assert_equal(i.value_explorer.result.value, 0) - }), - - test('let_versions multiple vars with same name', () => { - const code = ` - let x - function x_1() { - x = 1 - } - if(true) { - let x = 0 - function x_2() { - x = 2 - } - x /* result 0 */ - x_1() - x /* result 1 */ - x_2() - x /* result 2 */ - } - ` - const i = test_initial_state(code, code.indexOf('x /* result 0 */')) - const frame = active_frame(i) - const result_0 = find_node(frame, n => n.index == code.indexOf('x /* result 0 */')).result - assert_equal(result_0.value, 0) - const result_1 = find_node(frame, n => n.index == code.indexOf('x /* result 1 */')).result - assert_equal(result_1.value, 0) - const result_2 = find_node(frame, n => n.index == code.indexOf('x /* result 2 */')).result - assert_equal(result_2.value, 2) - }), - - test('let_versions closed let vars bug', () => { - const code = ` - let x = 0 - function inc() { - x = x + 1 - } - function test() { - inc() - x /*x*/ - } - test() - ` - const i = test_initial_state(code, code.indexOf('x /*x*/')) - assert_equal(i.value_explorer.result.value, 1) - }), - - test('let_versions assign and read variable multiple times within call', () => { - const code = ` - let x; - (() => { - x = 1 - console.log(x) - x = 2 - console.log(x) - })() - ` - }), - - test('let_versions let assigned undefined bug', () => { - const code = ` - let x = 1 - function set(value) { - x = value - } - set(2) - set(undefined) - x /*x*/ - ` - const i = test_initial_state(code, code.indexOf('x /*x*/')) - assert_equal(i.value_explorer.result.value, undefined) - }), - - // TODO function args should have multiple versions same as let vars - - // test('let_versions function args closure', () => { - // const code = ` - // (function(x) { - // function y() { - // x /*x*/ - // } - // y() - // x = 1 - // y() - // })(0) - // ` - // const i = test_initial_state(code) - // const second_y_call = root_calltree_node(i).children[0].children[1] - // const selected = COMMANDS.calltree.select_and_toggle_expanded(i, second_y_call.id) - // const moved = COMMANDS.move_cursor(selected, code.indexOf('x /*x*/')) - // assert_equal(moved.value_explorer.result.value, 1) - // }), - - test('let_versions async/await', async () => { - const code = ` - let x - function set(value) { - x = value - } - await set(1) - x /*x*/ - ` - const i = await test_initial_state_async(code, code.indexOf('x /*x*/')) - assert_equal(i.value_explorer.result.value, 1) - }), - - /* - TODO this test fails. To fix it, we should record version_counter after - await finished and save it in calltree_node - */ - //test('let_versions async/await 2', async () => { - // const code = ` - // let x - // function set(value) { - // x = value - // Promise.resolve().then(() => { - // x = 10 - // }) - // } - // await set(1) - // x /*x*/ - // ` - // const i = await test_initial_state_async(code, code.indexOf('x /*x*/')) - // assert_equal(i.value_explorer.result.value, 10) - //}), - - // Test that expand_calltree_node produces correct id for expanded nodes - test('let_versions native call', () => { - const code = ` - function x() {} - [1,2].map(x) - [1,2].map(x) - ` - const i = test_initial_state(code) - const second_map_call = i.calltree.children[0].children[1] - assert_equal(second_map_call.has_more_children, true) - const expanded = COMMANDS.calltree.select_and_toggle_expanded(i, second_map_call.id) - const second_map_call_exp = expanded.calltree.children[0].children[1] - assert_equal(second_map_call.id == second_map_call_exp.id, true) - assert_equal(second_map_call_exp.children[0].id == second_map_call_exp.id + 1, true) - }), - - test('let_versions expand_calltree_node twice', () => { - const code = ` - function test() { - let x = 0 - function test2() { - function foo() { - x /*x*/ - } - x = x + 1 - foo() - } - test2() - } - test() - test() - ` - const i = test_initial_state(code) - const test_call = root_calltree_node(i).children[1] - assert_equal(test_call.has_more_children , true) - - const expanded = COMMANDS.calltree.select_and_toggle_expanded(i, test_call.id) - const test2_call = root_calltree_node(expanded).children[1].children[0] - assert_equal(test2_call.has_more_children, true) - - const expanded2 = COMMANDS.calltree.select_and_toggle_expanded(expanded, test2_call.id) - const foo_call = root_calltree_node(expanded2).children[1].children[0].children[0] - - const expanded3 = COMMANDS.calltree.select_and_toggle_expanded(expanded2, foo_call.id) - - const moved = COMMANDS.move_cursor(expanded3, code.indexOf('x /*x*/')) - assert_equal(moved.value_explorer.result.value, 1) - }), - - test('let_versions deferred calls', () => { - const code = ` - let x = 0 - export const inc = () => { - return do_inc() - } - const do_inc = () => { - x = x + 1 - return x - } - inc() - ` - - const {state: i, on_deferred_call} = test_deferred_calls_state(code, code.indexOf('let x')) - - assert_value_explorer(i, 0) - - // Make deferred call - i.modules[''].inc() - - const state = on_deferred_call(i) - const call = get_deferred_calls(state)[0] - assert_equal(call.has_more_children, true) - assert_equal(call.value, 2) - - // Expand call - // first arrow rights selects do_inc call, second steps into it - const expanded = COMMANDS.calltree.arrow_right( - COMMANDS.calltree.arrow_right( - COMMANDS.calltree.select_and_toggle_expanded(state, call.id) - ) - ) - // Move cursor - const moved = COMMANDS.move_cursor(expanded, code.indexOf('return x')) - assert_equal(moved.value_explorer.result.value, 2) - }), - - - test('let_versions deferred calls get value', () => { - const code = ` - let x = 0 - - function noop() { - } - - function set(value) { - x = value - noop() - } - - set(1) - set(2) - set(3) - - export const get = () => x - ` - - const {state: i} = test_deferred_calls_state(code) - - const second_set_call = root_calltree_node(i).children[1] - assert_equal(second_set_call.has_more_children, true) - - const exp = COMMANDS.calltree.select_and_toggle_expanded(i, second_set_call.id) - assert_equal(exp.modules[''].get(), 3) - }), - - test('let_versions multiple assignments', () => { - const code = ` - let x - function foo () { - x /*x foo*/ - } - x = 1 - foo() - x = 2 - foo() /*foo 2*/ - x = 3 - x /*x*/ - ` - const i = test_initial_state(code, code.indexOf('x /*x*/')) - assert_value_explorer(i, 3) - const stepped = COMMANDS.step_into(i, code.indexOf('foo() /*foo 2*/')) - const moved = COMMANDS.move_cursor(stepped, code.indexOf('x /*x foo*/')) - assert_value_explorer(moved, 2) - }), - - test('let_versions bug access before init', () => { - const code = ` - Object.assign({}) - const x = {} - x.y = 1 - let result = 0 - function() { - result = 1 - } - ` - const i = test_initial_state(code, code.indexOf('let result')) - assert_value_explorer(i, 0) - }), - - test('let_versions bug version counter', () => { - const code = ` - let i = 0 - const x = {value: 1} - function unused() { - i = 1 - } - i = 2 - x.value = 2 - x /*result*/ - ` - const i = test_initial_state(code, code.indexOf('x /*result*/')) - assert_value_explorer(i, {value: 2}) - }), - - test('let_versions bug version counter 2', () => { - const code = ` - let i = 0 - function unused() { - i = 1 - } - i = 1 - i /*result*/ - i = 2 - ` - const i = test_initial_state(code, code.indexOf('i /*result*/')) - assert_value_explorer(i, 1) - }), - - test('let_versions bug version counter multiple assignments', () => { - const code = ` - let i = 0, j = 0 - function unused() { - i = 1 - } - i = 1, j = 1 - i /*result*/ - i = 2 - ` - const i = test_initial_state(code, code.indexOf('i /*result*/')) - assert_value_explorer(i, 1) - }), - - test('mutability array', () => { - const code = ` - const arr = [2,1] - arr.at(1) - arr.push(3) - arr /*after push*/ - arr.sort() - arr /*after sort*/ - arr[0] = 4 - arr /*after set*/ - ` - const i = test_initial_state(code, code.indexOf('arr.at')) - assert_value_explorer(i, 1) - - const s1 = COMMANDS.move_cursor(i, code.indexOf('arr /*after push*/')) - assert_value_explorer(s1, [2,1,3]) - - const s2 = COMMANDS.move_cursor(i, code.indexOf('arr /*after sort*/')) - assert_value_explorer(s2, [1,2,3]) - - const s3 = COMMANDS.move_cursor(i, code.indexOf('arr /*after set*/')) - assert_value_explorer(s3, [4,2,3]) - }), - - test('mutability array set length', () => { - const code = ` - const x = [1,2,3] - x.length = 2 - x /*x*/ - x.length = 1 - ` - const i = test_initial_state(code, code.indexOf('x /*x*/')) - assert_value_explorer(i, [1,2]) - }), - - test('mutability array method name', () => { - assert_code_evals_to(`[].sort.name`, 'sort') - assert_code_evals_to(`[].forEach.name`, 'forEach') - }), - - test('mutability array method returns itself', () => { - const code = ` - const x = [3,2,1] - const y = x.sort() - if(x != y) { - throw new Error('not eq') - } - x.push(4) - ` - const i = test_initial_state(code, code.indexOf('const y')) - assert_equal(root_calltree_node(i).ok, true) - assert_value_explorer(i, [1,2,3]) - }), - - test('mutability set', () => { - const code = ` - const s = new Set([1,2]) - s.delete(2) - if(s.size != 1) { - throw new Error('size not eq') - } - s.add(3) - s /*s*/ - ` - const i = test_initial_state(code, code.indexOf('const s')) - assert_value_explorer(i, new Set([1,2])) - const moved = COMMANDS.move_cursor(i, code.indexOf('s /*s*/')) - assert_value_explorer(moved, new Set([1,3])) - }), - - test('mutability set method name', () => { - assert_code_evals_to(`new Set().delete.name`, 'delete') - }), - - // This test is for browser environment where runtime is loaded from the main - // (IDE) window, and user code is loaded from app window - test('mutability instanceof', () => { - assert_code_evals_to(`{} instanceof Object`, true) - assert_code_evals_to(`new Object() instanceof Object`, true) - assert_code_evals_to(`[] instanceof Array`, true) - assert_code_evals_to(`new Array() instanceof Array`, true) - assert_code_evals_to(`new Set() instanceof Set`, true) - assert_code_evals_to(`new Map() instanceof Map`, true) - }), - - test('mutability map', () => { - const code = ` - const s = new Map([['foo', 1], ['bar', 2]]) - s.delete('foo') - s.set('baz', 3) - s /*s*/ - ` - const i = test_initial_state(code, code.indexOf('const s')) - assert_value_explorer(i, {foo: 1, bar: 2}) - const moved = COMMANDS.move_cursor(i, code.indexOf('s /*s*/')) - assert_value_explorer(moved, {bar: 2, baz: 3}) - }), - - test('mutability object', () => { - const code = ` - const s = {foo: 1, bar: 2} - s.foo = 2 - s.baz = 3 - s /*s*/ - ` - const i = test_initial_state(code, code.indexOf('const s')) - assert_value_explorer(i, {foo: 1, bar: 2}) - const moved = COMMANDS.move_cursor(i, code.indexOf('s /*s*/')) - assert_value_explorer(moved, {foo: 2, bar: 2, baz: 3}) - }), - - test('mutability', () => { - const code = ` - const make_array = () => [3,2,1] - const x = make_array() - x.sort() - ` - - const i = test_initial_state(code) - - const index = code.indexOf('x.sort()') - - const selected_x = COMMANDS.eval_selection(i, index, true).state - - assert_equal(selected_x.selection_state.node.length, 'x'.length) - - assert_selection(selected_x, [3, 2, 1]) - - const selected_sort = COMMANDS.eval_selection( - COMMANDS.eval_selection(selected_x, index, true).state, index, true - ).state - - assert_equal(selected_sort.selection_state.node.length, 'x.sort()'.length) - - assert_selection(selected_sort, [1,2,3]) - }), - - test('mutability value_explorer bug', () => { - const code = ` - const x = [3,2,1] - x.sort() - x /*x*/ - ` - const i = test_initial_state(code, code.indexOf('x /*x*/')) - assert_value_explorer( - i, - [1,2,3] - ) - }), - - test('mutability with_version_number', () => { - const code = ` - const make_array = () => [3,2,1] - const x = make_array() - x.sort() - ` - const i = test_initial_state(code, code.indexOf('const x')) - - assert_value_explorer(i, [3,2,1]) - }), - - test('mutability member access version', () => { - const code = ` - const x = [0] - x[0] /*x[0]*/ - x[0] = 1 - ` - const i = test_initial_state(code, code.indexOf('x[0] /*x[0]*/')) - assert_equal(i.value_explorer.result.value, 0) - }), - - test('mutability assignment', () => { - const code = ` - const x = [0] - x[0] = 1 - ` - const i = test_initial_state(code) - const index = code.indexOf('x[0]') - const evaled = COMMANDS.eval_selection( - COMMANDS.eval_selection(i, index).state, - index, - ).state - assert_equal(evaled.selection_state.node.length, 'x[0]'.length) - assert_selection(evaled, 1) - }), - - test('mutability assignment value explorer', () => { - const code = ` - const x = [0] - x[0] = 1 - ` - const i = test_initial_state(code, code.indexOf('x[0]')) - assert_value_explorer(i, 1) - }), - - test('mutability multiple assignment value explorer', () => { - const code = ` - const x = [0] - x[0] = 1, x[0] = 2 - x /*x*/ - ` - const i = test_initial_state(code, code.indexOf('x[0]')) - assert_equal(i.value_explorer, null) - const moved = COMMANDS.move_cursor(i, code.indexOf('x /*x*/')) - assert_value_explorer(moved, [2]) - }), - - test('mutability assignment value explorer new value', () => { - const code = ` - const x = [0] - x[0] = 1 - x[0] /*x*/ - ` - const i = test_initial_state(code, code.indexOf('x[0] /*x*/')) - assert_value_explorer(i, [1]) - }), - - test('mutability eval_selection lefthand', () => { - const code = ` - const x = [0] - x[0] = 1 - ` - const i = test_initial_state(code) - const evaled = COMMANDS.eval_selection(i, code.indexOf('x[0]')).state - assert_selection(evaled, [0]) - // expand eval to x[0] - const evaled2 = COMMANDS.eval_selection(evaled, code.indexOf('x[0]')).state - assert_selection(evaled2, 1) - }), - - test('mutability multiple assignments', () => { - const code = ` - const x = [0] - x[0] = 1 - x /*x*/ - x[0] = 2 - ` - const i = test_initial_state(code, code.indexOf('x /*x*/')) - assert_value_explorer(i, [1]) - }), - - test('mutability value explorer', () => { - const code = ` - const x = [0] - x[0] = 1 - ` - const i = test_initial_state(code, code.indexOf('x[0] = 1')) - assert_value_explorer(i, 1) - }), - - test('mutability calltree value explorer', () => { - const i = test_initial_state(` - const array = [3,2,1] - function sort(array) { - return array.sort() - } - sort(array) - `) - const selected = COMMANDS.calltree.select_and_toggle_expanded(i, root_calltree_node(i).children[0].id) - - const args = selected.value_explorer.result.value['*arguments*'] - assert_versioned_value(i, args, {array: [3,2,1]}) - - const returned = selected.value_explorer.result.value['*return*'] - assert_versioned_value(i, returned, [1,2,3]) - }), - - test('mutability import mutable value', () => { - const code = { - '': ` - import {array} from 'x.js' - import {change_array} from 'x.js' - change_array() - array /*result*/ - `, - 'x.js': ` - export const array = ['initial'] - export const change_array = () => { - array[0] = 'changed' - } - ` - } - const main = code[''] - const i = test_initial_state(code, main.indexOf('import')) - assert_value_explorer(i, {array: ['initial']}) - const sel = COMMANDS.eval_selection(i, main.indexOf('array')).state - assert_selection(sel, ['initial']) - const moved = COMMANDS.move_cursor(sel, main.indexOf('array /*result*/')) - assert_value_explorer(moved, ['changed']) - }), - - test('mutability Object.assign', () => { - const i = test_initial_state(`Object.assign({}, {foo: 1})`) - assert_value_explorer(i, {foo: 1}) - }), - - test('mutability wrap external arrays', () => { - const code = ` - const x = "foo bar".split(' ') - x.push('baz') - x /*x*/ - ` - const i = test_initial_state(code, code.indexOf('const x')) - assert_value_explorer(i, ['foo', 'bar']) - }), - - test('mutability logs', () => { - const i = test_initial_state(` - const x = [1] - console.log(x) - x.push(2) - console.log(x) - `) - const log1 = i.logs.logs[0] - with_version_number_of_log(i, log1, () => - assert_equal( - [[1]], - log1.args, - ) - ) - const log2 = i.logs.logs[1] - with_version_number_of_log(i, log2, () => - assert_equal( - [[1,2]], - log2.args, - ) - ) - - }), - - // copypasted from the same test for let_versions - test('mutability expand_calltree_node', () => { - const code = ` - const y = [] - - function foo(x) { - y /*y*/ - bar(y) - } - - function bar(arg) { - } - - foo(0) - y[0] = 11 - foo(0) - y[0] = 12 - ` - const i = test_initial_state(code) - const second_foo_call = root_calltree_node(i).children[1] - assert_equal(second_foo_call.has_more_children, true) - const expanded = COMMANDS.calltree.select_and_toggle_expanded(i, second_foo_call.id) - const bar_call = root_calltree_node(expanded).children[1].children[0] - assert_equal(bar_call.fn.name, 'bar') - const moved = COMMANDS.move_cursor(expanded, code.indexOf('y /*y*/')) - assert_value_explorer(moved, [11]) - }), - - // copypasted from the same test for let_versions - test('mutability expand_calltree_node twice', () => { - const code = ` - function test() { - let x = {value: 0} - function test2() { - function foo() { - x /*x*/ - } - x.value = x.value + 1 - foo() - } - test2() - } - test() - test() - ` - const i = test_initial_state(code) - const test_call = root_calltree_node(i).children[1] - assert_equal(test_call.has_more_children , true) - - const expanded = COMMANDS.calltree.select_and_toggle_expanded(i, test_call.id) - const test2_call = root_calltree_node(expanded).children[1].children[0] - assert_equal(test2_call.has_more_children, true) - - const expanded2 = COMMANDS.calltree.select_and_toggle_expanded(expanded, test2_call.id) - const foo_call = root_calltree_node(expanded2).children[1].children[0].children[0] - - const expanded3 = COMMANDS.calltree.select_and_toggle_expanded(expanded2, foo_call.id) - - const moved = COMMANDS.move_cursor(expanded3, code.indexOf('x /*x*/')) - assert_equal(moved.value_explorer.result.value, {value: 1 }) - }), - - test('mutability quicksort', () => { - const code = ` - const loop = new Function('action', 'while(true) { if(action()) { return } }') - - function partition(arr, begin, end) { - const pivot = arr[begin] - - let i = begin - 1, j = end + 1 - - loop(() => { - - i = i + 1 - loop(() => { - if(arr[i] < pivot) { - i = i + 1 - } else { - return true /* stop */ - } - }) - - j = j - 1 - loop(() => { - if(arr[j] > pivot) { - j = j - 1 - } else { - return true // stop iteration - } - }) - - if(i >= j) { - return true // stop iteration - } - - const temp = arr[i] - arr[i] = arr[j] - arr[j] = temp - }) - - return j - } - - - function qsort(arr, begin = 0, end = arr.length - 1) { - if(begin >= 0 && end >= 0 && begin < end) { - const p = partition(arr, begin, end) - qsort(arr, begin, p) - qsort(arr, p + 1, end) - } - } - - const arr = [ 2, 15, 13, 12, 3, 9, 14, 3, 18, 0 ] - - qsort(arr) - - arr /*result*/ - ` - const i = test_initial_state(code, code.indexOf('arr /*result*/')) - const expected = [ 0, 2, 3, 3, 9, 12, 13, 14, 15, 18 ] - assert_value_explorer(i, expected) - - }), - - test('leporello storage API', () => { - const i = test_initial_state(` - const value = leporello.storage.get('value') - if(value == null) { - leporello.storage.set('value', 1) - } - `) - const with_storage = input(i, 'leporello.storage.get("value")', 0).state - assert_value_explorer(with_storage, 1) - const with_cleared_storage = run_code( - COMMANDS.open_app_window(with_storage) - ) - assert_value_explorer(with_cleared_storage, undefined) - }), -] diff --git a/test/utils.js b/test/utils.js deleted file mode 100644 index 5daac32..0000000 --- a/test/utils.js +++ /dev/null @@ -1,323 +0,0 @@ -import {find_error_origin_node} from '../src/ast_utils.js' -import {parse, print_debug_node, load_modules} from '../src/parse_js.js' -import {active_frame, pp_calltree, version_number_symbol} from '../src/calltree.js' -import {COMMANDS} from '../src/cmd.js' - -// external -import {create_app_window} from './run_utils.js' - -// external -import {with_version_number} from '../src/runtime/runtime.js' - -Object.assign(globalThis, - { - // for convenience, to type just `log` instead of `console.log` - log: console.log, - } -) - -export const do_parse = code => parse( - code, - new Set(Object.getOwnPropertyNames(globalThis)) -) - -export const parse_modules = (entry, modules) => - load_modules( - entry, - module_name => modules[module_name], - {}, - new Set(Object.getOwnPropertyNames(globalThis.app_window)) - ) - -export const assert_code_evals_to = (codestring, expected) => { - const s = test_initial_state(codestring) - if(!s.parse_result.ok) { - console.error('parse problems', s.parse_result.problems) - throw new Error('parse failed') - } - const frame = active_frame(s) - const result = frame.children.at(-1).result - assert_equal(result.ok, true) - assert_equal(result.value, expected) - return frame -} - -export const assert_code_error = (codestring, error) => { - const state = test_initial_state(codestring) - const frame = active_frame(state) - assert_equal(frame.result.ok, false) - assert_equal(find_error_origin_node(frame).result.error, error) -} - -export const assert_code_evals_to_async = async (codestring, expected) => { - const s = await test_initial_state_async(codestring) - const frame = active_frame(s) - const result = frame.children.at(-1).result - assert_equal(result.ok, true) - assert_equal(result.value, expected) -} - -export const assert_code_error_async = async (codestring, error) => { - const s = await test_initial_state_async(codestring) - const frame = active_frame(s) - const result = frame.children[frame.children.length - 1].result - assert_equal(result.ok, false) - assert_equal(result.error, error) -} - -function patch_app_window(app_window_patches) { - Object.entries(app_window_patches).forEach(([path, value]) => { - let obj = globalThis.app_window - const path_arr = path.split('.') - path_arr.forEach((el, i) => { - if(i == path_arr.length - 1) { - return - } - obj = obj[el] - }) - const prop = path_arr.at(-1) - obj[prop] = value - }) -} - -export const run_code = (state, app_window_patches) => { - globalThis.app_window = create_app_window() - if(app_window_patches != null) { - patch_app_window(app_window_patches) - } - return COMMANDS.run_code( - state, - state.globals ?? new Set(Object.getOwnPropertyNames(globalThis.app_window)) - ) -} - -export const test_initial_state = (code, cursor_pos, options = {}) => { - if(cursor_pos < 0) { - throw new Error('illegal cursor_pos') - } - if(typeof(options) != 'object') { - throw new Error('illegal state') - } - const { - current_module, - project_dir, - on_deferred_call, - app_window_patches, - } = options - const entrypoint = options.entrypoint ?? '' - - return run_code( - COMMANDS.get_initial_state( - { - files: typeof(code) == 'object' ? code : { '' : code}, - project_dir, - on_deferred_call, - }, - { - entrypoint, - current_module: current_module ?? '', - }, - cursor_pos - ), - app_window_patches, - ) -} - -const wait_for_result = async state => { - assert_equal(state.eval_modules_state != null, true) - const result = await state.eval_modules_state.promise - return COMMANDS.eval_modules_finished( - state, - state, - result, - ) -} - -export const test_initial_state_async = async (code, ...args) => { - const s = test_initial_state(code, ...args) - return wait_for_result(s) -} - -export const input = (s, code, index, options = {}) => { - if(typeof(options) != 'object') { - throw new Error('illegal state') - } - const {state, effects} = COMMANDS.input(s, code, index) - const nextstate = run_code(state, options.app_window_patches) - if(nextstate.rt_cxt?.io_trace_is_replay_aborted) { - const with_clear_trace = run_code( - COMMANDS.clear_io_trace(nextstate), - options.app_window_patches - ) - return { state: with_clear_trace, effects } - } else { - return { state: nextstate, effects } - } -} - -export const input_async = async (s, code, index, options) => { - const after_input = input(s, code, index, options).state - const state = await wait_for_result(after_input) - if(state.rt_cxt?.io_trace_is_replay_aborted) { - return wait_for_result( - run_code(COMMANDS.clear_io_trace(state), options.app_window_patches) - ) - } else { - return state - } -} - -export const test_deferred_calls_state = (code, index) => { - const {get_deferred_call, on_deferred_call} = (new Function(` - let args - return { - get_deferred_call() { - return args - }, - on_deferred_call(..._args) { - args = _args - } - } - `))() - - const state = test_initial_state(code, index, { on_deferred_call }) - - return { - state, - get_deferred_call, - on_deferred_call: state => COMMANDS.on_deferred_call(state, ...get_deferred_call()) - } -} - -export const stringify = val => - JSON.stringify(val, (key, value) => { - if(value?.[Symbol.toStringTag] == 'Set'){ - return [...value] - } else if(value?.[Symbol.toStringTag] == 'Map'){ - return Object.fromEntries([...value.entries()]) - } else if(value instanceof Error) { - return {message: value.message} - } else { - return value - } - }, 2) - -export const assert_equal = (exp, actual) => { - if(typeof(exp) == 'object' && typeof(actual) == 'object'){ - const exp_json = stringify(exp) - const act_json = stringify(actual) - if(exp_json != act_json){ - throw new Error(`FAIL: ${exp_json} != ${act_json}`) - } - } else { - if(exp != actual){ - throw new Error(`FAIL: ${exp} != ${actual}`) - } - } -} - -export const print_debug_ct_node = node => { - const do_print = node => { - const {id, fn, ok, value, error, args, has_more_children} = node - const res = {id, fn: fn?.name, ok, value, error, args, has_more_children} - if(node.children == null) { - return res - } else { - const next_children = node.children.map(do_print) - return {...res, children: next_children} - } - } - return stringify(do_print(node)) -} - -export const assert_versioned_value = (state, versioned, expected) => { - const version_number = versioned[version_number_symbol] ?? versioned.version_number - if(version_number == null) { - throw new Error('illegal state') - } - return with_version_number(state.rt_cxt, version_number, () => - assert_equal(versioned.value, expected) - ) -} - -export const assert_value_explorer = (state, expected) => - assert_versioned_value(state, state.value_explorer.result, expected) - -export const assert_selection = (state, expected) => - assert_versioned_value(state, state.selection_state.node.result, expected) - -export const test = (message, test, only = false) => { - return { - message, - test: Object.defineProperty(test, 'name', {value: message}), - only, - } -} - -export const test_only = (message, t) => test(message, t, true) - -// Create `run` function like this because we do not want its internals to be -// present in calltree -export const run = new Function('create_app_window', ` -return function run(tests) { - // create app window for simple tests, that do not use 'test_initial_state' - globalThis.app_window = create_app_window() - - // Runs test, return failure or null if not failed - const run_test = t => { - return Promise.resolve().then(t.test) - .then(() => null) - .catch(e => { - if(globalThis.process != null) { - // In node.js runner, fail fast - console.error('Failed: ' + t.message) - throw e - } else { - return e - } - }) - } - - // If not run in node, then dont apply filter - const filter = globalThis.process && globalThis.process.argv[2] - - if(filter == null) { - - const only = tests.find(t => t.only) - const tests_to_run = only == null ? tests : [only] - - // Exec each test. After all tests are done, we rethrow first error if - // any. So we will mark root calltree node if one of tests failed - return tests_to_run.reduce( - (failureP, t) => - Promise.resolve(failureP).then(failure => - run_test(t).then(next_failure => failure ?? next_failure) - ) - , - null - ).then(failure => { - - if(failure != null) { - throw failure - } else { - if(globalThis.process != null) { - console.log('Ok') - } - } - - }) - - } else { - const test = tests.find(t => t.message.includes(filter)) - if(test == null) { - throw new Error('test not found') - } else { - return run_test(test).then(() => { - if(globalThis.process != null) { - console.log('Ok') - } - }) - } - } -} -`)(create_app_window)