Skip to main content

logicaffeine_compile/codegen_c/
value_runtime.rs

1//! The value runtime a C program links when the exact extension reaches it: `logicaffeine-data`'s C ABI
2//! over the compiled value model (`logos_rt_value_*`, see `logicaffeine_data::abi`), built on demand as
3//! one static library, so both C backends compute with the compiler's own values. A program the
4//! extension never reaches declares nothing and links nothing more than it did.
5//!
6//! The library is a two-line crate over `logicaffeine-data` with its `c-abi` feature, built in release
7//! under a scratch directory of its own: path-depending on this checkout's `logicaffeine_data` inside
8//! the monorepo, on the crates.io release of this compiler's own version anywhere else. Each build's
9//! archive is copied to a file named by its content, which is never written again, so a link always
10//! reads a whole archive however many processes build at once. It is built for the machine a program
11//! runs on ([`Target`]): the host for the C backends, RV64GC Linux for the RISC-V tier.
12
13use std::path::{Path, PathBuf};
14
15/// What an emitted C program that calls the value runtime declares, and what [`links`] reads.
16pub(crate) const DECLARATION: &str = "#define LOGOS_LINKS_VALUE_RUNTIME 1";
17
18/// Whether the emitted C program `code` links the value runtime.
19pub fn links(code: &str) -> bool {
20    code.contains(DECLARATION)
21}
22
23/// What `code` puts on its link line after its own source: the value runtime's archive and the system
24/// libraries the archive calls, when the program links it; nothing otherwise.
25pub fn link_args(code: &str) -> Result<Vec<String>, String> {
26    if !links(code) {
27        return Ok(Vec::new());
28    }
29    archive_link_args(Target::Host)
30}
31
32/// The archive built for `target` and the system libraries it calls, as a link line names them.
33pub fn archive_link_args(target: Target) -> Result<Vec<String>, String> {
34    let archive = archive_for(target)?;
35    let mut args = vec![archive.path.display().to_string()];
36    args.extend(archive.system_libs.iter().cloned());
37    Ok(args)
38}
39
40/// The machine a library is built for.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum Target {
43    /// The machine the compiler runs on, where the C backends' programs run.
44    Host,
45    /// RV64GC Linux with glibc, which the RISC-V tier links its programs for, statically.
46    Riscv64,
47}
48
49impl Target {
50    /// The triple cargo builds for; `None` for the host.
51    pub fn triple(self) -> Option<&'static str> {
52        match self {
53            Target::Host => None,
54            Target::Riscv64 => Some("riscv64gc-unknown-linux-gnu"),
55        }
56    }
57
58    /// The file the system libraries of the target's last build are kept in.
59    fn system_libs_file(self) -> String {
60        match self.triple() {
61            None => SYSTEM_LIBS.to_string(),
62            Some(triple) => format!("system-libs-{triple}.txt"),
63        }
64    }
65
66    /// The system libraries rustc names for the archive, as the target's link spells them: a static link has no
67    /// shared unwinder, so libgcc_s is the static libgcc_eh there.
68    fn link_libs(self, libs: Vec<String>) -> Vec<String> {
69        match self {
70            Target::Host => libs,
71            Target::Riscv64 => libs
72                .into_iter()
73                .map(|l| if l == "-lgcc_s" { "-lgcc_eh".to_string() } else { l })
74                .collect(),
75        }
76    }
77}
78
79/// The archive cargo writes for `target` under the target directory `target_dir`.
80fn built_archive(target_dir: &Path, target: Target) -> PathBuf {
81    match target.triple() {
82        None => target_dir.join("release").join(ARCHIVE),
83        Some(triple) => target_dir.join(triple).join("release").join(ARCHIVE),
84    }
85}
86
87/// The built library.
88#[derive(Debug, Clone, PartialEq, Eq)]
89pub struct Archive {
90    /// The archive, named by its content and never written again.
91    pub path: PathBuf,
92    /// The system libraries a link line names after the archive (`-lpthread`, `-ldl`, …).
93    pub system_libs: Vec<String>,
94}
95
96/// The library's root: the ABI, and the allocator whose count the program's collector reads.
97const ROOT: &str = "//! The C value runtime: `logicaffeine-data`'s C ABI and the allocator that counts what it holds.\n\
98pub use logicaffeine_data::abi::*;\n\
99\n\
100#[global_allocator]\n\
101static ALLOCATOR: logicaffeine_data::abi::Counting = logicaffeine_data::abi::Counting;\n";
102
103/// The archive cargo writes for the library's crate.
104const ARCHIVE: &str = "liblogos_value_runtime.a";
105
106/// The system libraries of the last build, kept beside the archive for a build cargo found fresh.
107const SYSTEM_LIBS: &str = "system-libs.txt";
108
109/// The library's manifest, its `logicaffeine-data` dependency as `data` spells it.
110fn manifest(data: &str) -> String {
111    format!(
112        "[package]\nname = \"logos-value-runtime\"\nversion = \"0.0.0\"\nedition = \"2021\"\npublish = false\n\n\
113         [lib]\npath = \"lib.rs\"\ncrate-type = [\"staticlib\"]\n\n\
114         [dependencies]\n{data}\n\n\
115         [profile.release]\ndebug = false\n\n\
116         [workspace]\n"
117    )
118}
119
120/// The `logicaffeine-data` dependency line: this checkout's crate inside the monorepo, the crates.io
121/// release of this compiler's version anywhere else.
122fn data_dependency() -> String {
123    match crate::compile::find_workspace_root() {
124        Ok(root) => format!(
125            "logicaffeine-data = {{ path = {:?}, features = [\"c-abi\"] }}",
126            root.join("crates/logicaffeine_data").display().to_string()
127        ),
128        Err(_) => format!(
129            "logicaffeine-data = {{ version = \"={}\", features = [\"c-abi\"] }}",
130            env!("CARGO_PKG_VERSION")
131        ),
132    }
133}
134
135/// The system libraries a `native-static-libs` note names, in its order.
136fn system_libs_of(note: &str) -> Option<Vec<String>> {
137    let (_, libs) = note.split_once("native-static-libs:")?;
138    Some(libs.split_whitespace().map(str::to_string).collect())
139}
140
141/// FNV-1a over the archive's bytes: the name of the file its content is kept in.
142fn content_name(bytes: &[u8]) -> String {
143    let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
144    for b in bytes {
145        hash ^= u64::from(*b);
146        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
147    }
148    format!("logos_value_runtime-{hash:016x}.a")
149}
150
151/// Write `contents` to `path` unless it already holds exactly them, so cargo sees an unchanged source
152/// as unchanged.
153fn write_if_changed(path: &Path, contents: &str) -> Result<(), String> {
154    if std::fs::read_to_string(path).is_ok_and(|have| have == contents) {
155        return Ok(());
156    }
157    std::fs::write(path, contents).map_err(|e| format!("cannot write {}: {e}", path.display()))
158}
159
160/// The library for the host, built once per process.
161pub fn archive() -> Result<std::sync::Arc<Archive>, String> {
162    archive_for(Target::Host)
163}
164
165/// The library for `target`, built once per process.
166#[cfg(not(target_arch = "wasm32"))]
167pub fn archive_for(target: Target) -> Result<std::sync::Arc<Archive>, String> {
168    use std::collections::HashMap;
169    use std::sync::{Arc, Mutex, OnceLock};
170    static BUILT: OnceLock<Mutex<HashMap<Target, Arc<Archive>>>> = OnceLock::new();
171    let mut built = BUILT
172        .get_or_init(|| Mutex::new(HashMap::new()))
173        .lock()
174        .unwrap_or_else(|poisoned| poisoned.into_inner());
175    if let Some(archive) = built.get(&target) {
176        return Ok(Arc::clone(archive));
177    }
178    let archive = Arc::new(build(target)?);
179    built.insert(target, Arc::clone(&archive));
180    Ok(archive)
181}
182
183#[cfg(target_arch = "wasm32")]
184pub fn archive_for(_target: Target) -> Result<std::sync::Arc<Archive>, String> {
185    Err("the C value runtime is built by a native toolchain".to_string())
186}
187
188/// The scratch directory the library builds in: one per checkout of the compiler, so two source trees
189/// never share a build.
190#[cfg(not(target_arch = "wasm32"))]
191fn project_dir() -> PathBuf {
192    use std::hash::{Hash, Hasher};
193    let mut hash = std::collections::hash_map::DefaultHasher::new();
194    env!("CARGO_MANIFEST_DIR").hash(&mut hash);
195    std::env::temp_dir().join(format!("logos_value_runtime-{:016x}", hash.finish()))
196}
197
198#[cfg(not(target_arch = "wasm32"))]
199fn build(target: Target) -> Result<Archive, String> {
200    use std::process::Command;
201    let project = project_dir();
202    std::fs::create_dir_all(project.join("archives"))
203        .map_err(|e| format!("cannot create {}: {e}", project.display()))?;
204    let lock_path = project.with_extension("lock");
205    let lock = std::fs::OpenOptions::new()
206        .read(true)
207        .write(true)
208        .create(true)
209        .truncate(false)
210        .open(&lock_path)
211        .map_err(|e| format!("cannot open {}: {e}", lock_path.display()))?;
212    fs2::FileExt::lock_exclusive(&lock).map_err(|e| format!("cannot lock {}: {e}", lock_path.display()))?;
213
214    write_if_changed(&project.join("Cargo.toml"), &manifest(&data_dependency()))?;
215    write_if_changed(&project.join("lib.rs"), ROOT)?;
216    if let Ok(root) = crate::compile::find_workspace_root() {
217        let seed = root.join("Cargo.lock");
218        let lockfile = project.join("Cargo.lock");
219        if seed.exists() && !lockfile.exists() {
220            std::fs::copy(&seed, &lockfile).map_err(|e| format!("cannot seed {}: {e}", lockfile.display()))?;
221        }
222    }
223
224    let target_dir = project.join("target");
225    let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
226    let mut command = Command::new(cargo);
227    command
228        .args(["rustc", "--release", "--lib", "--quiet", "--manifest-path"])
229        .arg(project.join("Cargo.toml"))
230        .arg("--target-dir")
231        .arg(&target_dir);
232    if let Some(triple) = target.triple() {
233        command.args(["--target", triple]);
234    }
235    let out = command
236        .args(["--", "--print", "native-static-libs"])
237        .env_remove("RUSTFLAGS")
238        .env_remove("CARGO_ENCODED_RUSTFLAGS")
239        .env_remove("CARGO_BUILD_RUSTFLAGS")
240        .env_remove("CARGO_TARGET_DIR")
241        .output()
242        .map_err(|e| format!("cargo did not start: {e}"))?;
243    let said = String::from_utf8_lossy(&out.stderr);
244    if !out.status.success() {
245        let install = target
246            .triple()
247            .map(|triple| format!(" (its standard library installs with `rustup target add {triple}`)"))
248            .unwrap_or_default();
249        return Err(format!("cargo could not build the C value runtime{install}:\n{said}"));
250    }
251    let libs_path = project.join(target.system_libs_file());
252    let system_libs = match said.lines().find_map(system_libs_of) {
253        Some(libs) => {
254            write_if_changed(&libs_path, &libs.join(" "))?;
255            libs
256        }
257        None => std::fs::read_to_string(&libs_path)
258            .map(|kept| kept.split_whitespace().map(str::to_string).collect())
259            .map_err(|_| "cargo built the C value runtime without naming the system libraries it calls".to_string())?,
260    };
261
262    let built = built_archive(&target_dir, target);
263    let bytes = std::fs::read(&built).map_err(|e| format!("cannot read {}: {e}", built.display()))?;
264    let path = project.join("archives").join(content_name(&bytes));
265    if !path.exists() {
266        let partial = path.with_extension(format!("a.{}", std::process::id()));
267        std::fs::write(&partial, &bytes).map_err(|e| format!("cannot write {}: {e}", partial.display()))?;
268        std::fs::rename(&partial, &path).map_err(|e| format!("cannot keep {}: {e}", path.display()))?;
269    }
270    drop(lock);
271    Ok(Archive {
272        path,
273        system_libs: target.link_libs(system_libs),
274    })
275}
276
277#[cfg(test)]
278#[path = "../../../logicaffeine_tests/unit/logicaffeine_compile/codegen_c/value_runtime/tests.rs"]
279mod tests;