serc-cli 0.1.2

An experimental CPython bytecode compiler, written in Rust.
use serc::{CompileError, CompileOptions, compile_with_options};

use crate::common;

#[test]
fn rejects_transfers_leaving_except_star_before_discarding_code() {
    let mut sources = vec![
        "def f():\n    try: pass\n    except* Exception: return\n".to_owned(),
        "def f():\n    try: pass\n    except* Exception: return value\n".to_owned(),
        "def f():\n    return\n    try: pass\n    except* Exception: return\n".to_owned(),
        "def f():\n    try: pass\n    except* Exception:\n        if False: return\n".to_owned(),
        "def f():\n    try: pass\n    except* Exception:\n        try: pass\n        except Exception: return\n".to_owned(),
        "def f():\n    try: pass\n    except* Exception:\n        try: pass\n        finally: return\n".to_owned(),
        "def f():\n    try: pass\n    except* Exception:\n        with manager: return\n".to_owned(),
    ];
    for transfer in ["break", "continue"] {
        sources.extend([
            format!("try: pass\nexcept* Exception: {transfer}\n"),
            format!("while condition:\n    try: pass\n    except* Exception: {transfer}\n"),
            format!("while False:\n    try: pass\n    except* Exception: {transfer}\n"),
            format!("while condition:\n    try: pass\n    except* Exception:\n        for x in xs: pass\n        else: {transfer}\n"),
            format!("try: pass\nexcept* Exception:\n    while condition:\n        try: pass\n        except* Exception: {transfer}\n"),
        ]);
    }
    for version in common::PYTHON_VERSIONS {
        for source in &sources {
            let native = common::python_command(version)
                .args([
                    "-c",
                    "import sys; compile(sys.argv[1], 'except_star.py', 'exec')",
                    source,
                ])
                .output()
                .unwrap();
            let message = "'break', 'continue' and 'return' cannot appear in an except* block";
            assert!(!native.status.success(), "{version}: {source}");
            assert!(
                String::from_utf8_lossy(&native.stderr)
                    .contains(&format!("SyntaxError: {message}")),
                "{version}: {source}: {native:?}"
            );
            let error = compile_with_options(
                source,
                "except_star.py",
                &CompileOptions {
                    python_version: version,
                    ..CompileOptions::default()
                },
            )
            .unwrap_err();
            let CompileError::InvalidSyntax {
                message: actual,
                range,
            } = error
            else {
                panic!("{version}: {source}: {error:?}");
            };
            assert_eq!(actual, message, "{version}: {source}");
            assert!(
                ["break", "continue", "return"]
                    .iter()
                    .any(|keyword| source[range].starts_with(keyword)),
                "{version}: {source}: {range:?}"
            );
        }
    }
}

#[test]
fn except_star_boundaries_preserve_valid_code_and_nested_scope_diagnostics() {
    let mut sources = vec![
        "def f():\n    try: return\n    except* Exception: pass\n".to_owned(),
        "def f():\n    try: pass\n    except* Exception: pass\n    else: return\n".to_owned(),
        "def f():\n    try: pass\n    except* Exception: pass\n    finally: return\n".to_owned(),
        "def f():\n    try: pass\n    except* Exception: pass\n    return\n".to_owned(),
        "def f():\n    try: pass\n    except Exception: return\n".to_owned(),
        "try: pass\nexcept* Exception:\n    def f(): return value\n    async def g(): return value\n    class C:\n        def method(self): return value\n    value = lambda: result\n".to_owned(),
        "try: pass\nexcept* Exception:\n    @decorate(lambda: value)\n    def f(x=(lambda: value)()): return x\n    class C((lambda: object)()): pass\n".to_owned(),
        "async def f():\n    try: pass\n    except* Exception:\n        async for x in xs:\n            if x: break\n            continue\n".to_owned(),
    ];
    for transfer in ["break", "continue"] {
        sources.extend([
            format!("try: pass\nexcept* Exception:\n    while condition: {transfer}\n"),
            format!("try: pass\nexcept* Exception:\n    for x in xs: {transfer}\n"),
            format!("try: pass\nexcept* Exception:\n    class C:\n        while condition: {transfer}\n"),
            format!("try: pass\nexcept* Exception:\n    while condition:\n        try: pass\n        except* Exception: pass\n        {transfer}\n"),
            format!("try: pass\nexcept* Exception:\n    while condition:\n        for x in xs: pass\n        else: {transfer}\n"),
            format!("while condition:\n    try: pass\n    except* Exception: pass\n    {transfer}\n"),
            format!("while condition:\n    try: pass\n    except Exception: {transfer}\n"),
        ]);
    }
    for version in common::PYTHON_VERSIONS {
        let options = CompileOptions {
            python_version: version,
            ..CompileOptions::default()
        };
        for source in &sources {
            let native = common::python_command(version)
                .args([
                    "-c",
                    "import sys; compile(sys.argv[1], 'except_star.py', 'exec')",
                    source,
                ])
                .output()
                .unwrap();
            assert!(native.status.success(), "{version}: {source}: {native:?}");
            compile_with_options(source, "except_star.py", &options)
                .unwrap_or_else(|error| panic!("{version}: {source}: {error:?}"));
        }
        for (source, message) in [
            (
                "try: pass\nexcept* Exception: return\n",
                "'return' outside function",
            ),
            (
                "def f():\n    try: pass\n    except* Exception:\n        class C: return\n",
                "'return' outside function",
            ),
            (
                "while condition:\n    try: pass\n    except* Exception:\n        def f(): break\n",
                "'break' outside loop",
            ),
            (
                "while condition:\n    try: pass\n    except* Exception:\n        class C: continue\n",
                "'continue' not properly in loop",
            ),
        ] {
            let native = common::python_command(version)
                .args([
                    "-c",
                    "import sys; compile(sys.argv[1], 'except_star.py', 'exec')",
                    source,
                ])
                .output()
                .unwrap();
            assert!(!native.status.success(), "{version}: {source}");
            assert!(
                String::from_utf8_lossy(&native.stderr)
                    .contains(&format!("SyntaxError: {message}"))
            );
            let error = compile_with_options(source, "except_star.py", &options).unwrap_err();
            assert!(
                matches!(error, CompileError::InvalidSyntax { message: actual, .. } if actual == message)
            );
        }
    }
}

#[test]
fn invalid_return_expressions_precede_except_star_transfer_errors() {
    for version in common::PYTHON_VERSIONS {
        for (expression, native_message, expected_message) in [
            (
                "*values",
                "can't use starred expression here",
                "Starred expression cannot be used here",
            ),
            (
                "await value",
                "'await' outside async function",
                "'await' outside async function",
            ),
        ] {
            let source =
                format!("def f():\n    try: pass\n    except* Exception: return {expression}\n");
            let native = common::python_command(version)
                .args([
                    "-c",
                    "import sys; compile(sys.argv[1], 'except_star.py', 'exec')",
                    &source,
                ])
                .output()
                .unwrap();
            assert!(!native.status.success(), "{version}: {source}");
            assert!(
                String::from_utf8_lossy(&native.stderr)
                    .contains(&format!("SyntaxError: {native_message}"))
            );
            let error = compile_with_options(
                &source,
                "except_star.py",
                &CompileOptions {
                    python_version: version,
                    ..CompileOptions::default()
                },
            )
            .unwrap_err();
            assert!(
                matches!(error, CompileError::InvalidSyntax { message, .. } if message == expected_message)
            );
        }
    }
}