use python_ast::{parse, CodeGen, CodeGenContext, PythonOptions, SymbolTableScopes};
fn compile(src: &str, name: &str) -> String {
let module = parse(src, name).unwrap_or_else(|e| panic!("parse failed for {:?}: {}", src, e));
let symbols = module.clone().find_symbols(SymbolTableScopes::new());
module
.to_rust(
CodeGenContext::Module(name.replace(".py", "")),
PythonOptions::default(),
symbols,
)
.unwrap_or_else(|e| panic!("codegen failed for {:?}: {}", src, e))
.to_string()
}
#[test]
fn power_uses_py_pow() {
let out = compile("y = 2 ** 3", "pow.py");
assert!(out.contains("py_pow"), "generated: {}", out);
assert!(!out.contains(". pow"), "generated: {}", out);
}
#[test]
fn power_aug_assign_uses_py_pow() {
let out = compile("x = 2\nx **= 3", "pow2.py");
assert!(out.contains("py_pow"), "generated: {}", out);
}
#[test]
fn list_literals_keep_element_types() {
let out = compile("nums = [1, 2, 3]", "list.py");
assert!(out.contains("vec ! [1 , 2 , 3]"), "generated: {}", out);
assert!(!out.contains("to_string"), "generated: {}", out);
}
#[test]
fn rust_keywords_are_escaped() {
let out = compile("type = 5", "kw.py");
assert!(out.contains("r#type"), "generated: {}", out);
let out = compile("def loop():\n pass\n", "kw2.py");
assert!(out.contains("fn r#loop"), "generated: {}", out);
}
#[test]
fn assignments_hoist_declaration_and_store() {
let out = compile("x = 1 + 1", "mut.py");
assert!(out.contains("let x"), "generated: {}", out);
assert!(!out.contains("let mut x"), "single store needs no mut: {}", out);
}
#[test]
fn mut_is_inferred_only_where_needed() {
let src = "def f(c) -> int:\n if c:\n x = 1\n else:\n x = 2\n return x\n";
let out = compile(src, "branches.py");
assert!(out.contains("let x ;"), "generated: {}", out);
assert!(!out.contains("let mut x"), "generated: {}", out);
let src = "def g(items):\n total = 0\n for i in items:\n total = total + i\n return total\n";
let out = compile(src, "loopmut.py");
assert!(out.contains("let mut total"), "generated: {}", out);
let out = compile("def h():\n items = []\n items.append(1)\n", "append.py");
assert!(out.contains("let mut items"), "generated: {}", out);
let out = compile("def k(n: int) -> int:\n return n\n", "readonly.py");
assert!(!out.contains("let mut n"), "generated: {}", out);
}
#[test]
fn nested_block_assignment_stores_into_the_outer_variable() {
let src = "def pick(c) -> int:\n x = 1\n if c:\n x = 2\n return x\n";
let out = compile(src, "scope.py");
assert_eq!(
out.matches("let mut x").count(),
1,
"one declaration, plain stores elsewhere: {}",
out
);
assert!(
out.contains("if (c) . is_truthy () { x = 2"),
"generated: {}",
out
);
}
#[test]
fn assigned_parameters_are_rebound_mutably() {
let out = compile("def f(n: int) -> int:\n n = n + 1\n return n\n", "param.py");
assert!(out.contains("let mut n = n"), "generated: {}", out);
}
#[test]
fn chained_assignment_assigns_each_target() {
let out = compile("a = b = 1", "chain.py");
assert!(out.contains("__rython_chain"), "generated: {}", out);
assert!(out.contains("let a"), "generated: {}", out);
assert!(out.contains("let b"), "generated: {}", out);
assert!(out.contains("a = __rython_chain"), "generated: {}", out);
assert!(out.contains("b = __rython_chain"), "generated: {}", out);
}
#[test]
fn attribute_assignment_is_not_a_let() {
let out = compile("def f(obj):\n obj.field = 1\n", "attr.py");
assert!(!out.contains("let obj . field"), "generated: {}", out);
assert!(!out.contains("let mut obj . field"), "generated: {}", out);
}
#[test]
fn for_else_tracks_break() {
let src = "for x in items:\n break\nelse:\n done()\n";
let out = compile(src, "forelse.py");
assert!(out.contains("__rython_broke = true"), "generated: {}", out);
assert!(out.contains("if ! __rython_broke"), "generated: {}", out);
}
#[test]
fn plain_for_has_no_break_flag() {
let out = compile("for x in items:\n f(x)\n", "for.py");
assert!(!out.contains("__rython_broke"), "generated: {}", out);
}
#[test]
fn while_else_tracks_break() {
let src = "while cond:\n break\nelse:\n done()\n";
let out = compile(src, "whileelse.py");
assert!(out.contains("__rython_broke = true"), "generated: {}", out);
assert!(out.contains("if ! __rython_broke"), "generated: {}", out);
}
#[test]
fn nested_loop_break_stays_plain() {
let src = "for x in items:\n for y in inner:\n break\nelse:\n done()\n";
let out = compile(src, "nested.py");
assert!(!out.contains("__rython_broke"), "generated: {}", out);
assert!(out.contains("done ()"), "generated: {}", out);
}
#[test]
fn loop_else_without_break_has_no_flag() {
let src = "for x in items:\n f(x)\nelse:\n done()\n";
let out = compile(src, "forelse2.py");
assert!(!out.contains("__rython_broke"), "generated: {}", out);
assert!(out.contains("done ()"), "generated: {}", out);
}
#[test]
fn loop_else_break_inside_if_still_tracked() {
let src = "for x in items:\n if x:\n break\nelse:\n done()\n";
let out = compile(src, "forelse3.py");
assert!(out.contains("__rython_broke = true"), "generated: {}", out);
assert!(out.contains("if ! __rython_broke"), "generated: {}", out);
}
#[test]
fn with_binds_context_manager() {
let src = "with open(name) as fh:\n read(fh)\n";
let out = compile(src, "with.py");
assert!(out.contains("let mut fh"), "generated: {}", out);
assert!(out.contains("open"), "generated: {}", out);
}
#[test]
fn with_without_target_still_evaluates() {
let src = "with lock():\n body()\n";
let out = compile(src, "with2.py");
assert!(out.contains("let _ = lock ()"), "generated: {}", out);
}
#[test]
fn comprehension_binds_target() {
let out = compile("doubled = [x * 2 for x in items]", "comp.py");
assert!(out.contains("for x in"), "generated: {}", out);
assert!(!out.contains("_item"), "generated: {}", out);
assert!(out.contains("push"), "generated: {}", out);
}
#[test]
fn comprehension_condition_uses_continue() {
let out = compile("evens = [x for x in items if x % 2 == 0]", "comp2.py");
assert!(out.contains("continue"), "generated: {}", out);
}
#[test]
fn multi_generator_comprehension_nests_loops() {
let out = compile("pairs = [x + y for x in a for y in b]", "comp3.py");
let for_count = out.matches("for ").count();
assert!(for_count >= 2, "expected nested loops, generated: {}", out);
assert!(!out.contains("vec ! []"), "generated: {}", out);
}
#[test]
fn dict_comprehension_inserts_pairs() {
let out = compile("m = {k: v for k in keys}", "comp4.py");
assert!(out.contains("insert"), "generated: {}", out);
assert!(out.contains("PyDict"), "generated: {}", out);
}
#[test]
fn fstring_builds_single_format() {
let out = compile("s = f\"Hello {name}\"", "fstr.py");
assert!(out.contains("\"Hello {}\""), "generated: {}", out);
assert!(!out.contains("\" + "), "generated: {}", out);
}
#[test]
fn fstring_maps_precision_spec() {
let out = compile("s = f\"{pi:.2f}\"", "fstr2.py");
assert!(out.contains("{:.2}"), "generated: {}", out);
}
#[test]
fn fstring_repr_conversion_uses_pythons_repr() {
let out = compile("s = f\"{val!r}\"", "fstr3.py");
assert!(out.contains("repr (& (val))"), "generated: {}", out);
assert!(!out.contains("{:?}"), "generated: {}", out);
}
#[test]
fn statements_in_blocks_are_separated() {
let src = "if cond:\n first()\n second()\n";
let out = compile(src, "sep.py");
let first = out.find("first ()").expect("first call present");
let second = out.find("second ()").expect("second call present");
let between = &out[first..second];
assert!(between.contains(';'), "no separator between calls: {}", out);
}
#[test]
fn async_calls_do_not_guess_await() {
let src = "async def f(x):\n return abs(x)\n";
let out = compile(src, "await.py");
assert!(!out.contains(". await"), "generated: {}", out);
}
#[test]
fn explicit_await_still_awaits() {
let src = "async def f(x):\n return await g(x)\n";
let out = compile(src, "await2.py");
assert!(out.contains(". await"), "generated: {}", out);
}
#[test]
fn from_import_brings_name_into_scope() {
let out = compile("from os import path", "imp.py");
assert!(out.contains("use stdpython :: os :: path ;"), "generated: {}", out);
}
#[test]
fn from_import_with_alias() {
let out = compile("from os import path as p", "imp2.py");
assert!(out.contains("use stdpython :: os :: path as p ;"), "generated: {}", out);
}
#[test]
fn lambda_parameters_are_bare_names() {
let out = compile("f = lambda x: x", "lam.py");
assert!(out.contains("| x |"), "generated: {}", out);
assert!(!out.contains("impl Into"), "generated: {}", out);
}
#[test]
fn return_type_inferred_from_int_constant() {
let out = compile("def f():\n return 42\n", "ret.py");
assert!(out.contains("-> Result < i64 , PyException >"), "generated: {}", out);
}
#[test]
fn return_type_inferred_from_fstring() {
let out = compile("def f():\n return f\"x={x}\"\n", "ret2.py");
assert!(out.contains("-> Result < String , PyException >"), "generated: {}", out);
}
#[test]
fn return_type_inferred_from_string_literal() {
let out = compile("def f():\n return \"hi\"\n", "ret3.py");
assert!(out.contains("-> Result < & 'static str , PyException >"), "generated: {}", out);
}
#[test]
fn mixed_returns_get_no_annotation() {
let out = compile("def f(c):\n if c:\n return 1\n return \"s\"\n", "ret4.py");
assert!(out.contains("-> Result < () , PyException >"), "generated: {}", out);
}
#[test]
fn bare_return_gets_no_annotation() {
let out = compile("def f():\n return\n", "ret5.py");
assert!(out.contains("-> Result < () , PyException >"), "generated: {}", out);
assert!(out.contains("return Ok (())"), "generated: {}", out);
}
#[test]
fn return_type_inferred_through_local_variable() {
let out = compile("def f():\n n = 5\n n -= 1\n return n\n", "ret6.py");
assert!(out.contains("-> Result < i64 , PyException >"), "generated: {}", out);
}
#[test]
fn partial_return_gets_no_annotation() {
let out = compile("def f(c):\n if c:\n return 1\n", "ret7.py");
assert!(!out.contains("-> i64"), "generated: {}", out);
}
#[test]
fn return_in_loop_only_gets_no_annotation() {
let out = compile("def f(items):\n for x in items:\n return 1\n", "ret8.py");
assert!(!out.contains("-> i64"), "generated: {}", out);
}
#[test]
fn exhaustive_if_else_returns_get_annotation() {
let src = "def f(c):\n if c:\n return 1\n else:\n return 2\n";
let out = compile(src, "ret9.py");
assert!(out.contains("-> Result < i64 , PyException >"), "generated: {}", out);
}
#[test]
fn annotated_parameters_map_to_rust_types() {
let out = compile("def f(a: int, b: float, c: str, d: bool):\n pass\n", "ann_params.py");
assert!(out.contains("a : i64"), "generated: {}", out);
assert!(out.contains("b : f64"), "generated: {}", out);
assert!(out.contains("c : String"), "generated: {}", out);
assert!(out.contains("d : bool"), "generated: {}", out);
assert!(!out.contains(": int"), "generated: {}", out);
}
#[test]
fn return_annotation_used_when_inference_fails() {
let out = compile("def f(x: int) -> int:\n return x + 1\n", "ann_ret.py");
assert!(out.contains("-> Result < i64 , PyException >"), "generated: {}", out);
}
#[test]
fn string_repetition_uses_multiply_string() {
let out = compile("s = \"!\" * 3", "strmul.py");
assert!(out.contains("multiply_string"), "generated: {}", out);
let out = compile("s = 3 * \"!\"", "strmul2.py");
assert!(out.contains("multiply_string"), "generated: {}", out);
let out = compile("n = 3 * 4", "nummul.py");
assert!(!out.contains("multiply_string"), "generated: {}", out);
}
#[test]
fn stdlib_from_import_anchors_to_stdpython() {
let out = compile("from os import path", "imp3.py");
assert!(out.contains("use stdpython :: os :: path ;"), "generated: {}", out);
}
#[test]
fn sibling_from_import_anchors_to_crate() {
let out = compile("from helpers import util", "imp4.py");
assert!(out.contains("use crate :: helpers :: util ;"), "generated: {}", out);
}
#[test]
fn defaulted_annotated_parameter_maps_type() {
let out = compile("def f(x: int = 0):\n return x\n", "def_param.py");
assert!(out.contains("x : i64"), "generated: {}", out);
assert!(!out.contains("Option"), "generated: {}", out);
assert!(!out.contains(": int"), "generated: {}", out);
}
#[test]
fn kwonly_annotated_parameter_maps_type() {
let out = compile("def f(*, x: int):\n pass\n", "kwonly.py");
assert!(out.contains("x : i64"), "generated: {}", out);
assert!(!out.contains(": int"), "generated: {}", out);
}
#[test]
fn annotation_ignored_when_body_can_fall_through() {
let out = compile("def f(c) -> int:\n if c:\n return 1\n", "ann_partial.py");
assert!(!out.contains("-> i64"), "generated: {}", out);
assert!(out.contains("deprecated"), "generated: {}", out);
assert!(
out.contains("return annotation was ignored")
|| out.contains("return annotation `-> int`")
|| out.contains("`-> int` return annotation"),
"warning note should name the ignored annotation: {}",
out
);
let out = compile("def g() -> int:\n return 1\n", "ann_honored.py");
assert!(!out.contains("deprecated"), "generated: {}", out);
let out = compile("def h() -> None:\n print(1)\n", "ann_none.py");
assert!(!out.contains("deprecated"), "generated: {}", out);
}
#[test]
fn try_except_lowers_to_result_handling() {
let src = concat!(
"def f(n):\n",
" try:\n",
" raise ValueError(\"bad\")\n",
" except ValueError as e:\n",
" print(e)\n",
" except (TypeError, KeyError):\n",
" print(\"other\")\n",
);
let out = compile(src, "try.py");
assert!(
out.contains("Result < () , PyException >"),
"generated: {}",
out
);
assert!(
out.contains("return Err (PyException :: new (\"ValueError\""),
"generated: {}",
out
);
assert!(
out.contains("if __rython_exc . matches (\"ValueError\")"),
"generated: {}",
out
);
assert!(
out.contains("matches (\"TypeError\") || __rython_exc . matches (\"KeyError\")"),
"generated: {}",
out
);
assert!(out.contains("let mut e = __rython_exc . clone ()"), "generated: {}", out);
assert!(
out.contains("Err (__rython_exc) => { return Err (__rython_exc) ; }"),
"generated: {}",
out
);
}
#[test]
fn try_handler_bodies_only_run_on_matching_error() {
let src = concat!(
"def f():\n",
" try:\n",
" work()\n",
" except Exception:\n",
" cleanup()\n",
);
let out = compile(src, "tryarm.py");
let arm_pos = out.find("Err (__rython_exc)").expect("handler arm");
let cleanup_pos = out.find("cleanup ()").expect("handler body");
assert!(
cleanup_pos > arm_pos,
"handler body must be inside the Err arm: {}",
out
);
}
#[test]
fn nested_raise_propagates_to_outer_try() {
let src = concat!(
"def f():\n",
" try:\n",
" try:\n",
" raise KeyError(\"k\")\n",
" except ValueError:\n",
" pass\n",
" except KeyError:\n",
" pass\n",
);
let out = compile(src, "nested_try.py");
assert!(
out.contains("Err (__rython_exc) => { return Err (__rython_exc) ; }"),
"inner unmatched exception must propagate as Err: {}",
out
);
}
#[test]
fn finally_runs_before_reraise() {
let src = concat!(
"def f():\n",
" try:\n",
" work()\n",
" except ValueError:\n",
" pass\n",
" finally:\n",
" cleanup()\n",
);
let out = compile(src, "finally.py");
assert!(out.matches("cleanup ()").count() >= 2, "generated: {}", out);
}
#[test]
fn finally_runs_before_handler_and_else_returns() {
let src = concat!(
"def f(n: int) -> int:\n",
" try:\n",
" check(n)\n",
" except ValueError:\n",
" return 0\n",
" else:\n",
" return 1\n",
" finally:\n",
" cleanup()\n",
);
let out = compile(src, "finally_handler.py");
assert_eq!(
out.matches("Ok (PyFlow :: Return (__rython_ret)) => { cleanup () ; return Ok (__rython_ret) ; }")
.count(),
2,
"handler and else returns must run the finally first: {}",
out
);
let src = concat!(
"def g(n: int):\n",
" try:\n",
" check(n)\n",
" except ValueError:\n",
" raise RuntimeError(\"rethrown\")\n",
" finally:\n",
" cleanup()\n",
);
let out = compile(src, "finally_reraise.py");
assert!(
out.contains("Err (__rython_reraise) => { cleanup () ; return Err (__rython_reraise) ; }"),
"handler raise must run the finally first: {}",
out
);
let src = concat!(
"def h(n: int) -> int:\n",
" try:\n",
" check(n)\n",
" except ValueError:\n",
" return 0\n",
" return 1\n",
);
let out = compile(src, "no_finally.py");
assert!(!out.contains("__rython_inner"), "generated: {}", out);
}
#[test]
fn awaited_async_calls_propagate_exceptions() {
let src = concat!(
"async def helper() -> int:\n",
" return 1\n",
"\n",
"async def caller() -> int:\n",
" return await helper()\n",
);
let out = compile(src, "async_prop.py");
assert!(
out.contains("helper () . await ?"),
"awaited user call must unwrap the Result: {}",
out
);
}
#[test]
fn bare_trailing_return_gets_no_unreachable_tail() {
let out = compile("def f():\n work()\n return\n", "bareret.py");
assert!(out.contains("return Ok (())"), "generated: {}", out);
assert!(
!out.contains("return Ok (()) ; Ok (())"),
"no unreachable tail after a trailing bare return: {}",
out
);
}
#[test]
fn raise_returns_err_from_the_function() {
let out = compile(
"def f():\n raise RuntimeError(\"boom\")\n",
"raise.py",
);
assert!(
out.contains("return Err (PyException :: new (\"RuntimeError\""),
"generated: {}",
out
);
assert!(!out.contains("panic !"), "generated: {}", out);
}
#[test]
fn calls_to_user_functions_propagate_with_question_mark() {
let src = concat!(
"def helper() -> int:\n",
" return 1\n",
"\n",
"def caller() -> int:\n",
" return helper() + 1\n",
);
let out = compile(src, "prop.py");
assert!(out.contains("helper () ?"), "generated: {}", out);
let out = compile("def f(x: int):\n print(x)\n", "plaincall.py");
assert!(out.contains("print (& (x))"), "generated: {}", out);
assert!(!out.contains("print (& (x)) ?"), "generated: {}", out);
}
#[test]
fn return_inside_try_threads_through_controlflow() {
let src = concat!(
"def f(n: int) -> int:\n",
" try:\n",
" return n\n",
" except ValueError:\n",
" return 0\n",
" finally:\n",
" cleanup()\n",
);
let out = compile(src, "trystmt_ret.py");
assert!(
out.contains("PyFlow :: Return (n)"),
"generated: {}",
out
);
assert!(
out.contains("Ok (PyFlow :: Return (__rython_ret)) => { cleanup () ; return Ok (__rython_ret) ; }"),
"finally must run before the returned value leaves: {}",
out
);
}
#[test]
fn assert_lowers_to_assertion_error() {
let out = compile("def f(n):\n assert n > 0, \"need positive\"\n", "assert.py");
assert!(out.contains("if ! ((n) > (0))"), "generated: {}", out);
assert!(
out.contains("PyException :: new (\"AssertionError\""),
"generated: {}",
out
);
let src = concat!(
"def f(n):\n",
" try:\n",
" assert n > 0\n",
" except AssertionError:\n",
" pass\n",
);
let out = compile(src, "assert_try.py");
assert!(
out.contains("return Err (PyException :: new (\"AssertionError\""),
"generated: {}",
out
);
}
#[test]
fn unary_plus_emits_no_invalid_operator() {
let out = compile("y = +x", "uadd.py");
assert!(!out.contains("= + x"), "generated: {}", out);
assert!(out.contains("y = (x)"), "generated: {}", out);
}
#[test]
fn conditions_apply_python_truthiness() {
let out = compile("def f(items):\n if items:\n work()\n", "truthy.py");
assert!(out.contains("if (items) . is_truthy ()"), "generated: {}", out);
let out = compile("def f(n):\n while n:\n work()\n", "truthy_while.py");
assert!(out.contains("while (n) . is_truthy ()"), "generated: {}", out);
let out = compile("def f(n: int):\n if n < 0:\n work()\n", "truthy_cmp.py");
assert!(!out.contains("is_truthy"), "generated: {}", out);
let out = compile("def f(a, b):\n if a and not b:\n work()\n", "truthy_bool.py");
assert!(
out.contains("((a) . is_truthy ()) && (! ((b) . is_truthy ()))"),
"generated: {}",
out
);
}
#[test]
fn is_none_lowers_to_py_is_none() {
let out = compile("def f(x):\n if x is None:\n work()\n", "isnone.py");
assert!(out.contains("(x) . py_is_none ()"), "generated: {}", out);
let out = compile("def f(x):\n if x is not None:\n work()\n", "isnotnone.py");
assert!(out.contains("! (x) . py_is_none ()"), "generated: {}", out);
let out = compile("found = a is b", "isplain.py");
assert!(out.contains("& a == & b"), "generated: {}", out);
}
#[test]
fn python_list_methods_map_to_correct_rust() {
let src = concat!(
"def f() -> int:\n",
" items = [1, 2, 3]\n",
" items.append(4)\n",
" items.remove(2)\n",
" items.insert(0, 9)\n",
" last = items.pop()\n",
" return last + items.count(9)\n",
);
let out = compile(src, "listops.py");
assert!(out.contains("(items) . push (4)"), "generated: {}", out);
assert!(out.contains("position"), "generated: {}", out);
assert!(out.contains("\"ValueError\""), "generated: {}", out);
assert!(out.contains("py_insert (0 , 9)"), "generated: {}", out);
assert!(out.contains("\"IndexError\""), "generated: {}", out);
assert!(out.contains("pop () . ok_or_else"), "generated: {}", out);
assert!(out.contains("count (& (9))"), "generated: {}", out);
}
#[test]
fn python_str_methods_map_through_pystrops() {
let src = concat!(
"def f(s: str) -> str:\n",
" parts = s.split()\n",
" head = s.split(\",\")\n",
" n = s.find(\"x\")\n",
" return \"-\".join(parts)\n",
);
let out = compile(src, "strops.py");
assert!(out.contains("py_split_whitespace ()"), "generated: {}", out);
assert!(out.contains("py_split (& (\",\")) ?"), "generated: {}", out);
assert!(out.contains("py_find (& (\"x\"))"), "generated: {}", out);
assert!(out.contains(". join (parts)"), "generated: {}", out);
}
#[test]
fn str_parameters_accept_borrowed_and_owned_strings() {
let out = compile("def shout(name: str) -> str:\n return name.upper()\n", "strparam.py");
assert!(
out.contains("name : impl Into < String >"),
"generated: {}",
out
);
assert!(
out.contains("let name : String = name . into ()"),
"generated: {}",
out
);
}
#[test]
fn subscripts_lower_through_py_index() {
let out = compile("def f(items: list[int], i: int) -> int:\n return items[i]\n", "sub.py");
assert!(out.contains("(items) . py_index (i) ?"), "generated: {}", out);
let out = compile(
"def f(items: list[int]):\n items[0] = 5\n",
"substore.py",
);
assert!(
out.contains("(items) . py_set_index (0 , 5) ?"),
"generated: {}",
out
);
assert!(!out.contains("py_index (0) ? ="), "generated: {}", out);
let out = compile("def f():\n d = {\"a\": 1}\n d[\"b\"] = 2\n return d[\"a\"]\n", "dictsub.py");
assert!(out.contains("py_set_index (\"b\" , 2) ?"), "generated: {}", out);
assert!(out.contains("py_index (\"a\") ?"), "generated: {}", out);
}
#[test]
fn slices_lower_through_py_slice() {
let out = compile("def f(items: list[int]):\n return items[1:3]\n", "slice1.py");
assert!(
out.contains("py_slice (Some (1) , Some (3) , None)"),
"generated: {}",
out
);
let out = compile("def f(s: str) -> str:\n return s[::-1]\n", "slice2.py");
assert!(
out.contains("py_slice (None , None , Some (- 1))"),
"generated: {}",
out
);
}
#[test]
fn container_annotations_map_to_rust_types() {
let out = compile("def f(a: list[int], b: dict[str, int], c: set[int]):\n pass\n", "generics.py");
assert!(out.contains("a : Vec < i64 >"), "generated: {}", out);
assert!(
out.contains("b : PyDict < String , i64 >"),
"generated: {}",
out
);
assert!(
out.contains("c : std :: collections :: HashSet < i64 >"),
"generated: {}",
out
);
}
#[test]
fn augmented_assignment_to_subscript_reads_and_stores() {
let out = compile(
"def f():\n counts = {\"a\": 1}\n counts[\"a\"] += 5\n",
"augsub.py",
);
assert!(
out.contains("py_index (__rython_idx . clone ()) ?"),
"generated: {}",
out
);
assert!(
out.contains("py_set_index (__rython_idx , (__rython_elem) . py_add (& (5))) ?"),
"generated: {}",
out
);
let out = compile(
"def f():\n nums = [1, 2]\n nums[-1] *= 2\n",
"augsub2.py",
);
assert!(
out.contains("py_set_index (__rython_idx , __rython_elem * 2) ?"),
"generated: {}",
out
);
}
#[test]
fn bare_numeric_literals_are_anchored_in_addition() {
let out = compile("y = 1 + 2", "anchor.py");
assert!(
out.contains("((1) as i64) . py_add (& ((2) as i64))"),
"generated: {}",
out
);
let out = compile("y = 1.5 + 2.5", "anchor2.py");
assert!(
out.contains("((1.5) as f64) . py_add"),
"generated: {}",
out
);
}
#[test]
fn addition_lowers_through_py_add() {
let out = compile("def f(a: str, b: str) -> str:\n return a + b\n", "addstr.py");
assert!(out.contains("(a) . py_add (& (b))"), "generated: {}", out);
let out = compile("def f(n: int) -> int:\n n += 1\n return n\n", "addaug.py");
assert!(out.contains("n = (n) . py_add (& (1))"), "generated: {}", out);
}
#[test]
fn dict_literals_and_methods_lower_through_pydict() {
let out = compile("d = {\"a\": 1}", "dictlit.py");
assert!(out.contains("PyDict :: from"), "generated: {}", out);
assert!(!out.contains("HashMap :: from"), "generated: {}", out);
let src = concat!(
"def f() -> int:\n",
" d = {\"a\": 1}\n",
" x = d.get(\"a\", 0)\n",
" y = d.pop(\"a\")\n",
" z = d.pop(\"gone\", 9)\n",
" d.setdefault(\"b\", 2)\n",
" ks = d.keys()\n",
" vs = d.values()\n",
" it = d.items()\n",
" return x + y + z\n",
);
let out = compile(src, "dictops.py");
assert!(out.contains("py_get_default (& (\"a\") , 0)"), "generated: {}", out);
assert!(out.contains("py_pop (\"a\") ?"), "generated: {}", out);
assert!(out.contains("py_pop_default (\"gone\" , 9)"), "generated: {}", out);
assert!(out.contains("py_setdefault (\"b\" , 2)"), "generated: {}", out);
assert!(out.contains("py_keys ()"), "generated: {}", out);
assert!(out.contains("py_values ()"), "generated: {}", out);
assert!(out.contains("py_items ()"), "generated: {}", out);
let out = compile("def g(d: dict[str, int]):\n v = d.get(\"k\")\n", "dictget.py");
assert!(out.contains("py_get (& (\"k\"))"), "generated: {}", out);
}
#[test]
fn keyword_arguments_map_to_parameter_positions() {
let src = concat!(
"def volume(w: int, h: int, d: int) -> int:\n",
" return w * h * d\n",
"\n",
"def f() -> int:\n",
" return volume(d=2, w=3, h=4)\n",
);
let out = compile(src, "kw.py");
assert!(out.contains("volume (3 , 4 , 2) ?"), "generated: {}", out);
}
#[test]
fn omitted_defaults_fill_at_the_call_site() {
let src = concat!(
"def greet(name: str = \"world\", excited: bool = False) -> str:\n",
" return name\n",
"\n",
"def f() -> str:\n",
" return greet()\n",
"\n",
"def g() -> str:\n",
" return greet(excited=True)\n",
);
let out = compile(src, "kwdef.py");
assert!(
out.contains("greet (\"world\" , false) ?"),
"generated: {}",
out
);
assert!(
out.contains("greet (\"world\" , true) ?"),
"keyword for the second param leaves the first defaulted: {}",
out
);
}
#[test]
fn keywords_on_unknown_callees_error_loudly() {
let module = parse("unknown_func(a=1)\n", "kwunknown.py").unwrap();
let symbols = module.clone().find_symbols(SymbolTableScopes::new());
let err = module
.to_rust(
CodeGenContext::Module("kwunknown".into()),
PythonOptions::default(),
symbols,
)
.expect_err("keywords on unknown callee must not convert");
assert!(
format!("{}", err).contains("signature"),
"error: {}",
err
);
}
#[test]
fn dict_comprehensions_build_ordered_pydicts() {
let out = compile(
"def f(items: list[int]):\n return {x: x * 2 for x in items}\n",
"dictcomp.py",
);
assert!(out.contains("PyDict :: new ()"), "generated: {}", out);
assert!(!out.contains("HashMap :: new ()"), "generated: {}", out);
}
#[test]
fn none_lowers_to_option() {
let src = concat!(
"def f(items: list[int]) -> int:\n",
" found = None\n",
" for x in items:\n",
" found = x\n",
" if found is None:\n",
" return -1\n",
" return 0\n",
);
let out = compile(src, "opt.py");
assert!(out.contains("found = None"), "generated: {}", out);
assert!(out.contains("found = Some (x)"), "generated: {}", out);
assert!(out.contains("(found) . py_is_none ()"), "generated: {}", out);
}
#[test]
fn optional_annotations_map_to_option() {
let out = compile(
"def f(tag: Optional[int], n: int | None) -> int:\n return 0\n",
"optann.py",
);
assert!(out.contains("tag : Option < i64 >"), "generated: {}", out);
assert!(out.contains("n : Option < i64 >"), "generated: {}", out);
}
#[test]
fn optional_parameters_wrap_arguments_at_call_sites() {
let src = concat!(
"def label(tag: Optional[int]) -> int:\n",
" return 0\n",
"\n",
"def f() -> int:\n",
" a = label(7)\n",
" b = label(None)\n",
" return a + b\n",
);
let out = compile(src, "optcall.py");
assert!(out.contains("label (Some (7)) ?"), "generated: {}", out);
assert!(out.contains("label (None) ?"), "generated: {}", out);
}
#[test]
fn optional_stores_from_option_values_do_not_double_wrap() {
let src = concat!(
"def probe(d: dict[str, int], keys: list[str]) -> int:\n",
" result = None\n",
" for k in keys:\n",
" result = d.get(k)\n",
" alias = None\n",
" alias = result\n",
" if alias is None:\n",
" return -1\n",
" return 0\n",
);
let out = compile(src, "optget.py");
assert!(
out.contains("result = (d) . py_get"),
"generated: {}",
out
);
assert!(
!out.contains("Some ((d) . py_get"),
"double-wrapped dict.get store, generated: {}",
out
);
assert!(out.contains("alias = result"), "generated: {}", out);
assert!(
!out.contains("Some (result)"),
"double-wrapped optional-name store, generated: {}",
out
);
}
#[test]
fn conditional_stores_into_optional_names_wrap_per_arm() {
let src = concat!(
"def f(n: int) -> int:\n",
" tag = None\n",
" tag = n if n > 0 else None\n",
" if tag is None:\n",
" return 0\n",
" return 1\n",
);
let out = compile(src, "optifexp.py");
assert!(
out.contains("tag = if") && out.contains("Some (n)"),
"generated: {}",
out
);
assert!(
!out.contains("Some (if"),
"wrapped the whole conditional, generated: {}",
out
);
}
#[test]
fn conditional_with_option_arms_stores_without_rewrap() {
let src = concat!(
"def f(d: dict[int, int], n: int) -> int:\n",
" choice = None\n",
" choice = d.get(n) if n > 0 else None\n",
" if choice is None:\n",
" return -1\n",
" return 0\n",
);
let out = compile(src, "optifexp2.py");
assert!(
out.contains("choice = if"),
"generated: {}",
out
);
assert!(
!out.contains("Some (if") && !out.contains("Some ((d) . py_get"),
"double-wrapped a conditional Option, generated: {}",
out
);
}
#[test]
fn conditional_arguments_to_optional_parameters_wrap_per_arm() {
let src = concat!(
"def label(tag: Optional[int]) -> int:\n",
" return 0\n",
"\n",
"def f(n: int) -> int:\n",
" return label(n if n > 0 else None)\n",
);
let out = compile(src, "optifexp3.py");
assert!(
out.contains("label (if") && out.contains("Some (n)"),
"generated: {}",
out
);
assert!(
!out.contains("Some (if"),
"wrapped the whole conditional argument, generated: {}",
out
);
}
#[test]
fn optional_returning_calls_store_and_pass_without_rewrap() {
let src = concat!(
"def find(d: dict[str, int], k: str) -> Optional[int]:\n",
" return d.get(k)\n",
"\n",
"def label(tag: Optional[int]) -> int:\n",
" return 0\n",
"\n",
"def f(d: dict[str, int]) -> int:\n",
" hit = None\n",
" hit = find(d, \"a\")\n",
" return label(find(d, \"b\"))\n",
);
let out = compile(src, "optret.py");
assert!(out.contains("hit = find"), "generated: {}", out);
assert!(
!out.contains("hit = Some (find"),
"double-wrapped Optional-returning call store, generated: {}",
out
);
assert!(
!out.contains("label (Some (find"),
"double-wrapped Optional-returning call argument, generated: {}",
out
);
}
#[test]
fn typing_imports_lower_to_nothing() {
let out = compile("from typing import Optional\nx = 1\n", "typing.py");
assert!(!out.contains("typing"), "generated: {}", out);
}
#[test]
fn membership_uses_py_contains() {
let out = compile("found = x in items", "in.py");
assert!(out.contains("py_contains"), "generated: {}", out);
let out = compile("missing = x not in items", "notin.py");
assert!(out.contains("! (items) . py_contains"), "generated: {}", out);
}
#[test]
fn multiple_lossy_conversions_fold_into_one_attribute() {
let out = compile(
"def f(c, x: int = 3) -> int:\n if c:\n return x\n",
"lossy_both.py",
);
assert_eq!(
out.matches("deprecated").count(),
1,
"exactly one #[deprecated] attribute: {}",
out
);
assert!(out.contains("were dropped"), "generated: {}", out);
assert!(out.contains("return annotation"), "generated: {}", out);
}
#[test]
fn lossy_warnings_can_be_suppressed_by_options() {
let src = "def f(x: int = 3) -> int:\n if x:\n return x\n";
let module = parse(src, "suppress.py").unwrap();
let symbols = module.clone().find_symbols(SymbolTableScopes::new());
let options = PythonOptions {
lossy_warnings: false,
..Default::default()
};
let out = module
.to_rust(CodeGenContext::Module("suppress".into()), options, symbols)
.unwrap()
.to_string();
assert!(!out.contains("deprecated"), "generated: {}", out);
}
#[test]
fn dropped_defaults_emit_call_site_warning() {
let out = compile("def f(x: int = 3) -> int:\n return x\n", "warn_def.py");
assert!(out.contains("deprecated"), "generated: {}", out);
assert!(out.contains("were dropped"), "generated: {}", out);
let out = compile("def g(x: int) -> int:\n return x\n", "no_warn.py");
assert!(!out.contains("deprecated"), "generated: {}", out);
}
fn compile_err(src: &str, name: &str) -> String {
let module = parse(src, name).unwrap_or_else(|e| panic!("parse failed: {}", e));
let symbols = module.clone().find_symbols(SymbolTableScopes::new());
let err = module
.to_rust(
CodeGenContext::Module(name.replace(".py", "")),
PythonOptions::default(),
symbols,
)
.expect_err("conversion must fail loudly");
format!("{}", err)
}
const COUNTER: &str = concat!(
"class Counter:\n",
" def __init__(self, label: str, start: int = 0):\n",
" self.label = label\n",
" self.count = start\n",
"\n",
" def bump(self, amount: int) -> int:\n",
" self.count += amount\n",
" return self.count\n",
"\n",
" def double_bump(self, amount: int) -> int:\n",
" self.bump(amount)\n",
" self.bump(amount)\n",
" return self.count\n",
"\n",
" def peek(self) -> int:\n",
" return self.count\n",
);
#[test]
fn classes_lower_to_structs_with_inferred_fields() {
let out = compile(COUNTER, "counter.py");
assert!(out.contains("pub struct Counter"), "generated: {}", out);
assert!(out.contains("pub label : String"), "generated: {}", out);
assert!(out.contains("pub count : i64"), "generated: {}", out);
assert!(
out.contains("pub fn new (label : impl Into < String > , start : i64) -> Result < Self , PyException >"),
"generated: {}",
out
);
assert!(
out.contains("__rython_self . __init__ (label , start) ?"),
"generated: {}",
out
);
}
#[test]
fn method_receivers_follow_mutation_including_transitive_calls() {
let out = compile(COUNTER, "receivers.py");
assert!(out.contains("fn __init__ (& mut self ,"), "generated: {}", out);
assert!(out.contains("fn bump (& mut self ,"), "generated: {}", out);
assert!(
out.contains("fn double_bump (& mut self ,"),
"transitive self-call must select &mut self: {}",
out
);
assert!(out.contains("fn peek (& self ,"), "generated: {}", out);
}
#[test]
fn construction_and_method_calls_propagate_exceptions() {
let src = format!(
"{}\n\ndef run() -> int:\n c = Counter(\"hits\")\n c.bump(amount=2)\n return c.peek()\n",
COUNTER
);
let out = compile(&src, "classcalls.py");
assert!(
out.contains("Counter :: new (\"hits\" , 0) ?"),
"generated: {}",
out
);
assert!(out.contains("(c) . bump (2) ?"), "generated: {}", out);
assert!(out.contains("(c) . peek () ?"), "generated: {}", out);
assert!(out.contains("let mut c ;"), "generated: {}", out);
}
#[test]
fn user_methods_shadow_builtin_method_rewrites() {
let src = concat!(
"class Box:\n",
" def __init__(self, v: int):\n",
" self.v = v\n",
"\n",
" def get(self, bonus: int) -> int:\n",
" return self.v + bonus\n",
"\n",
"def run() -> int:\n",
" b = Box(3)\n",
" return b.get(1)\n",
);
let out = compile(src, "shadow.py");
assert!(out.contains("(b) . get (1) ?"), "generated: {}", out);
assert!(!out.contains("py_get"), "generated: {}", out);
}
#[test]
fn composed_fields_type_and_resolve_through_chains() {
let src = concat!(
"class Point:\n",
" def __init__(self, x: int):\n",
" self.x = x\n",
"\n",
" def shift(self, dx: int):\n",
" self.x += dx\n",
"\n",
"class Holder:\n",
" def __init__(self, p: Point):\n",
" self.p = p\n",
"\n",
" def nudge(self):\n",
" self.p.shift(1)\n",
);
let out = compile(src, "compose.py");
assert!(out.contains("pub p : Point"), "generated: {}", out);
assert!(out.contains("fn nudge (& mut self ,"), "generated: {}", out);
assert!(
out.contains(". shift (1) ?"),
"field-chain method calls propagate exceptions: {}",
out
);
}
#[test]
fn unsupported_class_constructs_error_loudly() {
let err = compile_err(
"class Base:\n pass\n\nclass Child(Base):\n pass\n",
"inherit.py",
);
assert!(err.contains("inheritance"), "error: {}", err);
let err = compile_err("class C:\n VERSION = 3\n", "classattr.py");
assert!(err.contains("class attribute"), "error: {}", err);
let err = compile_err(
"class C:\n def __init__(self):\n self.x = None\n",
"noneattr.py",
);
assert!(err.contains("cannot infer a type"), "error: {}", err);
}
#[test]
fn str_getters_clone_the_field_out_of_the_shared_receiver() {
let src = concat!(
"class Tag:\n",
" def __init__(self, name: str):\n",
" self.name = name\n",
"\n",
" def get_name(self) -> str:\n",
" return self.name\n",
);
let out = compile(src, "getter.py");
assert!(
out.contains("Ok ((self . name) . clone ())"),
"generated: {}",
out
);
}
#[test]
fn class_method_named_new_errors_loudly() {
let err = compile_err(
"class C:\n def new(self) -> int:\n return 1\n",
"newclash.py",
);
assert!(err.contains("`new`"), "error: {}", err);
assert!(err.contains("constructor"), "error: {}", err);
}
#[test]
fn read_only_methods_with_mutator_names_do_not_force_mut() {
let src = concat!(
"class Box:\n",
" def __init__(self, v: int):\n",
" self.v = v\n",
"\n",
" def pop(self) -> int:\n",
" return self.v\n",
"\n",
"def run() -> int:\n",
" b = Box(3)\n",
" return b.pop()\n",
);
let out = compile(src, "romut.py");
assert!(out.contains("fn pop (& self ,"), "generated: {}", out);
assert!(
out.contains("let b ;") && !out.contains("let mut b ;"),
"read-only pop must not force `mut`: {}",
out
);
}
#[test]
fn mutations_inside_keyword_arguments_are_detected() {
let src = concat!(
"class Counter:\n",
" def __init__(self, start: int):\n",
" self.count = start\n",
"\n",
" def bump(self, amount: int) -> int:\n",
" self.count += amount\n",
" return self.count\n",
"\n",
"def use_it(n: int) -> int:\n",
" return n\n",
"\n",
"def run() -> int:\n",
" c = Counter(1)\n",
" return use_it(n=c.bump(2))\n",
);
let out = compile(src, "kwmut.py");
assert!(
out.contains("let mut c ;"),
"keyword-nested mutation must mark `c` mutable: {}",
out
);
}
#[test]
fn split_keyword_arguments_map_or_error_loudly() {
let out = compile(
"def f(s: str):\n return s.split(\",\", maxsplit=1)\n",
"kwsplit.py",
);
assert!(
out.contains("py_split_maxsplit (& (\",\") , 1) ?"),
"generated: {}",
out
);
let out = compile(
"def f(s: str):\n return s.rsplit(maxsplit=2)\n",
"kwrsplit.py",
);
assert!(
out.contains("py_rsplit_whitespace_maxsplit (2)"),
"generated: {}",
out
);
let err = compile_err(
"def f(s: str):\n return s.split(\",\", bogus=1)\n",
"kwbad.py",
);
assert!(err.contains("unexpected keyword"), "error: {}", err);
let err = compile_err(
"def f(s: str):\n return s.ljust(5, fillchar=\".\")\n",
"kwljust.py",
);
assert!(err.contains("signature"), "error: {}", err);
}
#[test]
fn str_format_lowers_to_format_macro() {
let out = compile(
"def f(a: int, b: str) -> str:\n return \"{} and {}\".format(a, b)\n",
"fmt1.py",
);
assert!(out.contains("format !"), "generated: {}", out);
assert!(out.contains("__rython_fmt0"), "generated: {}", out);
let out = compile(
"def f(x: float) -> str:\n return \"{0} {0} {v:.2f}\".format(x, v=x)\n",
"fmt2.py",
);
assert!(out.contains("__rython_fmt_v"), "generated: {}", out);
}
#[test]
fn str_format_errors_are_loud() {
let err = compile_err(
"def f(a: int, b: int) -> str:\n return \"{} {1}\".format(a, b)\n",
"fmtmix.py",
);
assert!(err.contains("automatic field numbering"), "error: {}", err);
let err = compile_err(
"def f() -> str:\n return \"{missing}\".format(present=1)\n",
"fmtname.py",
);
assert!(err.contains("missing"), "error: {}", err);
let err = compile_err(
"def f(x: int) -> str:\n return \"{:,}\".format(x)\n",
"fmtgroup.py",
);
assert!(err.contains("thousands separator"), "error: {}", err);
let err = compile_err(
"def f(t: str, x: int) -> str:\n return t.format(x)\n",
"fmtdyn.py",
);
assert!(err.contains("non-literal template"), "error: {}", err);
}
#[test]
fn fstring_specs_translate_or_error_loudly() {
let out = compile(
"def f(n: int) -> str:\n return f\"{n:05d}|{n:>4}\"\n",
"fspec.py",
);
assert!(out.contains("{:05}"), "generated: {}", out);
assert!(out.contains("{:>4}"), "generated: {}", out);
let err = compile_err(
"def f(x: float) -> str:\n return f\"{x:e}\"\n",
"fspecbad.py",
);
assert!(err.contains("presentation type"), "error: {}", err);
}
#[test]
fn repr_conversion_keeps_its_format_spec() {
let out = compile(
"def f(n: int) -> str:\n return \"{0!r:>10}\".format(n)\n",
"reprspec.py",
);
assert!(out.contains(":>10}"), "generated: {}", out);
assert!(out.contains("repr ("), "generated: {}", out);
let out = compile(
"def f(n: int) -> str:\n return f\"{n!r:>10}\"\n",
"freprspec.py",
);
assert!(out.contains(":>10}"), "generated: {}", out);
assert!(out.contains("repr ("), "generated: {}", out);
let err = compile_err(
"def f(n: int) -> str:\n return \"{0!r:.2f}\".format(n)\n",
"reprbad.py",
);
assert!(err.contains("cannot combine"), "error: {}", err);
}
#[test]
fn bare_precision_without_type_errors_loudly() {
let err = compile_err(
"def f(x: float) -> str:\n return \"{:.3}\".format(x)\n",
"barep.py",
);
assert!(err.contains("presentation type is ambiguous"), "error: {}", err);
let err = compile_err(
"def f(x: float) -> str:\n return f\"{x:.3}\"\n",
"barepf.py",
);
assert!(err.contains("presentation type is ambiguous"), "error: {}", err);
}
#[test]
fn module_constants_lower_to_statics() {
let out = compile(
concat!(
"PI = 3.14159\n",
"GREETING = \"hello\"\n",
"DEBUG = True\n",
"OFFSET = -3\n",
"\n",
"def area(r: float) -> float:\n",
" return PI * r * r\n",
),
"consts.py",
);
assert!(out.contains("pub static PI : f64 = 3.14159"), "generated: {}", out);
assert!(
out.contains("pub static GREETING : & 'static str = \"hello\""),
"generated: {}",
out
);
assert!(out.contains("pub static DEBUG : bool = true"), "generated: {}", out);
assert!(out.contains("pub static OFFSET : i64 = - 3"), "generated: {}", out);
let out = compile("X = 1\nX = 2\n", "reassigned.py");
assert!(!out.contains("pub static X"), "generated: {}", out);
}
#[test]
fn value_returning_main_gets_a_wrapper_entry_point() {
let out = compile(
concat!(
"def main() -> int:\n",
" return 0\n",
"\n",
"if __name__ == \"__main__\":\n",
" main()\n",
),
"intmain.py",
);
assert!(out.contains("fn python_main ()"), "generated: {}", out);
assert!(
out.contains("fn main () {"),
"wrapper entry point expected: {}",
out
);
}
#[test]
fn integral_float_literals_keep_their_float_type() {
let out = compile("def f() -> float:\n y = 2.0\n return y\n", "flit.py");
assert!(out.contains("y = 2.0"), "generated: {}", out);
assert!(!out.contains("y = 2 ;"), "generated: {}", out);
}
#[test]
fn conditionally_reassigned_module_names_are_not_constants() {
let out = compile(
"DEBUG = False\nif 1 > 0:\n DEBUG = True\n",
"condglobal.py",
);
assert!(!out.contains("pub static DEBUG"), "generated: {}", out);
let out = compile("I = 0\nfor I in [1, 2]:\n pass\n", "forglobal.py");
assert!(!out.contains("pub static I"), "generated: {}", out);
let out = compile(
"MODE = \"a\"\ntry:\n MODE = \"b\"\nexcept ValueError:\n pass\n",
"tryglobal.py",
);
assert!(!out.contains("pub static MODE"), "generated: {}", out);
}
fn compile_nostd(src: &str, name: &str) -> Result<String, String> {
let module = parse(src, name).unwrap_or_else(|e| panic!("parse failed: {}", e));
let symbols = module.clone().find_symbols(SymbolTableScopes::new());
let options = PythonOptions {
no_std: true,
..Default::default()
};
module
.to_rust(CodeGenContext::Module(name.replace(".py", "")), options, symbols)
.map(|tokens| tokens.to_string())
.map_err(|e| python_ast::format_error_chain(e.as_ref()))
}
#[test]
fn nostd_modules_carry_an_alloc_prelude() {
let out = compile_nostd("def f(n: int) -> str:\n return f\"n={n}\"\n", "np.py")
.expect("OS-free module must convert");
assert!(out.contains("extern crate alloc"), "generated: {}", out);
assert!(out.contains("use alloc ::"), "generated: {}", out);
let std_out = compile("def f(n: int) -> str:\n return f\"n={n}\"\n", "sp.py");
assert!(!std_out.contains("extern crate alloc"), "generated: {}", std_out);
}
#[test]
fn nostd_io_builtins_error_loudly() {
for src in ["print(\"hi\")\n", "x = input()\n", "f = open(\"a.txt\")\n"] {
let err = compile_nostd(src, "io.py").expect_err("I/O builtin must fail");
assert!(err.contains("no_std profile"), "{:?}: {}", src, err);
}
let out = compile_nostd(
"def print(s: str) -> str:\n return s\n\ndef f() -> str:\n return print(\"x\")\n",
"shadow.py",
)
.expect("shadowed print is the user's own function");
assert!(out.contains("fn print"), "generated: {}", out);
}
#[test]
fn nostd_std_tier_imports_error_loudly() {
for src in [
"import os\n",
"import sys\n",
"from datetime import datetime\n",
"import math\n",
"from os.path import join\n",
] {
let err = compile_nostd(src, "imp.py").expect_err("std-tier import must fail");
assert!(err.contains("std tier"), "{:?}: {}", src, err);
}
for src in ["import json\n", "import collections\n", "import itertools\n"] {
compile_nostd(src, "ok.py").unwrap_or_else(|e| {
panic!("alloc-tier import must convert: {:?}: {}", src, e)
});
}
}
#[test]
fn nostd_main_blocks_error_loudly() {
let err = compile_nostd(
"def main() -> int:\n return 0\n\nif __name__ == \"__main__\":\n main()\n",
"entry.py",
)
.expect_err("__main__ needs a process entry point");
assert!(err.contains("no_std profile"), "error: {}", err);
}
#[test]
fn min_max_lower_to_variant_functions_with_exception_propagation() {
let out = compile("def f(xs: list[int]) -> int:\n return min(xs)\n", "m1.py");
assert!(out.contains("min (& (xs)) ?"), "generated: {}", out);
let out = compile("def f(a: int, b: int) -> int:\n return max(a, b)\n", "m2.py");
assert!(out.contains("max2 (a , b)"), "generated: {}", out);
let out = compile(
"def f(a: int, b: int, c: int) -> int:\n return min(a, b, c)\n",
"m3.py",
);
assert!(out.contains("min2 (min2 (a , b) , c)"), "generated: {}", out);
let out = compile(
"def f(xs: list[int]) -> int:\n return min(xs, default=7)\n",
"m4.py",
);
assert!(out.contains("min_default (& (xs) , 7)"), "generated: {}", out);
let out = compile(
"def f(xs: list[int]) -> int:\n return max(xs, key=lambda x: -x)\n",
"m5.py",
);
assert!(out.contains("max_key (& (xs) ,"), "generated: {}", out);
assert!(out.contains(") ?"), "generated: {}", out);
let err = compile_err("x = min([1], foo=2)\n", "m6.py");
assert!(err.contains("unexpected"), "error: {}", err);
}
#[test]
fn sorted_lowers_by_keyword_combination() {
let out = compile("def f(xs: list[int]) -> list[int]:\n return sorted(xs)\n", "s1.py");
assert!(out.contains("sorted (& (xs))"), "generated: {}", out);
let out = compile(
"def f(xs: list[int]) -> list[int]:\n return sorted(xs, reverse=True)\n",
"s2.py",
);
assert!(out.contains("sorted_reverse (& (xs) , true)"), "generated: {}", out);
let out = compile(
"def f(xs: list[int]) -> list[int]:\n return sorted(xs, key=lambda x: -x)\n",
"s3.py",
);
assert!(out.contains("sorted_key (& (xs) ,"), "generated: {}", out);
let out = compile(
"def f(xs: list[int]) -> list[int]:\n return sorted(xs, key=lambda x: -x, reverse=True)\n",
"s4.py",
);
assert!(out.contains("sorted_key_reverse (& (xs) ,"), "generated: {}", out);
}
#[test]
fn enumerate_start_and_pow_arities_lower_to_their_variants() {
let out = compile(
"for i, x in enumerate([10, 20], start=5):\n pass\n",
"e1.py",
);
assert!(out.contains("enumerate_start ("), "generated: {}", out);
let out = compile("for i, x in enumerate([10]):\n pass\n", "e2.py");
assert!(out.contains("enumerate ("), "generated: {}", out);
assert!(!out.contains("enumerate_start"), "generated: {}", out);
let out = compile("y = pow(2, 5)\n", "p1.py");
assert!(out.contains("pow (2 , 5)"), "generated: {}", out);
let out = compile("y = pow(2, 5, 7)\n", "p2.py");
assert!(out.contains("pow_mod (2 , 5 , 7) ?"), "generated: {}", out);
}
#[test]
fn by_reference_builtins_borrow_their_argument() {
let out = compile("def f(xs: list[int]) -> int:\n return len(xs)\n", "b1.py");
assert!(out.contains("len (& (xs))"), "generated: {}", out);
let out = compile("def f(xs: list[int]) -> str:\n return repr(xs)\n", "b2.py");
assert!(out.contains("repr (& (xs))"), "generated: {}", out);
let out = compile(
"def f(xs: list[int]) -> list[int]:\n return reversed(xs)\n",
"b3.py",
);
assert!(out.contains("reversed (& (xs))"), "generated: {}", out);
let out = compile(
"def len(x: int) -> int:\n return x\n\ndef g(v: int) -> int:\n return len(v)\n",
"b4.py",
);
assert!(out.contains("len (v)"), "generated: {}", out);
}
#[test]
fn datetime_constructors_map_keywords_onto_new() {
let out = compile(
"from datetime import timedelta\ntd = timedelta(days=1, hours=2)\n",
"td.py",
);
assert!(
out.contains("timedelta :: new (Some (1) , None , None , None , None , Some (2) , None)"),
"generated: {}",
out
);
let out = compile(
"from datetime import date\nd = date(2024, 3, 1)\n",
"d.py",
);
assert!(out.contains("date :: new (2024 , 3 , 1) ?"), "generated: {}", out);
let out = compile(
"from datetime import datetime\ndt = datetime(2024, 3, 1, hour=10)\n",
"dt.py",
);
assert!(
out.contains("datetime :: new (2024 , 3 , 1 , Some (10) , None , None , None) ?"),
"generated: {}",
out
);
let err = compile_err(
"from datetime import timedelta\ntd = timedelta(fortnights=1)\n",
"tde.py",
);
assert!(err.contains("unexpected keyword"), "error: {}", err);
let err = compile_err("from datetime import date\nd = date(2024)\n", "de.py");
assert!(err.contains("missing required argument"), "error: {}", err);
}
#[test]
fn strptime_and_module_attribute_calls_lower_to_paths() {
let out = compile(
"from datetime import datetime\ndt = datetime.strptime(\"x\", \"%Y\")\n",
"sp.py",
);
assert!(
out.contains("datetime :: strptime (\"x\" , \"%Y\") ?"),
"generated: {}",
out
);
let out = compile("import time\nt = time.monotonic()\n", "tm.py");
assert!(out.contains("time :: monotonic ()"), "generated: {}", out);
}
#[test]
fn runtime_module_imports_lower_to_nothing_and_aliases_stay_loud() {
let out = compile("import math\nimport random\n", "imp.py");
assert!(!out.contains("use math"), "generated: {}", out);
assert!(!out.contains("use random"), "generated: {}", out);
let err = compile_err("import time as t\n", "alias.py");
assert!(err.contains("aliasing"), "error: {}", err);
}
#[test]
fn itertools_keyword_spellings_lower_to_variants() {
let base = "from itertools import accumulate, product, zip_longest, groupby\n";
let out = compile(&format!("{}a = accumulate([1, 2])\n", base), "i1.py");
assert!(out.contains("accumulate_sum (& (vec ! [1 , 2]))"), "generated: {}", out);
let out = compile(
&format!("{}a = accumulate([1, 2], initial=10)\n", base),
"i2.py",
);
assert!(out.contains("accumulate_sum_initial ("), "generated: {}", out);
let out = compile(
&format!("{}a = accumulate([1, 2], lambda x, y: x * y)\n", base),
"i3.py",
);
assert!(out.contains("accumulate_func ("), "generated: {}", out);
let out = compile(&format!("{}p = product([1], [2])\n", base), "i4.py");
assert!(out.contains("product2 ("), "generated: {}", out);
let out = compile(&format!("{}p = product([1], repeat=2)\n", base), "i5.py");
assert!(out.contains("product_repeat2 ("), "generated: {}", out);
let err = compile_err(&format!("{}p = product([1], repeat=5)\n", base), "i6.py");
assert!(err.contains("literal 2 or 3"), "error: {}", err);
let out = compile(
&format!("{}z = zip_longest([1], [2], fillvalue=0)\n", base),
"i7.py",
);
assert!(out.contains("zip_longest_fill ("), "generated: {}", out);
let out = compile(
&format!("{}g = groupby([1], key=lambda x: x)\n", base),
"i8.py",
);
assert!(out.contains("groupby_key ("), "generated: {}", out);
let err = compile_err(&format!("{}g = groupby([1], foo=1)\n", base), "i9.py");
assert!(err.contains("unexpected"), "error: {}", err);
}
#[test]
fn pure_module_calls_lower_with_borrows_and_arity_variants() {
let out = compile(
"from functools import reduce\nr = reduce(lambda a, b: a + b, [1, 2])\n",
"f1.py",
);
assert!(out.contains("reduce ("), "generated: {}", out);
assert!(out.contains(") ?"), "generated: {}", out);
let out = compile(
"from functools import reduce\nr = reduce(lambda a, b: a + b, [1, 2], 10)\n",
"f2.py",
);
assert!(out.contains("reduce_initial ("), "generated: {}", out);
let out = compile(
"from heapq import heappush, heappop\nh = [3, 1]\nheappush(h, 2)\nx = heappop(h)\n",
"h1.py",
);
assert!(out.contains("heappush (& mut (h) , 2)"), "generated: {}", out);
assert!(out.contains("heappop (& mut (h)) ?"), "generated: {}", out);
assert!(out.contains("let mut h"), "heap binding must be mut: {}", out);
let out = compile("import heapq\nh = [2, 1]\nheapq.heapify(h)\n", "h2.py");
assert!(out.contains("heapq :: heapify (& mut (h))"), "generated: {}", out);
assert!(out.contains("let mut h"), "heap binding must be mut: {}", out);
let out = compile("from copy import deepcopy\nc = deepcopy([1])\n", "c1.py");
assert!(out.contains("deepcopy (& ("), "generated: {}", out);
let out = compile(
"from textwrap import indent\ns = indent(\"a\", \"> \")\n",
"t1.py",
);
assert!(out.contains("indent (& (\"a\") , & (\"> \"))"), "generated: {}", out);
}
#[test]
fn mutating_methods_on_subscripted_receivers_use_the_place_lowering() {
let out = compile("xs = [[1], [2]]\nxs[0].append(9)\n", "sub1.py");
assert!(
out.contains("py_index_mut (0) ?) . push (9)"),
"generated: {}",
out
);
let out = compile("xs = [[1]]\nn = xs[0].count(1)\n", "sub2.py");
assert!(!out.contains("py_index_mut"), "generated: {}", out);
let out = compile(
"from heapq import heappush\nrows = [[1], [2]]\nheappush(rows[0], 5)\n",
"sub3.py",
);
assert!(
out.contains("heappush ((rows) . py_index_mut (0) ? , 5)"),
"generated: {}",
out
);
}
#[test]
fn re_calls_lower_to_borrowing_fallible_paths() {
let out = compile("import re\nm = re.search(r\"\\d\", \"a1\")\n", "r1.py");
assert!(
out.contains("re :: search (& (\"\\\\d\") , & (\"a1\") , \"\") ?"),
"generated: {}",
out
);
let out = compile("import re\nm = re.match(r\"\\d\", \"1\")\n", "r2.py");
assert!(out.contains("re :: r#match ("), "generated: {}", out);
let out = compile(
"import re\ns = re.sub(r\"a\", \"b\", \"aa\")\n",
"r3.py",
);
assert!(out.contains("re :: sub ("), "generated: {}", out);
assert!(out.contains(") ?"), "generated: {}", out);
let out = compile(
"import re\nm = re.search(r\"a\", \"a\")\ng = m.group()\n",
"r4.py",
);
assert!(out.contains(". group (0)"), "generated: {}", out);
let out = compile(
"from re import findall, match\nxs = findall(r\"a\", \"aa\")\nm = match(r\"a\", \"ab\")\n",
"r5.py",
);
assert!(out.contains("findall (& ("), "generated: {}", out);
assert!(out.contains("r#match (& ("), "generated: {}", out);
let out = compile(
"import re\nxs = re.findall(r\"a\", \"A\", re.IGNORECASE)\n",
"r6.py",
);
assert!(out.contains("\"i\") ?"), "generated: {}", out);
let out = compile(
"import re\nxs = re.findall(r\"a\", \"A\", flags=re.IGNORECASE | re.MULTILINE)\n",
"r7.py",
);
assert!(out.contains("\"im\") ?"), "generated: {}", out);
let out = compile(
"import re\ns = re.sub(r\"a\", \"b\", \"aa\", count=1)\n",
"r8.py",
);
assert!(out.contains(", 1 , \"\") ?"), "generated: {}", out);
let err = compile_err(
"import re\nxs = re.findall(r\"a\", \"A\", re.VERBOSE)\n",
"r9.py",
);
assert!(err.contains("unsupported re flag"), "error: {}", err);
let out = compile(
"import re\nxs = re.split(r\"a\", \"b\", 1)\n",
"r10.py",
);
assert!(out.contains("re :: split (& (\"a\") , & (\"b\") , 1 , \"\") ?"), "generated: {}", out);
let out = compile(
"import re\nxs = re.split(r\"a\", \"b\", maxsplit=2, flags=re.IGNORECASE)\n",
"r11.py",
);
assert!(out.contains(", 2 , \"i\") ?"), "generated: {}", out);
let err = compile_err(
"import re\nm = re.search(r\"a\", \"b\", re.IGNORECASE, 5)\n",
"r12.py",
);
assert!(err.contains("at most 3"), "error: {}", err);
}
#[test]
fn map_filter_dispatch_on_the_function_arguments_shape() {
let out = compile("ys = list(map(lambda x: x * 2, [1, 2]))\n", "mf1.py");
assert!(out.contains("list (map (| x |"), "generated: {}", out);
assert!(!out.contains("map_fallible"), "generated: {}", out);
let out = compile(
"def double(n: int) -> int:\n return n * 2\n\nys = list(map(double, [1, 2]))\n",
"mf2.py",
);
assert!(out.contains("map_fallible (double ,"), "generated: {}", out);
assert!(out.contains(") ?"), "generated: {}", out);
let out = compile("ys = filter(lambda x: x > 1, [1, 2, 3])\n", "mf3.py");
assert!(out.contains("filter (| x |"), "generated: {}", out);
let out = compile("ys = filter(None, [0, 1, 2])\n", "mf4.py");
assert!(out.contains("filter_truthy ("), "generated: {}", out);
let err = compile_err("ys = list()\n", "mf5.py");
assert!(err.contains("iterable argument"), "error: {}", err);
}
#[test]
fn hashlib_and_encode_lower_correctly() {
let out = compile(
"import hashlib\nh = hashlib.sha256(\"x\".encode())\n",
"hl1.py",
);
assert!(
out.contains("hashlib :: sha256 (& ((\"x\") . as_bytes () . to_vec ()))"),
"generated: {}",
out
);
let out = compile("from hashlib import sha256\nh = sha256()\n", "hl2.py");
assert!(out.contains("sha256_new ()"), "generated: {}", out);
let err = compile_err("s = \"x\".encode(\"latin-1\")\n", "hl3.py");
assert!(err.contains("utf-8"), "error: {}", err);
}
#[test]
fn wrap_and_fill_lower_with_width_defaults() {
let out = compile("from textwrap import wrap\nxs = wrap(\"a b\")\n", "w1.py");
assert!(out.contains("wrap (& (\"a b\") , 70) ?"), "generated: {}", out);
let out = compile(
"from textwrap import fill\ns = fill(\"a b\", width=9)\n",
"w2.py",
);
assert!(out.contains("fill (& (\"a b\") , 9) ?"), "generated: {}", out);
let out = compile(
"import textwrap\nxs = textwrap.wrap(\"a b\", 12)\n",
"w3.py",
);
assert!(out.contains("textwrap :: wrap (& (\"a b\") , 12) ?"), "generated: {}", out);
let err = compile_err(
"from textwrap import wrap\nxs = wrap(\"a\", initial_indent=\"> \")\n",
"w4.py",
);
assert!(err.contains("unexpected keyword"), "error: {}", err);
}
#[test]
fn isinstance_lowers_to_a_static_constant_or_a_loud_error() {
let out = compile(
"def f(n: int) -> bool:\n return isinstance(n, int)\n",
"is1.py",
);
assert!(out.contains("return Ok (true)") || out.contains("true"), "generated: {}", out);
let out = compile(
"def f(n: int) -> bool:\n return isinstance(n, str)\n",
"is2.py",
);
assert!(out.contains("false"), "generated: {}", out);
let out = compile(
"def f() -> bool:\n x = 1.5\n return isinstance(x, float)\n",
"is3.py",
);
assert!(out.contains("true"), "generated: {}", out);
let out = compile(
"def f(b: bool) -> bool:\n return isinstance(b, int)\n",
"is4.py",
);
assert!(out.contains("true"), "generated: {}", out);
let out = compile(
"def f(n: int) -> bool:\n return isinstance(n, bool)\n",
"is5.py",
);
assert!(out.contains("false"), "generated: {}", out);
let err = compile_err(
"def f(v):\n return isinstance(v, int)\n",
"is6.py",
);
assert!(err.contains("statically"), "error: {}", err);
}
#[test]
fn hash_lowers_by_reference() {
let out = compile("h = hash(\"a\")\n", "hs1.py");
assert!(out.contains("hash (& (\"a\"))"), "generated: {}", out);
}
#[test]
fn csv_reader_lowers_by_reference() {
let out = compile(
"import csv\nrows = csv.reader([\"a,b\"])\n",
"cv1.py",
);
assert!(out.contains("csv :: reader (& ("), "generated: {}", out);
let out = compile(
"from csv import reader\nrows = reader([\"a,b\"])\n",
"cv2.py",
);
assert!(out.contains("reader (& ("), "generated: {}", out);
}
#[test]
fn print_multi_arg_renders_through_py_display() {
let out = compile("def f(x: int, s: str):\n print(x, s)\n", "pr1.py");
assert!(
out.contains("print_parts (& [py_display (& (x)) , py_display (& (s))] , \" \" , \"\\n\")"),
"generated: {}",
out
);
}
#[test]
fn print_sep_end_flush_keywords_map() {
let out = compile(
"def f(a: int, b: int):\n print(a, b, sep='-', end='!')\n",
"pr2.py",
);
assert!(
out.contains("print_parts (& [py_display (& (a)) , py_display (& (b))] , \"-\" , \"!\")"),
"generated: {}",
out
);
let out = compile(
"def f(a: int):\n print(a, sep=None, flush=True)\n",
"pr3.py",
);
assert!(
out.contains("print_parts_flush (& [py_display (& (a))] , \" \" , \"\\n\" , true)"),
"generated: {}",
out
);
}
#[test]
fn print_zero_and_single_arg_shapes() {
let out = compile("def f():\n print()\n", "pr4.py");
assert!(out.contains("println ! ()"), "generated: {}", out);
let out = compile("def f():\n print(end='')\n", "pr5.py");
assert!(
out.contains("print_parts (& [] as & [& str] , \" \" , \"\")"),
"generated: {}",
out
);
let out = compile("def f(x: int):\n print(x)\n", "pr6.py");
assert!(out.contains("print (& (x))"), "generated: {}", out);
}
#[test]
fn print_file_keyword_is_a_loud_error() {
let err = compile_err(
"import sys\n\ndef f():\n print('x', file=sys.stderr)\n",
"pr7.py",
);
assert!(err.contains("file"), "error: {}", err);
}
#[test]
fn list_sort_maps_keyword_shapes_in_place() {
let out = compile("def f(xs: list[int]):\n xs.sort()\n", "srt1.py");
assert!(out.contains("(xs) . py_sort ()"), "generated: {}", out);
let out = compile(
"def f(xs: list[int]):\n xs.sort(reverse=True)\n",
"srt2.py",
);
assert!(
out.contains("(xs) . py_sort_reverse (true)"),
"generated: {}",
out
);
let out = compile(
"def f(xs: list[str]):\n xs.sort(key=lambda w: len(w))\n",
"srt3.py",
);
assert!(out.contains("py_sort_key"), "generated: {}", out);
let out = compile(
"def f(xs: list[str]):\n xs.sort(key=lambda w: len(w), reverse=True)\n",
"srt4.py",
);
assert!(out.contains("py_sort_key_reverse"), "generated: {}", out);
}
#[test]
fn list_sort_on_subscript_uses_place_lowering() {
let out = compile(
"def f(grid: list[list[int]]):\n grid[0].sort()\n",
"srt5.py",
);
assert!(out.contains("py_index_mut"), "generated: {}", out);
assert!(out.contains("py_sort"), "generated: {}", out);
}
#[test]
fn list_sort_positional_arg_is_a_loud_error() {
let err = compile_err(
"def f(xs: list[int]):\n xs.sort(True)\n",
"srt6.py",
);
assert!(
err.contains("no positional arguments"),
"error: {}",
err
);
}
#[test]
fn findall_picks_variant_from_literal_group_count() {
let src = "import re\n\ndef f(s: str):\n return re.findall(r\"(\\w+)=(\\d+)\", s)\n";
let out = compile(src, "fa2.py");
assert!(out.contains("findall2"), "generated: {}", out);
let src = "import re\n\ndef f(s: str):\n return re.findall(r\"(\\d+)-(\\d+)-(\\d+)\", s)\n";
let out = compile(src, "fa3.py");
assert!(out.contains("findall3"), "generated: {}", out);
let src = "import re\n\ndef f(s: str):\n return re.findall(r\"\\d+\", s)\n";
let out = compile(src, "fa1.py");
assert!(out.contains("findall ("), "generated: {}", out);
assert!(!out.contains("findall2"), "generated: {}", out);
let src = "import re\n\ndef f(p: str, s: str):\n return re.findall(p, s)\n";
let out = compile(src, "fa_dyn.py");
assert!(out.contains("findall ("), "generated: {}", out);
}
#[test]
fn findall_bad_or_wide_literal_patterns_error_at_conversion() {
let err = compile_err(
"import re\n\ndef f(s: str):\n return re.findall(r\"(a)(b)(c)(d)\", s)\n",
"fa4.py",
);
assert!(err.contains("4 capture groups"), "error: {}", err);
let err = compile_err(
"import re\n\ndef f(s: str):\n return re.findall(r\"(unclosed\", s)\n",
"fa_bad.py",
);
assert!(err.contains("cannot compile pattern"), "error: {}", err);
}
#[test]
fn match_group_string_routes_to_group_name() {
let src = concat!(
"import re\n",
"\n",
"def f(s: str):\n",
" m = re.search(r\"(?P<word>\\w+)\", s)\n",
" return m.group(\"word\")\n",
);
let out = compile(src, "gn1.py");
assert!(
out.contains("group_name (\"word\")"),
"generated: {}",
out
);
let src = concat!(
"import re\n",
"\n",
"def f(s: str):\n",
" m = re.search(r\"(\\w+)\", s)\n",
" return m.group(1)\n",
);
let out = compile(src, "gn2.py");
assert!(out.contains("group (1)"), "generated: {}", out);
assert!(!out.contains("group_name"), "generated: {}", out);
}
#[test]
fn replace_keywords_lower_through_py_replace() {
let src = concat!(
"from datetime import datetime\n",
"\n",
"def f(d: datetime):\n",
" return d.replace(hour=14)\n",
);
let out = compile(src, "rep1.py");
assert!(out.contains("py_replace"), "generated: {}", out);
assert!(out.contains("hour : Some (14)"), "generated: {}", out);
assert!(
out.contains(".. ReplaceArgs :: default ()"),
"generated: {}",
out
);
let src = concat!(
"from datetime import datetime\n",
"\n",
"def f(d: datetime):\n",
" return d.replace(2023, day=28)\n",
);
let out = compile(src, "rep2.py");
assert!(out.contains("year : Some (2023)"), "generated: {}", out);
assert!(out.contains("day : Some (28)"), "generated: {}", out);
}
#[test]
fn replace_bad_keywords_are_loud_with_pythons_message() {
let err = compile_err(
"from datetime import datetime\n\ndef f(d: datetime):\n return d.replace(bogus=1)\n",
"rep3.py",
);
assert!(
err.contains("'bogus' is an invalid keyword argument for replace()"),
"error: {}",
err
);
let err = compile_err(
"from datetime import datetime\n\ndef f(d: datetime):\n return d.replace(2023, year=1)\n",
"rep4.py",
);
assert!(
err.contains("multiple values for argument 'year'"),
"error: {}",
err
);
}
#[test]
fn str_replace_positional_stays_a_plain_method_call() {
let out = compile(
"def f(s: str):\n return s.replace(\"a\", \"o\")\n",
"rep5.py",
);
assert!(out.contains("replace (\"a\" , \"o\")"), "generated: {}", out);
assert!(!out.contains("py_replace"), "generated: {}", out);
}
#[test]
fn partial_lowers_to_a_move_closure_with_remaining_params() {
let src = concat!(
"from functools import partial\n",
"\n",
"def add(a: int, b: int) -> int:\n",
" return a + b\n",
"\n",
"def f() -> int:\n",
" add5 = partial(add, 5)\n",
" return add5(3)\n",
);
let out = compile(src, "part1.py");
assert!(out.contains("move | b | add (5 , b)"), "generated: {}", out);
assert!(out.contains("add5 (3) ?"), "generated: {}", out);
assert!(!out.contains("use stdpython :: functools :: partial"), "generated: {}", out);
let src = concat!(
"from functools import partial\n",
"\n",
"def add(a: int, b: int) -> int:\n",
" return a + b\n",
"\n",
"def f() -> int:\n",
" g = partial(add, 2, 3)\n",
" return g()\n",
);
let out = compile(src, "part2.py");
assert!(out.contains("move | | add (2 , 3 ,)"), "generated: {}", out);
let src = concat!(
"import functools\n",
"\n",
"def add(a: int, b: int) -> int:\n",
" return a + b\n",
"\n",
"def f() -> int:\n",
" add5 = functools.partial(add, 5)\n",
" return add5(1)\n",
);
let out = compile(src, "part3.py");
assert!(out.contains("move | b | add (5 , b)"), "generated: {}", out);
}
#[test]
fn partial_rejects_unknown_functions_keywords_and_overbinding() {
let err = compile_err(
"from functools import partial\n\ndef f():\n g = partial(unknown_fn, 1)\n",
"part4.py",
);
assert!(
err.contains("not a function defined in this module"),
"error: {}",
err
);
let err = compile_err(
concat!(
"from functools import partial\n",
"\n",
"def add(a: int, b: int) -> int:\n",
" return a + b\n",
"\n",
"def f():\n",
" g = partial(add, b=1)\n",
),
"part5.py",
);
assert!(err.contains("keyword arguments"), "error: {}", err);
let err = compile_err(
concat!(
"from functools import partial\n",
"\n",
"def add(a: int, b: int) -> int:\n",
" return a + b\n",
"\n",
"def f():\n",
" g = partial(add, 1, 2, 3)\n",
),
"part6.py",
);
assert!(err.contains("takes 2 argument(s), but 3 were bound"), "error: {}", err);
}
#[test]
fn open_arity_splits_onto_the_option_mode() {
let out = compile("def f():\n g = open(\"x.txt\")\n return g.read()\n", "op1.py");
assert!(out.contains("open (& (\"x.txt\") , None :: < & str >) ?"), "generated: {}", out);
assert!(out.contains(". read () ?"), "generated: {}", out);
let out = compile("def f():\n g = open(\"x.txt\", \"w\")\n g.write(\"hi\")\n", "op2.py");
assert!(
out.contains("open (& (\"x.txt\") , Some (\"w\")) ?"),
"generated: {}",
out
);
assert!(out.contains(". write (& (\"hi\")) ?"), "generated: {}", out);
assert!(out.contains("let mut g"), "generated: {}", out);
}
#[test]
fn stringio_and_csv_writer_lower_with_mut_borrows() {
let src = concat!(
"import csv\n",
"import io\n",
"\n",
"def f() -> str:\n",
" buf = io.StringIO()\n",
" w = csv.writer(buf)\n",
" w.writerow([\"a\", \"b\"])\n",
" w.writerow([])\n",
" return buf.getvalue()\n",
);
let out = compile(src, "csw1.py");
assert!(out.contains("io :: StringIO ()"), "generated: {}", out);
assert!(out.contains("csv :: writer (& mut (buf))"), "generated: {}", out);
assert!(out.contains("let mut buf"), "generated: {}", out);
assert!(out.contains("let mut w"), "generated: {}", out);
assert!(out.contains(". writerow (& (vec ! [\"a\" . to_string () , \"b\" . to_string ()])) ?")
|| out.contains(". writerow ("), "generated: {}", out);
assert!(out.contains("writerow (& [] as & [& str]) ?"), "generated: {}", out);
assert!(out.contains(". getvalue () ?"), "generated: {}", out);
let out = compile(
"import io\n\ndef f() -> str:\n b = io.StringIO(\"seed\")\n return b.read()\n",
"csw2.py",
);
assert!(
out.contains("io :: StringIO_seeded (& (\"seed\"))"),
"generated: {}",
out
);
}
#[test]
fn lru_cache_wraps_the_body_with_a_static_cache() {
let src = concat!(
"from functools import lru_cache\n",
"\n",
"@lru_cache\n",
"def fib(n: int) -> int:\n",
" if n < 2:\n",
" return n\n",
" return fib(n - 1) + fib(n - 2)\n",
);
let out = compile(src, "lru1.py");
assert!(out.contains("PyLruCache :: new (Some (128"), "generated: {}", out);
assert!(out.contains("__lru_uncached"), "generated: {}", out);
assert!(out.contains("static __LRU_CACHE"), "generated: {}", out);
let src = concat!(
"from functools import lru_cache\n",
"\n",
"@lru_cache(maxsize=None)\n",
"def f(n: int) -> int:\n",
" return n\n",
);
let out = compile(src, "lru2.py");
assert!(out.contains("PyLruCache :: new (None)"), "generated: {}", out);
let src = concat!(
"import functools\n",
"\n",
"@functools.cache\n",
"def f(s: str) -> str:\n",
" return s\n",
);
let out = compile(src, "lru3.py");
assert!(out.contains("PyLruCache :: new (None)"), "generated: {}", out);
assert!(out.contains("(String ,)"), "generated: {}", out);
}
#[test]
fn unknown_decorators_and_unhashable_keys_are_loud() {
let err = compile_err(
"@mystery\ndef f(n: int) -> int:\n return n\n",
"lru4.py",
);
assert!(err.contains("not supported yet"), "error: {}", err);
assert!(err.contains("refuses to silently ignore"), "error: {}", err);
let err = compile_err(
concat!(
"from functools import lru_cache\n",
"\n",
"@lru_cache\n",
"def f(x: float) -> float:\n",
" return x\n",
),
"lru5.py",
);
assert!(err.contains("must be annotated int, bool, or str"), "error: {}", err);
}
#[test]
fn argparse_parser_statements_become_a_typed_struct() {
let src = concat!(
"import argparse\n",
"\n",
"def main() -> None:\n",
" p = argparse.ArgumentParser(prog=\"tool\", description=\"Demo\")\n",
" p.add_argument(\"name\")\n",
" p.add_argument(\"count\", type=int)\n",
" p.add_argument(\"--verbose\", action=\"store_true\")\n",
" p.add_argument(\"--scale\", type=float, default=1.0)\n",
" args = p.parse_args()\n",
" print(args.name, args.count, args.scale)\n",
);
let out = compile(src, "ap1.py");
assert!(out.contains("struct __ArgparseArgs"), "generated: {}", out);
assert!(out.contains("argparse :: run_parser"), "generated: {}", out);
assert!(out.contains("name : String"), "generated: {}", out);
assert!(out.contains("count : i64"), "generated: {}", out);
assert!(out.contains("verbose : bool"), "generated: {}", out);
assert!(out.contains("scale : f64"), "generated: {}", out);
assert!(!out.contains("ArgumentParser"), "generated: {}", out);
assert!(!out.contains("add_argument"), "generated: {}", out);
assert!(!out.contains("let p"), "generated: {}", out);
}
#[test]
fn argparse_dynamic_or_unsupported_specs_are_loud() {
let err = compile_err(
concat!(
"import argparse\n",
"\n",
"def main() -> None:\n",
" p = argparse.ArgumentParser()\n",
" p.add_argument(\"--scale\", type=float)\n",
" args = p.parse_args()\n",
),
"ap2.py",
);
assert!(err.contains("needs default="), "error: {}", err);
let err = compile_err(
concat!(
"import argparse\n",
"\n",
"def main(n: str) -> None:\n",
" p = argparse.ArgumentParser()\n",
" p.add_argument(n)\n",
" args = p.parse_args()\n",
),
"ap3.py",
);
assert!(err.contains("string literal"), "error: {}", err);
let err = compile_err(
concat!(
"import argparse\n",
"\n",
"def main() -> None:\n",
" p = argparse.ArgumentParser()\n",
" p.add_argument(\"xs\", nargs=\"+\")\n",
" args = p.parse_args()\n",
),
"ap4.py",
);
assert!(err.contains("'nargs' is not supported yet"), "error: {}", err);
}
#[test]
fn chained_comparison_evaluates_each_operand_once() {
let out = compile(
"def f(n: int) -> int:\n return n\n\ndef g() -> bool:\n return 1 < f(5) < 10\n",
"chain1.py",
);
assert_eq!(
out.matches("f (5)").count(),
1,
"middle operand must be evaluated once: {}",
out
);
assert!(out.contains("__rython_cmp"), "generated: {}", out);
let out = compile(
"def f(n: int) -> int:\n return n\n\ndef g() -> bool:\n return 1 < f(2) < f(3)\n",
"chain2.py",
);
assert!(out.contains("&& {"), "later operand must stay guarded: {}", out);
let out = compile("def g(a: int, b: int) -> bool:\n return a < b\n", "chain3.py");
assert!(!out.contains("__rython_cmp"), "generated: {}", out);
assert!(out.contains("(a) < (b)"), "generated: {}", out);
}
#[test]
fn break_and_continue_thread_out_of_a_try_body() {
let src = concat!(
"def f() -> None:\n",
" for i in range(3):\n",
" try:\n",
" if i == 1:\n",
" break\n",
" finally:\n",
" cleanup()\n",
);
let out = compile(src, "tryflow1.py");
assert!(out.contains("return Ok (PyFlow :: Break)"), "generated: {}", out);
assert!(
out.contains("Ok (PyFlow :: Break) => { cleanup () ; break ; }"),
"the finally must run before the break resumes: {}",
out
);
let src = concat!(
"def f() -> None:\n",
" try:\n",
" for i in range(3):\n",
" break\n",
" finally:\n",
" cleanup()\n",
);
let out = compile(src, "tryflow2.py");
assert!(!out.contains("PyFlow :: Break"), "generated: {}", out);
}
#[test]
fn loop_control_in_a_finally_guarded_handler_is_loud() {
let src = concat!(
"def f() -> None:\n",
" for i in range(3):\n",
" try:\n",
" risky()\n",
" except ValueError:\n",
" break\n",
" finally:\n",
" cleanup()\n",
);
let err = compile_err(src, "tryflow3.py");
assert!(err.contains("except handler"), "error: {}", err);
assert!(err.contains("finally"), "error: {}", err);
}
#[test]
fn f_strings_render_through_py_display_not_rust_display() {
let out = compile("def f(x: float):\n return f\"v={x}\"\n", "fs1.py");
assert!(out.contains("py_display (& (x))"), "generated: {}", out);
let out = compile("def f(x: float):\n return f\"v={x:.2f}\"\n", "fs2.py");
assert!(out.contains("{:.2}"), "generated: {}", out);
assert!(!out.contains("py_display"), "generated: {}", out);
let out = compile("def f(s: str):\n return f\"{s!r}\"\n", "fs3.py");
assert!(out.contains("repr (& (s))"), "generated: {}", out);
assert!(!out.contains("{:?}"), "generated: {}", out);
}
#[test]
fn augmented_division_is_true_division() {
let out = compile("def f(y: float):\n y /= 2\n return y\n", "td1.py");
assert!(out.contains("as f64 / (2) as f64"), "generated: {}", out);
assert!(!out.contains("y /= 2"), "generated: {}", out);
}
#[test]
fn not_is_a_truthiness_test_not_bitwise_complement() {
let out = compile("def f(n: int):\n return not n\n", "not1.py");
assert!(out.contains("! (n) . is_truthy ()"), "generated: {}", out);
let out = compile("def f(n: int):\n return ~n\n", "not2.py");
assert!(out.contains("! n"), "generated: {}", out);
assert!(!out.contains("is_truthy"), "generated: {}", out);
}
#[test]
fn or_none_yields_none_instead_of_dropping_it() {
let out = compile("def f(count: int):\n return count or None\n", "orn.py");
assert!(out.contains("is_truthy ()"), "generated: {}", out);
assert!(out.contains("Some (__rython_or)"), "generated: {}", out);
assert!(out.contains("None"), "generated: {}", out);
}