2022-10-17 02:49:21 +08:00
|
|
|
import {
|
|
|
|
|
zip,
|
|
|
|
|
stringify,
|
|
|
|
|
map_object,
|
|
|
|
|
filter_object,
|
|
|
|
|
} from './utils.js'
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
find_fn_by_location,
|
2023-01-08 08:25:22 +08:00
|
|
|
find_node,
|
2022-09-10 02:48:13 +08:00
|
|
|
collect_destructuring_identifiers,
|
|
|
|
|
map_destructuring_identifiers,
|
|
|
|
|
map_tree,
|
|
|
|
|
} from './ast_utils.js'
|
|
|
|
|
|
2022-12-02 08:17:49 +08:00
|
|
|
import {has_toplevel_await} from './find_definitions.js'
|
|
|
|
|
|
2023-02-05 03:04:39 +08:00
|
|
|
// import runtime as external because it has non-functional code
|
2023-02-04 22:37:35 +08:00
|
|
|
// external
|
2023-08-02 05:59:49 +03:00
|
|
|
import {run, do_eval_expand_calltree_node} from './runtime.js'
|
2023-02-04 22:37:35 +08:00
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
// 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).
|
|
|
|
|
|
2022-11-15 20:53:16 +08:00
|
|
|
For each invocation, we can replay function body with metacircular interpreter,
|
2022-09-10 02:48:13 +08:00
|
|
|
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
|
|
|
|
|
*/
|
|
|
|
|
|
2023-02-05 03:04:39 +08:00
|
|
|
const codegen_function_expr = (node, node_cxt) => {
|
|
|
|
|
const do_codegen = n => codegen(n, node_cxt)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
const args = node.function_args.children.map(do_codegen).join(',')
|
|
|
|
|
|
2023-05-16 00:04:53 +03:00
|
|
|
const decl = node.is_arrow
|
|
|
|
|
? `(${args}) => `
|
2023-06-10 20:17:04 +03:00
|
|
|
: `function(${args})`
|
2023-05-16 00:04:53 +03:00
|
|
|
|
2023-08-02 06:00:08 +03:00
|
|
|
// TODO gensym __obj, __fn, __call_id
|
|
|
|
|
const prolog = '{const __call_id = __cxt.call_counter;'
|
|
|
|
|
|
2023-05-16 00:04:53 +03:00
|
|
|
const call = (node.is_async ? 'async ' : '') + decl + (
|
2022-09-10 02:48:13 +08:00
|
|
|
(node.body.type == 'do')
|
2023-08-02 06:00:08 +03:00
|
|
|
? prolog + do_codegen(node.body) + '}'
|
|
|
|
|
: prolog + 'return ' + do_codegen(node.body) + '}'
|
2022-09-10 02:48:13 +08:00
|
|
|
)
|
|
|
|
|
|
2022-12-27 19:00:37 +08:00
|
|
|
const argscount = node
|
|
|
|
|
.function_args
|
|
|
|
|
.children
|
|
|
|
|
.find(a => a.type == 'destructuring_rest') == null
|
|
|
|
|
? node.function_args.children.length
|
|
|
|
|
: null
|
2022-09-10 02:48:13 +08:00
|
|
|
|
2023-02-05 03:04:39 +08:00
|
|
|
const location = `{index: ${node.index}, length: ${node.length}, module: '${node_cxt.module}'}`
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
// TODO first create all functions, then assign __closure, after everything
|
|
|
|
|
// is declared. See 'out of order decl' test. Currently we assign __closure
|
2023-01-15 22:13:10 +08:00
|
|
|
// on first call (see `__trace`)
|
2022-09-10 02:48:13 +08:00
|
|
|
const get_closure = `() => ({${[...node.closed].join(',')}})`
|
|
|
|
|
|
2023-02-04 22:37:35 +08:00
|
|
|
return `__trace(__cxt, ${call}, "${node.name}", ${argscount}, ${location}, \
|
|
|
|
|
${get_closure})`
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
/*
|
|
|
|
|
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'),
|
|
|
|
|
'(...)'
|
|
|
|
|
)
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
// TODO if statically can prove that function is hosted, then do not codegen
|
2023-01-15 22:13:10 +08:00
|
|
|
// __trace
|
2023-02-05 03:04:39 +08:00
|
|
|
const codegen_function_call = (node, node_cxt) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
|
2023-02-05 03:04:39 +08:00
|
|
|
const do_codegen = n => codegen(n, node_cxt)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
const args = `[${node.args.children.map(do_codegen).join(',')}]`
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
const errormessage = not_a_function_error(node.fn)
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
let call
|
|
|
|
|
if(node.fn.type == 'member_access') {
|
|
|
|
|
const op = node.fn.is_optional_chaining ? '?.' : ''
|
|
|
|
|
|
|
|
|
|
// TODO gensym __obj, __fn
|
2022-12-31 20:40:59 +08:00
|
|
|
// 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)}],
|
2023-02-04 22:37:35 +08:00
|
|
|
__trace_call(__cxt, __fn, __obj, ${args}, ${JSON.stringify(errormessage)})
|
2022-12-31 20:40:59 +08:00
|
|
|
)`
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
2023-02-04 22:37:35 +08:00
|
|
|
return `__trace_call(__cxt, ${do_codegen(node.fn)}, null, ${args}, \
|
2023-01-18 19:21:25 +08:00
|
|
|
${JSON.stringify(errormessage)})`
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-02 06:00:08 +03:00
|
|
|
// 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
|
|
|
|
|
|
2023-02-05 03:04:39 +08:00
|
|
|
const codegen = (node, node_cxt, parent) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
|
2023-02-05 03:04:39 +08:00
|
|
|
const do_codegen = (n, parent) => codegen(n, node_cxt, parent)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
if([
|
|
|
|
|
'identifier',
|
|
|
|
|
'number',
|
|
|
|
|
'string_literal',
|
|
|
|
|
'builtin_identifier',
|
|
|
|
|
'backtick_string',
|
|
|
|
|
].includes(node.type)){
|
|
|
|
|
return node.value
|
|
|
|
|
} else if(node.type == 'do'){
|
2023-05-16 00:04:53 +03:00
|
|
|
return [
|
|
|
|
|
// hoist function decls to the top
|
2023-10-26 10:29:57 +08:00
|
|
|
...node.children.filter(s => s.type == 'function_decl'),
|
|
|
|
|
...node.children.filter(s => s.type != 'function_decl'),
|
2023-05-16 00:04:53 +03:00
|
|
|
].reduce(
|
|
|
|
|
(result, stmt) => result + (do_codegen(stmt)) + ';\n',
|
|
|
|
|
''
|
|
|
|
|
)
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if(node.type == 'return') {
|
|
|
|
|
return 'return ' + do_codegen(node.expr) + ';'
|
|
|
|
|
} else if(node.type == 'throw') {
|
|
|
|
|
return 'throw ' + do_codegen(node.expr) + ';'
|
|
|
|
|
} else if(node.type == 'if') {
|
2023-08-02 06:00:08 +03:00
|
|
|
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
|
2022-09-10 02:48:13 +08:00
|
|
|
? left
|
2023-08-02 06:00:08 +03:00
|
|
|
: 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);`
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if(node.type == 'array_literal'){
|
|
|
|
|
return '[' + node.elements.map(c => do_codegen(c)).join(', ') + ']'
|
|
|
|
|
} else if(node.type == 'object_literal'){
|
|
|
|
|
const elements =
|
|
|
|
|
node.elements.map(el => {
|
2023-07-06 18:34:03 +03:00
|
|
|
if(el.type == 'object_spread'){
|
2022-09-10 02:48:13 +08:00
|
|
|
return do_codegen(el)
|
|
|
|
|
} else if(el.type == 'identifier') {
|
|
|
|
|
return el.value
|
|
|
|
|
} else if(el.type == 'key_value_pair') {
|
|
|
|
|
return '[' + do_codegen(el.key.type == 'computed_property' ? el.key.expr : el.key) + ']'
|
2022-11-08 20:06:26 +08:00
|
|
|
+ ': (' + do_codegen(el.value, el) + ')'
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
|
|
|
|
throw new Error('unknown node type ' + el.type)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.join(',')
|
|
|
|
|
return '({' + elements + '})'
|
|
|
|
|
} else if(node.type == 'function_call'){
|
2023-02-05 03:04:39 +08:00
|
|
|
return codegen_function_call(node, node_cxt)
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if(node.type == 'function_expr'){
|
2023-02-05 03:04:39 +08:00
|
|
|
return codegen_function_expr(node, node_cxt)
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if(node.type == 'ternary'){
|
2023-08-02 06:00:08 +03:00
|
|
|
const branches = node.branches.map(branch =>
|
|
|
|
|
`(__save_ct_node_for_path(__cxt, __calltree_node_by_loc, ${branch.index}, __call_id), `
|
|
|
|
|
+ do_codegen(branch)
|
|
|
|
|
+ ')'
|
|
|
|
|
)
|
2022-09-10 02:48:13 +08:00
|
|
|
return ''
|
|
|
|
|
+ '('
|
|
|
|
|
+ do_codegen(node.cond)
|
|
|
|
|
+ ')\n? '
|
2023-08-02 06:00:08 +03:00
|
|
|
+ branches[0]
|
2022-09-10 02:48:13 +08:00
|
|
|
+'\n: '
|
2023-08-02 06:00:08 +03:00
|
|
|
+ branches[1]
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if(node.type == 'const'){
|
|
|
|
|
const res = 'const ' + do_codegen(node.name_node) + ' = ' + do_codegen(node.expr, node) + ';'
|
|
|
|
|
if(node.name_node.type == 'identifier' && node.expr.type == 'function_call') {
|
2022-12-02 06:05:20 +08:00
|
|
|
// 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, ...)
|
2022-09-10 02:48:13 +08:00
|
|
|
return res + `
|
2022-12-02 06:05:20 +08:00
|
|
|
if(
|
|
|
|
|
typeof(${node.name_node.value}) == 'function'
|
|
|
|
|
&&
|
|
|
|
|
${node.name_node.value}.name == 'anonymous'
|
|
|
|
|
) {
|
|
|
|
|
Object.defineProperty(
|
|
|
|
|
${node.name_node.value},
|
|
|
|
|
"name",
|
|
|
|
|
{value: "${node.name_node.value}"}
|
|
|
|
|
);
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
`
|
|
|
|
|
} else {
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
} else if(node.type == 'let') {
|
|
|
|
|
return 'let ' + node.names.join(',') + ';';
|
|
|
|
|
} else if(node.type == 'assignment') {
|
|
|
|
|
return node.name + ' = ' + do_codegen(node.expr, node) + ';';
|
|
|
|
|
} else if(node.type == 'member_access'){
|
|
|
|
|
return '('
|
|
|
|
|
+ do_codegen(node.object)
|
|
|
|
|
+ (node.is_optional_chaining ? ')?.[' : ')[')
|
|
|
|
|
+ do_codegen(node.property)
|
|
|
|
|
+ ']'
|
|
|
|
|
} else if(node.type == 'unary') {
|
2022-12-08 09:42:42 +08:00
|
|
|
if(node.operator == 'await') {
|
2023-02-04 22:37:35 +08:00
|
|
|
return `(await __do_await(__cxt, ${do_codegen(node.expr)}))`
|
2022-12-08 09:42:42 +08:00
|
|
|
} else {
|
|
|
|
|
return '(' + node.operator + ' ' + do_codegen(node.expr) + ')'
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if(node.type == 'binary'){
|
|
|
|
|
return ''
|
|
|
|
|
+ do_codegen(node.args[0])
|
|
|
|
|
+ ' '
|
|
|
|
|
+ node.operator
|
|
|
|
|
+ ' '
|
|
|
|
|
+ do_codegen(node.args[1])
|
2023-07-06 18:34:03 +03:00
|
|
|
} else if(node.type == 'array_spread' || node.type == 'object_spread'){
|
2022-09-10 02:48:13 +08:00
|
|
|
return '...(' + do_codegen(node.expr) + ')'
|
|
|
|
|
} else if(node.type == 'new') {
|
2022-12-16 18:23:55 +08:00
|
|
|
const args = `[${node.args.children.map(do_codegen).join(',')}]`
|
2023-01-18 19:21:25 +08:00
|
|
|
const errormessage = not_a_function_error(node.constructor)
|
2023-02-04 22:37:35 +08:00
|
|
|
return `__trace_call(__cxt, ${do_codegen(node.constructor)}, null, ${args},\
|
2023-01-18 19:21:25 +08:00
|
|
|
${JSON.stringify(errormessage)}, true)`
|
2022-09-10 02:48:13 +08:00
|
|
|
} 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') {
|
2023-06-05 12:08:20 +03:00
|
|
|
let names, def
|
|
|
|
|
if(node.default_import != null) {
|
|
|
|
|
def = `default: ${node.default_import},`
|
|
|
|
|
names = node.children.slice(1).map(n => n.value)
|
2022-12-01 02:07:55 +08:00
|
|
|
} else {
|
2023-06-05 12:08:20 +03:00
|
|
|
def = ''
|
|
|
|
|
names = node.children.map(n => n.value)
|
2022-12-01 02:07:55 +08:00
|
|
|
}
|
2023-06-05 12:08:20 +03:00
|
|
|
return `const {${def} ${names.join(',')}} = __cxt.modules['${node.full_import_path}'];`;
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if(node.type == 'export') {
|
2023-06-05 12:08:20 +03:00
|
|
|
if(node.is_default) {
|
|
|
|
|
return `__cxt.modules['${node_cxt.module}'].default = ${do_codegen(node.children[0])};`
|
|
|
|
|
} else {
|
|
|
|
|
const identifiers = collect_destructuring_identifiers(node.binding.name_node)
|
|
|
|
|
.map(i => i.value)
|
|
|
|
|
return do_codegen(node.binding)
|
|
|
|
|
+
|
|
|
|
|
`Object.assign(__cxt.modules['${node_cxt.module}'], {${identifiers.join(',')}});`
|
|
|
|
|
}
|
2023-05-16 00:04:53 +03:00
|
|
|
} else if(node.type == 'function_decl') {
|
|
|
|
|
const expr = node.children[0]
|
2023-02-05 03:04:39 +08:00
|
|
|
return `const ${expr.name} = ${codegen_function_expr(expr, node_cxt)};`
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
|
|
|
|
console.error(node)
|
|
|
|
|
throw new Error('unknown node type: ' + node.type)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-05 02:08:53 +08:00
|
|
|
export const eval_modules = (
|
2022-11-15 16:58:15 +08:00
|
|
|
parse_result,
|
2022-10-25 02:29:59 +08:00
|
|
|
external_imports,
|
2022-12-02 04:31:16 +08:00
|
|
|
on_deferred_call,
|
2022-11-29 04:22:56 +08:00
|
|
|
calltree_changed_token,
|
2023-06-27 15:03:03 +03:00
|
|
|
io_trace,
|
2022-10-25 02:29:59 +08:00
|
|
|
location
|
2023-02-05 02:08:53 +08:00
|
|
|
) => {
|
2023-08-02 06:00:08 +03:00
|
|
|
// TODO gensym __cxt, __trace, __trace_call, __calltree_node_by_loc, __do_await
|
2022-09-10 02:48:13 +08:00
|
|
|
|
2022-10-19 03:22:48 +08:00
|
|
|
// TODO bug if module imported twice, once as external and as regular
|
|
|
|
|
|
2022-12-02 08:17:49 +08:00
|
|
|
const is_async = has_toplevel_await(parse_result.modules)
|
|
|
|
|
|
2023-02-06 01:53:34 +08:00
|
|
|
const Function = is_async
|
2023-07-11 18:24:28 +03:00
|
|
|
? globalThis.app_window.eval('(async function(){})').constructor
|
|
|
|
|
: globalThis.app_window.Function
|
2023-02-06 01:53:34 +08:00
|
|
|
|
|
|
|
|
const module_fns = parse_result.sorted.map(module => (
|
|
|
|
|
{
|
|
|
|
|
module,
|
2023-08-02 06:00:08 +03:00
|
|
|
// TODO refactor, instead of multiple args prefixed with '__', pass
|
|
|
|
|
// single arg called `runtime`
|
2023-02-06 01:53:34 +08:00
|
|
|
fn: new Function(
|
|
|
|
|
'__cxt',
|
2023-08-02 06:00:08 +03:00
|
|
|
'__calltree_node_by_loc',
|
2023-02-06 01:53:34 +08:00
|
|
|
'__trace',
|
|
|
|
|
'__trace_call',
|
|
|
|
|
'__do_await',
|
2023-08-02 06:00:08 +03:00
|
|
|
'__save_ct_node_for_path',
|
|
|
|
|
|
|
|
|
|
/* 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 = "SOMETHING_WRONG_HAPPENED";' +
|
2023-02-06 01:53:34 +08:00
|
|
|
codegen(parse_result.modules[module], {module})
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
))
|
|
|
|
|
|
2023-02-04 22:37:35 +08:00
|
|
|
const cxt = {
|
2023-02-05 03:04:39 +08:00
|
|
|
modules: external_imports == null
|
|
|
|
|
? {}
|
|
|
|
|
: map_object(external_imports, (name, {module}) => module),
|
|
|
|
|
|
2023-02-04 22:37:35 +08:00
|
|
|
call_counter: 0,
|
|
|
|
|
children: null,
|
|
|
|
|
prev_children: null,
|
2023-02-05 03:04:39 +08:00
|
|
|
// TODO use native array for stack for perf? stack contains booleans
|
|
|
|
|
stack: new Array(),
|
|
|
|
|
|
|
|
|
|
is_recording_deferred_calls: false,
|
2023-02-04 22:37:35 +08:00
|
|
|
on_deferred_call: (call, calltree_changed_token, logs) => {
|
|
|
|
|
return on_deferred_call(
|
|
|
|
|
assign_code(parse_result.modules, call),
|
|
|
|
|
calltree_changed_token,
|
|
|
|
|
logs,
|
|
|
|
|
)
|
|
|
|
|
},
|
2023-02-05 02:08:53 +08:00
|
|
|
calltree_changed_token,
|
2023-02-05 03:04:39 +08:00
|
|
|
is_toplevel_call: true,
|
|
|
|
|
|
2023-07-11 18:24:28 +03:00
|
|
|
window: globalThis.app_window,
|
2023-02-04 22:37:35 +08:00
|
|
|
}
|
2022-10-17 02:49:21 +08:00
|
|
|
|
2023-06-27 15:03:03 +03:00
|
|
|
const result = run(module_fns, cxt, io_trace)
|
2023-02-04 22:37:35 +08:00
|
|
|
|
2023-06-10 18:34:58 +03:00
|
|
|
const make_result = result => {
|
|
|
|
|
const calltree = assign_code(parse_result.modules, result.calltree)
|
|
|
|
|
return {
|
|
|
|
|
modules: result.modules,
|
|
|
|
|
logs: result.logs,
|
|
|
|
|
eval_cxt: result.eval_cxt,
|
|
|
|
|
calltree,
|
2023-08-02 06:00:08 +03:00
|
|
|
calltree_node_by_loc: result.calltree_node_by_loc,
|
2023-06-27 15:03:03 +03:00
|
|
|
io_trace: result.eval_cxt.io_trace,
|
2023-06-10 18:34:58 +03:00
|
|
|
}
|
|
|
|
|
}
|
2023-02-04 22:37:35 +08:00
|
|
|
|
2023-02-06 01:53:34 +08:00
|
|
|
if(is_async) {
|
2023-06-05 15:53:08 +03:00
|
|
|
return result.__original_then(make_result)
|
2023-02-04 22:37:35 +08:00
|
|
|
} else {
|
2023-02-05 02:08:53 +08:00
|
|
|
return make_result(result)
|
2023-02-04 22:37:35 +08:00
|
|
|
}
|
|
|
|
|
}
|
2022-12-25 16:28:06 +08:00
|
|
|
|
2023-01-08 08:25:22 +08:00
|
|
|
|
2023-02-05 02:08:53 +08:00
|
|
|
export const eval_expand_calltree_node = (cxt, parse_result, node) => {
|
|
|
|
|
return assign_code(
|
|
|
|
|
parse_result.modules,
|
|
|
|
|
do_eval_expand_calltree_node(cxt, node)
|
|
|
|
|
)
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
2022-10-17 02:49:21 +08:00
|
|
|
// TODO: assign_code: benchmark and use imperative version for perf?
|
2022-11-15 16:58:15 +08:00
|
|
|
const assign_code = (modules, call) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
if(call.toplevel) {
|
|
|
|
|
return {...call,
|
2022-11-15 16:58:15 +08:00
|
|
|
code: modules[call.module],
|
2022-09-10 02:48:13 +08:00
|
|
|
children: call.children && call.children.map(call => assign_code(modules, call)),
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return {...call,
|
|
|
|
|
code: call.fn == null || call.fn.__location == null
|
|
|
|
|
? null
|
2022-10-17 02:49:21 +08:00
|
|
|
// TODO cache find_fn_by_location calls?
|
2022-09-10 02:48:13 +08:00
|
|
|
: find_fn_by_location(modules[call.fn.__location.module], call.fn.__location),
|
|
|
|
|
children: call.children && call.children.map(call => assign_code(modules, call)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-05 02:08:53 +08:00
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
/* ------------- 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
|
2023-06-22 15:49:12 +03:00
|
|
|
export const eval_codestring = (codestring, scope) =>
|
2023-07-11 18:24:28 +03:00
|
|
|
// Note that we eval code in context of app_window
|
|
|
|
|
(new (globalThis.app_window.Function)('codestring', 'scope',
|
2023-06-22 15:49:12 +03:00
|
|
|
// Make a copy of `scope` to not mutate it with assignments
|
|
|
|
|
`
|
|
|
|
|
try {
|
|
|
|
|
return {ok: true, value: eval('with({...scope}){' + codestring + '}')}
|
|
|
|
|
} catch(error) {
|
2023-10-26 12:25:35 +08:00
|
|
|
return {ok: false, error, is_error_origin: true}
|
2023-06-22 15:49:12 +03:00
|
|
|
}
|
|
|
|
|
`
|
|
|
|
|
))(codestring, scope)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
2023-06-21 13:50:01 +03:00
|
|
|
const get_args_scope = (fn_node, args, closure) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
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)).join(',')
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
// TODO gensym __args. Or
|
|
|
|
|
new Function(`
|
|
|
|
|
return ({foo, bar}) => ({foo, bar})
|
|
|
|
|
`)(args)
|
|
|
|
|
|
|
|
|
|
to avoid gensym
|
|
|
|
|
*/
|
|
|
|
|
const codestring = `(([${destructuring}]) => [${arg_names.join(',')}])(__args)`
|
|
|
|
|
|
2023-06-21 13:50:01 +03:00
|
|
|
const {ok, value, error} = eval_codestring(codestring, {
|
|
|
|
|
...closure,
|
|
|
|
|
__args: args,
|
|
|
|
|
})
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
if(!ok) {
|
|
|
|
|
// TODO show exact destructuring error
|
2023-10-26 12:25:35 +08:00
|
|
|
return {ok, error, is_error_origin: true}
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
|
|
|
|
return {
|
|
|
|
|
ok,
|
|
|
|
|
value: Object.fromEntries(
|
|
|
|
|
zip(
|
|
|
|
|
arg_names,
|
|
|
|
|
value,
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
const eval_binary_expr = (node, scope, callsleft, context) => {
|
|
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
if(!ok) {
|
|
|
|
|
return {ok, children, calls}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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, calls, value}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
const do_eval_frame_expr = (node, scope, callsleft, context) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
if([
|
|
|
|
|
'identifier',
|
|
|
|
|
'builtin_identifier',
|
|
|
|
|
'number',
|
|
|
|
|
'string_literal',
|
|
|
|
|
'backtick_string',
|
|
|
|
|
].includes(node.type)){
|
|
|
|
|
// TODO exprs inside backtick string
|
|
|
|
|
// Pass scope for backtick string
|
|
|
|
|
return {...eval_codestring(node.value, scope), calls: callsleft}
|
2023-07-06 18:34:03 +03:00
|
|
|
} else if(node.type == 'array_spread') {
|
|
|
|
|
const result = eval_children(node, scope, callsleft, context)
|
|
|
|
|
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,
|
|
|
|
|
calls: result.calls,
|
|
|
|
|
error: new TypeError(child.string + ' is not iterable'),
|
2023-10-26 12:25:35 +08:00
|
|
|
is_error_origin: true,
|
2023-07-06 18:34:03 +03:00
|
|
|
}
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if([
|
2023-07-06 18:34:03 +03:00
|
|
|
'object_spread',
|
2022-09-10 02:48:13 +08:00
|
|
|
'key_value_pair',
|
|
|
|
|
'computed_property'
|
|
|
|
|
].includes(node.type)) {
|
2023-01-18 19:21:25 +08:00
|
|
|
return eval_children(node, scope, callsleft, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if(node.type == 'array_literal' || node.type == 'call_args'){
|
2023-01-18 19:21:25 +08:00
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
if(!ok) {
|
|
|
|
|
return {ok, children, calls}
|
|
|
|
|
}
|
|
|
|
|
const value = children.reduce(
|
|
|
|
|
(arr, el) => {
|
2023-07-06 18:34:03 +03:00
|
|
|
if(el.type == 'array_spread') {
|
2022-09-10 02:48:13 +08:00
|
|
|
return [...arr, ...el.children[0].result.value]
|
|
|
|
|
} else {
|
|
|
|
|
return [...arr, el.result.value]
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
return {ok, children, calls, value}
|
|
|
|
|
} else if(node.type == 'object_literal'){
|
2023-01-18 19:21:25 +08:00
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
if(!ok) {
|
|
|
|
|
return {ok, children, calls}
|
|
|
|
|
}
|
|
|
|
|
const value = children.reduce(
|
|
|
|
|
(value, el) => {
|
2023-07-06 18:34:03 +03:00
|
|
|
if(el.type == 'object_spread'){
|
2022-09-10 02:48:13 +08:00
|
|
|
return {...value, ...el.children[0].result.value}
|
|
|
|
|
} else if(el.type == 'identifier') {
|
|
|
|
|
// TODO check that it works
|
|
|
|
|
return {...value, ...{[el.value]: el.result.value}}
|
|
|
|
|
} else if(el.type == 'key_value_pair') {
|
|
|
|
|
const [key, val] = el.children
|
|
|
|
|
let k
|
|
|
|
|
if(key.type == 'computed_property') {
|
|
|
|
|
k = key.children[0].result.value
|
|
|
|
|
} else {
|
|
|
|
|
k = key.result.value
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
...value,
|
|
|
|
|
...{[k]: val.result.value},
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error('unknown node type ' + el.type)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{}
|
|
|
|
|
)
|
|
|
|
|
return {ok, children, value, calls}
|
2022-12-16 18:23:55 +08:00
|
|
|
} else if(node.type == 'function_call' || node.type == 'new'){
|
2023-01-18 19:21:25 +08:00
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
if(!ok) {
|
|
|
|
|
return {ok: false, children, calls}
|
|
|
|
|
} else {
|
|
|
|
|
if(typeof(children[0].result.value) != 'function') {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
2023-01-18 19:21:25 +08:00
|
|
|
error: context.calltree_node.error,
|
2023-10-26 12:25:35 +08:00
|
|
|
is_error_origin: true,
|
2022-09-10 02:48:13 +08:00
|
|
|
children,
|
2022-12-01 02:08:02 +08:00
|
|
|
calls,
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const c = calls[0]
|
|
|
|
|
if(c == null) {
|
|
|
|
|
throw new Error('illegal state')
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
ok: c.ok,
|
|
|
|
|
call: c,
|
|
|
|
|
value: c.value,
|
|
|
|
|
error: c.error,
|
2023-10-26 12:25:35 +08:00
|
|
|
is_error_origin: !c.ok,
|
2022-09-10 02:48:13 +08:00
|
|
|
children,
|
|
|
|
|
calls: calls.slice(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} 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',
|
2022-12-02 06:05:20 +08:00
|
|
|
{value: node.name}
|
2022-09-10 02:48:13 +08:00
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
value: fn_placeholder,
|
|
|
|
|
calls: callsleft,
|
|
|
|
|
children: node.children,
|
|
|
|
|
}
|
2023-05-16 00:04:53 +03:00
|
|
|
} else if(node.type == 'ternary') {
|
2022-09-10 02:48:13 +08:00
|
|
|
const {node: cond_evaled, calls: calls_after_cond} = eval_frame_expr(
|
|
|
|
|
node.cond,
|
|
|
|
|
scope,
|
2023-01-18 19:21:25 +08:00
|
|
|
callsleft,
|
|
|
|
|
context
|
2022-09-10 02:48:13 +08:00
|
|
|
)
|
|
|
|
|
const {ok, value} = cond_evaled.result
|
|
|
|
|
const branches = node.branches
|
|
|
|
|
if(!ok) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
children: [cond_evaled, branches[0], branches[1]],
|
|
|
|
|
calls: calls_after_cond,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const {node: branch_evaled, calls: calls_after_branch} = eval_frame_expr(
|
|
|
|
|
branches[value ? 0 : 1],
|
|
|
|
|
scope,
|
2023-01-18 19:21:25 +08:00
|
|
|
calls_after_cond,
|
|
|
|
|
context
|
2022-09-10 02:48:13 +08:00
|
|
|
)
|
|
|
|
|
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, calls: calls_after_branch, value: branch_evaled.result.value}
|
|
|
|
|
} else {
|
|
|
|
|
return {ok, children, calls: calls_after_branch}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if(node.type == 'member_access'){
|
2023-01-18 19:21:25 +08:00
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
if(!ok) {
|
|
|
|
|
return {ok: false, children, calls}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [obj, prop] = children
|
|
|
|
|
|
|
|
|
|
const codestring = node.is_optional_chaining ? 'obj?.[prop]' : 'obj[prop]'
|
|
|
|
|
|
|
|
|
|
// TODO do not use eval here
|
|
|
|
|
return {
|
|
|
|
|
...eval_codestring(codestring, {
|
|
|
|
|
obj: obj.result.value,
|
|
|
|
|
prop: prop.result.value,
|
|
|
|
|
}),
|
|
|
|
|
children,
|
|
|
|
|
calls,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if(node.type == 'unary') {
|
2023-01-18 19:21:25 +08:00
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
if(!ok) {
|
|
|
|
|
return {ok: false, children, calls}
|
|
|
|
|
} else {
|
|
|
|
|
const expr = children[0]
|
2023-10-26 12:25:35 +08:00
|
|
|
let ok, value, error, is_error_origin
|
2022-09-10 02:48:13 +08:00
|
|
|
if(node.operator == '!') {
|
2022-12-07 05:06:15 +08:00
|
|
|
ok = true
|
2022-09-10 02:48:13 +08:00
|
|
|
value = !expr.result.value
|
|
|
|
|
} else if(node.operator == 'typeof') {
|
2022-12-07 05:06:15 +08:00
|
|
|
ok = true
|
2022-09-10 02:48:13 +08:00
|
|
|
value = typeof(expr.result.value)
|
2022-12-07 05:52:13 +08:00
|
|
|
} else if(node.operator == '-') {
|
2022-12-07 05:42:33 +08:00
|
|
|
ok = true
|
2022-12-07 05:52:13 +08:00
|
|
|
value = - expr.result.value
|
2022-12-02 04:13:32 +08:00
|
|
|
} else if(node.operator == 'await') {
|
2023-07-11 18:24:28 +03:00
|
|
|
if(expr.result.value instanceof globalThis.app_window.Promise) {
|
2022-12-07 05:06:15 +08:00
|
|
|
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
|
2023-10-26 12:25:35 +08:00
|
|
|
is_error_origin = !ok
|
2022-12-07 05:06:15 +08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ok = true
|
|
|
|
|
value = expr.result.value
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
|
|
|
|
throw new Error('unknown op')
|
|
|
|
|
}
|
2023-10-26 12:25:35 +08:00
|
|
|
return {ok, children, calls, value, error, is_error_origin}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
} else if(node.type == 'binary' && !['&&', '||', '??'].includes(node.operator)){
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
return eval_binary_expr(node, scope, callsleft, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
} else if(node.type == 'binary' && ['&&', '||', '??'].includes(node.operator)){
|
|
|
|
|
const {node: left_evaled, calls} = eval_frame_expr(
|
|
|
|
|
node.children[0],
|
|
|
|
|
scope,
|
2023-01-18 19:21:25 +08:00
|
|
|
callsleft,
|
|
|
|
|
context
|
2022-09-10 02:48:13 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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]],
|
|
|
|
|
calls,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2023-01-18 19:21:25 +08:00
|
|
|
return eval_binary_expr(node, scope, callsleft, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if(node.type == 'grouping'){
|
2023-01-18 19:21:25 +08:00
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
if(!ok) {
|
|
|
|
|
return {ok, children, calls}
|
|
|
|
|
} else {
|
|
|
|
|
return {ok: true, children, calls, value: children[0].result.value}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
console.error(node)
|
|
|
|
|
throw new Error('unknown node type: ' + node.type)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
const eval_children = (node, scope, calls, context) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
return node.children.reduce(
|
|
|
|
|
({ok, children, calls}, child) => {
|
|
|
|
|
let next_child, next_ok, next_calls
|
|
|
|
|
if(!ok) {
|
2023-05-16 00:04:53 +03:00
|
|
|
next_child = child
|
|
|
|
|
next_ok = false
|
|
|
|
|
next_calls = calls
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
2023-01-18 19:21:25 +08:00
|
|
|
const result = eval_frame_expr(child, scope, calls, context)
|
2023-05-16 00:04:53 +03:00
|
|
|
next_child = result.node
|
|
|
|
|
next_calls = result.calls
|
|
|
|
|
next_ok = next_child.result.ok
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
return {ok: next_ok, children: [...children, next_child], calls: next_calls}
|
|
|
|
|
},
|
|
|
|
|
{ok: true, children: [], calls}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
const eval_frame_expr = (node, scope, callsleft, context) => {
|
2023-10-26 12:25:35 +08:00
|
|
|
const {ok, error, is_error_origin, value, call, children, calls}
|
2023-01-18 19:21:25 +08:00
|
|
|
= do_eval_frame_expr(node, scope, callsleft, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
if(callsleft != null && calls == null) {
|
|
|
|
|
// TODO remove it, just for debug
|
|
|
|
|
console.error('node', node)
|
|
|
|
|
throw new Error('illegal state')
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
node: {
|
|
|
|
|
...node,
|
|
|
|
|
children,
|
|
|
|
|
// Add `call` for step_into
|
2023-10-26 12:25:35 +08:00
|
|
|
result: {ok, error, value, call, is_error_origin}
|
2022-09-10 02:48:13 +08:00
|
|
|
},
|
|
|
|
|
calls,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const apply_assignments = (do_node, assignments) => {
|
|
|
|
|
const let_ids = do_node
|
|
|
|
|
.children
|
|
|
|
|
.filter(c => c.type == 'let')
|
|
|
|
|
.map(l => l.children)
|
|
|
|
|
.flat()
|
|
|
|
|
.map(c => c.index)
|
|
|
|
|
|
|
|
|
|
// Scope we return to parent block
|
|
|
|
|
const scope = Object.fromEntries(
|
|
|
|
|
Object
|
|
|
|
|
.entries(assignments)
|
|
|
|
|
.filter(([index, v]) =>
|
|
|
|
|
let_ids.find(i => i.toString() == index) == null
|
|
|
|
|
)
|
|
|
|
|
.map(([k, {name, value}]) => [name, value])
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const node = {...do_node,
|
|
|
|
|
children: do_node.children.map(_let => {
|
|
|
|
|
if(_let.type != 'let') {
|
|
|
|
|
return _let
|
|
|
|
|
}
|
|
|
|
|
const children = _let.children.map(id => {
|
|
|
|
|
const a = assignments[id.index]
|
|
|
|
|
if(a == null) {
|
|
|
|
|
return id
|
|
|
|
|
} else {
|
|
|
|
|
return {...id, result: {ok: true, value: a.value}}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
return {..._let,
|
|
|
|
|
result: children.every(c => c.result != null) ? {ok: true} : null,
|
|
|
|
|
children
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {node, scope}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
const eval_statement = (s, scope, calls, context) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
if(s.type == 'do') {
|
|
|
|
|
const node = s
|
2023-05-16 00:04:53 +03:00
|
|
|
// hoist function decls to the top
|
2023-10-26 10:29:57 +08:00
|
|
|
const function_decls = node.children
|
2023-05-16 00:04:53 +03:00
|
|
|
.filter(s => s.type == 'function_decl')
|
|
|
|
|
.map(s => {
|
|
|
|
|
const {ok, children, calls: next_calls} = eval_children(s, scope, calls, context)
|
|
|
|
|
if(!ok) {
|
|
|
|
|
// Function decl can never fail
|
|
|
|
|
throw new Error('illegal state')
|
|
|
|
|
}
|
|
|
|
|
if(next_calls != calls) {
|
|
|
|
|
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 = {...scope, ...hoisted_functions_scope}
|
|
|
|
|
|
2023-10-26 10:29:57 +08:00
|
|
|
const {ok, assignments, returned, children, calls: next_calls} = node.children.reduce(
|
|
|
|
|
({ok, returned, children, scope, calls, assignments}, s) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
if(returned || !ok) {
|
2023-10-26 10:29:57 +08:00
|
|
|
return {ok, returned, scope, calls, children: [...children, s], assignments}
|
2023-05-16 00:04:53 +03:00
|
|
|
} else if(s.type == 'function_decl') {
|
|
|
|
|
const node = function_decls.find(decl => decl.index == s.index)
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
returned: false,
|
|
|
|
|
node,
|
|
|
|
|
assignments,
|
|
|
|
|
scope,
|
|
|
|
|
calls,
|
2023-10-26 10:29:57 +08:00
|
|
|
children: [...children, node],
|
2023-05-16 00:04:53 +03:00
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
|
|
|
|
const {
|
|
|
|
|
ok,
|
|
|
|
|
returned,
|
|
|
|
|
node,
|
|
|
|
|
assignments: next_assignments,
|
|
|
|
|
scope: nextscope,
|
|
|
|
|
calls: next_calls,
|
2023-01-18 19:21:25 +08:00
|
|
|
} = eval_statement(s, scope, calls, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
return {
|
|
|
|
|
ok,
|
|
|
|
|
returned,
|
|
|
|
|
assignments: {...assignments, ...next_assignments},
|
|
|
|
|
scope: nextscope,
|
|
|
|
|
calls: next_calls,
|
2023-10-26 10:29:57 +08:00
|
|
|
children: [...children, node],
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
2023-10-26 10:29:57 +08:00
|
|
|
{ok: true, returned: false, children: [], scope: initial_scope, calls, assignments: {}}
|
2022-09-10 02:48:13 +08:00
|
|
|
)
|
|
|
|
|
const {node: next_node, scope: next_scope} =
|
2023-10-26 10:29:57 +08:00
|
|
|
apply_assignments({...node, children: children, result: {ok}}, assignments)
|
2022-09-10 02:48:13 +08:00
|
|
|
return {
|
|
|
|
|
ok,
|
|
|
|
|
node: next_node,
|
|
|
|
|
scope: {...scope, ...next_scope},
|
|
|
|
|
returned,
|
|
|
|
|
assignments,
|
2023-05-16 00:04:53 +03:00
|
|
|
calls: next_calls,
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
} else if(s.type == 'const' || s.type == 'assignment') {
|
|
|
|
|
// TODO default values for destructuring can be function calls
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
const {node, calls: next_calls}
|
|
|
|
|
= eval_frame_expr(s.expr, scope, calls, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
const s_expr_evaled = {...s, children: [s.name_node, node]}
|
|
|
|
|
if(!node.result.ok) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
node: {...s_expr_evaled, result: {ok: false}},
|
|
|
|
|
scope,
|
|
|
|
|
calls: next_calls,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const name_nodes = collect_destructuring_identifiers(s.name_node)
|
|
|
|
|
const names = name_nodes.map(n => n.value)
|
|
|
|
|
const destructuring = codegen(s.name_node)
|
|
|
|
|
|
|
|
|
|
// TODO unique name for __value (gensym)
|
|
|
|
|
const codestring = `
|
|
|
|
|
const ${destructuring} = __value;
|
|
|
|
|
({${names.join(',')}});
|
|
|
|
|
`
|
|
|
|
|
const {ok, value: next_scope, error} = eval_codestring(
|
|
|
|
|
codestring,
|
|
|
|
|
{...scope, __value: node.result.value}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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 => ({...node,
|
|
|
|
|
result: {
|
|
|
|
|
ok,
|
|
|
|
|
error: ok ? null : error,
|
|
|
|
|
value: !ok ? null : next_scope[node.value],
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
n => 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
|
2023-10-26 12:25:35 +08:00
|
|
|
node: {...s_evaled, result: {ok, error, is_error_origin: true}},
|
2022-09-10 02:48:13 +08:00
|
|
|
scope,
|
|
|
|
|
calls,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
node: {...s_evaled, result: {ok: true}},
|
|
|
|
|
scope: {...scope, ...next_scope},
|
|
|
|
|
calls: next_calls,
|
|
|
|
|
assignments: s.type == 'assignment'
|
|
|
|
|
? Object.fromEntries(
|
|
|
|
|
name_nodes.map(n => [
|
|
|
|
|
n.definition.index,
|
|
|
|
|
{
|
|
|
|
|
value: next_scope[n.value],
|
|
|
|
|
name: n.value,
|
|
|
|
|
}
|
|
|
|
|
])
|
|
|
|
|
)
|
|
|
|
|
: null
|
|
|
|
|
}
|
2023-05-16 00:04:53 +03:00
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if(s.type == 'return') {
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
const {node, calls: next_calls} =
|
|
|
|
|
eval_frame_expr(s.expr, scope, calls, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ok: node.result.ok,
|
|
|
|
|
returned: node.result.ok,
|
|
|
|
|
node: {...s, children: [node], result: {ok: node.result.ok}},
|
|
|
|
|
scope,
|
|
|
|
|
calls: next_calls,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if(s.type == 'export') {
|
2023-01-18 19:21:25 +08:00
|
|
|
const {ok, scope: nextscope, calls: next_calls, node}
|
|
|
|
|
= eval_statement(s.binding, scope, calls, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
return {
|
|
|
|
|
ok,
|
|
|
|
|
scope: nextscope,
|
|
|
|
|
calls: next_calls,
|
|
|
|
|
node: {...s, children: [node], result: {ok: node.result.ok}}
|
|
|
|
|
}
|
|
|
|
|
} else if(s.type == 'import') {
|
2023-06-05 12:08:20 +03:00
|
|
|
const module = context.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]
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
))
|
2023-06-05 12:08:20 +03:00
|
|
|
const imported_scope = Object.fromEntries(
|
|
|
|
|
children.map(imp => [imp.value, imp.result.value])
|
|
|
|
|
)
|
2022-09-10 02:48:13 +08:00
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
scope: {...scope, ...imported_scope},
|
|
|
|
|
calls,
|
|
|
|
|
node: {...s, children, result: {ok: true}}
|
|
|
|
|
}
|
|
|
|
|
} else if(s.type == 'if') {
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
const {node, calls: next_calls} = eval_frame_expr(s.cond, scope, calls, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
if(!node.result.ok) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
node: {...s, children: [node, ...s.branches], result: {ok: false}},
|
|
|
|
|
scope,
|
|
|
|
|
calls: next_calls,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(s.branches.length == 1) {
|
|
|
|
|
// if without else
|
|
|
|
|
if(node.result.value) {
|
|
|
|
|
// Execute branch
|
|
|
|
|
const {
|
|
|
|
|
node: evaled_branch,
|
|
|
|
|
returned,
|
|
|
|
|
assignments,
|
|
|
|
|
scope: next_scope,
|
|
|
|
|
calls: next_calls2,
|
|
|
|
|
} = eval_statement(
|
|
|
|
|
s.branches[0],
|
|
|
|
|
scope,
|
|
|
|
|
next_calls,
|
2023-01-18 19:21:25 +08:00
|
|
|
context
|
2022-09-10 02:48:13 +08:00
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
ok: evaled_branch.result.ok,
|
|
|
|
|
returned,
|
|
|
|
|
assignments,
|
|
|
|
|
node: {...s,
|
|
|
|
|
children: [node, evaled_branch],
|
|
|
|
|
result: {ok: evaled_branch.result.ok}
|
|
|
|
|
},
|
|
|
|
|
scope: next_scope,
|
|
|
|
|
calls: next_calls2,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Branch is not executed
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
node: {...s, children: [node, s.branches[0]], result: {ok: true}},
|
|
|
|
|
scope,
|
|
|
|
|
calls: next_calls,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// if with else
|
|
|
|
|
const active_branch = node.result.value ? s.branches[0] : s.branches[1]
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
node: evaled_branch,
|
|
|
|
|
returned,
|
|
|
|
|
assignments,
|
|
|
|
|
scope: next_scope,
|
|
|
|
|
calls: next_calls2
|
|
|
|
|
} = eval_statement(
|
|
|
|
|
active_branch,
|
|
|
|
|
scope,
|
|
|
|
|
next_calls,
|
2023-01-18 19:21:25 +08:00
|
|
|
context,
|
2022-09-10 02:48:13 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const children = node.result.value
|
|
|
|
|
? [node, evaled_branch, s.branches[1]]
|
|
|
|
|
: [node, s.branches[0], evaled_branch]
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ok: evaled_branch.result.ok,
|
|
|
|
|
returned,
|
|
|
|
|
assignments,
|
|
|
|
|
node: {...s, children, result: {ok: evaled_branch.result.ok}},
|
|
|
|
|
scope: next_scope,
|
|
|
|
|
calls: next_calls2,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if(s.type == 'let') {
|
|
|
|
|
|
|
|
|
|
return { ok: true, node: s, scope, calls }
|
|
|
|
|
|
|
|
|
|
} else if(s.type == 'throw') {
|
|
|
|
|
|
2023-01-18 19:21:25 +08:00
|
|
|
const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
node: {...s,
|
|
|
|
|
children: [node],
|
|
|
|
|
result: {
|
|
|
|
|
ok: false,
|
2023-10-26 12:25:35 +08:00
|
|
|
is_error_origin: node.result.ok,
|
2022-09-10 02:48:13 +08:00
|
|
|
error: node.result.ok ? node.result.value : null,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
scope,
|
|
|
|
|
calls: next_calls,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// stmt type is expression
|
2023-01-18 19:21:25 +08:00
|
|
|
const {node, calls: next_calls} = eval_frame_expr(s, scope, calls, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
return {
|
|
|
|
|
ok: node.result.ok,
|
|
|
|
|
node,
|
|
|
|
|
scope,
|
|
|
|
|
calls: next_calls,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-15 20:53:16 +08:00
|
|
|
export const eval_frame = (calltree_node, modules) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
if(calltree_node.has_more_children) {
|
|
|
|
|
throw new Error('illegal state')
|
|
|
|
|
}
|
|
|
|
|
const node = calltree_node.code
|
2023-01-18 19:21:25 +08:00
|
|
|
const context = {calltree_node, modules}
|
2022-09-10 02:48:13 +08:00
|
|
|
if(node.type == 'do') {
|
|
|
|
|
return eval_statement(
|
|
|
|
|
node,
|
|
|
|
|
{},
|
|
|
|
|
calltree_node.children,
|
2023-01-18 19:21:25 +08:00
|
|
|
context,
|
2022-09-10 02:48:13 +08:00
|
|
|
).node
|
|
|
|
|
} else {
|
|
|
|
|
// TODO default values for destructuring can be function calls
|
|
|
|
|
|
2023-06-21 13:50:01 +03:00
|
|
|
const args_scope_result = get_args_scope(
|
|
|
|
|
node,
|
|
|
|
|
calltree_node.args,
|
|
|
|
|
calltree_node.fn.__closure
|
|
|
|
|
)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
// 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,
|
2023-10-26 12:25:35 +08:00
|
|
|
error: args_scope_result.ok ? null : args_scope_result.error,
|
|
|
|
|
is_error_origin: !args_scope_result.ok,
|
2022-09-10 02:48:13 +08:00
|
|
|
value: !args_scope_result.ok ? null : args_scope_result.value[a.value],
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
n => n.result == null
|
|
|
|
|
? {...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 scope = {...calltree_node.fn.__closure, ...args_scope_result.value}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let nextbody
|
|
|
|
|
|
|
|
|
|
if(body.type == 'do') {
|
|
|
|
|
nextbody = eval_statement(
|
|
|
|
|
body,
|
|
|
|
|
scope,
|
|
|
|
|
calltree_node.children,
|
2023-01-18 19:21:25 +08:00
|
|
|
context,
|
2022-09-10 02:48:13 +08:00
|
|
|
).node
|
|
|
|
|
} else {
|
2023-01-18 19:21:25 +08:00
|
|
|
nextbody = eval_frame_expr(body, scope, calltree_node.children, context)
|
2022-09-10 02:48:13 +08:00
|
|
|
.node
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {...node,
|
|
|
|
|
result: {ok: nextbody.result.ok},
|
|
|
|
|
children: [function_args_with_result, nextbody],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|