blue-lang-runtime 0.0.98

The blue runtime — one definition of what a blue program runs against.
Documentation
use blue_lang_runtime::pipeline::{check_entry, lower_with, Checking};
use blue_lang_runtime::uses::{Entry, NoLoader};
use std::path::Path;
use tatara_lisp::Spanned;
use tatara_lisp_eval::vm::Budget;

fn checked(src: &str) -> blue_lang_runtime::pipeline::Checked {
    let entry = Entry {
        path: Some(Path::new("stored.b")),
        text: src,
    };
    check_entry(entry, &NoLoader, None, Checking::WithTests).expect("checks")
}

fn codes(src: &str) -> Vec<String> {
    checked(src)
        .outcome
        .diagnostics
        .iter()
        .map(|d| d.code.as_str().to_string())
        .collect()
}

fn lowered(src: &str, simplify: bool) -> Vec<Spanned> {
    let c = checked(src);
    if simplify {
        c.erased()
    } else {
        blue_lang_runtime::erase_types(&lower_with(&c.program, &c.names, false))
    }
}

fn shown(forms: &[Spanned]) -> Vec<String> {
    forms.iter().map(|f| f.to_sexp().to_string()).collect()
}

fn run_within(forms: &[Spanned], fuel: usize) -> Option<String> {
    let mut interp = blue_lang_runtime::interpreter_hostless();
    interp
        .set_budget(Budget {
            fuel: Some(fuel),
            max_depth: Some(tatara_lisp_eval::vm::DEFAULT_MAX_DEPTH),
            quantum: None,
        })
        .expect("budget");
    let mut last = tatara_lisp_eval::Value::Nil;
    for f in forms {
        last = interp.eval_top_form(f, &mut ()).ok()?;
    }
    Some(blue_lang_runtime::literal::render_value(&last))
}

fn least_fuel(forms: &[Spanned]) -> usize {
    let (mut lo, mut hi) = (1, 1 << 22);
    assert!(
        run_within(forms, hi).is_some(),
        "does not run in {hi} steps"
    );
    while lo < hi {
        let mid = (lo + hi) / 2;
        if run_within(forms, mid).is_some() {
            hi = mid;
        } else {
            lo = mid + 1;
        }
    }
    lo
}

const CHAIN: &str = "def go(x)\n  to_s(to_s(to_s(to_s(x))))\nend\n\ngo(5)\n";
const PLAIN: &str = "def go(x)\n  to_s(x)\nend\n\ngo(5)\n";

#[test]
fn a_redundant_chain_is_reported_once_per_redundant_call() {
    assert_eq!(codes(CHAIN), ["B0045", "B0045", "B0045"]);
    assert_eq!(codes(PLAIN), Vec::<String>::new());
}

#[test]
fn a_chain_that_changes_a_value_is_left_alone() {
    for src in [
        "def f(x)\n  to_int(to_int(x))\nend\n",
        "def f(x)\n  to_float(to_float(x))\nend\n",
        "def f(x)\n  to_s(to_int(x))\nend\n",
        "def f(x)\n  upcase(downcase(x))\nend\n",
    ] {
        assert_eq!(codes(src), Vec::<String>::new(), "{src}");
    }
}

#[test]
fn a_local_called_like_a_conversion_is_not_the_conversion() {
    let src = "def f(to_s)\n  to_s(to_s(1))\nend\n";
    assert_eq!(codes(src), Vec::<String>::new());
    let unsimplified = shown(&lowered(src, false));
    assert_eq!(shown(&lowered(src, true)), unsimplified);
}

#[test]
fn the_canonical_rewrite_writes_the_simplified_chain() {
    let c = checked(CHAIN);
    let tree = c.canonical_entry(CHAIN).expect("canonical");
    let plain = checked(PLAIN).canonical_entry(PLAIN).expect("canonical");
    assert_eq!(
        tree.iter()
            .map(|f| f.to_sexp().to_string())
            .collect::<Vec<_>>(),
        plain
            .iter()
            .map(|f| f.to_sexp().to_string())
            .collect::<Vec<_>>()
    );
}

#[test]
fn the_evaluator_runs_the_simplified_chain_without_the_author_formatting_it() {
    assert_eq!(shown(&lowered(CHAIN, true)), shown(&lowered(PLAIN, true)));
    assert_ne!(shown(&lowered(CHAIN, false)), shown(&lowered(PLAIN, false)));
}

#[test]
fn the_simplified_chain_takes_fewer_evaluation_steps_and_the_same_value() {
    let with = lowered(CHAIN, true);
    let without = lowered(CHAIN, false);
    let (a, b) = (least_fuel(&with), least_fuel(&without));
    assert!(a < b, "with the pass {a} steps, without {b}");
    assert_eq!(run_within(&with, 1 << 22), run_within(&without, 1 << 22));
    println!("chain steps: {a} with the pass, {b} without");
}