external imports

This commit is contained in:
Dmitry Vasilev
2022-10-19 03:22:48 +08:00
parent 1bea819b1a
commit 68d7a88ac3
9 changed files with 362 additions and 32 deletions

View File

@@ -43,6 +43,7 @@ export const map_destructuring_identifiers = (node, mapper) => {
export const collect_imports = module => {
const imports = module.stmts
.filter(n => n.type == 'import')
.filter(n => !n.is_external)
.map(n =>
n.imports.map(i =>
({name: i.value, module: n.full_import_path})

View File

@@ -1,4 +1,5 @@
import {map_object, pick_keys, collect_nodes_with_parents} from './utils.js'
import {map_object, pick_keys, 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
@@ -82,12 +83,97 @@ const run_code = (s, index, dirty_files) => {
calltree_node_by_loc: null,
// TODO keep selection_state?
selection_state: null,
loading_external_imports_state: null,
}
if(!state.parse_result.ok) {
return state
}
const external_import_nodes =
Object
.entries(state.parse_result.modules)
.map(([module_name, node]) =>
node
.children
.filter(c => c.type == 'import' && c.is_external)
.map(node => ({node, module_name}))
)
.flat()
const external_imports = uniq(
external_import_nodes.map(i => i.node.full_import_path)
)
if(
external_imports.length != 0
&&
(
state.external_imports_cache == null
||
external_imports.some(i => state.external_imports_cache[i] == null)
)
) {
return {...state,
loading_external_imports_state: {
index,
external_imports,
external_import_nodes,
}
}
} else {
return external_imports_loaded(
state,
state,
state.external_imports_cache,
index
)
}
}
const do_external_imports_loaded = (
state,
prev_state,
external_imports,
index,
) => {
if(
state.loading_external_imports_state
!=
prev_state.loading_external_imports_state
) {
// code was modified after loading started, discard
return state
}
if(external_imports != null) {
const errors = new Set(
Object
.entries(external_imports)
.filter(([url, result]) => !result.ok)
.map(([url, result]) => url)
)
if(errors.size != 0) {
const problems = state
.loading_external_imports_state
.external_import_nodes
.filter(({node}) => errors.has(node.full_import_path))
.map(({node, module_name}) => ({
index: node.index,
message: external_imports[node.full_import_path].error.message,
module: module_name,
}))
return {...state,
parse_result: {
ok: false,
cache: state.parse_result.cache,
problems,
}
}
}
}
const node = find_call_node(state, index)
if(
@@ -96,7 +182,11 @@ const run_code = (s, index, dirty_files) => {
||
node.type == 'do' /* toplevel AST node */
) {
const result = eval_modules(state.parse_result.modules, state.parse_result.sorted)
const result = eval_modules(
state.parse_result.modules,
state.parse_result.sorted,
external_imports
)
const next = apply_eval_result(state, result)
if(node == state.parse_result.modules[root_calltree_module(next)]) {
@@ -117,6 +207,7 @@ const run_code = (s, index, dirty_files) => {
const result = eval_modules(
state.parse_result.modules,
state.parse_result.sorted,
external_imports,
{index: node.index, module: state.current_module},
)
@@ -145,19 +236,44 @@ const run_code = (s, index, dirty_files) => {
)
}
const external_imports_loaded = (
state,
prev_state,
external_imports,
maybe_index,
) => {
// index saved in loading_external_imports_state maybe stale, if cursor was
// moved after
// TODO refactor, make index controlled property saved in state?
const index = maybe_index ?? state.loading_external_imports_state.index
// TODO after edit we should fire embed_value_explorer, but we dont do it
// here because we dont have cursor position (index) here (see comment
// above). Currently it is fixed by having `external_imports_cache`, so code
// goes async path only on first external imports load
return {
...do_external_imports_loaded(state, prev_state, external_imports, index),
external_imports_cache: external_imports,
loading_external_imports_state: null
}
}
const input = (state, code, index) => {
const files = {...state.files, [state.current_module]: code}
const next = run_code({...state, files}, index, [state.current_module])
const effects1 = next.current_module == ''
const effect_save = next.current_module == ''
? {type: 'save_to_localstorage', args: ['code', code]}
: {type: 'write', args: [
next.current_module,
next.files[next.current_module],
]}
if(next.loading_external_imports_state != null) {
return {state: next, effects: [effect_save]}
}
const {state: next2, effects: effects2} = do_move_cursor(next, index)
return {
state: next2,
effects: [effects1, effects2],
effects: [effect_save, effects2],
}
}
@@ -675,5 +791,6 @@ export const COMMANDS = {
goto_problem,
move_cursor,
eval_selection,
external_imports_loaded,
calltree: calltree_commands,
}

33
src/effects.js vendored
View File

@@ -2,6 +2,30 @@ import {write_file} from './filesystem.js'
import {color_file} from './color.js'
import {root_calltree_node, calltree_node_loc} from './calltree.js'
import {FLAGS} from './feature_flags.js'
import {exec} from './index.js'
const load_external_imports = async state => {
if(state.loading_external_imports_state == null) {
return
}
const urls = state.loading_external_imports_state.external_imports
const results = await Promise.allSettled(
urls.map(u => import(u))
)
const modules = Object.fromEntries(
results.map((r, i) => (
[
urls[i],
{
ok: r.status == 'fulfilled',
error: r.reason,
module: r.value,
}
]
))
)
exec('external_imports_loaded', state /* becomes prev_state */, modules)
}
const ensure_session = (ui, state, file = state.current_module) => {
ui.editor.ensure_session(file, state.files[file])
@@ -84,6 +108,7 @@ export const render_initial_state = (ui, state) => {
ui.render_debugger(state)
render_coloring(ui, state)
}
load_external_imports(state)
}
export const render_common_side_effects = (prev, next, command, ui) => {
@@ -111,11 +136,15 @@ export const render_common_side_effects = (prev, next, command, ui) => {
ui.editor.switch_session(next.current_module)
}
if(prev.loading_external_imports_state != next.loading_external_imports_state) {
load_external_imports(next)
}
if(prev.parse_result != next.parse_result) {
render_parse_result(ui, next)
}
if(!next.parse_result.ok) {
if(!next.parse_result.ok || next.loading_external_imports_state != null) {
ui.calltree.clear_calltree()
ui.editor.for_each_session((file, session) => clear_coloring(ui, file))
@@ -124,6 +153,7 @@ export const render_common_side_effects = (prev, next, command, ui) => {
} else {
if(
// TODO refactor this condition
prev.current_calltree_node == null
||
prev.calltree_changed_token != next.calltree_changed_token
@@ -232,5 +262,6 @@ export const EFFECTS = {
ui.eval.clear_value_or_error()
}
},
}

View File

@@ -235,9 +235,11 @@ const codegen = (node, cxt, parent) => {
}
}
export const eval_modules = (modules, sorted, location) => {
export const eval_modules = (modules, sorted, external_imports, location) => {
// TODO gensym __modules, __exports
// TODO bug if module imported twice, once as external and as regular
const codestring =
`
let children, prev_children
@@ -435,7 +437,7 @@ export const eval_modules = (modules, sorted, location) => {
}
const run = entrypoint => {
const __modules = {}
const __modules = {...external_imports}
let current_call
`
@@ -483,7 +485,13 @@ export const eval_modules = (modules, sorted, location) => {
}
`
const actions = (new Function(codestring))()
const actions = new Function('external_imports', codestring)(
external_imports == null
? null
: map_object(external_imports, (name, {module}) =>
({exports: module, is_external: true})
)
)
const calltree_actions = {
expand_calltree_node: (node) => {
@@ -524,8 +532,10 @@ export const eval_modules = (modules, sorted, location) => {
const assign_code_calltree = (modules, calltree) =>
map_object(
calltree,
(module, {calls, exports}) => {
return {exports, calls: assign_code(modules, calls, modules[module])}
(module, {calls, exports, is_external}) => {
return is_external
? {is_external, exports}
: {exports, calls: assign_code(modules, calls, modules[module])}
}
)

View File

@@ -264,6 +264,7 @@ code analysis:
- 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
- module can be imported either as external or regular
- cannot return from modules (even inside toplevel if statements)
*/
export const analyze = (node, is_toplevel = true) => {

View File

@@ -78,6 +78,9 @@ const tokenize_js = (str) => {
]
const TOKENS = [
{name: 'pragma_external' , re: '//[\\s]*external[\\s]*[\r\n]'},
{name: 'pragma_external' , re: '\\/\\*[\\s]*external[\\s]*\\*\\/'},
{name: 'comment' , re: '//[^\n]*'},
{name: 'comment' , re: '\\/\\*[\\s\\S]*?\\*\\/'},
{name: 'newline' , re: '[\r\n\]+'},
@@ -187,6 +190,8 @@ const current = cxt => cxt.current < cxt.tokens.length
const literal = str => by_pred(token => token.string == str, 'expected ' + str)
const insignificant_types = new Set(['newline', 'pragma_external'])
const by_pred = (pred, error) => {
const result = cxt => {
const token = current(cxt)
@@ -203,7 +208,8 @@ const by_pred = (pred, error) => {
}
}
if(token.type == 'newline') {
// Skip non-significant tokens if they were not asked for explicitly
if(insignificant_types.has(token.type)) {
return result({...cxt, current: cxt.current + 1})
}
@@ -225,7 +231,7 @@ export const eof = cxt => {
if(c == null) {
return {ok: true, cxt}
}
if(c.type == 'newline') {
if(insignificant_types.has(c.type)) {
return eof({...cxt, current: cxt.current + 1})
}
return {ok: false, error: 'unexpected token, expected eof', cxt}
@@ -1169,23 +1175,30 @@ const import_statement =
// TODO import *, import as
if_ok(
seq([
literal('import'),
list(
['{', '}'],
identifier,
),
literal('from'),
string_literal
optional(by_type('pragma_external')),
seq([
literal('import'),
list(
['{', '}'],
identifier,
),
literal('from'),
string_literal
])
]),
({value: [_0, identifiers, _2, module], ...node}) => ({
...node,
not_evaluatable: true,
type: 'import',
// TODO refactor hanlding of string literals. Drop quotes from value and
// fix codegen for string_literal
module: module.value.slice(1, module.value.length - 1),
children: identifiers.value,
})
({value: [external, imp]}) => {
const {value: [_import, identifiers, _from, module], ...node} = imp
return {
...node,
not_evaluatable: true,
type: 'import',
is_external: external != null,
// TODO refactor hanlding of string literals. Drop quotes from value and
// fix codegen for string_literal
module: module.value.slice(1, module.value.length - 1),
children: identifiers.value,
}
}
)
const export_statement =