miden-base-macros 0.13.1

Provides proc macro support for Miden rollup SDK
Documentation
use std::collections::HashSet;

use syn::parse_quote;

use super::*;

#[test]
fn emits_hint_for_missing_export_type() {
    reset_export_type_registry_for_tests();
    let ty: Type = syn::parse_str("LocalType").unwrap();
    let exported = HashMap::new();
    let type_ref = map_type_to_type_ref(&ty, &exported).expect("type resolution should succeed");
    let exported_names = HashSet::new();
    let err = ensure_custom_type_defined(&type_ref, &exported_names, Span::call_site())
        .expect_err("expected failure");
    assert!(
        err.to_string().contains("add #[export_type]"),
        "error message missing hint: {err}"
    );
}

#[test]
fn allows_sdk_type_without_export_attribute() {
    reset_export_type_registry_for_tests();
    let ty: Type = syn::parse_str("Asset").unwrap();
    let exported = HashMap::new();
    let type_ref = map_type_to_type_ref(&ty, &exported).expect("asset should resolve");
    assert_eq!(type_ref.wit_name, "asset");
    assert!(!type_ref.is_custom);
    assert!(type_ref.requires_core_type_import());
    let exported_names = HashSet::new();
    ensure_custom_type_defined(&type_ref, &exported_names, Span::call_site())
        .expect("core types require no export");
}

#[test]
fn allows_wit_primitive_type_without_export_attribute() {
    reset_export_type_registry_for_tests();
    let ty: Type = syn::parse_str("u64").unwrap();
    let exported = HashMap::new();
    let type_ref = map_type_to_type_ref(&ty, &exported).expect("u64 should resolve");
    assert_eq!(type_ref.wit_name, "u64");
    assert!(!type_ref.is_custom);
    assert!(!type_ref.requires_core_type_import());
    let exported_names = HashSet::new();
    ensure_custom_type_defined(&type_ref, &exported_names, Span::call_site())
        .expect("primitive types require no export");
}

#[test]
fn struct_fields_allow_wit_primitive_types() {
    reset_export_type_registry_for_tests();
    let item: syn::ItemStruct = parse_quote! {
        struct Foo {
            first: u64,
            second: u32,
            third: u8,
        }
    };
    let def = exported_type_from_struct(&item).expect("struct definition should parse");
    let exported_names = HashSet::from([def.wit_name.clone()]);

    let ExportedTypeKind::Record { fields } = &def.kind else {
        panic!("expected record kind");
    };
    for field in fields {
        assert!(!field.ty.is_custom);
        assert!(!field.ty.requires_core_type_import());
        ensure_custom_type_defined(&field.ty, &exported_names, Span::call_site())
            .expect("primitive fields should not need #[export_type]");
    }
}

#[test]
fn maps_rust_primitive_types_to_wit_types() {
    reset_export_type_registry_for_tests();
    let exported = HashMap::new();
    let exported_names = HashSet::new();
    for (rust_type, wit_type) in [
        ("bool", "bool"),
        ("i8", "s8"),
        ("u8", "u8"),
        ("i16", "s16"),
        ("u16", "u16"),
        ("i32", "s32"),
        ("u32", "u32"),
        ("i64", "s64"),
        ("u64", "u64"),
    ] {
        let ty: Type = syn::parse_str(rust_type).unwrap();
        let type_ref = map_type_to_type_ref(&ty, &exported).expect("primitive should resolve");
        assert_eq!(type_ref.wit_name, wit_type);
        assert!(!type_ref.is_custom, "{rust_type} should not be custom");
        assert!(
            !type_ref.requires_core_type_import(),
            "{rust_type} should not require a core type import"
        );
        ensure_custom_type_defined(&type_ref, &exported_names, Span::call_site())
            .expect("primitive types require no export");
    }
}

#[test]
fn rejects_unsupported_component_primitives() {
    reset_export_type_registry_for_tests();
    let exported = HashMap::new();

    for rust_type in ["f32", "f64", "char"] {
        let ty: Type = syn::parse_str(rust_type).unwrap();
        let err = map_type_to_type_ref(&ty, &exported)
            .expect_err("unsupported primitive should be rejected");

        assert!(
            err.to_string().contains("is not supported in component interfaces yet"),
            "error message should explain unsupported primitive: {err}"
        );
    }
}

#[test]
fn rejects_unsupported_component_primitives_nested_in_option_or_result() {
    reset_export_type_registry_for_tests();
    let exported = HashMap::new();

    for rust_type in ["Option<f32>", "Option<char>", "Result<f64, u32>", "Result<u32, char>"] {
        let ty: Type = syn::parse_str(rust_type).unwrap();
        let err = map_type_to_type_ref(&ty, &exported)
            .expect_err("nested unsupported primitive should be rejected");

        assert!(
            err.to_string().contains("is not supported in component interfaces yet"),
            "error message should explain nested unsupported primitive: {err}"
        );
    }
}

#[test]
fn maps_rust_option_type_to_wit_option() {
    reset_export_type_registry_for_tests();
    let exported = HashMap::new();
    let exported_names = HashSet::new();
    let ty: Type = syn::parse_str("Option<u64>").unwrap();
    let type_ref = map_type_to_type_ref(&ty, &exported).expect("option should resolve");

    assert_eq!(type_ref.wit_name, "option<u64>");
    assert!(!type_ref.is_custom);
    assert!(!type_ref.requires_core_type_import());
    ensure_custom_type_defined(&type_ref, &exported_names, Span::call_site())
        .expect("primitive option should require no export");
}

#[test]
fn option_type_tracks_nested_core_type_imports() {
    reset_export_type_registry_for_tests();
    let exported = HashMap::new();
    let ty: Type = syn::parse_str("Option<Word>").unwrap();
    let type_ref = map_type_to_type_ref(&ty, &exported).expect("option should resolve");
    let mut imports = Vec::new();

    type_ref.add_required_core_type_imports(&mut imports);

    assert_eq!(type_ref.wit_name, "option<word>");
    assert_eq!(imports, vec!["word"]);
}

#[test]
fn option_type_validates_nested_custom_type() {
    reset_export_type_registry_for_tests();
    let exported = HashMap::new();
    let ty: Type = syn::parse_str("Option<LocalType>").unwrap();
    let type_ref = map_type_to_type_ref(&ty, &exported).expect("option should resolve");
    let exported_names = HashSet::new();
    let err = ensure_custom_type_defined(&type_ref, &exported_names, Span::call_site())
        .expect_err("expected unresolved type error");

    assert!(
        err.to_string().contains("add #[export_type]"),
        "error message missing hint: {err}"
    );
}

#[test]
fn maps_rust_result_type_to_wit_result() {
    reset_export_type_registry_for_tests();
    let exported = HashMap::new();
    let exported_names = HashSet::new();
    let ty: Type = syn::parse_str("Result<u64, Felt>").unwrap();
    let type_ref = map_type_to_type_ref(&ty, &exported).expect("result should resolve");

    assert_eq!(type_ref.wit_name, "result<u64, felt>");
    assert!(!type_ref.is_custom);
    assert!(!type_ref.requires_core_type_import());
    ensure_custom_type_defined(&type_ref, &exported_names, Span::call_site())
        .expect("result should require no unresolved custom export");
}

#[test]
fn result_type_tracks_nested_core_type_imports() {
    reset_export_type_registry_for_tests();
    let exported = HashMap::new();
    let ty: Type = syn::parse_str("Result<Word, Felt>").unwrap();
    let type_ref = map_type_to_type_ref(&ty, &exported).expect("result should resolve");
    let mut imports = Vec::new();

    type_ref.add_required_core_type_imports(&mut imports);

    assert_eq!(type_ref.wit_name, "result<word, felt>");
    assert_eq!(imports, vec!["word", "felt"]);
}

#[test]
fn result_type_validates_nested_custom_type() {
    reset_export_type_registry_for_tests();
    let exported = HashMap::new();
    let ty: Type = syn::parse_str("Result<u64, LocalType>").unwrap();
    let type_ref = map_type_to_type_ref(&ty, &exported).expect("result should resolve");
    let exported_names = HashSet::new();
    let err = ensure_custom_type_defined(&type_ref, &exported_names, Span::call_site())
        .expect_err("expected unresolved type error");

    assert!(
        err.to_string().contains("add #[export_type]"),
        "error message missing hint: {err}"
    );
}

#[test]
fn result_type_maps_unit_argument_to_wit_placeholder() {
    reset_export_type_registry_for_tests();
    let exported = HashMap::new();
    let ty: Type = syn::parse_str("Result<(), Felt>").unwrap();
    let type_ref = map_type_to_type_ref(&ty, &exported).expect("result should resolve");

    assert_eq!(type_ref.wit_name, "result<_, felt>");
}

#[test]
fn struct_field_missing_export_type_hint() {
    reset_export_type_registry_for_tests();
    let item: syn::ItemStruct = parse_quote! {
        struct Foo {
            value: LocalType,
        }
    };
    let def = exported_type_from_struct(&item).expect("struct definition should parse");
    let exported_names = HashSet::from([def.wit_name.clone()]);
    if let ExportedTypeKind::Record { fields } = &def.kind {
        let err = ensure_custom_type_defined(&fields[0].ty, &exported_names, Span::call_site())
            .expect_err("expected unresolved type error");
        assert!(
            err.to_string().contains("add #[export_type]"),
            "error message missing hint: {err}"
        );
    } else {
        panic!("expected record kind");
    }
}

#[test]
fn enum_payload_missing_export_type_hint() {
    reset_export_type_registry_for_tests();
    let item: syn::ItemEnum = parse_quote! {
        enum Foo {
            Variant(LocalType),
        }
    };
    let def = exported_type_from_enum(&item).expect("enum definition should parse");
    let exported_names = HashSet::from([def.wit_name.clone()]);
    if let ExportedTypeKind::Variant { variants } = &def.kind {
        if let Some(type_ref) = &variants[0].payload {
            let err = ensure_custom_type_defined(type_ref, &exported_names, Span::call_site())
                .expect_err("expected unresolved type error");
            assert!(
                err.to_string().contains("add #[export_type]"),
                "error message missing hint: {err}"
            );
        } else {
            panic!("expected payload");
        }
    } else {
        panic!("expected variant kind");
    }
}

#[test]
fn forward_reference_between_export_types_is_allowed() {
    reset_export_type_registry_for_tests();

    let first: syn::ItemStruct = parse_quote! {
        struct First {
            next: Second,
        }
    };
    let first_def = exported_type_from_struct(&first).expect("first struct should parse");

    let second: syn::ItemStruct = parse_quote! {
        struct Second {
            value: Felt,
        }
    };
    let second_def = exported_type_from_struct(&second).expect("second struct should parse");

    let exported_names = HashSet::from([first_def.wit_name.clone(), second_def.wit_name.clone()]);

    if let ExportedTypeKind::Record { fields } = &first_def.kind {
        ensure_custom_type_defined(&fields[0].ty, &exported_names, Span::call_site())
            .expect("forward reference should resolve once type is exported");
    } else {
        panic!("expected record kind");
    }
}