Skip to main content

native/egglog_nif/src/resources.rs

use std::sync::Mutex;

use egglog::{EGraph, ast::Command};
use rustler::{Resource, ResourceArc};

use crate::error::NativeError;

pub struct ProgramInner {
    pub base: EGraph,
}

pub struct ProgramResource {
    // Per-resource guard only. This is not a process-wide scheduler lock:
    // it protects the lifetime of this loaded base and serializes query-local
    // clone/run work for this one handle.
    pub inner: Mutex<Option<ProgramInner>>,
}

#[rustler::resource_impl]
impl Resource for ProgramResource {}

pub struct EGraphResource {
    // Mutable sessions are explicitly stateful. The Elixir side owns access
    // through one process; this guard protects the native resource itself.
    pub inner: Mutex<Option<EGraph>>,
}

#[rustler::resource_impl]
impl Resource for EGraphResource {}

pub struct ParsedProgramResource {
    pub commands: Vec<Command>,
}

#[rustler::resource_impl]
impl Resource for ParsedProgramResource {}

pub fn load_egraph(source: &str, proofs: bool) -> Result<EGraph, String> {
    let mut egraph = if proofs {
        EGraph::new_with_proofs()
    } else {
        EGraph::default()
    };

    if !source.trim().is_empty() {
        egraph
            .parse_and_run_program(None, source)
            .map_err(|err| err.to_string())?;
    }

    Ok(egraph)
}

pub fn with_cloned_program<T>(
    program: &ResourceArc<ProgramResource>,
    fun: impl FnOnce(&mut EGraph) -> Result<T, String>,
) -> Result<T, NativeError> {
    let guard = program
        .inner
        .lock()
        .map_err(|_| NativeError::Native("program mutex poisoned".to_string()))?;

    let inner = guard
        .as_ref()
        .ok_or_else(|| NativeError::Closed("program is closed".to_string()))?;

    let mut egraph = inner.base.clone();
    fun(&mut egraph).map_err(NativeError::Native)
}

pub fn with_program<T>(
    program: &ResourceArc<ProgramResource>,
    fun: impl FnOnce(&ProgramInner) -> Result<T, String>,
) -> Result<T, NativeError> {
    let guard = program
        .inner
        .lock()
        .map_err(|_| NativeError::Native("program mutex poisoned".to_string()))?;

    let inner = guard
        .as_ref()
        .ok_or_else(|| NativeError::Closed("program is closed".to_string()))?;

    fun(inner).map_err(NativeError::Native)
}

pub fn with_egraph<T>(
    resource: &ResourceArc<EGraphResource>,
    fun: impl FnOnce(&EGraph) -> Result<T, String>,
) -> Result<T, NativeError> {
    let guard = resource
        .inner
        .lock()
        .map_err(|_| NativeError::Native("egraph mutex poisoned".to_string()))?;

    let egraph = guard
        .as_ref()
        .ok_or_else(|| NativeError::Closed("egraph is closed".to_string()))?;

    fun(egraph).map_err(NativeError::Native)
}

pub fn with_egraph_mut<T>(
    resource: &ResourceArc<EGraphResource>,
    fun: impl FnOnce(&mut EGraph) -> Result<T, String>,
) -> Result<T, NativeError> {
    let mut guard = resource
        .inner
        .lock()
        .map_err(|_| NativeError::Native("egraph mutex poisoned".to_string()))?;

    let egraph = guard
        .as_mut()
        .ok_or_else(|| NativeError::Closed("egraph is closed".to_string()))?;

    fun(egraph).map_err(NativeError::Native)
}

pub fn close_program(program: &ResourceArc<ProgramResource>) -> Result<(), NativeError> {
    let mut guard = program
        .inner
        .lock()
        .map_err(|_| NativeError::Native("program mutex poisoned".to_string()))?;

    guard
        .take()
        .map(|_inner| ())
        .ok_or_else(|| NativeError::Closed("program is already closed".to_string()))
}

pub fn close_egraph(egraph: &ResourceArc<EGraphResource>) -> Result<(), NativeError> {
    let mut guard = egraph
        .inner
        .lock()
        .map_err(|_| NativeError::Native("egraph mutex poisoned".to_string()))?;

    guard
        .take()
        .map(|_egraph| ())
        .ok_or_else(|| NativeError::Closed("egraph is already closed".to_string()))
}