kcl-lib 0.2.158

KittyCAD Language implementation and tools
Documentation
use crate::SourceRange;
use crate::errors::KclError;
use crate::errors::KclErrorDetails;
use crate::execution::ConsumedSolidInfo;
use crate::execution::ConsumedSolidKey;
use crate::execution::ConsumedSolidOperation;
use crate::execution::ExecState;
use crate::execution::KclValue;
use crate::execution::Solid;

pub(crate) fn validate_value_not_consumed(
    value: &KclValue,
    exec_state: &ExecState,
    source_range: SourceRange,
) -> Result<(), KclError> {
    match value {
        KclValue::Solid { value } => validate_solid_not_consumed(value, exec_state, source_range),
        KclValue::HomArray { value, .. } | KclValue::Tuple { value, .. } => value
            .iter()
            .try_for_each(|v| validate_value_not_consumed(v, exec_state, source_range)),
        KclValue::Object { value, .. } => value
            .values()
            .try_for_each(|v| validate_value_not_consumed(v, exec_state, source_range)),
        _ => Ok(()),
    }
}

pub(super) fn validate_solids_not_consumed(
    solids: &[Solid],
    exec_state: &ExecState,
    source_range: SourceRange,
) -> Result<(), KclError> {
    solids
        .iter()
        .try_for_each(|solid| validate_solid_not_consumed(solid, exec_state, source_range))
}

fn validate_solid_not_consumed(
    solid: &Solid,
    exec_state: &ExecState,
    source_range: SourceRange,
) -> Result<(), KclError> {
    let key = consumed_solid_key(solid);
    let Some(info) = exec_state.check_solid_consumed(&key) else {
        if let Some(info) = exec_state.check_solid_id_consumed(&solid.id)
            && info.should_report_reused_engine_id_as_consumed(key)
        {
            let operation = info.operation();
            let current_var = exec_state.find_var_name_for_solid_key(key);
            let output_var = exec_state
                .latest_consumed_output(info.suggested_replacement_key())
                .and_then(|key| exec_state.find_var_name_for_solid_key(key));
            let message = build_stale_body_error_message(current_var.as_deref(), operation, output_var.as_deref());

            return Err(KclError::new_semantic(KclErrorDetails::new(
                message,
                vec![source_range],
            )));
        }

        return Ok(());
    };
    let operation = info.operation();
    let suggested_replacement_key = info.suggested_replacement_key();
    let consumed_var = exec_state.find_var_name_for_solid_key(key);
    let output_var = exec_state
        .latest_consumed_output(suggested_replacement_key)
        .and_then(|key| exec_state.find_var_name_for_solid_key(key));
    let message = build_consumed_error_message(consumed_var.as_deref(), operation, output_var.as_deref());

    Err(KclError::new_semantic(KclErrorDetails::new(
        message,
        vec![source_range],
    )))
}

pub(super) fn record_consumed_solids(
    exec_state: &mut ExecState,
    solids: &[Solid],
    operation: ConsumedSolidOperation,
    output_solids: &[Solid],
) {
    let returned_solid_keys = output_solids.iter().map(consumed_solid_key).collect::<Vec<_>>();
    for solid in solids {
        let info = ConsumedSolidInfo::new(operation, returned_solid_keys.clone());
        exec_state.mark_solid_consumed(consumed_solid_key(solid), info.clone());
        exec_state.mark_solid_id_consumed(solid.id, info);
    }
}

fn consumed_solid_key(solid: &Solid) -> ConsumedSolidKey {
    ConsumedSolidKey::new(solid.id, solid.value_id)
}

fn build_consumed_error_message(
    consumed_var: Option<&str>,
    operation: ConsumedSolidOperation,
    output_var: Option<&str>,
) -> String {
    let article = operation.indefinite_article();

    match (consumed_var, output_var) {
        (Some(consumed), Some(output)) => format!(
            "`{consumed}` was already consumed by {article} `{operation}` operation. \
             The operation result is now in `{output}`; use that for subsequent operations."
        ),
        (Some(consumed), None) => format!(
            "`{consumed}` was already consumed by {article} `{operation}` operation \
             and can no longer be used. Some operations destroy their inputs; \
             assign the result to a variable and use it for subsequent operations."
        ),
        (None, Some(output)) => format!(
            "A solid was already consumed by {article} `{operation}` operation. \
             The operation result is now in `{output}`; use that for subsequent operations."
        ),
        (None, None) => format!(
            "A solid was already consumed by {article} `{operation}` operation \
             and can no longer be used. Some operations destroy their inputs; \
             assign the result to a variable and use it for subsequent operations."
        ),
    }
}

fn build_stale_body_error_message(
    current_var: Option<&str>,
    operation: ConsumedSolidOperation,
    output_var: Option<&str>,
) -> String {
    let article = operation.indefinite_article();

    match (current_var, output_var) {
        (Some(current), Some(output)) => format!(
            "`{current}` refers to a solid body that was already consumed by {article} `{operation}` operation. \
             The operation result is now in `{output}`; use that for subsequent operations."
        ),
        (Some(current), None) => format!(
            "`{current}` refers to a solid body that was already consumed by {article} `{operation}` operation \
             and can no longer be used."
        ),
        (None, Some(output)) => format!(
            "A solid body was already consumed by {article} `{operation}` operation. \
             The operation result is now in `{output}`; use that for subsequent operations."
        ),
        (None, None) => {
            format!("A solid body was already consumed by {article} `{operation}` operation and can no longer be used.")
        }
    }
}

#[cfg(test)]
mod tests {
    use kittycad_modeling_cmds::units::UnitLength;
    use uuid::Uuid;

    use super::*;
    use crate::MockConfig;
    use crate::execution::ArtifactId;
    use crate::execution::SolidCreator;

    fn procedural_solid(id: Uuid, value_id: Uuid) -> Solid {
        Solid {
            id,
            value_id,
            artifact_id: ArtifactId::new(id),
            value: vec![],
            creator: SolidCreator::Procedural,
            start_cap_id: None,
            end_cap_id: None,
            edge_cuts: vec![],
            units: UnitLength::Millimeters,
            sectional: false,
            meta: vec![SourceRange::default().into()],
        }
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn replacement_solid_reusing_consumed_engine_id_remains_usable() {
        let ctx = crate::ExecutorContext::new_mock(None).await;
        let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
        let engine_id = Uuid::from_u128(1);
        let consumed = procedural_solid(engine_id, Uuid::from_u128(2));
        let output = procedural_solid(engine_id, Uuid::from_u128(3));

        // Boolean operations may return a replacement KCL solid that reuses an
        // engine body id from one of the consumed inputs.
        record_consumed_solids(
            &mut exec_state,
            std::slice::from_ref(&consumed),
            ConsumedSolidOperation::Subtract,
            std::slice::from_ref(&output),
        );

        validate_solids_not_consumed(std::slice::from_ref(&output), &exec_state, SourceRange::default())
            .expect("replacement output should remain usable");
        assert!(
            validate_solids_not_consumed(std::slice::from_ref(&consumed), &exec_state, SourceRange::default()).is_err()
        );

        ctx.close().await;
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn stale_engine_id_on_distinct_value_is_rejected() {
        let ctx = crate::ExecutorContext::new_mock(None).await;
        let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
        let consumed = procedural_solid(Uuid::from_u128(1), Uuid::from_u128(2));
        let output = procedural_solid(Uuid::from_u128(3), Uuid::from_u128(3));
        let stale_alias = procedural_solid(consumed.id, Uuid::from_u128(4));

        record_consumed_solids(
            &mut exec_state,
            std::slice::from_ref(&consumed),
            ConsumedSolidOperation::Subtract,
            std::slice::from_ref(&output),
        );

        let err = validate_solids_not_consumed(std::slice::from_ref(&stale_alias), &exec_state, SourceRange::default())
            .expect_err("stale engine body id should be rejected before the engine sees it");
        assert!(
            err.message()
                .contains("A solid body was already consumed by a `subtract` operation"),
            "{err:?}"
        );

        ctx.close().await;
    }

    #[tokio::test(flavor = "multi_thread")]
    async fn extra_replacement_solid_reusing_consumed_engine_id_remains_usable() {
        let ctx = crate::ExecutorContext::new_mock(None).await;
        let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
        let consumed = procedural_solid(Uuid::from_u128(1), Uuid::from_u128(2));
        let primary_output = procedural_solid(Uuid::from_u128(3), Uuid::from_u128(3));
        let extra_output = procedural_solid(consumed.id, Uuid::from_u128(3));

        record_consumed_solids(
            &mut exec_state,
            std::slice::from_ref(&consumed),
            ConsumedSolidOperation::Subtract,
            &[primary_output, extra_output.clone()],
        );

        validate_solids_not_consumed(std::slice::from_ref(&extra_output), &exec_state, SourceRange::default())
            .expect("any replacement output should remain usable");

        ctx.close().await;
    }
}