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, pub artifact: ProjectArtifact, } pub struct ProjectTestSuccess { pub output: String, pub report: test_runner::TestReport, pub sources: Vec, pub artifact: ProjectArtifact, } pub struct ProjectTestFailure { pub diagnostics: Vec, pub report: Option, pub sources: Vec, pub artifact: Option, } pub struct ToolProject { pub sources: Vec, pub artifact: ProjectArtifact, } pub struct ToolFailure { pub diagnostics: Vec, pub sources: Vec, pub artifact: Option, } #[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, pub workspace: Option, } #[derive(Debug, Clone)] pub struct ProjectArtifactModule { pub name: String, pub path: String, pub imports: Vec, pub c_imports: Vec, } #[derive(Debug, Clone)] pub struct ProjectArtifactCImport { pub source_module: String, pub name: String, pub symbol: String, pub params: Vec, pub return_type: String, pub abi: String, } #[derive(Debug, Clone)] pub struct WorkspaceArtifact { pub workspace_root: String, pub workspace_manifest: String, pub members: Vec, pub packages: Vec, pub dependencies: Vec, pub selected_build_entry_package: Option, } #[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, 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, default_package: Option, } #[derive(Clone)] struct PackageManifest { path: PathBuf, root: PathBuf, member: String, name: String, version: String, source_root: String, entry: String, dependencies: Vec, } #[derive(Clone)] struct PackageDependency { key: String, path: String, span: Span, } #[derive(Clone)] struct PackageUnit { manifest: PackageManifest, modules: Vec, dependency_indices: Vec, } #[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, imports: Vec, program: Program, local_functions: HashMap, local_structs: HashMap, local_enums: HashMap, local_aliases: HashMap, } #[derive(Debug, Clone)] struct ImportDecl { module: String, module_span: Span, names: Vec, } #[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 { 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 { 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 { 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 { 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 { 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, artifact: ProjectArtifact, } struct ProjectLoadFailure { diagnostics: Vec, sources: Vec, artifact: Option, } fn load_checked_project( input: &str, require_entry_wrapper: bool, ) -> Result { 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, sources: Vec, 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 { 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 { 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, ) -> Result { 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>>, selected_build_entry_package: Option, ) -> 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::>(); 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> { 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::; let mut source_root = None::; let mut entry = None::; 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> { 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::>; let mut default_package = None::; 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::::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> { 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::; let mut version = None::; let mut source_root = None::; let mut entry = None::; 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> { 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 { 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> { 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 { 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, slot: &mut Option, 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 { normalize_rel_path(value, false) } fn normalize_dependency_path( workspace: &WorkspaceManifest, package: &PackageManifest, value: &str, ) -> Result { 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 { 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 { 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> { 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, sources: &mut Vec, diagnostics: &mut Vec, ) { let mut loaded = modules .iter() .map(|module| module.name.clone()) .collect::>(); 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, sources: &mut Vec, diagnostics: &mut Vec, ) { let mut loaded = modules .iter() .map(|module| module.name.clone()) .collect::>(); 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, sources: &mut Vec, diagnostics: &mut Vec, ) { let mut loaded = modules .iter() .map(|module| module.name.clone()) .collect::>(); 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) -> VecDeque { imports .filter(|name| standard_module_leaf(name).is_some()) .cloned() .collect::>() .into_iter() .collect() } fn standard_module_path(project_root: &Path, import_name: &str) -> Option { 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 { 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 { 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> { 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, 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::>(); 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, SExpr), Vec> { 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), ); "".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> { 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), ); "".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 { let Ok(tokens) = lexer::lex("", source) else { return Vec::new(); }; let Ok(forms) = crate::sexpr::parse("", &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 { let Ok(tokens) = lexer::lex("", source) else { return Vec::new(); }; let Ok(forms) = crate::sexpr::parse("", &tokens) else { return Vec::new(); }; let Ok(program) = lower::lower_program("", &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 { 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 { 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, HashMap, HashMap, HashMap, Vec, ) { let mut all = HashMap::::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, ) { let mut by_member = HashMap::::new(); for (index, package) in packages.iter().enumerate() { by_member.insert(package.manifest.member.clone(), index); } let mut by_name = BTreeMap::>::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::::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::>(); 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) { let mut state = vec![0_u8; packages.len()]; let mut stack = Vec::new(); let mut indices = (0..packages.len()).collect::>(); 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::>() .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, ) -> 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 { 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::::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::>(); 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::>::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, sources: Vec, require_entry_wrapper: bool, ) -> Result { let mut diagnostics = Vec::new(); let mut by_name = HashMap::::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::::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, ) -> Vec> { 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, export_maps: &[HashMap], diagnostics: &mut Vec, ) -> Vec> { modules .iter() .map(|module| { let mut imports = HashMap::::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], export_maps: &[Vec>], diagnostics: &mut Vec, ) -> Vec>> { let mut package_by_name = HashMap::::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::::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], package_by_name: &HashMap, package_index: usize, module: &ModuleUnit, import: &ImportDecl, diagnostics: &mut Vec, ) -> 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 `.`", 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, diagnostics: &mut Vec, ) { 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::>() .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, state: &mut [u8], stack: &mut Vec, ) -> 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) -> Vec { let mut indegree = vec![0_usize; modules.len()]; let mut dependents = vec![Vec::::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::>(); 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 { let mut indegree = vec![0_usize; packages.len()]; let mut dependents = vec![Vec::::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::>(); 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, ) -> HashMap { 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, ) -> HashMap { 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) { 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) { 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) { 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) { 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, combined: &mut CheckedProgram, diagnostics: &mut Vec, ) { 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], ) -> Result { 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::>(); 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, combined: &mut CheckedProgram, diagnostics: &mut Vec, ) { 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 { expect_list(form)?.get(1).map(|expr| expr.span) }