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,
|
|
|
|
|
collect_destructuring_identifiers,
|
|
|
|
|
map_destructuring_identifiers,
|
|
|
|
|
map_tree,
|
|
|
|
|
} from './ast_utils.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).
|
|
|
|
|
|
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
|
|
|
|
|
*/
|
|
|
|
|
|
2022-10-25 02:29:59 +08:00
|
|
|
// TODO just export const Iframe_Function?
|
2022-10-26 01:05:52 +08:00
|
|
|
const make_function = (...args) => {
|
2022-11-29 03:37:15 +08:00
|
|
|
if(globalThis.run_window == null) {
|
|
|
|
|
// Code is executed in test env
|
2022-10-26 01:05:52 +08:00
|
|
|
return new Function(...args)
|
|
|
|
|
} else {
|
|
|
|
|
// Code run in browser and user opened run_window
|
|
|
|
|
const fn_constructor = globalThis.run_window.Function
|
|
|
|
|
return new fn_constructor(...args)
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-10-25 02:29:59 +08:00
|
|
|
|
2022-12-02 06:05:20 +08:00
|
|
|
const codegen_function_expr = (node, cxt) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
const do_codegen = n => codegen(n, cxt)
|
|
|
|
|
|
|
|
|
|
const args = node.function_args.children.map(do_codegen).join(',')
|
|
|
|
|
|
2022-12-02 04:13:32 +08:00
|
|
|
const call = (node.is_async ? 'async ' : '') + `(${args}) => ` + (
|
2022-09-10 02:48:13 +08:00
|
|
|
(node.body.type == 'do')
|
|
|
|
|
? '{' + do_codegen(node.body) + '}'
|
|
|
|
|
: '(' + do_codegen(node.body) + ')'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const argscount = node.function_args.children.find(a => a.type == 'rest') != null
|
|
|
|
|
? node.function_args.children.length
|
|
|
|
|
: null
|
|
|
|
|
|
|
|
|
|
const location = `{index: ${node.index}, length: ${node.length}, module: '${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(',')}})`
|
|
|
|
|
|
2022-12-02 06:05:20 +08:00
|
|
|
return `trace(${call}, "${node.name}", ${argscount}, ${location}, ${get_closure})`
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO if statically can prove that function is hosted, then do not codegen
|
|
|
|
|
// trace
|
|
|
|
|
const codegen_function_call = (node, cxt) => {
|
|
|
|
|
|
|
|
|
|
const do_codegen = n => codegen(n, cxt)
|
|
|
|
|
|
|
|
|
|
const args = `[${node.args.children.map(do_codegen).join(',')}]`
|
|
|
|
|
|
|
|
|
|
let call
|
|
|
|
|
if(node.fn.type == 'member_access') {
|
|
|
|
|
// Wrap to IIFE to create scope to calculate obj.
|
|
|
|
|
// We cant do `codegen(obj)[prop].bind(codegen(obj))` because codegen(obj)
|
|
|
|
|
// can be expr we dont want to eval twice
|
|
|
|
|
|
|
|
|
|
const op = node.fn.is_optional_chaining ? '?.' : ''
|
|
|
|
|
|
|
|
|
|
// TODO gensym __obj, __fn
|
|
|
|
|
return `((() => {
|
|
|
|
|
const __obj = ${do_codegen(node.fn.object)};
|
|
|
|
|
const __fn = __obj${op}[${do_codegen(node.fn.property)}]
|
|
|
|
|
return trace_call(__fn, __obj, ${args})
|
|
|
|
|
})())`
|
|
|
|
|
} else {
|
|
|
|
|
return `trace_call(${do_codegen(node.fn)}, null, ${args})`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const codegen = (node, cxt, parent) => {
|
|
|
|
|
|
|
|
|
|
const do_codegen = (n, parent) => codegen(n, cxt, parent)
|
|
|
|
|
|
|
|
|
|
if([
|
|
|
|
|
'identifier',
|
|
|
|
|
'number',
|
|
|
|
|
'string_literal',
|
|
|
|
|
'builtin_identifier',
|
|
|
|
|
'backtick_string',
|
|
|
|
|
].includes(node.type)){
|
|
|
|
|
return node.value
|
|
|
|
|
} else if(node.type == 'do'){
|
|
|
|
|
return node.stmts.reduce(
|
|
|
|
|
(result, stmt) => result + (do_codegen(stmt)) + ';\n',
|
|
|
|
|
''
|
|
|
|
|
)
|
|
|
|
|
} 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') {
|
|
|
|
|
const left = 'if(' + do_codegen(node.cond) + '){' +
|
|
|
|
|
do_codegen(node.branches[0]) + ' } '
|
|
|
|
|
return node.branches[1] == null
|
|
|
|
|
? left
|
|
|
|
|
: left + ' else { ' + do_codegen(node.branches[1]) + ' }'
|
|
|
|
|
} 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 => {
|
|
|
|
|
if(el.type == 'spread'){
|
|
|
|
|
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'){
|
|
|
|
|
return codegen_function_call(node, cxt)
|
|
|
|
|
} else if(node.type == 'function_expr'){
|
2022-12-02 06:05:20 +08:00
|
|
|
return codegen_function_expr(node, cxt)
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if(node.type == 'ternary'){
|
|
|
|
|
return ''
|
|
|
|
|
+ '('
|
|
|
|
|
+ do_codegen(node.cond)
|
|
|
|
|
+ ')\n? '
|
|
|
|
|
+ do_codegen(node.branches[0])
|
|
|
|
|
+'\n: '
|
|
|
|
|
+ do_codegen(node.branches[1])
|
|
|
|
|
} 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') {
|
|
|
|
|
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 == 'spread'){
|
|
|
|
|
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(',')}]`
|
|
|
|
|
return `trace_call(${do_codegen(node.constructor)}, null, ${args}, 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') {
|
|
|
|
|
const names = node.imports.map(n => n.value)
|
2022-12-01 02:07:55 +08:00
|
|
|
if(names.length == 0) {
|
|
|
|
|
return ''
|
|
|
|
|
} else {
|
|
|
|
|
return `const {${names.join(',')}} = __modules['${node.full_import_path}'];`;
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
} else if(node.type == 'export') {
|
|
|
|
|
const identifiers = collect_destructuring_identifiers(node.binding.name_node)
|
|
|
|
|
.map(i => i.value)
|
|
|
|
|
return do_codegen(node.binding)
|
|
|
|
|
+
|
|
|
|
|
`Object.assign(__exports, {${identifiers.join(',')}});`
|
|
|
|
|
} else {
|
|
|
|
|
console.error(node)
|
|
|
|
|
throw new Error('unknown node type: ' + node.type)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-25 02:29:59 +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,
|
2022-10-25 02:29:59 +08:00
|
|
|
location
|
|
|
|
|
) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
// TODO gensym __modules, __exports
|
|
|
|
|
|
2022-10-19 03:22:48 +08:00
|
|
|
// TODO bug if module imported twice, once as external and as regular
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
const codestring =
|
|
|
|
|
`
|
2022-10-17 02:49:21 +08:00
|
|
|
let children, prev_children
|
2022-10-25 02:29:59 +08:00
|
|
|
// TODO use native array for stack for perf?
|
2022-10-17 02:49:21 +08:00
|
|
|
const stack = new Array()
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
let call_counter = 0
|
|
|
|
|
|
2022-11-15 21:42:37 +08:00
|
|
|
let current_module
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
let searched_location
|
|
|
|
|
let found_call
|
|
|
|
|
|
2022-12-02 04:31:16 +08:00
|
|
|
let is_recording_deferred_calls
|
2022-10-25 02:29:59 +08:00
|
|
|
let is_toplevel_call = true
|
|
|
|
|
|
2022-10-17 02:49:21 +08:00
|
|
|
const set_record_call = () => {
|
|
|
|
|
for(let i = 0; i < stack.length; i++) {
|
|
|
|
|
stack[i] = true
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const expand_calltree_node = (node) => {
|
2022-12-02 04:31:16 +08:00
|
|
|
is_recording_deferred_calls = false
|
2022-10-17 02:49:21 +08:00
|
|
|
children = null
|
2022-09-10 02:48:13 +08:00
|
|
|
try {
|
|
|
|
|
node.fn.apply(node.context, node.args)
|
|
|
|
|
} catch(e) {
|
|
|
|
|
// do nothing. Exception was caught and recorded inside 'trace'
|
|
|
|
|
}
|
2022-12-02 04:31:16 +08:00
|
|
|
is_recording_deferred_calls = true
|
2022-09-10 02:48:13 +08:00
|
|
|
if(node.fn.__location != null) {
|
|
|
|
|
// fn is hosted, it created call, this time with children
|
2022-10-17 02:49:21 +08:00
|
|
|
const result = children[0]
|
2022-09-10 02:48:13 +08:00
|
|
|
result.id = node.id
|
2022-10-17 02:49:21 +08:00
|
|
|
result.children = prev_children
|
|
|
|
|
result.has_more_children = false
|
2022-09-10 02:48:13 +08:00
|
|
|
return result
|
|
|
|
|
} else {
|
|
|
|
|
// fn is native, it did not created call, only its child did
|
|
|
|
|
return {...node,
|
2022-10-17 02:49:21 +08:00
|
|
|
children,
|
|
|
|
|
has_more_children: false,
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-02 04:31:16 +08:00
|
|
|
const find_call = (location, deferred_calls) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
searched_location = location
|
2022-12-02 04:31:16 +08:00
|
|
|
let is_found_deferred_call = false
|
2022-11-24 04:41:34 +08:00
|
|
|
let i
|
|
|
|
|
|
|
|
|
|
let {calltree} = run()
|
|
|
|
|
|
2022-12-02 04:31:16 +08:00
|
|
|
is_recording_deferred_calls = false
|
|
|
|
|
if(found_call == null && deferred_calls != null) {
|
|
|
|
|
for(i = 0; i < deferred_calls.length; i++) {
|
|
|
|
|
const c = deferred_calls[i]
|
2022-11-24 04:41:34 +08:00
|
|
|
try {
|
|
|
|
|
c.fn.apply(c.context, c.args)
|
|
|
|
|
} catch(e) {
|
|
|
|
|
// do nothing. Exception was caught and recorded inside 'trace'
|
|
|
|
|
}
|
2022-11-23 18:03:00 +08:00
|
|
|
if(found_call != null) {
|
2022-12-02 04:31:16 +08:00
|
|
|
is_found_deferred_call = true
|
2022-11-24 04:41:34 +08:00
|
|
|
calltree = children[0]
|
|
|
|
|
children = null
|
2022-11-23 18:03:00 +08:00
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-11-24 04:41:34 +08:00
|
|
|
|
2022-12-02 04:31:16 +08:00
|
|
|
is_recording_deferred_calls = true
|
2022-11-24 04:41:34 +08:00
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
searched_location = null
|
|
|
|
|
const call = found_call
|
|
|
|
|
found_call = null
|
2022-11-24 04:41:34 +08:00
|
|
|
return {
|
2022-12-02 04:31:16 +08:00
|
|
|
is_found_deferred_call,
|
|
|
|
|
deferred_call_index: i,
|
2022-11-24 04:41:34 +08:00
|
|
|
calltree,
|
|
|
|
|
call
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const trace = (fn, name, argscount, __location, get_closure) => {
|
|
|
|
|
const result = (...args) => {
|
|
|
|
|
if(result.__closure == null) {
|
|
|
|
|
result.__closure = get_closure()
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-17 02:49:21 +08:00
|
|
|
const children_copy = children
|
|
|
|
|
children = null
|
|
|
|
|
stack.push(false)
|
|
|
|
|
|
|
|
|
|
const is_found_call =
|
|
|
|
|
(searched_location != null && found_call == null)
|
|
|
|
|
&&
|
|
|
|
|
(
|
|
|
|
|
__location.index == searched_location.index
|
2022-09-10 02:48:13 +08:00
|
|
|
&&
|
2022-10-17 02:49:21 +08:00
|
|
|
__location.module == searched_location.module
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if(is_found_call) {
|
|
|
|
|
// Assign temporary value to prevent nested calls from populating
|
|
|
|
|
// found_call
|
|
|
|
|
found_call = {}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
2022-10-17 02:49:21 +08:00
|
|
|
let ok, value, error
|
|
|
|
|
|
2022-10-25 02:29:59 +08:00
|
|
|
const is_toplevel_call_copy = is_toplevel_call
|
|
|
|
|
is_toplevel_call = false
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
try {
|
2022-10-17 02:49:21 +08:00
|
|
|
value = fn(...args)
|
|
|
|
|
ok = true
|
2022-12-02 04:13:32 +08:00
|
|
|
return value instanceof Promise.Original
|
|
|
|
|
? value
|
|
|
|
|
.then(v => {
|
|
|
|
|
value.status = {ok: true, value: v}
|
|
|
|
|
return v
|
|
|
|
|
})
|
|
|
|
|
.catch(e => {
|
|
|
|
|
value.status = {ok: false, error: e}
|
|
|
|
|
throw e
|
|
|
|
|
})
|
|
|
|
|
: value
|
2022-10-17 02:49:21 +08:00
|
|
|
} catch(_error) {
|
|
|
|
|
ok = false
|
|
|
|
|
error = _error
|
|
|
|
|
set_record_call()
|
2022-09-10 02:48:13 +08:00
|
|
|
throw error
|
|
|
|
|
} finally {
|
2022-10-17 02:49:21 +08:00
|
|
|
|
|
|
|
|
prev_children = children
|
|
|
|
|
|
|
|
|
|
const call = {
|
|
|
|
|
id: call_counter++,
|
|
|
|
|
ok,
|
|
|
|
|
value,
|
|
|
|
|
error,
|
|
|
|
|
fn: result,
|
|
|
|
|
args: argscount == null
|
|
|
|
|
? args
|
|
|
|
|
// Do not capture unused args
|
|
|
|
|
: args.slice(0, argscount),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(is_found_call) {
|
|
|
|
|
found_call = call
|
|
|
|
|
set_record_call()
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
2022-10-17 02:49:21 +08:00
|
|
|
|
|
|
|
|
const should_record_call = stack.pop()
|
|
|
|
|
|
|
|
|
|
if(should_record_call) {
|
|
|
|
|
call.children = children
|
|
|
|
|
} else {
|
|
|
|
|
call.has_more_children = children != null && children.length != 0
|
|
|
|
|
}
|
|
|
|
|
children = children_copy
|
|
|
|
|
if(children == null) {
|
|
|
|
|
children = []
|
|
|
|
|
}
|
|
|
|
|
children.push(call)
|
2022-10-25 02:29:59 +08:00
|
|
|
|
|
|
|
|
is_toplevel_call = is_toplevel_call_copy
|
|
|
|
|
|
2022-12-02 04:31:16 +08:00
|
|
|
if(is_recording_deferred_calls && is_toplevel_call) {
|
2022-10-26 13:11:51 +08:00
|
|
|
if(children.length != 1) {
|
|
|
|
|
throw new Error('illegal state')
|
|
|
|
|
}
|
|
|
|
|
const call = children[0]
|
|
|
|
|
children = null
|
2022-12-02 04:31:16 +08:00
|
|
|
on_deferred_call(call, calltree_changed_token)
|
2022-10-25 02:29:59 +08:00
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Object.defineProperty(result, 'name', {value: name})
|
|
|
|
|
result.__location = __location
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-16 18:23:55 +08:00
|
|
|
const trace_call = (fn, context, args, is_new = false) => {
|
|
|
|
|
if(fn != null && fn.__location != null && !is_new) {
|
|
|
|
|
// Call will be traced, because tracing code is already embedded inside
|
|
|
|
|
// fn
|
2022-09-10 02:48:13 +08:00
|
|
|
return fn(...args)
|
|
|
|
|
}
|
2022-10-17 02:49:21 +08:00
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
if(typeof(fn) != 'function') {
|
2022-12-16 18:23:55 +08:00
|
|
|
// Raise error
|
|
|
|
|
if(is_new) {
|
|
|
|
|
return new fn(...args)
|
|
|
|
|
} else {
|
|
|
|
|
return fn.apply(context, args)
|
|
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
2022-10-17 02:49:21 +08:00
|
|
|
|
|
|
|
|
const children_copy = children
|
|
|
|
|
children = null
|
|
|
|
|
stack.push(false)
|
|
|
|
|
|
2022-11-15 16:01:13 +08:00
|
|
|
// TODO: other console fns
|
|
|
|
|
const is_log = fn == console.log || fn == console.error
|
2022-10-17 02:49:21 +08:00
|
|
|
|
|
|
|
|
if(is_log) {
|
|
|
|
|
set_record_call()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ok, value, error
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
try {
|
2022-10-17 02:49:21 +08:00
|
|
|
if(!is_log) {
|
2022-12-16 18:23:55 +08:00
|
|
|
if(is_new) {
|
|
|
|
|
value = new fn(...args)
|
|
|
|
|
} else {
|
|
|
|
|
value = fn.apply(context, args)
|
|
|
|
|
}
|
2022-10-17 02:49:21 +08:00
|
|
|
} else {
|
|
|
|
|
value = undefined
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
2022-10-17 02:49:21 +08:00
|
|
|
ok = true
|
2022-09-10 02:48:13 +08:00
|
|
|
return value
|
2022-10-17 02:49:21 +08:00
|
|
|
} catch(_error) {
|
|
|
|
|
ok = false
|
|
|
|
|
error = _error
|
|
|
|
|
set_record_call()
|
2022-09-10 02:48:13 +08:00
|
|
|
throw error
|
|
|
|
|
} finally {
|
2022-10-17 02:49:21 +08:00
|
|
|
|
|
|
|
|
prev_children = children
|
|
|
|
|
|
|
|
|
|
const call = {
|
|
|
|
|
id: call_counter++,
|
|
|
|
|
ok,
|
|
|
|
|
value,
|
|
|
|
|
error,
|
|
|
|
|
fn,
|
|
|
|
|
args,
|
|
|
|
|
context,
|
|
|
|
|
is_log,
|
2022-12-16 18:23:55 +08:00
|
|
|
is_new,
|
2022-10-17 02:49:21 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const should_record_call = stack.pop()
|
|
|
|
|
|
|
|
|
|
if(should_record_call) {
|
|
|
|
|
call.children = children
|
|
|
|
|
} else {
|
|
|
|
|
call.has_more_children = children != null && children.length != 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
children = children_copy
|
|
|
|
|
if(children == null) {
|
|
|
|
|
children = []
|
|
|
|
|
}
|
|
|
|
|
children.push(call)
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-15 15:03:19 +08:00
|
|
|
const run = () => {
|
2022-10-25 02:29:59 +08:00
|
|
|
|
2022-12-02 04:31:16 +08:00
|
|
|
is_recording_deferred_calls = false
|
2022-10-25 02:29:59 +08:00
|
|
|
|
|
|
|
|
const __modules = {
|
|
|
|
|
/* external_imports passed as an argument to function generated with
|
|
|
|
|
* 'new Function' constructor */
|
|
|
|
|
...external_imports
|
|
|
|
|
}
|
2022-10-17 02:49:21 +08:00
|
|
|
let current_call
|
|
|
|
|
|
2022-09-10 02:48:13 +08:00
|
|
|
`
|
|
|
|
|
+
|
2022-11-15 16:58:15 +08:00
|
|
|
parse_result.sorted
|
2022-09-10 02:48:13 +08:00
|
|
|
.map((m, i) =>
|
|
|
|
|
`
|
2022-11-15 21:42:37 +08:00
|
|
|
current_module = '${m}'
|
2022-11-15 16:01:13 +08:00
|
|
|
found_call = null
|
2022-10-17 02:49:21 +08:00
|
|
|
children = null
|
2022-09-10 02:48:13 +08:00
|
|
|
current_call = {
|
|
|
|
|
toplevel: true,
|
2022-11-15 16:58:15 +08:00
|
|
|
module: current_module,
|
2022-09-10 02:48:13 +08:00
|
|
|
id: call_counter++
|
|
|
|
|
}
|
2022-11-15 16:58:15 +08:00
|
|
|
__modules[current_module] =
|
2022-09-10 02:48:13 +08:00
|
|
|
(() => {
|
|
|
|
|
try {
|
|
|
|
|
const __exports = {};
|
2022-11-15 16:58:15 +08:00
|
|
|
${codegen(parse_result.modules[m], {module: m})};
|
2022-09-10 02:48:13 +08:00
|
|
|
current_call.ok = true
|
|
|
|
|
return __exports
|
|
|
|
|
} catch(error) {
|
|
|
|
|
current_call.ok = false
|
|
|
|
|
current_call.error = error
|
|
|
|
|
}
|
|
|
|
|
})()
|
2022-10-17 02:49:21 +08:00
|
|
|
current_call.children = children
|
2022-11-15 16:58:15 +08:00
|
|
|
if(!current_call.ok) {
|
2022-12-02 04:31:16 +08:00
|
|
|
is_recording_deferred_calls = true
|
2022-10-25 02:29:59 +08:00
|
|
|
children = null
|
2022-11-15 16:58:15 +08:00
|
|
|
return { modules: __modules, calltree: current_call }
|
2022-10-17 02:49:21 +08:00
|
|
|
}
|
2022-09-10 02:48:13 +08:00
|
|
|
`
|
|
|
|
|
)
|
|
|
|
|
.join('')
|
|
|
|
|
+
|
|
|
|
|
`
|
2022-12-02 04:31:16 +08:00
|
|
|
is_recording_deferred_calls = true
|
2022-10-25 02:29:59 +08:00
|
|
|
children = null
|
2022-11-15 21:42:37 +08:00
|
|
|
return { modules: __modules, calltree: current_call }
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
run,
|
|
|
|
|
expand_calltree_node,
|
|
|
|
|
find_call,
|
|
|
|
|
}
|
|
|
|
|
`
|
|
|
|
|
|
2022-10-25 02:29:59 +08:00
|
|
|
const actions = make_function(
|
|
|
|
|
'external_imports',
|
2022-12-02 04:31:16 +08:00
|
|
|
'on_deferred_call',
|
2022-11-29 04:22:56 +08:00
|
|
|
'calltree_changed_token',
|
2022-10-25 02:29:59 +08:00
|
|
|
codestring
|
|
|
|
|
)(
|
|
|
|
|
/* external_imports */
|
2022-10-19 03:22:48 +08:00
|
|
|
external_imports == null
|
2022-11-15 21:42:37 +08:00
|
|
|
? null
|
|
|
|
|
: map_object(external_imports, (name, {module}) => module),
|
2022-10-25 02:29:59 +08:00
|
|
|
|
2022-12-02 04:31:16 +08:00
|
|
|
/* on_deferred_call */
|
2022-11-29 04:22:56 +08:00
|
|
|
(call, calltree_changed_token) => {
|
2022-12-02 04:31:16 +08:00
|
|
|
return on_deferred_call(
|
2022-11-29 04:22:56 +08:00
|
|
|
assign_code(parse_result.modules, call),
|
|
|
|
|
calltree_changed_token,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
/* calltree_changed_token */
|
|
|
|
|
calltree_changed_token
|
2022-10-19 03:22:48 +08:00
|
|
|
)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
const calltree_actions = {
|
|
|
|
|
expand_calltree_node: (node) => {
|
|
|
|
|
const expanded = actions.expand_calltree_node(node)
|
2022-11-15 16:58:15 +08:00
|
|
|
return assign_code(parse_result.modules, expanded)
|
2022-09-10 02:48:13 +08:00
|
|
|
},
|
2022-12-02 04:31:16 +08:00
|
|
|
find_call: (loc, deferred_calls) => {
|
2022-11-24 04:41:34 +08:00
|
|
|
const {
|
2022-12-02 04:31:16 +08:00
|
|
|
is_found_deferred_call,
|
|
|
|
|
deferred_call_index,
|
2022-11-24 04:41:34 +08:00
|
|
|
calltree,
|
|
|
|
|
call
|
2022-12-02 04:31:16 +08:00
|
|
|
} = actions.find_call(loc, deferred_calls)
|
2022-09-10 02:48:13 +08:00
|
|
|
return {
|
2022-12-02 04:31:16 +08:00
|
|
|
is_found_deferred_call,
|
|
|
|
|
deferred_call_index,
|
2022-11-15 16:58:15 +08:00
|
|
|
calltree: assign_code(parse_result.modules, calltree),
|
2022-09-10 02:48:13 +08:00
|
|
|
// TODO: `call` does not have `code` property here. Currently it is
|
|
|
|
|
// worked around by callers. Refactor
|
|
|
|
|
call,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-15 16:58:15 +08:00
|
|
|
const result = location == null
|
|
|
|
|
? actions.run()
|
|
|
|
|
: calltree_actions.find_call(location)
|
2022-09-10 02:48:13 +08:00
|
|
|
|
|
|
|
|
return {
|
2022-11-15 16:58:15 +08:00
|
|
|
modules: result.modules,
|
|
|
|
|
calltree: assign_code(parse_result.modules, result.calltree),
|
|
|
|
|
call: result.call,
|
2022-09-10 02:48:13 +08:00
|
|
|
calltree_actions,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const eval_tree = node => {
|
2022-11-15 16:58:15 +08:00
|
|
|
return eval_modules(
|
|
|
|
|
{
|
2022-11-15 21:42:37 +08:00
|
|
|
modules: {'': node},
|
2022-11-15 16:58:15 +08:00
|
|
|
sorted: ['']
|
|
|
|
|
}
|
2022-11-15 20:53:16 +08:00
|
|
|
).calltree
|
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
|
2022-10-25 02:29:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO also create in Iframe Context?
|
2022-09-10 02:48:13 +08:00
|
|
|
const eval_codestring = new 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}
|
|
|
|
|
}
|
|
|
|
|
`
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const get_args_scope = (fn_node, args) => {
|
|
|
|
|
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)`
|
|
|
|
|
|
|
|
|
|
const {ok, value, error} = eval_codestring(codestring, {__args: args})
|
|
|
|
|
|
|
|
|
|
if(!ok) {
|
|
|
|
|
// TODO show exact destructuring error
|
|
|
|
|
return {ok, error}
|
|
|
|
|
} else {
|
|
|
|
|
return {
|
|
|
|
|
ok,
|
|
|
|
|
value: Object.fromEntries(
|
|
|
|
|
zip(
|
|
|
|
|
arg_names,
|
|
|
|
|
value,
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const eval_binary_expr = (node, scope, callsleft) => {
|
|
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft)
|
|
|
|
|
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}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const do_eval_frame_expr = (node, scope, callsleft) => {
|
|
|
|
|
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}
|
|
|
|
|
} else if([
|
|
|
|
|
'spread',
|
|
|
|
|
'key_value_pair',
|
|
|
|
|
'computed_property'
|
|
|
|
|
].includes(node.type)) {
|
|
|
|
|
return eval_children(node, scope, callsleft)
|
|
|
|
|
} else if(node.type == 'array_literal' || node.type == 'call_args'){
|
|
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft)
|
|
|
|
|
if(!ok) {
|
|
|
|
|
return {ok, children, calls}
|
|
|
|
|
}
|
|
|
|
|
const value = children.reduce(
|
|
|
|
|
(arr, el) => {
|
|
|
|
|
if(el.type == 'spread') {
|
|
|
|
|
return [...arr, ...el.children[0].result.value]
|
|
|
|
|
} else {
|
|
|
|
|
return [...arr, el.result.value]
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[],
|
|
|
|
|
)
|
|
|
|
|
return {ok, children, calls, value}
|
|
|
|
|
} else if(node.type == 'object_literal'){
|
|
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft)
|
|
|
|
|
if(!ok) {
|
|
|
|
|
return {ok, children, calls}
|
|
|
|
|
}
|
|
|
|
|
const value = children.reduce(
|
|
|
|
|
(value, el) => {
|
|
|
|
|
if(el.type == 'spread'){
|
|
|
|
|
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'){
|
2022-09-10 02:48:13 +08:00
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft)
|
|
|
|
|
if(!ok) {
|
|
|
|
|
return {ok: false, children, calls}
|
|
|
|
|
} else {
|
|
|
|
|
if(typeof(children[0].result.value) != 'function') {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
// TODO pass calltree_node here and extract error
|
|
|
|
|
// TODO fix error messages
|
|
|
|
|
error: new Error('is not a function'),
|
|
|
|
|
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,
|
|
|
|
|
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,
|
|
|
|
|
}
|
|
|
|
|
} else if(node.type == 'ternary'){
|
|
|
|
|
const {node: cond_evaled, calls: calls_after_cond} = eval_frame_expr(
|
|
|
|
|
node.cond,
|
|
|
|
|
scope,
|
|
|
|
|
callsleft
|
|
|
|
|
)
|
|
|
|
|
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,
|
|
|
|
|
calls_after_cond
|
|
|
|
|
)
|
|
|
|
|
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'){
|
|
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft)
|
|
|
|
|
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') {
|
|
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft)
|
|
|
|
|
if(!ok) {
|
|
|
|
|
return {ok: false, children, calls}
|
|
|
|
|
} else {
|
|
|
|
|
const expr = children[0]
|
|
|
|
|
let value
|
|
|
|
|
if(node.operator == '!') {
|
|
|
|
|
value = !expr.result.value
|
|
|
|
|
} else if(node.operator == 'typeof') {
|
|
|
|
|
value = typeof(expr.result.value)
|
2022-12-07 05:52:13 +08:00
|
|
|
} else if(node.operator == '-') {
|
|
|
|
|
value = - expr.result.value
|
2022-12-02 04:13:32 +08:00
|
|
|
} else if(node.operator == 'await') {
|
|
|
|
|
log('expr', expr.result.value.status)
|
|
|
|
|
value = expr.result.value
|
|
|
|
|
//throw new Error('not implemented')
|
2022-09-10 02:48:13 +08:00
|
|
|
} else {
|
|
|
|
|
throw new Error('unknown op')
|
|
|
|
|
}
|
|
|
|
|
return {ok: true, children, calls, value}
|
|
|
|
|
}
|
|
|
|
|
} else if(node.type == 'binary' && !['&&', '||', '??'].includes(node.operator)){
|
|
|
|
|
|
|
|
|
|
return eval_binary_expr(node, scope, callsleft)
|
|
|
|
|
|
|
|
|
|
} else if(node.type == 'binary' && ['&&', '||', '??'].includes(node.operator)){
|
|
|
|
|
const {node: left_evaled, calls} = eval_frame_expr(
|
|
|
|
|
node.children[0],
|
|
|
|
|
scope,
|
|
|
|
|
callsleft
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
return eval_binary_expr(node, scope, callsleft)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else if(node.type == 'grouping'){
|
|
|
|
|
const {ok, children, calls} = eval_children(node, scope, callsleft)
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const eval_children = (node, scope, calls) => {
|
|
|
|
|
return node.children.reduce(
|
|
|
|
|
({ok, children, calls}, child) => {
|
|
|
|
|
let next_child, next_ok, next_calls
|
|
|
|
|
if(!ok) {
|
|
|
|
|
next_child = child;
|
|
|
|
|
next_ok = false;
|
|
|
|
|
next_calls = calls;
|
|
|
|
|
} else {
|
|
|
|
|
const result = eval_frame_expr(child, scope, calls);
|
|
|
|
|
next_child = result.node;
|
|
|
|
|
next_calls = result.calls;
|
|
|
|
|
next_ok = next_child.result.ok;
|
|
|
|
|
}
|
|
|
|
|
return {ok: next_ok, children: [...children, next_child], calls: next_calls}
|
|
|
|
|
},
|
|
|
|
|
{ok: true, children: [], calls}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const eval_frame_expr = (node, scope, callsleft) => {
|
|
|
|
|
const {ok, error, value, call, children, calls} = do_eval_frame_expr(node, scope, callsleft)
|
|
|
|
|
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
|
|
|
|
|
result: {ok, error, value, call}
|
|
|
|
|
},
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
const unused_assignments = filter_object(assignments, (index, val) =>
|
|
|
|
|
let_ids.find(i => i.toString() == index) == null
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2022-11-15 20:53:16 +08:00
|
|
|
const eval_statement = (s, scope, calls, modules) => {
|
2022-09-10 02:48:13 +08:00
|
|
|
if(s.type == 'do') {
|
|
|
|
|
const node = s
|
|
|
|
|
const {ok, assignments, returned, stmts, calls: nextcalls} = node.stmts.reduce(
|
|
|
|
|
({ok, returned, stmts, scope, calls, assignments}, s) => {
|
|
|
|
|
if(returned || !ok) {
|
|
|
|
|
return {ok, returned, scope, calls, stmts: [...stmts, s], assignments}
|
|
|
|
|
} else {
|
|
|
|
|
const {
|
|
|
|
|
ok,
|
|
|
|
|
returned,
|
|
|
|
|
node,
|
|
|
|
|
assignments: next_assignments,
|
|
|
|
|
scope: nextscope,
|
|
|
|
|
calls: next_calls,
|
2022-11-15 20:53:16 +08:00
|
|
|
} = eval_statement(s, scope, calls, modules)
|
2022-09-10 02:48:13 +08:00
|
|
|
return {
|
|
|
|
|
ok,
|
|
|
|
|
returned,
|
|
|
|
|
assignments: {...assignments, ...next_assignments},
|
|
|
|
|
scope: nextscope,
|
|
|
|
|
calls: next_calls,
|
|
|
|
|
stmts: [...stmts, node],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ok: true, returned: false, stmts: [], scope, calls, assignments: {}}
|
|
|
|
|
)
|
|
|
|
|
const {node: next_node, scope: next_scope} =
|
|
|
|
|
apply_assignments({...node, children: stmts, result: {ok}}, assignments)
|
|
|
|
|
return {
|
|
|
|
|
ok,
|
|
|
|
|
node: next_node,
|
|
|
|
|
scope: {...scope, ...next_scope},
|
|
|
|
|
returned,
|
|
|
|
|
assignments,
|
|
|
|
|
calls: nextcalls,
|
|
|
|
|
}
|
|
|
|
|
} else if(s.type == 'const' || s.type == 'assignment') {
|
|
|
|
|
// TODO default values for destructuring can be function calls
|
|
|
|
|
|
|
|
|
|
const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls)
|
|
|
|
|
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
|
|
|
|
|
node: {...s_evaled, result: {ok, error}},
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
} else if(s.type == 'return') {
|
|
|
|
|
|
|
|
|
|
const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls)
|
|
|
|
|
|
|
|
|
|
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') {
|
|
|
|
|
const {ok, scope: nextscope, calls: next_calls, node} = eval_statement(s.binding, scope, calls)
|
|
|
|
|
return {
|
|
|
|
|
ok,
|
|
|
|
|
scope: nextscope,
|
|
|
|
|
calls: next_calls,
|
|
|
|
|
node: {...s, children: [node], result: {ok: node.result.ok}}
|
|
|
|
|
}
|
|
|
|
|
} else if(s.type == 'import') {
|
|
|
|
|
const children = s.imports.map(i => (
|
|
|
|
|
{...i,
|
2022-11-15 20:53:16 +08:00
|
|
|
result: {ok: true, value: modules[s.full_import_path][i.value]}
|
2022-09-10 02:48:13 +08:00
|
|
|
}
|
|
|
|
|
))
|
|
|
|
|
const imported_scope = Object.fromEntries(children.map(i => [i.value, i.result.value]))
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
scope: {...scope, ...imported_scope},
|
|
|
|
|
calls,
|
|
|
|
|
node: {...s, children, result: {ok: true}}
|
|
|
|
|
}
|
|
|
|
|
} else if(s.type == 'if') {
|
|
|
|
|
|
|
|
|
|
const {node, calls: next_calls} = eval_frame_expr(s.cond, scope, calls)
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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') {
|
|
|
|
|
|
|
|
|
|
const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
node: {...s,
|
|
|
|
|
children: [node],
|
|
|
|
|
result: {
|
|
|
|
|
ok: false,
|
|
|
|
|
error: node.result.ok ? node.result.value : null,
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
scope,
|
|
|
|
|
calls: next_calls,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// stmt type is expression
|
|
|
|
|
const {node, calls: next_calls} = eval_frame_expr(
|
|
|
|
|
s,
|
|
|
|
|
scope,
|
|
|
|
|
calls,
|
|
|
|
|
)
|
|
|
|
|
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
|
|
|
|
|
if(node.type == 'do') {
|
|
|
|
|
return eval_statement(
|
|
|
|
|
node,
|
|
|
|
|
{},
|
|
|
|
|
calltree_node.children,
|
2022-11-15 20:53:16 +08:00
|
|
|
modules,
|
2022-09-10 02:48:13 +08:00
|
|
|
).node
|
|
|
|
|
} else {
|
|
|
|
|
// TODO default values for destructuring can be function calls
|
|
|
|
|
|
|
|
|
|
const args_scope_result = get_args_scope(node, calltree_node.args)
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
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,
|
|
|
|
|
).node
|
|
|
|
|
} else {
|
|
|
|
|
nextbody = eval_frame_expr(body, scope, calltree_node.children)
|
|
|
|
|
.node
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {...node,
|
|
|
|
|
result: {ok: nextbody.result.ok},
|
|
|
|
|
children: [function_args_with_result, nextbody],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|