rusty-javac 0.2.3

A Java compiler written in Rust.
Documentation
use crate::ast::JavaSyntaxNode;
use crate::call_resolver::ClassCatalog;
use crate::compiler::classpath::build_class_catalog;
use crate::compiler::config::CompilerConfig;
use crate::compiler::diagnostic::{render_bytecode_error, render_lower_error};
use crate::compiler::incremental::IncrementalBuild;
use crate::diagnostics::{SourceFile, render_diagnostics};
use crate::hir::CompilationUnit;
use std::path::{Path, PathBuf};

type CompileResult<T> = Result<T, Vec<String>>;

pub struct Compiler {
    config: CompilerConfig,
}

struct ClassArtifact {
    internal_name: String,
    bytes: Vec<u8>,
}

struct ClassPlan {
    unit: CompilationUnit,
    internal_name: String,
    source_file: String,
}

impl Compiler {
    pub fn new(config: CompilerConfig) -> Self {
        Self { config }
    }

    pub fn compile(self) -> CompileResult<()> {
        let catalog = build_class_catalog(&self.config.classpath, &self.config.source_files)?;
        let incremental = IncrementalBuild::from_config(&self.config)?;
        let mut errors = Vec::new();
        for source_file in &self.config.source_files {
            if let Err(error) = self.compile_file(source_file, &catalog, incremental.as_ref()) {
                errors.extend(error);
            }
        }
        if errors.is_empty() {
            Ok(())
        } else {
            Err(errors)
        }
    }

    fn compile_file(
        &self,
        source_file: &str,
        catalog: &ClassCatalog,
        incremental: Option<&IncrementalBuild>,
    ) -> CompileResult<()> {
        let source = read_source_file(source_file)?;
        let plan = plan_source(source_file, &source, catalog)?;
        let class_path = class_file_path(&self.config.output_dir, &plan.internal_name);

        if incremental.is_some_and(|incremental| incremental.class_is_fresh(&class_path)) {
            return Ok(());
        }

        let artifacts = compile_plan(source_file, &source, catalog, plan)?;
        for artifact in artifacts {
            write_class_file(&self.config.output_dir, &artifact)?;
        }
        Ok(())
    }
}

fn read_source_file(path: &str) -> CompileResult<String> {
    std::fs::read_to_string(path).map_err(|e| vec![format!("failed to read {}: {}", path, e)])
}

fn plan_source(filename: &str, source: &str, catalog: &ClassCatalog) -> CompileResult<ClassPlan> {
    let unit = parse_and_lower(filename, source, catalog)?;
    let internal_name = top_level_class_name(filename, &unit)?;
    let source_file = source_file_attribute_name(filename);

    Ok(ClassPlan {
        unit,
        internal_name,
        source_file,
    })
}

fn compile_plan(
    filename: &str,
    source: &str,
    catalog: &ClassCatalog,
    plan: ClassPlan,
) -> CompileResult<Vec<ClassArtifact>> {
    let classes = crate::bytecode::class_gen::gen_classes_with_source_file(
        &plan.unit,
        catalog,
        Some(&plan.source_file),
    )
    .map_err(|e| render_bytecode_error(filename, source, &e))?;

    Ok(classes
        .into_iter()
        .map(|class| ClassArtifact {
            internal_name: class.internal_name,
            bytes: class.bytes,
        })
        .collect())
}

fn source_file_attribute_name(filename: &str) -> String {
    Path::new(filename)
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or(filename)
        .to_string()
}

fn parse_and_lower(
    filename: &str,
    source: &str,
    catalog: &ClassCatalog,
) -> CompileResult<CompilationUnit> {
    let parse = crate::parser::Parser::parse(source);
    if !parse.errors.is_empty() {
        let diagnostics = parse
            .errors
            .iter()
            .map(|error| error.diagnostic())
            .collect::<Vec<_>>();
        return Err(render_diagnostics(
            SourceFile::new(filename, source),
            &diagnostics,
        ));
    }

    let root = JavaSyntaxNode::new_root(parse.green_node);
    crate::hir::lowering::lower_with_catalog(&root, catalog)
        .map_err(|e| render_lower_error(filename, source, &e))
}

fn top_level_class_name(filename: &str, unit: &CompilationUnit) -> CompileResult<String> {
    unit.type_decls
        .first()
        .map(|decl| decl.name.to_string())
        .ok_or_else(|| vec![format!("{}: no class declaration found", filename)])
}

fn write_class_file(output_dir: &str, artifact: &ClassArtifact) -> CompileResult<()> {
    let class_path = class_file_path(output_dir, &artifact.internal_name);
    if let Some(parent) = class_path.parent() {
        std::fs::create_dir_all(parent)
            .map_err(|e| vec![format!("failed to create {}: {}", parent.display(), e)])?;
    }
    std::fs::write(&class_path, &artifact.bytes)
        .map_err(|e| vec![format!("failed to write {}: {}", class_path.display(), e)])
}

fn class_file_path(output_dir: &str, class_name: &str) -> PathBuf {
    let mut path = Path::new(output_dir).to_path_buf();
    for segment in class_name.split('/') {
        path.push(segment);
    }
    path.set_extension("class");
    path
}