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)