4644 lines
156 KiB
Rust
4644 lines
156 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),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn list_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,
|
|
})?;
|
|
|
|
let success = test_runner::list(&checked.program, filter);
|
|
Ok(ProjectTestSuccess {
|
|
output: success.output,
|
|
report: success.report,
|
|
sources: checked.sources,
|
|
artifact: 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)
|
|
}
|