Files
leporello-js/src/eval.js

1573 lines
41 KiB
JavaScript
Raw Normal View History

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-04 22:37:35 +08:00
// external
// TODO
// import {} from './runtime.js'
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
*/
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(',')
2023-05-16 00:04:53 +03:00
const decl = node.is_arrow
? `(${args}) => `
: `function ${node.name}(${args})`
const call = (node.is_async ? 'async ' : '') + decl + (
2023-01-15 10:09:17 +08:00
// TODO gensym __obj, __fn
2022-09-10 02:48:13 +08:00
(node.body.type == 'do')
2022-12-31 20:40:59 +08:00
? '{ let __obj, __fn; ' + do_codegen(node.body) + '}'
: '{ let __obj, __fn; 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
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
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
2022-09-10 02:48:13 +08:00
const codegen_function_call = (node, cxt) => {
const do_codegen = n => codegen(n, cxt)
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-02-04 22:37:35 +08:00
// TODO rename cxt, to not confuse with another cxt
2022-09-10 02:48:13 +08:00
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'){
2023-05-16 00:04:53 +03:00
return [
// hoist function decls to the top
...node.stmts.filter(s => s.type == 'function_decl'),
...node.stmts.filter(s => s.type != 'function_decl'),
].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') {
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') {
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])
} 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(',')}]`
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') {
const names = node.imports.map(n => n.value)
2022-12-01 02:07:55 +08:00
if(names.length == 0) {
return ''
} else {
2023-02-04 22:37:35 +08:00
return `const {${names.join(',')}} = __cxt.modules['${node.full_import_path}'];`;
2022-12-01 02:07:55 +08:00
}
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)
+
2023-02-04 22:37:35 +08:00
`Object.assign(__cxt.modules[cxt.module], {${identifiers.join(',')}});`
2023-05-16 00:04:53 +03:00
} else if(node.type == 'function_decl') {
const expr = node.children[0]
return `const ${expr.name} = ${codegen_function_expr(expr, cxt)};`
2022-09-10 02:48:13 +08:00
} 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
) => {
2023-02-04 22:37:35 +08:00
// TODO gensym __cxt, __trace, __trace_call
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-04 22:37:35 +08:00
/*
TODO remove
cxt vars:
- modules
- is_recording_deferred_calls
- logs
- children
- prev_children
- call_counter
- is_toplevel_call
- searched_location
- found_call
- promise_then
- stack
- on_deferred_call
- calltree_changed_token
*/
// TODO sort
const cxt = {
is_recording_deferred_calls: false,
call_counter: 0,
logs: [],
is_toplevel_call: true,
2022-12-25 16:28:06 +08:00
// TODO use native array for stack for perf? stack contains booleans
2023-02-04 22:37:35 +08:00
stack: new Array(),
children: null,
prev_children: null,
searched_location: location,
found_call: null,
promise_then: null,
modules: external_imports == null
? null
: map_object(external_imports, (name, {module}) => module),
on_deferred_call: (call, calltree_changed_token, logs) => {
return on_deferred_call(
assign_code(parse_result.modules, call),
calltree_changed_token,
logs,
)
},
calltree_changed_token
}
2023-02-04 22:37:35 +08:00
const Function = is_async
? globalThis.run_window.eval('(async function(){})').constructor
: globalThis.run_window.Function
let calltree
apply_promise_patch(cxt)
for(let current_module of parse_result.sorted) {
cxt.found_call = null
cxt.children = null
calltree = {
toplevel: true,
module: current_module,
id: cxt.call_counter++
}
const module_fn = new Function(
'__cxt',
codegen(node, {module: module_name})
)
cxt.modules[current_module] =
try {
// cxt.modules[current_module] = {}
// TODO await
module_fn(cxt)
calltree.ok = true
} catch(error) {
calltree.ok = false
calltree.error = error
}
calltree.children = cxt.children
if(!calltree.ok) {
break
}
}
2022-12-15 21:15:52 +08:00
2023-02-04 22:37:35 +08:00
cxt.is_recording_deferred_calls = true
const _logs = cxt.logs
cxt.logs = []
cxt.children = null
2022-09-10 02:48:13 +08:00
2023-02-04 22:37:35 +08:00
remove_promise_patch(cxt)
2022-11-15 21:42:37 +08:00
2023-02-04 22:37:35 +08:00
searched_location = null
const call = found_call
found_call = null
2022-09-10 02:48:13 +08:00
2023-02-04 22:37:35 +08:00
return {
modules: cxt.modules,
calltree: assign_code(parse_result.modules, calltree),
call,
logs: _logs,
eval_cxt: cxt,
}
}
2022-10-25 02:29:59 +08:00
2023-02-04 22:37:35 +08:00
const apply_promise_patch = cxt => {
2022-12-25 16:28:06 +08:00
2023-02-04 22:37:35 +08:00
promise_then = Promise.prototype.then
2022-12-25 16:28:06 +08:00
2023-02-04 22:37:35 +08:00
Promise.prototype.then = function then(on_resolve, on_reject) {
2022-12-25 16:28:06 +08:00
2023-02-04 22:37:35 +08:00
if(children == null) {
children = []
}
let children_copy = children
2023-01-17 17:27:31 +08:00
2023-02-04 22:37:35 +08:00
const make_callback = (cb, ok) => typeof(cb) != 'function'
? cb
: value => {
if(this.status == null) {
this.status = ok ? {ok, value} : {ok, error: value}
}
const current = children
children = children_copy
try {
return cb(value)
} finally {
children = current
}
2022-12-25 16:28:06 +08:00
}
2023-02-04 22:37:35 +08:00
return promise_then.call(
this,
make_callback(on_resolve, true),
make_callback(on_reject, false),
)
}
}
const remove_promise_patch = cxt => {
Promise.prototype.then = promise_then
}
const set_record_call = () => {
for(let i = 0; i < stack.length; i++) {
stack[i] = true
}
}
const do_expand_calltree_node = node => {
if(node.fn.__location != null) {
// fn is hosted, it created call, this time with children
const result = children[0]
result.id = node.id
result.children = prev_children
result.has_more_children = false
return result
} else {
// fn is native, it did not created call, only its child did
return {...node,
children,
has_more_children: false,
2022-12-25 16:28:06 +08:00
}
2023-02-04 22:37:35 +08:00
}
}
2022-12-25 16:28:06 +08:00
2023-02-04 22:37:35 +08:00
export const eval_expand_calltree_node = (parse_result, node) => {
is_recording_deferred_calls = false
children = null
try {
if(node.is_new) {
new node.fn(...node.args)
} else {
node.fn.apply(node.context, node.args)
2022-12-25 16:28:06 +08:00
}
2023-02-04 22:37:35 +08:00
} catch(e) {
// do nothing. Exception was caught and recorded inside '__trace'
}
is_recording_deferred_calls = true
return assign_code(parse_result.modules, do_expand_calltree_node(node))
}
2022-12-25 16:28:06 +08:00
2023-02-04 22:37:35 +08:00
/*
Try to find call of function with given 'location'
2022-09-10 02:48:13 +08:00
2023-02-04 22:37:35 +08:00
Function is synchronous, because we recorded calltree nodes for all async
function calls. Here we walk over calltree, find leaves that have
'has_more_children' set to true, and rerunning fns in these leaves with
'searched_location' being set, until we find find call or no children
left.
We dont rerun entire execution because we want find_call to be
synchronous for simplicity
*/
export const eval_find_call = (cxt, parse_result, calltree, location) => {
// TODO remove
if(children != null) {
throw new Error('illegal state')
}
const do_find = node => {
if(node.children != null) {
for(let c of node.children) {
const result = do_find(c)
if(result != null) {
return result
2022-09-10 02:48:13 +08:00
}
}
2023-02-04 22:37:35 +08:00
// call was not find in children, return null
return null
2022-09-10 02:48:13 +08:00
}
2023-02-04 22:37:35 +08:00
if(node.has_more_children) {
2023-01-08 08:25:22 +08:00
try {
if(node.is_new) {
new node.fn(...node.args)
} else {
node.fn.apply(node.context, node.args)
}
} catch(e) {
2023-01-15 22:13:10 +08:00
// do nothing. Exception was caught and recorded inside '__trace'
2023-01-08 08:25:22 +08:00
}
2022-12-29 20:13:15 +08:00
2023-02-04 22:37:35 +08:00
if(found_call != null) {
2022-12-29 20:13:15 +08:00
return {
2023-02-04 22:37:35 +08:00
node: do_expand_calltree_node(node),
call: found_call,
2022-12-29 20:13:15 +08:00
}
} else {
2023-02-04 22:37:35 +08:00
children = null
2022-12-29 20:13:15 +08:00
}
}
2023-02-04 22:37:35 +08:00
// node has no children, return null
return null
}
2023-02-04 22:37:35 +08:00
is_recording_deferred_calls = false
searched_location = location
2023-01-08 08:25:22 +08:00
2023-02-04 22:37:35 +08:00
const result = do_find(calltree)
2023-01-08 08:25:22 +08:00
2023-02-04 22:37:35 +08:00
children = null
searched_location = null
found_call = null
is_recording_deferred_calls = true
2023-01-08 08:25:22 +08:00
2023-02-04 22:37:35 +08:00
if(result == null) {
return null
}
const {node, call} = result
const node_with_code = assign_code(parse_result.modules, node)
const call_with_code = find_node(node_with_code, n => n.id == call.id)
return {
node: node_with_code,
call: call_with_code,
}
}
2023-01-08 08:25:22 +08:00
2023-02-04 22:37:35 +08:00
const __do_await = async (cxt, value) => {
// children is an array of child calls for current function call. But it
// can be null to save one empty array allocation in case it has no child
// calls. Allocate array now, so we can have a reference to this array
// which will be used after await
if(children == null) {
children = []
}
const children_copy = children
if(value instanceof Promise) {
promise_then.call(value,
v => {
value.status = {ok: true, value: v}
},
e => {
value.status = {ok: false, error: e}
2022-11-23 18:03:00 +08:00
}
2023-02-04 22:37:35 +08:00
)
}
try {
return await value
} finally {
children = children_copy
}
}
2023-02-04 22:37:35 +08:00
const __trace = (cxt, fn, name, argscount, __location, get_closure) => {
const result = (...args) => {
if(result.__closure == null) {
result.__closure = get_closure()
}
2023-01-08 08:25:22 +08:00
2023-02-04 22:37:35 +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
&&
__location.module == searched_location.module
)
2022-09-10 02:48:13 +08:00
2023-02-04 22:37:35 +08:00
if(is_found_call) {
// Assign temporary value to prevent nested calls from populating
// found_call
found_call = {}
2023-01-08 08:25:22 +08:00
}
2022-12-19 19:05:36 +08:00
2023-02-04 22:37:35 +08:00
let ok, value, error
const is_toplevel_call_copy = is_toplevel_call
is_toplevel_call = false
try {
value = fn(...args)
ok = true
2022-12-25 16:39:20 +08:00
if(value instanceof Promise) {
2023-02-04 22:37:35 +08:00
set_record_call()
2022-12-23 20:14:38 +08:00
}
2023-02-04 22:37:35 +08:00
return value
} catch(_error) {
ok = false
error = _error
set_record_call()
throw error
} finally {
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),
2022-12-08 09:42:42 +08:00
}
2022-10-25 02:29:59 +08:00
2023-02-04 22:37:35 +08:00
if(is_found_call) {
found_call = call
set_record_call()
2022-09-10 02:48:13 +08:00
}
2023-02-04 22:37:35 +08:00
const should_record_call = stack.pop()
2022-09-10 02:48:13 +08:00
2023-02-04 22:37:35 +08:00
if(should_record_call) {
call.children = children
} else {
call.has_more_children = children != null && children.length != 0
2022-09-10 02:48:13 +08:00
}
2023-02-04 22:37:35 +08:00
children = children_copy
if(children == null) {
children = []
2022-09-10 02:48:13 +08:00
}
2023-02-04 22:37:35 +08:00
children.push(call)
2023-02-04 22:37:35 +08:00
is_toplevel_call = is_toplevel_call_copy
2023-02-04 22:37:35 +08:00
if(is_recording_deferred_calls && is_toplevel_call) {
if(children.length != 1) {
throw new Error('illegal state')
}
const call = children[0]
children = null
const _logs = logs
logs = []
on_deferred_call(call, calltree_changed_token, _logs)
}
2023-02-04 22:37:35 +08:00
}
}
2023-02-04 22:37:35 +08:00
Object.defineProperty(result, 'name', {value: name})
result.__location = __location
return result
}
2023-02-04 22:37:35 +08:00
const __trace_call = (cxt, fn, context, args, errormessage, is_new = false) => {
if(fn != null && fn.__location != null && !is_new) {
// Call will be traced, because tracing code is already embedded inside
// fn
return fn(...args)
}
2023-02-04 22:37:35 +08:00
if(typeof(fn) != 'function') {
throw new TypeError(
errormessage
+ ' is not a '
+ (is_new ? 'constructor' : 'function')
)
}
2023-02-04 22:37:35 +08:00
const children_copy = children
children = null
stack.push(false)
2022-09-10 02:48:13 +08:00
2023-02-04 22:37:35 +08:00
// TODO: other console fns
const is_log = fn == console.log || fn == console.error
2022-10-25 02:29:59 +08:00
2023-02-04 22:37:35 +08:00
if(is_log) {
set_record_call()
}
2022-10-25 02:29:59 +08:00
2023-02-04 22:37:35 +08:00
let ok, value, error
2023-01-21 16:49:22 +08:00
2023-02-04 22:37:35 +08:00
try {
if(!is_log) {
if(is_new) {
value = new fn(...args)
} else {
value = fn.apply(context, args)
2022-10-25 02:29:59 +08:00
}
2023-02-04 22:37:35 +08:00
} else {
value = undefined
2022-09-10 02:48:13 +08:00
}
2023-02-04 22:37:35 +08:00
ok = true
if(value instanceof Promise) {
set_record_call()
}
return value
} catch(_error) {
ok = false
error = _error
set_record_call()
throw error
} finally {
prev_children = children
const call = {
id: call_counter++,
ok,
value,
error,
fn,
args,
context,
is_log,
is_new,
}
if(is_log) {
// TODO do not collect logs on find_call?
logs.push(call)
2022-09-10 02:48:13 +08:00
}
2022-11-29 04:22:56 +08:00
2023-02-04 22:37:35 +08:00
const should_record_call = stack.pop()
2022-09-10 02:48:13 +08:00
2023-02-04 22:37:35 +08:00
if(should_record_call) {
call.children = children
} else {
call.has_more_children = children != null && children.length != 0
2022-09-10 02:48:13 +08:00
}
2023-02-04 22:37:35 +08:00
children = children_copy
if(children == null) {
children = []
}
children.push(call)
}
2022-09-10 02:48:13 +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
// 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,
)
),
}
}
}
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}
} else if([
'spread',
'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) => {
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'){
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) => {
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'){
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,
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,
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]
2022-12-07 05:06:15 +08:00
let ok, value, error
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') {
2022-12-23 02:14:38 +08:00
if(expr.result.value instanceof globalThis.run_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
}
} else {
ok = true
value = expr.result.value
}
2022-09-10 02:48:13 +08:00
} else {
throw new Error('unknown op')
}
2022-12-07 05:06:15 +08:00
return {ok, children, calls, value, error}
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) => {
const {ok, error, value, call, children, calls}
= 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
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}
}
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
const function_decls = node.stmts
.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}
const {ok, assignments, returned, stmts, calls: next_calls} = node.stmts.reduce(
2022-09-10 02:48:13 +08:00
({ok, returned, stmts, scope, calls, assignments}, s) => {
if(returned || !ok) {
return {ok, returned, scope, calls, stmts: [...stmts, 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,
stmts: [...stmts, node],
}
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,
stmts: [...stmts, node],
}
}
},
2023-05-16 00:04:53 +03:00
{ok: true, returned: false, stmts: [], scope: initial_scope, calls, assignments: {}}
2022-09-10 02:48:13 +08:00
)
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,
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
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
}
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') {
const children = s.imports.map(i => (
{...i,
2023-01-18 19:21:25 +08:00
result: {ok: true, value: context.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') {
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,
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
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,
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],
}
}
}