Skip to main content

hrx/loom/
source.rs

1//! Explicit translation-unit inputs, independent of the host filesystem.
2use crate::{Error, Result};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6/// Source language accepted by the native frontend.
7#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
8pub enum CxxStandard {
9    /// ISO C23.
10    C23,
11    /// ISO C++26.
12    #[default]
13    Cpp26,
14}
15impl CxxStandard {
16    pub(super) fn as_str(self) -> &'static str {
17        match self {
18            Self::C23 => "c23",
19            Self::Cpp26 => "c++26",
20        }
21    }
22}
23
24/// A C/C++ translation unit and all of its explicit include dependencies.
25/// Embedded Loom facade headers are supplied by the pinned compiler.
26#[derive(Clone, Debug, Serialize, Deserialize)]
27pub struct CxxSource {
28    /// Main source identifier, also used for quoted include resolution.
29    pub identifier: String,
30    /// Main translation-unit contents.
31    pub contents: String,
32    /// Language standard.
33    pub standard: CxxStandard,
34    /// Virtual paths to header contents. No implicit filesystem reads occur.
35    pub headers: BTreeMap<String, String>,
36    /// Ordered include-search roots within the virtual source set.
37    pub include_paths: Vec<String>,
38    /// Ordered preprocessor definitions, as (name, replacement) pairs.
39    pub defines: Vec<(String, String)>,
40    /// Exported source functions; empty selects visible concrete definitions.
41    pub roots: Vec<String>,
42    /// Permit approximate math under Loom's AFN contract.
43    pub approximate_functions: bool,
44}
45impl CxxSource {
46    /// C++26, LP64, strict math, and the compiler's embedded facade headers.
47    pub fn new(identifier: impl Into<String>, contents: impl Into<String>) -> Self {
48        Self {
49            identifier: identifier.into(),
50            contents: contents.into(),
51            standard: CxxStandard::default(),
52            headers: BTreeMap::new(),
53            include_paths: Vec::new(),
54            defines: Vec::new(),
55            roots: Vec::new(),
56            approximate_functions: false,
57        }
58    }
59    pub(super) fn validate(&self) -> Result<()> {
60        if self.identifier.is_empty()
61            || self.identifier.contains('\0')
62            || self
63                .headers
64                .keys()
65                .any(|key| key.is_empty() || key.contains('\0'))
66            || self.include_paths.iter().any(|key| key.contains('\0'))
67        {
68            return Err(Error::Message(
69                "invalid C/C++ source identifier or include path".into(),
70            ));
71        }
72        Ok(())
73    }
74}
75
76/// One input to a linked compilation module.
77#[derive(Clone, Debug, Serialize, Deserialize)]
78pub enum Source {
79    /// Textual Loom IR.
80    Loom {
81        /// Diagnostic source identifier.
82        identifier: String,
83        /// Source contents.
84        contents: String,
85    },
86    /// Native C/C++ import, producing ordinary Loom IR.
87    Cxx(CxxSource),
88}
89impl Source {
90    /// Construct a named Loom input.
91    pub fn loom(identifier: impl Into<String>, contents: impl Into<String>) -> Self {
92        Self::Loom {
93            identifier: identifier.into(),
94            contents: contents.into(),
95        }
96    }
97    pub(super) fn validate(&self) -> Result<()> {
98        match self {
99            Self::Loom { identifier, .. } if identifier.is_empty() || identifier.contains('\0') => {
100                Err(Error::Message("invalid Loom source identifier".into()))
101            }
102            Self::Cxx(source) => source.validate(),
103            Self::Loom { .. } => Ok(()),
104        }
105    }
106}
107
108// Lexical virtual paths only: never resolves symlinks or touches host files.
109// Absolute virtual names have their own root; traversal cannot escape either.
110pub(super) fn virtual_path(path: &str) -> Result<String> {
111    if path.contains('\0') || path.contains('\\') {
112        return Err(Error::Message("invalid virtual include path".into()));
113    }
114    let mut parts = Vec::new();
115    for part in path.split('/') {
116        match part {
117            "" | "." => {}
118            ".." => {
119                if parts.pop().is_none() {
120                    return Err(Error::Message(
121                        "virtual include escapes its source root".into(),
122                    ));
123                }
124            }
125            part => parts.push(part),
126        }
127    }
128    let prefix = if path.starts_with('/') { "/" } else { "" };
129    Ok(format!("{prefix}{}", parts.join("/")))
130}
131impl CxxSource {
132    pub(super) fn normalize(&mut self) -> Result<()> {
133        self.validate()?;
134        self.identifier = virtual_path(&self.identifier)?;
135        if self.identifier.is_empty() || self.identifier == "/" {
136            return Err(Error::Message("source identifier must name a file".into()));
137        }
138        let mut headers = BTreeMap::new();
139        for (path, contents) in &self.headers {
140            let path = virtual_path(path)?;
141            if path.is_empty()
142                || path == "/"
143                || path == self.identifier
144                || headers.insert(path, contents.clone()).is_some()
145            {
146                return Err(Error::Message(
147                    "empty or aliased virtual header path".into(),
148                ));
149            }
150        }
151        self.headers = headers;
152        for path in &mut self.include_paths {
153            *path = virtual_path(path)?;
154        }
155        Ok(())
156    }
157}
158#[cfg(test)]
159mod tests {
160    use super::*;
161    #[test]
162    fn virtual_paths_are_lexical_and_ambiguous_aliases_are_rejected() {
163        assert_eq!(
164            virtual_path("/src/./nested/../factor.h").unwrap(),
165            "/src/factor.h"
166        );
167        assert!(virtual_path("../secret.h").is_err());
168        assert!(virtual_path("/../../secret.h").is_err());
169        let mut source = CxxSource::new("src/kernel.cpp", "");
170        source
171            .headers
172            .insert("src/nested/../factor.h".into(), "1".into());
173        source.headers.insert("src/factor.h".into(), "2".into());
174        assert!(source.normalize().is_err());
175    }
176}