syn-canon 0.5.2

Canonical form of a syn file, equal across formatting drift and local binder names
Documentation
//! Compound assignment to proven fixed-width integer and `bool` locals.

#[path = "support/view.rs"]
mod view;

use syn_canon::ContextKind;

fn parse(source: &str) -> syn::File {
    syn::parse_str(source).unwrap_or_else(|error| panic!("{source}\n{error}"))
}

fn equivalent(written: &str, expanded: &str) {
    let left = syn_canon::canonicalize(parse(written));
    let right = syn_canon::canonicalize(parse(expanded));
    assert_eq!(left, right, "{written}\n{expanded}");
    assert_eq!(left.leaf_tokens(), right.leaf_tokens(), "{written}");
    assert_eq!(left.body_units(), right.body_units(), "{written}");
    assert_ne!(
        syn_canon::canonicalize_failing(parse(written)),
        syn_canon::canonicalize_failing(parse(expanded)),
        "{written}\n{expanded}"
    );
}

fn separate(written: &str, expanded: &str) {
    for canonicalize in [syn_canon::canonicalize, syn_canon::canonicalize_failing] {
        assert_ne!(
            canonicalize(parse(written)),
            canonicalize(parse(expanded)),
            "{written}\n{expanded}"
        );
    }
}

fn function(ty: &str, statement: &str) -> String {
    format!("fn f(mut x: {ty}, y: {ty}) -> {ty} {{ {statement}; x }}")
}

const INTEGER_OPERATORS: [(&str, &str); 10] = [
    ("+=", "+"),
    ("-=", "-"),
    ("*=", "*"),
    ("/=", "/"),
    ("%=", "%"),
    ("&=", "&"),
    ("|=", "|"),
    ("^=", "^"),
    ("<<=", "<<"),
    (">>=", ">>"),
];

#[test]
fn every_integer_operator_matches_its_expanded_assignment() {
    for ty in [
        "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128",
    ] {
        for (compound, plain) in INTEGER_OPERATORS {
            equivalent(
                &function(ty, &format!("x {compound} y")),
                &function(ty, &format!("x = x {plain} y")),
            );
        }
    }
}

#[test]
fn bitwise_operators_match_on_booleans() {
    for (compound, plain) in [("&=", "&"), ("|=", "|"), ("^=", "^")] {
        equivalent(
            &function("bool", &format!("x {compound} y")),
            &function("bool", &format!("x = x {plain} y")),
        );
    }
    separate(&function("bool", "x += y"), &function("bool", "x = x + y"));
}

#[test]
fn literals_locals_and_shift_amounts_of_another_type_match() {
    for (written, expanded) in [
        (
            "fn f(mut x: u32) -> u32 { x += 1; x }",
            "fn f(mut x: u32) -> u32 { x = x + 1; x }",
        ),
        (
            "fn f(x: u64, y: u64) -> u64 { let mut v = x; v -= y; v }",
            "fn f(x: u64, y: u64) -> u64 { let mut v = x; v = v - y; v }",
        ),
        (
            "fn f(mut x: u8, y: u32) -> u8 { x <<= y; x }",
            "fn f(mut x: u8, y: u32) -> u8 { x = x << y; x }",
        ),
        (
            "fn f(mut x: i16) -> i16 { x >>= 3; x }",
            "fn f(mut x: i16) -> i16 { x = x >> 3; x }",
        ),
    ] {
        equivalent(written, expanded);
    }
}

#[test]
fn targets_keep_their_type_through_assignments_and_nested_blocks() {
    for (written, expanded) in [
        (
            "fn f(mut x: u32, y: u32) -> u32 { x += y; x *= y; x }",
            "fn f(mut x: u32, y: u32) -> u32 { x = x + y; x = x * y; x }",
        ),
        (
            "fn f(x: u32, y: u32) -> u32 { let mut v = x; v ^= y; v |= x; v }",
            "fn f(x: u32, y: u32) -> u32 { let mut v = x; v = v ^ y; v = v | x; v }",
        ),
        (
            "fn f(y: u32) -> u32 { let mut v = 0u32; for _ in 0..3u32 { v += y; v ^= y; } v }",
            "fn f(y: u32) -> u32 { let mut v = 0u32; for _ in 0..3u32 { v = v + y; v = v ^ y; } v }",
        ),
        (
            "fn f(p: bool, q: bool) -> bool { let mut all = p; if q { all &= q; } all }",
            "fn f(p: bool, q: bool) -> bool { let mut all = p; if q { all = all & q; } all }",
        ),
    ] {
        equivalent(written, expanded);
    }
}

#[test]
fn expanded_values_take_the_existing_operand_order() {
    for (written, expanded) in [
        ("x ^= y", "x = y ^ x"),
        ("x &= y", "x = y & x"),
        ("x |= y ^ x", "x = x | (x ^ y)"),
    ] {
        equivalent(&function("u32", written), &function("u32", expanded));
    }
}

#[test]
fn accepted_total_helpers_are_proven_operands() {
    let helper = "fn h(x: u32) -> u32 { x & 7u32 }";
    equivalent(
        &format!("{helper} fn f(mut x: u32, y: u32) -> u32 {{ x += h(y); x }}"),
        &format!("{helper} fn f(mut x: u32, y: u32) -> u32 {{ x = x + h(y); x }}"),
    );
}

#[test]
fn places_beyond_a_local_keep_the_compound_operator() {
    for (written, expanded) in [
        (
            "struct S { x: u32 } fn f(mut s: S, y: u32) -> u32 { s.x += y; s.x }",
            "struct S { x: u32 } fn f(mut s: S, y: u32) -> u32 { s.x = s.x + y; s.x }",
        ),
        (
            "fn f(mut a: [u32; 1], y: u32) -> u32 { a[0] += y; a[0] }",
            "fn f(mut a: [u32; 1], y: u32) -> u32 { a[0] = a[0] + y; a[0] }",
        ),
        (
            "fn f(x: &mut u32, y: u32) { *x += y; }",
            "fn f(x: &mut u32, y: u32) { *x = *x + y; }",
        ),
        (
            "static mut X: u32 = 0; fn f(y: u32) { unsafe { X += y; } }",
            "static mut X: u32 = 0; fn f(y: u32) { unsafe { X = X + y; } }",
        ),
    ] {
        separate(written, expanded);
    }
}

#[test]
fn unproven_types_and_operands_keep_the_compound_operator() {
    for ty in ["f32", "f64", "usize", "isize", "char", "String", "Unknown"] {
        separate(&function(ty, "x += y"), &function(ty, "x = x + y"));
    }
    for (written, expanded) in [
        (
            "fn f(mut x: u32, y: &u32) -> u32 { x += y; x }",
            "fn f(mut x: u32, y: &u32) -> u32 { x = x + y; x }",
        ),
        (
            "fn f(mut x: u32) -> u32 { x += read(); x }",
            "fn f(mut x: u32) -> u32 { x = x + read(); x }",
        ),
        (
            "fn f(mut x: u8, y: usize) -> u8 { x <<= y; x }",
            "fn f(mut x: u8, y: usize) -> u8 { x = x << y; x }",
        ),
    ] {
        separate(written, expanded);
    }
}

#[test]
fn observed_assignments_keep_their_source() {
    for source in [
        "#[observe] fn f(mut x: u32, y: u32) -> u32 { x += y; x }",
        "fn f(mut x: u32, y: u32) -> u32 { #[observe] { x += y; } x }",
        "fn f(mut x: u32, y: u32) { unknown!(x += y); }",
    ] {
        separate(source, &source.replace("x += y", "x = x + y"));
    }
}

#[test]
fn function_views_expand_proven_targets_and_lexical_bodies_keep_unknown_types() {
    let file = parse(
        "fn first(mut x: u32, y: u32) -> u32 { x *= y; x } fn second(mut a: u32, b: u32) -> u32 { a = a * b; a }",
    );
    assert_eq!(
        view::view_of(&file, "first"),
        view::view_of(&file, "second")
    );
    let file = parse(
        "fn first(mut x: Unknown, y: Unknown) -> Unknown { x *= y; x } fn second(mut x: Unknown, y: Unknown) -> Unknown { x = x * y; x }",
    );
    assert_ne!(
        view::view_of(&file, "first"),
        view::view_of(&file, "second")
    );
    let mut first = None;
    let mut second = None;
    syn_canon::contexts(&file, &mut |context, form| {
        if context.kind == ContextKind::FunctionBody {
            match context.item {
                "first" => first = Some(form),
                "second" => second = Some(form),
                _ => {}
            }
        }
    });
    assert_ne!(
        first.expect("first function body"),
        second.expect("second function body")
    );
}