geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
use super::{
    write_binary, write_call, write_constant, write_function_call, write_length, write_literal,
    write_projection, write_unary,
};
use crate::plan::execution::constant::ConstantId;
use crate::plan::execution::explain::{Explain, ExplainContext};
use crate::plan::execution::function::BoolFunctionId;
use crate::plan::execution::graph::LocalLabel;
use crate::plan::execution::graph::{
    BoolFunctionLocalId, BoolListLocalId, BoolLocalId, CustomLocal, FloatLocalId, IntLocalId,
    ListLocal, ParamLocal, StringLocalId, TupleLocalId,
};
use ecow::EcoString;

pub(crate) enum BoolInstruction {
    Value(bool),
    Constant(ConstantId<BoolLocalId>),
    Call {
        function: BoolFunctionId,
        args: Box<[ParamLocal]>,
        site: crate::plan::HostCallSite,
    },
    FunctionCall {
        function: BoolFunctionLocalId,
        args: Box<[ParamLocal]>,
        site: crate::plan::HostCallSite,
    },
    TupleIndex {
        tuple: TupleLocalId,
        index: usize,
    },
    CustomField {
        source: CustomLocal,
        index: usize,
    },
    ListIndex {
        list: BoolListLocalId,
        index: usize,
    },
    Not(BoolLocalId),
    LtInt {
        left: IntLocalId,
        right: IntLocalId,
    },
    LtEqInt {
        left: IntLocalId,
        right: IntLocalId,
    },
    GtInt {
        left: IntLocalId,
        right: IntLocalId,
    },
    GtEqInt {
        left: IntLocalId,
        right: IntLocalId,
    },
    LtFloat {
        left: FloatLocalId,
        right: FloatLocalId,
    },
    LtEqFloat {
        left: FloatLocalId,
        right: FloatLocalId,
    },
    GtFloat {
        left: FloatLocalId,
        right: FloatLocalId,
    },
    GtEqFloat {
        left: FloatLocalId,
        right: FloatLocalId,
    },
    Equal {
        left: ParamLocal,
        right: ParamLocal,
    },
    NotEqual {
        left: ParamLocal,
        right: ParamLocal,
    },
    StringStartsWith {
        value: StringLocalId,
        prefix: EcoString,
    },
    ListLengthEquals {
        value: ListLocal,
        length: usize,
    },
    ListLengthAtLeast {
        value: ListLocal,
        length: usize,
    },
}

impl Explain for BoolInstruction {
    fn write_explanation(&self, context: &mut ExplainContext<'_, '_>) {
        let output = context.output();
        match self {
            BoolInstruction::Value(value) => {
                write_literal(output, "bool.value", if *value { "True" } else { "False" });
            }
            BoolInstruction::Constant(id) => write_constant(output, "bool", *id),
            BoolInstruction::Call { function, args, .. } => {
                write_call(output, "bool.call", function, args);
            }
            BoolInstruction::FunctionCall { function, args, .. } => {
                write_function_call(output, "bool.function_call", function, args);
            }
            BoolInstruction::TupleIndex { tuple, index } => {
                write_projection(output, "bool.tuple_index", tuple, *index);
            }
            BoolInstruction::CustomField { source, index } => {
                write_projection(output, "bool.custom_field", source, *index);
            }
            BoolInstruction::ListIndex { list, index } => {
                write_projection(output, "bool.list_index", list, *index);
            }
            BoolInstruction::Not(value) => write_unary(output, "bool.not", value),
            BoolInstruction::LtInt { left, right } => {
                write_binary(output, "bool.lt_int", left, right)
            }
            BoolInstruction::LtEqInt { left, right } => {
                write_binary(output, "bool.lte_int", left, right);
            }
            BoolInstruction::GtInt { left, right } => {
                write_binary(output, "bool.gt_int", left, right)
            }
            BoolInstruction::GtEqInt { left, right } => {
                write_binary(output, "bool.gte_int", left, right);
            }
            BoolInstruction::LtFloat { left, right } => {
                write_binary(output, "bool.lt_float", left, right);
            }
            BoolInstruction::LtEqFloat { left, right } => {
                write_binary(output, "bool.lte_float", left, right);
            }
            BoolInstruction::GtFloat { left, right } => {
                write_binary(output, "bool.gt_float", left, right);
            }
            BoolInstruction::GtEqFloat { left, right } => {
                write_binary(output, "bool.gte_float", left, right);
            }
            BoolInstruction::Equal { left, right } => {
                write_binary(output, "bool.equal", left, right)
            }
            BoolInstruction::NotEqual { left, right } => {
                write_binary(output, "bool.not_equal", left, right);
            }
            BoolInstruction::StringStartsWith { value, prefix } => {
                output.push_str("bool.string_starts_with ");
                value.write_local_label(output);
                output.push_str(" prefix=");
                output.push_str(&format!("{prefix:?}"));
            }
            BoolInstruction::ListLengthEquals { value, length } => {
                write_length(output, "bool.list_length_equals", value, *length);
            }
            BoolInstruction::ListLengthAtLeast { value, length } => {
                write_length(output, "bool.list_length_at_least", value, *length);
            }
        }
    }
}

#[cfg(test)]
mod explain_tests {
    use crate::plan::execution::explain;
    use crate::plan::execution::function::BoolFunctionId;
    use crate::plan::execution::graph::ProfiledInstructionKind;

    #[test]
    fn writes_bool_instruction_grammar() {
        let source = r#"
pub fn main() {
  let integer = 1
  let float = 1.0
  let values = [1]
  !True
  && integer < 2
  && integer <= 2
  && integer > 0
  && integer >= 0
  && float <. 2.0
  && float <=. 2.0
  && float >. 0.0
  && float >=. 0.0
  && integer == 1
  && integer != 2
  && values == [1]
}
"#;
        let expected = concat!(
            "bool.value True | bool.not %bool#0 | bool.lt_int %int#0 %int#1 | ",
            "bool.lte_int %int#0 %int#1 | bool.gt_int %int#0 %int#1 | ",
            "bool.gte_int %int#0 %int#1 | bool.lt_float %float#0 %float#1 | ",
            "bool.lte_float %float#0 %float#1 | bool.gt_float %float#0 %float#1 | ",
            "bool.gte_float %float#0 %float#1 | bool.equal %int#0 %int#1 | ",
            "bool.not_equal %int#0 %int#1 | bool.equal %list.int#0 %list.int#1 | ",
            "bool.value True | bool.value False",
        );

        assert_explanation(source, expected);
    }

    #[test]
    fn writes_bool_constants_calls_projections_and_pattern_checks() {
        let source = r#"
const saved = True

pub type Flag {
  Flag(value: Bool)
}

fn bool_value(value: Bool) { value }
fn bool_values(values: List(Bool)) { values }
fn string_value(value: String) { value }

pub fn main() {
  let function = bool_value
  let values = bool_values([True])
  let selected = case values {
    [value, ..] -> value
    _ -> False
  }
  let exact = case values {
    [value] -> value
    _ -> False
  }
  let tuple = #(True)
  let record = Flag(True)
  let text = string_value("prefix-tail")
  let prefix = case text {
    "prefix-" <> _ -> True
    _ -> False
  }

  saved
  && bool_value(True)
  && function(True)
  && tuple.0
  && record.value
  && selected
  && exact
  && prefix
}
"#;
        let expected = concat!(
            "bool.value True | bool.list_length_at_least %list.bool#1 length=1 | ",
            "bool.list_index %list.bool#0 index=0 | bool.value True | bool.value True | ",
            "bool.list_length_equals %list.bool#0 length=1 | ",
            "bool.list_index %list.bool#0 index=0 | bool.value True | bool.value True | ",
            "bool.value True | bool.value True | ",
            "bool.string_starts_with %string#1 prefix=\"prefix-\" | bool.value True | ",
            "bool.value True | constant.bool#0 | bool.value True | ",
            "bool.call bool#1 args=[%bool#3] | bool.value True | ",
            "bool.function_call %function.bool#0 args=[%bool#3] | ",
            "bool.tuple_index %tuple#0 index=0 | bool.custom_field %custom#0 index=0 | ",
            "bool.value True | bool.value False | bool.value False | bool.value False | ",
            "bool.value False | bool.value False | bool.value False | bool.value False | ",
            "bool.value False | bool.value False",
        );

        assert_explanation(source, expected);
    }

    fn assert_explanation(source: &str, expected: &str) {
        explain::assert_rendered(source, expected, |plan, output| {
            let graph = plan.bool_function(BoolFunctionId(0)).body().block_graph();
            let mut first = true;
            for instruction in graph.blocks().iter().flat_map(|block| block.instructions()) {
                if let ProfiledInstructionKind::Bool(instruction) = instruction.kind() {
                    if first {
                        first = false;
                    } else {
                        output.push_str(" | ");
                    }
                    let mut context = explain::ExplainContext::new(plan, output);
                    context.write(instruction);
                }
            }
        });
    }
}