solar-cli 0.2.0

Solar compiler CLI definitions, utils, and driver
Documentation
use solar_codegen::{Backend, EvmCodegen, lower};
use solar_config::CompilerOutput;
use solar_data_structures::map::{FxHashMap, FxHashSet};
use solar_interface::Result;
use solar_sema::{CompilerRef, Gcx, hir::ContractId};
use std::{
    collections::BTreeMap,
    io::{self, Write},
    path::Path,
};

type Hashes = BTreeMap<String, String>;

#[derive(Default, serde::Serialize)]
struct SemaCombinedJson<Abi> {
    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
    contracts: BTreeMap<String, SemaCombinedJsonContract<Abi>>,
    version: &'static str,
}

#[derive(Default, serde::Serialize)]
struct SemaCombinedJsonContract<Abi> {
    #[serde(skip_serializing_if = "Option::is_none")]
    abi: Option<Abi>,
    #[serde(skip_serializing_if = "Option::is_none")]
    hashes: Option<Hashes>,
}

#[derive(Default, serde::Serialize)]
struct CodegenCombinedJson {
    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
    contracts: BTreeMap<String, CodegenCombinedJsonContract>,
    version: &'static str,
}

#[derive(Default, serde::Serialize)]
struct CodegenCombinedJsonContract {
    #[serde(skip_serializing_if = "Option::is_none")]
    abi: Option<serde_json::Value>,
    #[serde(skip_serializing_if = "Option::is_none")]
    bin: Option<String>,
    #[serde(rename = "bin-runtime", skip_serializing_if = "Option::is_none")]
    bin_runtime: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    hashes: Option<Hashes>,
}

pub(crate) fn emit_requested(compiler: &mut CompilerRef<'_>) -> Result {
    let gcx = compiler.gcx();
    emit_sema_json(gcx)?;
    emit_mir(gcx)?;
    emit_codegen_json(gcx)
}

fn emit_sema_json(gcx: Gcx<'_>) -> Result {
    let sess = gcx.sess;
    let emit_abi = sess.opts.emit.contains(&CompilerOutput::Abi);
    let emit_hashes = sess.opts.emit.contains(&CompilerOutput::Hashes);

    if !emit_abi && !emit_hashes {
        return Ok(());
    }

    let mut output = SemaCombinedJson {
        contracts: BTreeMap::default(),
        version: solar_config::version::SEMVER_VERSION,
    };

    for id in gcx.hir.contract_ids() {
        let name = contract_output_name(gcx, id);
        let contract_output = output.contracts.entry(name).or_default();

        if emit_abi {
            contract_output.abi = Some(gcx.contract_abi(id));
        }
        if emit_hashes {
            contract_output.hashes = Some(contract_hashes(gcx, id));
        }
    }

    write_output_json(gcx, &output, false)
}

fn emit_codegen_json(gcx: Gcx<'_>) -> Result {
    let sess = gcx.sess;
    let emit_abi = sess.opts.emit.contains(&CompilerOutput::Abi);
    let emit_hashes = sess.opts.emit.contains(&CompilerOutput::Hashes);
    let emit_bin = sess.opts.emit.contains(&CompilerOutput::Bin);
    let emit_bin_runtime = sess.opts.emit.contains(&CompilerOutput::BinRuntime);

    if !emit_bin && !emit_bin_runtime {
        return Ok(());
    }

    let bytecodes =
        if emit_bin || emit_bin_runtime { Some(generate_contract_bytecodes(gcx)?) } else { None };

    let mut output = CodegenCombinedJson {
        contracts: BTreeMap::default(),
        version: solar_config::version::SEMVER_VERSION,
    };

    for id in gcx.hir.contract_ids() {
        let name = contract_output_name(gcx, id);
        let contract_output = output.contracts.entry(name).or_default();

        if emit_abi {
            contract_output.abi = Some(serde_json::to_value(gcx.contract_abi(id)).unwrap());
        }
        if emit_hashes {
            contract_output.hashes = Some(contract_hashes(gcx, id));
        }

        if let Some(bytecodes) = &bytecodes
            && let Some(bytecode) = bytecodes.get(&id)
        {
            if emit_bin {
                contract_output.bin = Some(bytecode.deployment.clone());
            }
            if emit_bin_runtime {
                contract_output.bin_runtime = Some(bytecode.runtime.clone());
            }
        }
    }

    write_output_json(gcx, &output, true)
}

fn write_output_json<T: serde::Serialize>(
    gcx: Gcx<'_>,
    output: &T,
    trailing_newline: bool,
) -> Result {
    let sess = gcx.sess;
    let out_path = sess.opts.out_dir.as_deref().map(|dir| dir.join("combined.json"));
    let mut writer = out_writer(out_path.as_deref())
        .map_err(|e| sess.dcx.err(format!("failed to write to output: {e}")).emit())?;
    to_json(&mut writer, &output, sess.opts.pretty_json)
        .map_err(|e| sess.dcx.err(format!("failed to write to output: {e}")).emit())?;
    if trailing_newline {
        writer
            .write_all(b"\n")
            .map_err(|e| sess.dcx.err(format!("failed to write to output: {e}")).emit())?;
    }
    writer.flush().map_err(|e| sess.dcx.err(format!("failed to write to output: {e}")).emit())?;

    Ok(())
}

fn contract_output_name(gcx: Gcx<'_>, id: ContractId) -> String {
    let contract = gcx.hir.contract(id);
    let source = gcx.hir.source(contract.source);
    format!("{}:{}", source.file.name.display().to_string().replace('\\', "/"), contract.name)
}

fn emit_mir(gcx: Gcx<'_>) -> Result {
    let sess = gcx.sess;
    if !sess.opts.emit.contains(&CompilerOutput::Mir) {
        return Ok(());
    }

    let out_path = sess.opts.out_dir.as_deref().map(|dir| dir.join("combined.mir"));
    let mut writer = out_writer(out_path.as_deref())
        .map_err(|e| sess.dcx.err(format!("failed to write to output: {e}")).emit())?;
    for id in gcx.hir.contract_ids() {
        let contract = gcx.hir.contract(id);
        if contract.kind.is_interface() || contract.kind.is_abstract_contract() {
            continue;
        }
        let module = lower::lower_contract(gcx, id);
        gcx.dcx().has_errors()?;
        let name = gcx.contract_fully_qualified_name(id);
        writeln!(writer, "// === {name} ===")
            .map_err(|e| sess.dcx.err(format!("failed to write to output: {e}")).emit())?;
        writeln!(writer, "{}", module.to_text())
            .map_err(|e| sess.dcx.err(format!("failed to write to output: {e}")).emit())?;
    }
    writer.flush().map_err(|e| sess.dcx.err(format!("failed to write to output: {e}")).emit())?;

    Ok(())
}

#[derive(Clone)]
struct GeneratedBytecodes {
    deployment: String,
    runtime: String,
}

fn generate_contract_bytecodes(gcx: Gcx<'_>) -> Result<FxHashMap<ContractId, GeneratedBytecodes>> {
    let mut all_bytecodes = FxHashMap::default();
    let mut visiting = FxHashSet::default();
    for id in gcx.hir.contract_ids() {
        let contract = gcx.hir.contract(id);
        if !contract.kind.is_interface() && !contract.kind.is_abstract_contract() {
            ensure_contract_bytecode(gcx, id, &mut all_bytecodes, &mut visiting)?;
        }
    }

    let mut bytecodes = FxHashMap::default();
    for id in gcx.hir.contract_ids() {
        let contract = gcx.hir.contract(id);
        if contract.kind.is_interface() || contract.kind.is_abstract_contract() {
            continue;
        }

        let mut module = lower::lower_contract_with_bytecodes(gcx, id, &all_bytecodes);
        gcx.dcx().has_errors()?;
        let mut codegen = EvmCodegen::new(gcx);
        let artifact = codegen.lower_module(&mut module);
        bytecodes.insert(
            id,
            GeneratedBytecodes {
                deployment: alloy_primitives::hex::encode(artifact.deployment),
                runtime: alloy_primitives::hex::encode(artifact.runtime),
            },
        );
    }

    Ok(bytecodes)
}

fn ensure_contract_bytecode(
    gcx: Gcx<'_>,
    contract_id: ContractId,
    all_bytecodes: &mut FxHashMap<ContractId, Vec<u8>>,
    visiting: &mut FxHashSet<ContractId>,
) -> Result {
    if all_bytecodes.contains_key(&contract_id) {
        return Ok(());
    }

    let contract = gcx.hir.contract(contract_id);
    if contract.kind.is_interface() || contract.kind.is_abstract_contract() {
        return Err(gcx
            .dcx()
            .err("cannot generate creation bytecode for non-deployable contract")
            .span(contract.span)
            .emit());
    }

    if !visiting.insert(contract_id) {
        return Err(gcx
            .dcx()
            .err("recursive contract creation bytecode dependency")
            .span(contract.span)
            .emit());
    }

    for dep in lower::contract_bytecode_dependencies(gcx, contract_id) {
        ensure_contract_bytecode(gcx, dep, all_bytecodes, visiting)?;
    }

    let mut module = lower::lower_contract_with_bytecodes(gcx, contract_id, all_bytecodes);
    gcx.dcx().has_errors()?;
    let mut codegen = EvmCodegen::new(gcx);
    let artifact = codegen.lower_module(&mut module);
    all_bytecodes.insert(contract_id, artifact.deployment);
    visiting.remove(&contract_id);

    Ok(())
}

fn contract_hashes(gcx: Gcx<'_>, id: ContractId) -> Hashes {
    let mut hashes = Hashes::default();
    for function in gcx.interface_functions(id) {
        hashes.insert(
            gcx.item_signature(function.id.into()).to_string(),
            alloy_primitives::hex::encode(function.selector),
        );
    }
    hashes
}

fn out_writer(path: Option<&Path>) -> io::Result<impl io::Write> {
    let out: Box<dyn io::Write> = if let Some(path) = path {
        Box::new(std::fs::File::create(path)?)
    } else {
        Box::new(std::io::stdout())
    };
    Ok(io::BufWriter::new(out))
}

fn to_json<W: io::Write, T: serde::Serialize>(
    writer: W,
    value: &T,
    pretty: bool,
) -> serde_json::Result<()> {
    if pretty {
        serde_json::to_writer_pretty(writer, value)
    } else {
        serde_json::to_writer(writer, value)
    }
}