pub mod ast;
pub mod codegen;
pub mod diagnostic;
pub mod ir;
pub mod lexer;
pub mod parser;
pub mod semantic;
pub mod symbol;
use std::path::Path;
use diagnostic::Diagnostic;
pub type CompileResult = Result<CompileOutput, Vec<Diagnostic>>;
#[derive(Debug)]
pub struct CompileOutput {
pub code: String,
pub namespace: String,
pub warnings: Vec<Diagnostic>,
}
pub fn compile(source_path: impl AsRef<Path>, out_dir: impl AsRef<Path>) -> CompileResult {
let source_path = source_path.as_ref();
let out_dir = out_dir.as_ref();
let file_str = source_path.display().to_string();
println!("cargo:rerun-if-changed={file_str}");
let source = std::fs::read_to_string(source_path)
.map_err(|e| vec![Diagnostic::error(&file_str, format!("cannot read: {e}"))])?;
compile_source(&source, &file_str, Some(out_dir))
}
pub fn compile_source(source: &str, file_name: &str, out_dir: Option<&Path>) -> CompileResult {
compile_one(source, file_name, out_dir, &[]).map(|(output, _exports)| output)
}
fn compile_one(
source: &str,
file_name: &str,
out_dir: Option<&Path>,
available: &[symbol::NamespaceExports],
) -> Result<(CompileOutput, symbol::NamespaceExports), Vec<Diagnostic>> {
let mut warnings = Vec::new();
let is_md = file_name.ends_with(".md");
let ast = if is_md {
parser::parse_md(source, file_name)?
} else {
parser::parse_pht(source, file_name)?
};
let (table, sym_diags) = symbol::build_with_imports(&ast, file_name, available);
let (sym_errors, sym_warnings): (Vec<_>, Vec<_>) =
sym_diags.into_iter().partition(|d| d.is_error());
warnings.extend(sym_warnings);
if !sym_errors.is_empty() {
return Err(sym_errors);
}
let (module, ir_diags) = ir::lower(&ast, &table, file_name);
let (ir_errors, ir_warnings): (Vec<_>, Vec<_>) =
ir_diags.into_iter().partition(|d| d.is_error());
warnings.extend(ir_warnings);
if !ir_errors.is_empty() {
return Err(ir_errors);
}
let sem_diags = semantic::validate(&module, file_name);
let (sem_errors, sem_warnings): (Vec<_>, Vec<_>) =
sem_diags.into_iter().partition(|d| d.is_error());
warnings.extend(sem_warnings);
if !sem_errors.is_empty() {
return Err(sem_errors);
}
let code = codegen::generate(&module);
if let Some(out_dir) = out_dir {
let ns_path = module.namespace.replace('/', std::path::MAIN_SEPARATOR_STR);
let dir = out_dir.join(&ns_path);
let file = dir.join("mod.rs");
std::fs::create_dir_all(&dir).map_err(|e| {
vec![Diagnostic::error(
file_name,
format!("cannot create directory '{}': {e}", dir.display()),
)]
})?;
std::fs::write(&file, &code).map_err(|e| {
vec![Diagnostic::error(
file_name,
format!("cannot write '{}': {e}", file.display()),
)]
})?;
}
let exports = table.exports();
Ok((
CompileOutput {
code,
namespace: module.namespace.clone(),
warnings,
},
exports,
))
}
pub fn compile_with_roots(
source_path: impl AsRef<Path>,
roots: &[impl AsRef<Path>],
out_dir: impl AsRef<Path>,
) -> Result<Vec<CompileOutput>, Vec<Diagnostic>> {
let source_path = source_path.as_ref();
let out_dir = out_dir.as_ref();
let roots: Vec<&Path> = roots.iter().map(AsRef::as_ref).collect();
let mut set = CompileSet {
roots,
out_dir,
compiled: Vec::new(),
exports: Vec::new(),
in_progress: Vec::new(),
};
set.compile_file(source_path, None)?;
let namespaces: Vec<String> = set.compiled.iter().map(|o| o.namespace.clone()).collect();
write_module_tree(out_dir, &namespaces)?;
Ok(set.compiled)
}
struct CompileSet<'a> {
roots: Vec<&'a Path>,
out_dir: &'a Path,
compiled: Vec<CompileOutput>,
exports: Vec<symbol::NamespaceExports>,
in_progress: Vec<String>,
}
impl CompileSet<'_> {
fn compile_file(
&mut self,
path: &Path,
expected_ns: Option<&str>,
) -> Result<(), Vec<Diagnostic>> {
let file_str = path.display().to_string();
println!("cargo:rerun-if-changed={file_str}");
let source = std::fs::read_to_string(path)
.map_err(|e| vec![Diagnostic::error(&file_str, format!("cannot read: {e}"))])?;
let is_md = file_str.ends_with(".md");
let ast = if is_md {
parser::parse_md(&source, &file_str)?
} else {
parser::parse_pht(&source, &file_str)?
};
let ns = ast.ns.path.join("/");
if let Some(expected) = expected_ns {
if ns != expected {
return Err(vec![Diagnostic::error(
&file_str,
format!("expected namespace `{expected}` but the file declares `{ns}`"),
)]);
}
}
if self.exports.iter().any(|e| e.namespace == ns) {
return Ok(()); }
if let Some(pos) = self.in_progress.iter().position(|n| n == &ns) {
let mut cycle = self.in_progress[pos..].to_vec();
cycle.push(ns.clone());
return Err(vec![Diagnostic::error(
&file_str,
format!("cycle in `uses`: {}", cycle.join(" -> ")),
)]);
}
self.in_progress.push(ns.clone());
let dep_namespaces: Vec<String> = ast
.ns
.uses
.iter()
.flatten()
.map(|u| u.path.join("/"))
.filter(|dep| *dep != ns)
.collect();
for dep in &dep_namespaces {
if self.exports.iter().any(|e| e.namespace == *dep) {
continue;
}
let dep_path = self.locate(dep).ok_or_else(|| {
vec![Diagnostic::error(
&file_str,
format!(
"cannot resolve `uses {dep};` — no `{dep}.pht` or `{dep}.md` under {}",
self.roots
.iter()
.map(|r| r.display().to_string())
.collect::<Vec<_>>()
.join(", ")
),
)]
})?;
self.compile_file(&dep_path, Some(dep))?;
}
self.in_progress.pop();
let (output, exports) = compile_one(&source, &file_str, Some(self.out_dir), &self.exports)?;
self.compiled.push(output);
self.exports.push(exports);
Ok(())
}
fn locate(&self, ns: &str) -> Option<std::path::PathBuf> {
let rel = ns.replace('/', std::path::MAIN_SEPARATOR_STR);
for root in &self.roots {
for ext in ["pht", "md"] {
let candidate = root.join(&rel).with_extension(ext);
if candidate.is_file() {
return Some(candidate);
}
}
}
None
}
}
fn write_module_tree(out_dir: &Path, namespaces: &[String]) -> Result<(), Vec<Diagnostic>> {
use std::collections::{BTreeMap, BTreeSet};
let mut children: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for ns in namespaces {
let segments: Vec<&str> = ns.split('/').collect();
let mut prefix = String::new();
for segment in &segments {
children
.entry(prefix.clone())
.or_default()
.insert((*segment).to_string());
if !prefix.is_empty() {
prefix.push('/');
}
prefix.push_str(segment);
}
}
let io_err = |path: &Path, e: std::io::Error| {
vec![Diagnostic::error(
path.display().to_string(),
format!("cannot write module tree: {e}"),
)]
};
for (prefix, kids) in &children {
let dir = if prefix.is_empty() {
out_dir.to_path_buf()
} else {
out_dir.join(prefix.replace('/', std::path::MAIN_SEPARATOR_STR))
};
let file = dir.join("mod.rs");
let decls: String = kids.iter().map(|k| format!("pub mod {k};\n")).collect();
if namespaces.iter().any(|ns| ns == prefix) {
let mut code = std::fs::read_to_string(&file).map_err(|e| io_err(&file, e))?;
code.push('\n');
code.push_str(&decls);
std::fs::write(&file, code).map_err(|e| io_err(&file, e))?;
} else {
std::fs::create_dir_all(&dir).map_err(|e| io_err(&dir, e))?;
let header = "// This file is @generated by phenotyper. Do not edit.\n\n";
std::fs::write(&file, format!("{header}{decls}")).map_err(|e| io_err(&file, e))?;
}
}
Ok(())
}