yulang-runtime 0.1.0

Runtime IR, validation, monomorphization, and VM support for Yulang
Documentation
use std::collections::HashMap;

use super::*;
use crate::ir::{Binding, Module};
use crate::types::hir_type_has_vars;

#[derive(Debug, Default, Clone)]
pub struct SpecializationTable {
    cache: HashMap<DemandKey, typed_ir::Path>,
    known: Vec<DemandSpecialization>,
    fresh: Vec<DemandSpecialization>,
    next_index: usize,
    semantics: DemandSemantics,
}

impl SpecializationTable {
    pub fn from_module(module: &Module) -> Self {
        let mut table = Self {
            next_index: next_specialization_index(module),
            semantics: DemandSemantics::from_module(module),
            ..Self::default()
        };
        table.seed_existing(module);
        table
    }

    pub fn allocate_fresh(&mut self, checked: &CheckedDemand) -> typed_ir::Path {
        let key = checked_key(checked);
        if !should_materialize_demand(&self.semantics, &key) {
            return checked.target.clone();
        }
        if let Some(path) = self.cache.get(&key) {
            return path.clone();
        }
        let path = specialized_path(&checked.target, self.next_index);
        self.next_index += 1;
        let specialization = DemandSpecialization {
            target: checked.target.clone(),
            path: path.clone(),
            key,
            solved: checked.solved.clone(),
        };
        self.cache.insert(specialization.key.clone(), path.clone());
        self.known.push(specialization.clone());
        self.fresh.push(specialization);
        path
    }

    pub fn into_specializations(self) -> Vec<DemandSpecialization> {
        self.known
    }

    pub fn into_output(self) -> SpecializationOutput {
        SpecializationOutput {
            known: self.known,
            fresh: self.fresh,
        }
    }

    fn seed_existing(&mut self, module: &Module) {
        self.seed_existing_demand_specializations(module);
        self.seed_existing_legacy_specializations(module);
    }

    fn seed_existing_demand_specializations(&mut self, module: &Module) {
        for binding in &module.bindings {
            let Some(target) = unspecialized_demand_path(&binding.name) else {
                continue;
            };
            self.seed_one_existing(target, binding);
        }
    }

    fn seed_existing_legacy_specializations(&mut self, module: &Module) {
        for binding in &module.bindings {
            let Some(target) = unspecialized_legacy_mono_path(&binding.name) else {
                continue;
            };
            self.seed_one_existing(target, binding);
        }
    }

    fn seed_one_existing(&mut self, target: typed_ir::Path, binding: &Binding) {
        if !binding.type_params.is_empty() || hir_type_has_vars(&binding.body.ty) {
            debug_seed_existing_specialization("skip-open-binding", &target, &binding.name, None);
            return;
        }
        let signature = DemandSignature::from_runtime_type(&binding.body.ty);
        if !signature.is_closed() {
            debug_seed_existing_specialization(
                "skip-open-signature",
                &target,
                &binding.name,
                Some(&signature),
            );
            return;
        }
        let specialization = existing_specialization(target, binding, signature);
        if !self.cache.contains_key(&specialization.key) {
            debug_seed_existing_specialization(
                "seed",
                &specialization.target,
                &specialization.path,
                Some(&specialization.key.signature),
            );
            self.cache
                .insert(specialization.key.clone(), specialization.path.clone());
            self.known.push(specialization);
        }
    }
}

pub(super) fn demand_call_target(path: &typed_ir::Path) -> typed_ir::Path {
    unspecialized_demand_path(path)
        .or_else(|| unspecialized_legacy_mono_path(path))
        .unwrap_or_else(|| path.clone())
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpecializationOutput {
    pub known: Vec<DemandSpecialization>,
    pub fresh: Vec<DemandSpecialization>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DemandSpecialization {
    pub target: typed_ir::Path,
    pub path: typed_ir::Path,
    pub key: DemandKey,
    pub solved: DemandSignature,
}

fn checked_key(checked: &CheckedDemand) -> DemandKey {
    DemandKey::from_signature(checked.target.clone(), checked.expected.clone())
}

fn specialized_path(target: &typed_ir::Path, index: usize) -> typed_ir::Path {
    let mut path = target.clone();
    match path.segments.last_mut() {
        Some(name) => {
            name.0 = format!("{}__ddmono{index}", name.0);
        }
        None => {
            path.segments
                .push(typed_ir::Name(format!("__ddmono{index}")));
        }
    }
    path
}

fn existing_specialization(
    target: typed_ir::Path,
    binding: &Binding,
    signature: DemandSignature,
) -> DemandSpecialization {
    DemandSpecialization {
        target: target.clone(),
        path: binding.name.clone(),
        key: DemandKey::from_signature(target, signature.clone()),
        solved: signature,
    }
}

fn should_materialize_demand(_semantics: &DemandSemantics, _key: &DemandKey) -> bool {
    true
}

fn debug_seed_existing_specialization(
    action: &str,
    target: &typed_ir::Path,
    path: &typed_ir::Path,
    signature: Option<&DemandSignature>,
) {
    if std::env::var_os("YULANG_DEBUG_DEMAND_SOURCE").is_none() {
        return;
    }
    eprintln!("specialization seed {action} {target:?} <- {path:?}: {signature:?}");
}

fn next_specialization_index(module: &Module) -> usize {
    module
        .bindings
        .iter()
        .filter_map(|binding| demand_specialization_suffix(&binding.name))
        .max()
        .map(|index| index + 1)
        .unwrap_or(0)
}

fn demand_specialization_suffix(path: &typed_ir::Path) -> Option<usize> {
    path.segments
        .last()?
        .0
        .rsplit_once("__ddmono")?
        .1
        .parse()
        .ok()
}

fn unspecialized_demand_path(path: &typed_ir::Path) -> Option<typed_ir::Path> {
    let mut base = path.clone();
    let name = &mut base.segments.last_mut()?.0;
    let index = name.find("__ddmono")?;
    name.truncate(index);
    Some(base)
}

fn unspecialized_legacy_mono_path(path: &typed_ir::Path) -> Option<typed_ir::Path> {
    let mut base = path.clone();
    let name = &mut base.segments.last_mut()?.0;
    let index = name.find("__mono")?;
    name.truncate(index);
    Some(base)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn specialization_table_reuses_same_key() {
        let checked = checked("id", DemandSignature::Core(named("int")));
        let mut table = SpecializationTable::default();

        let first = table.allocate_fresh(&checked);
        let second = table.allocate_fresh(&checked);
        let specializations = table.into_specializations();

        assert_eq!(first, second);
        assert_eq!(specializations.len(), 1);
    }

    #[test]
    fn specialization_table_allocates_paths_in_order() {
        let mut table = SpecializationTable::default();
        let first = table.allocate_fresh(&checked("id", DemandSignature::Core(named("int"))));
        let second = table.allocate_fresh(&checked("id", DemandSignature::Core(named("str"))));

        assert_eq!(first, path("id__ddmono0"));
        assert_eq!(second, path("id__ddmono1"));
    }

    #[test]
    fn demand_call_target_strips_demand_and_legacy_suffixes() {
        assert_eq!(demand_call_target(&path("id__ddmono12")), path("id"));
        assert_eq!(demand_call_target(&path("id__mono3")), path("id"));
        assert_eq!(demand_call_target(&path("id")), path("id"));
    }

    fn checked(name: &str, signature: DemandSignature) -> CheckedDemand {
        CheckedDemand {
            target: path(name),
            expected: signature.clone(),
            actual: signature.clone(),
            solved: signature,
            substitutions: DemandSubstitution::default(),
            child_demands: DemandQueue::default(),
            local_signatures: HashMap::new(),
        }
    }

    fn named(name: &str) -> DemandCoreType {
        DemandCoreType::Named {
            path: path(name),
            args: Vec::new(),
        }
    }

    fn path(name: &str) -> typed_ir::Path {
        typed_ir::Path::from_name(typed_ir::Name(name.to_string()))
    }
}