knave 0.14.22

A Lisp programming language for humans and agents that infers types and ownership and compiles to readable Rust
Documentation
mod source_loader;

use std::path::{Path, PathBuf};
use std::rc::Rc;

use anstream::ColorChoice;

use super::build_directory::BuildDirectory;
use super::checking::Checking;
use super::diagnostic_output::{DiagnosticOutput, RenderedDiagnostics};
use super::emission::Emission;
use super::entry_span::EntrySpan;
use super::exit_status::ExitStatus;
use super::expansion::Expansion;
use super::interruptible_formatting::InterruptibleFormatting;
use super::interruption::Interruption;
use super::located_program::LocatedProgram;
use super::program_file::ProgramFile;
use super::project_task::ProjectTask;
use super::rustfmt_time_limit::RustfmtTimeLimit;
use super::source_report::SourceReport;
use super::stage_work::StageWork;
use super::standard_error::StandardError;
use crate::build::{BuildError, BuildPlan, BuildProfile, CargoCompiler, CargoTarget, RustCompiler};
use crate::emit::EmittedCrate;
use crate::language::Language;
use crate::source::{SourceFile, SourceId, SourceMap};
use crate::types::TypedProgram;
pub use source_loader::SourceLoader;

pub struct BuildOptions {
    pub profile: BuildProfile,
    pub output: Option<PathBuf>,
}

pub struct BuildTarget<'a> {
    pub directory: PathBuf,
    pub crate_name: &'a str,
    pub output: &'a Path,
}

pub enum BuildRoute<'a> {
    Rustc(BuildTarget<'a>),
    Cargo(CargoTarget<'a>),
}

pub struct Terminal {
    sources: SourceMap,
    work: StageWork,
    diagnostics: Rc<dyn DiagnosticOutput>,
    interruption: Interruption,
}

impl Terminal {
    pub const BUILD_PARENT: &str = "target";
    pub const WORKING_DIRECTORY: &str = ".";

    pub fn new(standard_error: ColorChoice) -> Self {
        Self::reporting_to(Rc::new(RenderedDiagnostics::new(standard_error)))
    }

    pub fn scratch(&self) -> Self {
        Self::reporting_to(self.diagnostic_output())
    }

    pub fn reporting_to(diagnostics: Rc<dyn DiagnosticOutput>) -> Self {
        Self {
            sources: SourceMap::default(),
            work: StageWork::default(),
            diagnostics,
            interruption: Interruption::default(),
        }
    }

    pub fn stop_before_rustc_on(&mut self, interruption: Interruption) {
        self.interruption = interruption;
    }

    pub fn report_diagnostics_to(&mut self, diagnostics: Rc<dyn DiagnosticOutput>) {
        self.diagnostics = diagnostics;
    }

    pub fn diagnostic_output(&self) -> Rc<dyn DiagnosticOutput> {
        Rc::clone(&self.diagnostics)
    }

    pub fn work(&self) -> &StageWork {
        &self.work
    }

    pub fn sources(&mut self) -> SourceLoader<'_> {
        SourceLoader::new(&mut self.sources, self.diagnostics.as_ref())
    }

    pub fn source_file(&self, source: SourceId) -> &SourceFile {
        self.sources.file(source)
    }

    pub fn expansion(&mut self) -> Expansion<'_> {
        Expansion::new(&mut self.sources, &mut self.work, self.diagnostics.as_ref())
    }

    pub fn checking(&mut self) -> Checking<'_> {
        Checking::new(
            SourceReport::new(&self.sources, self.diagnostics.as_ref()),
            &mut self.work,
        )
    }

    pub fn emission(&mut self) -> Result<Emission<'_>, ExitStatus> {
        let limit = Self::rustfmt_time_limit()?;
        Ok(self.emission_within(&limit))
    }

    pub fn source_report(&self) -> SourceReport<'_> {
        SourceReport::new(&self.sources, self.diagnostics.as_ref())
    }

    pub fn build(
        &mut self,
        program: &LocatedProgram,
        options: &BuildOptions,
    ) -> Result<PathBuf, ExitStatus> {
        let emitted = self.emit_file(program.file())?;
        let crate_name = Self::crate_name(program.file().path());
        let (directory, output_name) = match program.entry_project() {
            Some(project) => (project.build_directory(), project.name().to_string()),
            None => (
                Self::build_directory(program.file().path(), &crate_name)?,
                crate_name.clone(),
            ),
        };
        let output = options
            .output
            .clone()
            .unwrap_or_else(|| directory.join(&output_name));
        let cargo = program.project().and_then(|project| {
            project
                .crates()
                .program_target(project.name(), directory.clone(), output.clone())
        });
        let route = match cargo {
            Some(target) => BuildRoute::Cargo(target),
            None => BuildRoute::Rustc(BuildTarget {
                directory,
                crate_name: &crate_name,
                output: &output,
            }),
        };
        self.compile_into(&emitted, &route, options.profile)?;
        Ok(output)
    }

    pub fn build_task(
        &mut self,
        emitted: &EmittedCrate,
        task: &ProjectTask<'_>,
        profile: BuildProfile,
    ) -> Result<PathBuf, ExitStatus> {
        self.compile_into(emitted, &task.route(), profile)?;
        Ok(task.binary().to_path_buf())
    }

    pub fn compile_into(
        &self,
        emitted: &EmittedCrate,
        route: &BuildRoute<'_>,
        profile: BuildProfile,
    ) -> Result<(), ExitStatus> {
        match route {
            BuildRoute::Rustc(target) => self.compile(emitted, target, profile),
            BuildRoute::Cargo(target) => self.compile_with_cargo(emitted, target, profile),
        }
    }

    fn compile(
        &self,
        emitted: &EmittedCrate,
        target: &BuildTarget<'_>,
        profile: BuildProfile,
    ) -> Result<(), ExitStatus> {
        if self.interruption.is_raised() {
            return Err(self.report_build(BuildError::Interrupted));
        }
        RustCompiler::new(target.directory.clone(), profile)
            .compile(
                emitted,
                target.crate_name,
                target.output,
                EntrySpan::start(),
            )
            .map_err(|error| self.report_build(self.interrupted_or(error)))
    }

    fn compile_with_cargo(
        &self,
        emitted: &EmittedCrate,
        target: &CargoTarget<'_>,
        profile: BuildProfile,
    ) -> Result<(), ExitStatus> {
        if self.interruption.is_raised() {
            return Err(self.report_build(BuildError::Interrupted));
        }
        let waiting = |target: &str| {
            self.source_report()
                .note(&format!("waiting for another build in {target}"));
        };
        CargoCompiler::new(target.directory.clone(), BuildPlan::new(profile), &waiting)
            .compile(emitted, target, EntrySpan::start())
            .map_err(|error| self.report_build(self.interrupted_or(error)))
    }

    fn interrupted_or(&self, error: BuildError) -> BuildError {
        if self.interruption.caused_a_failure() {
            return BuildError::Interrupted;
        }
        error
    }

    pub fn emit_file(&mut self, file: &ProgramFile) -> Result<EmittedCrate, ExitStatus> {
        let limit = Self::rustfmt_time_limit()?;
        let program = self.check_file(file)?;
        self.emission_within(&limit).emit(&program)
    }

    fn emission_within(&mut self, limit: &RustfmtTimeLimit) -> Emission<'_> {
        Emission::new(
            SourceReport::new(&self.sources, self.diagnostics.as_ref()),
            &mut self.work,
            InterruptibleFormatting::new(limit.formatting(), &self.interruption),
        )
    }

    fn rustfmt_time_limit() -> Result<RustfmtTimeLimit, ExitStatus> {
        RustfmtTimeLimit::from_environment().map_err(|malformed| malformed.report())
    }

    pub fn check_file(&mut self, file: &ProgramFile) -> Result<TypedProgram, ExitStatus> {
        let entry = self.sources().load(file.path())?;
        let program = self.expansion().expand(file, entry)?;
        self.checking().check(&program)
    }

    fn report_build(&self, error: BuildError) -> ExitStatus {
        match error {
            BuildError::Program(diagnostics) => self.source_report().report(&diagnostics),
            BuildError::Environment(message) => {
                StandardError::write_line(format_args!("error: {message}"));
                ExitStatus::Environment
            }
            BuildError::Interrupted => ExitStatus::Interrupted,
        }
    }

    fn build_directory(path: &Path, crate_name: &str) -> Result<PathBuf, ExitStatus> {
        let working_directory = Self::canonical(Path::new(Self::WORKING_DIRECTORY))?;
        let entry_directory = Self::canonical(&ProgramFile::directory_of(path))?;
        Ok(BuildDirectory::new(Self::build_root(), working_directory)
            .for_entry(&entry_directory, crate_name))
    }

    pub fn build_root() -> PathBuf {
        Path::new(Self::BUILD_PARENT).join(Language::NAME)
    }

    pub fn canonical(directory: &Path) -> Result<PathBuf, ExitStatus> {
        Path::new(Self::WORKING_DIRECTORY)
            .join(directory)
            .canonicalize()
            .map_err(|error| Self::report_unresolvable(directory, &error))
    }

    pub fn crate_name(path: &Path) -> String {
        Self::module_name(path).replace(|character: char| !character.is_ascii_alphanumeric(), "_")
    }

    fn module_name(path: &Path) -> String {
        path.file_stem()
            .map(|stem| stem.to_string_lossy().into_owned())
            .unwrap_or_default()
    }

    fn report_unresolvable(directory: &Path, error: &std::io::Error) -> ExitStatus {
        StandardError::write_line(format_args!(
            "error: cannot resolve `{}`: {error}",
            directory.display()
        ));
        ExitStatus::UsageError
    }
}