wolfram-expr 0.6.0-alpha.2

Efficient and ergonomic representation of Wolfram expressions in Rust
Documentation
use crate::symbol::{ContextRef, RelativeContext, SymbolNameRef, SymbolRef};
use crate::{
    expr, Association, ByteArray, Expr, ExprKind, NumericArray, NumericArrayEnum,
    PackedArray, PackedArrayEnum, Symbol,
};

/// `(input, is Symbol, is SymbolName, is Context, is RelativeContext)`
#[rustfmt::skip]
const DATA: &[(&str, bool, bool, bool, bool)] = &[
    // Symbol-like
    ("foo`bar",     true , false, false, false),
    ("foo`bar`baz", true , false, false, false),
    ("foo`bar5",    true , false, false, false),
    ("foo`5bar",    false, false, false, false),
    ("5foo`bar",    false, false, false, false),
    ("foo``bar",    false, false, false, false),
    ("foo`$bar",    true , false, false, false),
    ("$foo`$bar",   true , false, false, false),
    ("$foo`$$$",    true , false, false, false),
    ("$$$`$$$",     true , false, false, false),

    // SymbolName-like
    ("foo",         false, true,  false, false),
    ("foo5",        false, true,  false, false),
    ("foo5bar",     false, true,  false, false),
    ("$foo",        false, true,  false, false),
    ("5foo",        false, false, false, false),
    ("foo_bar",     false, false, false, false),
    ("_foo",        false, false, false, false),

    // TODO: RelativeSymbol-like
    ("`foo",        false, false, false, false),
    ("`foo`bar",    false, false, false, false),

    // Context-like
    ("foo`",        false, false, true,  false),
    ("foo`bar`",    false, false, true,  false),

    // RelativeContext-like
    ("`foo`",       false, false, false, true),
    ("`foo`bar`",   false, false, false, true),
];

#[test]
pub fn test_symbol_like_parsing() {
    for (input, is_symbol, is_symbol_name, is_context, is_rel_context) in
        DATA.iter().copied()
    {
        println!("input: {input}");
        assert_eq!(SymbolRef::try_new(input).is_some(), is_symbol);
        assert_eq!(SymbolNameRef::try_new(input).is_some(), is_symbol_name);
        assert_eq!(ContextRef::try_new(input).is_some(), is_context);
        assert_eq!(RelativeContext::try_new(input).is_some(), is_rel_context);
    }
}

//==========================================================================
// New WXF-derived ExprKind variants — construct, extract, equality, Display
//==========================================================================

#[test]
fn byte_array_variant_roundtrip() {
    let ba = ByteArray::from(vec![0x01, 0x02, 0x03, 0xff]);
    let expr = Expr::from(ba.clone());
    match expr.kind() {
        ExprKind::ByteArray(got) => assert_eq!(got, &ba),
        other => panic!("expected ByteArray, got {:?}", other),
    }
    assert!(expr.tag().is_none());
}

#[test]
fn association_variant_roundtrip() {
    use crate::RuleEntry;
    let a: Association = vec![
        RuleEntry::rule(Expr::from("k1"), Expr::from(1)),
        RuleEntry::rule_delayed(Expr::from("k2"), Expr::from(2)),
    ];
    let expr = Expr::from(a.clone());
    let ExprKind::Association(extracted) = expr.kind() else {
        panic!("expected Association, got {:?}", expr.kind());
    };
    assert_eq!(extracted, &a);
    let mut it = extracted.iter();
    let e0 = it.next().unwrap();
    assert_eq!(e0.key, Expr::from("k1"));
    assert_eq!(e0.value, Expr::from(1));
    assert!(!e0.delayed);
    let e1 = it.next().unwrap();
    assert_eq!(e1.key, Expr::from("k2"));
    assert_eq!(e1.value, Expr::from(2));
    assert!(e1.delayed);
    assert!(it.next().is_none());
}

#[test]
fn numeric_array_variant_roundtrip() {
    let arr = NumericArray::from_slice::<i32>(vec![2, 2], &[10, 20, 30, 40]);
    let expr = Expr::from(arr.clone());
    let ExprKind::NumericArray(got) = expr.kind() else {
        panic!("expected NumericArray, got {:?}", expr.kind());
    };
    assert_eq!(got.dimensions(), &[2, 2]);
    assert_eq!(got.data_type(), NumericArrayEnum::Integer32);
    assert_eq!(got.try_as_slice::<i32>(), Some([10, 20, 30, 40].as_slice()));
}

#[test]
fn packed_array_variant_roundtrip() {
    let arr = PackedArray::from_slice::<f64>(vec![3], &[1.0, 2.0, 3.0]);
    let expr = Expr::from(arr.clone());
    let ExprKind::PackedArray(got) = expr.kind() else {
        panic!("expected PackedArray, got {:?}", expr.kind());
    };
    assert_eq!(got.dimensions(), &[3]);
    assert_eq!(got.data_type(), PackedArrayEnum::Real64);
    assert_eq!(got.try_as_slice::<f64>(), Some([1.0, 2.0, 3.0].as_slice()));
}

#[test]
fn new_variants_have_no_tag() {
    // Symbol → has tag.
    let sym = expr!(Global::x);
    assert!(sym.tag().is_some());

    // Atom-like new variants → no tag (matching the existing convention for
    // Integer/Real/String, which also return None).
    let ba = Expr::from(ByteArray::from(vec![1, 2, 3]));
    let na = Expr::from(NumericArray::from_slice::<i64>(vec![3], &[1, 2, 3]));
    let pa = Expr::from(PackedArray::from_slice::<i64>(vec![3], &[1, 2, 3]));
    let assoc = Expr::from(Association::new());
    assert!(ba.tag().is_none());
    assert!(na.tag().is_none());
    assert!(pa.tag().is_none());
    assert!(assoc.tag().is_none());
}

#[test]
fn new_variants_have_no_normal_head() {
    let ba = Expr::from(ByteArray::new());
    let na = Expr::from(NumericArray::from_slice::<u8>(vec![0], &[]));
    assert!(ba.normal_head().is_none());
    assert!(na.normal_head().is_none());
}

#[test]
fn display_of_new_variants_is_non_empty() {
    let ba = Expr::from(ByteArray::from(vec![0xab]));
    let assoc = {
        use crate::RuleEntry;
        let a: Association = vec![RuleEntry::rule(Expr::from("k"), Expr::from(1))];
        Expr::from(a)
    };
    let na = Expr::from(NumericArray::from_slice::<u8>(vec![1], &[42]));
    let pa = Expr::from(PackedArray::from_slice::<i32>(vec![1], &[42]));
    assert!(format!("{}", ba).starts_with("ByteArray[\"") && format!("{}", ba).ends_with("\"]"));
    assert!(
        format!("{}", assoc).starts_with("<|") && format!("{}", assoc).ends_with("|>")
    );
    assert!(format!("{}", na).starts_with("BinaryDeserialize[ByteArray[\""));
    assert!(format!("{}", pa).starts_with("BinaryDeserialize[ByteArray[\""));
}
#[test]
fn display_uses_wl_surface_syntax() {
    // Known heads (System`-qualified or context-less) render with their WL
    // surface syntax instead of `Head[…]`.
    assert_eq!(expr!(System::List[1, 2, 3]).to_string(), "{1, 2, 3}");
    assert_eq!(expr!(::List[1, 2]).to_string(), "{1, 2}");
    assert_eq!(expr!(System::Rule[1, 2]).to_string(), "1 -> 2");
    assert_eq!(expr!(System::RuleDelayed[1, 2]).to_string(), "1 :> 2");
    assert_eq!(expr!(System::Set[1, 2]).to_string(), "1 = 2");

    // Slots: positional → `#n`, named → bare `#foo` (not `#"foo"`).
    assert_eq!(expr!(System::Slot[1]).to_string(), "#1");
    assert_eq!(expr!(::Slot["foo"]).to_string(), "#foo");
    assert_eq!(expr!(System::SlotSequence[1]).to_string(), "##1");
    assert_eq!(expr!(::SlotSequence["bar"]).to_string(), "##bar");

    // Unknown heads, known heads with a non-infix arity, and slots whose
    // argument is neither a number nor a string all fall back to `head[…]`.
    assert_eq!(expr!(System::Foo[1, 2]).to_string(), "System`Foo[1, 2]");
    assert_eq!(expr!(System::Set[1, 2, 3]).to_string(), "System`Set[1, 2, 3]");
    assert_eq!(expr!(System::Slot[System::x]).to_string(), "System`Slot[System`x]");
    assert_eq!(expr!(System::Slot[1, 2]).to_string(), "System`Slot[1, 2]");
}

#[test]
fn big_integer_variant_roundtrip() {
    use crate::BigInteger;
    let huge = BigInteger("999999999999999999999999999999".into());
    let expr = Expr::from(huge.clone());
    match expr.kind() {
        ExprKind::BigInteger(n) => assert_eq!(n, &huge),
        other => panic!("expected BigInteger, got {:?}", other),
    }
}
#[test]
fn expr_macro_nested_head_in_arg() {
    // Style[Foo[2]] — nested function call in arg position
    let by_macro = expr!(System::Style[System::Foo[2]]);
    let by_hand = Expr::normal(
        Symbol::new("System`Style"),
        vec![Expr::normal(Symbol::new("System`Foo"), vec![Expr::from(2)])],
    );
    assert_eq!(by_macro, by_hand);

    // Deeply nested: Style[Foo[Bar[3]], "x" -> "y"]
    let by_macro = expr!(System::Style[System::Foo[System::Bar[3]], "x" -> "y"]);
    let by_hand = Expr::normal(
        Symbol::new("System`Style"),
        vec![
            Expr::normal(
                Symbol::new("System`Foo"),
                vec![Expr::normal(Symbol::new("System`Bar"), vec![Expr::from(3)])],
            ),
            Expr::normal(
                Symbol::new("System`Rule"),
                vec![Expr::from("x"), Expr::from("y")],
            ),
        ],
    );
    assert_eq!(by_macro, by_hand);
}

#[test]
fn expr_macro_inline_rule() {
    // expr!(Style[col, "FontFamily" -> "Courier"]) must equal the hand-built form
    let col = Expr::from("content");
    let by_macro = expr!(System::Style[col, "FontFamily" -> "Courier"]);

    let col = Expr::from("content");
    let by_hand = Expr::normal(
        Symbol::new("System`Style"),
        vec![
            col,
            Expr::normal(
                Symbol::new("System`Rule"),
                vec![Expr::from("FontFamily"), Expr::from("Courier")],
            ),
        ],
    );

    assert_eq!(by_macro, by_hand);
}

#[test]
fn expr_macro_contextless_symbols_nest() {
    // `::Name` context-less symbols must work in nested arg and splice
    // positions — not only at the top level. `::$Name` covers `$`-prefixed
    // WL system symbols (the `$` is pasted back onto the name).
    let items = vec![Expr::from(1), Expr::from(2)];
    let by_macro = expr!(::List[::Inner["x", ::Bare], ::List[..items], ::$Context]);

    let no_ctx = |name: &str, args: Vec<Expr>| {
        // Symbol::new stores the bare name verbatim — no context prefix.
        Expr::normal(Symbol::new(name), args)
    };
    let by_hand = no_ctx(
        "List",
        vec![
            no_ctx("Inner", vec![Expr::from("x"), Expr::symbol(Symbol::new("Bare"))]),
            no_ctx("List", vec![Expr::from(1), Expr::from(2)]),
            Expr::symbol(Symbol::new("$Context")),
        ],
    );
    assert_eq!(by_macro, by_hand);
    assert_eq!(format!("{}", expr!(::$InputFileName)), "$InputFileName");

    // Same forms must also work as association values.
    let assoc = expr!({"a" -> ::Inner["v"], "b" -> ::Bare});
    let ExprKind::Association(entries) = assoc.kind() else {
        panic!("expected Association, got {}", assoc);
    };
    let mut it = entries.iter();
    assert_eq!(it.next().unwrap().value, no_ctx("Inner", vec![Expr::from("v")]));
    assert_eq!(it.next().unwrap().value, Expr::symbol(Symbol::new("Bare")));
}

#[test]
fn big_real_variant_roundtrip() {
    use crate::BigReal;
    let r = BigReal("3.14159265358979323846`50.".into());
    let expr = Expr::from(r.clone());
    match expr.kind() {
        ExprKind::BigReal(s) => assert_eq!(s, &r),
        other => panic!("expected BigReal, got {:?}", other),
    }
}