slovo/compiler/src/main.rs

2609 lines
84 KiB
Rust

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::<Vec<_>>();
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::<String>() {
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 <dir>`",
"UsageError",
ExitCode::Usage,
&invocation,
PrimaryOutput::Diagnostics {
text: "`doc` requires `-o <dir>`",
},
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<project::ProjectArtifact>,
foreign_imports: Vec<project::ProjectArtifactCImport>,
) -> ! {
let Some(output_path) = invocation.output_path.as_deref() else {
emit_message_diagnostic(
"`build` requires `-o <binary>`",
"UsageError",
ExitCode::Usage,
&invocation,
PrimaryOutput::Diagnostics {
text: "`build` requires `-o <binary>`",
},
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<project::ProjectArtifact>,
foreign_imports: Vec<project::ProjectArtifactCImport>,
) -> ! {
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<String>,
manifest_path: Option<String>,
diagnostics: DiagnosticFormat,
command_line: String,
fmt_action: FmtAction,
project_name: Option<String>,
project_template: scaffold::ProjectTemplate,
link_c_paths: Vec<String>,
test_filter: Option<String>,
test_list: bool,
run_args: Vec<String>,
}
#[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<String>,
diagnostics: DiagnosticFormat,
command_line: String,
}
fn parse_args(raw_args: &[String]) -> Result<Args, ParseError> {
let mut mode = None;
let mut mode_spelling = None::<String>;
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::<Vec<_>>()
.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 <binary>`",
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 <dir>`",
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>,
mode_spelling: &mut Option<String>,
next: Mode,
spelling: &str,
manifest_path: &Option<String>,
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<T>(
message: impl Into<String>,
manifest_path: Option<String>,
diagnostics: DiagnosticFormat,
command_line: String,
) -> Result<T, ParseError> {
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<TestSummary> {
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<String>,
}
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<TestSummary>,
build_info: Option<BuildInfo<'_>>,
) {
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<TestSummary>,
build_info: Option<BuildInfo<'_>>,
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<TestSummary>,
build_info: Option<BuildInfo<'_>>,
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<TestSummary>,
build_info: Option<BuildInfo<'_>>,
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<usize> {
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<PathBuf> {
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<project::ProjectArtifactCImport> {
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<PathBuf> {
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 <path>] [--link-c <path>] [-o <path>] [--filter <substring>] [--list] <file.slo|project> [-- <program-args>...]\n glagol fmt [--check|--write] <file.slo|project>\n glagol new <project-dir> [--name <name>] [--template binary|library|workspace]\n glagol doc <file.slo|project> -o <dir>\n glagol symbols <file.slo|project|workspace>\n glagol [--emit=llvm|--format|--print-tree|--inspect-lowering=surface|--inspect-lowering=checked|--check-tests|--run-tests] [--json-diagnostics] [--no-color] [-o <path>] [--manifest <path>] [--filter <substring>] [--list] <file.slo>\n glagol --version"
);
}