Refactor const, let, assignment

allow multiple const decls and multiple assignments
This commit is contained in:
Dmitry Vasilev
2023-10-30 12:51:15 +08:00
parent 487ec28b1c
commit 89c4728181
6 changed files with 361 additions and 238 deletions

View File

@@ -13,9 +13,11 @@ export const collect_destructuring_identifiers = node => {
['array_destructuring', 'object_destructuring', 'function_args'] ['array_destructuring', 'object_destructuring', 'function_args']
.includes(node.type) .includes(node.type)
) { ) {
return node.elements return node.children
.map(collect_destructuring_identifiers) .map(collect_destructuring_identifiers)
.flat() .flat()
} else if (node.type == 'decl_pair') {
return collect_destructuring_identifiers(node.children[0])
} else { } else {
console.error(node) console.error(node)
throw new Error('not implemented') throw new Error('not implemented')

View File

@@ -539,7 +539,9 @@ const goto_definition = (state, index) => {
if(n.is_default && d.is_default) { if(n.is_default && d.is_default) {
return n.children[0] return n.children[0]
} else if(!n.is_default && !d.is_default) { } else if(!n.is_default && !d.is_default) {
const ids = collect_destructuring_identifiers(n.binding.name_node) const ids = n.binding.children.flatMap(c =>
collect_destructuring_identifiers(c.name_node)
)
return ids.find(i => i.value == node.value) return ids.find(i => i.value == node.value)
} }
}) })
@@ -592,6 +594,71 @@ const filter_calltree = (calltree, pred) => {
} }
*/ */
const get_stmt_value_explorer = (state, stmt) => {
if(stmt.result == null) {
// statement was not evaluated
return null
}
let result
if(stmt.result.ok) {
if(stmt.type == 'return') {
result = stmt.children[0].result
} else if(['let', 'const', 'assignment'].includes(stmt.type)) {
const identifiers = stmt
.children
.flatMap(
collect_destructuring_identifiers
)
.filter(id => id.result != null)
.map(id => [id.value, id.result.value])
let value
if(
stmt.children.length == 1
&&
(
stmt.children[0].type == 'identifier'
||
stmt.children[0].type == 'decl_pair'
&&
stmt.children[0].name_node.type == 'identifier'
)
) {
// Just a single declaration
if(identifiers.length != 1) {
throw new Error('illegal state')
}
value = identifiers[0][1]
} else {
value = Object.fromEntries(identifiers)
}
return {
index: stmt.index,
length: stmt.length,
result: {ok: true, value},
}
} else if(stmt.type == 'if'){
return null
} else if(stmt.type == 'import'){
result = {
ok: true,
value: state.modules[stmt.full_import_path],
}
} else if (stmt.type == 'export') {
return get_stmt_value_explorer(state, stmt.children[0])
} else {
result = stmt.result
}
} else {
result = find_error_origin_node(stmt).result
}
return {index: stmt.index, length: stmt.length, result}
}
const get_value_explorer = (state, index) => { const get_value_explorer = (state, index) => {
if( if(
state.active_calltree_node == null state.active_calltree_node == null
@@ -677,49 +744,8 @@ const get_value_explorer = (state, index) => {
const do_node = anc[do_index] const do_node = anc[do_index]
const stmt = anc[do_index - 1] const stmt = anc[do_index - 1]
if(stmt.result == null) { return get_stmt_value_explorer(state, stmt)
// statement was not evaluated
return null
}
let result
if(stmt.result.ok) {
if(['const', 'assignment'].includes(stmt.type)) {
result = stmt.children[1].result
} else if(stmt.type == 'return') {
result = stmt.children[0].result
} else if(stmt.type == 'let') {
return {
index: stmt.index,
length: stmt.length,
result:
{
ok: true,
value: Object.fromEntries(
stmt.children.map(c =>
[c.value, c.result.value]
)
)
}
}
} else if(stmt.type == 'if'){
return null
} else if(stmt.type == 'import'){
result = {
ok: true,
value: state.modules[stmt.full_import_path],
}
} else if (stmt.type == 'export') {
result = stmt.children[0].children[1].result
} else {
result = stmt.result
}
} else {
result = find_error_origin_node(stmt).result
}
return {index: stmt.index, length: stmt.length, result}
} }
const do_move_cursor = (state, index) => { const do_move_cursor = (state, index) => {

View File

@@ -217,32 +217,49 @@ const codegen = (node, node_cxt, parent) => {
+ branches[0] + branches[0]
+'\n: ' +'\n: '
+ branches[1] + branches[1]
} else if(node.type == 'const'){ } else if(['const', 'let', 'assignment'].includes(node.type)) {
const res = 'const ' + do_codegen(node.name_node) + ' = ' + do_codegen(node.expr, node) + ';' const prefix = node.type == 'assignment'
if(node.name_node.type == 'identifier' && node.expr.type == 'function_call') { ? ''
// deduce function name from variable it was assigned to if anonymous : node.type + ' '
// works for point-free programming, like return prefix + node
// const parse_statement = either(parse_import, parse_assignment, ...) .children
return res + ` .map(c => c.type == 'identifier'
? c.value
: do_codegen(c.children[0]) + ' = ' + do_codegen(c.children[1])
)
.join(',')
+ ';'
+ node.children.map(decl => {
if( if(
typeof(${node.name_node.value}) == 'function' node.type != 'assignment'
&& &&
${node.name_node.value}.name == 'anonymous' decl.type != 'identifier'
&&
decl.name_node.type == 'identifier'
&&
decl.expr.type == 'function_call'
) { ) {
Object.defineProperty( // deduce function name from variable it was assigned to if anonymous
${node.name_node.value}, // works for point-free programming, like
"name", // const parse_statement = either(parse_import, parse_assignment, ...)
{value: "${node.name_node.value}"} return `
); if(
typeof(${decl.name_node.value}) == 'function'
&&
${decl.name_node.value}.name == 'anonymous'
) {
Object.defineProperty(
${decl.name_node.value},
"name",
{value: "${decl.name_node.value}"}
);
}
`
} else {
return ''
} }
` })
} else { .join('')
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'){ } else if(node.type == 'member_access'){
return '(' return '('
+ do_codegen(node.object) + do_codegen(node.object)
@@ -295,7 +312,10 @@ ${JSON.stringify(errormessage)}, true)`
if(node.is_default) { if(node.is_default) {
return `__cxt.modules['${node_cxt.module}'].default = ${do_codegen(node.children[0])};` return `__cxt.modules['${node_cxt.module}'].default = ${do_codegen(node.children[0])};`
} else { } else {
const identifiers = collect_destructuring_identifiers(node.binding.name_node) const identifiers = node
.binding
.children
.flatMap(n => collect_destructuring_identifiers(n.name_node))
.map(i => i.value) .map(i => i.value)
return do_codegen(node.binding) return do_codegen(node.binding)
+ +
@@ -805,8 +825,8 @@ const eval_frame_expr = (node, scope, callsleft, context) => {
} }
} }
const apply_assignments = (do_node, assignments) => { const apply_assignments = (node, assignments) => {
const let_ids = do_node const let_ids = node
.children .children
.filter(c => c.type == 'let') .filter(c => c.type == 'let')
.map(l => l.children) .map(l => l.children)
@@ -823,34 +843,99 @@ const apply_assignments = (do_node, assignments) => {
.map(([k, {name, value}]) => [name, value]) .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} return {node, scope}
} }
const eval_decl_pair = (s, scope, calls, context, is_assignment) => {
if(s.type != 'decl_pair') {
throw new Error('illegal state')
}
// TODO default values for destructuring can be function calls
const {node, calls: next_calls}
= eval_frame_expr(s.expr, scope, calls, context)
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, is_error_origin: true}},
scope,
calls,
}
}
return {
ok: true,
node: {...s_evaled, result: {ok: true}},
scope: {...scope, ...next_scope},
calls: next_calls,
assignments: is_assignment
? Object.fromEntries(
name_nodes.map(n => [
n.definition.index,
{
value: next_scope[n.value],
name: n.value,
}
])
)
: null
}
}
const eval_statement = (s, scope, calls, context) => { const eval_statement = (s, scope, calls, context) => {
if(s.type == 'do') { if(s.type == 'do') {
const node = s const stmt = s
// hoist function decls to the top // hoist function decls to the top
const function_decls = node.children const function_decls = s.children
.filter(s => s.type == 'function_decl') .filter(s => s.type == 'function_decl')
.map(s => { .map(s => {
const {ok, children, calls: next_calls} = eval_children(s, scope, calls, context) const {ok, children, calls: next_calls} = eval_children(s, scope, calls, context)
@@ -872,7 +957,7 @@ const eval_statement = (s, scope, calls, context) => {
const initial_scope = {...scope, ...hoisted_functions_scope} const initial_scope = {...scope, ...hoisted_functions_scope}
const {ok, assignments, returned, children, calls: next_calls} = node.children.reduce( const {ok, assignments, returned, children, calls: next_calls} = s.children.reduce(
({ok, returned, children, scope, calls, assignments}, s) => { ({ok, returned, children, scope, calls, assignments}, s) => {
if(returned || !ok) { if(returned || !ok) {
return {ok, returned, scope, calls, children: [...children, s], assignments} return {ok, returned, scope, calls, children: [...children, s], assignments}
@@ -909,7 +994,7 @@ const eval_statement = (s, scope, calls, context) => {
{ok: true, returned: false, children: [], scope: initial_scope, calls, assignments: {}} {ok: true, returned: false, children: [], scope: initial_scope, calls, assignments: {}}
) )
const {node: next_node, scope: next_scope} = const {node: next_node, scope: next_scope} =
apply_assignments({...node, children: children, result: {ok}}, assignments) apply_assignments({...s, children: children, result: {ok}}, assignments)
return { return {
ok, ok,
node: next_node, node: next_node,
@@ -918,86 +1003,46 @@ const eval_statement = (s, scope, calls, context) => {
assignments, assignments,
calls: next_calls, calls: next_calls,
} }
} else if(s.type == 'const' || s.type == 'assignment') { } else if(['let', 'const', 'assignment'].includes(s.type)) {
// TODO default values for destructuring can be function calls const stmt = s
const {node, calls: next_calls} const initial = {ok: true, children: [], scope, calls, assignments: null}
= eval_frame_expr(s.expr, scope, calls, context)
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 {ok, children, calls: next_calls, scope: next_scope, assignments} = s.children.reduce(
const names = name_nodes.map(n => n.value) ({ok, children, scope, calls, assignments}, s) => {
const destructuring = codegen(s.name_node) if(!ok) {
return {ok, scope, calls, children: [...children, s]}
// TODO unique name for __value (gensym) }
const codestring = ` if(stmt.type == 'let' && s.type == 'identifier') {
const ${destructuring} = __value; const node = {...s, result: {ok: true}}
({${names.join(',')}}); return {ok, children: [...children, node], scope, calls}
` }
const {ok, value: next_scope, error} = eval_codestring( const {
codestring, ok: next_ok,
{...scope, __value: node.result.value} node,
scope: nextscope,
calls: next_calls,
assignments: next_assignments,
} = eval_decl_pair(s, scope, calls, context, stmt.type == 'assignment')
return {
ok: next_ok,
scope: nextscope,
calls: next_calls,
children: [...children, node],
assignments: stmt.type == 'assignment'
? {...assignments, ...next_assignments}
: null
}
},
initial
) )
// 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, is_error_origin: true}},
scope,
calls,
}
}
return { return {
ok: true, ok,
node: {...s_evaled, result: {ok: true}}, node: {...s, children, result: {ok}},
scope: {...scope, ...next_scope}, scope: {...scope, ...next_scope},
calls: next_calls, calls: next_calls,
assignments: s.type == 'assignment' assignments,
? Object.fromEntries(
name_nodes.map(n => [
n.definition.index,
{
value: next_scope[n.value],
name: n.value,
}
])
)
: null
} }
} else if(s.type == 'return') { } else if(s.type == 'return') {
const {node, calls: next_calls} = const {node, calls: next_calls} =
@@ -1121,10 +1166,6 @@ const eval_statement = (s, scope, calls, context) => {
} }
} }
} else if(s.type == 'let') {
return { ok: true, node: s, scope, calls }
} else if(s.type == 'throw') { } else if(s.type == 'throw') {
const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls, context) const {node, calls: next_calls} = eval_frame_expr(s.expr, scope, calls, context)

View File

@@ -26,11 +26,13 @@ const scope_from_node = n => {
) )
} else if(n.type == 'export'){ } else if(n.type == 'export'){
return scope_from_node(n.binding) return scope_from_node(n.binding)
} else if(n.type == 'const' || n.type == 'let'){ } else if(n.type == 'let' || n.type == 'const') {
return Object.fromEntries( return Object.fromEntries(
collect_destructuring_identifiers(n.name_node).map(node => [ n.children
node.value, node .flatMap(collect_destructuring_identifiers)
]) .map(node => [
node.value, node
])
) )
} else if(n.type == 'function_decl') { } else if(n.type == 'function_decl') {
// Return null because of hoisting. We take function decls into account // Return null because of hoisting. We take function decls into account
@@ -55,6 +57,11 @@ const add_trivial_definition = node => {
]} ]}
} else if(['array_destructuring', 'object_destructuring'].includes(node.type)) { } else if(['array_destructuring', 'object_destructuring'].includes(node.type)) {
return {...node, children: node.children.map(add_trivial_definition)} return {...node, children: node.children.map(add_trivial_definition)}
} else if (node.type == 'decl_pair') {
return {
...node,
children: node.children.with(0, add_trivial_definition(node.children[0]))
}
} else { } else {
console.error(node) console.error(node)
throw new Error('not implemented') throw new Error('not implemented')
@@ -150,10 +157,8 @@ export const find_definitions = (ast, globals, scope = {}, closure_scope = {}, m
is_default: i == 0 && ast.default_import != null, is_default: i == 0 && ast.default_import != null,
} }
})) }))
} else if(ast.type == 'const') { } else if(ast.type == 'const' || ast.type == 'let') {
children = [add_trivial_definition(ast.name_node), ...ast.children.slice(1)] children = ast.children.map(add_trivial_definition)
} else if(ast.type == 'let') {
children = ast.name_node.map(add_trivial_definition)
} else { } else {
children = ast.children children = ast.children
} }

View File

@@ -1091,55 +1091,47 @@ const function_decl = if_ok(
node => ({...node, type: 'function_decl', children: [node]}) node => ({...node, type: 'function_decl', children: [node]})
) )
// TODO multiple decls, like `const x = 1, y = 2` const decl_pair = if_ok(
const const_statement = seq([destructuring, literal('='), expr]),
if_ok( ({value, ...node}) => {
seq([ const [lefthand, _eq, expr] = value
literal('const'), return {
destructuring, ...node,
literal('='), type: 'decl_pair',
expr, not_evaluatable: true,
]), children: [lefthand, expr],
({value, ...node}) => {
const [_const, identifier, _eq, expr] = value
return {
...node,
type: 'const',
name: identifier.value,
children: [identifier, expr],
}
} }
) }
)
const let_declaration = if_ok( const const_or_let = is_const => if_ok(
seq_select(1, [ seq_select(1, [
literal('let'), literal(is_const ? 'const' : 'let'),
comma_separated_1(identifier), comma_separated_1(
is_const
? decl_pair
: either(decl_pair, identifier)
)
]), ]),
({value, ...node}) => ({ ({value, ...node}) => ({
...node, ...node,
type: 'let', type: is_const ? 'const' : 'let',
children: value.value, children: value.value,
names: value.value.map(n => n.value),
}) })
) )
const const_statement = const_or_let(true)
const let_declaration = const_or_let(false)
// TODO object assignment required braces, like ({foo} = {foo: 1})
const assignment = if_ok( const assignment = if_ok(
seq([ comma_separated_1(decl_pair),
// TODO object assignment required braces, like ({foo} = {foo: 1}) ({value, ...node}) => ({
destructuring, ...node,
literal('='), type: 'assignment',
expr, children: value,
]), })
({value, ...node}) => {
const [identifier, _eq, expr] = value
return {
...node,
type: 'assignment',
name: identifier.value,
children: [identifier, expr],
}
},
) )
const return_statement = const return_statement =
@@ -1422,21 +1414,14 @@ const update_children_not_rec = (node, children = node.children) => {
} }
} else if(node.type == 'const'){ } else if(node.type == 'const'){
return {...node, return {...node,
name_node: children[0],
expr: children[1],
is_statement: true, is_statement: true,
} }
} else if(node.type == 'let'){ } else if(node.type == 'let'){
return {...node, return {...node, is_statement: true }
name_node: children, } else if(node.type == 'decl_pair') {
is_statement: true, return {...node, expr: node.children[1], name_node: node.children[0]}
}
} else if(node.type == 'assignment'){ } else if(node.type == 'assignment'){
return {...node, return {...node, is_statement: true}
name_node: children[0],
expr: children[1],
is_statement: true,
}
} else if(node.type == 'do'){ } else if(node.type == 'do'){
return {...node, is_statement: true} return {...node, is_statement: true}
} else if(node.type == 'function_decl'){ } else if(node.type == 'function_decl'){
@@ -1568,8 +1553,10 @@ const do_deduce_fn_names = (node, parent) => {
node_result.name == null node_result.name == null
) { ) {
let name let name
if(parent?.type == 'const') { if(parent?.type == 'decl_pair') {
name = parent.name if(parent.name_node.type == 'identifier') {
name = parent.name_node.value
}
} else if(parent?.type == 'key_value_pair') { } else if(parent?.type == 'key_value_pair') {
// unwrap quotes with JSON.parse // unwrap quotes with JSON.parse
name = JSON.parse(parent.key.value) name = JSON.parse(parent.key.value)

View File

@@ -320,6 +320,15 @@ export const tests = [
) )
}), }),
test('let variable', () => {
const code = `
let x, y = 2, unused, [z,q] = [3,4]
x = 1
`
const i = test_initial_state(code, code.indexOf('x'))
assert_equal(i.value_explorer.result.value, {y: 2, z: 3, q: 4})
}),
test('else if', () => { test('else if', () => {
const code = ` const code = `
let x let x
@@ -764,7 +773,10 @@ export const tests = [
const {calltree, modules} = eval_modules(parsed); const {calltree, modules} = eval_modules(parsed);
const frame = eval_frame(calltree, modules) const frame = eval_frame(calltree, modules)
assert_equal(frame.children[1].result, {ok: true}) assert_equal(frame.children[1].result, {ok: true})
assert_equal(frame.children[1].children[0].children[1].result, {ok: true, value: 2}) assert_equal(
find_node(frame, n => n.string == 'b').result.value,
2
)
}), }),
test('eval_frame error', () => { test('eval_frame error', () => {
@@ -972,7 +984,6 @@ export const tests = [
assert_equal(result.ok, true) assert_equal(result.ok, true)
}), }),
test('module external', () => { test('module external', () => {
const code = ` const code = `
// external // external
@@ -1195,6 +1206,12 @@ export const tests = [
assert_equal(active_frame(next).children.at(-1).result.value, 'foo_value') assert_equal(active_frame(next).children.at(-1).result.value, 'foo_value')
}), }),
test('export value explorer', () => {
const code = 'export const x = 1'
const i = test_initial_state(code)
assert_equal(i.value_explorer.result.value, 1)
}),
// Static analysis // Static analysis
test('undeclared', () => { test('undeclared', () => {
@@ -1446,8 +1463,53 @@ export const tests = [
) )
// assert let has result // assert let has result
assert_equal(frame.children[0].result, {ok: true}) assert_equal(frame.children[0].result, {ok: true})
// assert x value }),
assert_equal(frame.children[0].children[0].result, {ok: true, value: 1})
test('multiple assignments', () => {
assert_code_evals_to(
`
let x, y
x = 1, {y} = {y: 2}
{x,y}
`,
{x: 1, y: 2}
)
}),
test('assigments value explorer', () => {
const code = `
let x
x = 1
`
const i = test_initial_state(code, code.indexOf('x = 1'))
assert_equal(i.value_explorer.result.value, 1)
}),
test('multiple assigments value explorer', () => {
const code = `
let x, y
x = 1, y = 2
`
const i = test_initial_state(code, code.indexOf('x = 1'))
assert_equal(i.value_explorer.result.value, {x: 1, y: 2})
}),
test('assigments destructuring value explorer', () => {
const code = `
let x, y
x = 1, {y} = {y:2}
`
const i = test_initial_state(code, code.indexOf('x = 1'))
assert_equal(i.value_explorer.result.value, {x: 1, y: 2})
}),
test('assigments error', () => {
const code = `
let x, y
x = 1, y = null.foo
`
const i = test_initial_state(code, code.indexOf('x = 1'))
assert_equal(i.value_explorer.result.ok, false)
}), }),
test('block scoping', () => { test('block scoping', () => {
@@ -2471,15 +2533,15 @@ const y = x()`
test('move_cursor let', () => { test('move_cursor let', () => {
const code = ` const code = `
let x let x = 1
x = 1
` `
const s1 = test_initial_state(code) const s1 = test_initial_state(code)
const s2 = COMMANDS.move_cursor(s1, code.indexOf('x')) const s2 = COMMANDS.move_cursor(s1, code.indexOf('x'))
const lettext = 'let x = 1'
assert_equal(s2.value_explorer, { assert_equal(s2.value_explorer, {
index: code.indexOf('let x'), index: code.indexOf(lettext),
length: 5, length: lettext.length,
result: {ok: true, value: {x: 1}}, result: {ok: true, value: 1},
}) })
}), }),