mod ast; mod check; mod diag; mod docgen; mod driver; mod formatter; mod lexer; mod llvm; mod lower; mod project; mod reserved; mod scaffold; mod sexpr; mod std_runtime; mod symbols; mod test_runner; mod token; mod types; mod unsafe_ops; use std::{ env, fs, io::{self, Write}, panic::{self, AssertUnwindSafe}, path::{Path, PathBuf}, process::{self, Command as ProcessCommand}, thread, }; const TEST_RUNNER_THREAD_STACK_SIZE: usize = 16 * 1024 * 1024; fn main() { let raw_args = env::args().collect::>(); let command_line = raw_args.join(" "); let args = match parse_args(&raw_args) { Ok(args) => args, Err(err) => exit_parse_error(err, &command_line), }; match args { Args::Help => { print_usage(); } Args::Version => { println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); } Args::Run(invocation) => run_invocation_guarded(invocation), } } fn run_invocation_guarded(invocation: Invocation) -> ! { let previous_hook = panic::take_hook(); panic::set_hook(Box::new(|_| {})); let run_invocation = invocation.clone(); let result = if invocation.mode == Mode::RunTests { let thread_invocation = run_invocation.clone(); match thread::Builder::new() .name("glagol-test-runner".to_string()) .stack_size(TEST_RUNNER_THREAD_STACK_SIZE) .spawn(move || { panic::catch_unwind(AssertUnwindSafe(move || { run_invocation_inner(thread_invocation); })) }) { Ok(handle) => match handle.join() { Ok(result) => result, Err(payload) => Err(payload), }, Err(_) => panic::catch_unwind(AssertUnwindSafe(move || { run_invocation_inner(run_invocation); })), } } else { panic::catch_unwind(AssertUnwindSafe(move || { run_invocation_inner(run_invocation); })) }; panic::set_hook(previous_hook); match result { Ok(()) => unreachable!("compiler invocation returned without exiting"), Err(payload) => { let detail = panic_payload_message(payload.as_ref()); let message = format!("internal compiler error: {}", detail); emit_message_diagnostic( &message, "InternalCompilerError", ExitCode::Internal, &invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, Some("report this as a compiler bug with the source file and command line"), ); } } } fn panic_payload_message(payload: &(dyn std::any::Any + Send)) -> &str { if let Some(message) = payload.downcast_ref::<&str>() { message } else if let Some(message) = payload.downcast_ref::() { message.as_str() } else { "non-string panic payload" } } fn run_invocation_inner(invocation: Invocation) -> ! { if invocation.mode == Mode::New { run_new(invocation); } if invocation.mode == Mode::Clean { run_clean(invocation); } if invocation.mode == Mode::Doc { run_doc(invocation); } if invocation.mode == Mode::Symbols && project::is_project_input(&invocation.path) { run_project_symbols(invocation); } if invocation.mode == Mode::Format && invocation.fmt_action != FmtAction::Stdout { run_fmt_action(invocation); } let project_capable_mode = matches!( invocation.mode, Mode::Check | Mode::Build | Mode::Run | Mode::Symbols ) || (invocation.mode == Mode::RunTests && invocation.manifest_mode_name == "test"); if project_capable_mode && project::is_project_input(&invocation.path) { match invocation.mode { Mode::Check => run_project_check(invocation), Mode::RunTests => run_project_test(invocation), Mode::Build => run_project_build(invocation), Mode::Run => run_project_run(invocation), Mode::Symbols => run_project_symbols(invocation), _ => unreachable!("project mode is selected only for check/test/build/run/symbols"), } } let source = match fs::read_to_string(&invocation.path) { Ok(source) => source, Err(err) => { let message = format!("cannot read `{}`: {}", invocation.path, err); emit_message_diagnostic( &message, "InputReadFailed", ExitCode::SourceFailure, &invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } }; match invocation.mode { Mode::Build => run_build(invocation, &source), Mode::Run => run_run(invocation, &source), mode => run_text_mode(invocation, mode, &source), } } fn run_project_check(invocation: Invocation) -> ! { match project::check_project(&invocation.path) { Ok(output) => { write_manifest_if_requested_with_project( &invocation, true, PrimaryOutput::NoOutput, None, None, Some(&output.artifact), ); process::exit(0); } Err(failure) => exit_project_failure(invocation, failure), } } fn run_project_test(invocation: Invocation) -> ! { let result = if invocation.test_list { project::list_tests(&invocation.path, invocation.test_filter.as_deref()) } else { project::run_tests(&invocation.path, invocation.test_filter.as_deref()) }; match result { Ok(success) => { let output = success.output; let primary_output = if let Some(output_path) = invocation.output_path.as_deref() { if let Err(err) = fs::write(output_path, &output) { let message = format!("cannot write `{}`: {}", output_path, err); emit_message_diagnostic( &message, "OutputWriteFailed", ExitCode::ArtifactFailure, &invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } PrimaryOutput::Path { kind: Mode::RunTests.output_kind(), path: output_path, } } else { print!("{}", output); PrimaryOutput::Stdout { kind: Mode::RunTests.output_kind(), text: &output, } }; write_manifest_if_requested_with_project( &invocation, true, primary_output, Some(test_summary_from_report(success.report)), None, Some(&success.artifact), ); process::exit(0); } Err(failure) => exit_project_failure(invocation, failure), } } fn run_project_build(invocation: Invocation) -> ! { let output = match project::compile_to_llvm(&invocation.path) { Ok(output) => output, Err(failure) => exit_project_failure(invocation, failure), }; run_build_from_llvm(invocation, output.text, Some(output.artifact), Vec::new()); } fn run_project_run(invocation: Invocation) -> ! { let output = match project::compile_to_llvm(&invocation.path) { Ok(output) => output, Err(failure) => exit_project_failure(invocation, failure), }; run_native_from_llvm(invocation, output.text, Some(output.artifact), Vec::new()); } fn run_project_symbols(invocation: Invocation) -> ! { let loaded = match project::load_project_sources_for_tools(&invocation.path) { Ok(loaded) => loaded, Err(failure) => exit_tool_failure(invocation, failure), }; let output = match symbols::render_project(&loaded) { Ok(output) => output, Err(diagnostics) => exit_tool_failure( invocation.clone(), project::ToolFailure { diagnostics, sources: loaded.sources.clone(), artifact: Some(loaded.artifact.clone()), }, ), }; finish_symbols_output(invocation, output, Some(&loaded.artifact)); } fn exit_project_failure(invocation: Invocation, failure: project::ProjectTestFailure) -> ! { let rendered = render_source_diagnostics_multi( &failure.diagnostics, &failure.sources, invocation.diagnostics, ); eprint!("{}", rendered.stderr); let test_summary = failure.report.map(test_summary_from_report); emit_filtered_test_summary_if_present(&invocation, test_summary.as_ref()); write_manifest_if_requested_with_project( &invocation, false, PrimaryOutput::Diagnostics { text: &rendered.machine_text, }, test_summary, None, failure.artifact.as_ref(), ); process::exit(ExitCode::SourceFailure.code()); } fn exit_tool_failure(invocation: Invocation, failure: project::ToolFailure) -> ! { let rendered = render_source_diagnostics_multi( &failure.diagnostics, &failure.sources, invocation.diagnostics, ); eprint!("{}", rendered.stderr); write_manifest_if_requested_with_project( &invocation, false, PrimaryOutput::Diagnostics { text: &rendered.machine_text, }, None, None, failure.artifact.as_ref(), ); process::exit(ExitCode::SourceFailure.code()); } fn run_text_mode(invocation: Invocation, mode: Mode, source: &str) -> ! { if mode == Mode::RunTests { run_test_mode(invocation, source); } if mode == Mode::Symbols { match symbols::render_file(&invocation.path, source) { Ok(output) => finish_symbols_output(invocation, output, None), Err(diagnostics) => { let rendered = render_source_diagnostics(&diagnostics, source, invocation.diagnostics); eprint!("{}", rendered.stderr); write_manifest_if_requested( &invocation, false, PrimaryOutput::Diagnostics { text: &rendered.machine_text, }, None, None, ); process::exit(ExitCode::SourceFailure.code()); } } } let foreign_imports = c_imports_for_manifest(&invocation.path, source); let result = match mode { Mode::EmitLlvm => driver::compile_to_llvm(&invocation.path, source), Mode::Check => driver::check_source(&invocation.path, source), Mode::Format => driver::format_source(&invocation.path, source), Mode::PrintTree => driver::print_parse_tree(&invocation.path, source), Mode::InspectLoweringSurface => driver::inspect_lowering_surface(&invocation.path, source), Mode::InspectLoweringChecked => driver::inspect_lowering_checked(&invocation.path, source), Mode::CheckTests => driver::check_tests(&invocation.path, source), Mode::Symbols => unreachable!("symbols mode is handled separately"), Mode::RunTests => unreachable!("test mode is handled separately"), Mode::Build => unreachable!("build is handled separately"), Mode::Run => unreachable!("run is handled separately"), Mode::New => unreachable!("new is handled separately"), Mode::Clean => unreachable!("clean is handled separately"), Mode::Doc => unreachable!("doc is handled separately"), }; match result { Ok(output) => { let primary_output = if mode == Mode::Check { PrimaryOutput::NoOutput } else if let Some(output_path) = invocation.output_path.as_deref() { if let Err(err) = fs::write(output_path, &output) { let message = format!("cannot write `{}`: {}", output_path, err); emit_message_diagnostic( &message, "OutputWriteFailed", ExitCode::ArtifactFailure, &invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } PrimaryOutput::Path { kind: mode.output_kind(), path: output_path, } } else { print!("{}", output); PrimaryOutput::Stdout { kind: mode.output_kind(), text: &output, } }; write_manifest_if_requested_with_foreign_imports( &invocation, true, primary_output, mode.test_summary(&output), None, &foreign_imports, None, ); process::exit(0); } Err(diagnostics) => { let rendered = render_source_diagnostics(&diagnostics, source, invocation.diagnostics); eprint!("{}", rendered.stderr); write_manifest_if_requested_with_foreign_imports( &invocation, false, PrimaryOutput::Diagnostics { text: &rendered.machine_text, }, None, None, &foreign_imports, None, ); process::exit(ExitCode::SourceFailure.code()); } } } fn finish_symbols_output( invocation: Invocation, output: String, project_artifact: Option<&project::ProjectArtifact>, ) -> ! { let primary_output = if let Some(output_path) = invocation.output_path.as_deref() { if let Err(err) = fs::write(output_path, &output) { let message = format!("cannot write `{}`: {}", output_path, err); emit_message_diagnostic( &message, "OutputWriteFailed", ExitCode::ArtifactFailure, &invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } PrimaryOutput::Path { kind: Mode::Symbols.output_kind(), path: output_path, } } else { print!("{}", output); PrimaryOutput::Stdout { kind: Mode::Symbols.output_kind(), text: &output, } }; write_manifest_if_requested_with_project( &invocation, true, primary_output, None, None, project_artifact, ); process::exit(0); } fn run_new(invocation: Invocation) -> ! { match scaffold::create_project( &invocation.path, invocation.project_name.as_deref(), invocation.project_template, ) { Ok(()) => { write_manifest_if_requested(&invocation, true, PrimaryOutput::NoOutput, None, None); process::exit(0); } Err(diagnostic) => { let rendered = render_source_diagnostics(&[diagnostic], "", invocation.diagnostics); eprint!("{}", rendered.stderr); write_manifest_if_requested( &invocation, false, PrimaryOutput::Diagnostics { text: &rendered.machine_text, }, None, None, ); process::exit(ExitCode::SourceFailure.code()); } } } fn run_clean(invocation: Invocation) -> ! { let build_dir = generated_build_dir(&invocation.path); if build_dir.exists() { if let Err(err) = fs::remove_dir_all(&build_dir) { let message = format!("cannot remove `{}`: {}", build_dir.display(), err); emit_message_diagnostic( &message, "CleanFailed", ExitCode::ArtifactFailure, &invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } } write_manifest_if_requested(&invocation, true, PrimaryOutput::NoOutput, None, None); process::exit(0); } fn run_doc(invocation: Invocation) -> ! { let Some(output_dir) = invocation.output_path.as_deref() else { emit_message_diagnostic( "`doc` requires `-o `", "UsageError", ExitCode::Usage, &invocation, PrimaryOutput::Diagnostics { text: "`doc` requires `-o `", }, None, ); }; match docgen::generate(&invocation.path, output_dir) { Ok(()) => { let index = Path::new(output_dir).join("index.md"); let index = index.display().to_string(); write_manifest_if_requested( &invocation, true, PrimaryOutput::Path { kind: Mode::Doc.output_kind(), path: &index, }, None, None, ); process::exit(0); } Err(failure) => { let rendered = render_source_diagnostics_multi( &failure.diagnostics, &failure.sources, invocation.diagnostics, ); eprint!("{}", rendered.stderr); write_manifest_if_requested_with_project( &invocation, false, PrimaryOutput::Diagnostics { text: &rendered.machine_text, }, None, None, failure.artifact.as_ref(), ); process::exit(ExitCode::SourceFailure.code()); } } } fn run_fmt_action(invocation: Invocation) -> ! { let result = if project::is_project_input(&invocation.path) { format_project(&invocation) } else { format_single_file(&invocation) }; match result { Ok(()) => { write_manifest_if_requested(&invocation, true, PrimaryOutput::NoOutput, None, None); process::exit(0); } Err(failure) => { let rendered = render_source_diagnostics_multi( &failure.diagnostics, &failure.sources, invocation.diagnostics, ); eprint!("{}", rendered.stderr); write_manifest_if_requested_with_project( &invocation, false, PrimaryOutput::Diagnostics { text: &rendered.machine_text, }, None, None, failure.artifact.as_ref(), ); process::exit(ExitCode::SourceFailure.code()); } } } fn format_single_file(invocation: &Invocation) -> Result<(), project::ToolFailure> { let source = fs::read_to_string(&invocation.path).map_err(|err| project::ToolFailure { diagnostics: vec![diag::Diagnostic::new( &invocation.path, "InputReadFailed", format!("cannot read `{}`: {}", invocation.path, err), )], sources: vec![], artifact: None, })?; let formatted = driver::format_source(&invocation.path, &source).map_err(|diagnostics| { project::ToolFailure { diagnostics, sources: vec![project::SourceFile { path: invocation.path.clone(), source: source.clone(), }], artifact: None, } })?; finish_formatted_source(invocation, &invocation.path, &source, &formatted, None) } fn format_project(invocation: &Invocation) -> Result<(), project::ToolFailure> { let loaded = project::load_project_sources_for_tools(&invocation.path)?; for source in &loaded.sources { let formatted = driver::format_source(&source.path, &source.source).map_err(|diagnostics| { project::ToolFailure { diagnostics, sources: loaded.sources.clone(), artifact: Some(loaded.artifact.clone()), } })?; finish_formatted_source( invocation, &source.path, &source.source, &formatted, Some((&loaded.sources, &loaded.artifact)), )?; } Ok(()) } fn finish_formatted_source( invocation: &Invocation, path: &str, original: &str, formatted: &str, project_context: Option<(&[project::SourceFile], &project::ProjectArtifact)>, ) -> Result<(), project::ToolFailure> { if original == formatted { return Ok(()); } match invocation.fmt_action { FmtAction::Check => { let sources = project_context .map(|(sources, _)| sources.to_vec()) .unwrap_or_else(|| { vec![project::SourceFile { path: path.to_string(), source: original.to_string(), }] }); Err(project::ToolFailure { diagnostics: vec![diag::Diagnostic::new( path, "FormatCheckFailed", format!("`{}` is not formatted", path), ) .hint("run `glagol fmt --write` to update canonical formatting")], sources, artifact: project_context.map(|(_, artifact)| artifact.clone()), }) } FmtAction::Write => fs::write(path, formatted).map_err(|err| project::ToolFailure { diagnostics: vec![diag::Diagnostic::new( path, "OutputWriteFailed", format!("cannot write `{}`: {}", path, err), )], sources: project_context .map(|(sources, _)| sources.to_vec()) .unwrap_or_default(), artifact: project_context.map(|(_, artifact)| artifact.clone()), }), FmtAction::Stdout => unreachable!("stdout formatting is handled by text mode"), } } fn run_test_mode(invocation: Invocation, source: &str) -> ! { let foreign_imports = c_imports_for_manifest(&invocation.path, source); let result = if invocation.test_list { driver::list_tests(&invocation.path, source, invocation.test_filter.as_deref()) } else { driver::run_tests(&invocation.path, source, invocation.test_filter.as_deref()) }; match result { Ok(result) => { let output = result.output; let primary_output = if let Some(output_path) = invocation.output_path.as_deref() { if let Err(err) = fs::write(output_path, &output) { let message = format!("cannot write `{}`: {}", output_path, err); emit_message_diagnostic( &message, "OutputWriteFailed", ExitCode::ArtifactFailure, &invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } PrimaryOutput::Path { kind: Mode::RunTests.output_kind(), path: output_path, } } else { print!("{}", output); PrimaryOutput::Stdout { kind: Mode::RunTests.output_kind(), text: &output, } }; write_manifest_if_requested_with_foreign_imports( &invocation, true, primary_output, Some(test_summary_from_report(result.report)), None, &foreign_imports, None, ); process::exit(0); } Err(failure) => { let rendered = render_source_diagnostics(&failure.diagnostics, source, invocation.diagnostics); eprint!("{}", rendered.stderr); let test_summary = failure.report.map(test_summary_from_report); emit_filtered_test_summary_if_present(&invocation, test_summary.as_ref()); write_manifest_if_requested_with_foreign_imports( &invocation, false, PrimaryOutput::Diagnostics { text: &rendered.machine_text, }, test_summary, None, &foreign_imports, None, ); process::exit(ExitCode::SourceFailure.code()); } } } fn run_build(invocation: Invocation, source: &str) -> ! { let foreign_imports = c_imports_for_manifest(&invocation.path, source); let llvm_ir = match driver::compile_to_llvm(&invocation.path, source) { Ok(output) => output, Err(diagnostics) => { let rendered = render_source_diagnostics(&diagnostics, source, invocation.diagnostics); eprint!("{}", rendered.stderr); write_manifest_if_requested_with_foreign_imports( &invocation, false, PrimaryOutput::Diagnostics { text: &rendered.machine_text, }, None, None, &foreign_imports, None, ); process::exit(ExitCode::SourceFailure.code()); } }; run_build_from_llvm(invocation, llvm_ir, None, foreign_imports); } fn run_run(invocation: Invocation, source: &str) -> ! { let foreign_imports = c_imports_for_manifest(&invocation.path, source); let llvm_ir = match driver::compile_to_llvm(&invocation.path, source) { Ok(output) => output, Err(diagnostics) => { let rendered = render_source_diagnostics(&diagnostics, source, invocation.diagnostics); eprint!("{}", rendered.stderr); write_manifest_if_requested_with_foreign_imports( &invocation, false, PrimaryOutput::Diagnostics { text: &rendered.machine_text, }, None, None, &foreign_imports, None, ); process::exit(ExitCode::SourceFailure.code()); } }; run_native_from_llvm(invocation, llvm_ir, None, foreign_imports); } fn run_build_from_llvm( invocation: Invocation, llvm_ir: String, project_artifact: Option, foreign_imports: Vec, ) -> ! { let Some(output_path) = invocation.output_path.as_deref() else { emit_message_diagnostic( "`build` requires `-o `", "UsageError", ExitCode::Usage, &invocation, PrimaryOutput::Diagnostics { text: "`build` requires `-o `", }, None, ); }; let output_path = PathBuf::from(output_path); let native = build_native_executable_or_exit(&invocation, llvm_ir, &output_path); let output_display = native.output_path.display().to_string(); write_manifest_if_requested_with_foreign_imports( &invocation, true, PrimaryOutput::Path { kind: "native-executable", path: &output_display, }, None, Some(BuildInfo { clang: &native.clang, runtime: &native.runtime, c_inputs: &invocation.link_c_paths, }), &foreign_imports, project_artifact.as_ref(), ); process::exit(0); } fn run_native_from_llvm( invocation: Invocation, llvm_ir: String, project_artifact: Option, foreign_imports: Vec, ) -> ! { let output_path = run_output_path(&invocation, project_artifact.as_ref()); if invocation.output_path.is_none() { if let Some(parent) = output_path.parent() { if let Err(err) = fs::create_dir_all(parent) { let message = format!("cannot create `{}`: {}", parent.display(), err); emit_message_diagnostic( &message, "OutputWriteFailed", ExitCode::ArtifactFailure, &invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } } } let native = build_native_executable_or_exit(&invocation, llvm_ir, &output_path); let run_output = match ProcessCommand::new(&native.output_path) .args(&invocation.run_args) .output() { Ok(output) => output, Err(err) => { let message = format!("cannot run `{}`: {}", native.output_path.display(), err); emit_message_diagnostic( &message, "RunFailed", ExitCode::ArtifactFailure, &invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } }; let _ = io::stdout().write_all(&run_output.stdout); let _ = io::stderr().write_all(&run_output.stderr); let stdout = String::from_utf8_lossy(&run_output.stdout).to_string(); write_manifest_if_requested_with_foreign_imports( &invocation, run_output.status.success(), PrimaryOutput::Stdout { kind: Mode::Run.output_kind(), text: &stdout, }, None, Some(BuildInfo { clang: &native.clang, runtime: &native.runtime, c_inputs: &invocation.link_c_paths, }), &foreign_imports, project_artifact.as_ref(), ); process::exit(run_output.status.code().unwrap_or(1)); } struct NativeBuild { output_path: PathBuf, clang: String, runtime: PathBuf, } fn build_native_executable_or_exit( invocation: &Invocation, llvm_ir: String, output_path: &Path, ) -> NativeBuild { if let Some(parent) = output_path.parent() { if !parent.as_os_str().is_empty() && !parent.is_dir() { let message = format!( "cannot write `{}`: parent directory does not exist", output_path.display() ); emit_message_diagnostic( &message, "OutputWriteFailed", ExitCode::ArtifactFailure, invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } } let runtime = runtime_path(); if !runtime.is_file() { let message = format!("cannot find runtime C input `{}`", runtime.display()); emit_message_diagnostic( &message, "ToolchainUnavailable", ExitCode::Toolchain, invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, Some("build from a Glagol checkout or install that includes runtime/runtime.c"), ); } for path in &invocation.link_c_paths { let c_path = Path::new(path); if !c_path.is_file() { let message = format!("cannot find C link input `{}`", path); emit_message_diagnostic( &message, "InputReadFailed", ExitCode::SourceFailure, invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, Some("pass an explicit local C source path after `--link-c`"), ); } } let output_dir = output_path .parent() .filter(|parent| !parent.as_os_str().is_empty()) .unwrap_or_else(|| Path::new(".")); let stem = output_path .file_name() .and_then(|name| name.to_str()) .unwrap_or("glagol-output"); let temp_llvm = output_dir.join(format!(".{}.{}.glagol.ll", stem, process::id())); let temp_binary = output_dir.join(format!( ".{}.{}.glagol{}", stem, process::id(), env::consts::EXE_SUFFIX )); if let Err(err) = fs::write(&temp_llvm, llvm_ir) { let message = format!("cannot write `{}`: {}", temp_llvm.display(), err); emit_message_diagnostic( &message, "OutputWriteFailed", ExitCode::ArtifactFailure, invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } let clang = env::var("GLAGOL_CLANG").unwrap_or_else(|_| "clang".to_string()); let mut clang_command = ProcessCommand::new(&clang); clang_command.arg("-O2").arg(&runtime).arg(&temp_llvm); for path in &invocation.link_c_paths { clang_command.arg(path); } let clang_output = clang_command.arg("-o").arg(&temp_binary).output(); let clang_output = match clang_output { Ok(output) => output, Err(err) if err.kind() == io::ErrorKind::NotFound => { let _ = fs::remove_file(&temp_llvm); let message = format!("cannot find Clang executable `{}`", clang); emit_message_diagnostic( &message, "ToolchainUnavailable", ExitCode::Toolchain, invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, Some("set GLAGOL_CLANG or add clang to PATH"), ); } Err(err) => { let _ = fs::remove_file(&temp_llvm); let message = format!("cannot run Clang executable `{}`: {}", clang, err); emit_message_diagnostic( &message, "ToolchainUnavailable", ExitCode::Toolchain, invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, Some("set GLAGOL_CLANG to a usable clang-compatible compiler"), ); } }; if !clang_output.status.success() { let _ = fs::remove_file(&temp_llvm); let _ = fs::remove_file(&temp_binary); let stderr = String::from_utf8_lossy(&clang_output.stderr); let message = if stderr.trim().is_empty() { format!("Clang failed with status {}", clang_output.status) } else { format!( "Clang failed with status {}: {}", clang_output.status, stderr.trim() ) }; emit_message_diagnostic( &message, "ToolchainUnavailable", ExitCode::Toolchain, invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } if let Err(err) = fs::rename(&temp_binary, output_path) { let _ = fs::remove_file(&temp_llvm); let _ = fs::remove_file(&temp_binary); let message = format!("cannot write `{}`: {}", output_path.display(), err); emit_message_diagnostic( &message, "OutputWriteFailed", ExitCode::ArtifactFailure, invocation, PrimaryOutput::Diagnostics { text: message.as_str(), }, None, ); } let _ = fs::remove_file(&temp_llvm); NativeBuild { output_path: output_path.to_path_buf(), clang, runtime, } } #[derive(Clone)] enum Args { Help, Version, Run(Invocation), } #[derive(Clone)] struct Invocation { mode: Mode, manifest_mode_name: String, path: String, output_path: Option, manifest_path: Option, diagnostics: DiagnosticFormat, command_line: String, fmt_action: FmtAction, project_name: Option, project_template: scaffold::ProjectTemplate, link_c_paths: Vec, test_filter: Option, test_list: bool, run_args: Vec, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum FmtAction { Stdout, Check, Write, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum DiagnosticFormat { TextAndSexpr, Json, } #[derive(Debug)] struct ParseError { message: String, manifest_path: Option, diagnostics: DiagnosticFormat, command_line: String, } fn parse_args(raw_args: &[String]) -> Result { let mut mode = None; let mut mode_spelling = None::; let mut path = None; let mut output_path = None; let mut manifest_path = None; let mut diagnostics = DiagnosticFormat::TextAndSexpr; let mut fmt_action = FmtAction::Stdout; let mut project_name = None; let mut project_template = scaffold::ProjectTemplate::Binary; let mut link_c_paths = Vec::new(); let mut test_filter = None; let mut test_list = false; let mut run_args = Vec::new(); let mut no_color = false; let command_line = raw_args.join(" "); let mut iter = raw_args .iter() .skip(1) .cloned() .collect::>() .into_iter(); while let Some(arg) = iter.next() { if arg == "--" { run_args.extend(iter); break; } match arg.as_str() { "-h" | "--help" => return Ok(Args::Help), "--version" => return Ok(Args::Version), "--json-diagnostics" => { if diagnostics == DiagnosticFormat::Json { return parse_error( "--json-diagnostics was provided more than once", manifest_path, diagnostics, command_line, ); } diagnostics = DiagnosticFormat::Json; } "--no-color" => no_color = true, "--emit=llvm" => set_mode( &mut mode, &mut mode_spelling, Mode::EmitLlvm, "--emit=llvm", &manifest_path, diagnostics, &command_line, )?, "--format" => set_mode( &mut mode, &mut mode_spelling, Mode::Format, "--format", &manifest_path, diagnostics, &command_line, )?, "--print-tree" => set_mode( &mut mode, &mut mode_spelling, Mode::PrintTree, "--print-tree", &manifest_path, diagnostics, &command_line, )?, "--inspect-lowering=surface" => set_mode( &mut mode, &mut mode_spelling, Mode::InspectLoweringSurface, "--inspect-lowering=surface", &manifest_path, diagnostics, &command_line, )?, "--inspect-lowering=checked" => set_mode( &mut mode, &mut mode_spelling, Mode::InspectLoweringChecked, "--inspect-lowering=checked", &manifest_path, diagnostics, &command_line, )?, "--check-tests" => set_mode( &mut mode, &mut mode_spelling, Mode::CheckTests, "--check-tests", &manifest_path, diagnostics, &command_line, )?, "--run-tests" => set_mode( &mut mode, &mut mode_spelling, Mode::RunTests, "--run-tests", &manifest_path, diagnostics, &command_line, )?, "-o" => { if output_path.is_some() { return parse_error( "output path was provided more than once", manifest_path, diagnostics, command_line, ); } output_path = Some(iter.next().ok_or_else(|| ParseError { message: "`-o` requires a following path".to_string(), manifest_path: manifest_path.clone(), diagnostics, command_line: command_line.clone(), })?); } "--check" => { if fmt_action != FmtAction::Stdout { return parse_error( "formatter action was provided more than once", manifest_path, diagnostics, command_line, ); } fmt_action = FmtAction::Check; } "--write" => { if fmt_action != FmtAction::Stdout { return parse_error( "formatter action was provided more than once", manifest_path, diagnostics, command_line, ); } fmt_action = FmtAction::Write; } "--name" => { if project_name.is_some() { return parse_error( "project name was provided more than once", manifest_path, diagnostics, command_line, ); } project_name = Some(iter.next().ok_or_else(|| ParseError { message: "`--name` requires a following project name".to_string(), manifest_path: manifest_path.clone(), diagnostics, command_line: command_line.clone(), })?); } "--template" => { let value = iter.next().ok_or_else(|| ParseError { message: "`--template` requires one of: binary, library, workspace".to_string(), manifest_path: manifest_path.clone(), diagnostics, command_line: command_line.clone(), })?; let Some(parsed) = scaffold::ProjectTemplate::parse(&value) else { return parse_error( format!( "unsupported project template `{}`; expected binary, library, or workspace", value ), manifest_path, diagnostics, command_line, ); }; project_template = parsed; } "--manifest" => { if manifest_path.is_some() { return parse_error( "manifest path was provided more than once", manifest_path, diagnostics, command_line, ); } manifest_path = Some(iter.next().ok_or_else(|| ParseError { message: "`--manifest` requires a following path".to_string(), manifest_path: manifest_path.clone(), diagnostics, command_line: command_line.clone(), })?); } "--link-c" => { link_c_paths.push(iter.next().ok_or_else(|| ParseError { message: "`--link-c` requires a following local C source path".to_string(), manifest_path: manifest_path.clone(), diagnostics, command_line: command_line.clone(), })?); } "--filter" => { if test_filter.is_some() { return parse_error( "`--filter` was provided more than once", manifest_path, diagnostics, command_line, ); } test_filter = Some(iter.next().ok_or_else(|| ParseError { message: "`--filter` requires a following substring".to_string(), manifest_path: manifest_path.clone(), diagnostics, command_line: command_line.clone(), })?); } "--list" => { if test_list { return parse_error( "`--list` was provided more than once", manifest_path, diagnostics, command_line, ); } test_list = true; } "check" | "fmt" | "test" | "build" | "run" | "clean" | "new" | "doc" | "symbols" if path.is_none() => { let next = match arg.as_str() { "check" => Mode::Check, "fmt" => Mode::Format, "test" => Mode::RunTests, "build" => Mode::Build, "run" => Mode::Run, "clean" => Mode::Clean, "new" => Mode::New, "doc" => Mode::Doc, "symbols" => Mode::Symbols, _ => unreachable!(), }; set_mode( &mut mode, &mut mode_spelling, next, arg.as_str(), &manifest_path, diagnostics, &command_line, )?; } _ if arg.starts_with("--emit=") => { return parse_error( format!("unsupported emit mode `{}`", arg), manifest_path, diagnostics, command_line, ); } _ if arg.starts_with('-') => { return parse_error( format!("unexpected argument `{}`", arg), manifest_path, diagnostics, command_line, ); } _ if path.is_none() => path = Some(arg), _ => { return parse_error( format!("unexpected argument `{}`", arg), manifest_path, diagnostics, command_line, ); } } } let _ = no_color; let mode = mode.unwrap_or(Mode::EmitLlvm); if path.is_none() { return parse_error( "missing source file", manifest_path, diagnostics, command_line, ); } if mode == Mode::Build && output_path.is_none() { return parse_error( "`build` requires `-o `", manifest_path, diagnostics, command_line, ); } if fmt_action != FmtAction::Stdout && mode != Mode::Format { return parse_error( "`--check` and `--write` are only supported with `fmt`", manifest_path, diagnostics, command_line, ); } if project_name.is_some() && mode != Mode::New { return parse_error( "`--name` is only supported with `new`", manifest_path, diagnostics, command_line, ); } if project_template != scaffold::ProjectTemplate::Binary && mode != Mode::New { return parse_error( "`--template` is only supported with `new`", manifest_path, diagnostics, command_line, ); } if !link_c_paths.is_empty() && !matches!(mode, Mode::Build | Mode::Run) { return parse_error( "`--link-c` is only supported with `build` and `run`", manifest_path, diagnostics, command_line, ); } if test_filter.is_some() && mode != Mode::RunTests { return parse_error( "`--filter` is only supported with `test` and `--run-tests`", manifest_path, diagnostics, command_line, ); } if test_list && mode != Mode::RunTests { return parse_error( "`--list` is only supported with `test` and `--run-tests`", manifest_path, diagnostics, command_line, ); } if !run_args.is_empty() && mode != Mode::Run { return parse_error( "`--` program arguments are only supported with `run`", manifest_path, diagnostics, command_line, ); } if mode == Mode::Clean && output_path.is_some() { return parse_error( "`clean` does not support `-o`", manifest_path, diagnostics, command_line, ); } if mode == Mode::Doc && output_path.is_none() { return parse_error( "`doc` requires `-o `", manifest_path, diagnostics, command_line, ); } if let (Some(output_path), Some(manifest_path)) = (&output_path, &manifest_path) { if same_output_path(output_path, manifest_path) { return parse_error( "output path and manifest path must be different", Some(manifest_path.clone()), diagnostics, command_line, ); } } Ok(Args::Run(Invocation { mode, manifest_mode_name: manifest_mode_name(mode, mode_spelling.as_deref()).to_string(), path: path.expect("checked above"), output_path, manifest_path, diagnostics, command_line, fmt_action, project_name, project_template, link_c_paths, test_filter, test_list, run_args, })) } fn set_mode( mode: &mut Option, mode_spelling: &mut Option, next: Mode, spelling: &str, manifest_path: &Option, diagnostics: DiagnosticFormat, command_line: &str, ) -> Result<(), ParseError> { if let Some(existing) = mode_spelling { return parse_error( format!( "mode flags are mutually exclusive: cannot combine `{}` and `{}`", existing, spelling ), manifest_path.clone(), diagnostics, command_line.to_string(), ); } *mode = Some(next); *mode_spelling = Some(spelling.to_string()); Ok(()) } fn parse_error( message: impl Into, manifest_path: Option, diagnostics: DiagnosticFormat, command_line: String, ) -> Result { Err(ParseError { message: message.into(), manifest_path, diagnostics, command_line, }) } fn exit_parse_error(err: ParseError, command_line: &str) -> ! { let stderr = if err.diagnostics == DiagnosticFormat::Json { let json = diag::render_json_message("error", "UsageError", &err.message, None); eprintln!("{}", json); json } else { eprintln!("error[UsageError]: {}", err.message); print_usage(); format!("error[UsageError]: {}", err.message) }; if let Some(manifest_path) = err.manifest_path.as_deref() { let invocation = Invocation { mode: Mode::EmitLlvm, manifest_mode_name: "usage-error".to_string(), path: String::new(), output_path: None, manifest_path: Some(manifest_path.to_string()), diagnostics: err.diagnostics, command_line: if err.command_line.is_empty() { command_line.to_string() } else { err.command_line }, fmt_action: FmtAction::Stdout, project_name: None, project_template: scaffold::ProjectTemplate::Binary, link_c_paths: Vec::new(), test_filter: None, test_list: false, run_args: Vec::new(), }; write_manifest_or_exit( manifest_path, &render_manifest( None, &invocation.command_line, None, false, PrimaryOutput::Diagnostics { text: &stderr }, None, None, &[], None, err.diagnostics, ), ); } process::exit(ExitCode::Usage.code()); } #[derive(Copy, Clone, PartialEq, Eq)] enum Mode { EmitLlvm, Check, Format, PrintTree, InspectLoweringSurface, InspectLoweringChecked, CheckTests, RunTests, Build, Run, Clean, New, Doc, Symbols, } impl Mode { fn manifest_name(self) -> &'static str { match self { Self::EmitLlvm => "emit-llvm", Self::Check => "check", Self::Format => "format", Self::PrintTree => "print-tree", Self::InspectLoweringSurface => "inspect-lowering-surface", Self::InspectLoweringChecked => "inspect-lowering-checked", Self::CheckTests => "check-tests", Self::RunTests => "test", Self::Build => "build", Self::Run => "run", Self::Clean => "clean", Self::New => "new", Self::Doc => "doc", Self::Symbols => "symbols", } } fn output_kind(self) -> &'static str { match self { Self::EmitLlvm => "llvm-ir", Self::Check => "no-output", Self::Format => "formatted-source", Self::PrintTree => "parse-tree", Self::InspectLoweringSurface | Self::InspectLoweringChecked => "lowering-inspector", Self::CheckTests | Self::RunTests => "stdout", Self::Build => "native-executable", Self::Run => "program-stdout", Self::Clean => "no-output", Self::New => "no-output", Self::Doc => "documentation", Self::Symbols => "symbols", } } fn test_summary(self, output: &str) -> Option { match self { Self::CheckTests => { parse_test_count(output, " test(s) checked").map(|total| TestSummary { total_discovered: total, selected: total, passed: 0, failed: 0, skipped: total, filter: None, }) } Self::RunTests => { parse_test_count(output, " test(s) passed").map(|total| TestSummary { total_discovered: total, selected: total, passed: total, failed: 0, skipped: 0, filter: None, }) } _ => None, } } } fn manifest_mode_name(mode: Mode, spelling: Option<&str>) -> &'static str { match (mode, spelling) { (Mode::RunTests, Some("--run-tests")) => "run-tests", _ => mode.manifest_name(), } } #[derive(Copy, Clone)] enum ExitCode { SourceFailure, Usage, Toolchain, Internal, ArtifactFailure, } impl ExitCode { fn code(self) -> i32 { match self { Self::SourceFailure => 1, Self::Usage => 2, Self::Toolchain => 3, Self::Internal => 4, Self::ArtifactFailure => 5, } } } struct RenderedDiagnostics { stderr: String, machine_text: String, } fn render_source_diagnostics( diagnostics: &[diag::Diagnostic], source: &str, format: DiagnosticFormat, ) -> RenderedDiagnostics { let mut stderr = String::new(); let mut machine = Vec::new(); for diagnostic in diagnostics { match format { DiagnosticFormat::TextAndSexpr => { stderr.push_str(&diagnostic.render_human(source)); stderr.push('\n'); let rendered = diagnostic.render_machine(source); stderr.push_str(&rendered); stderr.push('\n'); machine.push(rendered); } DiagnosticFormat::Json => { let rendered = diagnostic.render_json(source); stderr.push_str(&rendered); stderr.push('\n'); machine.push(rendered); } } } RenderedDiagnostics { stderr, machine_text: machine.join("\n"), } } fn render_source_diagnostics_multi( diagnostics: &[diag::Diagnostic], sources: &[project::SourceFile], format: DiagnosticFormat, ) -> RenderedDiagnostics { let mut stderr = String::new(); let mut machine = Vec::new(); for diagnostic in diagnostics { match format { DiagnosticFormat::TextAndSexpr => { stderr.push_str(&diagnostic.render_human_with_sources(|file| { sources .iter() .find(|source| source.path == file) .map(|source| source.source.as_str()) })); stderr.push('\n'); let rendered = diagnostic.render_machine_with_sources(|file| { sources .iter() .find(|source| source.path == file) .map(|source| source.source.as_str()) }); stderr.push_str(&rendered); stderr.push('\n'); machine.push(rendered); } DiagnosticFormat::Json => { let rendered = diagnostic.render_json_with_sources(|file| { sources .iter() .find(|source| source.path == file) .map(|source| source.source.as_str()) }); stderr.push_str(&rendered); stderr.push('\n'); machine.push(rendered); } } } RenderedDiagnostics { stderr, machine_text: machine.join("\n"), } } fn emit_message_diagnostic( message: &str, code: &str, exit_code: ExitCode, invocation: &Invocation, _primary_output: PrimaryOutput<'_>, hint: Option<&str>, ) -> ! { let rendered = if invocation.diagnostics == DiagnosticFormat::Json { diag::render_json_message("error", code, message, hint) } else { let mut text = format!("error[{}]: {}", code, message); if let Some(hint) = hint { text.push_str("\nhint: "); text.push_str(hint); } text }; eprintln!("{}", rendered); write_manifest_if_requested( invocation, false, PrimaryOutput::Diagnostics { text: &rendered }, None, None, ); process::exit(exit_code.code()); } #[derive(Copy, Clone)] enum PrimaryOutput<'a> { NoOutput, Path { kind: &'static str, path: &'a str }, Stdout { kind: &'static str, text: &'a str }, Diagnostics { text: &'a str }, } struct TestSummary { total_discovered: usize, selected: usize, passed: usize, failed: usize, skipped: usize, filter: Option, } fn test_summary_from_report(report: test_runner::TestReport) -> TestSummary { TestSummary { total_discovered: report.total_discovered, selected: report.selected, passed: report.passed, failed: report.failed, skipped: report.skipped, filter: report.filter, } } fn emit_filtered_test_summary_if_present(invocation: &Invocation, summary: Option<&TestSummary>) { let Some(summary) = summary else { return; }; if summary.filter.is_none() { return; } let message = test_summary_line(summary); if invocation.diagnostics == DiagnosticFormat::Json { eprintln!( "{}", diag::render_json_message("note", "TestRunSummary", &message, None) ); } else { eprintln!("{}", message); } } fn test_summary_line(summary: &TestSummary) -> String { let mut line = format!( "test summary: total_discovered {}, selected {}, passed {}, failed {}, skipped {}", summary.total_discovered, summary.selected, summary.passed, summary.failed, summary.skipped ); if let Some(filter) = summary.filter.as_deref() { line.push_str(", filter "); line.push_str(&diag::render_string(filter)); } line } struct BuildInfo<'a> { clang: &'a str, runtime: &'a Path, c_inputs: &'a [String], } fn write_manifest_if_requested( invocation: &Invocation, success: bool, primary_output: PrimaryOutput<'_>, test_summary: Option, build_info: Option>, ) { write_manifest_if_requested_with_foreign_imports( invocation, success, primary_output, test_summary, build_info, &[], None, ); } fn write_manifest_if_requested_with_project( invocation: &Invocation, success: bool, primary_output: PrimaryOutput<'_>, test_summary: Option, build_info: Option>, project_info: Option<&project::ProjectArtifact>, ) { write_manifest_if_requested_with_foreign_imports( invocation, success, primary_output, test_summary, build_info, &[], project_info, ); } fn write_manifest_if_requested_with_foreign_imports( invocation: &Invocation, success: bool, primary_output: PrimaryOutput<'_>, test_summary: Option, build_info: Option>, foreign_imports: &[project::ProjectArtifactCImport], project_info: Option<&project::ProjectArtifact>, ) { if let Some(manifest_path) = invocation.manifest_path.as_deref() { let manifest = render_manifest( Some(&invocation.path), &invocation.command_line, Some(invocation.manifest_mode_name.as_str()), success, primary_output, test_summary, build_info, foreign_imports, project_info, invocation.diagnostics, ); write_manifest_or_exit(manifest_path, &manifest); } } fn write_manifest_or_exit(path: &str, manifest: &str) { if let Err(err) = fs::write(path, manifest) { eprintln!("cannot write manifest `{}`: {}", path, err); process::exit(ExitCode::ArtifactFailure.code()); } } fn render_manifest( source: Option<&str>, command: &str, mode: Option<&str>, success: bool, primary_output: PrimaryOutput<'_>, test_summary: Option, build_info: Option>, foreign_imports: &[project::ProjectArtifactCImport], project_info: Option<&project::ProjectArtifact>, diagnostics: DiagnosticFormat, ) -> String { let mut out = String::new(); out.push_str("(artifact-manifest\n"); out.push_str(" (schema slovo.artifact-manifest)\n"); out.push_str(" (version 1)\n"); match source { Some(source) => out.push_str(&format!(" (source {})\n", diag::render_string(source))), None => out.push_str(" (source null)\n"), } out.push_str(&format!(" (command {})\n", diag::render_string(command))); if let Some(mode) = mode { out.push_str(&format!(" (mode {})\n", mode)); } else { out.push_str(" (mode usage-error)\n"); } out.push_str(&format!( " (success {})\n", if success { "true" } else { "false" } )); out.push_str(&format!( " (diagnostics-schema-version {})\n", diag::DIAGNOSTIC_SCHEMA_VERSION )); out.push_str(&format!( " (diagnostics-encoding {})\n", match diagnostics { DiagnosticFormat::TextAndSexpr => "sexpr", DiagnosticFormat::Json => "json", } )); let project_diagnostics_count = diagnostics_count(&primary_output); let project_build_output = match &primary_output { PrimaryOutput::Path { kind: "native-executable", path, } => Some(*path), _ => None, }; match primary_output { PrimaryOutput::NoOutput => { out.push_str(" (primary-output\n"); out.push_str(" (kind no-output)\n"); out.push_str(" )\n"); out.push_str(" (artifacts)"); } PrimaryOutput::Path { kind, path } => { out.push_str(" (primary-output\n"); out.push_str(&format!(" (kind {})\n", kind)); out.push_str(&format!(" (path {})\n", diag::render_string(path))); out.push_str(" )\n"); out.push_str(" (artifacts\n"); out.push_str(" (artifact\n"); out.push_str(&format!(" (kind {})\n", kind)); out.push_str(&format!(" (path {})\n", diag::render_string(path))); out.push_str(" )\n"); out.push_str(" )"); } PrimaryOutput::Stdout { kind, text } => { out.push_str(" (primary-output\n"); out.push_str(&format!(" (kind {})\n", kind)); out.push_str(&format!(" (stdout {})\n", diag::render_string(text))); out.push_str(" )\n"); out.push_str(" (artifacts)"); } PrimaryOutput::Diagnostics { text } => { out.push_str(" (primary-output\n"); out.push_str(" (kind diagnostics)\n"); out.push_str(&format!(" (stderr {})\n", diag::render_string(text))); out.push_str(" )\n"); out.push_str(" (artifacts\n"); out.push_str(" (artifact\n"); out.push_str(" (kind diagnostics)\n"); out.push_str(" (stream stderr)\n"); out.push_str(" )\n"); out.push_str(" )"); } } if let Some(summary) = test_summary { out.push('\n'); out.push_str(" (test-report\n"); out.push_str(&format!(" (total {})\n", summary.total_discovered)); out.push_str(&format!( " (total_discovered {})\n", summary.total_discovered )); out.push_str(&format!(" (selected {})\n", summary.selected)); out.push_str(&format!(" (passed {})\n", summary.passed)); out.push_str(&format!(" (failed {})\n", summary.failed)); out.push_str(&format!(" (skipped {})\n", summary.skipped)); if let Some(filter) = summary.filter.as_deref() { out.push_str(&format!(" (filter {})\n", diag::render_string(filter))); } out.push_str(" )"); } if let Some(build) = build_info { out.push('\n'); out.push_str(" (hosted-build\n"); out.push_str(&format!( " (clang {})\n", diag::render_string(build.clang) )); out.push_str(&format!( " (runtime {})\n", diag::render_string(&build.runtime.display().to_string()) )); out.push_str(" (c_link_inputs"); if build.c_inputs.is_empty() { out.push_str(")\n"); } else { out.push('\n'); for input in build.c_inputs { out.push_str(&format!(" (input {})\n", diag::render_string(input))); } out.push_str(" )\n"); } out.push_str(" )"); } if !foreign_imports.is_empty() { out.push('\n'); render_c_imports(" ", foreign_imports, &mut out); } if let Some(project) = project_info { out.push('\n'); out.push_str(" (project\n"); out.push_str(&format!( " (project_manifest {})\n", diag::render_string(&project.manifest_path) )); out.push_str(&format!( " (project_root {})\n", diag::render_string(&project.project_root) )); out.push_str(&format!( " (source_root {})\n", diag::render_string(&project.source_root) )); out.push_str(&format!( " (project_name {})\n", diag::render_string(&project.project_name) )); out.push_str(&format!( " (entry_module {})\n", diag::render_string(&project.entry) )); if let Some(workspace) = &project.workspace { out.push_str(" (workspace\n"); out.push_str(&format!( " (workspace_root {})\n", diag::render_string(&workspace.workspace_root) )); out.push_str(&format!( " (workspace_manifest {})\n", diag::render_string(&workspace.workspace_manifest) )); out.push_str(" (members"); if workspace.members.is_empty() { out.push_str(")\n"); } else { out.push('\n'); for member in &workspace.members { out.push_str(&format!( " (member {})\n", diag::render_string(member) )); } out.push_str(" )\n"); } out.push_str(" (packages"); if workspace.packages.is_empty() { out.push_str(")\n"); } else { out.push('\n'); for package in &workspace.packages { out.push_str(" (package\n"); out.push_str(&format!( " (name {})\n", diag::render_string(&package.name) )); out.push_str(&format!( " (version {})\n", diag::render_string(&package.version) )); out.push_str(&format!( " (root {})\n", diag::render_string(&package.root) )); out.push_str(&format!( " (manifest {})\n", diag::render_string(&package.manifest) )); out.push_str(&format!( " (source_root {})\n", diag::render_string(&package.source_root) )); out.push_str(&format!( " (entry {})\n", diag::render_string(&package.entry) )); out.push_str(&format!(" (test_count {})\n", package.test_count)); out.push_str(" (modules"); if package.modules.is_empty() { out.push_str(")\n"); } else { out.push('\n'); for module in &package.modules { out.push_str(" (module\n"); out.push_str(&format!( " (name {})\n", diag::render_string(&module.name) )); out.push_str(&format!( " (path {})\n", diag::render_string(&module.path) )); out.push_str(" (imports"); if module.imports.is_empty() { out.push_str(")\n"); } else { out.push('\n'); for import in &module.imports { out.push_str(&format!( " (import {})\n", diag::render_string(import) )); } out.push_str(" )\n"); } if !module.c_imports.is_empty() { render_c_imports(" ", &module.c_imports, &mut out); } out.push_str(" )\n"); } out.push_str(" )\n"); } out.push_str(" )\n"); } out.push_str(" )\n"); } out.push_str(" (package_dependency_edges"); if workspace.dependencies.is_empty() { out.push_str(")\n"); } else { out.push('\n'); for dependency in &workspace.dependencies { out.push_str(" (package_dependency\n"); out.push_str(&format!( " (from {})\n", diag::render_string(&dependency.from) )); out.push_str(&format!( " (to {})\n", diag::render_string(&dependency.to) )); out.push_str(&format!( " (kind {})\n", diag::render_string(&dependency.kind) )); out.push_str(&format!( " (path {})\n", diag::render_string(&dependency.path) )); out.push_str(" )\n"); } out.push_str(" )\n"); } match &workspace.selected_build_entry_package { Some(package) => out.push_str(&format!( " (selected_build_entry_package {})\n", diag::render_string(package) )), None => out.push_str(" (selected_build_entry_package)\n"), } out.push_str(" )\n"); } out.push_str(" (modules\n"); for module in &project.modules { out.push_str(" (module\n"); out.push_str(&format!( " (name {})\n", diag::render_string(&module.name) )); out.push_str(&format!( " (path {})\n", diag::render_string(&module.path) )); out.push_str(" (imports"); if module.imports.is_empty() { out.push(')'); } else { out.push('\n'); for import in &module.imports { out.push_str(&format!( " (import {})\n", diag::render_string(import) )); } out.push_str(" )"); } out.push('\n'); if !module.c_imports.is_empty() { render_c_imports(" ", &module.c_imports, &mut out); } out.push_str(" )\n"); } out.push_str(" )\n"); out.push_str(" (import_edges"); let mut has_import_edges = false; for module in &project.modules { for import in &module.imports { if !has_import_edges { out.push('\n'); has_import_edges = true; } out.push_str(" (import_edge\n"); out.push_str(&format!( " (from {})\n", diag::render_string(&module.name) )); out.push_str(&format!(" (to {})\n", diag::render_string(import))); out.push_str(" )\n"); } } if has_import_edges { out.push_str(" )\n"); } else { out.push_str(")\n"); } out.push_str(&format!( " (diagnostics_count {})\n", project_diagnostics_count )); if project_diagnostics_count > 0 { out.push_str(" (diagnostic_artifacts\n"); out.push_str(" (artifact\n"); out.push_str(" (kind diagnostics)\n"); out.push_str(" (stream stderr)\n"); out.push_str(" )\n"); out.push_str(" )\n"); } else { out.push_str(" (diagnostic_artifacts)\n"); } if let Some(path) = project_build_output { out.push_str(" (build_outputs\n"); out.push_str(&format!(" (output {})\n", diag::render_string(path))); out.push_str(" )\n"); } else { out.push_str(" (build_outputs)\n"); } out.push_str(" )"); } out.push_str("\n)\n"); out } fn diagnostics_count(primary_output: &PrimaryOutput<'_>) -> usize { match primary_output { PrimaryOutput::Diagnostics { text } => { let sexpr_count = text.matches("(diagnostic\n").count(); if sexpr_count > 0 { sexpr_count } else { text.lines().filter(|line| !line.trim().is_empty()).count() } } _ => 0, } } fn parse_test_count(output: &str, suffix: &str) -> Option { output .lines() .rev() .find_map(|line| line.strip_suffix(suffix)?.parse().ok()) } fn runtime_path() -> PathBuf { runtime_path_candidates() .into_iter() .find(|path| path.is_file()) .unwrap_or_else(checkout_runtime_path) } fn runtime_path_candidates() -> Vec { let mut candidates = Vec::new(); if let Some(path) = env::var_os("SLOVO_RUNTIME_C") { candidates.push(PathBuf::from(path)); } if let Some(path) = env::var_os("GLAGOL_RUNTIME_C") { candidates.push(PathBuf::from(path)); } if let Ok(exe) = env::current_exe() { if let Some(bin_dir) = exe.parent() { candidates.push(bin_dir.join("runtime/runtime.c")); candidates.push(bin_dir.join("../runtime/runtime.c")); candidates.push(bin_dir.join("../share/slovo/runtime/runtime.c")); } } candidates.push(checkout_runtime_path()); candidates } fn checkout_runtime_path() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")).join("../runtime/runtime.c") } fn run_output_path( invocation: &Invocation, project_artifact: Option<&project::ProjectArtifact>, ) -> PathBuf { if let Some(output_path) = invocation.output_path.as_deref() { return PathBuf::from(output_path); } let stem = project_artifact .map(|artifact| artifact.project_name.as_str()) .or_else(|| { Path::new(&invocation.path) .file_stem() .and_then(|stem| stem.to_str()) }) .map(sanitize_output_stem) .filter(|stem| !stem.is_empty()) .unwrap_or_else(|| "slovo-program".to_string()); generated_build_dir(&invocation.path).join(format!("{}{}", stem, env::consts::EXE_SUFFIX)) } fn generated_build_dir(input: &str) -> PathBuf { generated_build_root(input).join(".slovo").join("build") } fn generated_build_root(input: &str) -> PathBuf { let path = Path::new(input); if project::is_project_input(input) { if path.is_dir() { return path.to_path_buf(); } return path .parent() .filter(|parent| !parent.as_os_str().is_empty()) .unwrap_or_else(|| Path::new(".")) .to_path_buf(); } path.parent() .filter(|parent| !parent.as_os_str().is_empty()) .unwrap_or_else(|| Path::new(".")) .to_path_buf() } fn sanitize_output_stem(value: &str) -> String { let mut out = String::new(); let mut previous_dash = false; for ch in value.chars().flat_map(char::to_lowercase) { let mapped = if ch.is_ascii_lowercase() || ch.is_ascii_digit() { Some(ch) } else if ch == '-' || ch == '_' || ch == '.' || ch.is_whitespace() { Some('-') } else { None }; let Some(ch) = mapped else { continue; }; if ch == '-' { if !out.is_empty() && !previous_dash { out.push('-'); previous_dash = true; } } else { out.push(ch); previous_dash = false; } } while out.ends_with('-') { out.pop(); } out } fn c_imports_for_manifest(file: &str, source: &str) -> Vec { let Ok(tokens) = lexer::lex(file, source) else { return Vec::new(); }; let Ok(forms) = sexpr::parse(file, &tokens) else { return Vec::new(); }; let Ok(program) = lower::lower_program(file, &forms) else { return Vec::new(); }; program .c_imports .iter() .map(|import| project::project_artifact_c_import(&program.module, import)) .collect() } fn render_c_imports(indent: &str, imports: &[project::ProjectArtifactCImport], out: &mut String) { out.push_str(indent); out.push_str("(foreign_imports"); if imports.is_empty() { out.push_str(")\n"); return; } out.push('\n'); for import in imports { out.push_str(indent); out.push_str(" (foreign_import\n"); out.push_str(&format!( "{} (name {})\n", indent, diag::render_string(&import.name) )); out.push_str(&format!( "{} (source_module {})\n", indent, diag::render_string(&import.source_module) )); out.push_str(&format!( "{} (symbol {})\n", indent, diag::render_string(&import.symbol) )); out.push_str(indent); out.push_str(" (params"); if import.params.is_empty() { out.push_str(")\n"); } else { out.push('\n'); for param in &import.params { out.push_str(&format!( "{} (param {})\n", indent, diag::render_string(param) )); } out.push_str(indent); out.push_str(" )\n"); } out.push_str(&format!( "{} (return {})\n", indent, diag::render_string(&import.return_type) )); out.push_str(&format!( "{} (abi {})\n", indent, diag::render_string(&import.abi) )); out.push_str(indent); out.push_str(" )\n"); } out.push_str(indent); out.push_str(")\n"); } fn same_output_path(left: &str, right: &str) -> bool { match (normalized_output_path(left), normalized_output_path(right)) { (Some(left), Some(right)) => left == right, _ => left == right, } } fn normalized_output_path(path: &str) -> Option { let path = Path::new(path); if let Ok(canonical) = fs::canonicalize(path) { return Some(canonical); } let parent = path .parent() .filter(|parent| !parent.as_os_str().is_empty()) .unwrap_or_else(|| Path::new(".")); let mut normalized = fs::canonicalize(parent).ok()?; normalized.push(path.file_name()?); Some(normalized) } fn print_usage() { eprintln!( "usage: glagol [check|fmt|test|build|run|clean|symbols] [--json-diagnostics] [--no-color] [--manifest ] [--link-c ] [-o ] [--filter ] [--list] [-- ...]\n glagol fmt [--check|--write] \n glagol new [--name ] [--template binary|library|workspace]\n glagol doc -o \n glagol symbols \n glagol [--emit=llvm|--format|--print-tree|--inspect-lowering=surface|--inspect-lowering=checked|--check-tests|--run-tests] [--json-diagnostics] [--no-color] [-o ] [--manifest ] [--filter ] [--list] \n glagol --version" ); }