sim-expr-tree-calc 0.2.0

Bounded incremental calculation of ordinary SIM expressions and values.
Documentation
use super::*;

pub(super) fn encode_node(
    node: &SnapshotNode<CalcQuery, MemoValue>,
    cx: &mut Cx,
) -> Result<Expr, DerivedSnapshotError> {
    Ok(record(vec![
        ("key", encode_query(&node.key)),
        ("revision", number(node.revision.get())),
        ("dirty", Expr::Bool(node.dirty)),
        ("value", encode_optional_memo(node.value.as_ref(), cx)?),
        (
            "fingerprint",
            optional_number(node.fingerprint.map(ValueFingerprint::get)),
        ),
        (
            "observations",
            Expr::Vector(node.dependencies.iter().map(encode_observation).collect()),
        ),
    ]))
}

pub(super) fn decode_node(
    expr: &Expr,
    cx: &mut Cx,
) -> DecodeResult<SnapshotNode<CalcQuery, MemoValue>> {
    let fields = record_fields(expr)?;
    let (value, reusable) = decode_optional_memo(required(&fields, "value")?, cx)?;
    Ok(SnapshotNode {
        key: decode_query(required(&fields, "key")?)?,
        revision: Revision::new(parse_u64(required(&fields, "revision")?)?),
        dirty: parse_bool(required(&fields, "dirty")?)? || !reusable,
        value,
        fingerprint: parse_optional_u64(required(&fields, "fingerprint")?)?
            .map(ValueFingerprint::new),
        dependencies: vector(required(&fields, "observations")?)?
            .iter()
            .map(decode_observation)
            .collect::<DecodeResult<Vec<_>>>()?,
    })
}

fn encode_optional_memo(
    memo: Option<&MemoValue>,
    cx: &mut Cx,
) -> Result<Expr, DerivedSnapshotError> {
    let Some(memo) = memo else {
        return Ok(Expr::Nil);
    };
    match &memo.outcome {
        MemoOutcome::Value(value) => Ok(record(vec![
            ("kind", text("value")),
            ("expr", value.object().as_expr(cx).map_err(backend_error)?),
            ("volatile", Expr::Bool(memo.is_volatile())),
        ])),
        MemoOutcome::Failure(failure) => Ok(record(vec![
            ("kind", text("failure")),
            ("failure", encode_failure(failure)),
        ])),
    }
}

fn decode_optional_memo(expr: &Expr, cx: &mut Cx) -> DecodeResult<(Option<MemoValue>, bool)> {
    if matches!(expr, Expr::Nil) {
        return Ok((None, true));
    }
    let fields = record_fields(expr)?;
    match parse_text(required(&fields, "kind")?)? {
        "value" => {
            let projected = required(&fields, "expr")?.clone();
            let (value, structurally_reusable) = restore_value(cx, projected.clone())
                .map_err(|error| DecodeError::Corrupt(error.to_string()))?;
            let volatile = parse_bool(required(&fields, "volatile")?)?;
            let memo = if volatile {
                MemoValue::volatile(value, 0)
            } else {
                MemoValue::canonical(value, projected.canonical_key())
            };
            Ok((Some(memo), structurally_reusable && !volatile))
        }
        "failure" => Ok((
            Some(MemoValue::failure(decode_failure(required(
                &fields, "failure",
            )?)?)),
            true,
        )),
        other => corrupt(format!("unknown memo kind {other:?}")),
    }
}

pub(in crate::calc::persistence) fn restore_value(
    cx: &mut Cx,
    expr: Expr,
) -> sim_kernel::Result<(Value, bool)> {
    match expr {
        Expr::Nil => cx.factory().nil().map(|value| (value, true)),
        Expr::Bool(raw) => cx.factory().bool(raw).map(|value| (value, true)),
        Expr::Number(raw) => cx
            .factory()
            .number_literal(raw.domain, raw.canonical)
            .map(|value| (value, true)),
        Expr::Symbol(raw) => cx.factory().symbol(raw).map(|value| (value, true)),
        Expr::String(raw) => cx.factory().string(raw).map(|value| (value, true)),
        Expr::Bytes(raw) => cx.factory().bytes(raw).map(|value| (value, true)),
        Expr::List(items) => {
            let mut reusable = true;
            let values = items
                .into_iter()
                .map(|item| {
                    let (value, item_reusable) = restore_value(cx, item)?;
                    reusable &= item_reusable;
                    Ok(value)
                })
                .collect::<sim_kernel::Result<Vec<_>>>()?;
            cx.factory().list(values).map(|value| (value, reusable))
        }
        Expr::Map(entries) => {
            let mut reusable = true;
            let values = entries
                .into_iter()
                .map(|(key, value)| {
                    let Expr::Symbol(key) = key else {
                        return Err(sim_kernel::Error::Eval(
                            "persisted value map key is not a symbol".to_owned(),
                        ));
                    };
                    let (value, item_reusable) = restore_value(cx, value)?;
                    reusable &= item_reusable;
                    Ok((key, value))
                })
                .collect::<sim_kernel::Result<Vec<_>>>()?;
            cx.factory().table(values).map(|value| (value, reusable))
        }
        other => cx.factory().expr(other).map(|value| (value, false)),
    }
}

fn encode_observation(observation: &Observation<CalcQuery>) -> Expr {
    record(vec![
        ("key", encode_query(observation.key())),
        ("kind", text(observation_kind_name(observation.kind()))),
        ("revision", number(observation.revision().get())),
        (
            "fingerprint",
            optional_number(observation.fingerprint().map(ValueFingerprint::get)),
        ),
    ])
}

fn decode_observation(expr: &Expr) -> DecodeResult<Observation<CalcQuery>> {
    let fields = record_fields(expr)?;
    Ok(Observation::new(
        decode_query(required(&fields, "key")?)?,
        decode_observation_kind(parse_text(required(&fields, "kind")?)?)?,
        Revision::new(parse_u64(required(&fields, "revision")?)?),
        parse_optional_u64(required(&fields, "fingerprint")?)?.map(ValueFingerprint::new),
    ))
}

pub(super) fn observation_kind_name(kind: &ObservationKind) -> &'static str {
    match kind {
        ObservationKind::Read => "read",
        ObservationKind::Missing => "missing",
        ObservationKind::Listing => "listing",
        ObservationKind::Policy => "policy",
        ObservationKind::Epoch => "epoch",
        ObservationKind::Custom("cell-source") => "custom:cell-source",
        ObservationKind::Custom("lexical-binding") => "custom:lexical-binding",
        ObservationKind::Custom("codec-registry") => "custom:codec-registry",
        ObservationKind::Custom("force-epoch") => "custom:force-epoch",
        ObservationKind::Custom("lookup-step") => "custom:lookup-step",
        ObservationKind::Custom(_) => "custom:unsupported",
    }
}

pub(super) fn decode_observation_kind(name: &str) -> DecodeResult<ObservationKind> {
    Ok(match name {
        "read" => ObservationKind::Read,
        "missing" => ObservationKind::Missing,
        "listing" => ObservationKind::Listing,
        "policy" => ObservationKind::Policy,
        "epoch" => ObservationKind::Epoch,
        "custom:cell-source" => ObservationKind::Custom("cell-source"),
        "custom:lexical-binding" => ObservationKind::Custom("lexical-binding"),
        "custom:codec-registry" => ObservationKind::Custom("codec-registry"),
        "custom:force-epoch" => ObservationKind::Custom("force-epoch"),
        "custom:lookup-step" => ObservationKind::Custom("lookup-step"),
        other => return corrupt(format!("unknown observation kind {other:?}")),
    })
}

pub(super) fn encode_query(query: &CalcQuery) -> Expr {
    let (kind, argument) = match query {
        CalcQuery::Cell(value) => ("cell", Some(value.as_str())),
        CalcQuery::NameSlot(value) => ("name-slot", Some(value.as_str())),
        CalcQuery::LookupStep(value) => ("lookup-step", Some(value.as_str())),
        CalcQuery::Listing(value) => ("listing", Some(value.as_str())),
        CalcQuery::MountEpoch(value) => ("mount-epoch", Some(value.as_str())),
        CalcQuery::EffectivePolicy(value) => ("effective-policy", Some(value.as_str())),
        CalcQuery::AuthorityPolicy(value) => ("authority-policy", Some(value.as_str())),
        CalcQuery::CodecRegistry => ("codec-registry", None),
        CalcQuery::AuthorityCeiling => ("authority-ceiling", None),
        CalcQuery::ForceEpoch(value) => ("force-epoch", Some(value.as_str())),
    };
    record(vec![
        ("kind", text(kind)),
        (
            "argument",
            argument.map_or(Expr::Nil, |value| Expr::String(value.to_owned())),
        ),
    ])
}

pub(super) fn decode_query(expr: &Expr) -> DecodeResult<CalcQuery> {
    let fields = record_fields(expr)?;
    let kind = parse_text(required(&fields, "kind")?)?;
    let argument = match required(&fields, "argument")? {
        Expr::Nil => None,
        Expr::String(value) => Some(value.clone()),
        _ => return corrupt("query argument is not text or nil"),
    };
    let required_argument = || {
        argument
            .clone()
            .ok_or_else(|| DecodeError::Corrupt(format!("{kind} query has no argument")))
    };
    Ok(match kind {
        "cell" => CalcQuery::Cell(required_argument()?),
        "name-slot" => CalcQuery::NameSlot(required_argument()?),
        "lookup-step" => CalcQuery::LookupStep(required_argument()?),
        "listing" => CalcQuery::Listing(required_argument()?),
        "mount-epoch" => CalcQuery::MountEpoch(required_argument()?),
        "effective-policy" => CalcQuery::EffectivePolicy(required_argument()?),
        "authority-policy" => CalcQuery::AuthorityPolicy(required_argument()?),
        "codec-registry" if argument.is_none() => CalcQuery::CodecRegistry,
        "authority-ceiling" if argument.is_none() => CalcQuery::AuthorityCeiling,
        "force-epoch" => CalcQuery::ForceEpoch(required_argument()?),
        other => return corrupt(format!("unknown query kind {other:?}")),
    })
}

pub(super) fn encode_reverse(reverse: &BTreeMap<CalcQuery, BTreeSet<CalcQuery>>) -> Expr {
    Expr::Vector(
        reverse
            .iter()
            .map(|(key, dependents)| {
                record(vec![
                    ("key", encode_query(key)),
                    (
                        "dependents",
                        Expr::Vector(dependents.iter().map(encode_query).collect()),
                    ),
                ])
            })
            .collect(),
    )
}

pub(super) fn decode_reverse(
    expr: &Expr,
) -> DecodeResult<BTreeMap<CalcQuery, BTreeSet<CalcQuery>>> {
    let mut reverse = BTreeMap::new();
    for row in vector(expr)? {
        let fields = record_fields(row)?;
        let key = decode_query(required(&fields, "key")?)?;
        let dependents = vector(required(&fields, "dependents")?)?
            .iter()
            .map(decode_query)
            .collect::<DecodeResult<BTreeSet<_>>>()?;
        if reverse.insert(key.clone(), dependents).is_some() {
            return corrupt(format!("duplicate reverse-edge key {key:?}"));
        }
    }
    Ok(reverse)
}

fn encode_failure(failure: &CellFailure) -> Expr {
    match failure {
        CellFailure::Evaluation { message } => record(vec![
            ("kind", text("evaluation")),
            ("message", text(message)),
        ]),
        CellFailure::Cycle { path } => record(vec![
            ("kind", text("cycle")),
            (
                "path",
                Expr::Vector(path.iter().map(encode_query).collect()),
            ),
        ]),
        CellFailure::ExpressionDepth { limit } => record(vec![
            ("kind", text("expression-depth")),
            ("limit", number(*limit as u64)),
        ]),
        CellFailure::Blocked { path, reason } => record(vec![
            ("kind", text("blocked")),
            ("path", text(path)),
            ("reason", text(reason)),
        ]),
        CellFailure::RequiredCapability { path, capability } => record(vec![
            ("kind", text("required-capability")),
            ("path", text(path)),
            ("capability", text(capability.as_str())),
        ]),
    }
}

fn decode_failure(expr: &Expr) -> DecodeResult<CellFailure> {
    let fields = record_fields(expr)?;
    Ok(match parse_text(required(&fields, "kind")?)? {
        "evaluation" => CellFailure::Evaluation {
            message: parse_text(required(&fields, "message")?)?.to_owned(),
        },
        "cycle" => CellFailure::Cycle {
            path: vector(required(&fields, "path")?)?
                .iter()
                .map(decode_query)
                .collect::<DecodeResult<Vec<_>>>()?,
        },
        "expression-depth" => CellFailure::ExpressionDepth {
            limit: parse_usize(required(&fields, "limit")?)?,
        },
        "blocked" => CellFailure::Blocked {
            path: parse_text(required(&fields, "path")?)?.to_owned(),
            reason: parse_text(required(&fields, "reason")?)?.to_owned(),
        },
        "required-capability" => CellFailure::RequiredCapability {
            path: parse_text(required(&fields, "path")?)?.to_owned(),
            capability: sim_kernel::CapabilityName::new(parse_text(required(
                &fields,
                "capability",
            )?)?),
        },
        other => return corrupt(format!("unknown cell failure {other:?}")),
    })
}