Skip to main content

logicaffeine_cli/project/
build_c.rs

1//! The C build: `largo build [--release] --emit c|c-bytecode`.
2//!
3//! The Rust build with another backend. The same two profiles, the artifact published at
4//! `target/<mode>/<name>`, and the generated source kept in the build directory beside it
5//! (`target/<mode>/build-c/main.c`, where the Rust build keeps `build/src/main.rs`). The C compiler is
6//! `$CC`, default `cc`.
7//!
8//! | profile | flags |
9//! |---|---|
10//! | debug | [`C_DEBUG_FLAGS`] |
11//! | release | [`C_RELEASE_FLAGS`] — the flags the benchmark suite builds its hand-written C with |
12//!
13//! A release build strips its symbol table, as the Rust release profile does. The same override the Rust
14//! build honours keeps it: `CARGO_PROFILE_RELEASE_STRIP=false` (or `none`).
15
16use std::fs;
17use std::path::{Path, PathBuf};
18use std::process::Command;
19
20use super::build::{BuildConfig, BuildError, BuildResult};
21use super::manifest::Manifest;
22
23/// The optimization flags of a debug C build.
24pub const C_DEBUG_FLAGS: &[&str] = &["-O0", "-g"];
25
26/// The optimization flags of a release C build: full optimization, the host CPU, link-time optimization, and no
27/// floating-point contraction — `a*b + c` stays two rounded operations, as it is on the Rust tier and the VM, so every
28/// tier computes the same bits.
29pub const C_RELEASE_FLAGS: &[&str] = &["-O3", "-march=native", "-flto", "-ffp-contract=off"];
30
31/// Which LOGOS → C lowering a C build runs.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum CLowering {
34    /// The optimized AST, lowered by [`logicaffeine_compile::compile::compile_to_c`] (`--emit c`).
35    Ast,
36    /// The optimized bytecode, lowered by
37    /// [`logicaffeine_compile::compile::compile_to_c_via_bytecode`] (`--emit c-bytecode`).
38    Bytecode,
39}
40
41impl CLowering {
42    /// The `--emit` value that selects this lowering.
43    pub fn emit_name(self) -> &'static str {
44        match self {
45            CLowering::Ast => "c",
46            CLowering::Bytecode => "c-bytecode",
47        }
48    }
49}
50
51/// The C compiler a build runs: `$CC`, else `cc`.
52pub fn c_compiler() -> String {
53    std::env::var("CC")
54        .ok()
55        .filter(|cc| !cc.trim().is_empty())
56        .unwrap_or_else(|| "cc".to_string())
57}
58
59/// Whether a release build strips its symbol table: yes, unless `CARGO_PROFILE_RELEASE_STRIP` says
60/// `false` or `none` — the knob the Rust release profile answers to.
61fn release_strips() -> bool {
62    !matches!(
63        std::env::var("CARGO_PROFILE_RELEASE_STRIP")
64            .ok()
65            .as_deref()
66            .map(str::trim),
67        Some("false") | Some("none")
68    )
69}
70
71/// The complete compiler argument list for `source` → `output` under `release`, linking `links` after the source:
72/// what the generated C asks for beyond the C library (the exact extension's value runtime,
73/// [`logicaffeine_compile::codegen_c::value_runtime::link_args`]).
74pub fn c_compiler_args(release: bool, source: &Path, output: &Path, links: &[String]) -> Vec<String> {
75    let mut args: Vec<String> = (if release {
76        C_RELEASE_FLAGS
77    } else {
78        C_DEBUG_FLAGS
79    })
80    .iter()
81    .map(|f| f.to_string())
82    .collect();
83    if release && release_strips() {
84        args.push("-s".to_string());
85    }
86    args.push("-o".to_string());
87    args.push(output.display().to_string());
88    args.push(source.display().to_string());
89    args.extend(links.iter().cloned());
90    args.push("-lm".to_string());
91    args
92}
93
94pub(crate) fn build_c_with_entry(
95    config: &BuildConfig,
96    manifest: &Manifest,
97    entry_path: &Path,
98    lowering: CLowering,
99) -> Result<BuildResult, BuildError> {
100    if config.lib_mode {
101        return Err(BuildError::Unsupported(format!(
102            "`--emit {}` builds an executable; `--lib` is a Rust build (drop `--emit` to build the library)",
103            lowering.emit_name()
104        )));
105    }
106    if config.target.is_some() {
107        return Err(BuildError::Unsupported(format!(
108            "`--emit {}` builds for this machine; `--target` cross-compiles the Rust build (drop `--emit` to cross-compile)",
109            lowering.emit_name()
110        )));
111    }
112
113    let started = std::time::Instant::now();
114    crate::ui::phase(
115        "Compiling",
116        format!(
117            "{} v{} (LOGOS → C)",
118            manifest.package.name, manifest.package.version
119        ),
120    );
121
122    let build_dir =
123        config
124            .project_dir
125            .join("target")
126            .join(if config.release { "release" } else { "debug" });
127    let c_dir = build_dir.join("build-c");
128    if c_dir.exists() {
129        fs::remove_dir_all(&c_dir).map_err(|e| BuildError::Io(e.to_string()))?;
130    }
131    fs::create_dir_all(&c_dir).map_err(|e| BuildError::Io(e.to_string()))?;
132
133    let dep_roots = manifest
134        .resolve_dependencies(&config.project_dir)
135        .map_err(|e| BuildError::Io(e.to_string()))?
136        .into_iter()
137        .map(|d| logicaffeine_compile::loader::DepRoot {
138            name: d.name,
139            root: d.root,
140        })
141        .collect::<Vec<_>>();
142    let closure = crate::closure::assemble_closure_with_deps(entry_path, dep_roots)
143        .map_err(|e| BuildError::Io(format!("could not assemble project: {e}")))?;
144    let emitted = match lowering {
145        CLowering::Ast => logicaffeine_compile::compile::compile_to_c(&closure.source),
146        CLowering::Bytecode => {
147            logicaffeine_compile::compile::compile_to_c_via_bytecode(&closure.source)
148        }
149    };
150    let c_code = emitted.map_err(|pe| {
151        let at = crate::diagnostics::Placed::locate(
152            pe.span,
153            &closure.source,
154            &closure.source_map,
155            entry_path,
156        );
157        BuildError::Diagnostic(crate::diagnostics::render_parse_error(&pe, &at))
158    })?;
159
160    let source = c_dir.join("main.c");
161    fs::write(&source, &c_code).map_err(|e| BuildError::Io(e.to_string()))?;
162    let built = c_dir.join(&manifest.package.name);
163
164    let cc = c_compiler();
165    if logicaffeine_compile::codegen_c::value_runtime::links(&c_code) {
166        crate::ui::phase("Building", "the exact extension's value runtime (Rust)".to_string());
167    }
168    let links = logicaffeine_compile::codegen_c::value_runtime::link_args(&c_code).map_err(|why| {
169        BuildError::CToolchain(format!("the exact extension's value runtime did not build: {why}"))
170    })?;
171    let args = c_compiler_args(config.release, &source, &built, &links);
172    crate::ui::phase("Building", format!("generated C ({cc})"));
173    if crate::ui::verbosity() > 0 {
174        crate::ui::phase("Running", format!("`{cc} {}`", args.join(" ")));
175    }
176    let status = Command::new(&cc).args(&args).status().map_err(|e| {
177        if e.kind() == std::io::ErrorKind::NotFound {
178            BuildError::CToolchain(format!(
179                "the C compiler `{cc}` was not found — `largo build --emit {}` needs a C compiler (set CC to choose one)",
180                lowering.emit_name()
181            ))
182        } else {
183            BuildError::Io(format!("could not run the C compiler `{cc}`: {e}"))
184        }
185    })?;
186    if !status.success() {
187        return Err(BuildError::CCompiler(format!(
188            "the C compiler `{cc}` failed ({status}) on the generated C at {}; its diagnostics are above",
189            source.display()
190        )));
191    }
192
193    crate::ui::phase(
194        "Finished",
195        format!(
196            "{} profile in {:.1}s",
197            if config.release { "release" } else { "dev" },
198            started.elapsed().as_secs_f64()
199        ),
200    );
201    let binary_path: PathBuf = build_dir.join(&manifest.package.name);
202    fs::copy(&built, &binary_path).map_err(|e| {
203        BuildError::Io(format!(
204            "the build succeeded but its artifact could not be published to {}: {e}\n\
205             The freshly built file is at {}. Anything at the stable path is from an EARLIER \
206             build — do not ship it.",
207            binary_path.display(),
208            built.display()
209        ))
210    })?;
211    crate::ui::phase("Artifact", binary_path.display().to_string());
212
213    Ok(BuildResult {
214        target_dir: build_dir,
215        binary_path,
216    })
217}