use ast;
use core::errors::{Error, Result};
use core::{translator, Context, CoreFlavor, Diagnostics, Flavor, FlavorTranslator, Import, Loc,
PackageTranslator, Range, Resolved, Resolver, RpFile, RpName, RpPackage, RpReg,
RpRequiredPackage, RpVersionedPackage, Source, Translate, Translator, Version};
use into_model::IntoModel;
use linked_hash_map::LinkedHashMap;
use naming::Naming;
use parser;
use scope::Scope;
use std::cell::RefCell;
use std::collections::{btree_map, BTreeMap, HashMap};
use std::path::Path;
use std::rc::Rc;
use std::result;
use translated::Translated;
macro_rules! try_with_diag {
($ctx:expr, $diag:expr, $block:block) => {
match $block {
Err(()) => {
$ctx.diagnostics($diag)?;
return Err("error in environment".into());
}
Ok(ok) => {
if $diag.has_errors() {
$ctx.diagnostics($diag)?;
return Err("error in environment".into());
}
ok
}
}
};
}
#[derive(Clone, Debug)]
pub struct File<F: 'static>
where
F: Flavor,
{
file: RpFile<F>,
source: Source,
}
pub struct Environment<'a, F: 'static>
where
F: Flavor,
{
pub ctx: Rc<Context>,
package_prefix: Option<RpPackage>,
resolver: &'a mut Resolver,
visited: HashMap<RpRequiredPackage, Option<RpVersionedPackage>>,
files: BTreeMap<RpVersionedPackage, File<F>>,
types: Rc<LinkedHashMap<RpName<F>, Loc<RpReg>>>,
keywords: Rc<HashMap<String, String>>,
safe_packages: bool,
package_naming: Option<Rc<Box<Naming>>>,
field_ident_naming: Option<Box<Naming>>,
endpoint_ident_naming: Option<Box<Naming>>,
path_hook: Option<Box<Fn(&Path) -> Result<()>>>,
}
impl<'a, F: 'static> Environment<'a, F>
where
F: Flavor,
{
pub fn new(
ctx: Rc<Context>,
package_prefix: Option<RpPackage>,
resolver: &'a mut Resolver,
) -> Environment<'a, F> {
Environment {
ctx: ctx,
package_prefix,
resolver,
visited: HashMap::new(),
files: BTreeMap::new(),
types: Rc::new(LinkedHashMap::new()),
keywords: Rc::new(HashMap::new()),
safe_packages: false,
package_naming: None,
field_ident_naming: None,
endpoint_ident_naming: None,
path_hook: None,
}
}
pub fn with_path_hook<H: 'static>(self, path_hook: H) -> Self
where
H: Fn(&Path) -> Result<()>,
{
Self {
path_hook: Some(Box::new(path_hook)),
..self
}
}
pub fn with_safe_packages(self, safe_packages: bool) -> Self {
Self {
safe_packages,
..self
}
}
pub fn with_keywords(self, keywords: HashMap<String, String>) -> Self {
Self {
keywords: Rc::new(keywords),
..self
}
}
pub fn with_package_naming(self, package_naming: Box<Naming>) -> Self {
Self {
package_naming: Some(Rc::new(package_naming)),
..self
}
}
pub fn with_field_ident_naming(self, field_ident_naming: Box<Naming>) -> Self {
Self {
field_ident_naming: Some(field_ident_naming),
..self
}
}
pub fn with_endpoint_ident_naming(self, endpoint_ident_naming: Box<Naming>) -> Self {
Self {
endpoint_ident_naming: Some(endpoint_ident_naming),
..self
}
}
fn package_version_unsafe(c: char) -> bool {
match c {
'.' | '-' | '~' => true,
_ => false,
}
}
fn version_package(version: &Version, level: usize, random: &str) -> String {
let mut parts = String::new();
parts.push_str("v");
parts.push_str(&version.major.to_string());
if level > 0 {
parts.push_str("_");
parts.push_str(&version.minor.to_string());
}
if level > 1 {
parts.push_str("_");
parts.push_str(&version.patch.to_string());
}
if level > 2 {
for p in &version.pre {
parts.push_str("_");
parts.push_str(&p.to_string().replace(Self::package_version_unsafe, "_"));
}
}
if level > 3 {
for b in &version.build {
parts.push_str("_");
parts.push_str(&b.to_string().replace(Self::package_version_unsafe, "_"));
}
}
if level > 4 {
parts.push_str("_");
parts.push_str(random);
}
parts
}
fn package_with_level(
&self,
package: &RpVersionedPackage,
level: usize,
random: &str,
) -> RpPackage {
package.to_package(|v| Self::version_package(v, level, random))
}
}
impl<'a> Environment<'a, CoreFlavor> {
pub fn translator<T: 'static>(&self, flavor: T) -> Result<translator::Context<T>>
where
T: FlavorTranslator<Source = CoreFlavor>,
{
Ok(translator::Context {
flavor: flavor,
types: Rc::clone(&self.types),
decls: Some(RefCell::new(LinkedHashMap::new())),
})
}
pub fn translate<T>(self, mut ctx: translator::Context<T>) -> Result<Translated<T::Target>>
where
T: FlavorTranslator<Source = CoreFlavor>,
{
if self.ctx.has_diagnostics()? {
return Err(Error::new("error in context"));
}
let mut files = BTreeMap::new();
for (package, file) in self.files {
let package = ctx.translate_package(package)?;
let mut diag = Diagnostics::new(file.source.clone());
let file = match file.file.translate(&mut diag, &ctx) {
Ok(file) => file,
Err(e) => {
self.ctx.diagnostics(diag)?;
return Err(e);
}
};
files.insert(package, file);
}
let mut decls = LinkedHashMap::new();
if let Some(d) = ctx.decls.take() {
let mut diag = Diagnostics::new(Source::empty("no diagnostics"));
for (name, reg) in d.into_inner() {
let name = name.translate(&mut diag, &ctx)?;
decls.insert(name, reg);
}
}
Ok(Translated::new(decls, files))
}
pub fn packages(&self) -> Result<Rc<Packages>> {
let mut queue = self.files
.keys()
.cloned()
.map(|p| (p, 0))
.collect::<Vec<_>>();
let mut files = HashMap::new();
while !queue.is_empty() {
let mut candidates = HashMap::new();
for (count, (package, level)) in queue.drain(..).enumerate() {
let random = count.to_string();
let converted = self.package_with_level(&package, level, &random);
candidates
.entry(converted)
.or_insert_with(Vec::new)
.push((package, level + 1));
}
for (converted, partial) in candidates {
if partial.len() > 1 {
for p in partial {
queue.push(p);
}
continue;
}
if let Some((original, _)) = partial.into_iter().next() {
files.insert(original, converted);
}
}
}
let package_prefix = self.package_prefix.clone();
let keywords = self.keywords.clone();
let package_naming = self.package_naming.clone();
Ok(Rc::new(Packages {
files,
package_prefix,
keywords,
safe_packages: self.safe_packages,
package_naming,
}))
}
pub fn translate_default(self) -> Result<Translated<CoreFlavor>> {
let ctx = self.translator(translator::CoreFlavorTranslator::<_, CoreFlavor>::new(()))?;
self.translate(ctx)
}
pub fn import_path<P: AsRef<Path>>(
&mut self,
path: P,
package: Option<RpVersionedPackage>,
) -> Result<RpVersionedPackage> {
self.import_source(Source::from_path(path), package)
}
pub fn import_source(
&mut self,
source: Source,
package: Option<RpVersionedPackage>,
) -> Result<RpVersionedPackage> {
let package = package.unwrap_or_else(|| RpVersionedPackage::new(RpPackage::empty(), None));
let required = RpRequiredPackage::new(package.package.clone(), Range::any());
if !self.visited.contains_key(&required) {
let mut diag = Diagnostics::new(source.clone());
try_with_diag!(self.ctx, diag, {
self.load_source_diag(&mut diag, &package)
.and_then(|file| self.process_file(&mut diag, package.clone(), file))
});
self.visited.insert(required, Some(package.clone()));
}
Ok(package)
}
pub fn import_file(
&mut self,
file: ast::File,
package: Option<RpVersionedPackage>,
) -> Result<RpVersionedPackage> {
let package = package.unwrap_or_else(|| RpVersionedPackage::new(RpPackage::empty(), None));
let required = RpRequiredPackage::new(package.package.clone(), Range::any());
if !self.visited.contains_key(&required) {
let mut diag = Diagnostics::new(Source::empty("generated"));
try_with_diag!(self.ctx, diag, {
self.load_file(&mut diag, file, &package)
.and_then(|file| self.process_file(&mut diag, package.clone(), file))
});
self.visited.insert(required, Some(package.clone()));
}
Ok(package)
}
pub fn verify(&mut self) -> Result<()> {
Ok(())
}
pub fn load_source(
&mut self,
source: Source,
package: &RpVersionedPackage,
) -> Result<RpFile<CoreFlavor>> {
let mut diag = Diagnostics::new(source.clone());
Ok(try_with_diag!(self.ctx, diag, {
self.load_source_diag(&mut diag, &package)
}))
}
fn load_source_diag(
&mut self,
diag: &mut Diagnostics,
package: &RpVersionedPackage,
) -> result::Result<RpFile<CoreFlavor>, ()> {
if let Some(hook) = self.path_hook.as_ref() {
let r = if let Some(path) = diag.source.path() {
Some(hook(path))
} else {
None
};
if let Some(e) = r {
match e {
Ok(()) => {}
Err(e) => {
diag.err((0, 0), format!("failed to call path hook: {}", e.display()));
}
}
}
}
let reader = match diag.source.read() {
Ok(reader) => reader,
Err(e) => {
diag.err(
(0, 0),
format!("failed to open file for reading: {}", e.display()),
);
return Err(());
}
};
let input = match parser::read_to_string(reader) {
Ok(input) => input,
Err(e) => {
diag.err((0, 0), format!("failed to read file: {}", e.display()));
return Err(());
}
};
let file = match parser::parse(diag, input.as_str()) {
Ok(file) => file,
Err(()) => return Err(()),
};
self.load_file(diag, file, package)
}
fn load_file<'input>(
&mut self,
diag: &mut Diagnostics,
file: ast::File,
package: &RpVersionedPackage,
) -> result::Result<RpFile<CoreFlavor>, ()> {
let mut scope = Scope::new(
package.clone(),
self.keywords.clone(),
self.field_ident_naming.as_ref().map(|n| n.copy()),
self.endpoint_ident_naming.as_ref().map(|n| n.copy()),
self,
);
file.into_model(diag, &mut scope)
}
fn process_file(
&mut self,
diag: &mut Diagnostics,
package: RpVersionedPackage,
file: RpFile<CoreFlavor>,
) -> result::Result<(), ()> {
use linked_hash_map::Entry::*;
let file = match self.files.entry(package) {
btree_map::Entry::Vacant(entry) => entry.insert(File {
file,
source: diag.source.clone(),
}),
btree_map::Entry::Occupied(_) => {
return Ok(());
}
};
for (key, _, t) in file.file.decls.iter().flat_map(|d| d.to_reg()) {
let (key, span) = Loc::borrow_pair(key);
let key = key.clone().without_prefix();
debug!("new reg ty: {}", key);
let types = match Rc::get_mut(&mut self.types) {
None => {
diag.err(span, "non-unique access to environment");
continue;
}
Some(types) => types,
};
match types.entry(key.clone()) {
Vacant(entry) => entry.insert(Loc::new(t, span)),
Occupied(entry) => {
diag.err(
span,
format!("`{}` conflicts with existing declaration", key),
);
diag.info(Loc::span(entry.get()), "existing declaration here");
continue;
}
};
}
if diag.has_errors() {
return Err(());
}
Ok(())
}
}
impl<'e> Import for Environment<'e, CoreFlavor> {
fn import(&mut self, required: &RpRequiredPackage) -> Result<Option<RpVersionedPackage>> {
debug!("import: {}", required);
if let Some(existing) = self.visited.get(required) {
debug!("already loaded: {:?} ({})", existing, required);
return Ok(existing.clone());
}
let files = self.resolver.resolve(required)?;
let result = if let Some(Resolved { version, source }) = files.into_iter().last() {
debug!("loading: {}", source);
let package = RpVersionedPackage::new(required.package.clone(), version);
debug!("found: {} ({})", package, required);
let mut diag = Diagnostics::new(source.clone());
try_with_diag!(self.ctx, diag, {
self.load_source_diag(&mut diag, &package)
.and_then(|file| self.process_file(&mut diag, package.clone(), file))
});
Some(package)
} else {
None
};
self.visited.insert(required.clone(), result.clone());
Ok(result)
}
}
impl<'a, 'e: 'a> Import for &'a mut Environment<'e, CoreFlavor> {
fn import(&mut self, required: &RpRequiredPackage) -> Result<Option<RpVersionedPackage>> {
(*self).import(required)
}
}
pub struct Packages {
files: HashMap<RpVersionedPackage, RpPackage>,
package_prefix: Option<RpPackage>,
keywords: Rc<HashMap<String, String>>,
safe_packages: bool,
package_naming: Option<Rc<Box<Naming>>>,
}
impl Packages {
pub fn new(&self, package: &str) -> Result<RpPackage> {
self.package(RpPackage::parse(package))
}
pub fn package(&self, package: RpPackage) -> Result<RpPackage> {
let package = if let Some(package_prefix) = self.package_prefix.as_ref() {
package_prefix.clone().join_package(package)
} else {
package
};
let package = if let Some(naming) = self.package_naming.as_ref() {
package.with_naming(|part| naming.convert(part))
} else {
package
};
let package = if !self.safe_packages {
package.with_replacements(&self.keywords)
} else {
package
};
Ok(package)
}
}
impl PackageTranslator<RpVersionedPackage, RpPackage> for Packages {
fn translate_package(&self, package: RpVersionedPackage) -> Result<RpPackage> {
let package = self.files
.get(&package)
.ok_or_else(|| format!("no such package: {}", package))?;
self.package(package.clone())
}
}