omena-scss-eval 0.3.0

Oracle-first SCSS and Less value evaluator rail for Omena CSS
Documentation
use omena_abstract_value::{
    AbstractCssValueV0, BoundedJoinFixpointNodeV0, MAX_FLOW_ANALYSIS_ITERATIONS,
    analyze_bounded_join_fixpoint, join_abstract_css_values,
};

use crate::{abstract_css_value_kind, value_eval::static_scss_literal_truthiness};

use super::{
    ScssControlFlowAnalysisNode,
    model::OmenaScssEvalControlFlowBindingValueV0,
    typed_truthiness::{ScssTruthinessConsumer, typed_truthiness_label},
};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum ScssControlFlowTransfer {
    BranchCondition {
        value: AbstractCssValueV0,
    },
    LoopCondition {
        bindings: Vec<ScssControlFlowBindingValue>,
        value: AbstractCssValueV0,
    },
    LoopCarried {
        bindings: Vec<ScssControlFlowBindingValue>,
        value: AbstractCssValueV0,
    },
    PassThrough,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ScssControlFlowBindingValue {
    pub(super) name: String,
    pub(super) value: AbstractCssValueV0,
}

impl ScssControlFlowTransfer {
    pub(super) const fn kind_label(&self) -> &'static str {
        match self {
            Self::BranchCondition { .. } => "branchCondition",
            Self::LoopCondition { .. } => "loopCondition",
            Self::LoopCarried { .. } => "loopCarriedBindings",
            Self::PassThrough => "passThrough",
        }
    }

    pub(super) fn loop_carried_bindings(&self) -> Vec<String> {
        match self {
            Self::LoopCondition { bindings, .. } | Self::LoopCarried { bindings, .. } => bindings
                .iter()
                .map(|binding| binding.name.clone())
                .collect(),
            Self::BranchCondition { .. } | Self::PassThrough => Vec::new(),
        }
    }

    pub(super) fn loop_carried_binding_values(
        &self,
    ) -> Vec<OmenaScssEvalControlFlowBindingValueV0> {
        match self {
            Self::LoopCondition { bindings, .. } | Self::LoopCarried { bindings, .. } => bindings
                .iter()
                .map(|binding| OmenaScssEvalControlFlowBindingValueV0 {
                    name: binding.name.clone(),
                    value_kind: abstract_css_value_kind(&binding.value),
                    value: binding.value.clone(),
                })
                .collect(),
            Self::BranchCondition { .. } | Self::PassThrough => Vec::new(),
        }
    }

    pub(super) fn transfer_value(&self) -> Option<AbstractCssValueV0> {
        match self {
            Self::BranchCondition { value }
            | Self::LoopCondition { value, .. }
            | Self::LoopCarried { value, .. } => Some(value.clone()),
            Self::PassThrough => None,
        }
    }

    pub(super) fn transfer_truthiness(
        &self,
        consumer: ScssTruthinessConsumer,
    ) -> Option<&'static str> {
        match self {
            Self::BranchCondition { value } | Self::LoopCondition { value, .. } => {
                truthiness_label_for_consumer(value, consumer)
            }
            Self::LoopCarried { .. } | Self::PassThrough => None,
        }
    }

    fn apply(&self, input_value: &AbstractCssValueV0) -> AbstractCssValueV0 {
        match self {
            Self::BranchCondition { value }
            | Self::LoopCondition { value, .. }
            | Self::LoopCarried { value, .. } => join_abstract_css_values(input_value, value),
            Self::PassThrough => input_value.clone(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct ScssControlFlowFixpointResult {
    pub(super) converged: bool,
    pub(super) iteration_count: usize,
    pub(super) widened_to_top_count: usize,
    pub(super) input_values: Vec<AbstractCssValueV0>,
    pub(super) output_values: Vec<AbstractCssValueV0>,
}

pub(super) fn run_scss_control_flow_fixpoint(
    nodes: &[ScssControlFlowAnalysisNode],
) -> ScssControlFlowFixpointResult {
    let flow_nodes = nodes
        .iter()
        .map(|node| BoundedJoinFixpointNodeV0 {
            id: node.block.node_key.0.clone(),
            predecessor_ids: node
                .predecessor_indices
                .iter()
                .filter_map(|index| nodes.get(*index).map(|node| node.block.node_key.0.clone()))
                .collect(),
            transfer: node.transfer.clone(),
        })
        .collect::<Vec<_>>();
    let fixpoint = analyze_bounded_join_fixpoint(
        &flow_nodes,
        MAX_FLOW_ANALYSIS_ITERATIONS,
        AbstractCssValueV0::Bottom,
        AbstractCssValueV0::Top,
        join_abstract_css_values,
        |input_value, transfer| transfer.apply(input_value),
    );
    let input_values = fixpoint
        .nodes
        .iter()
        .map(|node| node.input_value.clone())
        .collect::<Vec<_>>();
    let mut output_values = fixpoint
        .nodes
        .iter()
        .map(|node| node.output_value.clone())
        .collect::<Vec<_>>();
    let widened_to_top_count = if fixpoint.converged {
        0
    } else {
        output_values
            .iter_mut()
            .filter(|value| !matches!(value, AbstractCssValueV0::Top))
            .map(|value| {
                *value = AbstractCssValueV0::Top;
            })
            .count()
    };

    ScssControlFlowFixpointResult {
        converged: fixpoint.converged,
        iteration_count: fixpoint.iteration_count,
        widened_to_top_count,
        input_values,
        output_values,
    }
}

pub(super) fn truthiness_label_for_consumer(
    value: &AbstractCssValueV0,
    consumer: ScssTruthinessConsumer,
) -> Option<&'static str> {
    match consumer {
        ScssTruthinessConsumer::StringLattice => scss_static_truthiness_label(value),
        ScssTruthinessConsumer::TypedPayload => {
            typed_truthiness_label(value).or_else(|| scss_static_truthiness_label(value))
        }
    }
}

pub(super) fn scss_static_truthiness_label(value: &AbstractCssValueV0) -> Option<&'static str> {
    match value {
        AbstractCssValueV0::Exact { value, .. } => scss_static_truthiness_label_from_text(value),
        AbstractCssValueV0::FiniteSet { values, .. } => {
            let mut truthiness = values
                .iter()
                .filter_map(|value| {
                    scss_static_truthiness_label(&AbstractCssValueV0::Exact {
                        typed: None,
                        value: value.clone(),
                    })
                })
                .collect::<Vec<_>>();
            truthiness.sort_unstable();
            truthiness.dedup();
            (truthiness.len() == 1).then_some(truthiness[0])
        }
        AbstractCssValueV0::Raw { value } => scss_static_truthiness_label_from_text(value),
        AbstractCssValueV0::Bottom | AbstractCssValueV0::Top => None,
    }
}

fn scss_static_truthiness_label_from_text(value: &str) -> Option<&'static str> {
    static_scss_literal_truthiness(value).map(|truthy| if truthy { "truthy" } else { "falsey" })
}