neovm-core 0.0.2

Core runtime structures for NeoVM
use super::*;

fn unibyte_name(bytes: &[u8]) -> crate::heap_types::LispString {
    crate::heap_types::LispString::from_unibyte(bytes.to_vec())
}

#[test]
fn name_interner_dedup() {
    crate::test_utils::init_test_tracing();
    let mut interner = StringInterner::new();
    let a = interner.intern("foo");
    let b = interner.intern("foo");
    let c = interner.intern("bar");
    assert_eq!(a, b);
    assert_ne!(a, c);
    assert_eq!(interner.resolve(a), "foo");
    assert_eq!(interner.resolve(c), "bar");
}

#[test]
fn runtime_intern() {
    crate::test_utils::init_test_tracing();
    let a = intern("hello");
    let b = intern("hello");
    let c = intern("world");
    assert_eq!(a, b);
    assert_ne!(a, c);
    assert_eq!(resolve_sym(a), "hello");
    assert_eq!(resolve_sym(c), "world");
}

#[test]
fn runtime_symbol_name_id_stable_across_growth() {
    crate::test_utils::init_test_tracing();
    let early = intern("early-runtime-name");
    let early_name = symbol_name_id(early);
    for i in 0..500 {
        intern(&format!("growth-runtime-{i}"));
    }
    assert_eq!(symbol_name_id(early), early_name);
    assert_eq!(resolve_name(early_name), "early-runtime-name");
}

#[test]
fn name_interner_empty_string() {
    crate::test_utils::init_test_tracing();
    let mut interner = StringInterner::new();
    let id = interner.intern("");
    assert_eq!(interner.resolve(id), "");
    assert_eq!(interner.intern(""), id);
}

#[test]
fn name_interner_many_strings() {
    crate::test_utils::init_test_tracing();
    let mut interner = StringInterner::new();
    let ids: Vec<NameId> = (0..1000)
        .map(|i| interner.intern(&format!("sym-{i}")))
        .collect();
    for (i, id) in ids.iter().enumerate() {
        assert_eq!(interner.resolve(*id), format!("sym-{i}"));
    }
    let unique: std::collections::HashSet<NameId> = ids.iter().copied().collect();
    assert_eq!(unique.len(), 1000);
}

#[test]
fn name_interner_idempotent() {
    crate::test_utils::init_test_tracing();
    let mut interner = StringInterner::new();
    let first = interner.intern("repeated");
    for _ in 0..100 {
        assert_eq!(interner.intern("repeated"), first);
    }
}

#[test]
fn name_interner_canonicalizes_ascii_multibyte_names_to_unibyte_atoms() {
    crate::test_utils::init_test_tracing();
    let mut interner = StringInterner::new();
    let multibyte = crate::heap_types::LispString::from_utf8("batch-byte-compile");
    let unibyte = crate::heap_types::LispString::from_unibyte(b"batch-byte-compile".to_vec());

    let from_multibyte = interner.intern_lisp_string(&multibyte);
    let from_unibyte = interner.intern_lisp_string(&unibyte);

    assert_eq!(from_multibyte, from_unibyte);
    let resolved = interner.resolve_lisp_string(from_multibyte);
    assert_eq!(resolved.as_bytes(), b"batch-byte-compile");
    assert!(!resolved.is_multibyte());
}

#[test]
fn name_interner_lookup_reuses_ascii_multibyte_canonical_atom() {
    crate::test_utils::init_test_tracing();
    let mut interner = StringInterner::new();
    let multibyte = crate::heap_types::LispString::from_utf8("symbol-name");

    let id = interner.intern_lisp_string(&multibyte);

    assert_eq!(interner.lookup("symbol-name"), Some(id));
    assert_eq!(interner.intern("symbol-name"), id);
}

#[test]
fn symid_copy_eq_hash() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();
    let a = registry.intern("x");
    let b = a;
    assert_eq!(a, b);

    use std::collections::HashSet;
    let mut set = HashSet::new();
    set.insert(a);
    assert!(set.contains(&b));
}

#[test]
fn resolve_sym_stable_across_growth() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();
    let early = registry.intern("early");
    assert_eq!(registry.resolve(early), "early");
    for i in 0..500 {
        registry.intern(&format!("growth-{i}"));
    }
    assert_eq!(registry.resolve(early), "early");
}

#[test]
fn canonical_id_distinguishes_interned_from_uninterned_duplicates() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();
    let canonical = registry.intern("dup");
    let uninterned = registry.intern_uninterned("dup");

    assert!(registry.is_canonical_id(canonical));
    assert!(!registry.is_canonical_id(uninterned));
    assert_eq!(registry.lookup("dup"), Some(canonical));
}

#[test]
fn runtime_registry_canonicalizes_ascii_multibyte_and_unibyte_names() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();
    let multibyte = crate::heap_types::LispString::from_utf8("foo");
    let unibyte = crate::heap_types::LispString::from_unibyte(b"foo".to_vec());

    let from_multibyte = registry.intern_lisp_string(&multibyte);
    let from_unibyte = registry.intern_lisp_string(&unibyte);

    assert_eq!(from_multibyte, from_unibyte);
    let resolved = registry.resolve_lisp_string(from_multibyte);
    assert_eq!(resolved.as_bytes(), b"foo");
    assert!(!resolved.is_multibyte());
}

#[test]
fn canonical_id_survives_dump_style_reconstruction() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();
    let remap = registry
        .restore_dump_symbol_table(
            &[
                unibyte_name(b"nil"),
                unibyte_name(b"t"),
                unibyte_name(b"dup"),
            ],
            &[0, 1, 2, 2],
            None,
        )
        .expect("dump symbol table should restore");

    assert!(registry.is_canonical_id(remap.symbols[2]));
    assert!(!registry.is_canonical_id(remap.symbols[3]));
    assert_eq!(registry.lookup("dup"), Some(remap.symbols[2]));
}

#[test]
fn restore_dump_slots_remaps_reordered_layout() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();
    let runtime_bar = registry.intern("bar");
    let runtime_foo = registry.intern("foo");

    let remap = registry
        .restore_dump_symbol_table(
            &[
                unibyte_name(b"nil"),
                unibyte_name(b"t"),
                unibyte_name(b"bar"),
                unibyte_name(b"foo"),
            ],
            &[0, 1, 3, 2],
            Some(&[true, true, true, true]),
        )
        .expect("dump symbol table should restore");

    assert_eq!(
        remap.symbols,
        vec![NIL_SYM_ID, T_SYM_ID, runtime_foo, runtime_bar]
    );
}

#[test]
fn restore_dump_slots_preserves_lone_uninterned_slot() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();
    let remap = registry
        .restore_dump_symbol_table(
            &[
                unibyte_name(b"nil"),
                unibyte_name(b"t"),
                unibyte_name(b"solo"),
            ],
            &[0, 1, 2],
            Some(&[true, true, false]),
        )
        .expect("dump symbol table should restore");

    assert_eq!(registry.resolve(remap.symbols[2]), "solo");
    assert!(!registry.is_canonical_id(remap.symbols[2]));
    assert_eq!(registry.lookup("solo"), None);
}

#[test]
fn dump_symbol_table_separates_name_atoms_from_symbol_slots() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();
    let canonical = registry.intern("shared-name");
    let uninterned = registry.intern_uninterned("shared-name");

    let dumped = registry.dump_symbol_table();

    let shared_name_id = registry.name_id(canonical);
    assert_eq!(registry.name_id(uninterned), shared_name_id);
    assert_eq!(
        dumped.names[shared_name_id.0 as usize],
        unibyte_name(b"shared-name")
    );
    assert_eq!(dumped.symbol_names[canonical.0 as usize], shared_name_id.0);
    assert_eq!(dumped.symbol_names[uninterned.0 as usize], shared_name_id.0);
    assert!(dumped.canonical[canonical.0 as usize]);
    assert!(!dumped.canonical[uninterned.0 as usize]);
}

#[test]
fn restore_dump_symbol_table_reuses_existing_name_atoms() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();
    let existing = registry.intern("shared-name");
    let existing_name = registry.name_id(existing);

    let remap = registry
        .restore_dump_symbol_table(
            &[
                unibyte_name(b"nil"),
                unibyte_name(b"t"),
                unibyte_name(b"shared-name"),
            ],
            &[0, 1, 2, 2],
            Some(&[true, true, true, false]),
        )
        .expect("dump symbol table should restore");

    assert_eq!(registry.name_id(remap.symbols[2]), existing_name);
    assert_eq!(registry.name_id(remap.symbols[3]), existing_name);
    assert!(registry.is_canonical_id(remap.symbols[2]));
    assert!(!registry.is_canonical_id(remap.symbols[3]));
}

#[test]
fn restore_dump_symbol_table_supports_multiple_independent_layouts() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();

    let first = registry
        .restore_dump_symbol_table(
            &[
                unibyte_name(b"nil"),
                unibyte_name(b"t"),
                unibyte_name(b"foo"),
                unibyte_name(b"bar"),
            ],
            &[0, 1, 2, 3],
            Some(&[true, true, true, true]),
        )
        .expect("first dump symbol table should restore");

    let second = registry
        .restore_dump_symbol_table(
            &[
                unibyte_name(b"nil"),
                unibyte_name(b"t"),
                unibyte_name(b"bar"),
                unibyte_name(b"foo"),
            ],
            &[0, 1, 2, 3],
            Some(&[true, true, true, true]),
        )
        .expect("second dump symbol table should restore");

    assert_eq!(registry.resolve(first.symbols[2]), "foo");
    assert_eq!(registry.resolve(first.symbols[3]), "bar");
    assert_eq!(registry.resolve(second.symbols[2]), "bar");
    assert_eq!(registry.resolve(second.symbols[3]), "foo");
    assert_eq!(first.symbols[2], second.symbols[3]);
    assert_eq!(first.symbols[3], second.symbols[2]);
}

#[test]
fn restore_dump_symbol_table_rejects_duplicate_canonical_names() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();

    let err = registry
        .restore_dump_symbol_table(
            &[
                unibyte_name(b"nil"),
                unibyte_name(b"t"),
                unibyte_name(b"dup"),
            ],
            &[0, 1, 2, 2],
            Some(&[true, true, true, true]),
        )
        .expect_err("duplicate canonical names should be rejected");

    assert!(
        err.contains("canonical symbol slots"),
        "unexpected error: {err}"
    );
    assert!(err.contains("dup"), "unexpected error: {err}");
}

#[test]
fn symbol_registry_exposes_name_ids_separately() {
    crate::test_utils::init_test_tracing();
    let mut registry = SymbolRegistry::new();
    let canonical = registry.intern("shared-name");
    let uninterned = registry.intern_uninterned("shared-name");

    let canonical_name = registry.name_id(canonical);
    let uninterned_name = registry.name_id(uninterned);

    assert_eq!(canonical_name, uninterned_name);
    assert_eq!(registry.resolve_name(canonical_name), "shared-name");
    assert_ne!(canonical, uninterned);
}