hara-native 0.1.21

HAL-free native host runtime and package launcher for Hara
Documentation
use crate::kernel::{read_forms, Form, GeneratedNamespaceConfig, SpannedForm};
use crate::project::Project;
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceLocation {
    pub path: String,
    pub line: usize,
    pub column: usize,
    pub end_line: usize,
    pub end_column: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
    pub code: String,
    pub operation: String,
    pub module: String,
    pub location: SourceLocation,
    pub message: String,
}

#[derive(Debug, Clone)]
pub struct SourceModule {
    pub name: String,
    pub path: String,
    pub source: String,
    pub namespace_form: String,
    pub body_line_base: usize,
    pub forms: Vec<SpannedForm>,
    pub dependencies: Vec<String>,
    pub digest: String,
    pub standard_library: bool,
}

impl SourceModule {
    pub fn parse(
        expected_name: Option<&str>,
        path: impl Into<String>,
        source: impl Into<String>,
        standard_library: bool,
    ) -> Result<Self, String> {
        let path = path.into();
        let source = source.into();
        let parsed = read_forms(&source).map_err(|error| format!("{path}: {error}"))?;
        let namespace = parsed
            .iter()
            .find(|form| namespace_declaration(&form.form).is_some())
            .ok_or_else(|| format!("{path}: HAL module is missing ns form"))?;
        let name = namespace_declaration(&namespace.form).unwrap().to_owned();
        if expected_name.is_some_and(|expected| expected != name.as_str()) {
            return Err(format!(
                "{path}: declared namespace {name} does not match resource {}",
                expected_name.unwrap()
            ));
        }
        let namespace_form = source
            .get(namespace.span.start.offset..namespace.span.end.offset)
            .ok_or_else(|| format!("{path}: invalid namespace source span"))?
            .to_owned();
        let body_start = namespace.span.end.offset;
        let body_line_base = source[..body_start]
            .chars()
            .filter(|character| *character == '\n')
            .count();
        let body = source
            .get(body_start..)
            .ok_or_else(|| format!("{path}: invalid module body span"))?;
        let forms = read_forms(body).map_err(|error| format!("{path}: {error}"))?;
        let dependencies = namespace_dependencies(&namespace.form)?;
        let digest = hex(&Sha256::digest(source.as_bytes()));
        Ok(Self {
            name,
            path,
            source,
            namespace_form,
            body_line_base,
            forms,
            dependencies,
            digest,
            standard_library,
        })
    }

    #[cfg(test)]
    pub fn synthetic(name: &str, source: &str) -> Self {
        Self::parse(Some(name), format!("fixture:{name}"), source, false).unwrap()
    }
}

pub fn collect_project_modules(project: &Project) -> Result<Vec<SourceModule>, String> {
    let mut modules = Vec::new();
    for path in crate::project::files_in(&project.root, &project.source_paths)? {
        let source = fs::read_to_string(&path)
            .map_err(|error| format!("cannot read {}: {error}", path.display()))?;
        let relative = path
            .strip_prefix(&project.root)
            .unwrap_or(path.as_path())
            .to_string_lossy()
            .replace('\\', "/");
        modules.push(SourceModule::parse(None, relative, source, false)?);
    }
    modules.sort_by(|left, right| left.name.cmp(&right.name));
    reject_duplicate_names(&modules)?;
    Ok(modules)
}

pub fn collect_embedded_modules() -> Result<Vec<SourceModule>, String> {
    let mut modules = crate::EMBEDDED_HAL_RESOURCES
        .iter()
        .filter(|(name, _, _)| standard_library_namespace(name))
        .map(|(name, path, source)| {
            SourceModule::parse(Some(name), (*path).to_owned(), (*source).to_owned(), true)
        })
        .collect::<Result<Vec<_>, _>>()?;
    modules.sort_by(|left, right| left.name.cmp(&right.name));
    reject_duplicate_names(&modules)?;
    Ok(modules)
}

pub fn deterministic_module_order(modules: &[SourceModule]) -> Vec<usize> {
    let positions = modules
        .iter()
        .enumerate()
        .map(|(index, module)| (module.name.as_str(), index))
        .collect::<BTreeMap<_, _>>();
    let mut remaining = (0..modules.len()).collect::<BTreeSet<_>>();
    let mut complete = BTreeSet::new();
    let mut output = Vec::with_capacity(modules.len());
    loop {
        let ready = remaining
            .iter()
            .copied()
            .filter(|index| {
                modules[*index]
                    .dependencies
                    .iter()
                    .filter_map(|dependency| positions.get(dependency.as_str()))
                    .all(|dependency| complete.contains(dependency))
            })
            .collect::<Vec<_>>();
        if ready.is_empty() {
            break;
        }
        for index in ready {
            remaining.remove(&index);
            complete.insert(index);
            output.push(index);
        }
    }
    output.extend(remaining);
    output
}

pub fn aggregate_digest(modules: &[SourceModule]) -> (usize, String) {
    let mut hasher = Sha256::new();
    let mut bytes = 0usize;
    let mut ordered = modules.iter().collect::<Vec<_>>();
    ordered.sort_by(|left, right| left.name.cmp(&right.name));
    for module in ordered {
        bytes += module.source.len();
        hasher.update(module.name.as_bytes());
        hasher.update([0]);
        hasher.update(module.source.as_bytes());
        hasher.update([0]);
    }
    (bytes, hex(&hasher.finalize()))
}

fn namespace_declaration(form: &Form) -> Option<&str> {
    let Form::List(values) = without_metadata(form) else {
        return None;
    };
    if !matches!(values.first(), Some(Form::Symbol(head)) if head == "ns" || head == "ns+") {
        return None;
    }
    match values.get(1) {
        Some(Form::Symbol(name)) => Some(name),
        _ => None,
    }
}

fn namespace_dependencies(form: &Form) -> Result<Vec<String>, String> {
    let Form::List(values) = without_metadata(form) else {
        return Err("module namespace declaration must be a list".into());
    };
    if values.len() < 2 {
        return Err("module namespace declaration is incomplete".into());
    }
    let config = GeneratedNamespaceConfig::configure_with(&values[2..], |_| true)?;
    let mut dependencies = config.required_namespaces().to_vec();
    dependencies.extend(config.used_namespaces().iter().cloned());
    dependencies.sort();
    dependencies.dedup();
    Ok(dependencies)
}

fn without_metadata(form: &Form) -> &Form {
    match form {
        Form::Metadata(_, value) => without_metadata(value),
        value => value,
    }
}

fn standard_library_namespace(namespace: &str) -> bool {
    ["std.", "code.", "lang."]
        .iter()
        .any(|prefix| namespace.starts_with(prefix))
}

fn reject_duplicate_names(modules: &[SourceModule]) -> Result<(), String> {
    for pair in modules.windows(2) {
        if pair[0].name == pair[1].name {
            return Err(format!(
                "duplicate production module {}: {} and {}",
                pair[0].name, pair[0].path, pair[1].path
            ));
        }
    }
    Ok(())
}

fn hex(bytes: &[u8]) -> String {
    bytes.iter().map(|byte| format!("{byte:02x}")).collect()
}