2609 lines
84 KiB
Rust
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"
|
|
);
|
|
}
|