ranty 1.0.0

The Ranty procedural templating language
Documentation
mod common;

use assert_matches::assert_matches;
use common::compile_with_reporter;
use ranty::runtime::{RuntimeError, RuntimeResult, VM};
use ranty::{IntoRanty, Ranty, RantyList, RantyValue};
use std::sync::{
    atomic::{AtomicUsize, Ordering},
    Mutex,
};

static LAZY_COUNTER: AtomicUsize = AtomicUsize::new(0);
static LAZY_TEST_LOCK: Mutex<()> = Mutex::new(());

fn reset_counter() {
    LAZY_COUNTER.store(0, Ordering::SeqCst);
}

fn counter() -> usize {
    LAZY_COUNTER.load(Ordering::SeqCst)
}

fn next_value(vm: &mut VM, _: ()) -> RuntimeResult<()> {
    let next = LAZY_COUNTER.fetch_add(1, Ordering::SeqCst) + 1;
    vm.cur_frame_mut().write(next as i64);
    Ok(())
}

fn next_list(vm: &mut VM, _: ()) -> RuntimeResult<()> {
    LAZY_COUNTER.fetch_add(1, Ordering::SeqCst);
    vm.cur_frame_mut().write(
        RantyList::from(vec![RantyValue::Int(1), RantyValue::Int(2), RantyValue::Int(3)])
            .into_ranty(),
    );
    Ok(())
}

fn ok_func(vm: &mut VM, _: ()) -> RuntimeResult<()> {
    vm.cur_frame_mut().write("ok");
    Ok(())
}

fn make_func(vm: &mut VM, _: ()) -> RuntimeResult<()> {
    LAZY_COUNTER.fetch_add(1, Ordering::SeqCst);
    vm.cur_frame_mut().write(RantyValue::from_func(ok_func));
    Ok(())
}

fn run_with_lazy_helpers(source: &str) -> Result<String, RuntimeError> {
    reset_counter();
    let mut ranty = Ranty::new();
    ranty.set_global_const("next", RantyValue::from_func(next_value));
    ranty.set_global_const("next-list", RantyValue::from_func(next_list));
    ranty.set_global_const("make-func", RantyValue::from_func(make_func));
    let program = ranty
        .compile_quiet(source)
        .expect("failed to compile lazy test program");
    ranty.run(&program).map(|value| value.to_string())
}

#[test]
fn lazy_definition_forces_once() {
    let _guard = LAZY_TEST_LOCK.lock().unwrap();
    let output = run_with_lazy_helpers(r#"<$lazy ?= [next]><lazy>,<lazy>"#).unwrap();
    assert_eq!(output, "1,1");
    assert_eq!(counter(), 1);
}

#[test]
fn lazy_definition_can_be_overwritten_before_force() {
    let _guard = LAZY_TEST_LOCK.lock().unwrap();
    let output = run_with_lazy_helpers(r#"<$lazy ?= [next]><lazy = 7><lazy>"#).unwrap();
    assert_eq!(output, "7");
    assert_eq!(counter(), 0);
}

#[test]
fn lazy_constant_memoizes() {
    let _guard = LAZY_TEST_LOCK.lock().unwrap();
    let output = run_with_lazy_helpers(r#"<%lazy ?= [next]><lazy>,<lazy>"#).unwrap();
    assert_eq!(output, "1,1");
    assert_eq!(counter(), 1);
}

#[test]
fn lazy_parameter_can_skip_unused_argument() {
    let _guard = LAZY_TEST_LOCK.lock().unwrap();
    let output = run_with_lazy_helpers(r#"[$ignore: @lazy x] { ok }[ignore: [next]]"#).unwrap();
    assert_eq!(output, "ok");
    assert_eq!(counter(), 0);
}

#[test]
fn lazy_parameter_forces_once_when_read_multiple_times() {
    let _guard = LAZY_TEST_LOCK.lock().unwrap();
    let output = run_with_lazy_helpers(r#"[$dup: @lazy x] { <x>,<x> }[dup: [next]]"#).unwrap();
    assert_eq!(output, "1,1");
    assert_eq!(counter(), 1);
}

#[test]
fn lazy_optional_default_only_runs_when_accessed() {
    let _guard = LAZY_TEST_LOCK.lock().unwrap();
    let output = run_with_lazy_helpers(
        r#"
[$unused: @lazy x ? [next]] { ok }
[$used: @lazy x ? [next]] { <x>,<x> }
[unused]\n[used]
"#,
    )
    .unwrap();
    assert_eq!(output, "ok\n1,1");
    assert_eq!(counter(), 1);
}

#[test]
fn lazy_definition_captures_by_reference() {
    let _guard = LAZY_TEST_LOCK.lock().unwrap();
    let output = run_with_lazy_helpers(r#"<$value = 1><$lazy ?= <value>><value = 2><lazy>"#).unwrap();
    assert_eq!(output, "2");
}

#[test]
fn lazy_argument_capture_survives_for_closure_use() {
    let _guard = LAZY_TEST_LOCK.lock().unwrap();
    let output = run_with_lazy_helpers(
        r#"
[$defer: @lazy x] {
  [?] { <x> }
}
<$value = 1>
<$reader = [defer: <value>]>
<value = 2>
[reader]
"#,
    )
    .unwrap();
    assert_eq!(output, "2");
}

#[test]
fn descendant_setter_forces_lazy_root() {
    let _guard = LAZY_TEST_LOCK.lock().unwrap();
    let output = run_with_lazy_helpers(r#"<$items ?= [next-list]><items/0 = 9><items/0>,<items/1>,<items/2>"#).unwrap();
    assert_eq!(output, "9,2,3");
    assert_eq!(counter(), 1);
}

#[test]
fn function_lookup_forces_lazy_binding() {
    let _guard = LAZY_TEST_LOCK.lock().unwrap();
    let output = run_with_lazy_helpers(r#"<$f ?= [make-func]>[f]"#).unwrap();
    assert_eq!(output, "ok");
    assert_eq!(counter(), 1);
}

#[test]
fn self_referential_lazy_binding_raises_runtime_error() {
    let _guard = LAZY_TEST_LOCK.lock().unwrap();
    reset_counter();
    let mut ranty = Ranty::new();
    let program = ranty
        .compile_quiet(r#"<$x ?= <x>><x>"#)
        .expect("failed to compile self-referential lazy test");
    assert_matches!(
        ranty.run(&program),
        Err(RuntimeError {
            error_type: ranty::runtime::RuntimeErrorType::LazyBindingCycle,
            ..
        })
    );
}

#[test]
fn lazy_variadic_parameters_are_rejected() {
    let (result, messages) = compile_with_reporter(r#"[$bad: @lazy xs*] { <xs> }"#);
    assert!(result.is_err(), "lazy variadic parameter should fail to compile");
    assert!(messages.iter().any(|msg| msg.code() == "R0029"));
}

#[test]
fn lazy_optional_parameter_without_default_is_still_fallible() {
    let (result, messages) = compile_with_reporter(r#"[$bad: @lazy x?] { <x> }"#);
    assert!(result.is_err(), "fallible lazy optional access should fail to compile");
    assert!(messages.iter().any(|msg| msg.code() == "R0067"));
}

#[test]
fn lazy_constant_reassignment_is_rejected() {
    let (result, messages) = compile_with_reporter(r#"<%x ?= 1><x = 2>"#);
    assert!(result.is_err(), "lazy constant reassignment should fail to compile");
    assert!(messages.iter().any(|msg| msg.code() == "R0100"));
}