mangle 1.0.0

Exolang that can take any utf8 text as valid code and return some result out of it
Documentation
use std::collections::HashMap;

use crate::operators::{get_operator, Flow, Operator};
use crate::value::{grapheme_len, render, MangleError, Scope, Value};

const SENTENCE_ENDINGS: &[char] = &['.', ',', ';', ':', '?', '!'];
pub const MAX_OPS: usize = 1_000_000;

struct Op {
    operator: Operator,
    args: Vec<String>,
}

struct Sentence {
    #[allow(dead_code)]
    label: Option<String>,
    ops: Vec<Op>,
}

pub struct Interpreter {
    word_separator: char,
}

impl Interpreter {
    pub fn new(word_separator: char) -> Self {
        Self { word_separator }
    }

    pub fn eval(&mut self, source: &str) -> Result<String, MangleError> {
        let mut scope = Scope::default();
        let last = execute(source, self.word_separator, &mut scope)?;
        if scope.emitted_output {
            Ok(String::new())
        } else {
            Ok(render(&last))
        }
    }
}

pub fn execute(text: &str, separator: char, scope: &mut Scope) -> Result<Value, MangleError> {
    execute_with_limit(text, separator, scope, MAX_OPS)
}

pub fn execute_with_limit(
    text: &str,
    separator: char,
    scope: &mut Scope,
    max_ops: usize,
) -> Result<Value, MangleError> {
    let (sentences, labels) = parse_program(text, separator)?;
    scope.assign("it", Value::Int(0));
    let mut last_value = Value::Int(0);
    let mut instruction_pointer = 0usize;
    let mut ops_executed = 0usize;
    while instruction_pointer < sentences.len() {
        let mut next_pointer = instruction_pointer + 1;
        for op in &sentences[instruction_pointer].ops {
            ops_executed += 1;
            if ops_executed > max_ops {
                return Err(MangleError(format!(
                    "operation limit ({}) exceeded; likely infinite loop",
                    max_ops
                )));
            }
            let args: Vec<&str> = op.args.iter().map(String::as_str).collect();
            match (op.operator.apply)(&args, scope)? {
                Flow::Next(value) => {
                    last_value = value.clone();
                    scope.assign("it", value);
                }
                Flow::Jump(label) => {
                    next_pointer = *labels.get(&label).ok_or_else(|| {
                        MangleError(format!("unknown label: {:?}", label))
                    })?;
                    break;
                }
            }
        }
        instruction_pointer = next_pointer;
    }
    Ok(last_value)
}

fn parse_program(
    text: &str,
    separator: char,
) -> Result<(Vec<Sentence>, HashMap<String, usize>), MangleError> {
    let normalized: String = text
        .chars()
        .map(|c| if matches!(c, '\n' | '\r' | '\t') { separator } else { c })
        .collect();
    let mut sentences = Vec::new();
    let mut labels: HashMap<String, usize> = HashMap::new();
    for raw in normalized.split(|ch| SENTENCE_ENDINGS.contains(&ch)) {
        let words: Vec<&str> = raw
            .split(separator)
            .map(str::trim)
            .filter(|word| !word.is_empty())
            .collect();
        if words.is_empty() {
            continue;
        }
        let (body, label) = if sentences.is_empty() {
            (words, None)
        } else {
            let split = words.len() - 1;
            (words[..split].to_vec(), Some(words[split].to_string()))
        };
        let position = sentences.len();
        if let Some(ref label) = label {
            labels.entry(label.clone()).or_insert(position);
        }
        sentences.push(Sentence {
            label,
            ops: parse_ops(&body)?,
        });
    }
    Ok((sentences, labels))
}

fn parse_ops(words: &[&str]) -> Result<Vec<Op>, MangleError> {
    let mut ops = Vec::new();
    let mut cursor = 0usize;
    while cursor < words.len() {
        let word = words[cursor];
        let operator = get_operator(word).ok_or_else(|| {
            MangleError(format!(
                "no operator for word length {}: {:?}",
                grapheme_len(word),
                word
            ))
        })?;
        let end = if operator.variadic {
            words.len()
        } else {
            cursor + 1 + operator.arity
        };
        if !operator.variadic && end > words.len() {
            return Err(MangleError(format!(
                "operator {:?} needs {} argument(s)",
                word, operator.arity
            )));
        }
        let args: Vec<String> = words[cursor + 1..end]
            .iter()
            .map(|word| word.to_string())
            .collect();
        ops.push(Op { operator, args });
        cursor = end;
    }
    Ok(ops)
}

#[cfg(test)]
mod tests {
    use super::*;

    fn run(text: &str) -> i64 {
        let mut scope = Scope::default();
        match execute(text, ' ', &mut scope) {
            Ok(Value::Int(n)) => n,
            Ok(Value::Stack(items)) => items.len() as i64,
            Err(error) => panic!("expected success, got: {}", error),
        }
    }

    fn run_err(text: &str) -> String {
        let mut scope = Scope::default();
        match execute(text, ' ', &mut scope) {
            Ok(_) => panic!("expected error, got success"),
            Err(error) => error.0,
        }
    }

    #[test]
    fn sum_two_literals() {
        assert_eq!(run("add cat fat"), 6);
    }

    #[test]
    fn subtract_two_literals() {
        assert_eq!(run("reduces fat cat"), 0);
    }

    #[test]
    fn multiply_two_literals() {
        assert_eq!(run("multiply fat fat"), 9);
    }

    #[test]
    fn divide_two_literals() {
        assert_eq!(run("dividedby things cat"), 2);
    }

    #[test]
    fn modulo_two_literals() {
        assert_eq!(run("remainderops hello cat"), 2);
    }

    #[test]
    fn assign_then_sum() {
        assert_eq!(run("is x cat add x fat"), 6);
    }

    #[test]
    fn literal_passthrough() {
        assert_eq!(run("a cat"), 3);
    }

    #[test]
    fn result_register_it() {
        assert_eq!(run("reduces cat cat add it fat"), 3);
    }

    #[test]
    fn goto_skips_fallthrough() {
        let program = "jumps skip. add cat cat mid. add it fat skip";
        assert_eq!(run(program), 3);
    }

    #[test]
    fn ifzero_taken_jumps() {
        let program = "reduces cat cat when it skip. add cat cat mid. add it fat skip";
        assert_eq!(run(program), 3);
    }

    #[test]
    fn ifzero_not_taken_falls_through() {
        let program = "when cat skip. add cat cat mid. add it fat skip";
        assert_eq!(run(program), 9);
    }

    #[test]
    fn push_then_pop() {
        assert_eq!(run("pushesvalue cat mystack popreturns mystack"), 3);
    }

    #[test]
    fn graphemes_not_codepoints() {
        assert_eq!(run("add a\u{0301} cat"), 4);
    }

    #[test]
    fn unknown_label_errors() {
        assert!(run_err("jumps nowhere").contains("unknown label"));
    }

    #[test]
    fn division_by_zero_errors() {
        assert!(run_err("reduces cat cat dividedby cat it").contains("division by zero"));
    }

    #[test]
    fn unknown_operator_length_errors() {
        assert!(run_err("abcdefghijklmno cat").contains("no operator"));
    }

    #[test]
    fn arity_mismatch_errors() {
        assert!(run_err("add cat").contains("argument"));
    }

    #[test]
    fn infinite_goto_hits_op_limit() {
        let mut scope = Scope::default();
        let program = "add cat fat. jumps loop loop";
        let result = execute_with_limit(program, ' ', &mut scope, 100);
        assert!(result.unwrap_err().0.contains("operation limit"));
    }

    #[test]
    fn variable_copy_via_assign() {
        assert_eq!(run("is x cat is y x add y fat"), 6);
    }

    #[test]
    fn falls_through_to_next_sentence() {
        assert_eq!(run("add cat fat. reduces fat fat end"), 0);
    }

    #[test]
    fn pop_empty_stack_yields_zero() {
        assert_eq!(run("popreturns s"), 0);
    }

    #[test]
    fn arithmetic_coerces_stack_to_its_length() {
        assert_eq!(run("pushesvalue cat s pushesvalue fat s add s cat"), 5);
    }

    #[test]
    fn eval_renders_stack() {
        let mut interpreter = Interpreter::new(' ');
        assert_eq!(
            interpreter.eval("pushesvalue cat s pushesvalue fat s").unwrap(),
            "3 3"
        );
    }

    #[test]
    fn eval_suppresses_echo_after_output() {
        let mut interpreter = Interpreter::new(' ');
        assert_eq!(interpreter.eval("output things").unwrap(), "");
    }

    #[test]
    fn multiline_program_is_one_program() {
        assert_eq!(run("is x cat\nadd x fat"), 6);
    }

    #[test]
    fn empty_program_returns_zero() {
        assert_eq!(run(""), 0);
    }

    #[test]
    fn custom_separator() {
        let mut scope = Scope::default();
        match execute("add-cat-fat", '-', &mut scope) {
            Ok(Value::Int(n)) => assert_eq!(n, 6),
            other => panic!("expected 6, got {:?}", other),
        }
    }
}