Backup commit
My backspace key started ghosting. Nothing works atm.
This commit is contained in:
@@ -1,4 +1,2 @@
|
||||
mod resolve_aliases;
|
||||
pub mod resolve_aliases;
|
||||
mod walk_with_links;
|
||||
|
||||
pub use resolve_aliases::resolve_aliases;
|
||||
|
||||
@@ -1,95 +1,174 @@
|
||||
use std::iter;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use intern_all::Tok;
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::walk_with_links::walk_with_links;
|
||||
use crate::ast::{Expr, Rule};
|
||||
use crate::representations::project::{
|
||||
ItemKind, ProjectExt, ProjectItem, ProjectMod,
|
||||
use crate::error::{ErrorPosition, ProjectError, ProjectResult};
|
||||
use crate::location::CodeLocation;
|
||||
use crate::name::{Sym, VPath};
|
||||
use crate::parse::parsed::Expr;
|
||||
use crate::pipeline::project::{
|
||||
ItemKind, ProjItem, ProjRule, ProjXEnt, ProjXMod, ProjectMod, SourceModule,
|
||||
};
|
||||
use crate::tree::{ModEntry, ModMember, Module};
|
||||
use crate::utils::pure_seq::pushed;
|
||||
use crate::{Interner, ProjectTree, Tok, VName};
|
||||
use crate::utils::pure_seq::with_pushed;
|
||||
|
||||
#[must_use]
|
||||
fn resolve_aliases_rec(
|
||||
root: &ProjectMod<VName>,
|
||||
module: &ProjectMod<VName>,
|
||||
updated: &impl Fn(&[Tok<String>]) -> bool,
|
||||
is_root: bool,
|
||||
) -> ProjectMod<VName> {
|
||||
if !is_root && !updated(&module.extra.path) {
|
||||
return module.clone();
|
||||
#[derive(Clone)]
|
||||
struct NotFound {
|
||||
location: CodeLocation,
|
||||
last_stop: VPath,
|
||||
bad_step: Tok<String>,
|
||||
}
|
||||
impl ProjectError for NotFound {
|
||||
const DESCRIPTION: &'static str = "A path pointed out of the tree";
|
||||
fn message(&self) -> String {
|
||||
format!("{} doesn't contain {}", self.last_stop, self.bad_step)
|
||||
}
|
||||
let process_expr = |expr: &Expr<VName>| {
|
||||
expr
|
||||
.map_names(&|n| {
|
||||
let full_name = (module.extra.path.iter()).chain(n.iter()).cloned();
|
||||
match walk_with_links(root, full_name, false) {
|
||||
Ok(rep) => Some(rep.abs_path),
|
||||
Err(e) => {
|
||||
let leftovers = e.tail.collect::<Vec<_>>();
|
||||
if !leftovers.is_empty() {
|
||||
let full_name = (module.extra.path.iter())
|
||||
.chain(n.iter())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let _ = walk_with_links(root, full_name.iter().cloned(), true);
|
||||
panic!(
|
||||
"Invalid path {} while resolving {} should have been noticed \
|
||||
earlier",
|
||||
(e.abs_path.into_iter())
|
||||
.chain(iter::once(e.name))
|
||||
.chain(leftovers.into_iter())
|
||||
.join("::"),
|
||||
Interner::extern_all(&full_name).join("::"),
|
||||
);
|
||||
}
|
||||
Some(pushed(e.abs_path, e.name))
|
||||
},
|
||||
fn one_position(&self) -> CodeLocation { self.location.clone() }
|
||||
}
|
||||
|
||||
struct NameErrors(Vec<NotFound>);
|
||||
impl ProjectError for NameErrors {
|
||||
const DESCRIPTION: &'static str = "Some symbols were missing";
|
||||
fn positions(&self) -> impl IntoIterator<Item = ErrorPosition> {
|
||||
self.0.clone().into_iter().map(|nf| ErrorPosition {
|
||||
location: nf.one_position(),
|
||||
message: Some(nf.message()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_name(
|
||||
name: Sym,
|
||||
root: &ProjectMod,
|
||||
path: &[Tok<String>],
|
||||
location: CodeLocation,
|
||||
env: &Module<impl Sized, impl Sized, impl Sized>,
|
||||
) -> Result<Option<Sym>, NotFound> {
|
||||
let full_name = path.iter().chain(&name[..]).cloned().collect_vec();
|
||||
match walk_with_links(root, full_name.clone().into_iter()) {
|
||||
Ok(rep) => Ok(Some(rep.abs_path.to_sym())),
|
||||
Err(mut e) => match e.tail.next() {
|
||||
// If it got stuck on the very last step, allow it through for
|
||||
// now in case it is a binding. If the name doesn't get bound, by
|
||||
// macros it will be raised at the postmacro check.
|
||||
None => Ok(Some(e.consumed_path().to_sym())),
|
||||
Some(step) => {
|
||||
// If there's more, rebuild the last full path after redirects and
|
||||
// try to resolve it on the env tree. The env tree doesn't contain
|
||||
// redirects so a plain tree walk is enough.
|
||||
let fallback_path = (e.abs_path.iter())
|
||||
.chain(iter::once(&e.name))
|
||||
.cloned()
|
||||
.chain(iter::once(step))
|
||||
.chain(e.tail)
|
||||
.collect_vec();
|
||||
let valid_in_env = env.walk1_ref(&[], &fallback_path, |_| true).is_ok();
|
||||
match valid_in_env {
|
||||
false => Err(NotFound {
|
||||
location,
|
||||
last_stop: VPath(e.abs_path),
|
||||
bad_step: e.name,
|
||||
}),
|
||||
true => Ok(e.aliased.then(|| {
|
||||
Sym::new(fallback_path).expect("Not empty by construction")
|
||||
})),
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| expr.clone())
|
||||
};
|
||||
Module {
|
||||
extra: ProjectExt {
|
||||
path: module.extra.path.clone(),
|
||||
file: module.extra.file.clone(),
|
||||
imports_from: module.extra.imports_from.clone(),
|
||||
rules: (module.extra.rules.iter())
|
||||
.map(|Rule { pattern, prio, template }| Rule {
|
||||
pattern: pattern.iter().map(process_expr).collect(),
|
||||
template: template.iter().map(process_expr).collect(),
|
||||
prio: *prio,
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
entries: module
|
||||
.entries
|
||||
.iter()
|
||||
.map(|(k, v)| {
|
||||
(k.clone(), ModEntry {
|
||||
exported: v.exported,
|
||||
}
|
||||
}
|
||||
|
||||
fn process_expr(
|
||||
expr: &Expr,
|
||||
root: &ProjectMod,
|
||||
path: &[Tok<String>],
|
||||
errors: &mut Vec<NotFound>,
|
||||
env: &Module<impl Sized, impl Sized, impl Sized>,
|
||||
) -> Expr {
|
||||
let location = CodeLocation::Source(expr.range.clone());
|
||||
expr
|
||||
.map_names(&mut |n| {
|
||||
resolve_name(n, root, path, location.clone(), env).unwrap_or_else(|e| {
|
||||
errors.push(e);
|
||||
None
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| expr.clone())
|
||||
}
|
||||
|
||||
fn resolve_aliases_rec(
|
||||
root: &ProjectMod,
|
||||
path: &mut Vec<Tok<String>>,
|
||||
module: &ProjectMod,
|
||||
env: &Module<impl Sized, impl Sized, impl Sized>,
|
||||
) -> ProjectResult<ProjectMod> {
|
||||
let mut errors = Vec::new();
|
||||
let module = Module {
|
||||
x: ProjXMod {
|
||||
src: module.x.src.as_ref().map(|s| SourceModule {
|
||||
range: s.range.clone(),
|
||||
rules: (s.rules.iter())
|
||||
.map(|ProjRule { pattern, prio, template, comments }| ProjRule {
|
||||
pattern: (pattern.iter())
|
||||
.map(|e| process_expr(e, root, path, &mut errors, env))
|
||||
.collect(),
|
||||
template: (template.iter())
|
||||
.map(|e| process_expr(e, root, path, &mut errors, env))
|
||||
.collect(),
|
||||
comments: comments.clone(),
|
||||
prio: *prio,
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
},
|
||||
entries: (module.entries.iter())
|
||||
.map(|(k, v)| -> ProjectResult<_> {
|
||||
Ok((k.clone(), ModEntry {
|
||||
x: ProjXEnt {
|
||||
exported: v.x.exported,
|
||||
comments: v.x.comments.clone(),
|
||||
locations: v.x.locations.clone(),
|
||||
},
|
||||
member: match &v.member {
|
||||
ModMember::Sub(module) =>
|
||||
ModMember::Sub(resolve_aliases_rec(root, module, updated, false)),
|
||||
ModMember::Item(item) => ModMember::Item(ProjectItem {
|
||||
ModMember::Sub(module) => {
|
||||
let m = with_pushed(path, k.clone(), |p| {
|
||||
resolve_aliases_rec(root, p, module, env)
|
||||
});
|
||||
ModMember::Sub(m.1?)
|
||||
},
|
||||
ModMember::Item(item) => ModMember::Item(ProjItem {
|
||||
kind: match &item.kind {
|
||||
ItemKind::Const(value) => ItemKind::Const(process_expr(value)),
|
||||
other => other.clone(),
|
||||
ItemKind::Const(v) => {
|
||||
let v = process_expr(v, root, path, &mut errors, env);
|
||||
ItemKind::Const(v)
|
||||
},
|
||||
ItemKind::Alias(n) => {
|
||||
let location = (v.x.locations.first().cloned())
|
||||
.expect("Aliases are never created without source code");
|
||||
// this is an absolute path so we set the path to empty
|
||||
match resolve_name(n.clone(), root, &[], location, env) {
|
||||
Ok(Some(n)) => ItemKind::Alias(n),
|
||||
Ok(None) => ItemKind::Alias(n.clone()),
|
||||
Err(e) => return Err(e.pack()),
|
||||
}
|
||||
},
|
||||
_ => item.kind.clone(),
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
}))
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
.collect::<Result<HashMap<_, _>, _>>()?,
|
||||
};
|
||||
errors.is_empty().then_some(module).ok_or_else(|| NameErrors(errors).pack())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn resolve_aliases(
|
||||
project: ProjectTree<VName>,
|
||||
updated: &impl Fn(&[Tok<String>]) -> bool,
|
||||
) -> ProjectTree<VName> {
|
||||
ProjectTree(resolve_aliases_rec(&project.0, &project.0, updated, true))
|
||||
project: ProjectMod,
|
||||
env: &Module<impl Sized, impl Sized, impl Sized>,
|
||||
) -> ProjectResult<ProjectMod> {
|
||||
resolve_aliases_rec(&project, &mut Vec::new(), &project, env)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
#[allow(unused)] // for doc
|
||||
use crate::representations::project::ProjectEntry;
|
||||
use crate::representations::project::{ItemKind, ProjectItem, ProjectMod};
|
||||
use intern_all::Tok;
|
||||
|
||||
use crate::name::{VName, VPath};
|
||||
use crate::pipeline::project::{ItemKind, ProjectMemberRef, ProjectMod};
|
||||
use crate::tree::ModMember;
|
||||
use crate::utils::{unwrap_or, BoxedIter};
|
||||
use crate::{Interner, NameLike, Tok, VName};
|
||||
use crate::utils::boxed_iter::BoxedIter;
|
||||
use crate::utils::unwrap_or::unwrap_or;
|
||||
|
||||
/// The destination of a linked walk. [ProjectEntry] cannot be used for this
|
||||
/// purpose because it might be the project root.
|
||||
pub enum Target<'a, N: NameLike> {
|
||||
Mod(&'a ProjectMod<N>),
|
||||
Leaf(&'a ProjectItem<N>),
|
||||
}
|
||||
|
||||
#[must_use = "this is the sole product of this function"]
|
||||
pub struct WalkReport<'a, N: NameLike> {
|
||||
pub target: Target<'a, N>,
|
||||
pub struct WalkReport<'a> {
|
||||
pub target: ProjectMemberRef<'a>,
|
||||
pub abs_path: VName,
|
||||
pub aliased: bool,
|
||||
}
|
||||
|
||||
pub struct LinkWalkError<'a> {
|
||||
/// The last known valid path
|
||||
pub abs_path: VName,
|
||||
pub abs_path: Vec<Tok<String>>,
|
||||
/// The name that wasn't found
|
||||
pub name: Tok<String>,
|
||||
/// Leftover steps
|
||||
@@ -29,29 +22,29 @@ pub struct LinkWalkError<'a> {
|
||||
/// Whether an alias was ever encountered
|
||||
pub aliased: bool,
|
||||
}
|
||||
impl<'a> LinkWalkError<'a> {
|
||||
pub fn consumed_path(self) -> VName {
|
||||
VPath::new(self.abs_path).as_prefix_of(self.name)
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_with_links_rec<'a, 'b, N: NameLike>(
|
||||
mut abs_path: VName,
|
||||
root: &'a ProjectMod<N>,
|
||||
cur: &'a ProjectMod<N>,
|
||||
prev_tgt: Target<'a, N>,
|
||||
fn walk_with_links_rec<'a, 'b>(
|
||||
mut abs_path: Vec<Tok<String>>,
|
||||
root: &'a ProjectMod,
|
||||
cur: &'a ProjectMod,
|
||||
prev_tgt: ProjectMemberRef<'a>,
|
||||
aliased: bool,
|
||||
mut path: impl Iterator<Item = Tok<String>> + 'b,
|
||||
l: bool,
|
||||
) -> Result<WalkReport<'a, N>, LinkWalkError<'b>> {
|
||||
let name = unwrap_or! {path.next();
|
||||
) -> Result<WalkReport<'a>, LinkWalkError<'b>> {
|
||||
let name = match path.next() {
|
||||
Some(name) => name,
|
||||
// ends on this module
|
||||
return Ok(WalkReport{ target: prev_tgt, abs_path, aliased })
|
||||
None => {
|
||||
let abs_path = VName::new(abs_path).expect("Aliases are never empty");
|
||||
return Ok(WalkReport { target: prev_tgt, abs_path, aliased });
|
||||
},
|
||||
};
|
||||
if l {
|
||||
eprintln!(
|
||||
"Resolving {} in {}",
|
||||
name,
|
||||
Interner::extern_all(&abs_path).join("::")
|
||||
)
|
||||
}
|
||||
let entry = unwrap_or! {cur.entries.get(&name); {
|
||||
// panic!("No entry {name} on {}", Interner::extern_all(&cur.extra.path).join("::"));
|
||||
// leads into a missing branch
|
||||
return Err(LinkWalkError{ abs_path, aliased, name, tail: Box::new(path) })
|
||||
}};
|
||||
@@ -59,24 +52,19 @@ fn walk_with_links_rec<'a, 'b, N: NameLike>(
|
||||
ModMember::Sub(m) => {
|
||||
// leads into submodule
|
||||
abs_path.push(name);
|
||||
walk_with_links_rec(abs_path, root, m, Target::Mod(m), aliased, path, l)
|
||||
let tgt = ProjectMemberRef::Mod(m);
|
||||
walk_with_links_rec(abs_path, root, m, tgt, aliased, path)
|
||||
},
|
||||
ModMember::Item(item) => match &item.kind {
|
||||
ItemKind::Alias(alias) => {
|
||||
// leads into alias (reset acc, cur, cur_entry)
|
||||
if l {
|
||||
eprintln!(
|
||||
"{} points to {}",
|
||||
Interner::extern_all(&abs_path).join("::"),
|
||||
Interner::extern_all(alias).join("::")
|
||||
)
|
||||
}
|
||||
abs_path.clone_from(alias);
|
||||
abs_path.clear();
|
||||
abs_path.extend_from_slice(&alias[..]);
|
||||
abs_path.extend(path);
|
||||
let path_acc = Vec::with_capacity(abs_path.len());
|
||||
let new_path = abs_path.into_iter();
|
||||
let tgt = Target::Mod(root);
|
||||
walk_with_links_rec(path_acc, root, root, tgt, true, new_path, l)
|
||||
let tgt = ProjectMemberRef::Mod(root);
|
||||
walk_with_links_rec(path_acc, root, root, tgt, true, new_path)
|
||||
},
|
||||
ItemKind::Const(_) | ItemKind::None => {
|
||||
abs_path.push(name);
|
||||
@@ -88,7 +76,8 @@ fn walk_with_links_rec<'a, 'b, N: NameLike>(
|
||||
},
|
||||
None => {
|
||||
// ends on leaf
|
||||
let target = Target::Leaf(item);
|
||||
let target = ProjectMemberRef::Item(item);
|
||||
let abs_path = VName::new(abs_path).expect("pushed just above");
|
||||
Ok(WalkReport { target, abs_path, aliased })
|
||||
},
|
||||
}
|
||||
@@ -100,24 +89,16 @@ fn walk_with_links_rec<'a, 'b, N: NameLike>(
|
||||
/// Execute a walk down the tree, following aliases.
|
||||
/// If the path ends on an alias, that alias is also resolved.
|
||||
/// If the path leads out of the tree, the shortest failing path is returned
|
||||
pub fn walk_with_links<'a, N: NameLike>(
|
||||
root: &ProjectMod<N>,
|
||||
pub fn walk_with_links<'a>(
|
||||
root: &ProjectMod,
|
||||
path: impl Iterator<Item = Tok<String>> + 'a,
|
||||
l: bool,
|
||||
) -> Result<WalkReport<'_, N>, LinkWalkError<'a>> {
|
||||
) -> Result<WalkReport<'_>, LinkWalkError<'a>> {
|
||||
let path_acc = path.size_hint().1.map_or_else(Vec::new, Vec::with_capacity);
|
||||
let mut result = walk_with_links_rec(
|
||||
path_acc,
|
||||
root,
|
||||
root,
|
||||
Target::Mod(root),
|
||||
false,
|
||||
path,
|
||||
l,
|
||||
);
|
||||
let tgt = ProjectMemberRef::Mod(root);
|
||||
let mut result = walk_with_links_rec(path_acc, root, root, tgt, false, path);
|
||||
// cut off excess preallocated space within normal vector growth policy
|
||||
let abs_path = match &mut result {
|
||||
Ok(rep) => &mut rep.abs_path,
|
||||
Ok(rep) => rep.abs_path.vec_mut(),
|
||||
Err(err) => &mut err.abs_path,
|
||||
};
|
||||
abs_path.shrink_to(abs_path.len().next_power_of_two());
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
//! Source loader callback definition and builtin implementations
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::{fs, io};
|
||||
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
use crate::error::{ProjectError, ProjectResult};
|
||||
#[allow(unused)] // for doc
|
||||
use crate::facade::System;
|
||||
use crate::interner::Interner;
|
||||
use crate::utils::Cache;
|
||||
use crate::{Location, Stok, Tok, VName};
|
||||
|
||||
/// All the data available about a failed source load call
|
||||
#[derive(Debug)]
|
||||
pub struct FileLoadingError {
|
||||
file: io::Error,
|
||||
dir: io::Error,
|
||||
path: VName,
|
||||
}
|
||||
impl ProjectError for FileLoadingError {
|
||||
fn description(&self) -> &str {
|
||||
"Neither a file nor a directory could be read from the requested path"
|
||||
}
|
||||
fn one_position(&self) -> crate::Location {
|
||||
Location::File(Arc::new(self.path.clone()))
|
||||
}
|
||||
fn message(&self) -> String {
|
||||
format!("File: {}\nDirectory: {}", self.file, self.dir)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the result of loading code from a string-tree form such
|
||||
/// as the file system.
|
||||
#[derive(Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Loaded {
|
||||
/// Conceptually equivalent to a sourcefile
|
||||
Code(Arc<String>),
|
||||
/// Conceptually equivalent to the list of *.orc files in a folder, without
|
||||
/// the extension
|
||||
Collection(Arc<Vec<String>>),
|
||||
}
|
||||
impl Loaded {
|
||||
/// Is the loaded item source code (not a collection)?
|
||||
pub fn is_code(&self) -> bool { matches!(self, Loaded::Code(_)) }
|
||||
}
|
||||
|
||||
/// Returned by any source loading callback
|
||||
pub type IOResult = ProjectResult<Loaded>;
|
||||
|
||||
/// Load a file from a path expressed in Rust strings, but relative to
|
||||
/// a root expressed as an OS Path.
|
||||
pub fn load_file(root: &Path, path: &[Tok<String>]) -> IOResult {
|
||||
let full_path = path.iter().fold(root.to_owned(), |p, t| p.join(t.as_str()));
|
||||
let file_path = full_path.with_extension("orc");
|
||||
let file_error = match fs::read_to_string(file_path) {
|
||||
Ok(string) => return Ok(Loaded::Code(Arc::new(string))),
|
||||
Err(err) => err,
|
||||
};
|
||||
let dir = match fs::read_dir(&full_path) {
|
||||
Ok(dir) => dir,
|
||||
Err(dir_error) =>
|
||||
return Err(
|
||||
FileLoadingError {
|
||||
file: file_error,
|
||||
dir: dir_error,
|
||||
path: path.to_vec(),
|
||||
}
|
||||
.rc(),
|
||||
),
|
||||
};
|
||||
let names = dir
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|ent| {
|
||||
let fname = ent.file_name().into_string().ok()?;
|
||||
let ftyp = ent.metadata().ok()?.file_type();
|
||||
Some(if ftyp.is_dir() {
|
||||
fname
|
||||
} else {
|
||||
fname.strip_suffix(".or")?.to_string()
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(Loaded::Collection(Arc::new(names)))
|
||||
}
|
||||
|
||||
/// Generates a cached file loader for a directory
|
||||
#[must_use]
|
||||
pub fn mk_dir_cache(root: PathBuf) -> Cache<'static, VName, IOResult> {
|
||||
Cache::new(move |vname: VName, _this| load_file(&root, &vname))
|
||||
}
|
||||
|
||||
/// Load a file from the specified path from an embed table
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// if the `RustEmbed` includes files that do not end in `ext`
|
||||
pub fn load_embed<T: 'static + RustEmbed>(path: &str, ext: &str) -> IOResult {
|
||||
let file_path = path.to_string() + ext;
|
||||
if let Some(file) = T::get(&file_path) {
|
||||
let s =
|
||||
String::from_utf8(file.data.to_vec()).expect("Embed must be valid UTF-8");
|
||||
Ok(Loaded::Code(Arc::new(s)))
|
||||
} else {
|
||||
let entries = T::iter()
|
||||
.map(|c| c.to_string())
|
||||
.filter_map(|path: String| {
|
||||
let item_prefix = path.to_string() + "/";
|
||||
path.strip_prefix(&item_prefix).map(|subpath| {
|
||||
let item_name = subpath
|
||||
.split_inclusive('/')
|
||||
.next()
|
||||
.expect("Exact match excluded earlier");
|
||||
item_name
|
||||
.strip_suffix('/') // subdirectory
|
||||
.or_else(|| item_name.strip_suffix(ext)) // file
|
||||
.expect("embed should be filtered to extension")
|
||||
.to_string()
|
||||
})
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
Ok(Loaded::Collection(Arc::new(entries)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates a cached file loader for a [RustEmbed]
|
||||
#[must_use]
|
||||
pub fn mk_embed_cache<T: 'static + RustEmbed>(
|
||||
ext: &str,
|
||||
) -> Cache<'_, Vec<Stok>, IOResult> {
|
||||
Cache::new(move |vname: VName, _this| -> IOResult {
|
||||
let path = Interner::extern_all(&vname).join("/");
|
||||
load_embed::<T>(&path, ext)
|
||||
})
|
||||
}
|
||||
|
||||
/// Load all files from an embed and convert them into a map usable in a
|
||||
/// [System]
|
||||
#[must_use]
|
||||
pub fn embed_to_map<T: 'static + RustEmbed>(
|
||||
suffix: &str,
|
||||
i: &Interner,
|
||||
) -> HashMap<Vec<Stok>, Loaded> {
|
||||
let mut files = HashMap::new();
|
||||
let mut dirs = HashMap::new();
|
||||
for path in T::iter() {
|
||||
let vpath = path
|
||||
.strip_suffix(suffix)
|
||||
.expect("the embed must be filtered for suffix")
|
||||
.split('/')
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let tokvpath = vpath.iter().map(|segment| i.i(segment)).collect::<Vec<_>>();
|
||||
let data = T::get(&path).expect("path from iterator").data;
|
||||
let text =
|
||||
String::from_utf8(data.to_vec()).expect("code embeds must be utf-8");
|
||||
files.insert(tokvpath.clone(), text);
|
||||
for (lvl, subname) in vpath.iter().enumerate() {
|
||||
let dirname = tokvpath.split_at(lvl).0;
|
||||
let (_, entries) = (dirs.raw_entry_mut().from_key(dirname))
|
||||
.or_insert_with(|| (dirname.to_vec(), HashSet::new()));
|
||||
entries.get_or_insert_with(subname, Clone::clone);
|
||||
}
|
||||
}
|
||||
(files.into_iter())
|
||||
.map(|(k, s)| (k, Loaded::Code(Arc::new(s))))
|
||||
.chain((dirs.into_iter()).map(|(k, entv)| {
|
||||
(k, Loaded::Collection(Arc::new(entv.into_iter().collect())))
|
||||
}))
|
||||
.collect()
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use crate::error::ProjectResult;
|
||||
use crate::interner::{Interner, Tok};
|
||||
use crate::representations::sourcefile::absolute_path;
|
||||
use crate::utils::substack::Substack;
|
||||
use crate::{Location, VName};
|
||||
|
||||
pub fn import_abs_path(
|
||||
src_path: &[Tok<String>],
|
||||
mod_stack: Substack<Tok<String>>,
|
||||
import_path: &[Tok<String>],
|
||||
i: &Interner,
|
||||
location: &Location,
|
||||
) -> ProjectResult<VName> {
|
||||
// path of module within file
|
||||
let mod_pathv = mod_stack.iter().rev_vec_clone();
|
||||
// path of module within compilation
|
||||
let abs_pathv =
|
||||
(src_path.iter()).chain(mod_pathv.iter()).cloned().collect::<Vec<_>>();
|
||||
// preload-target path relative to module
|
||||
// preload-target path within compilation
|
||||
absolute_path(&abs_pathv, import_path, i, location)
|
||||
}
|
||||
239
src/pipeline/load_solution.rs
Normal file
239
src/pipeline/load_solution.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
//! Load an Orchid project by starting from one or more entry points and
|
||||
//! following the imports
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::Arc;
|
||||
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use intern_all::{sweep_t, Tok};
|
||||
|
||||
use super::dealias::resolve_aliases::resolve_aliases;
|
||||
use super::process_source::{process_ns, resolve_globs, GlobImports};
|
||||
use super::project::{ItemKind, ProjItem, ProjXEnt, ProjectMod, ProjectTree};
|
||||
use crate::error::{
|
||||
bundle_location, ErrorPosition, ProjectError, ProjectResult,
|
||||
};
|
||||
use crate::location::{CodeGenInfo, CodeLocation, SourceCode, SourceRange};
|
||||
use crate::name::{PathSlice, Sym, VName, VPath};
|
||||
use crate::parse::context::ParseCtxImpl;
|
||||
use crate::parse::facade::parse_file;
|
||||
use crate::parse::lex_plugin::LexerPlugin;
|
||||
use crate::parse::parse_plugin::ParseLinePlugin;
|
||||
use crate::tree::{ModEntry, ModMember, Module};
|
||||
use crate::utils::combine::Combine;
|
||||
use crate::utils::sequence::Sequence;
|
||||
use crate::virt_fs::{Loaded, VirtFS};
|
||||
|
||||
// apply layer:
|
||||
// 1. build trees
|
||||
// Question: what if a file is not found?
|
||||
// - raising an error would risk failing on a std module
|
||||
// - moving on could obscure very simple errors
|
||||
// can we get rid of layers and show system sources alongside user sources?
|
||||
// what would break? Can we break it?
|
||||
// the project moves into a prefix, imports are either super:: or user::
|
||||
// custom support for root:: specifier
|
||||
// virtual file tree is back on
|
||||
// systems get free reign on their subtree, less jank
|
||||
// would also solve some weird accidental private member aliasing issues
|
||||
|
||||
/// Split off the longest prefix accepted by the validator
|
||||
fn split_max_prefix<'a, T>(
|
||||
path: &'a [T],
|
||||
is_valid: &impl Fn(&[T]) -> bool,
|
||||
) -> Option<(&'a [T], &'a [T])> {
|
||||
(0..=path.len())
|
||||
.rev()
|
||||
.map(|i| path.split_at(i))
|
||||
.find(|(file, _)| is_valid(file))
|
||||
}
|
||||
|
||||
/// Represents a prelude / implicit import requested by a library.
|
||||
/// A prelude extends any module with a glob import from the target module
|
||||
/// unless its path begins with exclude.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Prelude {
|
||||
/// Path the glob imports will point to
|
||||
pub target: VName,
|
||||
/// subtree to exclude (typically the region the prelude collates items from)
|
||||
pub exclude: VName,
|
||||
/// Location data attached to the aliases
|
||||
pub owner: CodeGenInfo,
|
||||
}
|
||||
|
||||
/// Hooks and extensions to the source loading process
|
||||
#[derive(Clone)]
|
||||
pub struct SolutionContext<'a> {
|
||||
/// Callbacks from the lexer to support literals of custom datatypes
|
||||
pub lexer_plugins: Sequence<'a, &'a (dyn LexerPlugin + 'a)>,
|
||||
/// Callbacks from the parser to support custom module tree elements
|
||||
pub line_parsers: Sequence<'a, &'a (dyn ParseLinePlugin + 'a)>,
|
||||
/// Lines prepended to various modules to import "global" values
|
||||
pub preludes: Sequence<'a, &'a Prelude>,
|
||||
}
|
||||
impl<'a> SolutionContext<'a> {
|
||||
/// Derive context for the parser
|
||||
pub fn parsing(&self, code: SourceCode) -> ParseCtxImpl<'a> {
|
||||
ParseCtxImpl {
|
||||
code,
|
||||
lexers: self.lexer_plugins.clone(),
|
||||
line_parsers: self.line_parsers.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load source files from a source tree and parse them starting from the
|
||||
/// specified targets and following imports. An in-memory environment tree is
|
||||
/// used to allow imports from modules that are defined by other loading steps
|
||||
/// and later merged into this source code.
|
||||
pub fn load_solution(
|
||||
ctx: SolutionContext,
|
||||
targets: impl IntoIterator<Item = (Sym, CodeLocation)>,
|
||||
env: &Module<impl Sized, impl Sized, impl Sized>,
|
||||
fs: &impl VirtFS,
|
||||
) -> ProjectResult<ProjectTree> {
|
||||
let mut target_queue = VecDeque::<(Sym, CodeLocation)>::new();
|
||||
target_queue.extend(targets.into_iter());
|
||||
target_queue.extend(
|
||||
(ctx.preludes.iter())
|
||||
.map(|p| (p.target.to_sym(), CodeLocation::Gen(p.owner.clone()))),
|
||||
);
|
||||
let mut known_files = HashSet::new();
|
||||
let mut tree_acc: ProjectMod = Module::wrap([]);
|
||||
let mut glob_acc: GlobImports = Module::wrap([]);
|
||||
while let Some((target, referrer)) = target_queue.pop_front() {
|
||||
let path_parts = split_max_prefix(&target[..], &|p| {
|
||||
fs.read(PathSlice(p)).map(|l| l.is_code()).unwrap_or(false)
|
||||
});
|
||||
if let Some((filename, _)) = path_parts {
|
||||
if known_files.contains(filename) {
|
||||
continue;
|
||||
}
|
||||
known_files.insert(filename.to_vec());
|
||||
let path = VPath(filename.to_vec());
|
||||
let loaded = fs
|
||||
.read(PathSlice(filename))
|
||||
.map_err(|e| bundle_location(&referrer, &*e))?;
|
||||
let code = match loaded {
|
||||
Loaded::Collection(_) =>
|
||||
return Err(UnexpectedDirectory { path }.pack()),
|
||||
Loaded::Code(source) => SourceCode { source, path: Arc::new(path) },
|
||||
};
|
||||
let full_range =
|
||||
SourceRange { range: 0..code.source.len(), code: code.clone() };
|
||||
let lines = parse_file(&ctx.parsing(code.clone()))?;
|
||||
let report = process_ns(code.path, lines, full_range)?;
|
||||
target_queue.extend(
|
||||
(report.external_references.into_iter())
|
||||
.map(|(k, v)| (k, CodeLocation::Source(v))),
|
||||
);
|
||||
if !report.comments.is_empty() && filename.is_empty() {
|
||||
todo!("panic - module op comments on root are lost")
|
||||
}
|
||||
let mut comments = Some(report.comments);
|
||||
let mut module = report.module;
|
||||
let mut glob = report.glob_imports;
|
||||
for i in (0..filename.len()).rev() {
|
||||
// i over valid indices of filename
|
||||
let key = filename[i].clone(); // last segment
|
||||
let comments = comments.take().into_iter().flatten().collect();
|
||||
glob =
|
||||
Module::wrap([(key.clone(), ModEntry::wrap(ModMember::Sub(glob)))]);
|
||||
module = Module::wrap([(key, ModEntry {
|
||||
member: ModMember::Sub(module),
|
||||
x: ProjXEnt { comments, ..Default::default() },
|
||||
})]);
|
||||
}
|
||||
glob_acc = (glob_acc.combine(glob))
|
||||
.expect("source code loaded for two nested paths");
|
||||
tree_acc = (tree_acc.combine(module))
|
||||
.expect("source code loaded for two nested paths");
|
||||
} else {
|
||||
known_files.insert(target[..].to_vec());
|
||||
// If the path is not within a file, load it as directory
|
||||
match fs.read(target.as_path_slice()) {
|
||||
Ok(Loaded::Collection(c)) => target_queue
|
||||
.extend(c.iter().map(|e| (Sym::parse(e).unwrap(), referrer.clone()))),
|
||||
Ok(Loaded::Code(_)) => unreachable!("Should have split to self and []"),
|
||||
// Ignore error if the path is walkable in the const tree
|
||||
Err(_) if env.walk1_ref(&[], &target[..], |_| true).is_ok() => (),
|
||||
Err(e) => return Err(bundle_location(&referrer, &*e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut contention = HashMap::new();
|
||||
resolve_globs(
|
||||
VPath(vec![]),
|
||||
glob_acc,
|
||||
ctx.preludes.clone(),
|
||||
&mut tree_acc,
|
||||
env,
|
||||
&mut contention,
|
||||
)?;
|
||||
let ret = resolve_aliases(tree_acc, env)?;
|
||||
for ((glob, original), locations) in contention {
|
||||
let (glob_val, _) = ret
|
||||
.walk1_ref(&[], &glob[..], |_| true)
|
||||
.expect("Should've emerged in dealias");
|
||||
let (original_val, _) = ret
|
||||
.walk1_ref(&[], &original[..], |_| true)
|
||||
.expect("Should've emerged in dealias");
|
||||
let glob_real = match &glob_val.member {
|
||||
ModMember::Item(ProjItem { kind: ItemKind::Alias(glob_tgt) }) => glob_tgt,
|
||||
_ => &glob,
|
||||
};
|
||||
let original_real = match &original_val.member {
|
||||
ModMember::Item(ProjItem { kind: ItemKind::Alias(orig_tgt) }) => orig_tgt,
|
||||
_ => &original,
|
||||
};
|
||||
if glob_real != original_real {
|
||||
let real = original_real.clone();
|
||||
let glob_real = glob_real.clone();
|
||||
let err = ConflictingGlobs { real, glob_real, original, glob, locations };
|
||||
return Err(err.pack());
|
||||
}
|
||||
}
|
||||
sweep_t::<String>();
|
||||
sweep_t::<Vec<Tok<String>>>();
|
||||
Ok(ProjectTree(ret))
|
||||
}
|
||||
|
||||
/// Produced when a stage that deals specifically with code encounters
|
||||
/// a path that refers to a directory
|
||||
#[derive(Debug)]
|
||||
struct UnexpectedDirectory {
|
||||
/// Path to the offending collection
|
||||
pub path: VPath,
|
||||
}
|
||||
impl ProjectError for UnexpectedDirectory {
|
||||
const DESCRIPTION: &'static str = "A stage that deals specifically with code \
|
||||
encountered a path that refers to a directory";
|
||||
fn message(&self) -> String {
|
||||
format!("{} was expected to be a file", self.path)
|
||||
}
|
||||
fn positions(&self) -> impl IntoIterator<Item = ErrorPosition> { [] }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ConflictingGlobs {
|
||||
original: Sym,
|
||||
real: Sym,
|
||||
glob: Sym,
|
||||
glob_real: Sym,
|
||||
locations: Vec<CodeLocation>,
|
||||
}
|
||||
impl ProjectError for ConflictingGlobs {
|
||||
const DESCRIPTION: &'static str =
|
||||
"A symbol from a glob import conflicts with an existing name";
|
||||
fn message(&self) -> String {
|
||||
let Self { glob, glob_real, original, real, .. } = self;
|
||||
format!(
|
||||
"glob import included {glob} which resolved to {glob_real}. \
|
||||
This conflicts with {original} which resolved to {real}"
|
||||
)
|
||||
}
|
||||
fn positions(&self) -> impl IntoIterator<Item = ErrorPosition> {
|
||||
(self.locations.iter())
|
||||
.map(|l| ErrorPosition { location: l.clone(), message: None })
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,6 @@
|
||||
//! Loading Orchid modules from source
|
||||
//! Loading Orchid projects from source
|
||||
mod dealias;
|
||||
pub mod file_loader;
|
||||
mod import_abs_path;
|
||||
mod parse_layer;
|
||||
mod project_tree;
|
||||
mod source_loader;
|
||||
// mod tree_loader;
|
||||
|
||||
pub use parse_layer::parse_layer;
|
||||
pub mod load_solution;
|
||||
mod path;
|
||||
mod process_source;
|
||||
pub mod project;
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
use super::dealias::resolve_aliases;
|
||||
use super::file_loader::IOResult;
|
||||
use super::{project_tree, source_loader};
|
||||
use crate::error::ProjectResult;
|
||||
use crate::interner::{Interner, Tok};
|
||||
use crate::parse::{LexerPlugin, LineParser};
|
||||
use crate::representations::sourcefile::FileEntry;
|
||||
use crate::representations::VName;
|
||||
use crate::utils::never;
|
||||
use crate::ProjectTree;
|
||||
|
||||
/// Using an IO callback, produce a project tree that includes the given
|
||||
/// target symbols or files if they're defined.
|
||||
///
|
||||
/// The environment accessible to the loaded source can be specified with
|
||||
/// a pre-existing tree which will be merged with the loaded data, and a
|
||||
/// prelude which will be prepended to each individual file. Since the
|
||||
/// prelude gets compiled with each file, normally it should be a glob
|
||||
/// import pointing to a module in the environment.
|
||||
pub fn parse_layer<'a>(
|
||||
targets: impl Iterator<Item = &'a [Tok<String>]>,
|
||||
loader: &impl Fn(&[Tok<String>], &[Tok<String>]) -> IOResult,
|
||||
environment: &'a ProjectTree<VName>,
|
||||
prelude: &[FileEntry],
|
||||
lexer_plugins: &[&dyn LexerPlugin],
|
||||
line_parsers: &[&dyn LineParser],
|
||||
i: &Interner,
|
||||
) -> ProjectResult<ProjectTree<VName>> {
|
||||
let sl_ctx =
|
||||
source_loader::Context { prelude, i, lexer_plugins, line_parsers };
|
||||
let (preparsed, source) =
|
||||
source_loader::load_source(targets, sl_ctx, loader, &|path| {
|
||||
environment.0.walk_ref(&[], path, false).is_ok()
|
||||
})?;
|
||||
let tree =
|
||||
project_tree::rebuild_tree(&source, preparsed, environment, prelude, i)?;
|
||||
let sum = ProjectTree(never::unwrap_always(
|
||||
environment.0.clone().overlay(tree.0.clone()),
|
||||
));
|
||||
let resolvd =
|
||||
resolve_aliases(sum, &|path| tree.0.walk1_ref(&[], path, false).is_ok());
|
||||
// Addition among modules favours the left hand side.
|
||||
Ok(resolvd)
|
||||
}
|
||||
82
src/pipeline/path.rs
Normal file
82
src/pipeline/path.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use intern_all::{i, Tok};
|
||||
|
||||
use crate::error::{ProjectError, ProjectResult};
|
||||
use crate::location::CodeLocation;
|
||||
use crate::name::VName;
|
||||
|
||||
/// Turn a relative (import) path into an absolute path.
|
||||
/// If the import path is empty, the return value is also empty.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// if the relative path contains as many or more `super` segments than the
|
||||
/// length of the absolute path.
|
||||
pub(super) fn absolute_path(
|
||||
abs_location: &[Tok<String>],
|
||||
rel_path: &[Tok<String>],
|
||||
location: CodeLocation,
|
||||
) -> ProjectResult<VName> {
|
||||
match absolute_path_rec(abs_location, rel_path) {
|
||||
Some(v) => VName::new(v).map_err(|_| ImportAll { location }.pack()),
|
||||
None => {
|
||||
let path = rel_path.try_into().expect("At least one super");
|
||||
Err(TooManySupers { path, location }.pack())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use = "this could be None which means that there are too many supers"]
|
||||
fn absolute_path_rec(
|
||||
mut abs_location: &[Tok<String>],
|
||||
mut rel_path: &[Tok<String>],
|
||||
) -> Option<Vec<Tok<String>>> {
|
||||
let mut relative = false;
|
||||
if rel_path.first().cloned() == Some(i("self")) {
|
||||
relative = true;
|
||||
rel_path = rel_path.split_first().expect("checked above").1;
|
||||
} else {
|
||||
while rel_path.first().cloned() == Some(i("super")) {
|
||||
match abs_location.split_last() {
|
||||
Some((_, torso)) => abs_location = torso,
|
||||
None => return None,
|
||||
};
|
||||
rel_path = rel_path.split_first().expect("checked above").1;
|
||||
relative = true;
|
||||
}
|
||||
}
|
||||
match relative {
|
||||
true => Some(abs_location.iter().chain(rel_path).cloned().collect()),
|
||||
false => Some(rel_path.to_vec()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Error produced when an import path starts with more `super` segments
|
||||
/// than the current module's absolute path
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
struct TooManySupers {
|
||||
/// The offending import path
|
||||
pub path: VName,
|
||||
/// The faulty import statement
|
||||
pub location: CodeLocation,
|
||||
}
|
||||
impl ProjectError for TooManySupers {
|
||||
const DESCRIPTION: &'static str = "an import path starts with more \
|
||||
`super` segments than the current module's absolute path";
|
||||
fn message(&self) -> String {
|
||||
format!("path {} contains too many `super` steps.", self.path)
|
||||
}
|
||||
|
||||
fn one_position(&self) -> CodeLocation { self.location.clone() }
|
||||
}
|
||||
|
||||
/// Error produced for the statement `import *`
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
struct ImportAll {
|
||||
/// The file containing the offending import
|
||||
pub location: CodeLocation,
|
||||
}
|
||||
impl ProjectError for ImportAll {
|
||||
const DESCRIPTION: &'static str = "a top-level glob import was used";
|
||||
fn message(&self) -> String { format!("{} imports *", self.location) }
|
||||
fn one_position(&self) -> CodeLocation { self.location.clone() }
|
||||
}
|
||||
358
src/pipeline/process_source.rs
Normal file
358
src/pipeline/process_source.rs
Normal file
@@ -0,0 +1,358 @@
|
||||
use std::mem;
|
||||
use std::sync::Arc;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use intern_all::Tok;
|
||||
use itertools::Itertools;
|
||||
use never::Never;
|
||||
|
||||
use super::load_solution::Prelude;
|
||||
use super::path::absolute_path;
|
||||
use super::project::{
|
||||
ItemKind, ProjItem, ProjRule, ProjXEnt, ProjXMod, ProjectEntry, ProjectMod,
|
||||
SourceModule,
|
||||
};
|
||||
use crate::error::{
|
||||
ErrorPosition, ProjectError, ProjectErrorObj, ProjectResult,
|
||||
};
|
||||
use crate::location::{CodeLocation, SourceRange};
|
||||
use crate::name::{Sym, VName, VPath};
|
||||
use crate::parse::parsed::{
|
||||
Constant, Member, MemberKind, ModuleBlock, Rule, SourceLine, SourceLineKind,
|
||||
};
|
||||
use crate::tree::{ModEntry, ModMember, Module, WalkError};
|
||||
use crate::utils::combine::Combine;
|
||||
use crate::utils::get_or::get_or_make;
|
||||
use crate::utils::pure_seq::pushed_ref;
|
||||
use crate::utils::sequence::Sequence;
|
||||
use crate::utils::unwrap_or::unwrap_or;
|
||||
|
||||
// Problem: import normalization
|
||||
//
|
||||
// Imports aren't explicitly present in the tree we're currently producing.
|
||||
// Named imports can be placed in Aliases, but glob imports should
|
||||
// not be included in the Project Tree. A separate Glob Import Tree
|
||||
// should be produced, which preferably utilizes the existing [Module]
|
||||
// tree. Then a postprocessing step can use the GIT to both look up the exports
|
||||
// and write them back into &mut PT.
|
||||
|
||||
/// This tree contains the absolute path of glob imports.
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct GlobImpReport {
|
||||
pub target: VName,
|
||||
pub location: SourceRange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(super) struct GlobImpXMod(pub Vec<GlobImpReport>);
|
||||
impl Combine for GlobImpXMod {
|
||||
type Error = Never;
|
||||
fn combine(self, other: Self) -> Result<Self, Self::Error> {
|
||||
Ok(GlobImpXMod(self.0.into_iter().chain(other.0).collect()))
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) type GlobImports = Module<Never, GlobImpXMod, ()>;
|
||||
|
||||
pub(super) struct FileReport {
|
||||
/// Absolute path of values outside the file
|
||||
pub external_references: HashMap<Sym, SourceRange>,
|
||||
pub comments: Vec<Arc<String>>,
|
||||
pub module: ProjectMod,
|
||||
pub glob_imports: GlobImports,
|
||||
}
|
||||
|
||||
fn default_entry() -> ProjectEntry {
|
||||
ProjectEntry {
|
||||
member: ModMember::Item(ProjItem::default()),
|
||||
x: ProjXEnt { comments: vec![], locations: vec![], exported: false },
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn process_ns(
|
||||
path: Arc<VPath>,
|
||||
lines: Vec<SourceLine>,
|
||||
ns_location: SourceRange,
|
||||
) -> ProjectResult<FileReport> {
|
||||
let mut file_comments = Vec::new();
|
||||
let mut new_comments = Vec::new();
|
||||
let mut entries = HashMap::new();
|
||||
let mut external_references = HashMap::new();
|
||||
let mut rules = Vec::new();
|
||||
let wrap = Module::wrap([]);
|
||||
let mut glob_imports: GlobImports = wrap;
|
||||
for SourceLine { kind: line_kind, range } in lines {
|
||||
match line_kind {
|
||||
SourceLineKind::Comment(comment) => new_comments.push(Arc::new(comment)),
|
||||
SourceLineKind::Export(names) => {
|
||||
let comments = (names.len() == 1).then(|| mem::take(&mut new_comments));
|
||||
for (name, name_location) in names {
|
||||
let entry = get_or_make(&mut entries, &name, default_entry);
|
||||
let location = CodeLocation::Source(name_location);
|
||||
if entry.x.exported {
|
||||
let err = MultipleExports {
|
||||
path: (*path).clone().as_prefix_of(name).to_sym(),
|
||||
locations: pushed_ref(&entry.x.locations, location),
|
||||
};
|
||||
return Err(err.pack());
|
||||
}
|
||||
entry.x.exported = true;
|
||||
entry.x.comments.extend(comments.iter().flatten().cloned());
|
||||
entry.x.locations.push(location);
|
||||
}
|
||||
},
|
||||
SourceLineKind::Import(imports) => {
|
||||
file_comments.append(&mut new_comments);
|
||||
for import in imports {
|
||||
let nonglob_path = import.nonglob_path();
|
||||
let location = CodeLocation::Source(range.clone());
|
||||
let abs = absolute_path(&path[..], &nonglob_path[..], location)?;
|
||||
if !abs[..].starts_with(&path[..]) {
|
||||
external_references.insert(abs.to_sym(), import.range.clone());
|
||||
}
|
||||
match import.name {
|
||||
None => (glob_imports.x.0)
|
||||
.push(GlobImpReport { target: abs, location: import.range }),
|
||||
Some(key) => {
|
||||
let entry = get_or_make(&mut entries, &key, default_entry);
|
||||
entry.x.locations.push(CodeLocation::Source(import.range));
|
||||
match &mut entry.member {
|
||||
ModMember::Item(ProjItem { kind: old @ ItemKind::None }) =>
|
||||
*old = ItemKind::Alias(abs.to_sym()),
|
||||
_ => {
|
||||
let err = MultipleDefinitions {
|
||||
path: (*path).clone().as_prefix_of(key).to_sym(),
|
||||
locations: entry.x.locations.clone(),
|
||||
};
|
||||
return Err(err.pack());
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
SourceLineKind::Member(Member { exported, kind }) => match kind {
|
||||
MemberKind::Constant(Constant { name, value }) => {
|
||||
let entry = get_or_make(&mut entries, &name, default_entry);
|
||||
entry.x.locations.push(CodeLocation::Source(range));
|
||||
match &mut entry.member {
|
||||
ModMember::Item(ProjItem { kind: old @ ItemKind::None }) =>
|
||||
*old = ItemKind::Const(value),
|
||||
_ => {
|
||||
let err = MultipleDefinitions {
|
||||
path: (*path).clone().as_prefix_of(name).to_sym(),
|
||||
locations: entry.x.locations.clone(),
|
||||
};
|
||||
return Err(err.pack());
|
||||
},
|
||||
}
|
||||
entry.x.exported |= exported;
|
||||
entry.x.comments.append(&mut new_comments);
|
||||
},
|
||||
MemberKind::Rule(Rule { pattern, prio, template }) => {
|
||||
let prule =
|
||||
ProjRule { pattern, prio, template, comments: new_comments };
|
||||
new_comments = Vec::new();
|
||||
for name in prule.collect_root_names() {
|
||||
let entry = get_or_make(&mut entries, &name, default_entry);
|
||||
entry.x.locations.push(CodeLocation::Source(range.clone()));
|
||||
if entry.x.exported && exported {
|
||||
let err = MultipleExports {
|
||||
path: (*path).clone().as_prefix_of(name).to_sym(),
|
||||
locations: entry.x.locations.clone(),
|
||||
};
|
||||
return Err(err.pack());
|
||||
}
|
||||
entry.x.exported |= exported;
|
||||
}
|
||||
rules.push(prule);
|
||||
},
|
||||
MemberKind::Module(ModuleBlock { name, body }) => {
|
||||
let entry = get_or_make(&mut entries, &name, default_entry);
|
||||
entry.x.locations.push(CodeLocation::Source(range.clone()));
|
||||
if !matches!(
|
||||
entry.member,
|
||||
ModMember::Item(ProjItem { kind: ItemKind::None })
|
||||
) {
|
||||
let err = MultipleDefinitions {
|
||||
path: (*path).clone().as_prefix_of(name).to_sym(),
|
||||
locations: entry.x.locations.clone(),
|
||||
};
|
||||
return Err(err.pack());
|
||||
}
|
||||
if entry.x.exported && exported {
|
||||
let err = MultipleExports {
|
||||
path: (*path).clone().as_prefix_of(name).to_sym(),
|
||||
locations: entry.x.locations.clone(),
|
||||
};
|
||||
return Err(err.pack());
|
||||
}
|
||||
let subpath = Arc::new(VPath(pushed_ref(&path.0, name.clone())));
|
||||
let report = process_ns(subpath, body, range)?;
|
||||
entry.x.comments.append(&mut new_comments);
|
||||
entry.x.comments.extend(report.comments);
|
||||
entry.x.exported |= exported;
|
||||
entry.member = ModMember::Sub(report.module);
|
||||
// record new external references
|
||||
external_references.extend(
|
||||
(report.external_references.into_iter())
|
||||
.filter(|(r, _)| !r[..].starts_with(&path.0)),
|
||||
);
|
||||
// add glob_imports subtree to own tree
|
||||
glob_imports.entries.insert(name, ModEntry {
|
||||
x: (),
|
||||
member: ModMember::Sub(report.glob_imports),
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(FileReport {
|
||||
external_references,
|
||||
comments: file_comments,
|
||||
glob_imports,
|
||||
module: Module {
|
||||
entries,
|
||||
x: ProjXMod { src: Some(SourceModule { range: ns_location, rules }) },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn walk_at_path(
|
||||
e: WalkError,
|
||||
root: &ProjectMod,
|
||||
path: &[Tok<String>],
|
||||
) -> ProjectErrorObj {
|
||||
let submod = (root.walk_ref(&[], path, |_| true))
|
||||
.expect("Invalid source path in walk error populator");
|
||||
let src =
|
||||
submod.x.src.as_ref().expect("Import cannot appear in implied module");
|
||||
e.at(&CodeLocation::Source(src.range.clone()))
|
||||
}
|
||||
|
||||
/// Resolve the glob tree separately produced by [process_ns] by looking up the
|
||||
/// keys of the referenced module and creating an [ItemKind::Alias] for each of
|
||||
/// them. Supports a prelude table which is applied to each module, and an
|
||||
/// environment whose keys are combined with those from within the [ProjectMod].
|
||||
pub fn resolve_globs(
|
||||
globtree_prefix: VPath,
|
||||
globtree: GlobImports,
|
||||
preludes: Sequence<&Prelude>,
|
||||
project_root: &mut ProjectMod,
|
||||
env: &Module<impl Sized, impl Sized, impl Sized>,
|
||||
contention: &mut HashMap<(Sym, Sym), Vec<CodeLocation>>,
|
||||
) -> ProjectResult<()> {
|
||||
// All glob imports in this module
|
||||
let all = (globtree.x.0.into_iter())
|
||||
.map(|gir| (gir.target, CodeLocation::Source(gir.location)))
|
||||
.chain(
|
||||
preludes
|
||||
.iter()
|
||||
.filter(|&pre| !globtree_prefix.0.starts_with(&pre.exclude[..]))
|
||||
.map(|Prelude { target, owner, .. }| {
|
||||
(target.clone(), CodeLocation::Gen(owner.clone()))
|
||||
}),
|
||||
);
|
||||
for (target, location) in all {
|
||||
let (tgt, parent) = project_root
|
||||
.inner_walk(&globtree_prefix.0, &target[..], |e| e.x.exported)
|
||||
.map_err(|e| walk_at_path(e, project_root, &globtree_prefix.0))?;
|
||||
match &tgt.member {
|
||||
ModMember::Item(_) => {
|
||||
use crate::tree::ErrKind::NotModule;
|
||||
let options = Sequence::new(|| parent.keys(|e| e.x.exported));
|
||||
let e = WalkError::last(&target[..], NotModule, options);
|
||||
return Err(walk_at_path(e, project_root, &globtree_prefix.0));
|
||||
},
|
||||
ModMember::Sub(module) => {
|
||||
// All public keys in this module and, if walkable, the environment.
|
||||
let pub_keys = (env.walk_ref(&[], &target[..], |_| true).into_iter())
|
||||
.flat_map(|m| m.keys(|_| true))
|
||||
.chain(module.keys(|e| e.x.exported))
|
||||
.collect_vec();
|
||||
// Reference to the module to be modified
|
||||
let mut_mod =
|
||||
globtree_prefix.0.iter().fold(&mut *project_root, |m, k| {
|
||||
let entry = m.entries.get_mut(k).expect("this is a source path");
|
||||
unwrap_or!(&mut entry.member => ModMember::Sub; {
|
||||
panic!("This is also a source path")
|
||||
})
|
||||
});
|
||||
// Walk errors for the environment are suppressed because leaf-node
|
||||
// conflicts will emerge when merging modules, and walking off the tree
|
||||
// is valid.
|
||||
for key in pub_keys {
|
||||
let entry = get_or_make(&mut mut_mod.entries, &key, default_entry);
|
||||
entry.x.locations.push(location.clone());
|
||||
let alias_tgt = target.clone().suffix([key.clone()]).to_sym();
|
||||
match &mut entry.member {
|
||||
ModMember::Item(ProjItem { kind: kref @ ItemKind::None }) =>
|
||||
*kref = ItemKind::Alias(alias_tgt),
|
||||
ModMember::Item(ProjItem { kind: ItemKind::Alias(prev_alias) }) =>
|
||||
if prev_alias != &alias_tgt {
|
||||
let local_name =
|
||||
globtree_prefix.clone().as_prefix_of(key.clone()).to_sym();
|
||||
let locs = pushed_ref(&entry.x.locations, location.clone());
|
||||
contention.insert((alias_tgt, local_name), locs);
|
||||
},
|
||||
_ => {
|
||||
let err = MultipleDefinitions {
|
||||
locations: entry.x.locations.clone(),
|
||||
path: globtree_prefix.as_prefix_of(key).to_sym(),
|
||||
};
|
||||
return Err(err.pack());
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
for (key, entry) in globtree.entries {
|
||||
match entry.member {
|
||||
ModMember::Item(n) => match n {},
|
||||
ModMember::Sub(module) => {
|
||||
let subpath = VPath(pushed_ref(&globtree_prefix.0, key));
|
||||
resolve_globs(
|
||||
subpath,
|
||||
module,
|
||||
preludes.clone(),
|
||||
project_root,
|
||||
env,
|
||||
contention,
|
||||
)?;
|
||||
},
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct MultipleExports {
|
||||
path: Sym,
|
||||
locations: Vec<CodeLocation>,
|
||||
}
|
||||
impl ProjectError for MultipleExports {
|
||||
const DESCRIPTION: &'static str = "A symbol was exported in multiple places";
|
||||
fn message(&self) -> String {
|
||||
format!("{} exported multiple times", self.path)
|
||||
}
|
||||
fn positions(&self) -> impl IntoIterator<Item = ErrorPosition> {
|
||||
(self.locations.iter())
|
||||
.map(|l| ErrorPosition { location: l.clone(), message: None })
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct MultipleDefinitions {
|
||||
pub(super) path: Sym,
|
||||
pub(super) locations: Vec<CodeLocation>,
|
||||
}
|
||||
impl ProjectError for MultipleDefinitions {
|
||||
const DESCRIPTION: &'static str = "Symbol defined twice";
|
||||
fn message(&self) -> String {
|
||||
format!("{} refers to multiple conflicting items", self.path)
|
||||
}
|
||||
fn positions(&self) -> impl IntoIterator<Item = ErrorPosition> {
|
||||
(self.locations.iter())
|
||||
.map(|l| ErrorPosition { location: l.clone(), message: None })
|
||||
}
|
||||
}
|
||||
271
src/pipeline/project.rs
Normal file
271
src/pipeline/project.rs
Normal file
@@ -0,0 +1,271 @@
|
||||
//! Datastructures used to define an Orchid project
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::sync::Arc;
|
||||
|
||||
use hashbrown::{HashMap, HashSet};
|
||||
use intern_all::Tok;
|
||||
use itertools::Itertools;
|
||||
use never::Never;
|
||||
use ordered_float::NotNan;
|
||||
use substack::Substack;
|
||||
|
||||
use crate::location::{CodeLocation, SourceRange};
|
||||
use crate::name::Sym;
|
||||
use crate::parse::numeric::print_nat16;
|
||||
use crate::parse::parsed::{Clause, Expr};
|
||||
use crate::tree::{ModEntry, ModMember, Module, ModMemberRef};
|
||||
use crate::utils::combine::Combine;
|
||||
use crate::utils::unwrap_or::unwrap_or;
|
||||
|
||||
/// Different elements that can appear in a module other than submodules
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ItemKind {
|
||||
/// An imported symbol or module. The value is the absolute path of
|
||||
/// the symbol that should be used instead of this one.
|
||||
Alias(Sym),
|
||||
/// This name is only used in macros
|
||||
None,
|
||||
/// This name has a value associated with it
|
||||
Const(Expr),
|
||||
}
|
||||
|
||||
impl Default for ItemKind {
|
||||
fn default() -> Self { Self::None }
|
||||
}
|
||||
|
||||
/// Element in a module
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProjItem {
|
||||
/// The nature of the element
|
||||
pub kind: ItemKind,
|
||||
}
|
||||
|
||||
impl Default for ProjItem {
|
||||
fn default() -> Self { Self { kind: ItemKind::None } }
|
||||
}
|
||||
|
||||
impl Display for ProjItem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.kind {
|
||||
ItemKind::None => write!(f, "keyword"),
|
||||
ItemKind::Const(c) => write!(f, "constant {c}"),
|
||||
ItemKind::Alias(alias) => write!(f, "alias to {alias}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Combine for ProjItem {
|
||||
type Error = Never;
|
||||
fn combine(self, _: Self) -> Result<Self, Self::Error> {
|
||||
unimplemented!("Only implied project modules can be merged, not items")
|
||||
}
|
||||
}
|
||||
|
||||
/// A substitution rule as stored in the tree
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProjRule {
|
||||
/// Tree fragment in the source code that activates this rule
|
||||
pub pattern: Vec<Expr>,
|
||||
/// Influences the order in which rules are checked
|
||||
pub prio: NotNan<f64>,
|
||||
/// Tree fragment generated by this rule
|
||||
pub template: Vec<Expr>,
|
||||
/// Comments associated with this rule
|
||||
pub comments: Vec<Arc<String>>,
|
||||
}
|
||||
|
||||
impl ProjRule {
|
||||
/// Namespace all tokens in the rule
|
||||
#[must_use]
|
||||
pub fn prefix(
|
||||
self,
|
||||
prefix: &[Tok<String>],
|
||||
except: &impl Fn(Tok<String>) -> bool,
|
||||
) -> Self {
|
||||
let Self { comments, prio, mut pattern, mut template } = self;
|
||||
(pattern.iter_mut())
|
||||
.chain(template.iter_mut())
|
||||
.for_each(|e| *e = e.prefix(prefix, except));
|
||||
Self { prio, comments, pattern, template }
|
||||
}
|
||||
|
||||
/// Return a list of all names that don't contain a namespace separator `::`.
|
||||
/// These are exported when the rule is exported
|
||||
#[must_use]
|
||||
pub fn collect_root_names(&self) -> HashSet<Tok<String>> {
|
||||
let mut names = HashSet::new();
|
||||
for e in self.pattern.iter() {
|
||||
e.search_all(&mut |e| {
|
||||
if let Clause::Name(ns_name) = &e.value {
|
||||
names.extend(ns_name[..].iter().exactly_one().ok().cloned())
|
||||
}
|
||||
None::<()>
|
||||
});
|
||||
}
|
||||
names
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ProjRule {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}rule {} ={}=> {}",
|
||||
self.comments.iter().map(|s| format!("--[{s}]--\n")).join(""),
|
||||
self.pattern.iter().join(" "),
|
||||
print_nat16(self.prio),
|
||||
self.template.iter().join(" ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a module that is defined in a source file
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SourceModule {
|
||||
/// All rules defined in this module, exported or not
|
||||
pub rules: Vec<ProjRule>,
|
||||
/// Location of this module.
|
||||
pub range: SourceRange,
|
||||
}
|
||||
|
||||
/// Additional data about a loaded module beyond the list of constants and
|
||||
/// submodules
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ProjXMod {
|
||||
/// Details only available for a module loaded from a source file
|
||||
pub src: Option<SourceModule>,
|
||||
}
|
||||
|
||||
impl Combine for ProjXMod {
|
||||
type Error = Never;
|
||||
fn combine(self, other: Self) -> Result<Self, Self::Error> {
|
||||
match (self.src, other.src) {
|
||||
(None, None) => Ok(Self { src: None }),
|
||||
(..) => panic!("Only implied modules can be merged"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a module entry
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ProjXEnt {
|
||||
/// All comments appearing above the item or submodule
|
||||
pub comments: Vec<Arc<String>>,
|
||||
/// Whether the member is visible to modules other than the parent
|
||||
pub exported: bool,
|
||||
/// Location of this item
|
||||
pub locations: Vec<CodeLocation>,
|
||||
}
|
||||
impl Default for ProjXEnt {
|
||||
fn default() -> Self {
|
||||
Self { comments: vec![], exported: true, locations: vec![] }
|
||||
}
|
||||
}
|
||||
impl ProjXEnt {
|
||||
/// Implied modules can be merged easily. It's difficult to detect whether
|
||||
/// a module is implied so we just assert that it doesn't have an associated
|
||||
/// source location
|
||||
pub fn is_default(&self) -> bool { self.locations.is_empty() }
|
||||
}
|
||||
impl Combine for ProjXEnt {
|
||||
type Error = MergingFiles;
|
||||
fn combine(self, other: Self) -> Result<Self, Self::Error> {
|
||||
(self.is_default() && other.is_default())
|
||||
.then_some(self)
|
||||
.ok_or(MergingFiles)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error produced when a module defined in a file has an alternate definition
|
||||
/// in another file or as a folder. This is explicitly banned because
|
||||
/// implementing a good version of it would require undue complexity
|
||||
#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq)]
|
||||
pub struct MergingFiles;
|
||||
|
||||
impl Display for ProjXMod {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut dbg = f.debug_struct("ProjectExt");
|
||||
match &self.src {
|
||||
None => dbg.finish_non_exhaustive(),
|
||||
Some(SourceModule { rules, range }) =>
|
||||
dbg.field("rules", &rules).field("range", &range).finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A child to a [ProjectMod]
|
||||
pub type ProjectEntry = ModEntry<ProjItem, ProjXMod, ProjXEnt>;
|
||||
/// A node in the tree describing the project
|
||||
pub type ProjectMod = Module<ProjItem, ProjXMod, ProjXEnt>;
|
||||
pub type ProjectMemberRef<'a> = ModMemberRef<'a, ProjItem, ProjXMod, ProjXEnt>;
|
||||
|
||||
fn collect_rules_rec(bag: &mut Vec<ProjRule>, module: &ProjectMod) {
|
||||
bag.extend(module.x.src.iter().flat_map(|s| &s.rules).cloned());
|
||||
for item in module.entries.values() {
|
||||
if let ModMember::Sub(module) = &item.member {
|
||||
collect_rules_rec(bag, module);
|
||||
}
|
||||
}
|
||||
}
|
||||
fn collect_consts_rec(
|
||||
path: Substack<Tok<String>>,
|
||||
bag: &mut HashMap<Sym, ConstReport>,
|
||||
module: &ProjectMod,
|
||||
) {
|
||||
for (key, entry) in module.entries.iter() {
|
||||
match &entry.member {
|
||||
ModMember::Item(it) =>
|
||||
if let ItemKind::Const(expr) = &it.kind {
|
||||
let name = path.push(key.clone()).iter().unreverse();
|
||||
let name = Sym::new(name).expect("pushed above");
|
||||
let location =
|
||||
entry.x.locations.first().cloned().unwrap_or_else(|| {
|
||||
panic!("{name} is a const in source, yet it has no location")
|
||||
});
|
||||
let range = unwrap_or!(location => CodeLocation::Source; {
|
||||
panic!("{name} is a const in source, yet its \
|
||||
location is generated")
|
||||
});
|
||||
bag.insert(name, ConstReport {
|
||||
comments: entry.x.comments.clone(),
|
||||
value: expr.clone(),
|
||||
range,
|
||||
});
|
||||
},
|
||||
ModMember::Sub(module) =>
|
||||
collect_consts_rec(path.push(key.clone()), bag, module),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Module corresponding to the root of a project
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProjectTree(pub ProjectMod);
|
||||
impl ProjectTree {
|
||||
/// Collect the complete list of rules to be used by the rule repository
|
||||
#[must_use]
|
||||
pub fn all_rules(&self) -> Vec<ProjRule> {
|
||||
let mut rules = Vec::new();
|
||||
collect_rules_rec(&mut rules, &self.0);
|
||||
rules
|
||||
}
|
||||
|
||||
/// Extract the symbol table
|
||||
#[must_use]
|
||||
pub fn all_consts(&self) -> HashMap<Sym, ConstReport> {
|
||||
let mut consts = HashMap::new();
|
||||
collect_consts_rec(Substack::Bottom, &mut consts, &self.0);
|
||||
consts
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about a constant
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ConstReport {
|
||||
/// Operational comments
|
||||
pub comments: Vec<Arc<String>>,
|
||||
/// Value assigned to the constant
|
||||
pub value: Expr,
|
||||
pub range: SourceRange,
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
use hashbrown::HashMap;
|
||||
use itertools::{Either, Itertools};
|
||||
|
||||
use super::import_tree::ImpMod;
|
||||
use crate::ast::{Constant, Rule};
|
||||
use crate::error::{ConflictingRoles, ProjectError, ProjectResult};
|
||||
use crate::pipeline::source_loader::{PreItem, PreMod};
|
||||
use crate::representations::project::{
|
||||
ImpReport, ItemKind, ProjectEntry, ProjectExt, ProjectItem,
|
||||
};
|
||||
use crate::sourcefile::{
|
||||
FileEntry, FileEntryKind, Member, MemberKind, ModuleBlock,
|
||||
};
|
||||
use crate::tree::{ModEntry, ModMember, Module};
|
||||
use crate::utils::get_or::get_or_default;
|
||||
use crate::utils::pure_seq::pushed_ref;
|
||||
use crate::{Tok, VName};
|
||||
|
||||
#[must_use = "A submodule may not be integrated into the tree"]
|
||||
pub struct TreeReport {
|
||||
pub entries: HashMap<Tok<String>, ProjectEntry<VName>>,
|
||||
pub rules: Vec<Rule<VName>>,
|
||||
/// Maps imported symbols to the absolute paths of the modules they are
|
||||
/// imported from
|
||||
pub imports_from: HashMap<Tok<String>, ImpReport<VName>>,
|
||||
}
|
||||
|
||||
pub fn build_tree(
|
||||
path: &VName,
|
||||
source: Vec<FileEntry>,
|
||||
Module { entries, .. }: PreMod,
|
||||
imports: ImpMod,
|
||||
prelude: &[FileEntry],
|
||||
) -> ProjectResult<TreeReport> {
|
||||
let source =
|
||||
source.into_iter().chain(prelude.iter().cloned()).collect::<Vec<_>>();
|
||||
let (imports_from, mut submod_imports) = (imports.entries.into_iter())
|
||||
.partition_map::<HashMap<_, _>, HashMap<_, _>, _, _, _>(
|
||||
|(n, ent)| match ent.member {
|
||||
ModMember::Item(it) => Either::Left((n, it)),
|
||||
ModMember::Sub(s) => Either::Right((n, s)),
|
||||
},
|
||||
);
|
||||
let mut rule_fragments = Vec::new();
|
||||
let mut submodules = HashMap::<_, Vec<_>>::new();
|
||||
let mut consts = HashMap::new();
|
||||
for FileEntry { kind, locations: _ } in source {
|
||||
match kind {
|
||||
FileEntryKind::Import(_) => (),
|
||||
FileEntryKind::Comment(_) => (),
|
||||
FileEntryKind::Export(_) => (),
|
||||
FileEntryKind::Member(Member { kind, .. }) => match kind {
|
||||
MemberKind::Module(ModuleBlock { body, name }) => {
|
||||
get_or_default(&mut submodules, &name).extend(body.into_iter());
|
||||
},
|
||||
MemberKind::Constant(Constant { name, value }) => {
|
||||
consts.insert(name, value /* .prefix(path, &|_| false) */);
|
||||
},
|
||||
MemberKind::Rule(rule) => rule_fragments.push(rule),
|
||||
},
|
||||
}
|
||||
}
|
||||
let rules = rule_fragments;
|
||||
let (pre_subs, pre_items) = (entries.into_iter())
|
||||
.partition_map::<HashMap<_, _>, HashMap<_, _>, _, _, _>(
|
||||
|(k, ModEntry { exported, member })| match member {
|
||||
ModMember::Sub(s) => Either::Left((k, (exported, s))),
|
||||
ModMember::Item(it) => Either::Right((k, (exported, it))),
|
||||
},
|
||||
);
|
||||
let mut entries = (pre_subs.into_iter())
|
||||
.map(|(k, (exported, pre_member))| {
|
||||
let impmod = (submod_imports.remove(&k))
|
||||
.expect("Imports and preparsed should line up");
|
||||
(k, exported, pre_member, impmod)
|
||||
})
|
||||
.map(|(k, exported, pre, imp)| {
|
||||
let source = submodules
|
||||
.remove(&k)
|
||||
.expect("Submodules should not disappear after reparsing");
|
||||
(k, exported, pre, imp, source)
|
||||
})
|
||||
.map(|(k, exported, pre, imp, source)| {
|
||||
let path = pushed_ref(path, k.clone());
|
||||
let TreeReport { entries, imports_from, rules } =
|
||||
build_tree(&path, source, pre, imp, prelude)?;
|
||||
let extra = ProjectExt { path, file: None, imports_from, rules };
|
||||
let member = ModMember::Sub(Module { entries, extra });
|
||||
Ok((k, ModEntry { exported, member }))
|
||||
})
|
||||
.chain((pre_items.into_iter()).map(
|
||||
|(k, (exported, PreItem { has_value, location }))| {
|
||||
let item = match imports_from.get(&k) {
|
||||
Some(_) if has_value => {
|
||||
// Local value cannot be assigned to imported key
|
||||
let const_loc =
|
||||
consts.remove(&k).expect("has_value is true").location;
|
||||
let err = ConflictingRoles {
|
||||
locations: vec![location, const_loc],
|
||||
name: pushed_ref(path, k),
|
||||
};
|
||||
return Err(err.rc());
|
||||
},
|
||||
None => {
|
||||
let k = consts.remove(&k).map_or(ItemKind::None, ItemKind::Const);
|
||||
ProjectItem { kind: k }
|
||||
},
|
||||
Some(report) =>
|
||||
ProjectItem { kind: ItemKind::Alias(report.source.clone()) },
|
||||
};
|
||||
Ok((k, ModEntry { exported, member: ModMember::Item(item) }))
|
||||
},
|
||||
))
|
||||
.collect::<Result<HashMap<_, _>, _>>()?;
|
||||
for (k, from) in imports_from.iter() {
|
||||
let (_, ent) = entries.raw_entry_mut().from_key(k).or_insert_with(|| {
|
||||
(k.clone(), ModEntry {
|
||||
exported: false,
|
||||
member: ModMember::Item(ProjectItem {
|
||||
kind: ItemKind::Alias(from.source.clone()),
|
||||
}),
|
||||
})
|
||||
});
|
||||
debug_assert!(
|
||||
matches!(
|
||||
ent.member,
|
||||
ModMember::Item(ProjectItem { kind: ItemKind::Alias(_), .. })
|
||||
),
|
||||
"Should have emerged in the processing of pre_items"
|
||||
)
|
||||
}
|
||||
Ok(TreeReport { entries, rules, imports_from })
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use itertools::Itertools;
|
||||
|
||||
use crate::error::{ProjectError, ProjectResult};
|
||||
use crate::pipeline::source_loader::{PreMod, Preparsed};
|
||||
use crate::representations::project::ImpReport;
|
||||
use crate::sourcefile::{absolute_path, Import};
|
||||
use crate::tree::{ErrKind, ModEntry, ModMember, Module, WalkError};
|
||||
use crate::utils::boxed_iter::{box_chain, box_once};
|
||||
use crate::utils::pure_seq::pushed_ref;
|
||||
use crate::utils::{unwrap_or, BoxedIter};
|
||||
use crate::{Interner, ProjectTree, Tok, VName};
|
||||
|
||||
pub type ImpMod = Module<ImpReport<VName>, ()>;
|
||||
|
||||
/// Assert that a module identified by a path can see a given symbol
|
||||
fn assert_visible<'a>(
|
||||
source: &'a [Tok<String>], // must point to a file or submodule
|
||||
target: &'a [Tok<String>],
|
||||
root: &'a Module<impl Clone + Display, impl Clone + Display>,
|
||||
) -> Result<(), WalkError<'a>> {
|
||||
if target.split_last().map_or(true, |(_, m)| source.starts_with(m)) {
|
||||
// The global module (empty path) is always visible
|
||||
return Ok(()); // Symbols in ancestor modules are always visible
|
||||
}
|
||||
// walk the first section where visibility is ignored
|
||||
let shared_len =
|
||||
source.iter().zip(target.iter()).take_while(|(a, b)| a == b).count();
|
||||
let (shared_path, deep_path) = target.split_at(shared_len + 1);
|
||||
let private_root = root.walk_ref(&[], shared_path, false)?;
|
||||
// walk the second part where visibility matters
|
||||
private_root.walk1_ref(shared_path, deep_path, true)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn assert_visible_overlay<'a>(
|
||||
source: &'a [Tok<String>], // must point to a file or submodule
|
||||
target: &'a [Tok<String>],
|
||||
first: &'a Module<impl Clone + Display, impl Clone + Display>,
|
||||
fallback: &'a Module<impl Clone + Display, impl Clone + Display>,
|
||||
) -> Result<(), WalkError<'a>> {
|
||||
assert_visible(source, target, first).or_else(|e1| {
|
||||
if e1.kind == ErrKind::Missing {
|
||||
match assert_visible(source, target, fallback) {
|
||||
// if both are walk errors, report the longer of the two
|
||||
Err(mut e2) if e2.kind == ErrKind::Missing =>
|
||||
Err(match e1.depth().cmp(&e2.depth()) {
|
||||
Ordering::Less => e2,
|
||||
Ordering::Greater => e1,
|
||||
Ordering::Equal => {
|
||||
e2.options = box_chain!(e2.options, e1.options);
|
||||
e2
|
||||
},
|
||||
}),
|
||||
// otherwise return the parent's result
|
||||
x => x,
|
||||
}
|
||||
} else {
|
||||
Err(e1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn process_donor_module<TItem: Clone>(
|
||||
module: &Module<TItem, impl Clone>,
|
||||
abs_path: Rc<VName>,
|
||||
) -> impl Iterator<Item = (Tok<String>, VName)> + '_ {
|
||||
(module.entries.iter())
|
||||
.filter(|(_, ent)| ent.exported)
|
||||
.map(move |(n, _)| (n.clone(), pushed_ref(abs_path.as_ref(), n.clone())))
|
||||
}
|
||||
|
||||
pub fn import_tree(
|
||||
modpath: VName,
|
||||
pre: &PreMod,
|
||||
root: &Preparsed,
|
||||
prev_root: &ProjectTree<VName>,
|
||||
i: &Interner,
|
||||
) -> ProjectResult<ImpMod> {
|
||||
let imports = pre.extra.details().map_or(&[][..], |e| &e.imports[..]);
|
||||
let entries = (imports.iter())
|
||||
// imports become leaf sets
|
||||
.map(|Import { name, path, location }| -> ProjectResult<BoxedIter<_>> {
|
||||
let mut abs_path = absolute_path(&modpath, path, i, location)?;
|
||||
Ok(if let Some(name) = name {
|
||||
// named imports are validated and translated 1->1
|
||||
abs_path.push(name.clone());
|
||||
assert_visible_overlay(&modpath, &abs_path, &root.0, &prev_root.0)
|
||||
.map_err(|e| -> Rc<dyn ProjectError> {
|
||||
println!("Current root: {}", &root.0);
|
||||
// println!("Old root: {:#?}", &prev_root.0);
|
||||
panic!("{}", e.at(location))
|
||||
})?;
|
||||
box_once((name.clone(), abs_path))
|
||||
} else {
|
||||
let rc_path = Rc::new(abs_path);
|
||||
// wildcard imports are validated
|
||||
assert_visible_overlay(&modpath, &rc_path, &root.0, &prev_root.0)
|
||||
.map_err(|e| e.at(location))?;
|
||||
// and instantiated for all exports of the target 1->n
|
||||
let new_imports = match (root.0).walk_ref(&[], &rc_path, false) {
|
||||
Err(e) if e.kind == ErrKind::Missing => Err(e),
|
||||
Err(e) => return Err(e.at(location)),
|
||||
Ok(module) => Ok(process_donor_module(module, rc_path.clone()))
|
||||
};
|
||||
let old_m = match (prev_root.0).walk_ref(&[], &rc_path, false) {
|
||||
Err(e) if e.kind != ErrKind::Missing => return Err(e.at(location)),
|
||||
Err(e1) => match new_imports {
|
||||
Ok(it) => return Ok(Box::new(it)),
|
||||
Err(mut e2) => return Err(match e1.depth().cmp(&e2.depth()) {
|
||||
Ordering::Less => e2.at(location),
|
||||
Ordering::Greater => e1.at(location),
|
||||
Ordering::Equal => {
|
||||
e2.options = box_chain!(e2.options, e1.options);
|
||||
e2.at(location)
|
||||
},
|
||||
}),
|
||||
},
|
||||
Ok(old_m) => old_m,
|
||||
};
|
||||
let it1 = process_donor_module(old_m, rc_path.clone());
|
||||
match new_imports {
|
||||
Err(_) => Box::new(it1),
|
||||
Ok(it2) => box_chain!(it1, it2)
|
||||
}
|
||||
})
|
||||
})
|
||||
// leaf sets flattened to leaves
|
||||
.flatten_ok()
|
||||
// translated to entries
|
||||
.map_ok(|(name, source)| {
|
||||
(name, ModEntry {
|
||||
exported: false, // this is irrelevant but needed
|
||||
member: ModMember::Item(ImpReport { source }),
|
||||
})
|
||||
})
|
||||
.chain(
|
||||
(pre.entries.iter())
|
||||
// recurse on submodules
|
||||
.filter_map(|(k, v)| {
|
||||
Some((k, v, unwrap_or!(&v.member => ModMember::Sub; return None)))
|
||||
})
|
||||
.map(|(k, v, pre)| {
|
||||
let path = pushed_ref(&modpath, k.clone());
|
||||
Ok((k.clone(), ModEntry {
|
||||
exported: v.exported,
|
||||
member: ModMember::Sub(import_tree(path, pre, root, prev_root, i)?),
|
||||
}))
|
||||
}),
|
||||
)
|
||||
.collect::<Result<HashMap<_, _>, _>>()?;
|
||||
Ok(Module { extra: (), entries })
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
mod build_tree;
|
||||
mod import_tree;
|
||||
mod rebuild_tree;
|
||||
|
||||
pub use rebuild_tree::rebuild_tree;
|
||||
@@ -1,98 +0,0 @@
|
||||
use hashbrown::HashMap;
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::build_tree::{build_tree, TreeReport};
|
||||
use super::import_tree::{import_tree, ImpMod};
|
||||
use crate::error::ProjectResult;
|
||||
use crate::pipeline::source_loader::{
|
||||
LoadedSourceTable, PreExtra, PreMod, Preparsed,
|
||||
};
|
||||
use crate::representations::project::{ProjectExt, ProjectMod};
|
||||
use crate::sourcefile::FileEntry;
|
||||
use crate::tree::{ModEntry, ModMember, Module};
|
||||
use crate::utils::pure_seq::pushed_ref;
|
||||
use crate::utils::unwrap_or;
|
||||
use crate::{Interner, ProjectTree, Tok, VName};
|
||||
|
||||
pub fn rebuild_file(
|
||||
path: Vec<Tok<String>>,
|
||||
pre: PreMod,
|
||||
imports: ImpMod,
|
||||
source: &LoadedSourceTable,
|
||||
prelude: &[FileEntry],
|
||||
) -> ProjectResult<ProjectMod<VName>> {
|
||||
let file = match &pre.extra {
|
||||
PreExtra::Dir => panic!("Dir should not hand this node off"),
|
||||
PreExtra::Submod(_) => panic!("should not have received this"),
|
||||
PreExtra::File(f) => f,
|
||||
};
|
||||
let src = source.get(&file.name).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"{} should have been preparsed already. Preparsed files are {}",
|
||||
Interner::extern_all(&file.name).join("/") + ".orc",
|
||||
source
|
||||
.keys()
|
||||
.map(|f| Interner::extern_all(f).join("/") + ".orc")
|
||||
.join(", ")
|
||||
)
|
||||
});
|
||||
let entries = src.entries.clone();
|
||||
let TreeReport { entries, rules, imports_from } =
|
||||
build_tree(&path, entries, pre, imports, prelude)?;
|
||||
let file = Some(path.clone());
|
||||
Ok(Module { entries, extra: ProjectExt { file, path, imports_from, rules } })
|
||||
}
|
||||
|
||||
pub fn rebuild_dir(
|
||||
path: Vec<Tok<String>>,
|
||||
pre: PreMod,
|
||||
mut imports: ImpMod,
|
||||
source: &LoadedSourceTable,
|
||||
prelude: &[FileEntry],
|
||||
) -> ProjectResult<ProjectMod<VName>> {
|
||||
match pre.extra {
|
||||
PreExtra::Dir => (),
|
||||
PreExtra::File(_) =>
|
||||
return rebuild_file(path, pre, imports, source, prelude),
|
||||
PreExtra::Submod(_) => panic!("Dirs contain dirs and files"),
|
||||
}
|
||||
let entries = (pre.entries.into_iter())
|
||||
.map(|(name, entry)| {
|
||||
match imports.entries.remove(&name).map(|e| e.member) {
|
||||
Some(ModMember::Sub(impmod)) => (name, entry, impmod),
|
||||
_ => panic!("Imports must line up with modules"),
|
||||
}
|
||||
})
|
||||
.map(|(name, ModEntry { member, exported }, impmod)| -> ProjectResult<_> {
|
||||
let path = pushed_ref(&path, name.clone());
|
||||
let pre = unwrap_or!(member => ModMember::Sub;
|
||||
panic!("Dirs can only contain submodules")
|
||||
);
|
||||
let module = rebuild_dir(path, pre, impmod, source, prelude)?;
|
||||
Ok((name, ModEntry { exported, member: ModMember::Sub(module) }))
|
||||
})
|
||||
.collect::<Result<HashMap<_, _>, _>>()?;
|
||||
Ok(Module {
|
||||
extra: ProjectExt {
|
||||
path,
|
||||
imports_from: HashMap::new(),
|
||||
rules: Vec::new(),
|
||||
file: None,
|
||||
},
|
||||
entries,
|
||||
})
|
||||
}
|
||||
|
||||
/// Rebuild the entire tree
|
||||
pub fn rebuild_tree(
|
||||
source: &LoadedSourceTable,
|
||||
preparsed: Preparsed,
|
||||
prev_root: &ProjectTree<VName>,
|
||||
prelude: &[FileEntry],
|
||||
i: &Interner,
|
||||
) -> ProjectResult<ProjectTree<VName>> {
|
||||
let imports =
|
||||
import_tree(Vec::new(), &preparsed.0, &preparsed, prev_root, i)?;
|
||||
rebuild_dir(Vec::new(), preparsed.0, imports, source, prelude)
|
||||
.map(ProjectTree)
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
|
||||
use super::loaded_source::{LoadedSource, LoadedSourceTable};
|
||||
use super::preparse::preparse;
|
||||
use super::{PreExtra, Preparsed};
|
||||
use crate::error::{
|
||||
NoTargets, ProjectError, ProjectResult, UnexpectedDirectory,
|
||||
};
|
||||
use crate::interner::{Interner, Tok};
|
||||
use crate::parse::{self, LexerPlugin, LineParser, ParsingContext};
|
||||
use crate::pipeline::file_loader::{IOResult, Loaded};
|
||||
use crate::pipeline::import_abs_path::import_abs_path;
|
||||
use crate::representations::sourcefile::FileEntry;
|
||||
use crate::tree::Module;
|
||||
use crate::utils::pure_seq::pushed_ref;
|
||||
use crate::utils::{split_max_prefix, unwrap_or};
|
||||
use crate::Location;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Context<'a> {
|
||||
pub prelude: &'a [FileEntry],
|
||||
pub i: &'a Interner,
|
||||
pub lexer_plugins: &'a [&'a dyn LexerPlugin],
|
||||
pub line_parsers: &'a [&'a dyn LineParser],
|
||||
}
|
||||
|
||||
/// Load the source at the given path or all within if it's a collection,
|
||||
/// and all sources imported from these.
|
||||
fn load_abs_path_rec(
|
||||
referrer: &[Tok<String>],
|
||||
abs_path: &[Tok<String>],
|
||||
mut all: Preparsed,
|
||||
source: &mut LoadedSourceTable,
|
||||
get_source: &impl Fn(&[Tok<String>], &[Tok<String>]) -> IOResult,
|
||||
is_injected_module: &impl Fn(&[Tok<String>]) -> bool,
|
||||
ctx @ Context { i, lexer_plugins, line_parsers, prelude }: Context,
|
||||
) -> ProjectResult<Preparsed> {
|
||||
// # Termination
|
||||
//
|
||||
// Every recursion of this function either
|
||||
// - adds one of the files in the source directory to `visited` or
|
||||
// - recursively traverses a directory tree
|
||||
// therefore eventually the function exits, assuming that the directory tree
|
||||
// contains no cycles.
|
||||
|
||||
// try splitting the path to file, swallowing any IO errors
|
||||
let name_split = split_max_prefix(abs_path, &|p| {
|
||||
get_source(p, referrer).map(|l| l.is_code()).unwrap_or(false)
|
||||
});
|
||||
if let Some((filename, _)) = name_split {
|
||||
// Termination: exit if entry already visited
|
||||
if source.contains_key(filename) {
|
||||
return Ok(all);
|
||||
}
|
||||
// if the filename is valid, load, preparse and record this file
|
||||
let text = unwrap_or!(get_source(filename, referrer)? => Loaded::Code; {
|
||||
return Err(UnexpectedDirectory { path: filename.to_vec() }.rc())
|
||||
});
|
||||
let entries = parse::parse_file(ParsingContext::new(
|
||||
i,
|
||||
Arc::new(filename.to_vec()),
|
||||
text,
|
||||
lexer_plugins,
|
||||
line_parsers,
|
||||
))?;
|
||||
let preparsed = preparse(filename.to_vec(), entries.clone(), prelude)?;
|
||||
source.insert(filename.to_vec(), LoadedSource { entries });
|
||||
// recurse on all imported modules
|
||||
// will be taken and returned by the closure. None iff an error is thrown
|
||||
all = preparsed.0.search_all(all, &mut |modpath,
|
||||
module,
|
||||
mut all|
|
||||
-> ProjectResult<_> {
|
||||
let details = unwrap_or!(module.extra.details(); return Ok(all));
|
||||
let referrer = modpath.iter().rev_vec_clone();
|
||||
for import in &details.imports {
|
||||
let origin = &Location::Unknown;
|
||||
let abs_pathv = import_abs_path(
|
||||
filename,
|
||||
modpath.clone(),
|
||||
&import.nonglob_path(),
|
||||
i,
|
||||
origin,
|
||||
)?;
|
||||
if abs_path.starts_with(&abs_pathv) {
|
||||
continue;
|
||||
}
|
||||
// recurse on imported module
|
||||
all = load_abs_path_rec(
|
||||
&referrer,
|
||||
&abs_pathv,
|
||||
all,
|
||||
source,
|
||||
get_source,
|
||||
is_injected_module,
|
||||
ctx,
|
||||
)?;
|
||||
}
|
||||
Ok(all)
|
||||
})?;
|
||||
// Combine the trees
|
||||
all.0.overlay(preparsed.0).map(Preparsed)
|
||||
} else {
|
||||
// If the path is not within a file, load it as directory
|
||||
let coll = match get_source(abs_path, referrer) {
|
||||
Ok(Loaded::Collection(coll)) => coll,
|
||||
Ok(Loaded::Code(_)) => {
|
||||
unreachable!("split_name returned None but the path is a file")
|
||||
},
|
||||
Err(e) => {
|
||||
// todo: if this can actually be produced, return Err(ImportAll) instead
|
||||
let parent = abs_path.split_last().expect("import path nonzero").1;
|
||||
// exit without error if it was injected, or raise any IO error that was
|
||||
// previously swallowed
|
||||
return if is_injected_module(parent) { Ok(all) } else { Err(e) };
|
||||
},
|
||||
};
|
||||
// recurse on all files and folders within
|
||||
for item in coll.iter() {
|
||||
let abs_subpath = pushed_ref(abs_path, i.i(item));
|
||||
all = load_abs_path_rec(
|
||||
referrer,
|
||||
&abs_subpath,
|
||||
all,
|
||||
source,
|
||||
get_source,
|
||||
is_injected_module,
|
||||
ctx,
|
||||
)?;
|
||||
}
|
||||
Ok(all)
|
||||
}
|
||||
}
|
||||
|
||||
/// Load and preparse all files reachable from the load targets via
|
||||
/// imports that aren't injected.
|
||||
///
|
||||
/// is_injected_module must return false for injected symbols, but may return
|
||||
/// true for parents of injected modules that are not directly part of the
|
||||
/// injected data (the ProjectTree doesn't make a distinction between the two)
|
||||
pub fn load_source<'a>(
|
||||
targets: impl Iterator<Item = &'a [Tok<String>]>,
|
||||
ctx: Context,
|
||||
get_source: &impl Fn(&[Tok<String>], &[Tok<String>]) -> IOResult,
|
||||
is_injected_module: &impl Fn(&[Tok<String>]) -> bool,
|
||||
) -> ProjectResult<(Preparsed, LoadedSourceTable)> {
|
||||
let mut table = LoadedSourceTable::new();
|
||||
let mut all =
|
||||
Preparsed(Module { extra: PreExtra::Dir, entries: HashMap::new() });
|
||||
let mut any_target = false;
|
||||
for target in targets {
|
||||
any_target |= true;
|
||||
all = load_abs_path_rec(
|
||||
&[],
|
||||
target,
|
||||
all,
|
||||
&mut table,
|
||||
get_source,
|
||||
is_injected_module,
|
||||
ctx,
|
||||
)?;
|
||||
}
|
||||
if any_target { Ok((all, table)) } else { Err(NoTargets.rc()) }
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::representations::VName;
|
||||
use crate::sourcefile::FileEntry;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct LoadedSource {
|
||||
pub entries: Vec<FileEntry>,
|
||||
}
|
||||
|
||||
pub type LoadedSourceTable = HashMap<VName, LoadedSource>;
|
||||
@@ -1,25 +0,0 @@
|
||||
// PULL LOGISTICS BOUNDARY
|
||||
//
|
||||
// Specifying exactly what this module should be doing was an unexpectedly
|
||||
// hard challenge. It is intended to encapsulate all pull logistics, but
|
||||
// this definition is apparently prone to scope creep.
|
||||
//
|
||||
// Load files, preparse them to obtain a list of imports, follow these.
|
||||
// Preparsing also returns the module tree and list of exported synbols
|
||||
// for free, which is needed later so the output of preparsing is also
|
||||
// attached to the module output.
|
||||
//
|
||||
// The module checks for IO errors, syntax errors, malformed imports and
|
||||
// imports from missing files. All other errors must be checked later.
|
||||
//
|
||||
// Injection strategy:
|
||||
// see whether names are valid in the injected tree for is_injected
|
||||
|
||||
mod load_source;
|
||||
mod loaded_source;
|
||||
mod preparse;
|
||||
mod types;
|
||||
|
||||
pub use load_source::{load_source, Context};
|
||||
pub use loaded_source::{LoadedSource, LoadedSourceTable};
|
||||
pub use types::{PreExtra, PreFileExt, PreItem, PreMod, PreSubExt, Preparsed};
|
||||
@@ -1,147 +0,0 @@
|
||||
use hashbrown::HashMap;
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::types::{PreFileExt, PreItem, PreSubExt};
|
||||
use super::{PreExtra, Preparsed};
|
||||
use crate::ast::{Clause, Constant};
|
||||
use crate::error::{
|
||||
ConflictingRoles, ProjectError, ProjectResult, VisibilityMismatch,
|
||||
};
|
||||
use crate::representations::sourcefile::{FileEntry, MemberKind};
|
||||
use crate::representations::tree::{ModEntry, ModMember, Module};
|
||||
use crate::sourcefile::{FileEntryKind, Import, Member, ModuleBlock};
|
||||
use crate::utils::get_or::{get_or_default, get_or_make};
|
||||
use crate::utils::pure_seq::pushed;
|
||||
use crate::{Location, Tok, VName};
|
||||
|
||||
struct FileReport {
|
||||
entries: HashMap<Tok<String>, ModEntry<PreItem, PreExtra>>,
|
||||
imports: Vec<Import>,
|
||||
}
|
||||
|
||||
/// Convert source lines into a module
|
||||
fn to_module(
|
||||
file: &[Tok<String>],
|
||||
path: VName,
|
||||
src: Vec<FileEntry>,
|
||||
prelude: &[FileEntry],
|
||||
) -> ProjectResult<FileReport> {
|
||||
let mut imports = Vec::new();
|
||||
let mut items = HashMap::<Tok<String>, (bool, PreItem)>::new();
|
||||
let mut to_export = HashMap::<Tok<String>, Vec<Location>>::new();
|
||||
let mut submods =
|
||||
HashMap::<Tok<String>, (bool, Vec<Location>, Vec<FileEntry>)>::new();
|
||||
let entries = prelude.iter().cloned().chain(src);
|
||||
for FileEntry { kind, locations } in entries {
|
||||
match kind {
|
||||
FileEntryKind::Import(imp) => imports.extend(imp.into_iter()),
|
||||
FileEntryKind::Export(names) =>
|
||||
for (t, l) in names {
|
||||
get_or_default(&mut to_export, &t).push(l)
|
||||
},
|
||||
FileEntryKind::Member(Member { exported, kind }) => match kind {
|
||||
MemberKind::Constant(Constant { name, .. }) => {
|
||||
let (prev_exported, it) = get_or_default(&mut items, &name);
|
||||
if it.has_value {
|
||||
let err = ConflictingRoles { name: pushed(path, name), locations };
|
||||
return Err(err.rc());
|
||||
}
|
||||
if let Some(loc) = locations.get(0) {
|
||||
it.location = it.location.clone().or(loc.clone())
|
||||
};
|
||||
it.has_value = true;
|
||||
*prev_exported |= exported;
|
||||
},
|
||||
MemberKind::Module(ModuleBlock { name, body }) => {
|
||||
if let Some((prev_exported, locv, entv)) = submods.get_mut(&name) {
|
||||
if *prev_exported != exported {
|
||||
let mut namespace = path;
|
||||
namespace.push(name.clone());
|
||||
let err = VisibilityMismatch { namespace, file: file.to_vec() };
|
||||
return Err(err.rc());
|
||||
}
|
||||
locv.extend(locations.into_iter());
|
||||
entv.extend(body.into_iter())
|
||||
} else {
|
||||
submods.insert(name.clone(), (exported, locations, body.clone()));
|
||||
}
|
||||
},
|
||||
MemberKind::Rule(r) =>
|
||||
if exported {
|
||||
for ex in r.pattern {
|
||||
ex.search_all(&mut |ex| {
|
||||
if let Clause::Name(vname) = &ex.value {
|
||||
if let Ok(name) = vname.iter().exactly_one() {
|
||||
get_or_default(&mut to_export, name)
|
||||
.push(ex.location.clone());
|
||||
}
|
||||
}
|
||||
None::<()>
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
let mut entries = HashMap::with_capacity(items.len() + submods.len());
|
||||
entries.extend(items.into_iter().map(|(name, (exported, it))| {
|
||||
(name, ModEntry { member: ModMember::Item(it), exported })
|
||||
}));
|
||||
for (subname, (exported, locations, body)) in submods {
|
||||
let mut name = path.clone();
|
||||
entries
|
||||
.try_insert(subname.clone(), ModEntry {
|
||||
member: ModMember::Sub({
|
||||
name.push(subname);
|
||||
let FileReport { imports, entries: items } =
|
||||
to_module(file, name.clone(), body, prelude)?;
|
||||
Module {
|
||||
entries: items,
|
||||
extra: PreExtra::Submod(PreSubExt { imports }),
|
||||
}
|
||||
}),
|
||||
exported,
|
||||
})
|
||||
.map_err(|_| ConflictingRoles { locations, name }.rc())?;
|
||||
}
|
||||
for (item, locations) in to_export {
|
||||
get_or_make(&mut entries, &item, || ModEntry {
|
||||
member: ModMember::Item(PreItem {
|
||||
has_value: false,
|
||||
location: locations[0].clone(),
|
||||
}),
|
||||
exported: true,
|
||||
})
|
||||
.exported = true
|
||||
}
|
||||
Ok(FileReport { entries, imports })
|
||||
}
|
||||
|
||||
/// Preparse the module. At this stage, only the imports and
|
||||
/// names defined by the module can be parsed
|
||||
pub fn preparse(
|
||||
file: VName,
|
||||
entries: Vec<FileEntry>,
|
||||
prelude: &[FileEntry],
|
||||
) -> ProjectResult<Preparsed> {
|
||||
let FileReport { entries, imports } =
|
||||
to_module(&file, file.clone(), entries, prelude)?;
|
||||
let mut module = Module {
|
||||
entries,
|
||||
extra: PreExtra::File(PreFileExt {
|
||||
details: PreSubExt { imports },
|
||||
name: file.clone(),
|
||||
}),
|
||||
};
|
||||
for name in file.iter().rev() {
|
||||
module = Module {
|
||||
extra: PreExtra::Dir,
|
||||
entries: HashMap::from([(name.clone(), ModEntry {
|
||||
exported: true,
|
||||
member: ModMember::Sub(module),
|
||||
})]),
|
||||
};
|
||||
}
|
||||
Ok(Preparsed(module))
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
use std::fmt::Display;
|
||||
use std::ops::Add;
|
||||
|
||||
use crate::error::ProjectResult;
|
||||
use crate::sourcefile::Import;
|
||||
use crate::tree::Module;
|
||||
use crate::{Interner, Location, VName};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PreItem {
|
||||
pub has_value: bool,
|
||||
pub location: Location,
|
||||
}
|
||||
|
||||
impl Display for PreItem {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let Self { has_value, location } = self;
|
||||
let description = if *has_value { "value" } else { "keyword" };
|
||||
write!(f, "{description} {location}")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PreItem {
|
||||
fn default() -> Self {
|
||||
PreItem { has_value: false, location: Location::Unknown }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PreSubExt {
|
||||
pub imports: Vec<Import>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PreFileExt {
|
||||
pub name: VName,
|
||||
pub details: PreSubExt,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PreExtra {
|
||||
File(PreFileExt),
|
||||
Submod(PreSubExt),
|
||||
Dir,
|
||||
}
|
||||
|
||||
impl PreExtra {
|
||||
/// If the module is not a directory, returns the source-only details
|
||||
pub fn details(&self) -> Option<&PreSubExt> {
|
||||
match self {
|
||||
Self::Submod(sub) => Some(sub),
|
||||
Self::File(PreFileExt { details, .. }) => Some(details),
|
||||
Self::Dir => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for PreExtra {
|
||||
type Output = ProjectResult<Self>;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
match (self, rhs) {
|
||||
(alt, Self::Dir) | (Self::Dir, alt) => Ok(alt),
|
||||
(Self::File(_) | Self::Submod(_), Self::File(_) | Self::Submod(_)) => {
|
||||
panic!("Each file should be parsed once.")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PreExtra {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Dir => write!(f, "Directory"),
|
||||
Self::File(PreFileExt { name, .. }) => {
|
||||
write!(f, "File({}.orc)", Interner::extern_all(name).join("/"))
|
||||
},
|
||||
Self::Submod(_) => write!(f, "Submodule"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type PreMod = Module<PreItem, PreExtra>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Preparsed(pub PreMod);
|
||||
Reference in New Issue
Block a user