improve static analysis

This commit is contained in:
Dmitry Vasilev
2023-09-29 19:04:13 +03:00
parent 4c3d8dd93d
commit a255ba6067
2 changed files with 180 additions and 62 deletions

View File

@@ -71,7 +71,6 @@ const add_trivial_definition = node => {
* will be assigned by the time the closures would be called
*/
// TODO in same pass find already declared
export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, module_name) => {
@@ -246,63 +245,100 @@ export const check_imports = modules => {
.reduce((all, current) => [...all, ...current], [])
return {imports, exports}
// TODO check for each import, there is export. For default import there is
// default 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
- every assignment can only be to identifier is earlier declared by let
- cannot import names that are not exported from modules.If there is default import from module, there should be default export
- module can be imported either as external or regular
- cannot return from modules (even inside toplevel if statements)
- cannot return from modules toplevel
- await only in async fns
- import only from toplevel
*/
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
}
*/
return [
...analyze_await(node, true),
...named_declared_once(node),
]
}
const collect_problems = (node, context, collector) => {
const {context: next_context, problems: node_problems} = collector(node, context)
if(node.children == null) {
return node_problems
}
return node.children.reduce(
(problems, c) => {
const ps = collect_problems(c, next_context, collector)
if(ps == null) {
return problems
}
if(problems == null) {
return ps
}
return problems.concat(ps)
},
node_problems
)
}
const analyze_await = (node, is_async_context = true) => {
const result = collect_problems(node, is_async_context, (node, is_async_context) => {
if(node.type == 'function_expr') {
return {problems: null, context: node.is_async}
}
if(node.type == 'unary' && node.operator == 'await' && !is_async_context) {
const _await = node.children[0]
const problem = {
index: _await.index,
length: _await.length,
message: 'await is only valid in async functions and the top level bodies of modules',
}
return {problems: [problem], context: is_async_context}
}
return {problems: null, context: is_async_context}
})
return result ?? []
}
const named_declared_once = node => {
return collect_problems(node, null, (node, cxt) => {
if(node.type == 'do') {
const names = node
.children
.map(c => {
if(c.type == 'function_decl') {
const function_expr = c.children[0]
return {
value: function_expr.name,
index: function_expr.index,
length: function_expr.name.length
}
} else {
const scope = scope_from_node(c)
return scope == null
? null
: Object.values(scope)
}
})
.flat()
.filter(n => n != null)
const duplicates = names.filter((n, i) =>
names.find((name, j) => name.value == n.value && j < i) != null
)
const problems = duplicates.map(d => ({
index: d.index,
length: d.length,
message: `Identifier '${d.value}' has already been declared`,
}))
return {context: null, problems}
} else {
return {context: null, problems: null}
}
})
}

View File

@@ -1244,7 +1244,100 @@ export const tests = [
1
)
}),
test('await only inside async fns', () => {
const parse_result = do_parse('function x() { await 1 }')
assert_equal(parse_result.ok, false)
}),
test('identifier has already been declared', () => {
const code = `
const x = 1
const x = 2
`
const i = test_initial_state(code)
assert_equal(i.parse_result.ok, false)
assert_equal(
i.parse_result.problems,
[
{
index: code.indexOf('x = 2'),
length: 1,
message: "Identifier 'x' has already been declared",
module: '',
}
]
)
}),
test('identifier has already been declared fn decl', () => {
const code = `
const x = 1
function x() {
}
`
const i = test_initial_state(code)
assert_equal(i.parse_result.ok, false)
assert_equal(
i.parse_result.problems,
[
{
index: code.indexOf('function x()'),
length: 1,
message: "Identifier 'x' has already been declared",
module: '',
}
]
)
}),
test('identifier has already been declared export', () => {
const code = `
export const x = 1
function x() {
}
`
const i = test_initial_state(code)
assert_equal(i.parse_result.ok, false)
assert_equal(
i.parse_result.problems,
[
{
index: code.indexOf('function x()'),
length: 1,
message: "Identifier 'x' has already been declared",
module: '',
}
]
)
}),
test('identifier has already been declared import', () => {
const code = {
'': `
import {x} from 'x.js'
function x() {
}
`,
'x.js': `
export const x = 1
`
}
const i = test_initial_state(code)
assert_equal(i.parse_result.ok, false)
assert_equal(
i.parse_result.problems,
[
{
index: code[''].indexOf('function x()'),
length: 1,
message: "Identifier 'x' has already been declared",
module: '',
}
]
)
}),
test('function decl', () => {
const code = `
function fib(n) {
@@ -1265,17 +1358,6 @@ export const tests = [
assert_equal(s2.active_calltree_node.value, 5)
}),
/*
test('await only in async', () => {
const code = `
() => {
await 1
}
`
console.log(do_parse(code).problems[0])
//return assert_equal(do_parse(code).problems[0].message, 'undeclared identifier: x')
}),
*/
/*
TODO use before assignment