geam 0.1.1

Experimental Rust-embedded execution runtime for typed Gleam programs
mod bit_array;
mod list;

pub(crate) use bit_array::{
    BitArrayBindingPattern, BitArrayPattern, BitArrayPatternSegment, BitArrayPatternSize,
    BitArrayPatternSizeExpr, BitArrayPatternValue, BitArrayStringPattern, Signedness,
};
pub(crate) use list::{MatchPatternList, MatchPatternListTail};

use crate::plan::execution::explain::{Explain, ExplainContext};
use crate::plan::execution::type_::CustomConstructorId;
use ecow::EcoString;
use num_bigint::BigInt;

pub(crate) enum MatchPattern {
    Bind(MatchPatternBinding),
    Discard,
    Int(BigInt),
    Float(f64),
    String(EcoString),
    Bool(bool),
    Nil,
    Tuple(Box<[MatchPattern]>),
    List(MatchPatternList),
    BitArray(BitArrayPattern),
    Custom {
        constructor: CustomConstructorId,
        fields: Box<[MatchPattern]>,
    },
    StringPrefix {
        prefix: EcoString,
        left: Option<MatchPatternBinding>,
        right: Option<MatchPatternBinding>,
    },
    Alias {
        pattern: Box<MatchPattern>,
        binding: MatchPatternBinding,
    },
}

pub(crate) struct MatchPatternBinding {
    index: usize,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) struct MatchIntBindingId(usize);

impl MatchPatternBinding {
    pub(in crate::plan::execution) fn new(index: usize) -> Self {
        Self { index }
    }

    pub(crate) fn int_id(&self) -> MatchIntBindingId {
        MatchIntBindingId(self.index)
    }

    pub(in crate::plan::execution) fn index(&self) -> usize {
        self.index
    }
}

impl MatchIntBindingId {
    pub(in crate::plan::execution) fn new(index: usize) -> Self {
        Self(index)
    }

    pub(in crate::plan::execution) fn index(self) -> usize {
        self.0
    }
}

impl Explain for MatchPattern {
    fn write_explanation(&self, context: &mut ExplainContext<'_, '_>) {
        match self {
            Self::Bind(binding) => context.write(binding),
            Self::Discard => context.push('_'),
            Self::Int(value) => context.push_str(&value.to_string()),
            Self::Float(value) => context.push_str(&format!("{value:?}")),
            Self::String(value) => context.push_str(&format!("{value:?}")),
            Self::Bool(value) => context.push_str(if *value { "True" } else { "False" }),
            Self::Nil => context.push_str("Nil"),
            Self::Tuple(elements) => {
                context.push_str("#(");
                write_patterns(context, elements);
                context.push(')');
            }
            Self::List(list) => context.write(list),
            Self::BitArray(pattern) => context.write(pattern),
            Self::Custom {
                constructor,
                fields,
            } => {
                context.push_str("custom_type#");
                context.push_str(&constructor.type_id().index().to_string());
                context.push_str(".constructor#");
                context.push_str(&constructor.index().to_string());
                context.push('(');
                write_patterns(context, fields);
                context.push(')');
            }
            Self::StringPrefix {
                prefix,
                left,
                right,
            } => {
                context.push_str("string_prefix(");
                context.push_str(&format!("{prefix:?}"));
                context.push_str(", left=");
                write_optional_binding(context, left.as_ref());
                context.push_str(", right=");
                write_optional_binding(context, right.as_ref());
                context.push(')');
            }
            Self::Alias { pattern, binding } => {
                context.push_str("alias(");
                context.write(pattern.as_ref());
                context.push_str(", ");
                context.write(binding);
                context.push(')');
            }
        }
    }
}

impl Explain for MatchPatternBinding {
    fn write_explanation(&self, context: &mut ExplainContext<'_, '_>) {
        context.push_str("binding#");
        context.push_str(&self.index().to_string());
    }
}

fn write_patterns(context: &mut ExplainContext<'_, '_>, patterns: &[MatchPattern]) {
    for (index, pattern) in patterns.iter().enumerate() {
        if index > 0 {
            context.push_str(", ");
        }
        context.write(pattern);
    }
}

fn write_optional_binding(
    context: &mut ExplainContext<'_, '_>,
    binding: Option<&MatchPatternBinding>,
) {
    match binding {
        Some(binding) => context.write(binding),
        None => context.push('_'),
    }
}

#[cfg(test)]
mod explain_tests {
    use super::super::Terminator;
    use super::{MatchPattern, MatchPatternBinding};
    use crate::plan::execution::explain;
    use crate::plan::execution::function::IntFunctionId;

    #[test]
    fn writes_nested_patterns_from_a_lowered_match() {
        let source = r#"
pub type Payload { Payload(Int) }

fn identity(value: #(List(Int), Payload, String)) { value }

pub fn main() {
  let value = identity(#([1, 2], Payload(3), "prefix"))
  let assert #([1, ..rest], Payload(number), "pre" <> suffix) as whole = value
  number
}
"#;
        let expected = "alias(#([1, ..binding#0], custom_type#0.constructor#0(binding#1), string_prefix(\"pre\", left=_, right=binding#2)), binding#3)";

        assert_explanation(source, expected);
    }

    #[test]
    fn writes_match_pattern_binding() {
        let source = "pub fn main() { 1 }";
        let expected = "binding#3";

        explain::assert_rendered(source, expected, |plan, output| {
            let mut context = explain::ExplainContext::new(plan, output);
            context.write(&MatchPatternBinding::new(3));
        });
    }

    #[test]
    #[should_panic(expected = "let assert should lower to a match terminator")]
    fn match_pattern_shape_guard_is_visible() {
        explain::with_execution_plan("pub fn main() { 1 }", |plan| {
            match_pattern(&terminators(plan));
        });
    }

    #[test]
    #[should_panic(expected = "let assert should lower to one match terminator")]
    fn match_pattern_uniqueness_guard_is_visible() {
        let source = r#"
pub fn main() {
  let values = [1]
  let assert [head, ..] = values
  head
}
"#;
        explain::with_execution_plan(source, |plan| {
            let pattern = match_pattern(&terminators(plan));
            match_pattern_from_nodes(&[pattern, pattern]);
        });
    }

    fn terminators(
        plan: &crate::plan::execution::ExecutionPlan,
    ) -> Vec<&crate::plan::execution::graph::Terminator> {
        plan.int_function(IntFunctionId(0))
            .body()
            .block_graph()
            .blocks()
            .iter()
            .map(|block| block.terminator())
            .collect()
    }

    fn match_pattern<'a>(terminators: &[&'a Terminator]) -> &'a MatchPattern {
        let patterns = terminators
            .iter()
            .copied()
            .filter_map(|terminator| match terminator {
                Terminator::Match(matcher) => Some(matcher.pattern()),
                _ => None,
            })
            .collect::<Vec<_>>();
        match_pattern_from_nodes(&patterns)
    }

    fn match_pattern_from_nodes<'a>(patterns: &[&'a MatchPattern]) -> &'a MatchPattern {
        let [pattern] = patterns else {
            if patterns.is_empty() {
                panic!("let assert should lower to a match terminator");
            }
            panic!("let assert should lower to one match terminator");
        };
        pattern
    }

    fn assert_explanation(source: &str, expected: &str) {
        explain::assert_rendered(source, expected, |plan, output| {
            let pattern = match_pattern(&terminators(plan));
            let mut context = explain::ExplainContext::new(plan, output);
            context.write(pattern);
        });
    }
}