2022-09-10 02:48:13 +08:00
|
|
|
// TODO rename to analyze.js
|
|
|
|
|
|
|
|
|
|
import {set_push, set_diff, set_union, map_object, map_find, uniq} from './utils.js'
|
|
|
|
|
import {collect_destructuring_identifiers, collect_imports, ancestry, find_leaf} from './ast_utils.js'
|
|
|
|
|
|
|
|
|
|
// TODO get complete list of globals (borrow from eslint?)
|
|
|
|
|
import {globals} from './globals.js'
|
|
|
|
|
|
|
|
|
|
const map_find_definitions = (nodes, mapper) => {
|
|
|
|
|
const result = nodes.map(mapper)
|
|
|
|
|
const undeclared = result.reduce(
|
|
|
|
|
(acc, el) => el.undeclared == null ? acc : acc.concat(el.undeclared),
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
const closed = result.map(r => r.closed).reduce(set_union, new Set())
|
|
|
|
|
return {
|
|
|
|
|
nodes: result.map(r => r.node),
|
|
|
|
|
undeclared,
|
|
|
|
|
closed,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const scope_from_node = n => {
|
|
|
|
|
if(n.type == 'import') {
|
|
|
|
|
return Object.fromEntries(
|
|
|
|
|
n.imports.map(i => [i.value, i])
|
|
|
|
|
)
|
|
|
|
|
} else if(n.type == 'export'){
|
|
|
|
|
return scope_from_node(n.binding)
|
|
|
|
|
} else if(n.type == 'const' || n.type == 'let'){
|
|
|
|
|
return Object.fromEntries(
|
|
|
|
|
collect_destructuring_identifiers(n.name_node).map(node => [
|
|
|
|
|
node.value, node
|
|
|
|
|
])
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const add_trivial_definition = node => {
|
|
|
|
|
if(node.type == 'identifier') {
|
|
|
|
|
return {...node, definition: 'self'}
|
|
|
|
|
} else if(['destructuring_default', 'destructuring_rest'].includes(node.type)){
|
|
|
|
|
return {...node,
|
|
|
|
|
children: [add_trivial_definition(node.name_node), ...node.children.slice(1)]
|
|
|
|
|
}
|
|
|
|
|
} else if(node.type == 'destructuring_pair') {
|
|
|
|
|
return {...node, children: [
|
|
|
|
|
node.children[0], // key
|
|
|
|
|
add_trivial_definition(node.children[1]), // value
|
|
|
|
|
]}
|
|
|
|
|
} else if(['array_destructuring', 'object_destructuring'].includes(node.type)) {
|
|
|
|
|
return {...node, children: node.children.map(add_trivial_definition)}
|
|
|
|
|
} else {
|
|
|
|
|
console.error(node)
|
|
|
|
|
throw new Error('not implemented')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* The function does these things:
|
|
|
|
|
* - For each occurence of identifier, attaches definition to this identifier
|
|
|
|
|
* - For each closure, attaches 'closed` property with set of vars it closes
|
|
|
|
|
* over
|
|
|
|
|
* - Finds undeclared identifiers
|
|
|
|
|
*
|
|
|
|
|
* `scope` is names that are already defined and can be used immediately.
|
|
|
|
|
* `closure_scope` is names that are defined but not yet assigned, but they
|
|
|
|
|
* will be assigned by the time the closures would be called
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// TODO in same pass find already declared
|
|
|
|
|
export const find_definitions = (ast, scope = {}, closure_scope = {}, module_name) => {
|
|
|
|
|
if(ast.type == 'identifier'){
|
|
|
|
|
if(ast.definition != null) {
|
|
|
|
|
// Definition previously added by add_trivial_definition
|
|
|
|
|
return {node: ast, undeclared: null, closed: new Set([ast.value])}
|
|
|
|
|
} else {
|
|
|
|
|
const definition = scope[ast.value]
|
|
|
|
|
if(definition == null){
|
|
|
|
|
if(globals.has(ast.value)) {
|
|
|
|
|
return {node: {...ast, definition: 'global'}, undeclared: null, closed: new Set()}
|
|
|
|
|
} else {
|
|
|
|
|
return {node: ast, undeclared: [ast], closed: new Set()}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return {
|
|
|
|
|
node: {...ast, definition: {index: definition.index}},
|
|
|
|
|
undeclared: null,
|
|
|
|
|
closed: new Set([ast.value])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if(ast.type == 'do'){
|
|
|
|
|
const children_with_scope = ast.children.reduce(
|
|
|
|
|
({scope, children}, node) => ({
|
|
|
|
|
scope: {...scope, ...scope_from_node(node)},
|
|
|
|
|
children: children.concat([{node, scope}]),
|
|
|
|
|
})
|
|
|
|
|
,
|
|
|
|
|
{scope: {}, children: []}
|
|
|
|
|
)
|
|
|
|
|
const local_scope = children_with_scope.scope
|
|
|
|
|
const {nodes, undeclared, closed} = map_find_definitions(children_with_scope.children, cs =>
|
|
|
|
|
find_definitions(cs.node, {...scope, ...cs.scope}, local_scope, module_name)
|
|
|
|
|
)
|
|
|
|
|
return {
|
|
|
|
|
node: {...ast, children: nodes},
|
|
|
|
|
undeclared,
|
|
|
|
|
closed: set_diff(closed, new Set(Object.keys(local_scope))),
|
|
|
|
|
}
|
|
|
|
|
} else if (ast.type == 'function_expr'){
|
|
|
|
|
const args_identifiers = collect_destructuring_identifiers(ast.function_args)
|
|
|
|
|
const args_scope = Object.fromEntries(args_identifiers.map(a => [
|
|
|
|
|
a.value, a
|
|
|
|
|
]))
|
|
|
|
|
const {nodes, undeclared, closed} = map_find_definitions(ast.children,
|
|
|
|
|
node => find_definitions(node, {...scope, ...closure_scope, ...args_scope})
|
|
|
|
|
)
|
|
|
|
|
const next_closed = set_diff(closed, new Set(args_identifiers.map(a => a.value)))
|
|
|
|
|
return {
|
|
|
|
|
node: {...ast, children: nodes, closed: next_closed},
|
|
|
|
|
undeclared,
|
|
|
|
|
closed: new Set(),
|
|
|
|
|
}
|
|
|
|
|
} else if(ast.children != null){
|
|
|
|
|
let children, full_import_path
|
|
|
|
|
if(ast.type == 'import') {
|
|
|
|
|
full_import_path = concat_path(module_name, ast.module)
|
|
|
|
|
children = ast.children.map(c => ({...c, definition: {module: full_import_path}}))
|
|
|
|
|
} else if(ast.type == 'const') {
|
|
|
|
|
children = [add_trivial_definition(ast.name_node), ...ast.children.slice(1)]
|
|
|
|
|
} else if(ast.type == 'let') {
|
|
|
|
|
children = ast.name_node.map(add_trivial_definition)
|
|
|
|
|
} else {
|
|
|
|
|
children = ast.children
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const {nodes, undeclared, closed} = map_find_definitions(children,
|
|
|
|
|
c => find_definitions(c, scope, closure_scope)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
node: ast.type == 'import'
|
|
|
|
|
? {...ast, children: nodes, full_import_path}
|
|
|
|
|
: {...ast, children: nodes},
|
|
|
|
|
undeclared,
|
|
|
|
|
closed
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return {node: ast, undeclared: null, closed: new Set()}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// see https://stackoverflow.com/a/29855511
|
|
|
|
|
|
|
|
|
|
// Joins path segments. Preserves initial "/" and resolves ".." and "."
|
|
|
|
|
// Does not support using ".." to go above/outside the root.
|
|
|
|
|
// This means that join("foo", "../../bar") will not resolve to "../bar"
|
|
|
|
|
|
|
|
|
|
const join = new Function(`
|
|
|
|
|
// Split the inputs into a list of path commands.
|
|
|
|
|
var parts = [];
|
|
|
|
|
for (var i = 0, l = arguments.length; i < l; i++) {
|
|
|
|
|
parts = parts.concat(arguments[i].split("/"));
|
|
|
|
|
}
|
|
|
|
|
// Interpret the path commands to get the new resolved path.
|
|
|
|
|
var newParts = [];
|
|
|
|
|
for (i = 0, l = parts.length; i < l; i++) {
|
|
|
|
|
var part = parts[i];
|
|
|
|
|
// Remove leading and trailing slashes
|
|
|
|
|
// Also remove "." segments
|
|
|
|
|
if (!part || part === ".") continue;
|
|
|
|
|
// Interpret ".." to pop the last segment
|
|
|
|
|
if (part === "..") newParts.pop();
|
|
|
|
|
// Push new path segments.
|
|
|
|
|
else newParts.push(part);
|
|
|
|
|
}
|
|
|
|
|
// Preserve the initial slash if there was one.
|
|
|
|
|
if (parts[0] === "") newParts.unshift("");
|
|
|
|
|
// Turn back into a single string path.
|
|
|
|
|
return newParts.join("/") || (newParts.length ? "/" : ".");
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
const concat_path = (base, imported) =>
|
|
|
|
|
base == '' ? join(imported) : join(base, '..', imported)
|
|
|
|
|
|
|
|
|
|
export const find_export = (name, module) => {
|
|
|
|
|
return map_find(module.stmts, n => {
|
|
|
|
|
if(n.type != 'export') {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
const ids = collect_destructuring_identifiers(n.binding.name_node)
|
|
|
|
|
return ids.find(i => i.value == name)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const topsort_modules = (modules) => {
|
|
|
|
|
const sort_module_deps = (module) => {
|
|
|
|
|
return Object.keys(collect_imports(modules[module]))
|
|
|
|
|
.reduce(
|
|
|
|
|
(result, m) => result.concat(sort_module_deps(m)),
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
.concat(module)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sorted = Object.keys(modules).reduce(
|
|
|
|
|
(result, module) => result.concat(sort_module_deps(module)),
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// now remove duplicates
|
|
|
|
|
// quadratic, but N is supposed to be small
|
|
|
|
|
return sorted.reduce(
|
|
|
|
|
(result, elem) =>
|
|
|
|
|
result.includes(elem)
|
|
|
|
|
? result
|
|
|
|
|
: [...result, elem]
|
|
|
|
|
,
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO not implemented
|
|
|
|
|
// TODO detect cycles when loading modules
|
|
|
|
|
export const check_imports = modules => {
|
|
|
|
|
// TODO allow circular imports
|
|
|
|
|
return map_object(modules, (module, node) => {
|
|
|
|
|
const imports = node.stmts
|
|
|
|
|
.filter(n => n.type == 'import')
|
|
|
|
|
.reduce(
|
|
|
|
|
(imports, n) => [
|
|
|
|
|
...imports,
|
|
|
|
|
...(n.imports.map(i => ({name: i.value, from: n.module})))
|
|
|
|
|
],
|
|
|
|
|
[]
|
|
|
|
|
)
|
|
|
|
|
const exports = node.statement
|
|
|
|
|
.filter(n => n.type == 'export')
|
|
|
|
|
.map(n => collect_destructuring_identifiers(n.binding.name_node))
|
|
|
|
|
.reduce((all, current) => [...all, ...current], [])
|
|
|
|
|
|
|
|
|
|
return {imports, exports}
|
|
|
|
|
//TODO check for each import, there is export
|
|
|
|
|
})
|
|
|
|
|
// Topological sort
|
|
|
|
|
// For each module
|
|
|
|
|
// Depth-traverse deps and detect cycles
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
TODO: relax, only disallow code that leads to broken target code
|
|
|
|
|
|
|
|
|
|
code analysis:
|
|
|
|
|
- function must have one and only one return statement in every branch
|
|
|
|
|
- return must be the last statement in block
|
|
|
|
|
|
|
|
|
|
- name is declared once and only once (including function args). Name can be imported once
|
|
|
|
|
- let must be assigned once and only once (in each branch)
|
|
|
|
|
- every assignment can only be to if identifier is earlier declared by let
|
|
|
|
|
- assignment can only be inside if statement (after let) (relax it?)
|
|
|
|
|
- cannot import names that are not exported from modules
|
2022-10-19 03:22:48 +08:00
|
|
|
- module can be imported either as external or regular
|
2022-09-10 02:48:13 +08:00
|
|
|
- cannot return from modules (even inside toplevel if statements)
|
|
|
|
|
*/
|
|
|
|
|
export const analyze = (node, is_toplevel = true) => {
|
|
|
|
|
// TODO remove
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
// TODO sort by location?
|
|
|
|
|
if(node.type == 'do') {
|
|
|
|
|
let illegal_returns
|
|
|
|
|
if(is_toplevel) {
|
|
|
|
|
illegal_returns = node.stmts.filter(s => s.type == 'return')
|
|
|
|
|
} else {
|
|
|
|
|
const last = node.stmts[node.stmts.length - 1];
|
|
|
|
|
illegal_returns = node.stmts.filter(s => s.type == 'return' && s != last);
|
|
|
|
|
|
|
|
|
|
returns.map(node => ({
|
|
|
|
|
node,
|
|
|
|
|
message: 'illegal return statement',
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const last_return = last.type == 'return'
|
|
|
|
|
? null
|
|
|
|
|
: {node: last, message: 'block must end with return statement'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// TODO recur to childs
|
|
|
|
|
}
|
|
|
|
|
} else if(node.children != null){
|
|
|
|
|
return node.children
|
|
|
|
|
.map(n => analyze(n, node.type == 'function_expr' ? false : is_toplevel))
|
|
|
|
|
.reduce((ps, p) => ps.concat(p), [])
|
|
|
|
|
} else {
|
|
|
|
|
// TODO
|
|
|
|
|
1
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
}
|