serc-cli 0.1.2

An experimental CPython bytecode compiler, written in Rust.
mod common;

use serc::{CompileOptions, PythonVersion, compile_bytes_with_options, compile_with_options};

#[test]
fn reserved_debug_writes_match_target_rules() {
    let cases = [
        ("obj.__debug__ = 1\n", "store"),
        ("obj.__debug__ += 1\n", "modern"),
        ("obj.__debug__: int\n", "store"),
        ("obj.child.__debug__ = 1\n", "store"),
        ("obj.__debug__, other = values\n", "store"),
        ("for obj.__debug__ in values: pass\n", "store"),
        ("with manager() as obj.__debug__: pass\n", "store"),
        ("if False:\n    obj.__debug__ = 1\n", "store"),
        ("def f():\n    return\n    obj.__debug__ = 1\n", "store"),
        ("f(__debug__=1)\n", "store"),
        ("f(__debug__=1, x=1, x=2)\n", "store"),
        ("f(__debug__=1, __debug__=2)\n", "store"),
        ("f(x=1, x=2, __debug__=1)\n", "duplicate-first"),
        ("f(x=1, __debug__=1, x=2)\n", "duplicate-first"),
        ("f(__debug__=1)\nx=1\nglobal x\n", "declaration-first"),
        ("obj.__debug__=1\nx=1\nglobal x\n", "declaration-first"),
        ("f(g(x=1, x=2), __debug__=1)\n", "store"),
        ("g(x=1, x=2)(__debug__=1)\n", "store"),
        ("f(x=g(y=1, y=2), __debug__=1)\n", "store"),
        ("class C(__debug__=1, x=1, x=2): pass\n", "store"),
        ("class C(x=1, x=2, __debug__=1): pass\n", "duplicate-first"),
        (
            "@f(x=1, x=2)\nclass C(__debug__=1): pass\n",
            "duplicate-first",
        ),
        ("class C(__debug__=1):\n    g(y=1, y=2)\n", "body-duplicate"),
        (
            "class C[T](__debug__=1):\n    g(y=1, y=2)\n",
            "body-duplicate",
        ),
        ("class C(__debug__=1):\n    break\n", "body-break"),
        ("class C(g(y=1, y=2), __debug__=1): pass\n", "store"),
        ("@f(y=1,y=2)\nclass C(x=1,x=2): pass\n", "repeated-y"),
        ("class C[T: f(y=1,y=2)](x=1,x=2): pass\n", "repeated-y"),
        ("class C(f(y=1,y=2),x=1,x=2): pass\n", "repeated-x"),
        ("class C(x=1,x=2): f(y=1,y=2)\n", "repeated-y"),
        ("class C(x=1,x=2): pass\nf(y=1,y=2)\n", "repeated-x"),
        ("f(y=1,y=2)\nclass C(x=1,x=2): pass\n", "repeated-y"),
        (
            "class C[T: f(y=1,y=2)](x=1,x=2): f(z=1,z=2)\n",
            "repeated-y",
        ),
        ("class C(f(y=1,y=2),x=1,x=2): f(z=1,z=2)\n", "repeated-z"),
        (
            "class C(__debug__=1,x=1,x=2): f(y=1,y=2)\n",
            "body-duplicate",
        ),
        ("class C(x=1,x=2): pass\nx=1\nglobal x\n", "declaration"),
        ("class C[T,T](x=1,x=2): pass\n", "duplicate-type"),
        ("f(**values, __debug__=1)\n", "store"),
        ("class C(__debug__=1): pass\n", "store"),
        ("class C[T](__debug__=1): pass\n", "store"),
        ("match obj:\n    case C(__debug__=value): pass\n", "store"),
        ("def f(value=g(__debug__=1)): pass\n", "store"),
        ("value = lambda: f(__debug__=1)\n", "store"),
        ("del obj.__debug__\n", "delete"),
        (
            "from __future__ import annotations\nvalue: f(__debug__=1)\n",
            "modern",
        ),
        ("def f():\n    value: g(__debug__=1)\n", "modern"),
        (
            "from __future__ import annotations\nvalue: (lambda: f(__debug__=1))\n",
            "modern",
        ),
        ("result = obj.__debug__\n", "ok"),
        ("obj.__debug__.value = 1\n", "ok"),
        ("obj.__debug__[0] = 1\n", "ok"),
        ("setattr(obj, '__debug__', 1)\n", "ok"),
        ("f(**{'__debug__': 1})\n", "ok"),
        ("class C(**{'__debug__': 1}): pass\n", "ok"),
    ];
    let directory = tempfile::tempdir().unwrap();
    let path = directory.path().join("reserved_debug.py");
    for version in common::PYTHON_VERSIONS {
        for (source, kind) in cases {
            let modern = matches!(version, PythonVersion::Py314 | PythonVersion::Py315);
            let expected = match kind {
                "store" => Some("cannot assign to __debug__"),
                "modern" | "duplicate-first" | "body-duplicate" | "body-break"
                | "declaration-first"
                    if modern =>
                {
                    Some("cannot assign to __debug__")
                }
                "duplicate-first" => Some("keyword argument repeated: x"),
                "body-duplicate" => Some("keyword argument repeated: y"),
                "body-break" => Some("'break' outside loop"),
                "repeated-x" => Some("keyword argument repeated: x"),
                "repeated-y" => Some("keyword argument repeated: y"),
                "repeated-z" => Some("keyword argument repeated: z"),
                "declaration" | "declaration-first" => {
                    Some("name 'x' is assigned to before global declaration")
                }
                "duplicate-type" => Some("duplicate type parameter 'T'"),
                "delete" if modern => Some("cannot delete __debug__"),
                _ => None,
            };
            common::diagnostics::assert_compilation(source, &path, version, expected);
        }
    }
}

#[test]
fn reserved_debug_preserves_competing_error_order_and_ranges() {
    let cases = [
        "class C(__debug__=1):\n    f(x=1, x=2)\n",
        "class C(x=1, x=2):\n    obj.__debug__ = 1\n",
        "f(x=1, x=2)\nfrom __future__ import annotations\nmatch value:\n    case __debug__: pass\n",
        "f(x=1, x=2)\nmatch value:\n    case __debug__: pass\nfrom __future__ import annotations\n",
    ];
    let oracle = r"
import sys
source = sys.argv[1]
try:
    compile(source, 'reserved.py', 'exec', dont_inherit=True)
except SyntaxError as error:
    lines = source.splitlines(keepends=True)
    def pos(line, column):
        return sum(len(part.encode()) for part in lines[:line - 1]) + len(lines[line - 1][:column - 1].encode())
    print(error.msg)
    print(pos(error.lineno, error.offset))
    print(pos(error.end_lineno, error.end_offset))
else:
    raise AssertionError('expected a syntax error')
";
    for version in common::PYTHON_VERSIONS {
        let options = CompileOptions {
            python_version: version,
            ..CompileOptions::default()
        };
        for source in cases {
            let mut python = common::python_command(version);
            let output = python.args(["-c", oracle, source]).output().unwrap();
            assert!(output.status.success(), "{version}: {source}: {output:?}");
            let stdout = String::from_utf8(output.stdout).unwrap();
            let mut expected = stdout.lines();
            let message = expected.next().unwrap();
            let start: u32 = expected.next().unwrap().parse().unwrap();
            let end: u32 = expected.next().unwrap().parse().unwrap();
            for error in [
                compile_with_options(source, "reserved.py", &options).unwrap_err(),
                compile_bytes_with_options(source.as_bytes(), "reserved.py", &options).unwrap_err(),
            ] {
                assert!(
                    error.to_string().ends_with(message),
                    "{version}: {source}: {error}"
                );
                let range = error.range().unwrap();
                assert_eq!(u32::from(range.start()), start, "{version}: {source}");
                assert_eq!(u32::from(range.end()), end, "{version}: {source}");
            }
        }
    }
}