From 34639b0ae3926fe40ab649d509fb0bb084b7cc35 Mon Sep 17 00:00:00 2001 From: Dmitry Vasilev Date: Mon, 5 Jun 2023 12:08:20 +0300 Subject: [PATCH] default import and export --- src/ast_utils.js | 26 ++------------- src/cmd.js | 17 +++++++--- src/eval.js | 41 +++++++++++++++-------- src/find_definitions.js | 33 +++++++++--------- src/parse_js.js | 74 +++++++++++++++++++++++++++++------------ test/test.js | 31 +++++++++++++++++ 6 files changed, 142 insertions(+), 80 deletions(-) diff --git a/src/ast_utils.js b/src/ast_utils.js index 375603b..918c503 100644 --- a/src/ast_utils.js +++ b/src/ast_utils.js @@ -41,34 +41,12 @@ export const map_destructuring_identifiers = (node, mapper) => { } export const collect_imports = module => { - const import_statements = + return uniq( module.stmts .filter(n => n.type == 'import') .filter(n => !n.is_external) - - const imported_modules = uniq( - import_statements.map(n => n.full_import_path) + .map(n => n.full_import_path) ) - - const imports = - import_statements - .map(n => - n.imports.map(i => - ({name: i.value, module: n.full_import_path}) - ) - ) - .flat() - - const by_module = Object.fromEntries(imported_modules.map(m => - [ - m, - imports - .filter(i => i.module == m) - .map(i => i.name) - ] - )) - - return by_module } export const collect_external_imports = modules => diff --git a/src/cmd.js b/src/cmd.js index bb18aea..9ced303 100644 --- a/src/cmd.js +++ b/src/cmd.js @@ -1,12 +1,11 @@ -import {map_object, filter_object, collect_nodes_with_parents, uniq} +import {map_object, map_find, filter_object, collect_nodes_with_parents, uniq} from './utils.js' import { is_eq, is_child, ancestry, ancestry_inc, map_tree, find_leaf, find_fn_by_location, find_node, find_error_origin_node, - collect_external_imports + collect_external_imports, collect_destructuring_identifiers } from './ast_utils.js' import {load_modules} from './parse_js.js' -import {find_export} from './find_definitions.js' import {eval_modules} from './eval.js' import { root_calltree_node, root_calltree_module, make_calltree, @@ -555,7 +554,17 @@ const goto_definition = (state, index) => { } else { let loc if(d.module != null) { - const exp = find_export(node.value, state.parse_result.modules[d.module]) + const exp = map_find(state.parse_result.modules[d.module].stmts, n => { + if(n.type != 'export') { + return null + } + if(n.is_default && d.is_default) { + return n.children[0] + } else if(!n.is_default && !d.is_default) { + const ids = collect_destructuring_identifiers(n.binding.name_node) + return ids.find(i => i.value == node.value) + } + }) loc = {module: d.module, index: exp.index} } else { loc = {module: state.current_module, index: d.index} diff --git a/src/eval.js b/src/eval.js index 56c91ad..4283531 100644 --- a/src/eval.js +++ b/src/eval.js @@ -261,18 +261,25 @@ ${JSON.stringify(errormessage)}, true)` } 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) - if(names.length == 0) { - return '' + let names, def + if(node.default_import != null) { + def = `default: ${node.default_import},` + names = node.children.slice(1).map(n => n.value) } else { - return `const {${names.join(',')}} = __cxt.modules['${node.full_import_path}'];`; + def = '' + names = node.children.map(n => n.value) } + return `const {${def} ${names.join(',')}} = __cxt.modules['${node.full_import_path}'];`; } 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(__cxt.modules['${node_cxt.module}'], {${identifiers.join(',')}});` + if(node.is_default) { + return `__cxt.modules['${node_cxt.module}'].default = ${do_codegen(node.children[0])};` + } else { + const identifiers = collect_destructuring_identifiers(node.binding.name_node) + .map(i => i.value) + return do_codegen(node.binding) + + + `Object.assign(__cxt.modules['${node_cxt.module}'], {${identifiers.join(',')}});` + } } else if(node.type == 'function_decl') { const expr = node.children[0] return `const ${expr.name} = ${codegen_function_expr(expr, node_cxt)};` @@ -991,12 +998,20 @@ const eval_statement = (s, scope, calls, context) => { node: {...s, children: [node], result: {ok: node.result.ok}} } } else if(s.type == 'import') { - const children = s.imports.map(i => ( - {...i, - result: {ok: true, value: context.modules[s.full_import_path][i.value]} + const module = context.modules[s.full_import_path] + const children = s.children.map((imp, i) => ( + {...imp, + result: { + ok: true, + value: imp.definition.is_default + ? module['default'] + : module[imp.value] + } } )) - const imported_scope = Object.fromEntries(children.map(i => [i.value, i.result.value])) + const imported_scope = Object.fromEntries( + children.map(imp => [imp.value, imp.result.value]) + ) return { ok: true, scope: {...scope, ...imported_scope}, diff --git a/src/find_definitions.js b/src/find_definitions.js index 0e3f924..88564b5 100644 --- a/src/find_definitions.js +++ b/src/find_definitions.js @@ -22,7 +22,7 @@ const map_find_definitions = (nodes, mapper) => { const scope_from_node = n => { if(n.type == 'import') { return Object.fromEntries( - n.imports.map(i => [i.value, i]) + n.children.map(i => [i.value, i]) ) } else if(n.type == 'export'){ return scope_from_node(n.binding) @@ -137,7 +137,13 @@ export const find_definitions = (ast, scope = {}, closure_scope = {}, module_nam 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}})) + children = ast.children.map((c, i) => ({ + ...c, + definition: { + module: full_import_path, + is_default: i == 0 && ast.default_import != null, + } + })) } else if(ast.type == 'const') { children = [add_trivial_definition(ast.name_node), ...ast.children.slice(1)] } else if(ast.type == 'let') { @@ -162,16 +168,6 @@ export const find_definitions = (ast, scope = {}, closure_scope = {}, module_nam } } -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) - }) -} - const BASE = 'dummy://dummy/' const concat_path = (base, i) => { const result = new URL(i, BASE + base).toString() @@ -184,11 +180,9 @@ const concat_path = (base, i) => { 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)), - [] - ) + return collect_imports(modules[module]) + .map(m => sort_module_deps(m)) + .flat() .concat(module) } @@ -235,6 +229,8 @@ export const check_imports = modules => { .reduce( (imports, n) => [ ...imports, + // TODO imports + // TODO use flatmap ...(n.imports.map(i => ({name: i.value, from: n.module}))) ], [] @@ -245,7 +241,8 @@ export const check_imports = modules => { .reduce((all, current) => [...all, ...current], []) return {imports, exports} - //TODO check for each import, there is export + // TODO check for each import, there is export. For default import there is + // default export }) // Topological sort // For each module diff --git a/src/parse_js.js b/src/parse_js.js index 265d50a..a541abd 100644 --- a/src/parse_js.js +++ b/src/parse_js.js @@ -1226,20 +1226,34 @@ const import_statement = optional(by_type('pragma_external')), seq([ literal('import'), + // TODO import can have both named import and default import, + // like 'import foo, {bar} from "module"' optional( - seq([ - list( - ['{', '}'], + seq_select(0, [ + either( + list( + ['{', '}'], + identifier, + ), identifier, ), literal('from'), - ]) + ]), ), string_literal ]) ]), ({value: [pragma_external, imp]}) => { - const {value: [_import, identifiers, module], ...node} = imp + const {value: [_import, imports, module], ...node} = imp + let default_import, children + if(imports == null) { + children = [] + } else if(imports.value.type == 'identifier') { + default_import = imports.value.value + children = [imports.value] + } else { + children = imports.value.value + } // remove quotes const module_string = module.value.slice(1, module.value.length - 1) // if url starts with protocol, then it is always external @@ -1252,25 +1266,44 @@ const import_statement = // TODO refactor hanlding of string literals. Drop quotes from value and // fix codegen for string_literal module: module_string, - children: identifiers == null ? [] : identifiers.value[0].value, + default_import, + children, } } ) const export_statement = - if_ok( - seq_select(1, [ - literal('export'), - // TODO export let statement, export default, etc. Or not allow - // let statement because export let cannot be compiled properly? - const_statement, - ]), - ({value, ...node}) => ({ - ...node, - not_evaluatable: true, - type: 'export', - children: [value], - }) + either( + if_ok( + seq_select(1, [ + literal('export'), + // TODO export let statement, export default, etc. Or not allow + // let statement because export let cannot be compiled properly? + const_statement, + ]), + ({value, ...node}) => ({ + ...node, + not_evaluatable: true, + type: 'export', + is_default: false, + children: [value], + }) + ), + if_ok( + seq_select(2, [ + literal('export'), + literal('default'), + expr, + ]), + ({value, ...node}) => ({ + ...node, + not_evaluatable: true, + type: 'export', + is_default: true, + children: [value], + }) + ), + ) @@ -1479,7 +1512,6 @@ const update_children_not_rec = (node, children = node.children) => { } } else if(node.type == 'import') { return {...node, - imports: children, is_statement: true, } } else if(node.type == 'export') { @@ -1685,7 +1717,7 @@ const do_load_modules = (module_names, loader, already_loaded) => { const deps = uniq( Object.values(modules) - .map(m => Object.keys(collect_imports(m))) + .map(m => collect_imports(m)) .flat() ) diff --git a/test/test.js b/test/test.js index 37141d9..290919b 100644 --- a/test/test.js +++ b/test/test.js @@ -1145,6 +1145,37 @@ export const tests = [ assert_equal(state.parse_result.ok, true) }), + test('modules default export', () => { + const modules = { + '' : "import foo from 'foo'; foo", + 'foo': `export default 1` + } + assert_code_evals_to(modules , 1) + + const i = test_initial_state(modules) + const s = COMMANDS.goto_definition(i, modules[''].indexOf('foo')).state + assert_equal(current_cursor_position(s), modules['foo'].indexOf('1')) + assert_equal(s.current_module, 'foo') + }), + + test('modules default import', () => { + const code = ` + // external + import foo from 'foo.js' + foo + ` + const initial = test_initial_state(code) + + const next = COMMANDS.external_imports_loaded(initial, initial, { + 'foo.js': { + ok: true, + module: { + 'default': 'foo_value' + }, + } + }) + assert_equal(active_frame(next).children.at(-1).result.value, 'foo_value') + }), // Static analysis