dellingr 0.4.0

An embeddable, pure-Rust Lua VM with precise instruction-cost accounting
Documentation
use dellingr::{ArgCount, RetCount, State};

fn run_number(code: &str) -> f64 {
    let mut state = State::new();
    state.load_string(code).unwrap();
    state.call(ArgCount::Fixed(0), RetCount::Fixed(1)).unwrap();
    state.to_number(-1).unwrap()
}

#[test]
fn filter_in_place_survives_inline_and_map_tombstones() {
    for size in [4, 5] {
        let code = format!(
            "local t = {{}}; for i = 1, {size} do t[i] = i end; for k in pairs(t) do if k % 2 == 0 then t[k] = nil end end; local total = 0; for _, v in pairs(t) do total = total + v end; return total"
        );
        assert_eq!(
            run_number(&code),
            (1..=size).filter(|i| i % 2 != 0).sum::<usize>() as f64
        );
    }
}

#[test]
fn deleted_controls_and_reinsertion_keep_iteration_correct() {
    assert_eq!(
        run_number(
            "local t={a=1,b=2,c=3,d=4}; local k=next(t); t[k]=nil; t.c=nil; local n=next(t,k); return n == 'b' and 1 or 0"
        ),
        1.0
    );
    assert_eq!(
        run_number(
            "local t={a=1,b=2,c=3}; t.b=nil; t.b=4; local s=''; for k in pairs(t) do s=s..k end; return s == 'acb' and 1 or 0"
        ),
        1.0
    );
}

#[test]
fn collectable_tombstone_key_survives_next_control_lookup() {
    let mut state = State::new();
    state.gc_disable_auto();
    state
        .push_rust_fn(|state| {
            state.gc_collect();
            Ok(0)
        })
        .unwrap();
    state.set_global("force_gc");
    state
        .load_string("local t = {}; local a = {}; local b = {}; t[a] = 1; t[b] = 2; local control = next(t); t[control] = nil; force_gc(); return next(t, control) == b and 1 or 0")
        .unwrap();
    state.call(ArgCount::Fixed(0), RetCount::Fixed(1)).unwrap();
    assert_eq!(state.to_number(-1).unwrap(), 1.0);
}

#[test]
fn pairs_cursor_preserves_mutation_and_same_site_recursion_semantics() {
    assert_eq!(
        run_number(
            "local t={a=1,b=2,c=3,d=4}; local s=''; for k in pairs(t) do s=s..k; if k=='a' then t.b=nil; t.e=5 end end; return s == 'acde' and 1 or 0",
        ),
        1.0
    );
    assert_eq!(
        run_number(
            "local t={a=1,b=2,c=3,d=4}; local s=''; for k in pairs(t) do s=s..k; if k=='a' then t.e=5 end end; return s == 'abcde' and 1 or 0",
        ),
        1.0
    );
    assert_eq!(
        run_number(
            "local t={a=1,b=2,c=3,d=4,e=5}; local s=''; for k in pairs(t) do s=s..k; if k=='c' then t.a=nil; t.b=nil; t.d=nil; t.f=6 end end; return s == 'abcef' and 1 or 0",
        ),
        1.0
    );
    assert_eq!(
        run_number(
            "local t={a=1,b=2,c=3,d=4}; local s=''; for k in pairs(t) do s=s..k; if k=='a' then t.b=nil; t.b=2 end end; return s == 'acdb' and 1 or 0",
        ),
        1.0
    );
    assert_eq!(
        run_number(
            "local function walk(t, depth) local s=''; for k in pairs(t) do if depth > 0 then walk(t, depth-1) end s=s..k end return s end; return walk({a=1,b=2,c=3}, 1) == 'abc' and 1 or 0",
        ),
        1.0
    );
}

#[test]
fn pairs_cursor_keeps_invalid_controls_and_costs_unchanged() {
    let mut state = State::new();
    state
        .load_string("local t={a=1,b=2,c=3}; for k in pairs(t) do end; for k in pairs(t) do end")
        .unwrap();
    state.call(ArgCount::Fixed(0), RetCount::Fixed(0)).unwrap();
    assert_eq!(state.cost_used(), 4);

    let mut invalid = State::new();
    invalid
        .load_string("local t={a=1}; return next(t, 'missing')")
        .unwrap();
    let error = invalid
        .call(ArgCount::Fixed(0), RetCount::Fixed(1))
        .expect_err("invalid next control must take the generic builtin path");
    assert!(error.to_string().contains("invalid key to 'next'"));
}