this-me 0.3.3

Rust ground for the modern .me semantic kernel.
Documentation
use std::collections::BTreeSet;

use super::{IntoPath, Kernel, Value};

pub(super) fn extract_expression_refs(expression: &str) -> BTreeSet<String> {
    tokenize_eval_expression(expression)
        .unwrap_or_default()
        .into_iter()
        .filter_map(|token| match token {
            EvalToken::Identifier(label) => Some(label),
            _ => None,
        })
        .collect()
}

pub(super) fn evaluate_expression(
    kernel: &Kernel,
    eval_scope: &[String],
    raw: &str,
) -> Option<Value> {
    let tokens = tokenize_eval_expression(raw)?;
    if tokens.is_empty() {
        return None;
    }

    let mut output = Vec::new();
    let mut ops = Vec::new();
    let mut previous = EvalPrevious::Start;

    for token in tokens {
        match token {
            EvalToken::Literal(_) | EvalToken::Identifier(_) => {
                output.push(token);
                previous = EvalPrevious::Value;
            }
            EvalToken::LParen => {
                ops.push(EvalToken::LParen);
                previous = EvalPrevious::LParen;
            }
            EvalToken::RParen => {
                let mut found_lparen = false;
                while let Some(top) = ops.pop() {
                    if top == EvalToken::LParen {
                        found_lparen = true;
                        break;
                    }
                    output.push(top);
                }
                if !found_lparen {
                    return None;
                }
                previous = EvalPrevious::RParen;
            }
            EvalToken::Op(mut op) => {
                let unary_position = matches!(
                    previous,
                    EvalPrevious::Start | EvalPrevious::Op | EvalPrevious::LParen
                );
                let invalid_not_position =
                    op == "!" && matches!(previous, EvalPrevious::Value | EvalPrevious::RParen);
                let invalid_binary_position = op != "!" && op != "-" && unary_position;

                if op == "-" && unary_position {
                    op = "u-".to_string();
                } else if invalid_not_position || invalid_binary_position {
                    return None;
                }

                while let Some(EvalToken::Op(top)) = ops.last() {
                    let current_precedence = op_precedence(&op)?;
                    let top_precedence = op_precedence(top)?;
                    let should_pop = if op == "u-" || op == "!" {
                        current_precedence < top_precedence
                    } else {
                        current_precedence <= top_precedence
                    };
                    if !should_pop {
                        break;
                    }
                    output.push(ops.pop()?);
                }
                ops.push(EvalToken::Op(op));
                previous = EvalPrevious::Op;
            }
        }
    }

    if matches!(
        previous,
        EvalPrevious::Start | EvalPrevious::Op | EvalPrevious::LParen
    ) {
        return None;
    }

    while let Some(top) = ops.pop() {
        if top == EvalToken::LParen {
            return None;
        }
        output.push(top);
    }

    eval_rpn(kernel, eval_scope, output)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum EvalPrevious {
    Start,
    Value,
    Op,
    LParen,
    RParen,
}

#[derive(Debug, Clone, PartialEq)]
enum EvalToken {
    Literal(Value),
    Identifier(String),
    Op(String),
    LParen,
    RParen,
}

fn tokenize_eval_expression(raw: &str) -> Option<Vec<EvalToken>> {
    let chars = raw.trim().chars().collect::<Vec<_>>();
    let mut tokens = Vec::new();
    let mut index = 0;

    while index < chars.len() {
        let ch = chars[index];
        if ch.is_whitespace() {
            index += 1;
            continue;
        }

        if ch == '(' {
            tokens.push(EvalToken::LParen);
            index += 1;
            continue;
        }
        if ch == ')' {
            tokens.push(EvalToken::RParen);
            index += 1;
            continue;
        }

        if index + 1 < chars.len() {
            let two = [chars[index], chars[index + 1]].iter().collect::<String>();
            if matches!(two.as_str(), ">=" | "<=" | "==" | "!=" | "&&" | "||") {
                tokens.push(EvalToken::Op(two));
                index += 2;
                continue;
            }
        }

        if matches!(ch, '+' | '-' | '*' | '/' | '%' | '<' | '>' | '!') {
            tokens.push(EvalToken::Op(ch.to_string()));
            index += 1;
            continue;
        }

        if ch.is_ascii_digit()
            || (ch == '.' && chars.get(index + 1).is_some_and(|c| c.is_ascii_digit()))
        {
            let (value, next_index) = scan_number(&chars, index)?;
            tokens.push(EvalToken::Literal(Value::Number(value)));
            index = next_index;
            continue;
        }

        if ch.is_ascii_alphabetic() || ch == '_' {
            let (identifier, next_index) = scan_identifier(&chars, index)?;
            match identifier.as_str() {
                "true" => tokens.push(EvalToken::Literal(Value::Bool(true))),
                "false" => tokens.push(EvalToken::Literal(Value::Bool(false))),
                "null" | "undefined" => tokens.push(EvalToken::Literal(Value::Null)),
                _ => tokens.push(EvalToken::Identifier(identifier)),
            }
            index = next_index;
            continue;
        }

        return None;
    }

    Some(tokens)
}

fn scan_number(chars: &[char], start: usize) -> Option<(f64, usize)> {
    let mut index = start;
    while chars.get(index).is_some_and(|ch| ch.is_ascii_digit()) {
        index += 1;
    }
    if chars.get(index) == Some(&'.') {
        index += 1;
        while chars.get(index).is_some_and(|ch| ch.is_ascii_digit()) {
            index += 1;
        }
    }
    if matches!(chars.get(index), Some('e' | 'E')) {
        index += 1;
        if matches!(chars.get(index), Some('+' | '-')) {
            index += 1;
        }
        let exp_start = index;
        while chars.get(index).is_some_and(|ch| ch.is_ascii_digit()) {
            index += 1;
        }
        if index == exp_start {
            return None;
        }
    }

    let value = chars[start..index]
        .iter()
        .collect::<String>()
        .parse::<f64>()
        .ok()?;
    value.is_finite().then_some((value, index))
}

fn scan_identifier(chars: &[char], start: usize) -> Option<(String, usize)> {
    let mut index = start;
    let mut bracket_depth = 0_u32;
    let mut quote = None;

    while index < chars.len() {
        let ch = chars[index];
        if let Some(q) = quote {
            if ch == q {
                quote = None;
            }
            index += 1;
            continue;
        }

        if bracket_depth > 0 {
            if ch == '"' || ch == '\'' {
                quote = Some(ch);
            } else if ch == '[' {
                bracket_depth += 1;
            } else if ch == ']' {
                bracket_depth -= 1;
            }
            index += 1;
            continue;
        }

        if ch.is_ascii_alphanumeric() || ch == '_' || ch == '.' {
            index += 1;
            continue;
        }
        if ch == '[' {
            bracket_depth += 1;
            index += 1;
            continue;
        }
        break;
    }

    if bracket_depth != 0 || quote.is_some() || index == start {
        return None;
    }
    Some((chars[start..index].iter().collect(), index))
}

fn op_precedence(op: &str) -> Option<u8> {
    match op {
        "u-" | "!" => Some(7),
        "*" | "/" | "%" => Some(6),
        "+" | "-" => Some(5),
        "<" | "<=" | ">" | ">=" => Some(4),
        "==" | "!=" => Some(3),
        "&&" => Some(2),
        "||" => Some(1),
        _ => None,
    }
}

fn eval_rpn(kernel: &Kernel, eval_scope: &[String], tokens: Vec<EvalToken>) -> Option<Value> {
    let mut stack = Vec::new();

    for token in tokens {
        match token {
            EvalToken::Literal(value) => stack.push(value),
            EvalToken::Identifier(label) => {
                stack.push(resolve_eval_token(kernel, &label, eval_scope)?)
            }
            EvalToken::Op(op) if op == "u-" || op == "!" => {
                let value = stack.pop()?;
                if op == "u-" {
                    stack.push(Value::Number(-to_finite_number(&value)?));
                } else {
                    stack.push(Value::Bool(!truthy(&value)));
                }
            }
            EvalToken::Op(op) => {
                let right = stack.pop()?;
                let left = stack.pop()?;
                stack.push(apply_binary_op(&left, &op, &right)?);
            }
            EvalToken::LParen | EvalToken::RParen => return None,
        }
    }

    if stack.len() != 1 {
        return None;
    }
    match stack.pop()? {
        Value::Number(number) if number.is_finite() => Some(Value::Number(number)),
        Value::Bool(value) => Some(Value::Bool(value)),
        _ => None,
    }
}

fn resolve_eval_token(kernel: &Kernel, label: &str, eval_scope: &[String]) -> Option<Value> {
    let token_path = label.into_path().ok()?;
    let relative_path = eval_scope
        .iter()
        .cloned()
        .chain(token_path.iter().cloned())
        .collect::<Vec<_>>();

    kernel
        .read(relative_path)
        .cloned()
        .or_else(|| kernel.read(token_path).cloned())
}

fn apply_binary_op(left: &Value, op: &str, right: &Value) -> Option<Value> {
    if op == "&&" || op == "||" {
        return Some(Value::Bool(if op == "&&" {
            truthy(left) && truthy(right)
        } else {
            truthy(left) || truthy(right)
        }));
    }

    if op == "==" || op == "!=" {
        let equal = loose_equal(left, right);
        return Some(Value::Bool(if op == "==" { equal } else { !equal }));
    }

    if matches!(op, "<" | "<=" | ">" | ">=") {
        let left = to_finite_number(left)?;
        let right = to_finite_number(right)?;
        return Some(Value::Bool(match op {
            "<" => left < right,
            "<=" => left <= right,
            ">" => left > right,
            ">=" => left >= right,
            _ => unreachable!(),
        }));
    }

    let left = to_finite_number(left)?;
    let right = to_finite_number(right)?;
    let out = match op {
        "+" => left + right,
        "-" => left - right,
        "*" => left * right,
        "/" => left / right,
        "%" => left % right,
        _ => return None,
    };
    out.is_finite().then_some(Value::Number(out))
}

fn to_finite_number(value: &Value) -> Option<f64> {
    match value {
        Value::Number(number) if number.is_finite() => Some(*number),
        Value::String(value) => value.parse::<f64>().ok().filter(|n| n.is_finite()),
        _ => None,
    }
}

fn truthy(value: &Value) -> bool {
    match value {
        Value::Null => false,
        Value::Bool(value) => *value,
        Value::Number(value) => *value != 0.0 && value.is_finite(),
        Value::String(value) => !value.is_empty(),
        Value::Array(value) => !value.is_empty(),
        Value::Object(value) => !value.is_empty(),
        Value::Pointer(_) | Value::Identity(_) => true,
    }
}

fn loose_equal(left: &Value, right: &Value) -> bool {
    if left == right {
        return true;
    }
    match (to_finite_number(left), to_finite_number(right)) {
        (Some(left), Some(right)) => left == right,
        _ => false,
    }
}