Backup commit

My backspace key started ghosting. Nothing works atm.
This commit is contained in:
2024-01-27 14:50:33 +00:00
parent f77e4fd90a
commit a8887227e5
236 changed files with 10946 additions and 8977 deletions

View File

@@ -1,4 +1,2 @@
mod resolve_aliases;
pub mod resolve_aliases;
mod walk_with_links;
pub use resolve_aliases::resolve_aliases;

View File

@@ -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)
}

View File

@@ -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());

View File

@@ -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()
}

View File

@@ -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)
}

View 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 })
}
}

View File

@@ -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;

View File

@@ -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
View 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() }
}

View 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
View 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,
}

View File

@@ -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 })
}

View File

@@ -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 })
}

View File

@@ -1,5 +0,0 @@
mod build_tree;
mod import_tree;
mod rebuild_tree;
pub use rebuild_tree::rebuild_tree;

View File

@@ -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)
}

View File

@@ -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()) }
}

View File

@@ -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>;

View File

@@ -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};

View File

@@ -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))
}

View File

@@ -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);