slovo/compiler/src/project.rs

4624 lines
155 KiB
Rust

use std::{
collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
env, fs, io,
path::{Path, PathBuf},
};
use crate::{
ast::{MatchPatternKind, Program},
check::{
self, CheckedFunction, CheckedProgram, ExternalEnum, ExternalEnumVariant, ExternalFunction,
ExternalStruct, TExpr, TExprKind,
},
diag::Diagnostic,
lexer, llvm, lower,
sexpr::{Atom, SExpr, SExprKind},
std_runtime, test_runner,
token::Span,
types::Type,
};
pub struct ProjectOutput {
pub text: String,
pub sources: Vec<SourceFile>,
pub artifact: ProjectArtifact,
}
pub struct ProjectTestSuccess {
pub output: String,
pub report: test_runner::TestReport,
pub sources: Vec<SourceFile>,
pub artifact: ProjectArtifact,
}
pub struct ProjectTestFailure {
pub diagnostics: Vec<Diagnostic>,
pub report: Option<test_runner::TestReport>,
pub sources: Vec<SourceFile>,
pub artifact: Option<ProjectArtifact>,
}
pub struct ToolProject {
pub sources: Vec<SourceFile>,
pub artifact: ProjectArtifact,
}
pub struct ToolFailure {
pub diagnostics: Vec<Diagnostic>,
pub sources: Vec<SourceFile>,
pub artifact: Option<ProjectArtifact>,
}
#[derive(Debug, Clone)]
pub struct SourceFile {
pub path: String,
pub source: String,
}
#[derive(Debug, Clone)]
pub struct ProjectArtifact {
pub manifest_path: String,
pub project_root: String,
pub source_root: String,
pub project_name: String,
pub entry: String,
pub modules: Vec<ProjectArtifactModule>,
pub workspace: Option<WorkspaceArtifact>,
}
#[derive(Debug, Clone)]
pub struct ProjectArtifactModule {
pub name: String,
pub path: String,
pub imports: Vec<String>,
pub c_imports: Vec<ProjectArtifactCImport>,
}
#[derive(Debug, Clone)]
pub struct ProjectArtifactCImport {
pub source_module: String,
pub name: String,
pub symbol: String,
pub params: Vec<String>,
pub return_type: String,
pub abi: String,
}
#[derive(Debug, Clone)]
pub struct WorkspaceArtifact {
pub workspace_root: String,
pub workspace_manifest: String,
pub members: Vec<String>,
pub packages: Vec<WorkspaceArtifactPackage>,
pub dependencies: Vec<WorkspaceArtifactDependency>,
pub selected_build_entry_package: Option<String>,
}
#[derive(Debug, Clone)]
pub struct WorkspaceArtifactPackage {
pub name: String,
pub version: String,
pub root: String,
pub manifest: String,
pub source_root: String,
pub entry: String,
pub modules: Vec<ProjectArtifactModule>,
pub test_count: usize,
}
#[derive(Debug, Clone)]
pub struct WorkspaceArtifactDependency {
pub from: String,
pub to: String,
pub kind: String,
pub path: String,
}
#[derive(Clone)]
struct Manifest {
path: PathBuf,
source: String,
project_root: PathBuf,
name: String,
source_root: String,
entry: String,
}
#[derive(Clone)]
struct WorkspaceManifest {
path: PathBuf,
source: String,
root: PathBuf,
members: Vec<String>,
default_package: Option<String>,
}
#[derive(Clone)]
struct PackageManifest {
path: PathBuf,
root: PathBuf,
member: String,
name: String,
version: String,
source_root: String,
entry: String,
dependencies: Vec<PackageDependency>,
}
#[derive(Clone)]
struct PackageDependency {
key: String,
path: String,
span: Span,
}
#[derive(Clone)]
struct PackageUnit {
manifest: PackageManifest,
modules: Vec<ModuleUnit>,
dependency_indices: Vec<usize>,
}
#[derive(Debug, Clone)]
struct WorkspaceImportBinding {
provider_package: usize,
provider_module: usize,
kind: DeclKind,
import_span: Span,
}
#[derive(Clone)]
struct ModuleUnit {
name: String,
file: String,
exports: Vec<NameSpan>,
imports: Vec<ImportDecl>,
program: Program,
local_functions: HashMap<String, DeclInfo>,
local_structs: HashMap<String, DeclInfo>,
local_enums: HashMap<String, DeclInfo>,
local_aliases: HashMap<String, DeclInfo>,
}
#[derive(Debug, Clone)]
struct ImportDecl {
module: String,
module_span: Span,
names: Vec<NameSpan>,
}
#[derive(Debug, Clone)]
struct NameSpan {
name: String,
span: Span,
}
#[derive(Debug, Clone)]
struct DeclInfo {
span: Span,
kind: DeclKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DeclKind {
Function,
CImport,
Struct,
Enum,
TypeAlias,
}
#[derive(Debug, Clone)]
struct ImportBinding {
provider: String,
kind: DeclKind,
import_span: Span,
}
pub fn is_project_input(path: &str) -> bool {
let path = Path::new(path);
if path.is_dir() {
return path.join("slovo.toml").is_file();
}
path.file_name().and_then(|name| name.to_str()) == Some("slovo.toml") && path.is_file()
}
pub fn load_project_sources_for_tools(input: &str) -> Result<ToolProject, ToolFailure> {
let manifest_path = manifest_path(input);
let manifest_file = manifest_path.display().to_string();
let manifest_source = fs::read_to_string(&manifest_path).map_err(|err| ToolFailure {
diagnostics: vec![Diagnostic::new(
&manifest_file,
"ProjectManifestReadFailed",
format!("cannot read project manifest `{}`: {}", manifest_file, err),
)],
sources: Vec::new(),
artifact: None,
})?;
if is_workspace_manifest_source(&manifest_source) {
return load_workspace_sources_for_tools(manifest_path, manifest_source);
}
let manifest =
parse_manifest(manifest_path, manifest_source.clone()).map_err(|diagnostics| {
ToolFailure {
diagnostics,
sources: vec![SourceFile {
path: manifest_file.clone(),
source: manifest_source.clone(),
}],
artifact: None,
}
})?;
let mut sources = vec![SourceFile {
path: manifest.path.display().to_string(),
source: manifest.source.clone(),
}];
let artifact = empty_artifact(&manifest);
let source_root = manifest.project_root.join(&manifest.source_root);
if !source_root.is_dir() {
return Err(ToolFailure {
diagnostics: vec![Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceRootMissing",
format!(
"project source root `{}` does not exist",
source_root.display()
),
)],
sources,
artifact: Some(artifact),
});
}
if let Err(diagnostic) = validate_source_root_boundary(&manifest, &source_root) {
return Err(ToolFailure {
diagnostics: vec![diagnostic],
sources,
artifact: Some(artifact),
});
}
let canonical_source_root = fs::canonicalize(&source_root).ok();
let module_paths = discover_module_paths(&source_root).map_err(|err| ToolFailure {
diagnostics: vec![Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceReadFailed",
format!(
"cannot read source root `{}`: {}",
source_root.display(),
err
),
)],
sources: sources.clone(),
artifact: Some(artifact.clone()),
})?;
if module_paths.is_empty() {
return Err(ToolFailure {
diagnostics: vec![Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceReadFailed",
format!(
"project source root `{}` contains no `.slo` modules",
source_root.display()
),
)],
sources,
artifact: Some(artifact),
});
}
let mut modules = Vec::new();
let mut diagnostics = Vec::new();
for path in module_paths {
let file = path.display().to_string();
if let Some(root) = &canonical_source_root {
match fs::canonicalize(&path) {
Ok(canonical) if canonical.starts_with(root) => {}
Ok(_) => {
diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectManifestInvalid",
format!("project module `{}` escapes the source root", file),
));
continue;
}
Err(err) => {
diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceReadFailed",
format!("cannot canonicalize module `{}`: {}", file, err),
));
continue;
}
}
}
match fs::read_to_string(&path) {
Ok(source) => {
sources.push(SourceFile {
path: file.clone(),
source: source.clone(),
});
modules.push(ProjectArtifactModule {
name: path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("")
.to_string(),
path: file,
imports: tool_imports(&source),
c_imports: tool_c_imports(&source),
});
}
Err(err) => diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceReadFailed",
format!("cannot read module `{}`: {}", file, err),
)),
}
}
load_tool_standard_import_modules(&manifest, &mut modules, &mut sources, &mut diagnostics);
let artifact = ProjectArtifact {
modules,
..artifact
};
if diagnostics.is_empty() {
Ok(ToolProject {
sources: sources.into_iter().skip(1).collect(),
artifact,
})
} else {
Err(ToolFailure {
diagnostics,
sources,
artifact: Some(artifact),
})
}
}
pub fn compile_to_llvm(input: &str) -> Result<ProjectOutput, ProjectTestFailure> {
let checked = load_checked_project(input, true).map_err(|failure| ProjectTestFailure {
diagnostics: failure.diagnostics,
report: None,
sources: failure.sources,
artifact: failure.artifact,
})?;
let llvm =
llvm::emit(&checked.artifact.manifest_path, &checked.program).map_err(|diagnostics| {
ProjectTestFailure {
diagnostics,
report: None,
sources: checked.sources.clone(),
artifact: Some(checked.artifact.clone()),
}
})?;
Ok(ProjectOutput {
text: llvm,
sources: checked.sources,
artifact: checked.artifact,
})
}
pub fn check_project(input: &str) -> Result<ProjectOutput, ProjectTestFailure> {
let checked = load_checked_project(input, false).map_err(|failure| ProjectTestFailure {
diagnostics: failure.diagnostics,
report: None,
sources: failure.sources,
artifact: failure.artifact,
})?;
llvm::emit(&checked.artifact.manifest_path, &checked.program).map_err(|diagnostics| {
ProjectTestFailure {
diagnostics,
report: None,
sources: checked.sources.clone(),
artifact: Some(checked.artifact.clone()),
}
})?;
Ok(ProjectOutput {
text: String::new(),
sources: checked.sources,
artifact: checked.artifact,
})
}
pub fn run_tests(
input: &str,
filter: Option<&str>,
) -> Result<ProjectTestSuccess, ProjectTestFailure> {
let checked = load_checked_project(input, false).map_err(|failure| ProjectTestFailure {
diagnostics: failure.diagnostics,
report: None,
sources: failure.sources,
artifact: failure.artifact,
})?;
match test_runner::run(&checked.artifact.manifest_path, &checked.program, filter) {
Ok(success) => Ok(ProjectTestSuccess {
output: success.output,
report: success.report,
sources: checked.sources,
artifact: checked.artifact,
}),
Err(failure) => Err(ProjectTestFailure {
diagnostics: failure.diagnostics,
report: failure.report,
sources: checked.sources,
artifact: Some(checked.artifact),
}),
}
}
struct CheckedProject {
program: CheckedProgram,
sources: Vec<SourceFile>,
artifact: ProjectArtifact,
}
struct ProjectLoadFailure {
diagnostics: Vec<Diagnostic>,
sources: Vec<SourceFile>,
artifact: Option<ProjectArtifact>,
}
fn load_checked_project(
input: &str,
require_entry_wrapper: bool,
) -> Result<CheckedProject, ProjectLoadFailure> {
let manifest_path = manifest_path(input);
let manifest_file = manifest_path.display().to_string();
let manifest_source = match fs::read_to_string(&manifest_path) {
Ok(source) => source,
Err(err) => {
return Err(ProjectLoadFailure {
diagnostics: vec![Diagnostic::new(
&manifest_file,
"ProjectManifestReadFailed",
format!("cannot read project manifest `{}`: {}", manifest_file, err),
)],
sources: Vec::new(),
artifact: None,
});
}
};
if is_workspace_manifest_source(&manifest_source) {
return load_checked_workspace(manifest_path, manifest_source, require_entry_wrapper);
}
let manifest = match parse_manifest(manifest_path, manifest_source.clone()) {
Ok(manifest) => manifest,
Err(diagnostics) => {
return Err(ProjectLoadFailure {
sources: vec![SourceFile {
path: manifest_file,
source: manifest_source,
}],
diagnostics,
artifact: None,
});
}
};
let mut sources = vec![SourceFile {
path: manifest.path.display().to_string(),
source: manifest.source.clone(),
}];
let mut artifact = empty_artifact(&manifest);
let source_root = manifest.project_root.join(&manifest.source_root);
let source_root_file = source_root.display().to_string();
if !source_root.is_dir() {
return Err(ProjectLoadFailure {
diagnostics: vec![Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceRootMissing",
format!("project source root `{}` does not exist", source_root_file),
)],
sources,
artifact: Some(artifact),
});
}
if let Err(diagnostic) = validate_source_root_boundary(&manifest, &source_root) {
return Err(ProjectLoadFailure {
diagnostics: vec![diagnostic],
sources,
artifact: Some(artifact),
});
}
let canonical_source_root = fs::canonicalize(&source_root).ok();
let module_paths = match discover_module_paths(&source_root) {
Ok(paths) => paths,
Err(err) => {
return Err(ProjectLoadFailure {
diagnostics: vec![Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceReadFailed",
format!("cannot read source root `{}`: {}", source_root_file, err),
)],
sources,
artifact: Some(artifact),
});
}
};
if module_paths.is_empty() {
return Err(ProjectLoadFailure {
diagnostics: vec![Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceReadFailed",
format!(
"project source root `{}` contains no `.slo` modules",
source_root_file
),
)],
sources,
artifact: Some(artifact),
});
}
let mut modules = Vec::new();
let mut diagnostics = Vec::new();
for path in module_paths {
let file = path.display().to_string();
if let Some(root) = &canonical_source_root {
match fs::canonicalize(&path) {
Ok(canonical) if canonical.starts_with(root) => {}
Ok(_) => {
diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectManifestInvalid",
format!("project module `{}` escapes the source root", file),
));
continue;
}
Err(err) => {
diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceReadFailed",
format!("cannot canonicalize module `{}`: {}", file, err),
));
continue;
}
}
}
match fs::read_to_string(&path) {
Ok(source) => {
sources.push(SourceFile {
path: file.clone(),
source: source.clone(),
});
match parse_module(&path, file, source) {
Ok(module) => modules.push(module),
Err(mut errs) => diagnostics.append(&mut errs),
}
}
Err(err) => diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceReadFailed",
format!("cannot read module `{}`: {}", file, err),
)),
}
}
load_standard_import_modules(&manifest, &mut modules, &mut sources, &mut diagnostics);
artifact = artifact_from_modules(&manifest, &modules, None);
if !diagnostics.is_empty() {
return Err(ProjectLoadFailure {
diagnostics,
sources,
artifact: Some(artifact),
});
}
match resolve_and_check(manifest, modules, sources, require_entry_wrapper) {
Ok(checked) => Ok(checked),
Err(failure) => Err(failure),
}
}
struct WorkspaceLoaded {
workspace: WorkspaceManifest,
packages: Vec<PackageUnit>,
sources: Vec<SourceFile>,
artifact: ProjectArtifact,
}
fn is_workspace_manifest_source(source: &str) -> bool {
manifest_lines(source)
.iter()
.any(|line| line.text.trim() == "[workspace]")
}
fn load_workspace_sources_for_tools(
manifest_path: PathBuf,
manifest_source: String,
) -> Result<ToolProject, ToolFailure> {
match load_workspace_base(manifest_path, manifest_source, None) {
Ok(loaded) => Ok(ToolProject {
sources: loaded
.sources
.into_iter()
.filter(|source| source.path.ends_with(".slo"))
.collect(),
artifact: loaded.artifact,
}),
Err(failure) => Err(ToolFailure {
diagnostics: failure.diagnostics,
sources: failure.sources,
artifact: failure.artifact,
}),
}
}
fn load_checked_workspace(
manifest_path: PathBuf,
manifest_source: String,
require_entry_wrapper: bool,
) -> Result<CheckedProject, ProjectLoadFailure> {
let loaded = load_workspace_base(manifest_path, manifest_source, None)?;
resolve_and_check_workspace(loaded, require_entry_wrapper)
}
fn load_workspace_base(
manifest_path: PathBuf,
manifest_source: String,
selected_build_entry_package: Option<String>,
) -> Result<WorkspaceLoaded, ProjectLoadFailure> {
let manifest_file = manifest_path.display().to_string();
let workspace = match parse_workspace_manifest(manifest_path, manifest_source.clone()) {
Ok(workspace) => workspace,
Err(diagnostics) => {
return Err(ProjectLoadFailure {
sources: vec![SourceFile {
path: manifest_file,
source: manifest_source,
}],
diagnostics,
artifact: None,
});
}
};
let mut sources = vec![SourceFile {
path: workspace.path.display().to_string(),
source: workspace.source.clone(),
}];
let mut diagnostics = Vec::new();
let mut packages = Vec::new();
for member in &workspace.members {
let package_root = workspace.root.join(member);
let package_manifest = package_root.join("slovo.toml");
let package_file = package_manifest.display().to_string();
let package_source = match fs::read_to_string(&package_manifest) {
Ok(source) => source,
Err(err) => {
diagnostics.push(Diagnostic::new(
&workspace.path.display().to_string(),
"WorkspaceMemberManifestMissing",
format!(
"workspace member `{}` must contain `slovo.toml`: {}",
member, err
),
));
continue;
}
};
sources.push(SourceFile {
path: package_file.clone(),
source: package_source.clone(),
});
let manifest = match parse_package_manifest(
package_manifest,
package_source,
package_root,
member.clone(),
) {
Ok(manifest) => manifest,
Err(mut errs) => {
diagnostics.append(&mut errs);
continue;
}
};
if let Err(diagnostic) = validate_package_root_boundary(&workspace, &manifest) {
diagnostics.push(diagnostic);
continue;
}
let source_root = manifest.root.join(&manifest.source_root);
let source_root_file = source_root.display().to_string();
if !source_root.is_dir() {
diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"PackageSourceRootMissing",
format!("package source root `{}` does not exist", source_root_file),
));
continue;
}
if let Err(diagnostic) = validate_package_source_root_boundary(&manifest, &source_root) {
diagnostics.push(diagnostic);
continue;
}
let canonical_source_root = fs::canonicalize(&source_root).ok();
let module_paths = match discover_module_paths(&source_root) {
Ok(paths) => paths,
Err(err) => {
diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"PackageSourceReadFailed",
format!("cannot read source root `{}`: {}", source_root_file, err),
));
continue;
}
};
if module_paths.is_empty() {
diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"PackageSourceReadFailed",
format!(
"package source root `{}` contains no `.slo` modules",
source_root_file
),
));
continue;
}
let mut modules = Vec::new();
for path in module_paths {
let file = path.display().to_string();
if let Some(root) = &canonical_source_root {
match fs::canonicalize(&path) {
Ok(canonical) if canonical.starts_with(root) => {}
Ok(_) => {
diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"PackageManifestInvalid",
format!("package module `{}` escapes the source root", file),
));
continue;
}
Err(err) => {
diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"PackageSourceReadFailed",
format!("cannot canonicalize module `{}`: {}", file, err),
));
continue;
}
}
}
match fs::read_to_string(&path) {
Ok(source) => {
sources.push(SourceFile {
path: file.clone(),
source: source.clone(),
});
match parse_module(&path, file, source) {
Ok(module) => modules.push(module),
Err(mut errs) => diagnostics.append(&mut errs),
}
}
Err(err) => diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"PackageSourceReadFailed",
format!("cannot read module `{}`: {}", file, err),
)),
}
}
load_package_standard_import_modules(
&manifest,
&mut modules,
&mut sources,
&mut diagnostics,
);
packages.push(PackageUnit {
manifest,
modules,
dependency_indices: Vec::new(),
});
}
validate_workspace_packages(&workspace, &mut packages, &mut diagnostics);
let artifact = workspace_artifact(
&workspace,
&packages,
None,
None,
selected_build_entry_package,
);
if !diagnostics.is_empty() {
return Err(ProjectLoadFailure {
diagnostics,
sources,
artifact: Some(artifact),
});
}
Ok(WorkspaceLoaded {
workspace,
packages,
sources,
artifact,
})
}
fn manifest_path(input: &str) -> PathBuf {
let path = Path::new(input);
if path.is_dir() {
path.join("slovo.toml")
} else {
path.to_path_buf()
}
}
fn empty_artifact(manifest: &Manifest) -> ProjectArtifact {
ProjectArtifact {
manifest_path: manifest.path.display().to_string(),
project_root: manifest.project_root.display().to_string(),
source_root: manifest.source_root.clone(),
project_name: manifest.name.clone(),
entry: manifest.entry.clone(),
modules: Vec::new(),
workspace: None,
}
}
fn artifact_from_modules(
manifest: &Manifest,
modules: &[ModuleUnit],
order: Option<&[usize]>,
) -> ProjectArtifact {
let module_indices = order
.map(|order| order.to_vec())
.unwrap_or_else(|| (0..modules.len()).collect());
ProjectArtifact {
manifest_path: manifest.path.display().to_string(),
project_root: manifest.project_root.display().to_string(),
source_root: manifest.source_root.clone(),
project_name: manifest.name.clone(),
entry: manifest.entry.clone(),
modules: module_indices
.iter()
.map(|index| ProjectArtifactModule {
name: modules[*index].name.clone(),
path: modules[*index].file.clone(),
imports: modules[*index]
.imports
.iter()
.map(|import| import.module.clone())
.collect(),
c_imports: modules[*index]
.program
.c_imports
.iter()
.map(|import| project_artifact_c_import(&modules[*index].name, import))
.collect(),
})
.collect(),
workspace: None,
}
}
fn workspace_artifact(
workspace: &WorkspaceManifest,
packages: &[PackageUnit],
package_order: Option<&[usize]>,
module_orders: Option<&HashMap<usize, Vec<usize>>>,
selected_build_entry_package: Option<String>,
) -> ProjectArtifact {
let mut package_indices = package_order
.map(|order| order.to_vec())
.unwrap_or_else(|| (0..packages.len()).collect());
if package_order.is_none() {
package_indices.sort_by(|left, right| {
packages[*left]
.manifest
.name
.cmp(&packages[*right].manifest.name)
.then_with(|| {
packages[*left]
.manifest
.member
.cmp(&packages[*right].manifest.member)
})
});
}
let mut flat_modules = Vec::new();
let mut artifact_packages = Vec::new();
for package_index in &package_indices {
let package = &packages[*package_index];
let module_indices = module_orders
.and_then(|orders| orders.get(package_index))
.cloned()
.unwrap_or_else(|| (0..package.modules.len()).collect());
let modules = module_indices
.iter()
.map(|module_index| {
let module = &package.modules[*module_index];
ProjectArtifactModule {
name: module.name.clone(),
path: module.file.clone(),
imports: module
.imports
.iter()
.map(|import| import.module.clone())
.collect(),
c_imports: module
.program
.c_imports
.iter()
.map(|import| project_artifact_c_import(&module.name, import))
.collect(),
}
})
.collect::<Vec<_>>();
for module in &modules {
let qualified_module = format!("{}.{}", package.manifest.name, module.name);
flat_modules.push(ProjectArtifactModule {
name: qualified_module.clone(),
path: module.path.clone(),
imports: module.imports.clone(),
c_imports: module
.c_imports
.iter()
.map(|import| {
let mut import = import.clone();
import.source_module = qualified_module.clone();
import
})
.collect(),
});
}
artifact_packages.push(WorkspaceArtifactPackage {
name: package.manifest.name.clone(),
version: package.manifest.version.clone(),
root: package.manifest.root.display().to_string(),
manifest: package.manifest.path.display().to_string(),
source_root: package.manifest.source_root.clone(),
entry: package.manifest.entry.clone(),
test_count: package
.modules
.iter()
.map(|module| module.program.tests.len())
.sum(),
modules,
});
}
let mut dependencies = Vec::new();
for (from_index, package) in packages.iter().enumerate() {
for to_index in &package.dependency_indices {
dependencies.push(WorkspaceArtifactDependency {
from: packages[from_index].manifest.name.clone(),
to: packages[*to_index].manifest.name.clone(),
kind: "local-path".to_string(),
path: packages[*to_index].manifest.member.clone(),
});
}
}
dependencies.sort_by(|left, right| {
left.from
.cmp(&right.from)
.then_with(|| left.to.cmp(&right.to))
.then_with(|| left.path.cmp(&right.path))
});
ProjectArtifact {
manifest_path: workspace.path.display().to_string(),
project_root: workspace.root.display().to_string(),
source_root: String::new(),
project_name: workspace
.root
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("workspace")
.to_string(),
entry: selected_build_entry_package
.clone()
.unwrap_or_else(|| "main".to_string()),
modules: flat_modules,
workspace: Some(WorkspaceArtifact {
workspace_root: workspace.root.display().to_string(),
workspace_manifest: workspace.path.display().to_string(),
members: workspace.members.clone(),
packages: artifact_packages,
dependencies,
selected_build_entry_package,
}),
}
}
fn parse_manifest(path: PathBuf, source: String) -> Result<Manifest, Vec<Diagnostic>> {
let file = path.display().to_string();
let mut errors = Vec::new();
let mut in_project = false;
let mut saw_project = false;
let mut name = None::<String>;
let mut source_root = None::<String>;
let mut entry = None::<String>;
for line in manifest_lines(&source) {
let trimmed = line.text.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if trimmed.starts_with('[') {
if trimmed == "[project]" {
in_project = true;
saw_project = true;
} else {
errors.push(
Diagnostic::new(&file, "ProjectManifestInvalid", "unknown manifest section")
.with_span(line.span),
);
in_project = false;
}
continue;
}
if !in_project {
errors.push(
Diagnostic::new(
&file,
"ProjectManifestInvalid",
"manifest keys must be inside `[project]`",
)
.with_span(line.span),
);
continue;
}
let Some((key, value)) = trimmed.split_once('=') else {
errors.push(
Diagnostic::new(&file, "ProjectManifestInvalid", "manifest key must use `=`")
.with_span(line.span),
);
continue;
};
let key = key.trim();
let value = value.trim();
let parsed = match parse_manifest_string(value) {
Some(value) => value,
None => {
errors.push(
Diagnostic::new(
&file,
"ProjectManifestInvalid",
"manifest values must be strings",
)
.with_span(line.span),
);
continue;
}
};
match key {
"name" => set_manifest_key(&file, &mut errors, &mut name, parsed, line.span, "name"),
"source_root" => set_manifest_key(
&file,
&mut errors,
&mut source_root,
parsed,
line.span,
"source_root",
),
"entry" => set_manifest_key(&file, &mut errors, &mut entry, parsed, line.span, "entry"),
_ => errors.push(
Diagnostic::new(
&file,
"ProjectManifestInvalid",
format!("unknown project manifest key `{}`", key),
)
.with_span(line.span),
),
}
}
if !saw_project {
errors.push(Diagnostic::new(
&file,
"ProjectManifestInvalid",
"project manifest is missing `[project]` section",
));
}
let Some(name) = name else {
errors.push(Diagnostic::new(
&file,
"ProjectManifestInvalid",
"project manifest is missing required key `name`",
));
return Err(errors);
};
if !is_project_name(&name) {
errors.push(Diagnostic::new(
&file,
"ProjectManifestInvalid",
"project name must be lowercase ASCII and may contain only `a-z`, `0-9`, and `-` after the first letter",
));
}
let source_root = source_root.unwrap_or_else(|| "src".to_string());
if !is_safe_relative_source_root(&source_root) {
errors.push(Diagnostic::new(
&file,
"ProjectManifestInvalid",
"project source_root must be a relative path under the project root",
));
}
let entry = entry.unwrap_or_else(|| "main".to_string());
if !is_flat_module_identifier(&entry) {
errors.push(Diagnostic::new(
&file,
"ProjectManifestInvalid",
"project entry must be a flat module identifier",
));
}
if !errors.is_empty() {
return Err(errors);
}
let project_root = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
Ok(Manifest {
path,
source,
project_root,
name,
source_root,
entry,
})
}
fn parse_workspace_manifest(
path: PathBuf,
source: String,
) -> Result<WorkspaceManifest, Vec<Diagnostic>> {
let file = path.display().to_string();
let mut errors = Vec::new();
let mut in_workspace = false;
let mut saw_workspace = false;
let mut members = None::<Vec<String>>;
let mut default_package = None::<String>;
for line in manifest_lines(&source) {
let trimmed = line.text.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if trimmed.starts_with('[') {
if trimmed == "[workspace]" {
in_workspace = true;
saw_workspace = true;
} else {
errors.push(
Diagnostic::new(
&file,
"WorkspaceManifestInvalid",
"unknown workspace manifest section",
)
.with_span(line.span),
);
in_workspace = false;
}
continue;
}
if !in_workspace {
errors.push(
Diagnostic::new(
&file,
"WorkspaceManifestInvalid",
"workspace manifest keys must be inside `[workspace]`",
)
.with_span(line.span),
);
continue;
}
let Some((key, value)) = trimmed.split_once('=') else {
errors.push(
Diagnostic::new(
&file,
"WorkspaceManifestInvalid",
"workspace manifest key must use `=`",
)
.with_span(line.span),
);
continue;
};
match key.trim() {
"members" => match parse_manifest_string_array(value.trim()) {
Some(parsed) if !parsed.is_empty() => {
if members.replace(parsed).is_some() {
errors.push(
Diagnostic::new(
&file,
"WorkspaceManifestInvalid",
"duplicate workspace manifest key `members`",
)
.with_span(line.span),
);
}
}
_ => errors.push(
Diagnostic::new(
&file,
"WorkspaceManifestInvalid",
"workspace members must be a non-empty array of strings",
)
.with_span(line.span),
),
},
"default_package" => match parse_manifest_string(value.trim()) {
Some(parsed) => {
if default_package.replace(parsed).is_some() {
errors.push(
Diagnostic::new(
&file,
"WorkspaceManifestInvalid",
"duplicate workspace manifest key `default_package`",
)
.with_span(line.span),
);
}
}
None => errors.push(
Diagnostic::new(
&file,
"WorkspaceManifestInvalid",
"workspace default_package must be a string",
)
.with_span(line.span),
),
},
other => errors.push(
Diagnostic::new(
&file,
"WorkspaceManifestInvalid",
format!("unknown workspace manifest key `{}`", other),
)
.with_span(line.span),
),
}
}
if !saw_workspace {
errors.push(Diagnostic::new(
&file,
"WorkspaceManifestInvalid",
"workspace manifest is missing `[workspace]` section",
));
}
let mut members = members.unwrap_or_else(|| {
errors.push(Diagnostic::new(
&file,
"WorkspaceManifestInvalid",
"workspace manifest is missing required key `members`",
));
Vec::new()
});
let mut normalized_members = Vec::new();
let mut seen_members = BTreeMap::<String, String>::new();
for member in members {
if let Some(normalized) = normalize_workspace_member(&member) {
if let Some(original) = seen_members.insert(normalized.clone(), member.clone()) {
errors.push(Diagnostic::new(
&file,
"DuplicateWorkspaceMember",
format!(
"workspace member path `{}` duplicates `{}` after normalization",
member, original
),
));
}
normalized_members.push(normalized);
} else {
errors.push(Diagnostic::new(
&file,
"WorkspaceMemberPathEscape",
format!(
"workspace member path `{}` must be relative and stay under the workspace root",
member
),
));
}
}
members = normalized_members;
members.sort();
if let Some(default_package) = &default_package {
if !is_project_name(default_package) {
errors.push(Diagnostic::new(
&file,
"WorkspaceManifestInvalid",
"workspace default_package must be a package name",
));
}
}
if !errors.is_empty() {
return Err(errors);
}
let root = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."))
.to_path_buf();
Ok(WorkspaceManifest {
path,
source,
root,
members,
default_package,
})
}
fn parse_package_manifest(
path: PathBuf,
source: String,
root: PathBuf,
member: String,
) -> Result<PackageManifest, Vec<Diagnostic>> {
let file = path.display().to_string();
let mut errors = Vec::new();
let mut section = "";
let mut saw_package = false;
let mut saw_project = false;
let mut name = None::<String>;
let mut version = None::<String>;
let mut source_root = None::<String>;
let mut entry = None::<String>;
let mut dependencies = Vec::new();
for line in manifest_lines(&source) {
let trimmed = line.text.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
if trimmed.starts_with('[') {
match trimmed {
"[package]" => {
section = "package";
saw_package = true;
}
"[dependencies]" => section = "dependencies",
"[project]" => {
section = "project";
saw_project = true;
errors.push(
Diagnostic::new(
&file,
"PackageManifestInvalid",
"workspace package manifest may not contain `[project]`",
)
.with_span(line.span),
);
}
_ => {
section = "";
errors.push(
Diagnostic::new(
&file,
"PackageManifestInvalid",
"unknown package manifest section",
)
.with_span(line.span),
);
}
}
continue;
}
let Some((key, value)) = trimmed.split_once('=') else {
errors.push(
Diagnostic::new(
&file,
"PackageManifestInvalid",
"package manifest key must use `=`",
)
.with_span(line.span),
);
continue;
};
let key = key.trim();
let value = value.trim();
match section {
"package" => {
let parsed = match parse_manifest_string(value) {
Some(value) => value,
None => {
errors.push(
Diagnostic::new(
&file,
"PackageManifestInvalid",
"package manifest values must be strings",
)
.with_span(line.span),
);
continue;
}
};
match key {
"name" => set_manifest_key(
&file,
&mut errors,
&mut name,
parsed,
line.span,
"name",
),
"version" => set_manifest_key(
&file,
&mut errors,
&mut version,
parsed,
line.span,
"version",
),
"source_root" => set_manifest_key(
&file,
&mut errors,
&mut source_root,
parsed,
line.span,
"source_root",
),
"entry" => set_manifest_key(
&file,
&mut errors,
&mut entry,
parsed,
line.span,
"entry",
),
other => errors.push(
Diagnostic::new(
&file,
"PackageManifestInvalid",
format!("unknown package manifest key `{}`", other),
)
.with_span(line.span),
),
}
}
"dependencies" => match parse_dependency_path(value) {
Some(path) => dependencies.push(PackageDependency {
key: key.to_string(),
path,
span: line.span,
}),
None => errors.push(
Diagnostic::new(
&file,
"UnsupportedDependency",
"workspace dependencies must use `{ path = \"...\" }` local path records only",
)
.with_span(line.span),
),
},
"" | "project" => errors.push(
Diagnostic::new(
&file,
"PackageManifestInvalid",
"package manifest keys must be inside `[package]` or `[dependencies]`",
)
.with_span(line.span),
),
_ => unreachable!("package manifest parser section is known"),
}
}
if saw_project && saw_package {
errors.push(Diagnostic::new(
&file,
"PackageManifestInvalid",
"package manifest may not combine `[project]` and `[package]`",
));
}
if !saw_package {
errors.push(Diagnostic::new(
&file,
"PackageManifestInvalid",
"package manifest is missing `[package]` section",
));
}
let Some(name) = name else {
errors.push(Diagnostic::new(
&file,
"PackageManifestInvalid",
"package manifest is missing required key `name`",
));
return Err(errors);
};
let Some(version) = version else {
errors.push(Diagnostic::new(
&file,
"PackageManifestInvalid",
"package manifest is missing required key `version`",
));
return Err(errors);
};
if !is_project_name(&name) {
errors.push(Diagnostic::new(
&file,
"InvalidPackageName",
"package name must start with `a-z` and contain only `a-z`, `0-9`, and `-`",
));
}
if !is_package_version(&version) {
errors.push(Diagnostic::new(
&file,
"InvalidPackageVersion",
"package version must have literal `MAJOR.MINOR.PATCH` numeric shape",
));
}
let source_root = source_root.unwrap_or_else(|| "src".to_string());
if !is_safe_relative_source_root(&source_root) {
errors.push(Diagnostic::new(
&file,
"PackageSourceRootEscape",
"package source_root must be a relative path under the package root",
));
}
let entry = entry.unwrap_or_else(|| "main".to_string());
if !is_flat_module_identifier(&entry) {
errors.push(Diagnostic::new(
&file,
"PackageManifestInvalid",
"package entry must be a flat module identifier",
));
}
if !errors.is_empty() {
return Err(errors);
}
Ok(PackageManifest {
path,
root,
member,
name,
version,
source_root,
entry,
dependencies,
})
}
struct ManifestLine<'a> {
text: &'a str,
span: Span,
}
fn manifest_lines(source: &str) -> Vec<ManifestLine<'_>> {
let mut lines = Vec::new();
let mut offset = 0;
for line in source.split_inclusive('\n') {
let text = line.strip_suffix('\n').unwrap_or(line);
lines.push(ManifestLine {
text,
span: Span::new(offset, offset + text.len()),
});
offset += line.len();
}
if source.is_empty() {
lines.push(ManifestLine {
text: "",
span: Span::new(0, 0),
});
}
lines
}
fn parse_manifest_string(value: &str) -> Option<String> {
let value = value.trim();
let inner = value.strip_prefix('"')?.strip_suffix('"')?;
if inner.contains('"') || inner.contains('\\') {
return None;
}
Some(inner.to_string())
}
fn parse_manifest_string_array(value: &str) -> Option<Vec<String>> {
let inner = value.trim().strip_prefix('[')?.strip_suffix(']')?.trim();
if inner.is_empty() {
return Some(Vec::new());
}
let mut values = Vec::new();
for item in inner.split(',') {
values.push(parse_manifest_string(item.trim())?);
}
Some(values)
}
fn parse_dependency_path(value: &str) -> Option<String> {
let inner = value.trim().strip_prefix('{')?.strip_suffix('}')?.trim();
let (key, value) = inner.split_once('=')?;
if key.trim() != "path" {
return None;
}
parse_manifest_string(value.trim())
}
fn set_manifest_key(
file: &str,
errors: &mut Vec<Diagnostic>,
slot: &mut Option<String>,
value: String,
span: Span,
key: &str,
) {
if slot.replace(value).is_some() {
errors.push(
Diagnostic::new(
file,
"ProjectManifestInvalid",
format!("duplicate project manifest key `{}`", key),
)
.with_span(span),
);
}
}
fn is_package_version(value: &str) -> bool {
let mut parts = value.split('.');
let (Some(major), Some(minor), Some(patch), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return false;
};
[major, minor, patch]
.iter()
.all(|part| !part.is_empty() && part.bytes().all(|byte| byte.is_ascii_digit()))
}
fn is_project_name(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
first.is_ascii_lowercase()
&& chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
}
fn is_flat_module_identifier(value: &str) -> bool {
let mut chars = value.chars();
let Some(first) = chars.next() else {
return false;
};
(first.is_ascii_alphabetic() || first == '_')
&& chars.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
}
fn is_safe_relative_source_root(value: &str) -> bool {
if value.is_empty() {
return false;
}
let path = Path::new(value);
if path.is_absolute() {
return false;
}
path.components().all(|component| match component {
std::path::Component::Normal(name) => name.to_str() != Some("generated-source"),
std::path::Component::CurDir => true,
_ => false,
})
}
fn normalize_workspace_member(value: &str) -> Option<String> {
normalize_rel_path(value, false)
}
fn normalize_dependency_path(
workspace: &WorkspaceManifest,
package: &PackageManifest,
value: &str,
) -> Result<String, Diagnostic> {
if Path::new(value).is_absolute() || value.is_empty() {
return Err(Diagnostic::new(
&package.path.display().to_string(),
"DependencyPathEscape",
format!(
"dependency path `{}` must be relative and stay under the workspace root",
value
),
));
}
let combined = Path::new(&package.member).join(value);
let normalized = normalize_components(&combined).ok_or_else(|| {
Diagnostic::new(
&package.path.display().to_string(),
"DependencyPathEscape",
format!(
"dependency path `{}` must stay under the workspace root",
value
),
)
})?;
let target = workspace.root.join(&normalized);
let workspace_root = fs::canonicalize(&workspace.root).ok();
let target_root = fs::canonicalize(target).ok();
if let (Some(workspace_root), Some(target_root)) = (workspace_root, target_root) {
if !target_root.starts_with(&workspace_root) {
return Err(Diagnostic::new(
&package.path.display().to_string(),
"DependencyPathEscape",
format!(
"dependency path `{}` must not escape the workspace root",
value
),
));
}
}
Ok(normalized)
}
fn normalize_rel_path(value: &str, allow_parent_within_root: bool) -> Option<String> {
if value.is_empty() {
return None;
}
let path = Path::new(value);
if path.is_absolute() {
return None;
}
if !allow_parent_within_root
&& path
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
{
return None;
}
normalize_components(path)
}
fn normalize_components(path: &Path) -> Option<String> {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::CurDir => {}
std::path::Component::Normal(value) => {
let value = value.to_str()?;
if value.is_empty() {
return None;
}
components.push(value.to_string());
}
std::path::Component::ParentDir => {
components.pop()?;
}
_ => return None,
}
}
if components.is_empty() {
return None;
}
Some(components.join("/"))
}
fn validate_source_root_boundary(
manifest: &Manifest,
source_root: &Path,
) -> Result<(), Diagnostic> {
let project_root = fs::canonicalize(&manifest.project_root).map_err(|err| {
Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectManifestInvalid",
format!(
"cannot canonicalize project root `{}`: {}",
manifest.project_root.display(),
err
),
)
})?;
let source_root = fs::canonicalize(source_root).map_err(|err| {
Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectManifestInvalid",
format!(
"cannot canonicalize project source root `{}`: {}",
source_root.display(),
err
),
)
})?;
if !source_root.starts_with(&project_root) {
return Err(Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectManifestInvalid",
"project source_root must not escape the project root",
));
}
Ok(())
}
fn validate_package_root_boundary(
workspace: &WorkspaceManifest,
manifest: &PackageManifest,
) -> Result<(), Diagnostic> {
let workspace_root = fs::canonicalize(&workspace.root).map_err(|err| {
Diagnostic::new(
&workspace.path.display().to_string(),
"WorkspaceManifestInvalid",
format!(
"cannot canonicalize workspace root `{}`: {}",
workspace.root.display(),
err
),
)
})?;
let package_root = fs::canonicalize(&manifest.root).map_err(|err| {
Diagnostic::new(
&workspace.path.display().to_string(),
"WorkspaceManifestInvalid",
format!(
"cannot canonicalize workspace member `{}`: {}",
manifest.member, err
),
)
})?;
if !package_root.starts_with(&workspace_root) {
return Err(Diagnostic::new(
&workspace.path.display().to_string(),
"WorkspaceMemberPathEscape",
format!(
"workspace member `{}` must not escape the workspace root",
manifest.member
),
));
}
Ok(())
}
fn validate_package_source_root_boundary(
manifest: &PackageManifest,
source_root: &Path,
) -> Result<(), Diagnostic> {
let package_root = fs::canonicalize(&manifest.root).map_err(|err| {
Diagnostic::new(
&manifest.path.display().to_string(),
"PackageManifestInvalid",
format!(
"cannot canonicalize package root `{}`: {}",
manifest.root.display(),
err
),
)
})?;
let source_root = fs::canonicalize(source_root).map_err(|err| {
Diagnostic::new(
&manifest.path.display().to_string(),
"PackageManifestInvalid",
format!(
"cannot canonicalize package source root `{}`: {}",
source_root.display(),
err
),
)
})?;
if !source_root.starts_with(&package_root) {
return Err(Diagnostic::new(
&manifest.path.display().to_string(),
"PackageSourceRootEscape",
"package source_root must not escape the package root",
));
}
Ok(())
}
fn discover_module_paths(source_root: &Path) -> io::Result<Vec<PathBuf>> {
let mut paths = Vec::new();
for entry in fs::read_dir(source_root)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("slo") {
paths.push(path);
}
}
paths.sort_by(|left, right| left.file_name().cmp(&right.file_name()));
Ok(paths)
}
fn load_tool_standard_import_modules(
manifest: &Manifest,
modules: &mut Vec<ProjectArtifactModule>,
sources: &mut Vec<SourceFile>,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut loaded = modules
.iter()
.map(|module| module.name.clone())
.collect::<HashSet<_>>();
let mut queue = standard_import_queue(modules.iter().flat_map(|module| module.imports.iter()));
while let Some(import_name) = queue.pop_front() {
if !loaded.insert(import_name.clone()) {
continue;
}
let Some(path) = standard_module_path(&manifest.project_root, &import_name) else {
diagnostics.push(missing_standard_module(manifest, &import_name));
continue;
};
let file = path.display().to_string();
match fs::read_to_string(&path) {
Ok(source) => {
let imports = tool_imports(&source);
queue.extend(
imports
.iter()
.filter(|name| standard_module_leaf(name).is_some())
.filter(|name| !loaded.contains(*name))
.cloned(),
);
sources.push(SourceFile {
path: file.clone(),
source: source.clone(),
});
let mut c_imports = tool_c_imports(&source);
for c_import in &mut c_imports {
c_import.source_module = import_name.clone();
}
modules.push(ProjectArtifactModule {
name: import_name,
path: file,
imports,
c_imports,
});
}
Err(err) => diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceReadFailed",
format!("cannot read standard-library module `{}`: {}", file, err),
)),
}
}
}
fn load_standard_import_modules(
manifest: &Manifest,
modules: &mut Vec<ModuleUnit>,
sources: &mut Vec<SourceFile>,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut loaded = modules
.iter()
.map(|module| module.name.clone())
.collect::<HashSet<_>>();
let mut queue = standard_import_queue(
modules
.iter()
.flat_map(|module| module.imports.iter().map(|i| &i.module)),
);
while let Some(import_name) = queue.pop_front() {
if !loaded.insert(import_name.clone()) {
continue;
}
let Some(path) = standard_module_path(&manifest.project_root, &import_name) else {
diagnostics.push(missing_standard_module(manifest, &import_name));
continue;
};
let file = path.display().to_string();
match fs::read_to_string(&path) {
Ok(source) => {
sources.push(SourceFile {
path: file.clone(),
source: source.clone(),
});
match parse_module(&path, file, source) {
Ok(mut module) => {
module.program.module = import_name.clone();
module.name = import_name.clone();
queue.extend(
module
.imports
.iter()
.map(|import| &import.module)
.filter(|name| standard_module_leaf(name).is_some())
.filter(|name| !loaded.contains(*name))
.cloned(),
);
modules.push(module);
}
Err(mut errs) => diagnostics.append(&mut errs),
}
}
Err(err) => diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"ProjectSourceReadFailed",
format!("cannot read standard-library module `{}`: {}", file, err),
)),
}
}
}
fn load_package_standard_import_modules(
manifest: &PackageManifest,
modules: &mut Vec<ModuleUnit>,
sources: &mut Vec<SourceFile>,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut loaded = modules
.iter()
.map(|module| module.name.clone())
.collect::<HashSet<_>>();
let mut queue = standard_import_queue(
modules
.iter()
.flat_map(|module| module.imports.iter().map(|i| &i.module)),
);
while let Some(import_name) = queue.pop_front() {
if !loaded.insert(import_name.clone()) {
continue;
}
let Some(path) = standard_module_path(&manifest.root, &import_name) else {
diagnostics.push(missing_package_standard_module(manifest, &import_name));
continue;
};
let file = path.display().to_string();
match fs::read_to_string(&path) {
Ok(source) => {
sources.push(SourceFile {
path: file.clone(),
source: source.clone(),
});
match parse_module(&path, file, source) {
Ok(mut module) => {
module.program.module = import_name.clone();
module.name = import_name.clone();
queue.extend(
module
.imports
.iter()
.map(|import| &import.module)
.filter(|name| standard_module_leaf(name).is_some())
.filter(|name| !loaded.contains(*name))
.cloned(),
);
modules.push(module);
}
Err(mut errs) => diagnostics.append(&mut errs),
}
}
Err(err) => diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"PackageSourceReadFailed",
format!("cannot read standard-library module `{}`: {}", file, err),
)),
}
}
}
fn standard_import_queue<'a>(imports: impl Iterator<Item = &'a String>) -> VecDeque<String> {
imports
.filter(|name| standard_module_leaf(name).is_some())
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
fn standard_module_path(project_root: &Path, import_name: &str) -> Option<PathBuf> {
let leaf = standard_module_leaf(import_name)?;
standard_library_candidates(project_root)
.into_iter()
.map(|root| root.join(format!("{}.slo", leaf)))
.find(|path| path.is_file())
}
fn standard_module_leaf(import_name: &str) -> Option<&str> {
let leaf = import_name.strip_prefix("std.")?;
if leaf.is_empty()
|| leaf.contains('.')
|| leaf.contains('/')
|| leaf.contains('\\')
|| !leaf
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
{
return None;
}
Some(leaf)
}
fn standard_library_candidates(project_root: &Path) -> Vec<PathBuf> {
let mut candidates = Vec::new();
if let Some(paths) = env::var_os("SLOVO_STD_PATH") {
candidates.extend(env::split_paths(&paths));
}
candidates.extend(installed_standard_library_candidates());
for ancestor in project_root.ancestors() {
candidates.push(ancestor.join("std"));
candidates.push(ancestor.join("lib/std"));
candidates.push(ancestor.join("slovo/std"));
}
let compiler_root = Path::new(env!("CARGO_MANIFEST_DIR"));
for ancestor in compiler_root.ancestors() {
candidates.push(ancestor.join("std"));
candidates.push(ancestor.join("lib/std"));
candidates.push(ancestor.join("slovo/std"));
}
candidates
}
fn installed_standard_library_candidates() -> Vec<PathBuf> {
let Ok(exe) = env::current_exe() else {
return Vec::new();
};
let Some(bin_dir) = exe.parent() else {
return Vec::new();
};
vec![
bin_dir.join("std"),
bin_dir.join("../std"),
bin_dir.join("../share/slovo/std"),
]
}
fn missing_standard_module(manifest: &Manifest, import_name: &str) -> Diagnostic {
Diagnostic::new(
&manifest.path.display().to_string(),
"MissingStandardLibraryModule",
format!(
"standard-library module `{}` could not be found",
import_name
),
)
.hint("set SLOVO_STD_PATH, install `share/slovo/std`, or run from a checkout that has `lib/std` sources")
}
fn missing_package_standard_module(manifest: &PackageManifest, import_name: &str) -> Diagnostic {
Diagnostic::new(
&manifest.path.display().to_string(),
"MissingStandardLibraryModule",
format!(
"standard-library module `{}` could not be found",
import_name
),
)
.hint("set SLOVO_STD_PATH, install `share/slovo/std`, or run from a checkout that has `lib/std` sources")
}
fn parse_module(path: &Path, file: String, source: String) -> Result<ModuleUnit, Vec<Diagnostic>> {
let tokens = lexer::lex(&file, &source)?;
let forms = crate::sexpr::parse(&file, &tokens)?;
let mut errors = Vec::new();
let mut module_decl = None::<(String, Vec<NameSpan>, SExpr)>;
let mut imports = Vec::new();
let mut decls_started = false;
let mut lower_forms = Vec::new();
for (index, form) in forms.iter().enumerate() {
match head_ident(form) {
Some("module") => {
if index != 0 || module_decl.is_some() {
errors.push(
Diagnostic::new(
&file,
"InvalidModule",
"project module declaration must be the first top-level form",
)
.with_span(form.span),
);
continue;
}
match parse_module_decl(&file, form) {
Ok((name, exports, lowered_module)) => {
module_decl = Some((name, exports, lowered_module.clone()));
lower_forms.push(lowered_module);
}
Err(mut errs) => errors.append(&mut errs),
}
}
Some("import") => {
if module_decl.is_none() || decls_started {
errors.push(
Diagnostic::new(
&file,
"InvalidImport",
"imports must appear after the module declaration and before declarations",
)
.with_span(form.span),
);
continue;
}
match parse_import_decl(&file, form) {
Ok(import) => imports.push(import),
Err(mut errs) => errors.append(&mut errs),
}
}
_ => {
decls_started = true;
lower_forms.push(form.clone());
}
}
}
let Some((name, exports, _)) = module_decl else {
errors.push(Diagnostic::new(
&file,
"InvalidModule",
"project source must start with `(module name)` or `(module name (export ...))`",
));
return Err(errors);
};
let expected_name = path
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("");
if expected_name != name {
errors.push(
Diagnostic::new(
&file,
"MissingImport",
format!(
"module declaration `{}` must match source file stem `{}`",
name, expected_name
),
)
.with_span(module_name_span(&lower_forms[0]).unwrap_or(lower_forms[0].span)),
);
}
let imported_names = imports
.iter()
.flat_map(|import| import.names.iter().map(|name| name.name.clone()))
.collect::<Vec<_>>();
let program =
match lower::lower_program_with_imported_names(&file, &lower_forms, &imported_names) {
Ok(program) => program,
Err(mut errs) => {
errors.append(&mut errs);
Program {
module: name.clone(),
type_aliases: Vec::new(),
enums: Vec::new(),
structs: Vec::new(),
c_imports: Vec::new(),
functions: Vec::new(),
tests: Vec::new(),
}
}
};
let (local_functions, local_structs, local_enums, local_aliases, mut duplicate_errors) =
local_declarations(&file, &program);
errors.append(&mut duplicate_errors);
if errors.is_empty() {
Ok(ModuleUnit {
name,
file,
exports,
imports,
program,
local_functions,
local_structs,
local_enums,
local_aliases,
})
} else {
Err(errors)
}
}
fn parse_module_decl(
file: &str,
form: &SExpr,
) -> Result<(String, Vec<NameSpan>, SExpr), Vec<Diagnostic>> {
let mut errors = Vec::new();
let Some(items) = expect_list(form) else {
return Err(vec![Diagnostic::new(
file,
"InvalidModule",
"invalid module form",
)
.with_span(form.span)]);
};
if items.len() != 2 && items.len() != 3 {
return Err(vec![Diagnostic::new(
file,
"InvalidModule",
"project module form must be `(module name)` or `(module name (export ...))`",
)
.with_span(form.span)]);
}
let name = match expect_ident(&items[1]) {
Some(name) => name.to_string(),
None => {
errors.push(
Diagnostic::new(
file,
"InvalidModuleName",
"module name must be an identifier",
)
.with_span(items[1].span),
);
"<error>".to_string()
}
};
let mut exports = Vec::new();
if let Some(export_form) = items.get(2) {
let Some(export_items) = expect_list(export_form) else {
errors.push(
Diagnostic::new(
file,
"InvalidExport",
"export list must be `(export name...)`",
)
.with_span(export_form.span),
);
return Err(errors);
};
if !matches!(export_items.first().and_then(expect_ident), Some("export")) {
errors.push(
Diagnostic::new(
file,
"InvalidExport",
"module option must be an export list",
)
.with_span(export_form.span),
);
} else {
let mut seen = HashMap::new();
for item in &export_items[1..] {
match expect_ident(item) {
Some(name) => {
if let Some(original) = seen.insert(name.to_string(), item.span) {
errors.push(
Diagnostic::new(
file,
"DuplicateName",
format!("duplicate exported name `{}`", name),
)
.with_span(item.span)
.related("original exported name", original),
);
}
if is_reserved_intrinsic(name) {
errors.push(
Diagnostic::new(
file,
"Visibility",
format!("builtin or intrinsic `{}` cannot be exported", name),
)
.with_span(item.span),
);
}
exports.push(NameSpan {
name: name.to_string(),
span: item.span,
});
}
None => errors.push(
Diagnostic::new(
file,
"InvalidExport",
"exported name must be an identifier",
)
.with_span(item.span),
),
}
}
}
}
let lowered_module = SExpr {
kind: SExprKind::List(vec![items[0].clone(), items[1].clone()]),
span: form.span,
};
if errors.is_empty() {
Ok((name, exports, lowered_module))
} else {
Err(errors)
}
}
fn parse_import_decl(file: &str, form: &SExpr) -> Result<ImportDecl, Vec<Diagnostic>> {
let mut errors = Vec::new();
let Some(items) = expect_list(form) else {
return Err(vec![Diagnostic::new(
file,
"InvalidImport",
"invalid import form",
)
.with_span(form.span)]);
};
if items.len() != 3 {
return Err(vec![Diagnostic::new(
file,
"InvalidImport",
"import form must be `(import module (name...))`",
)
.with_span(form.span)]);
}
let module = match expect_ident(&items[1]) {
Some(name) => name.to_string(),
None => {
errors.push(
Diagnostic::new(file, "InvalidImport", "import module must be an identifier")
.with_span(items[1].span),
);
"<error>".to_string()
}
};
let mut names = Vec::new();
let mut seen = HashMap::new();
match expect_list(&items[2]) {
Some(import_items) if !import_items.is_empty() => {
for item in import_items {
match expect_ident(item) {
Some(name) => {
if let Some(original) = seen.insert(name.to_string(), item.span) {
errors.push(
Diagnostic::new(
file,
"DuplicateName",
format!("duplicate imported name `{}`", name),
)
.with_span(item.span)
.related("original imported name", original),
);
}
if is_reserved_intrinsic(name) {
errors.push(
Diagnostic::new(
file,
"Visibility",
format!("builtin or intrinsic `{}` cannot be imported", name),
)
.with_span(item.span),
);
}
names.push(NameSpan {
name: name.to_string(),
span: item.span,
});
}
None => errors.push(
Diagnostic::new(
file,
"InvalidImport",
"imported name must be an identifier",
)
.with_span(item.span),
),
}
}
}
_ => errors.push(
Diagnostic::new(
file,
"InvalidImport",
"import list must contain imported names",
)
.with_span(items[2].span),
),
}
if errors.is_empty() {
Ok(ImportDecl {
module,
module_span: items[1].span,
names,
})
} else {
Err(errors)
}
}
fn tool_imports(source: &str) -> Vec<String> {
let Ok(tokens) = lexer::lex("<tool>", source) else {
return Vec::new();
};
let Ok(forms) = crate::sexpr::parse("<tool>", &tokens) else {
return Vec::new();
};
forms
.iter()
.filter_map(|form| {
let items = expect_list(form)?;
if !matches!(items.first().and_then(expect_ident), Some("import")) {
return None;
}
items.get(1).and_then(expect_ident).map(str::to_string)
})
.collect()
}
fn tool_c_imports(source: &str) -> Vec<ProjectArtifactCImport> {
let Ok(tokens) = lexer::lex("<tool>", source) else {
return Vec::new();
};
let Ok(forms) = crate::sexpr::parse("<tool>", &tokens) else {
return Vec::new();
};
let Ok(program) = lower::lower_program("<tool>", &forms) else {
return Vec::new();
};
program
.c_imports
.iter()
.map(|import| project_artifact_c_import(&program.module, import))
.collect()
}
pub(crate) fn project_artifact_c_import(
source_module: &str,
import: &crate::ast::CImportDecl,
) -> ProjectArtifactCImport {
ProjectArtifactCImport {
source_module: source_module.to_string(),
name: import.name.clone(),
symbol: import.name.clone(),
params: import
.params
.iter()
.map(|param| param.ty.to_string())
.collect(),
return_type: import.return_type.to_string(),
abi: "experimental-fixture-c".to_string(),
}
}
fn external_enum_from_module(module: &ModuleUnit, name: &str) -> Option<ExternalEnum> {
module
.program
.enums
.iter()
.find(|enum_decl| enum_decl.name == name)
.map(|enum_decl| ExternalEnum {
name: name.to_string(),
span: enum_decl.name_span,
variants: enum_decl
.variants
.iter()
.map(|variant| ExternalEnumVariant {
name: variant.name.clone(),
name_span: variant.name_span,
payload_ty: variant.payload_ty.clone(),
payload_ty_span: variant.payload_ty_span,
})
.collect(),
})
}
fn external_enum_from_checked_module(
module: &ModuleUnit,
checked: &CheckedProgram,
name: &str,
) -> Option<ExternalEnum> {
let source_enum = module
.program
.enums
.iter()
.find(|enum_decl| enum_decl.name == name)?;
let checked_enum = checked
.enums
.iter()
.find(|enum_decl| enum_decl.name == name)?;
Some(ExternalEnum {
name: name.to_string(),
span: source_enum.name_span,
variants: checked_enum
.variants
.iter()
.map(|checked_variant| {
let source_variant = source_enum
.variants
.iter()
.find(|variant| variant.name == checked_variant.name);
ExternalEnumVariant {
name: checked_variant.name.clone(),
name_span: source_variant
.map_or(source_enum.name_span, |variant| variant.name_span),
payload_ty: checked_variant.payload_ty.clone(),
payload_ty_span: source_variant.and_then(|variant| variant.payload_ty_span),
}
})
.collect(),
})
}
fn local_declarations(
file: &str,
program: &Program,
) -> (
HashMap<String, DeclInfo>,
HashMap<String, DeclInfo>,
HashMap<String, DeclInfo>,
HashMap<String, DeclInfo>,
Vec<Diagnostic>,
) {
let mut all = HashMap::<String, DeclInfo>::new();
let mut functions = HashMap::new();
let mut structs = HashMap::new();
let mut enums = HashMap::new();
let mut aliases = HashMap::new();
let mut errors = Vec::new();
for alias in &program.type_aliases {
let decl = DeclInfo {
span: alias.name_span,
kind: DeclKind::TypeAlias,
};
if let Some(original) = all.insert(alias.name.clone(), decl.clone()) {
if original.kind == DeclKind::CImport {
errors.push(
Diagnostic::new(
file,
"DuplicateTopLevelName",
format!("duplicate top-level name `{}`", alias.name),
)
.with_span(alias.name_span)
.related("original declaration", original.span),
);
} else {
errors.push(duplicate_name(
file,
&alias.name,
alias.name_span,
original.span,
));
}
}
aliases.insert(alias.name.clone(), decl);
}
for function in &program.functions {
let decl = DeclInfo {
span: function.span,
kind: DeclKind::Function,
};
if is_reserved_intrinsic(&function.name) {
errors.push(
Diagnostic::new(
file,
"DuplicateName",
format!("builtin or intrinsic `{}` is reserved", function.name),
)
.with_span(function.span),
);
}
if let Some(original) = all.insert(function.name.clone(), decl.clone()) {
errors.push(duplicate_name(
file,
&function.name,
function.span,
original.span,
));
}
functions.insert(function.name.clone(), decl);
}
for import in &program.c_imports {
let decl = DeclInfo {
span: import.span,
kind: DeclKind::CImport,
};
if is_reserved_intrinsic(&import.name) {
errors.push(
Diagnostic::new(
file,
"ReservedName",
format!("builtin or intrinsic `{}` is reserved", import.name),
)
.with_span(import.name_span),
);
}
if let Some(original) = all.insert(import.name.clone(), decl.clone()) {
errors.push(
Diagnostic::new(
file,
"DuplicateTopLevelName",
format!("duplicate top-level name `{}`", import.name),
)
.with_span(import.name_span)
.related("original declaration", original.span),
);
}
functions.insert(import.name.clone(), decl);
}
for struct_decl in &program.structs {
let decl = DeclInfo {
span: struct_decl.name_span,
kind: DeclKind::Struct,
};
if is_reserved_intrinsic(&struct_decl.name) {
errors.push(
Diagnostic::new(
file,
"DuplicateName",
format!("builtin or intrinsic `{}` is reserved", struct_decl.name),
)
.with_span(struct_decl.name_span),
);
}
if let Some(original) = all.insert(struct_decl.name.clone(), decl.clone()) {
if original.kind == DeclKind::CImport {
errors.push(
Diagnostic::new(
file,
"DuplicateTopLevelName",
format!("duplicate top-level name `{}`", struct_decl.name),
)
.with_span(struct_decl.name_span)
.related("original declaration", original.span),
);
} else {
errors.push(duplicate_name(
file,
&struct_decl.name,
struct_decl.name_span,
original.span,
));
}
}
structs.insert(struct_decl.name.clone(), decl);
}
for enum_decl in &program.enums {
let decl = DeclInfo {
span: enum_decl.name_span,
kind: DeclKind::Enum,
};
if is_reserved_intrinsic(&enum_decl.name) {
errors.push(
Diagnostic::new(
file,
"DuplicateName",
format!("builtin or intrinsic `{}` is reserved", enum_decl.name),
)
.with_span(enum_decl.name_span),
);
}
if let Some(original) = all.insert(enum_decl.name.clone(), decl.clone()) {
if original.kind == DeclKind::CImport {
errors.push(
Diagnostic::new(
file,
"DuplicateTopLevelName",
format!("duplicate top-level name `{}`", enum_decl.name),
)
.with_span(enum_decl.name_span)
.related("original declaration", original.span),
);
} else {
errors.push(duplicate_name(
file,
&enum_decl.name,
enum_decl.name_span,
original.span,
));
}
}
enums.insert(enum_decl.name.clone(), decl);
}
(functions, structs, enums, aliases, errors)
}
fn validate_workspace_packages(
workspace: &WorkspaceManifest,
packages: &mut [PackageUnit],
diagnostics: &mut Vec<Diagnostic>,
) {
let mut by_member = HashMap::<String, usize>::new();
for (index, package) in packages.iter().enumerate() {
by_member.insert(package.manifest.member.clone(), index);
}
let mut by_name = BTreeMap::<String, Vec<usize>>::new();
for (index, package) in packages.iter().enumerate() {
by_name
.entry(package.manifest.name.clone())
.or_default()
.push(index);
}
for (name, indices) in by_name.iter().filter(|(_, indices)| indices.len() > 1) {
for index in indices {
diagnostics.push(Diagnostic::new(
&packages[*index].manifest.path.display().to_string(),
"DuplicatePackageName",
format!("duplicate package name `{}`", name),
));
}
}
if let Some(default_package) = &workspace.default_package {
if !by_name.contains_key(default_package) {
diagnostics.push(Diagnostic::new(
&workspace.path.display().to_string(),
"WorkspaceDefaultPackageMissing",
format!(
"workspace default_package `{}` is not a workspace package",
default_package
),
));
}
}
let mut resolved_dependencies = vec![Vec::<usize>::new(); packages.len()];
for (package_index, package) in packages.iter().enumerate() {
for dependency in &package.manifest.dependencies {
let target_member =
match normalize_dependency_path(workspace, &package.manifest, &dependency.path) {
Ok(member) => member,
Err(diagnostic) => {
diagnostics.push(diagnostic.with_span(dependency.span));
continue;
}
};
let Some(target_index) = by_member.get(&target_member).copied() else {
diagnostics.push(
Diagnostic::new(
&package.manifest.path.display().to_string(),
"MissingPackageDependency",
format!(
"dependency `{}` path `{}` is not a workspace member",
dependency.key, dependency.path
),
)
.with_span(dependency.span),
);
continue;
};
let target = &packages[target_index];
if dependency.key != target.manifest.name {
diagnostics.push(
Diagnostic::new(
&package.manifest.path.display().to_string(),
"DependencyNameMismatch",
format!(
"dependency key `{}` must match package name `{}`",
dependency.key, target.manifest.name
),
)
.with_span(dependency.span),
);
continue;
}
if !resolved_dependencies[package_index].contains(&target_index) {
resolved_dependencies[package_index].push(target_index);
}
}
}
let package_names = packages
.iter()
.map(|package| package.manifest.name.clone())
.collect::<Vec<_>>();
for (package, dependencies) in packages.iter_mut().zip(resolved_dependencies) {
package.dependency_indices = dependencies;
package
.dependency_indices
.sort_by(|left, right| package_names[*left].cmp(&package_names[*right]));
}
detect_package_cycle(packages, diagnostics);
}
fn detect_package_cycle(packages: &[PackageUnit], diagnostics: &mut Vec<Diagnostic>) {
let mut state = vec![0_u8; packages.len()];
let mut stack = Vec::new();
let mut indices = (0..packages.len()).collect::<Vec<_>>();
indices.sort_by(|left, right| {
packages[*left]
.manifest
.name
.cmp(&packages[*right].manifest.name)
});
for index in indices {
if state[index] == 0 && dfs_package_cycle(index, packages, &mut state, &mut stack) {
let cycle_start = stack
.last()
.and_then(|last| {
stack[..stack.len().saturating_sub(1)]
.iter()
.position(|idx| idx == last)
})
.unwrap_or(0);
let cycle_nodes = &stack[cycle_start..];
let cycle = cycle_nodes
.iter()
.map(|idx| packages[*idx].manifest.name.as_str())
.collect::<Vec<_>>()
.join(" -> ");
let mut diagnostic = Diagnostic::new(
&packages[index].manifest.path.display().to_string(),
"PackageDependencyCycle",
format!("workspace package dependencies contain a cycle: {}", cycle),
);
for edge in cycle_nodes.windows(2) {
let from = &packages[edge[0]];
let to = &packages[edge[1]];
if let Some(dep) = from
.manifest
.dependencies
.iter()
.find(|dep| dep.key == to.manifest.name)
{
diagnostic = diagnostic.related_in_file(
from.manifest.path.display().to_string(),
format!(
"package `{}` depends on `{}`",
from.manifest.name, to.manifest.name
),
dep.span,
);
}
}
diagnostics.push(diagnostic);
return;
}
}
}
fn dfs_package_cycle(
index: usize,
packages: &[PackageUnit],
state: &mut [u8],
stack: &mut Vec<usize>,
) -> bool {
state[index] = 1;
stack.push(index);
for next in &packages[index].dependency_indices {
if state[*next] == 1 {
stack.push(*next);
return true;
}
if state[*next] == 0 && dfs_package_cycle(*next, packages, state, stack) {
return true;
}
}
stack.pop();
state[index] = 2;
false
}
fn resolve_and_check_workspace(
loaded: WorkspaceLoaded,
require_entry_wrapper: bool,
) -> Result<CheckedProject, ProjectLoadFailure> {
let WorkspaceLoaded {
workspace,
packages,
sources,
..
} = loaded;
let mut diagnostics = Vec::new();
let mut module_maps = Vec::new();
for package in &packages {
let mut by_name = HashMap::<String, usize>::new();
for (index, module) in package.modules.iter().enumerate() {
if let Some(original) = by_name.insert(module.name.clone(), index) {
diagnostics.push(duplicate_name(
&module.file,
&module.name,
Span::new(0, 0),
package.modules[original]
.program
.functions
.first()
.map_or(Span::new(0, 0), |f| f.span),
));
}
}
module_maps.push(by_name);
}
let export_maps = packages
.iter()
.map(|package| build_export_maps(&package.modules, &mut diagnostics))
.collect::<Vec<_>>();
let import_maps =
resolve_workspace_imports(&packages, &module_maps, &export_maps, &mut diagnostics);
for (package_index, package) in packages.iter().enumerate() {
detect_import_cycle(
&package.modules,
&module_maps[package_index],
&mut diagnostics,
);
}
if !diagnostics.is_empty() {
return Err(ProjectLoadFailure {
diagnostics,
sources,
artifact: Some(workspace_artifact(&workspace, &packages, None, None, None)),
});
}
let package_order = package_topo_order(&packages);
let mut module_orders = HashMap::<usize, Vec<usize>>::new();
for package_index in &package_order {
module_orders.insert(
*package_index,
topo_order(
&packages[*package_index].modules,
&module_maps[*package_index],
),
);
}
let mut checked_by_module = HashMap::<(usize, String), CheckedProgram>::new();
let mut check_errors = Vec::new();
for package_index in &package_order {
let package = &packages[*package_index];
let module_order = module_orders
.get(package_index)
.expect("module order was computed");
for module_index in module_order {
let module = &package.modules[*module_index];
let imports = &import_maps[*package_index][*module_index];
let mut external_functions = Vec::new();
let mut external_structs = Vec::new();
let mut external_enums = Vec::new();
for (name, binding) in imports {
let provider = &packages[binding.provider_package].modules[binding.provider_module];
let checked_provider =
checked_by_module.get(&(binding.provider_package, provider.name.clone()));
match binding.kind {
DeclKind::Function => {
if let Some(function) = checked_provider
.and_then(|program| program.functions.iter().find(|f| f.name == *name))
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: function.params.iter().map(|(_, ty)| ty.clone()).collect(),
return_type: function.return_type.clone(),
foreign: false,
});
} else if let Some(function) =
provider.program.functions.iter().find(|f| f.name == *name)
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: function.params.iter().map(|p| p.ty.clone()).collect(),
return_type: function.return_type.clone(),
foreign: false,
});
}
}
DeclKind::CImport => {
if let Some(import) = checked_provider
.and_then(|program| program.c_imports.iter().find(|f| f.name == *name))
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: import.params.iter().map(|(_, ty)| ty.clone()).collect(),
return_type: import.return_type.clone(),
foreign: true,
});
} else if let Some(import) =
provider.program.c_imports.iter().find(|f| f.name == *name)
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: import.params.iter().map(|p| p.ty.clone()).collect(),
return_type: import.return_type.clone(),
foreign: true,
});
}
}
DeclKind::Struct => {
if let Some(struct_decl) = checked_provider
.and_then(|program| program.structs.iter().find(|s| s.name == *name))
{
external_structs.push(ExternalStruct {
name: name.clone(),
fields: struct_decl.fields.clone(),
span: struct_decl.span,
});
} else if let Some(struct_decl) =
provider.program.structs.iter().find(|s| s.name == *name)
{
external_structs.push(ExternalStruct {
name: name.clone(),
fields: struct_decl
.fields
.iter()
.map(|field| (field.name.clone(), field.ty.clone()))
.collect(),
span: struct_decl.name_span,
});
}
}
DeclKind::Enum => {
if let Some(enum_decl) = checked_provider
.and_then(|program| {
external_enum_from_checked_module(provider, program, name)
})
.or_else(|| external_enum_from_module(provider, name))
{
external_enums.push(enum_decl);
}
}
DeclKind::TypeAlias => {}
}
}
match check::check_program_with_imports(
&module.file,
module.program.clone(),
&external_functions,
&external_structs,
&external_enums,
) {
Ok(checked) => {
checked_by_module.insert((*package_index, module.name.clone()), checked);
}
Err(mut errs) => check_errors.append(&mut errs),
}
}
}
if !check_errors.is_empty() {
return Err(ProjectLoadFailure {
diagnostics: check_errors,
sources,
artifact: Some(workspace_artifact(
&workspace,
&packages,
Some(&package_order),
Some(&module_orders),
None,
)),
});
}
let mut combined = CheckedProgram {
module: workspace
.root
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("workspace")
.to_string(),
enums: Vec::new(),
structs: Vec::new(),
c_imports: Vec::new(),
functions: Vec::new(),
tests: Vec::new(),
};
for package_index in &package_order {
let package = &packages[*package_index];
let module_order = module_orders
.get(package_index)
.expect("module order was computed");
for module_index in module_order {
let module = &package.modules[*module_index];
let mut checked = checked_by_module
.remove(&(*package_index, module.name.clone()))
.expect("module was checked");
let symbols = workspace_symbol_map(
&packages,
*package_index,
*module_index,
&import_maps[*package_index][*module_index],
);
rewrite_checked_program(&mut checked, &symbols);
combined.enums.append(&mut checked.enums);
combined.structs.append(&mut checked.structs);
combined.c_imports.append(&mut checked.c_imports);
combined.functions.append(&mut checked.functions);
combined.tests.append(&mut checked.tests);
}
}
let mut selected_build_entry_package = None;
if require_entry_wrapper {
match select_workspace_build_entry(&workspace, &packages, &module_maps) {
Ok(package_index) => {
selected_build_entry_package = Some(package_index);
add_workspace_entry_wrapper(
&packages,
package_index,
&module_maps[package_index],
&mut combined,
&mut diagnostics,
);
}
Err(diagnostic) => diagnostics.push(diagnostic),
}
if !diagnostics.is_empty() {
return Err(ProjectLoadFailure {
diagnostics,
sources,
artifact: Some(workspace_artifact(
&workspace,
&packages,
Some(&package_order),
Some(&module_orders),
selected_build_entry_package.map(|index| packages[index].manifest.name.clone()),
)),
});
}
}
let artifact = workspace_artifact(
&workspace,
&packages,
Some(&package_order),
Some(&module_orders),
selected_build_entry_package.map(|index| packages[index].manifest.name.clone()),
);
Ok(CheckedProject {
program: combined,
sources,
artifact,
})
}
fn resolve_and_check(
manifest: Manifest,
modules: Vec<ModuleUnit>,
sources: Vec<SourceFile>,
require_entry_wrapper: bool,
) -> Result<CheckedProject, ProjectLoadFailure> {
let mut diagnostics = Vec::new();
let mut by_name = HashMap::<String, usize>::new();
for (index, module) in modules.iter().enumerate() {
if let Some(original) = by_name.insert(module.name.clone(), index) {
diagnostics.push(duplicate_name(
&module.file,
&module.name,
Span::new(0, 0),
modules[original]
.program
.functions
.first()
.map_or(Span::new(0, 0), |f| f.span),
));
}
}
if !by_name.contains_key(&manifest.entry) {
diagnostics.push(Diagnostic::new(
&manifest.path.display().to_string(),
"MissingImport",
format!("entry module `{}` does not exist", manifest.entry),
));
}
let export_maps = build_export_maps(&modules, &mut diagnostics);
let import_maps = resolve_imports(&modules, &by_name, &export_maps, &mut diagnostics);
detect_import_cycle(&modules, &by_name, &mut diagnostics);
if !diagnostics.is_empty() {
return Err(ProjectLoadFailure {
diagnostics,
sources,
artifact: Some(artifact_from_modules(&manifest, &modules, None)),
});
}
let order = topo_order(&modules, &by_name);
let mut checked_by_module = HashMap::<String, CheckedProgram>::new();
let mut check_errors = Vec::new();
for module_index in &order {
let module = &modules[*module_index];
let imports = &import_maps[*module_index];
let mut external_functions = Vec::new();
let mut external_structs = Vec::new();
let mut external_enums = Vec::new();
for (name, binding) in imports {
let provider = &modules[by_name[&binding.provider]];
let checked_provider = checked_by_module.get(&binding.provider);
match binding.kind {
DeclKind::Function => {
if let Some(function) = checked_provider
.and_then(|program| program.functions.iter().find(|f| f.name == *name))
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: function.params.iter().map(|(_, ty)| ty.clone()).collect(),
return_type: function.return_type.clone(),
foreign: false,
});
} else if let Some(function) =
provider.program.functions.iter().find(|f| f.name == *name)
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: function.params.iter().map(|p| p.ty.clone()).collect(),
return_type: function.return_type.clone(),
foreign: false,
});
}
}
DeclKind::CImport => {
if let Some(import) = checked_provider
.and_then(|program| program.c_imports.iter().find(|f| f.name == *name))
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: import.params.iter().map(|(_, ty)| ty.clone()).collect(),
return_type: import.return_type.clone(),
foreign: true,
});
} else if let Some(import) =
provider.program.c_imports.iter().find(|f| f.name == *name)
{
external_functions.push(ExternalFunction {
name: name.clone(),
params: import.params.iter().map(|p| p.ty.clone()).collect(),
return_type: import.return_type.clone(),
foreign: true,
});
}
}
DeclKind::Struct => {
if let Some(struct_decl) = checked_provider
.and_then(|program| program.structs.iter().find(|s| s.name == *name))
{
external_structs.push(ExternalStruct {
name: name.clone(),
fields: struct_decl.fields.clone(),
span: struct_decl.span,
});
} else if let Some(struct_decl) =
provider.program.structs.iter().find(|s| s.name == *name)
{
external_structs.push(ExternalStruct {
name: name.clone(),
fields: struct_decl
.fields
.iter()
.map(|field| (field.name.clone(), field.ty.clone()))
.collect(),
span: struct_decl.name_span,
});
}
}
DeclKind::Enum => {
if let Some(enum_decl) = checked_provider
.and_then(|program| {
external_enum_from_checked_module(provider, program, name)
})
.or_else(|| external_enum_from_module(provider, name))
{
external_enums.push(enum_decl);
}
}
DeclKind::TypeAlias => {}
}
}
match check::check_program_with_imports(
&module.file,
module.program.clone(),
&external_functions,
&external_structs,
&external_enums,
) {
Ok(checked) => {
checked_by_module.insert(module.name.clone(), checked);
}
Err(mut errs) => check_errors.append(&mut errs),
}
}
if !check_errors.is_empty() {
return Err(ProjectLoadFailure {
diagnostics: check_errors,
sources,
artifact: Some(artifact_from_modules(&manifest, &modules, Some(&order))),
});
}
let mut combined = CheckedProgram {
module: manifest.name.clone(),
enums: Vec::new(),
structs: Vec::new(),
c_imports: Vec::new(),
functions: Vec::new(),
tests: Vec::new(),
};
for module_index in &order {
let module = &modules[*module_index];
let mut checked = checked_by_module
.remove(&module.name)
.expect("module was checked");
let symbols = symbol_map(module, &import_maps[*module_index]);
rewrite_checked_program(&mut checked, &symbols);
combined.enums.append(&mut checked.enums);
combined.structs.append(&mut checked.structs);
combined.c_imports.append(&mut checked.c_imports);
combined.functions.append(&mut checked.functions);
combined.tests.append(&mut checked.tests);
}
if require_entry_wrapper {
add_entry_wrapper(
&manifest,
&modules,
&by_name,
&mut combined,
&mut diagnostics,
);
if !diagnostics.is_empty() {
return Err(ProjectLoadFailure {
diagnostics,
sources,
artifact: Some(artifact_from_modules(&manifest, &modules, Some(&order))),
});
}
}
let artifact = artifact_from_modules(&manifest, &modules, Some(&order));
Ok(CheckedProject {
program: combined,
sources,
artifact,
})
}
fn build_export_maps(
modules: &[ModuleUnit],
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<HashMap<String, DeclKind>> {
modules
.iter()
.map(|module| {
let mut exports = HashMap::new();
for export in &module.exports {
let kind = module
.local_functions
.get(&export.name)
.or_else(|| module.local_structs.get(&export.name))
.or_else(|| module.local_enums.get(&export.name))
.map(|decl| decl.kind);
match kind {
Some(kind) => {
exports.insert(export.name.clone(), kind);
}
None if module.local_aliases.contains_key(&export.name) => diagnostics.push(
Diagnostic::new(
&module.file,
"Visibility",
format!(
"type alias `{}` is module-local and cannot be exported",
export.name
),
)
.with_span(export.span)
.related(
"type alias declaration",
module.local_aliases[&export.name].span,
)
.hint("export functions, structs, or enums; imported signatures see concrete target types"),
),
None => diagnostics.push(
Diagnostic::new(
&module.file,
"Visibility",
format!(
"exported name `{}` is not a local function, struct, or enum",
export.name
),
)
.with_span(export.span),
),
}
}
exports
})
.collect()
}
fn resolve_imports(
modules: &[ModuleUnit],
by_name: &HashMap<String, usize>,
export_maps: &[HashMap<String, DeclKind>],
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<HashMap<String, ImportBinding>> {
modules
.iter()
.map(|module| {
let mut imports = HashMap::<String, ImportBinding>::new();
for import in &module.imports {
let Some(provider_index) = by_name.get(&import.module).copied() else {
diagnostics.push(
Diagnostic::new(
&module.file,
"MissingImport",
format!("imported module `{}` does not exist", import.module),
)
.with_span(import.module_span),
);
continue;
};
let provider = &modules[provider_index];
for name in &import.names {
if let Some(local) = module
.local_functions
.get(&name.name)
.or_else(|| module.local_structs.get(&name.name))
.or_else(|| module.local_enums.get(&name.name))
.or_else(|| module.local_aliases.get(&name.name))
{
diagnostics.push(duplicate_name(
&module.file,
&name.name,
name.span,
local.span,
));
continue;
}
if let Some(existing) = imports.get(&name.name) {
if existing.provider == provider.name {
diagnostics.push(
Diagnostic::new(
&module.file,
"DuplicateName",
format!("duplicate imported name `{}`", name.name),
)
.with_span(name.span)
.related("original imported name", existing.import_span),
);
} else {
diagnostics.push(
Diagnostic::new(
&module.file,
"AmbiguousName",
format!(
"name `{}` is imported from multiple modules",
name.name
),
)
.with_span(name.span)
.related("previous imported candidate", existing.import_span),
);
}
continue;
}
let provider_local = provider
.local_functions
.get(&name.name)
.or_else(|| provider.local_structs.get(&name.name))
.or_else(|| provider.local_enums.get(&name.name))
.or_else(|| provider.local_aliases.get(&name.name));
let exported = export_maps[provider_index].get(&name.name).copied();
match (provider_local, exported) {
(_, Some(kind)) => {
imports.insert(
name.name.clone(),
ImportBinding {
provider: provider.name.clone(),
kind,
import_span: name.span,
},
);
}
(Some(decl), None) if decl.kind == DeclKind::TypeAlias => diagnostics.push(
Diagnostic::new(
&module.file,
"Visibility",
format!(
"type alias `{}` is module-local and cannot be imported from module `{}`",
name.name, provider.name
),
)
.with_span(name.span)
.related_in_file(
provider.file.clone(),
"type alias declaration",
decl.span,
)
.hint("import functions, structs, or enums; function signatures expose the alias target type"),
),
(Some(decl), None) => diagnostics.push(
Diagnostic::new(
&module.file,
"Visibility",
format!(
"name `{}` is not exported by module `{}`",
name.name, provider.name
),
)
.with_span(name.span)
.related_in_file(
provider.file.clone(),
"private declaration",
decl.span,
),
),
(None, None) => diagnostics.push(
Diagnostic::new(
&module.file,
"MissingImport",
format!(
"module `{}` has no exported function, struct, or enum `{}`",
provider.name, name.name
),
)
.with_span(name.span),
),
}
}
}
imports
})
.collect()
}
fn resolve_workspace_imports(
packages: &[PackageUnit],
module_maps: &[HashMap<String, usize>],
export_maps: &[Vec<HashMap<String, DeclKind>>],
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<Vec<HashMap<String, WorkspaceImportBinding>>> {
let mut package_by_name = HashMap::<String, usize>::new();
for (index, package) in packages.iter().enumerate() {
package_by_name.insert(package.manifest.name.clone(), index);
}
packages
.iter()
.enumerate()
.map(|(package_index, package)| {
package
.modules
.iter()
.enumerate()
.map(|(module_index, module)| {
let mut imports = HashMap::<String, WorkspaceImportBinding>::new();
for import in &module.imports {
let Some((provider_package_index, provider_module_index)) =
resolve_workspace_import_module(
packages,
module_maps,
&package_by_name,
package_index,
module,
import,
diagnostics,
)
else {
continue;
};
let provider =
&packages[provider_package_index].modules[provider_module_index];
for name in &import.names {
if let Some(local) = module
.local_functions
.get(&name.name)
.or_else(|| module.local_structs.get(&name.name))
.or_else(|| module.local_enums.get(&name.name))
.or_else(|| module.local_aliases.get(&name.name))
{
diagnostics.push(duplicate_name(
&module.file,
&name.name,
name.span,
local.span,
));
continue;
}
if let Some(existing) = imports.get(&name.name) {
if existing.provider_package == provider_package_index
&& existing.provider_module == provider_module_index
{
diagnostics.push(
Diagnostic::new(
&module.file,
"DuplicateName",
format!("duplicate imported name `{}`", name.name),
)
.with_span(name.span)
.related("original imported name", existing.import_span),
);
} else {
diagnostics.push(
Diagnostic::new(
&module.file,
"AmbiguousName",
format!(
"name `{}` is imported from multiple modules",
name.name
),
)
.with_span(name.span)
.related(
"previous imported candidate",
existing.import_span,
),
);
}
continue;
}
let provider_local = provider
.local_functions
.get(&name.name)
.or_else(|| provider.local_structs.get(&name.name))
.or_else(|| provider.local_enums.get(&name.name))
.or_else(|| provider.local_aliases.get(&name.name));
let exported = export_maps[provider_package_index]
[provider_module_index]
.get(&name.name)
.copied();
match (provider_local, exported) {
(_, Some(kind)) => {
imports.insert(
name.name.clone(),
WorkspaceImportBinding {
provider_package: provider_package_index,
provider_module: provider_module_index,
kind,
import_span: name.span,
},
);
}
(Some(decl), None) if decl.kind == DeclKind::TypeAlias => {
diagnostics.push(
Diagnostic::new(
&module.file,
"Visibility",
format!(
"type alias `{}` is module-local and cannot be imported from module `{}`",
name.name, import.module
),
)
.with_span(name.span)
.related_in_file(
provider.file.clone(),
"type alias declaration",
decl.span,
)
.hint("import functions, structs, or enums; function signatures expose the alias target type"),
);
}
(Some(decl), None) => diagnostics.push(
Diagnostic::new(
&module.file,
"Visibility",
format!(
"name `{}` is not exported by module `{}`",
name.name, import.module
),
)
.with_span(name.span)
.related_in_file(
provider.file.clone(),
"private declaration",
decl.span,
),
),
(None, None) => diagnostics.push(
Diagnostic::new(
&module.file,
"MissingImport",
format!(
"module `{}` has no exported function, struct, or enum `{}`",
import.module, name.name
),
)
.with_span(name.span),
),
}
}
}
let _ = module_index;
imports
})
.collect()
})
.collect()
}
fn resolve_workspace_import_module(
packages: &[PackageUnit],
module_maps: &[HashMap<String, usize>],
package_by_name: &HashMap<String, usize>,
package_index: usize,
module: &ModuleUnit,
import: &ImportDecl,
diagnostics: &mut Vec<Diagnostic>,
) -> Option<(usize, usize)> {
if standard_module_leaf(&import.module).is_some() {
let Some(provider_module_index) = module_maps[package_index].get(&import.module).copied()
else {
diagnostics.push(
Diagnostic::new(
&module.file,
"MissingImport",
format!("imported module `{}` does not exist", import.module),
)
.with_span(import.module_span),
);
return None;
};
return Some((package_index, provider_module_index));
}
if let Some((package_name, module_name)) = import.module.split_once('.') {
if module_name.contains('.') || package_name.is_empty() || module_name.is_empty() {
diagnostics.push(
Diagnostic::new(
&module.file,
"MissingPackageModule",
format!(
"package-qualified import `{}` must be `<package>.<module>`",
import.module
),
)
.with_span(import.module_span),
);
return None;
}
let Some(provider_package_index) = package_by_name.get(package_name).copied() else {
diagnostics.push(
Diagnostic::new(
&module.file,
"MissingPackageDependency",
format!("imported package `{}` does not exist", package_name),
)
.with_span(import.module_span),
);
return None;
};
if !packages[package_index]
.dependency_indices
.contains(&provider_package_index)
{
diagnostics.push(
Diagnostic::new(
&module.file,
"PackageImportNotDependency",
format!(
"package `{}` is not a direct dependency of `{}`",
package_name, packages[package_index].manifest.name
),
)
.with_span(import.module_span),
);
return None;
}
let Some(provider_module_index) = module_maps[provider_package_index]
.get(module_name)
.copied()
else {
diagnostics.push(
Diagnostic::new(
&module.file,
"MissingPackageModule",
format!("package `{}` has no module `{}`", package_name, module_name),
)
.with_span(import.module_span),
);
return None;
};
return Some((provider_package_index, provider_module_index));
}
let Some(provider_module_index) = module_maps[package_index].get(&import.module).copied()
else {
diagnostics.push(
Diagnostic::new(
&module.file,
"MissingImport",
format!("imported module `{}` does not exist", import.module),
)
.with_span(import.module_span),
);
return None;
};
Some((package_index, provider_module_index))
}
fn detect_import_cycle(
modules: &[ModuleUnit],
by_name: &HashMap<String, usize>,
diagnostics: &mut Vec<Diagnostic>,
) {
let mut state = vec![0_u8; modules.len()];
let mut stack = Vec::new();
for index in 0..modules.len() {
if state[index] == 0 && dfs_cycle(index, modules, by_name, &mut state, &mut stack) {
let cycle_start = stack
.last()
.and_then(|last| {
stack[..stack.len().saturating_sub(1)]
.iter()
.position(|idx| idx == last)
})
.unwrap_or(0);
let cycle_nodes = &stack[cycle_start..];
let cycle = cycle_nodes
.iter()
.map(|idx| modules[*idx].name.as_str())
.collect::<Vec<_>>()
.join(" -> ");
let primary_edge = cycle_nodes
.windows(2)
.last()
.and_then(|edge| import_edge(&modules[edge[0]], &modules[edge[1]].name));
let primary_file = primary_edge
.map(|(file, _)| file)
.unwrap_or_else(|| modules[index].file.as_str());
let primary_span = primary_edge.map(|(_, span)| span);
let mut diagnostic = Diagnostic::new(
primary_file,
"ImportCycle",
format!("project imports contain a cycle: {}", cycle),
);
if let Some(span) = primary_span {
diagnostic = diagnostic.with_span(span);
}
for edge in cycle_nodes.windows(2) {
if let Some((file, span)) = import_edge(&modules[edge[0]], &modules[edge[1]].name) {
diagnostic = diagnostic.related_in_file(
file.to_string(),
format!(
"module `{}` imports `{}`",
modules[edge[0]].name, modules[edge[1]].name
),
span,
);
}
}
diagnostics.push(diagnostic);
return;
}
}
}
fn import_edge<'a>(from: &'a ModuleUnit, to: &str) -> Option<(&'a str, Span)> {
from.imports
.iter()
.find(|import| import.module == to)
.map(|import| (from.file.as_str(), import.module_span))
}
fn dfs_cycle(
index: usize,
modules: &[ModuleUnit],
by_name: &HashMap<String, usize>,
state: &mut [u8],
stack: &mut Vec<usize>,
) -> bool {
state[index] = 1;
stack.push(index);
for import in &modules[index].imports {
let Some(next) = by_name.get(&import.module).copied() else {
continue;
};
if state[next] == 1 {
stack.push(next);
return true;
}
if state[next] == 0 && dfs_cycle(next, modules, by_name, state, stack) {
return true;
}
}
stack.pop();
state[index] = 2;
false
}
fn topo_order(modules: &[ModuleUnit], by_name: &HashMap<String, usize>) -> Vec<usize> {
let mut indegree = vec![0_usize; modules.len()];
let mut dependents = vec![Vec::<usize>::new(); modules.len()];
for (index, module) in modules.iter().enumerate() {
let mut seen = HashSet::new();
for import in &module.imports {
if let Some(provider) = by_name.get(&import.module).copied() {
if seen.insert(provider) {
indegree[index] += 1;
dependents[provider].push(index);
}
}
}
}
for deps in &mut dependents {
deps.sort_by(|left, right| modules[*left].name.cmp(&modules[*right].name));
}
let mut ready = modules
.iter()
.enumerate()
.filter(|(index, _)| indegree[*index] == 0)
.map(|(index, _)| index)
.collect::<VecDeque<_>>();
ready
.make_contiguous()
.sort_by(|left, right| modules[*left].name.cmp(&modules[*right].name));
let mut order = Vec::new();
while let Some(index) = ready.pop_front() {
order.push(index);
for dependent in &dependents[index] {
indegree[*dependent] -= 1;
if indegree[*dependent] == 0 {
ready.push_back(*dependent);
ready
.make_contiguous()
.sort_by(|left, right| modules[*left].name.cmp(&modules[*right].name));
}
}
}
order
}
fn package_topo_order(packages: &[PackageUnit]) -> Vec<usize> {
let mut indegree = vec![0_usize; packages.len()];
let mut dependents = vec![Vec::<usize>::new(); packages.len()];
for (index, package) in packages.iter().enumerate() {
let mut seen = HashSet::new();
for dependency in &package.dependency_indices {
if seen.insert(*dependency) {
indegree[index] += 1;
dependents[*dependency].push(index);
}
}
}
for deps in &mut dependents {
deps.sort_by(|left, right| {
packages[*left]
.manifest
.name
.cmp(&packages[*right].manifest.name)
});
}
let mut ready = packages
.iter()
.enumerate()
.filter(|(index, _)| indegree[*index] == 0)
.map(|(index, _)| index)
.collect::<VecDeque<_>>();
ready.make_contiguous().sort_by(|left, right| {
packages[*left]
.manifest
.name
.cmp(&packages[*right].manifest.name)
});
let mut order = Vec::new();
while let Some(index) = ready.pop_front() {
order.push(index);
for dependent in &dependents[index] {
indegree[*dependent] -= 1;
if indegree[*dependent] == 0 {
ready.push_back(*dependent);
ready.make_contiguous().sort_by(|left, right| {
packages[*left]
.manifest
.name
.cmp(&packages[*right].manifest.name)
});
}
}
}
order
}
fn symbol_map(
module: &ModuleUnit,
imports: &HashMap<String, ImportBinding>,
) -> HashMap<String, String> {
let mut symbols = HashMap::new();
for (name, decl) in &module.local_functions {
if decl.kind == DeclKind::Function {
symbols.insert(name.clone(), symbol_name(&module.name, name));
}
}
for name in module.local_structs.keys() {
symbols.insert(name.clone(), symbol_name(&module.name, name));
}
for name in module.local_enums.keys() {
symbols.insert(name.clone(), symbol_name(&module.name, name));
}
for (name, binding) in imports {
if matches!(
binding.kind,
DeclKind::Function | DeclKind::Struct | DeclKind::Enum
) {
symbols.insert(name.clone(), symbol_name(&binding.provider, name));
}
}
symbols
}
fn workspace_symbol_map(
packages: &[PackageUnit],
package_index: usize,
module_index: usize,
imports: &HashMap<String, WorkspaceImportBinding>,
) -> HashMap<String, String> {
let package = &packages[package_index];
let module = &package.modules[module_index];
let mut symbols = HashMap::new();
for (name, decl) in &module.local_functions {
if decl.kind == DeclKind::Function {
symbols.insert(
name.clone(),
package_symbol_name(&package.manifest.name, &module.name, name),
);
}
}
for name in module.local_structs.keys() {
symbols.insert(
name.clone(),
package_symbol_name(&package.manifest.name, &module.name, name),
);
}
for name in module.local_enums.keys() {
symbols.insert(
name.clone(),
package_symbol_name(&package.manifest.name, &module.name, name),
);
}
for (name, binding) in imports {
if matches!(
binding.kind,
DeclKind::Function | DeclKind::Struct | DeclKind::Enum
) {
let provider_package = &packages[binding.provider_package];
let provider_module = &provider_package.modules[binding.provider_module];
symbols.insert(
name.clone(),
package_symbol_name(&provider_package.manifest.name, &provider_module.name, name),
);
}
}
symbols
}
fn rewrite_checked_program(program: &mut CheckedProgram, symbols: &HashMap<String, String>) {
for enum_decl in &mut program.enums {
if let Some(name) = symbols.get(&enum_decl.name) {
enum_decl.name = name.clone();
}
for variant in &mut enum_decl.variants {
if let Some(payload_ty) = &mut variant.payload_ty {
rewrite_type(payload_ty, symbols);
}
}
}
for struct_decl in &mut program.structs {
if let Some(name) = symbols.get(&struct_decl.name) {
struct_decl.name = name.clone();
}
for (_, ty) in &mut struct_decl.fields {
rewrite_type(ty, symbols);
}
}
for function in &mut program.functions {
if let Some(name) = symbols.get(&function.name) {
function.name = name.clone();
}
for (_, ty) in &mut function.params {
rewrite_type(ty, symbols);
}
rewrite_type(&mut function.return_type, symbols);
for expr in &mut function.body {
rewrite_expr(expr, symbols);
}
}
for test in &mut program.tests {
for expr in &mut test.body {
rewrite_expr(expr, symbols);
}
}
}
fn rewrite_expr(expr: &mut TExpr, symbols: &HashMap<String, String>) {
rewrite_type(&mut expr.ty, symbols);
match &mut expr.kind {
TExprKind::StructInit { name, fields } => {
if let Some(symbol) = symbols.get(name) {
*name = symbol.clone();
}
for (_, value) in fields {
rewrite_expr(value, symbols);
}
}
TExprKind::ArrayInit { elements } => {
for element in elements {
rewrite_expr(element, symbols);
}
}
TExprKind::OptionSome { value }
| TExprKind::ResultOk { value }
| TExprKind::ResultErr { value }
| TExprKind::OptionIsSome { value }
| TExprKind::OptionIsNone { value }
| TExprKind::OptionUnwrapSome { value }
| TExprKind::ResultIsOk { value, .. }
| TExprKind::ResultIsErr { value, .. }
| TExprKind::ResultUnwrapOk { value, .. }
| TExprKind::ResultUnwrapErr { value, .. }
| TExprKind::FieldAccess { value, .. } => rewrite_expr(value, symbols),
TExprKind::Index { array, index } => {
rewrite_expr(array, symbols);
rewrite_expr(index, symbols);
}
TExprKind::Local { initializer, .. } => rewrite_expr(initializer, symbols),
TExprKind::Set { expr, .. } => rewrite_expr(expr, symbols),
TExprKind::Binary { left, right, .. } => {
rewrite_expr(left, symbols);
rewrite_expr(right, symbols);
}
TExprKind::If {
condition,
then_expr,
else_expr,
} => {
rewrite_expr(condition, symbols);
rewrite_expr(then_expr, symbols);
rewrite_expr(else_expr, symbols);
}
TExprKind::Match { subject, arms } => {
rewrite_expr(subject, symbols);
for arm in arms {
rewrite_match_pattern(&mut arm.pattern, symbols);
for expr in &mut arm.body {
rewrite_expr(expr, symbols);
}
}
}
TExprKind::While { condition, body } => {
rewrite_expr(condition, symbols);
for expr in body {
rewrite_expr(expr, symbols);
}
}
TExprKind::Unsafe { body } => {
for expr in body {
rewrite_expr(expr, symbols);
}
}
TExprKind::Call { name, args } => {
if let Some(symbol) = symbols.get(name) {
*name = symbol.clone();
}
for arg in args {
rewrite_expr(arg, symbols);
}
}
TExprKind::EnumVariant {
enum_name, payload, ..
} => {
if let Some(symbol) = symbols.get(enum_name) {
*enum_name = symbol.clone();
}
if let Some(payload) = payload {
rewrite_expr(payload, symbols);
}
}
TExprKind::Int(_)
| TExprKind::Int64(_)
| TExprKind::UInt32(_)
| TExprKind::UInt64(_)
| TExprKind::Float(_)
| TExprKind::Bool(_)
| TExprKind::String(_)
| TExprKind::Var(_)
| TExprKind::OptionNone => {}
}
}
fn rewrite_match_pattern(pattern: &mut MatchPatternKind, symbols: &HashMap<String, String>) {
if let MatchPatternKind::EnumVariant { enum_name, .. } = pattern {
if let Some(symbol) = symbols.get(enum_name) {
*enum_name = symbol.clone();
}
}
}
fn rewrite_type(ty: &mut Type, symbols: &HashMap<String, String>) {
match ty {
Type::Named(name) => {
if let Some(symbol) = symbols.get(name) {
*name = symbol.clone();
}
}
Type::Array(inner, _)
| Type::Vec(inner)
| Type::Option(inner)
| Type::Ptr(inner)
| Type::Slice(inner) => rewrite_type(inner, symbols),
Type::Result(ok, err) => {
rewrite_type(ok, symbols);
rewrite_type(err, symbols);
}
Type::I32
| Type::I64
| Type::U32
| Type::U64
| Type::F64
| Type::Bool
| Type::Unit
| Type::String => {}
}
}
fn add_entry_wrapper(
manifest: &Manifest,
modules: &[ModuleUnit],
by_name: &HashMap<String, usize>,
combined: &mut CheckedProgram,
diagnostics: &mut Vec<Diagnostic>,
) {
let Some(entry_index) = by_name.get(&manifest.entry).copied() else {
return;
};
let entry_module = &modules[entry_index];
let Some(entry_function) = entry_module
.program
.functions
.iter()
.find(|function| function.name == "main")
else {
diagnostics.push(Diagnostic::new(
&entry_module.file,
"ProjectEntryMainMissing",
format!(
"entry module `{}` has no `main` function; build/run require `(fn main () -> i32 ...)`",
manifest.entry
),
));
return;
};
if !entry_function.params.is_empty() || entry_function.return_type != Type::I32 {
diagnostics.push(
Diagnostic::new(
&entry_module.file,
"ProjectEntryMainInvalidSignature",
format!(
"project entry `main` must have signature `(fn main () -> i32 ...)`; found {} parameter(s) and return `{}`",
entry_function.params.len(),
entry_function.return_type
),
)
.with_span(entry_function.span),
);
return;
}
let entry_symbol = symbol_name(&entry_module.name, "main");
combined.functions.push(CheckedFunction {
name: "main".to_string(),
params: Vec::new(),
return_type: Type::I32,
body: vec![TExpr {
ty: Type::I32,
span: entry_function.span,
kind: TExprKind::Call {
name: entry_symbol,
args: Vec::new(),
},
}],
span: entry_function.span,
file: entry_module.file.clone(),
});
}
fn select_workspace_build_entry(
workspace: &WorkspaceManifest,
packages: &[PackageUnit],
module_maps: &[HashMap<String, usize>],
) -> Result<usize, Diagnostic> {
if let Some(default_package) = &workspace.default_package {
let Some(package_index) = packages
.iter()
.position(|package| package.manifest.name == *default_package)
else {
return Err(Diagnostic::new(
&workspace.path.display().to_string(),
"WorkspaceDefaultPackageMissing",
format!(
"workspace default_package `{}` is not a workspace package",
default_package
),
));
};
let package = &packages[package_index];
if module_maps[package_index].contains_key(&package.manifest.entry) {
return Ok(package_index);
}
return Err(Diagnostic::new(
&package.manifest.path.display().to_string(),
"WorkspaceDefaultPackageEntryMissing",
format!(
"workspace default_package `{}` has no entry module `{}`; build/run require one selected package entry module",
default_package, package.manifest.entry
),
));
}
let candidates = packages
.iter()
.enumerate()
.filter(|(index, package)| module_maps[*index].contains_key(&package.manifest.entry))
.map(|(index, _)| index)
.collect::<Vec<_>>();
if candidates.len() == 1 {
Ok(candidates[0])
} else {
Err(Diagnostic::new(
&workspace.path.display().to_string(),
"WorkspaceBuildAmbiguousEntryPackage",
"workspace build requires exactly one package with its entry module or `[workspace] default_package = \"name\"`",
))
}
}
fn add_workspace_entry_wrapper(
packages: &[PackageUnit],
package_index: usize,
by_name: &HashMap<String, usize>,
combined: &mut CheckedProgram,
diagnostics: &mut Vec<Diagnostic>,
) {
let package = &packages[package_index];
let Some(entry_index) = by_name.get(&package.manifest.entry).copied() else {
return;
};
let entry_module = &package.modules[entry_index];
let Some(entry_function) = entry_module
.program
.functions
.iter()
.find(|function| function.name == "main")
else {
diagnostics.push(Diagnostic::new(
&entry_module.file,
"WorkspaceEntryMainMissing",
format!(
"entry module `{}` in package `{}` has no `main` function; build/run require `(fn main () -> i32 ...)`",
package.manifest.entry, package.manifest.name
),
));
return;
};
if !entry_function.params.is_empty() || entry_function.return_type != Type::I32 {
diagnostics.push(
Diagnostic::new(
&entry_module.file,
"WorkspaceEntryMainInvalidSignature",
format!(
"workspace entry `main` must have signature `(fn main () -> i32 ...)`; found {} parameter(s) and return `{}`",
entry_function.params.len(),
entry_function.return_type
),
)
.with_span(entry_function.span),
);
return;
}
let entry_symbol = package_symbol_name(&package.manifest.name, &entry_module.name, "main");
combined.functions.push(CheckedFunction {
name: "main".to_string(),
params: Vec::new(),
return_type: Type::I32,
body: vec![TExpr {
ty: Type::I32,
span: entry_function.span,
kind: TExprKind::Call {
name: entry_symbol,
args: Vec::new(),
},
}],
span: entry_function.span,
file: entry_module.file.clone(),
});
}
fn duplicate_name(file: &str, name: &str, span: Span, original: Span) -> Diagnostic {
Diagnostic::new(file, "DuplicateName", format!("duplicate name `{}`", name))
.with_span(span)
.related("original declaration", original)
}
fn is_reserved_intrinsic(name: &str) -> bool {
std_runtime::is_reserved_name(name) || crate::unsafe_ops::is_reserved_head(name)
}
fn symbol_name(module: &str, name: &str) -> String {
format!(
"__glagol_{}_{}",
symbol_component(module),
symbol_component(name)
)
}
fn package_symbol_name(package: &str, module: &str, name: &str) -> String {
format!(
"__glagol_{}_{}_{}",
symbol_component(package),
symbol_component(module),
symbol_component(name)
)
}
fn symbol_component(value: &str) -> String {
let mut out = String::new();
for byte in value.bytes() {
match byte {
b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_' => out.push(byte as char),
_ => out.push_str(&format!("_{:02x}", byte)),
}
}
out
}
fn head_ident(form: &SExpr) -> Option<&str> {
let items = expect_list(form)?;
items.first().and_then(expect_ident)
}
fn expect_list(form: &SExpr) -> Option<&[SExpr]> {
match &form.kind {
SExprKind::List(items) => Some(items),
_ => None,
}
}
fn expect_ident(form: &SExpr) -> Option<&str> {
match &form.kind {
SExprKind::Atom(Atom::Ident(name)) => Some(name),
_ => None,
}
}
fn module_name_span(form: &SExpr) -> Option<Span> {
expect_list(form)?.get(1).map(|expr| expr.span)
}