sim-run-loaders 0.1.0

Feature-composable SIM bootloader loaders.
Documentation
use std::sync::Arc;

use sim_kernel::{
    Cx, Expr, Linker, LoadCx, NumberBinaryOp, NumberDomain, NumberLiteral, NumberReductionOp,
    NumberUnaryOp, Object, ObjectCompat, Result, Symbol, Value, ValueNumberBinaryOp,
    ValueNumberReductionOp, ValueNumberUnaryOp,
};

use super::native::NativeGuest;

#[derive(Clone)]
struct NativeAbiNumberDomain {
    guest: Arc<dyn NativeGuest>,
    symbol: Symbol,
}

impl NativeAbiNumberDomain {
    fn invoke_expr(&self, op: &str, expr: &Expr) -> Result<Expr> {
        let args = sim_codec_binary::encode_frame(expr)?.0;
        let bytes = self.guest.invoke(&format!("{}/{op}", self.symbol), &args)?;
        let (_, expr) = sim_codec_binary::decode_frame(sim_kernel::CodecId(0), &bytes)?;
        Ok(expr)
    }
}

impl Object for NativeAbiNumberDomain {
    fn display(&self, _cx: &mut Cx) -> Result<String> {
        Ok(format!("#<native-number-domain {}>", self.symbol))
    }

    fn as_any(&self) -> &dyn std::any::Any {
        self
    }
}

impl ObjectCompat for NativeAbiNumberDomain {
    fn class(&self, cx: &mut Cx) -> Result<sim_kernel::ClassRef> {
        if let Some(value) = cx
            .registry()
            .class_by_symbol(&Symbol::qualified("core", "NumberDomain"))
        {
            return Ok(value.clone());
        }
        cx.factory().class_stub(
            sim_kernel::CORE_NUMBER_DOMAIN_CLASS_ID,
            Symbol::qualified("core", "NumberDomain"),
        )
    }

    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
        Ok(Expr::Symbol(self.symbol.clone()))
    }

    fn as_number_domain(&self) -> Option<&dyn NumberDomain> {
        Some(self)
    }
}

impl NumberDomain for NativeAbiNumberDomain {
    fn symbol(&self) -> Symbol {
        self.symbol.clone()
    }

    fn parse_literal(&self, cx: &mut Cx, text: &str) -> Result<Option<Value>> {
        match self.invoke_expr("parse-literal", &Expr::String(text.to_owned()))? {
            Expr::Nil => Ok(None),
            Expr::Number(number) => cx
                .factory()
                .number_literal(number.domain, number.canonical)
                .map(Some),
            other => Err(sim_kernel::Error::HostError(format!(
                "native number-domain {} parse-literal returned non-number {other:?}",
                self.symbol
            ))),
        }
    }

    fn encode_literal(&self, cx: &mut Cx, value: Value) -> Result<Option<NumberLiteral>> {
        let expr = value.object().as_expr(cx)?;
        match self.invoke_expr("encode-literal", &expr)? {
            Expr::Nil => Ok(None),
            Expr::Number(number) => Ok(Some(number)),
            other => Err(sim_kernel::Error::HostError(format!(
                "native number-domain {} encode-literal returned non-number {other:?}",
                self.symbol
            ))),
        }
    }
}

pub(super) fn register_native_number_domain(
    cx: &mut LoadCx,
    linker: &mut Linker<'_>,
    guest: Arc<dyn NativeGuest>,
    symbol: Symbol,
) -> Result<()> {
    let value = cx.factory().opaque(Arc::new(NativeAbiNumberDomain {
        guest,
        symbol: symbol.clone(),
    }))?;
    linker.number_domain_value(symbol.clone(), value)?;
    register_native_scalar_ops(linker, symbol);
    Ok(())
}

fn register_native_scalar_ops(linker: &mut Linker<'_>, domain: Symbol) {
    for (operator, literal_apply, value_apply) in [
        (
            math_symbol("add"),
            native_number_add as fn(&mut Cx, NumberLiteral, NumberLiteral) -> Result<Value>,
            native_value_number_add as fn(&mut Cx, Value, Value) -> Result<Value>,
        ),
        (
            math_symbol("sub"),
            native_number_sub,
            native_value_number_sub,
        ),
        (
            math_symbol("mul"),
            native_number_mul,
            native_value_number_mul,
        ),
        (
            math_symbol("div"),
            native_number_div,
            native_value_number_div,
        ),
    ] {
        linker.number_binary_op(NumberBinaryOp {
            operator: operator.clone(),
            left_domain: domain.clone(),
            right_domain: domain.clone(),
            cost: 0,
            apply: literal_apply,
        });
        linker.value_number_binary_op(ValueNumberBinaryOp {
            operator,
            left_domain: domain.clone(),
            right_domain: domain.clone(),
            cost: 1,
            apply: value_apply,
        });
    }

    linker.number_unary_op(NumberUnaryOp {
        operator: math_symbol("neg"),
        operand_domain: domain.clone(),
        cost: 0,
        apply: native_number_neg,
    });
    linker.value_number_unary_op(ValueNumberUnaryOp {
        operator: math_symbol("neg"),
        operand_domain: domain.clone(),
        cost: 1,
        apply: native_value_number_neg,
    });

    for (operator, literal_apply, value_apply) in [
        (
            math_symbol("sum"),
            native_number_sum as fn(&mut Cx, Vec<NumberLiteral>) -> Result<Value>,
            native_value_number_sum as fn(&mut Cx, Vec<Value>) -> Result<Value>,
        ),
        (
            math_symbol("product"),
            native_number_product,
            native_value_number_product,
        ),
    ] {
        linker.number_reduction_op(NumberReductionOp {
            operator: operator.clone(),
            operand_domain: domain.clone(),
            cost: 0,
            apply: literal_apply,
        });
        linker.value_number_reduction_op(ValueNumberReductionOp {
            operator,
            operand_domain: domain.clone(),
            cost: 1,
            apply: value_apply,
        });
    }
}

fn math_symbol(name: &str) -> Symbol {
    Symbol::qualified("math", name)
}

fn native_number_add(cx: &mut Cx, left: NumberLiteral, right: NumberLiteral) -> Result<Value> {
    native_binary_number_op(cx, "add", left, right)
}

fn native_number_sub(cx: &mut Cx, left: NumberLiteral, right: NumberLiteral) -> Result<Value> {
    native_binary_number_op(cx, "sub", left, right)
}

fn native_number_mul(cx: &mut Cx, left: NumberLiteral, right: NumberLiteral) -> Result<Value> {
    native_binary_number_op(cx, "mul", left, right)
}

fn native_number_div(cx: &mut Cx, left: NumberLiteral, right: NumberLiteral) -> Result<Value> {
    native_binary_number_op(cx, "div", left, right)
}

fn native_number_neg(cx: &mut Cx, operand: NumberLiteral) -> Result<Value> {
    native_unary_number_op(cx, "neg", operand)
}

fn native_number_sum(cx: &mut Cx, operands: Vec<NumberLiteral>) -> Result<Value> {
    native_reduction_number_op(cx, "sum", operands)
}

fn native_number_product(cx: &mut Cx, operands: Vec<NumberLiteral>) -> Result<Value> {
    native_reduction_number_op(cx, "product", operands)
}

fn native_value_number_add(cx: &mut Cx, left: Value, right: Value) -> Result<Value> {
    let left = expect_literal(cx, left, "left")?;
    let right = expect_literal(cx, right, "right")?;
    native_number_add(cx, left, right)
}

fn native_value_number_sub(cx: &mut Cx, left: Value, right: Value) -> Result<Value> {
    let left = expect_literal(cx, left, "left")?;
    let right = expect_literal(cx, right, "right")?;
    native_number_sub(cx, left, right)
}

fn native_value_number_mul(cx: &mut Cx, left: Value, right: Value) -> Result<Value> {
    let left = expect_literal(cx, left, "left")?;
    let right = expect_literal(cx, right, "right")?;
    native_number_mul(cx, left, right)
}

fn native_value_number_div(cx: &mut Cx, left: Value, right: Value) -> Result<Value> {
    let left = expect_literal(cx, left, "left")?;
    let right = expect_literal(cx, right, "right")?;
    native_number_div(cx, left, right)
}

fn native_value_number_neg(cx: &mut Cx, operand: Value) -> Result<Value> {
    let operand = expect_literal(cx, operand, "operand")?;
    native_number_neg(cx, operand)
}

fn native_value_number_sum(cx: &mut Cx, operands: Vec<Value>) -> Result<Value> {
    let operands = expect_literals(cx, operands, "operand")?;
    native_number_sum(cx, operands)
}

fn native_value_number_product(cx: &mut Cx, operands: Vec<Value>) -> Result<Value> {
    let operands = expect_literals(cx, operands, "operand")?;
    native_number_product(cx, operands)
}

fn native_binary_number_op(
    cx: &mut Cx,
    op: &str,
    left: NumberLiteral,
    right: NumberLiteral,
) -> Result<Value> {
    if left.domain != right.domain {
        return Err(sim_kernel::Error::Eval(format!(
            "native number op {op} requires matching domains, got {} and {}",
            left.domain, right.domain
        )));
    }
    invoke_native_number_op(cx, &left.domain.clone(), op, &[left, right])
}

fn native_unary_number_op(cx: &mut Cx, op: &str, operand: NumberLiteral) -> Result<Value> {
    invoke_native_number_op(cx, &operand.domain.clone(), op, &[operand])
}

fn native_reduction_number_op(
    cx: &mut Cx,
    op: &str,
    operands: Vec<NumberLiteral>,
) -> Result<Value> {
    let Some(first) = operands.first() else {
        return Err(sim_kernel::Error::Eval(format!(
            "native number op {op} requires at least one operand"
        )));
    };
    let domain = first.domain.clone();
    if operands.iter().any(|operand| operand.domain != domain) {
        return Err(sim_kernel::Error::Eval(format!(
            "native number op {op} requires matching operand domains"
        )));
    }
    invoke_native_number_op(cx, &domain, op, &operands)
}

fn invoke_native_number_op(
    cx: &mut Cx,
    domain: &Symbol,
    op: &str,
    operands: &[NumberLiteral],
) -> Result<Value> {
    let domain_value = cx
        .registry()
        .number_domain_by_symbol(domain)
        .cloned()
        .ok_or_else(|| sim_kernel::Error::UnknownSymbol {
            symbol: domain.clone(),
        })?;
    let Some(native_domain) = domain_value
        .object()
        .downcast_ref::<NativeAbiNumberDomain>()
    else {
        return Err(sim_kernel::Error::HostError(format!(
            "number domain {domain} is not a native ABI proxy"
        )));
    };
    let args = Expr::List(operands.iter().cloned().map(Expr::Number).collect());
    expr_to_value(cx, native_domain.invoke_expr(op, &args)?)
}

fn expr_to_value(cx: &mut Cx, expr: Expr) -> Result<Value> {
    match expr {
        Expr::Number(number) => cx.factory().number_literal(number.domain, number.canonical),
        other => cx.factory().expr(other),
    }
}

fn expect_literal(cx: &mut Cx, value: Value, side: &str) -> Result<NumberLiteral> {
    cx.number_value_ref(value)?
        .and_then(|number| number.literal)
        .ok_or_else(|| {
            sim_kernel::Error::Eval(format!(
                "native number op {side} operand has no literal representation"
            ))
        })
}

fn expect_literals(cx: &mut Cx, values: Vec<Value>, side: &str) -> Result<Vec<NumberLiteral>> {
    values
        .into_iter()
        .map(|value| expect_literal(cx, value, side))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use sim_kernel::{DefaultFactory, NoopEvalPolicy};

    struct MockGuest;

    impl NativeGuest for MockGuest {
        fn invoke(&self, op: &str, args: &[u8]) -> Result<Vec<u8>> {
            let (_, expr) = sim_codec_binary::decode_frame(sim_kernel::CodecId(0), args)?;
            let out = match op {
                "numbers/f64/parse-literal" => match expr {
                    Expr::String(text) if text == "1.5" => Expr::Number(NumberLiteral {
                        domain: Symbol::qualified("numbers", "f64"),
                        canonical: text,
                    }),
                    Expr::String(text) if text == "bad-shape" => Expr::String(text),
                    Expr::String(_) => Expr::Nil,
                    other => panic!("unexpected parse input {other:?}"),
                },
                "numbers/f64/encode-literal" => expr,
                other => panic!("unexpected op {other}"),
            };
            Ok(sim_codec_binary::encode_frame(&out)?.0)
        }
    }

    #[test]
    fn native_number_domain_proxy_marshals_parse_and_encode() {
        let domain = NativeAbiNumberDomain {
            guest: Arc::new(MockGuest),
            symbol: Symbol::qualified("numbers", "f64"),
        };
        let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));

        let parsed = domain.parse_literal(&mut cx, "1.5").unwrap().unwrap();
        assert_eq!(
            parsed.object().as_expr(&mut cx).unwrap(),
            Expr::Number(NumberLiteral {
                domain: Symbol::qualified("numbers", "f64"),
                canonical: "1.5".to_owned(),
            })
        );
        assert!(domain.parse_literal(&mut cx, "nope").unwrap().is_none());
        assert!(matches!(
            domain.parse_literal(&mut cx, "bad-shape").unwrap_err(),
            sim_kernel::Error::HostError(message)
                if message.contains("parse-literal returned non-number")
        ));

        let encoded = domain.encode_literal(&mut cx, parsed).unwrap().unwrap();
        assert_eq!(encoded.domain, Symbol::qualified("numbers", "f64"));
        assert_eq!(encoded.canonical, "1.5");
    }
}