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();
    let stderr = String::from_utf8_lossy(&run_output.stderr).to_string();
    let exit_status = run_output.status.code();
    write_manifest_if_requested_with_run_report(
        &invocation,
        run_output.status.success(),
        PrimaryOutput::Stdout {
            kind: Mode::Run.output_kind(),
            text: &stdout,
        },
        Some(BuildInfo {
            clang: &native.clang,
            runtime: &native.runtime,
            c_inputs: &invocation.link_c_paths,
        }),
        &foreign_imports,
        project_artifact.as_ref(),
        RunReport {
            exit_status,
            stdout: &stdout,
            stderr: &stderr,
            args: &invocation.run_args,
        },
    );
    process::exit(exit_status.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,
                &[],
                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>,
}

struct RunReport<'a> {
    exit_status: Option<i32>,
    stdout: &'a str,
    stderr: &'a str,
    args: &'a [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,
            None,
            build_info,
            foreign_imports,
            project_info,
            invocation.diagnostics,
        );
        write_manifest_or_exit(manifest_path, &manifest);
    }
}

fn write_manifest_if_requested_with_run_report(
    invocation: &Invocation,
    success: bool,
    primary_output: PrimaryOutput<'_>,
    build_info: Option<BuildInfo<'_>>,
    foreign_imports: &[project::ProjectArtifactCImport],
    project_info: Option<&project::ProjectArtifact>,
    run_report: RunReport<'_>,
) {
    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,
            None,
            Some(run_report),
            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>,
    run_report: Option<RunReport<'_>>,
    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(report) = run_report {
        out.push('\n');
        out.push_str("  (run-report\n");
        match report.exit_status {
            Some(status) => out.push_str(&format!("    (exit-status {})\n", status)),
            None => out.push_str("    (exit-status null)\n"),
        }
        out.push_str(&format!(
            "    (stdout {})\n",
            diag::render_string(report.stdout)
        ));
        out.push_str(&format!(
            "    (stderr {})\n",
            diag::render_string(report.stderr)
        ));
        out.push_str("    (args");
        if report.args.is_empty() {
            out.push_str(")\n");
        } else {
            out.push('\n');
            for arg in report.args {
                out.push_str(&format!("      (arg {})\n", diag::render_string(arg)));
            }
            out.push_str("    )\n");
        }
        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"
    );
}
