From 592f0b82c60c3843826cfcdf3445cb289ee8016f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:16:52 +0000 Subject: [PATCH 1/2] fix: implement any() and all() via V helper functions Replaced the incorrect transpilation of Python's `any()` and `all()` to non-existent native V methods `.any(it)` and `.all(it)`. Changes: - Implemented `py_any[T](a []T) bool`, `py_all[T](a []T) bool`, and `py_bool(val Any) bool` generic V helper functions. - Updated `calls_generators.py` to use these helpers. - Handled generator expressions by transpiling them to boolean arrays via `.map()`. - Updated unit tests. Co-authored-by: yaskhan <3676373+yaskhan@users.noreply.github.com> --- .../expressions_split/calls_generators.py | 14 +- py2v_transpiler/core/translator/module.py | 58 ++++++ .../input/transpile/test_builtin_modules.v | 112 +++++++++++ .../transpile/test_builtin_modules_helpers.v | 189 ++++++++++++++++++ .../tests/translator/test_any_all.py | 12 +- 5 files changed, 372 insertions(+), 13 deletions(-) create mode 100644 py2v_transpiler/tests/input/transpile/test_builtin_modules.v create mode 100644 py2v_transpiler/tests/input/transpile/test_builtin_modules_helpers.v diff --git a/py2v_transpiler/core/translator/expressions_split/calls_generators.py b/py2v_transpiler/core/translator/expressions_split/calls_generators.py index 501b53e6..e3c98e49 100644 --- a/py2v_transpiler/core/translator/expressions_split/calls_generators.py +++ b/py2v_transpiler/core/translator/expressions_split/calls_generators.py @@ -84,9 +84,10 @@ def _handle_iterator_functions(self, node: ast.Call, func_name_str: str, args: l def _handle_any_all(self, node: ast.Call, func_name_str: str, args: list) -> str: """Handle any() and all().""" arg = node.args[0] + self.used_builtins.add(f"py_{func_name_str}") if isinstance(arg, ast.GeneratorExp): - # any(expr for target in iter) -> iter.any(expr_with_it) + # any(expr for target in iter) -> py_any(iter.map(expr)) comp_gen = arg.generators[0] target = comp_gen.target iter_expr = self.visit(comp_gen.iter) @@ -96,15 +97,12 @@ def _handle_any_all(self, node: ast.Call, func_name_str: str, args: list) -> str self.name_remap[target.id] = "it" elt = self.visit(arg.elt) del self.name_remap[target.id] - return f"{iter_expr}.{func_name_str}({elt})" - else: - # any(iterable) -> iterable.any(it) - val = self.visit(arg) - return f"{val}.{func_name_str}(it)" + return f"py_{func_name_str}({iter_expr}.map({elt}))" - # Fallback + # any(iterable) -> py_any(iterable) val = self.visit(arg) - return f"{val}.{func_name_str}(it)" + return f"py_{func_name_str}({val})" + def _handle_len_function(self, node: ast.Call, func_name_str: str, args: list) -> str | None: """Handle len() -> obj.len.""" diff --git a/py2v_transpiler/core/translator/module.py b/py2v_transpiler/core/translator/module.py index 824167aa..541672ff 100644 --- a/py2v_transpiler/core/translator/module.py +++ b/py2v_transpiler/core/translator/module.py @@ -1420,5 +1420,63 @@ def visit_Module(self, node: ast.Module) -> str: } } return res +}""") + if "py_any" in self.used_builtins: + self.used_builtins.add("py_bool") + self.emitter.add_helper_function("""fn py_any[T](a []T) bool { + for it in a { + $if T is bool { + if it { return true } + } $else $if T is int { + if it != 0 { return true } + } $else $if T is i64 { + if it != 0 { return true } + } $else $if T is f64 { + if it != 0.0 { return true } + } $else $if T is string { + if it.len > 0 { return true } + } $else $if T is Any { + if py_bool(it) { return true } + } $else { + if it != none { return true } + } + } + return false +}""") + + if "py_all" in self.used_builtins: + self.used_builtins.add("py_bool") + self.emitter.add_helper_function("""fn py_all[T](a []T) bool { + for it in a { + $if T is bool { + if !it { return false } + } $else $if T is int { + if it == 0 { return false } + } $else $if T is i64 { + if it == 0 { return false } + } $else $if T is f64 { + if it == 0.0 { return false } + } $else $if T is string { + if it.len == 0 { return false } + } $else $if T is Any { + if !py_bool(it) { return false } + } $else { + if it == none { return false } + } + } + return true +}""") + + if "py_bool" in self.used_builtins: + self.emitter.add_helper_function("""fn py_bool(val Any) bool { + if val is bool { return val } + if val is int { return val != 0 } + if val is i64 { return val != 0 } + if val is f64 { return val != 0.0 } + if val is string { return val.len > 0 } + if val is []Any { return val.len > 0 } + if val is map[string]Any { return val.len > 0 } + if val is NoneType { return false } + return true }""") return self.emitter.emit() diff --git a/py2v_transpiler/tests/input/transpile/test_builtin_modules.v b/py2v_transpiler/tests/input/transpile/test_builtin_modules.v new file mode 100644 index 00000000..7d63e1a5 --- /dev/null +++ b/py2v_transpiler/tests/input/transpile/test_builtin_modules.v @@ -0,0 +1,112 @@ +module main + +import math +import rand +import os + +// @line: test_builtin_modules.py:5:0 +pub fn test_math_functions() { + println('Pi: ${math.pi}') + println('E: ${math.e}') + println('Sqrt(16): ${math.sqrt(f64(16))}') + println('Pow(2, 3): ${math.pow(f64(2), f64(3))}') + println('Ceil(3.2): ${math.ceil(f64(3.2))}') + println('Floor(3.8): ${math.floor(f64(3.8))}') + println('Abs(-5): ${abs(-5)}') + println('Round(3.5): ${math.round(3.5)}') + println('Round(3.14159, 2): ${py_round(f64(3.14159), 2)}') + println('Sin(0): ${math.sin(f64(0))}') + println('Cos(0): ${math.cos(f64(0))}') + println('Tan(0): ${math.tan(f64(0))}') + println('Log(e): ${math.log(f64(math.e))}') + println('Log10(100): ${math.log10(f64(100))}') + println('Exp(1): ${math.exp(f64(1))}') +} +// @line: test_builtin_modules.py:27:0 +pub fn test_random_functions() { + rand.seed(42) + println('Random: ${rand.f64()}') + println('Random int 1-10: ${rand.intn(10 - 1 + 1) + 1}') + println('Random int 1-10: ${rand.intn(10 - 1 + 1) + 1}') + choices := ['apple', 'banana', 'cherry'] + println('Choice: ${choices[rand.intn(choices.len)]}') + println('Sample: ${random.sample(choices, 2)}') + random.shuffle(choices) + println('Shuffled: ${choices}') +} +// @line: test_builtin_modules.py:42:0 +pub fn test_os_functions() { + cwd := os.getwd() + println('CWD: ${cwd}') + path := os.join_path('folder', 'subfolder', 'file.txt') + println('Joined path: ${path}') + py_destruct_0 := os.path.split(path) + dirname := py_destruct_0[0] + basename := py_destruct_0[1] + println('Dir: ${dirname}, Base: ${basename}') + py_destruct_1 := os.path.splitext('file.txt') + root := py_destruct_1[0] + ext := py_destruct_1[1] + println('Root: ${root}, Ext: ${ext}') + println('Exists: ${os.exists(cwd)}') + println('Is dir: ${os.is_dir(cwd)}') + println('Is file: ${os.is_file(cwd)}') +} +// @line: test_builtin_modules.py:64:0 +pub fn test_builtin_functions() { + println('Len: ${[1, 2, 3, 4, 5].len}') + println('Range: ${[]Any(range(5))}') + println('Range with start: ${[]Any(range(2, 7))}') + println('Range with step: ${[]Any(range(0, 10, 2))}') + nums := [5, 2, 8, 1, 9] + println('Sum: ${sum(nums)}') + println('Min: ${min(nums)}') + println('Max: ${max(nums)}') + println('Abs: ${abs(-10)}') + println('Pow: ${pow(2, 10)}') + py_destruct_2 := divmod(17, 5) + q := py_destruct_2[0] + r := py_destruct_2[1] + println('Divmod: quotient=${q}, remainder=${r}') + println('All True: ${py_all([true, true, true])}') + println('All with False: ${py_all([true, false, true])}') + println('Any True: ${py_any([false, false, true])}') + println('Any False: ${py_any([false, false, false])}') + println('Ord(\'A\'): ${ord("A")}') + println('Chr(65): ${chr(65)}') +} +// @line: test_builtin_modules.py:97:0 +pub fn test_string_builtin() { + s := 'Hello, World!' + println('Len: ${s.len}') + println('Upper: ${s.to_upper()}') + println('Lower: ${s.to_lower()}') + println('Replace: ${s.replace("World", "Universe")}') + println('Split: ${s.split(", ")}') +} +// @line: test_builtin_modules.py:106:0 +pub fn test_list_builtin() { + lst := [3, 1, 4, 1, 5, 9, 2, 6] + println('Sorted: ${py_sorted(lst)}') + println('Sorted desc: ${py_sorted(lst)}') + println('Reversed: ${[]Any(py_reversed(lst))}') + names := ['Alice', 'Bob', 'Charlie'] + ages := [25, 30, 35] + println('Zipped: ${[]Any(zip(names, ages))}') + println('Enumerated: ${[]Any(enumerate(["a", "b", "c"]))}') +} +// @line: test_builtin_modules.py:121:0 +pub fn test() { + test_math_functions() + test_random_functions() + test_os_functions() + test_builtin_functions() + test_string_builtin() + test_list_builtin() +} + +fn main() { + // @line: test_builtin_modules.py:129:0 + // if __name__ == '__main__': + test() +} \ No newline at end of file diff --git a/py2v_transpiler/tests/input/transpile/test_builtin_modules_helpers.v b/py2v_transpiler/tests/input/transpile/test_builtin_modules_helpers.v new file mode 100644 index 00000000..97fd0538 --- /dev/null +++ b/py2v_transpiler/tests/input/transpile/test_builtin_modules_helpers.v @@ -0,0 +1,189 @@ +module main + +pub struct NoneType {} + +pub fn (n NoneType) str() string { + return 'None' +} + +pub struct Interpolation { +pub: + value Any + expression string + conversion string + format_spec string +} + +pub struct Template { +pub: + strings []string + interpolations []Interpolation +} + +pub fn (t Template) values() []Any { + mut res := []Any{cap: t.interpolations.len} + for i in t.interpolations { + res << i.value + } + return res +} + +pub fn (t1 Template) + (t2 Template) Template { + if t1.strings.len == 0 { return t2 } + if t2.strings.len == 0 { return t1 } + mut new_strings := t1.strings[..t1.strings.len - 1].clone() + new_strings << t1.strings.last() + t2.strings[0] + if t2.strings.len > 1 { + new_strings << t2.strings[1..] + } + mut new_interpolations := t1.interpolations.clone() + new_interpolations << t2.interpolations + return Template{ + strings: new_strings + interpolations: new_interpolations + } +} + +pub type Any = Interpolation | NoneType | Template | []Any | []u8 | bool | f64 | i64 | int | map[string]Any | string + +pub enum PyAnnotationFormat { value forwardref string } + +pub fn py_get_type_hints[T]() map[string]string { + mut hints := map[string]string{} + $for field in T.fields { + hints[field.name] = field.typ + } + return hints +} + +pub fn py_get_type_hints_generic(obj Any) map[string]string { + return map[string]string{} +} + +struct PyGeneratorInput { + val Any + is_exc bool + exc_msg string +} +struct PyGenerator[T] { +mut: + out chan T + in_ chan PyGeneratorInput + open bool = true +} + +fn py_sorted[T](a []T) []T { + mut b := a.clone() + b.sort() + return b +} +fn py_reversed[T](a []T) []T { + mut b := a.clone() + b.reverse() + return b +} +fn py_round(number f64, ndigits int) f64 { + p := math.pow(10, f64(ndigits)) + return math.round(number * p) / p +} +fn (mut g PyGenerator[T]) next() ?T { + if !g.open { return none } + g.in_ <- PyGeneratorInput{val: 0} // Send dummy value + res := <-g.out + if res == none { g.open = false } + return res +} +fn (mut g PyGenerator[T]) send(val Any) ?T { + if !g.open { panic('StopIteration') } + g.in_ <- PyGeneratorInput{val: val} + res := <-g.out + if res == none { g.open = false } + return res +} +fn (mut g PyGenerator[T]) throw(msg string) ?T { + if !g.open { panic('StopIteration') } + g.in_ <- PyGeneratorInput{is_exc: true, exc_msg: msg} + res := <-g.out + if res == none { g.open = false } + return res +} +fn (mut g PyGenerator[T]) close() { + g.open = false + g.in_.close() + // g.out will be closed by the generator function loop when it detects in_ closed or panic +} +fn py_yield[T](ch_out chan T, ch_in chan PyGeneratorInput, val T) Any { + ch_out <- val + inp := <-ch_in + if inp.is_exc { + panic(inp.exc_msg) + } + return inp.val +} +//##LLM@@ String formatting for bytes is stubbed and might be incorrect. Please implement proper bytes formatting or use V string interpolation. +fn py_bytes_format(fmt []u8, args Any) []u8 { + // Simplistic implementation for b'%s' % b'val' + // Converts bytes to string, formats, and converts back. + // This is not efficient or correct for non-ASCII bytes but works for simple cases. + fmt_str := fmt.bytestr() + // TODO: handle args properly. V's string interpolation/formatting expects distinct args. + // If args is []u8, treat as string. + arg_str := if args is []u8 { args.bytestr() } else { '${args}' } + + // Manual substitution of %s + // V does not have sprintf for runtime strings easily available in core without C interop. + // Simple replace for %s + res := fmt_str.replace('%s', arg_str) + return res.bytes() +} +fn py_any[T](a []T) bool { + for it in a { + $if T is bool { + if it { return true } + } $else $if T is int { + if it != 0 { return true } + } $else $if T is i64 { + if it != 0 { return true } + } $else $if T is f64 { + if it != 0.0 { return true } + } $else $if T is string { + if it.len > 0 { return true } + } $else $if T is Any { + if py_bool(it) { return true } + } $else { + if it != none { return true } + } + } + return false +} +fn py_all[T](a []T) bool { + for it in a { + $if T is bool { + if !it { return false } + } $else $if T is int { + if it == 0 { return false } + } $else $if T is i64 { + if it == 0 { return false } + } $else $if T is f64 { + if it == 0.0 { return false } + } $else $if T is string { + if it.len == 0 { return false } + } $else $if T is Any { + if !py_bool(it) { return false } + } $else { + if it == none { return false } + } + } + return true +} +fn py_bool(val Any) bool { + if val is bool { return val } + if val is int { return val != 0 } + if val is i64 { return val != 0 } + if val is f64 { return val != 0.0 } + if val is string { return val.len > 0 } + if val is []Any { return val.len > 0 } + if val is map[string]Any { return val.len > 0 } + if val is NoneType { return false } + return true +} diff --git a/py2v_transpiler/tests/translator/test_any_all.py b/py2v_transpiler/tests/translator/test_any_all.py index b1f1e6cc..bb19af2f 100644 --- a/py2v_transpiler/tests/translator/test_any_all.py +++ b/py2v_transpiler/tests/translator/test_any_all.py @@ -20,7 +20,8 @@ def test_any_basic(): b = any(a) """ v_code = translate(source) - assert "b := a.any(it)" in v_code + assert "b := py_any(a)" in v_code + assert "fn py_any[T](a []T) bool" in v_code def test_all_basic(): source = """ @@ -28,7 +29,8 @@ def test_all_basic(): b = all(a) """ v_code = translate(source) - assert "b := a.all(it)" in v_code + assert "b := py_all(a)" in v_code + assert "fn py_all[T](a []T) bool" in v_code def test_any_generator(): source = """ @@ -36,7 +38,7 @@ def test_any_generator(): b = any(x > 0 for x in nums) """ v_code = translate(source) - assert "b := nums.any(it > 0)" in v_code + assert "b := py_any(nums.map(it > 0))" in v_code def test_all_generator(): source = """ @@ -44,7 +46,7 @@ def test_all_generator(): b = all(y < 10 for y in nums) """ v_code = translate(source) - assert "b := nums.all(it < 10)" in v_code + assert "b := py_all(nums.map(it < 10))" in v_code def test_any_generator_renamed(): # Ensure nested renaming works or at least basic usage @@ -53,4 +55,4 @@ def test_any_generator_renamed(): b = any(val == 1 for val in nums) """ v_code = translate(source) - assert "b := nums.any(it == 1)" in v_code + assert "b := py_any(nums.map(it == 1))" in v_code From b4f6cd63dfa3663483ab32a7f9dce8a6f089f2d4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:00:51 +0000 Subject: [PATCH 2/2] Implement comprehensive Python set operations transpilation Co-authored-by: yaskhan <3676373+yaskhan@users.noreply.github.com> --- analyzer_patch.py | 58 ++++++++ analyzer_patch_visitor.py | 11 ++ calls_methods_fix.py | 51 +++++++ calls_methods_patch.py | 131 ++++++++++++++++++ module_patch.py | 121 ++++++++++++++++ operators_patch.py | 25 ++++ py2v_transpiler/core/analyzer_split/utils.py | 18 +++ .../core/analyzer_split/visitor.py | 2 +- .../expressions_split/calls_builtin.py | 8 +- .../expressions_split/calls_methods.py | 94 +++++++++++++ .../translator/expressions_split/operators.py | 16 +++ py2v_transpiler/core/translator/module.py | 88 ++++++++++++ py2v_transpiler/main.py | 2 +- .../tests/translator/test_set_full_ops.py | 93 +++++++++++++ 14 files changed, 712 insertions(+), 6 deletions(-) create mode 100644 analyzer_patch.py create mode 100644 analyzer_patch_visitor.py create mode 100644 calls_methods_fix.py create mode 100644 calls_methods_patch.py create mode 100644 module_patch.py create mode 100644 operators_patch.py create mode 100644 py2v_transpiler/tests/translator/test_set_full_ops.py diff --git a/analyzer_patch.py b/analyzer_patch.py new file mode 100644 index 00000000..c756dcc1 --- /dev/null +++ b/analyzer_patch.py @@ -0,0 +1,58 @@ +<<<<<<< SEARCH + elif isinstance(node, ast.Dict): + if not node.keys: + return "map[string]Any" + key_types = set() + val_types = set() + for k, v in zip(node.keys, node.values): + if k: + key_types.add(self._guess_node_type(k)) + if v: + val_types.add(self._guess_node_type(v)) + + k_type = "string" + if len(key_types) == 1: + k_type = list(key_types)[0] + elif len(key_types) > 1: + k_type = "Any" + + v_type = "Any" + if len(val_types) == 1: + v_type = list(val_types)[0] + + return f"map[{k_type}]{v_type}" + return "Any" +======= + elif isinstance(node, ast.Dict): + if not node.keys: + return "map[string]Any" + key_types = set() + val_types = set() + for k, v in zip(node.keys, node.values): + if k: + key_types.add(self._guess_node_type(k)) + if v: + val_types.add(self._guess_node_type(v)) + + k_type = "string" + if len(key_types) == 1: + k_type = list(key_types)[0] + elif len(key_types) > 1: + k_type = "Any" + + v_type = "Any" + if len(val_types) == 1: + v_type = list(val_types)[0] + + return f"map[{k_type}]{v_type}" + elif isinstance(node, ast.Set): + if not node.elts: + return "map[string]bool" + element_types = set() + for elt in node.elts: + element_types.add(self._guess_node_type(elt)) + if len(element_types) == 1: + return f"map[{list(element_types)[0]}]bool" + return "map[string]bool" + return "Any" +>>>>>>> REPLACE diff --git a/analyzer_patch_visitor.py b/analyzer_patch_visitor.py new file mode 100644 index 00000000..ea2533c2 --- /dev/null +++ b/analyzer_patch_visitor.py @@ -0,0 +1,11 @@ +<<<<<<< SEARCH + if isinstance(node.value, (ast.List, ast.Dict)): + inferred = self._infer_collection_type(node.value) + if target.id not in self.type_map or self.type_map[target.id] == "Any": + self.type_map[target.id] = inferred +======= + if isinstance(node.value, (ast.List, ast.Dict, ast.Set)): + inferred = self._infer_collection_type(node.value) + if target.id not in self.type_map or self.type_map[target.id] == "Any": + self.type_map[target.id] = inferred +>>>>>>> REPLACE diff --git a/calls_methods_fix.py b/calls_methods_fix.py new file mode 100644 index 00000000..48e71116 --- /dev/null +++ b/calls_methods_fix.py @@ -0,0 +1,51 @@ +<<<<<<< SEARCH + # list.pop() / dict.pop() + elif attr == "pop": + obj_type = self._guess_type(func_node.value) + if obj_type.startswith("map[") and obj_type.endswith("]bool"): + return self._handle_set_methods(node, func_node, args) + + obj_type = self._guess_type(func_node.value) + obj = self.visit(func_node.value) + if obj_type.startswith("[]"): +======= + # list.pop() / dict.pop() + elif attr == "pop": + obj_type = self._guess_type(func_node.value) + if obj_type.startswith("map[") and obj_type.endswith("]bool"): + return self._handle_set_methods(node, func_node, args) + + obj = self.visit(func_node.value) + if obj_type.startswith("[]"): +>>>>>>> REPLACE +<<<<<<< SEARCH + # list.remove() + elif attr == "remove" and len(args) == 1: + obj_type = self._guess_type(func_node.value) + if obj_type.startswith("map[") and obj_type.endswith("]bool"): + return self._handle_set_methods(node, func_node, args) + + obj_type = self._guess_type(func_node.value) + if obj_type.startswith("[]") or obj_type == "Any": +======= + # list.remove() + elif attr == "remove" and len(args) == 1: + obj_type = self._guess_type(func_node.value) + if obj_type.startswith("map[") and obj_type.endswith("]bool"): + return self._handle_set_methods(node, func_node, args) + + if obj_type.startswith("[]") or obj_type == "Any": +>>>>>>> REPLACE +<<<<<<< SEARCH + elif attr == "remove" and len(args) == 1: + obj_type = self._guess_type(func_node.value) + if obj_type.startswith("map[") and obj_type.endswith("]bool"): + return self._handle_set_methods(node, func_node, args) + + self.used_builtins.add("py_set_remove") + return f"py_set_remove(mut {obj}, {args[0]})" +======= + elif attr == "remove" and len(args) == 1: + self.used_builtins.add("py_set_remove") + return f"py_set_remove(mut {obj}, {args[0]})" +>>>>>>> REPLACE diff --git a/calls_methods_patch.py b/calls_methods_patch.py new file mode 100644 index 00000000..8530767a --- /dev/null +++ b/calls_methods_patch.py @@ -0,0 +1,131 @@ +<<<<<<< SEARCH + # dict.get() / update() / setdefault() + elif attr == "update": + obj_type = self._guess_type(func_node.value) + # Heuristic to avoid collision with hashlib + if obj_type in ("PyHashSha256", "PyHashMd5"): + return None + + # Additional heuristic: hashlib objects usually start with 'h' + if obj_type == "Any" and isinstance(func_node.value, ast.Name) and func_node.value.id.startswith("h"): + return None + + return self._handle_dict_update(node, func_node, args) + + elif attr == "setdefault": + return self._handle_dict_setdefault(node, func_node, args) + + elif attr == "get": + return self._handle_dict_get(node, func_node, args) + + return None +======= + # dict.get() / update() / setdefault() + elif attr == "update": + obj_type = self._guess_type(func_node.value) + # Heuristic to avoid collision with hashlib + if obj_type in ("PyHashSha256", "PyHashMd5"): + return None + + # Additional heuristic: hashlib objects usually start with 'h' + if obj_type == "Any" and isinstance(func_node.value, ast.Name) and func_node.value.id.startswith("h"): + return None + + if obj_type.startswith("map[") and obj_type.endswith("]bool"): + return self._handle_set_methods(node, func_node, args) + + return self._handle_dict_update(node, func_node, args) + + elif attr == "setdefault": + return self._handle_dict_setdefault(node, func_node, args) + + elif attr == "get": + return self._handle_dict_get(node, func_node, args) + + # Set methods + elif attr in ("add", "discard", "union", "intersection", "difference", "symmetric_difference", + "intersection_update", "difference_update", "symmetric_difference_update", + "issubset", "issuperset", "isdisjoint"): + return self._handle_set_methods(node, func_node, args) + + return None + + def _handle_set_methods(self, node: ast.Call, func_node: ast.Attribute, args: list) -> str | None: + """Handle set methods.""" + attr = func_node.attr + obj_type = self._guess_type(func_node.value) + if not (obj_type.startswith("map[") and obj_type.endswith("]bool")) and obj_type != "Any": + return None + + obj = self.visit(func_node.value) + + if attr == "add" and len(args) == 1: + return f"{obj}[{args[0]}] = true" + elif attr == "discard" and len(args) == 1: + return f"{obj}.delete({args[0]})" + elif attr == "remove" and len(args) == 1: + self.used_builtins.add("py_set_remove") + return f"py_set_remove(mut {obj}, {args[0]})" + elif attr == "pop" and len(args) == 0: + self.used_builtins.add("py_set_pop") + return f"py_set_pop(mut {obj})" + elif attr == "union" and len(args) >= 1: + self.used_builtins.add("py_set_union") + res = obj + for arg in args: + res = f"py_set_union({res}, {arg})" + return res + elif attr == "intersection" and len(args) >= 1: + self.used_builtins.add("py_set_intersection") + res = obj + for arg in args: + res = f"py_set_intersection({res}, {arg})" + return res + elif attr == "difference" and len(args) >= 1: + self.used_builtins.add("py_set_difference") + res = obj + for arg in args: + res = f"py_set_difference({res}, {arg})" + return res + elif attr == "symmetric_difference" and len(args) == 1: + self.used_builtins.add("py_set_xor") + return f"py_set_xor({obj}, {args[0]})" + elif attr == "update" and len(args) >= 1: + self.used_builtins.add("py_set_update") + res = "" + for i, arg in enumerate(args): + call = f"py_set_update(mut {obj}, {arg})" + if i == 0: res = call + else: res = f"({res}, {call})" # Nested calls for multiple args + return res + elif attr == "intersection_update" and len(args) >= 1: + self.used_builtins.add("py_set_intersection_update") + res = "" + for i, arg in enumerate(args): + call = f"py_set_intersection_update(mut {obj}, {arg})" + if i == 0: res = call + else: res = f"({res}, {call})" + return res + elif attr == "difference_update" and len(args) >= 1: + self.used_builtins.add("py_set_difference_update") + res = "" + for i, arg in enumerate(args): + call = f"py_set_difference_update(mut {obj}, {arg})" + if i == 0: res = call + else: res = f"({res}, {call})" + return res + elif attr == "symmetric_difference_update" and len(args) == 1: + self.used_builtins.add("py_set_xor_update") + return f"py_set_xor_update(mut {obj}, {args[0]})" + elif attr == "issubset" and len(args) == 1: + self.used_builtins.add("py_set_subset") + return f"py_set_subset({obj}, {args[0]})" + elif attr == "issuperset" and len(args) == 1: + self.used_builtins.add("py_set_subset") + return f"py_set_subset({args[0]}, {obj})" + elif attr == "isdisjoint" and len(args) == 1: + self.used_builtins.add("py_set_isdisjoint") + return f"py_set_isdisjoint({obj}, {args[0]})" + + return None +>>>>>>> REPLACE diff --git a/module_patch.py b/module_patch.py new file mode 100644 index 00000000..8ffe7a76 --- /dev/null +++ b/module_patch.py @@ -0,0 +1,121 @@ +<<<<<<< SEARCH + if "py_set_xor" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_xor[K](a map[K]bool, b map[K]bool) map[K]bool { + mut res := map[K]bool{} + for k, _ in a { + if k !in b { + res[k] = true + } + } + for k, _ in b { + if k !in a { + res[k] = true + } + } + return res +}""") +======= + if "py_set_xor" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_xor[K](a map[K]bool, b map[K]bool) map[K]bool { + mut res := map[K]bool{} + for k, _ in a { + if k !in b { + res[k] = true + } + } + for k, _ in b { + if k !in a { + res[k] = true + } + } + return res +}""") + + if "py_set_from_list" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_from_list[T](a []Any) T { + mut res := T{} + for x in a { + T is map[string]bool { + res[x.str()] = true + } T is map[int]bool { + res[x as int] = true + } { + // Fallback + } + } + return res +}""") + + if "py_set_remove" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_remove[K](mut s map[K]bool, val K) { + if val !in s { panic('KeyError') } + s.delete(val) +}""") + + if "py_set_pop" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_pop[K](mut s map[K]bool) K { + for k, _ in s { + s.delete(k) + return k + } + panic('KeyError: pop from an empty set') +}""") + + if "py_set_subset" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_subset[K](a map[K]bool, b map[K]bool) bool { + for k, _ in a { + if k !in b { return false } + } + return true +}""") + + if "py_set_strict_subset" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_strict_subset[K](a map[K]bool, b map[K]bool) bool { + return a.len < b.len && py_set_subset(a, b) +}""") + + if "py_set_isdisjoint" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_isdisjoint[K](a map[K]bool, b map[K]bool) bool { + for k, _ in a { + if k in b { return false } + } + return true +}""") + + if "py_set_update" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_update[K](mut a map[K]bool, b map[K]bool) map[K]bool { + for k, _ in b { + a[k] = true + } + return a +}""") + + if "py_set_intersection_update" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_intersection_update[K](mut a map[K]bool, b map[K]bool) map[K]bool { + mut to_delete := []K{} + for k, _ in a { + if k !in b { to_delete << k } + } + for k in to_delete { + a.delete(k) + } + return a +}""") + + if "py_set_difference_update" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_difference_update[K](mut a map[K]bool, b map[K]bool) map[K]bool { + for k, _ in b { + a.delete(k) + } + return a +}""") + + if "py_set_xor_update" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_xor_update[K](mut a map[K]bool, b map[K]bool) map[K]bool { + for k, _ in b { + if k in a { a.delete(k) } + else { a[k] = true } + } + return a +}""") +>>>>>>> REPLACE diff --git a/operators_patch.py b/operators_patch.py new file mode 100644 index 00000000..701b51ce --- /dev/null +++ b/operators_patch.py @@ -0,0 +1,25 @@ +<<<<<<< SEARCH + return f"{left} {op_str} {right}" + + parts = [] +======= + if isinstance(op, (ast.Lt, ast.LtE, ast.Gt, ast.GtE)): + right_type = self._guess_type(node.comparators[0]) + if (left_type.startswith("map[") and left_type.endswith("]bool")) and (right_type.startswith("map[") and right_type.endswith("]bool")): + if isinstance(op, ast.Lt): + self.used_builtins.add("py_set_strict_subset") + return f"py_set_strict_subset({left}, {right})" + elif isinstance(op, ast.LtE): + self.used_builtins.add("py_set_subset") + return f"py_set_subset({left}, {right})" + elif isinstance(op, ast.Gt): + self.used_builtins.add("py_set_strict_subset") + return f"py_set_strict_subset({right}, {left})" + elif isinstance(op, ast.GtE): + self.used_builtins.add("py_set_subset") + return f"py_set_subset({right}, {left})" + + return f"{left} {op_str} {right}" + + parts = [] +>>>>>>> REPLACE diff --git a/py2v_transpiler/core/analyzer_split/utils.py b/py2v_transpiler/core/analyzer_split/utils.py index 8d22409a..1c748019 100644 --- a/py2v_transpiler/core/analyzer_split/utils.py +++ b/py2v_transpiler/core/analyzer_split/utils.py @@ -92,6 +92,15 @@ def _guess_node_type(self, node: ast.AST) -> str: v_type = list(val_types)[0] return f"map[{k_type}]{v_type}" + elif isinstance(node, ast.Set): + if not node.elts: + return "map[string]bool" + element_types = set() + for elt in node.elts: + element_types.add(self._guess_node_type(elt)) + if len(element_types) == 1: + return f"map[{list(element_types)[0]}]bool" + return "map[string]bool" return "Any" def _infer_collection_type(self, node: ast.AST) -> str: @@ -106,3 +115,12 @@ def resolve_type(self, node: ast.AST) -> str: def get_variable_types(self) -> Dict[str, str]: """Returns the map of variable names to their V types.""" return self.type_map + + def _get_base_node(self, node: ast.AST) -> ast.AST: + curr = node + while isinstance(curr, (ast.Subscript, ast.Attribute)): + if isinstance(curr, ast.Subscript): + curr = curr.value + else: + curr = curr.value + return curr diff --git a/py2v_transpiler/core/analyzer_split/visitor.py b/py2v_transpiler/core/analyzer_split/visitor.py index 2f10e873..b16ce2f6 100644 --- a/py2v_transpiler/core/analyzer_split/visitor.py +++ b/py2v_transpiler/core/analyzer_split/visitor.py @@ -136,7 +136,7 @@ def visit_Assign(self, node: ast.Assign) -> Any: if inferred != "Any": if target.id not in self.type_map or self.type_map[target.id] == "Any": self.type_map[target.id] = inferred - if isinstance(node.value, (ast.List, ast.Dict)): + if isinstance(node.value, (ast.List, ast.Dict, ast.Set)): inferred = self._infer_collection_type(node.value) if target.id not in self.type_map or self.type_map[target.id] == "Any": self.type_map[target.id] = inferred diff --git a/py2v_transpiler/core/translator/expressions_split/calls_builtin.py b/py2v_transpiler/core/translator/expressions_split/calls_builtin.py index fb10d962..90cfdca2 100644 --- a/py2v_transpiler/core/translator/expressions_split/calls_builtin.py +++ b/py2v_transpiler/core/translator/expressions_split/calls_builtin.py @@ -55,7 +55,7 @@ def _handle_builtin_type_cast(self, node: ast.Call, func_name_str: str, original if len(args) == 0 and not node.keywords: return f"{v_type}{{}}" - # Handle dict([("x", 10)]) -> py_dict_from_pairs + # Handle dict([(\"x\", 10)]) -> py_dict_from_pairs if len(args) == 1 and not node.keywords: self.used_builtins.add("py_dict_from_pairs") return f"py_dict_from_pairs<{v_type}>({args[0]})" @@ -111,13 +111,13 @@ def _handle_builtin_type_cast(self, node: ast.Call, func_name_str: str, original v_type = "map[string]bool" if "map[Any]" in v_type: v_type = v_type.replace("map[Any]", "map[string]") - if not getattr(self, '_emitted_any_map_comment', False): + if not getattr(self, "_emitted_any_map_comment", False): self.output.append(f"{self._indent()}//##LLM@@ V requires map keys to be comparable types (like string, int). 'Any' was used as a map key in Python, which has been fallback-mapped to 'string'. Please review and manually adjust the map key type and its usage if necessary.") self._emitted_any_map_comment = True if len(args) == 0: return f"{v_type}{{}}" - return f"{v_type}({', '.join(args)})" - + self.used_builtins.add("py_set_from_list") + return f"py_set_from_list<{v_type}>({", ".join(args)})" # int() elif func_name_str == "int" or (original_id == "int" and func_name_str == "py_int"): if len(args) == 0: diff --git a/py2v_transpiler/core/translator/expressions_split/calls_methods.py b/py2v_transpiler/core/translator/expressions_split/calls_methods.py index 5cfa8e9e..2c67992b 100644 --- a/py2v_transpiler/core/translator/expressions_split/calls_methods.py +++ b/py2v_transpiler/core/translator/expressions_split/calls_methods.py @@ -49,6 +49,9 @@ def _handle_object_method_call(self, node: ast.Call, func_node: ast.AST, func_na # list.pop() / dict.pop() elif attr == "pop": obj_type = self._guess_type(func_node.value) + if obj_type.startswith("map[") and obj_type.endswith("]bool"): + return self._handle_set_methods(node, func_node, args) + obj = self.visit(func_node.value) if obj_type.startswith("[]"): if len(args) == 0: @@ -72,6 +75,9 @@ def _handle_object_method_call(self, node: ast.Call, func_node: ast.AST, func_na # list.remove() elif attr == "remove" and len(args) == 1: obj_type = self._guess_type(func_node.value) + if obj_type.startswith("map[") and obj_type.endswith("]bool"): + return self._handle_set_methods(node, func_node, args) + if obj_type.startswith("[]") or obj_type == "Any": obj = self.visit(func_node.value) self.used_builtins.add("py_list_remove") @@ -107,6 +113,9 @@ def _handle_object_method_call(self, node: ast.Call, func_node: ast.AST, func_na if obj_type == "Any" and isinstance(func_node.value, ast.Name) and func_node.value.id.startswith("h"): return None + if obj_type.startswith("map[") and obj_type.endswith("]bool"): + return self._handle_set_methods(node, func_node, args) + return self._handle_dict_update(node, func_node, args) elif attr == "setdefault": @@ -115,6 +124,91 @@ def _handle_object_method_call(self, node: ast.Call, func_node: ast.AST, func_na elif attr == "get": return self._handle_dict_get(node, func_node, args) + # Set methods + elif attr in ("add", "discard", "union", "intersection", "difference", "symmetric_difference", + "intersection_update", "difference_update", "symmetric_difference_update", + "issubset", "issuperset", "isdisjoint"): + return self._handle_set_methods(node, func_node, args) + + return None + + def _handle_set_methods(self, node: ast.Call, func_node: ast.Attribute, args: list) -> str | None: + """Handle set methods.""" + attr = func_node.attr + obj_type = self._guess_type(func_node.value) + if not (obj_type.startswith("map[") and obj_type.endswith("]bool")) and obj_type != "Any": + return None + + obj = self.visit(func_node.value) + + if attr == "add" and len(args) == 1: + return f"{obj}[{args[0]}] = true" + elif attr == "discard" and len(args) == 1: + return f"{obj}.delete({args[0]})" + elif attr == "remove" and len(args) == 1: + self.used_builtins.add("py_set_remove") + return f"py_set_remove(mut {obj}, {args[0]})" + elif attr == "pop" and len(args) == 0: + self.used_builtins.add("py_set_pop") + return f"py_set_pop(mut {obj})" + elif attr == "union" and len(args) >= 1: + self.used_builtins.add("py_set_union") + res = obj + for arg in args: + res = f"py_set_union({res}, {arg})" + return res + elif attr == "intersection" and len(args) >= 1: + self.used_builtins.add("py_set_intersection") + res = obj + for arg in args: + res = f"py_set_intersection({res}, {arg})" + return res + elif attr == "difference" and len(args) >= 1: + self.used_builtins.add("py_set_difference") + res = obj + for arg in args: + res = f"py_set_difference({res}, {arg})" + return res + elif attr == "symmetric_difference" and len(args) == 1: + self.used_builtins.add("py_set_xor") + return f"py_set_xor({obj}, {args[0]})" + elif attr == "update" and len(args) >= 1: + self.used_builtins.add("py_set_update") + res = "" + for i, arg in enumerate(args): + call = f"py_set_update(mut {obj}, {arg})" + if i == 0: res = call + else: res = f"({res}, {call})" # Nested calls for multiple args + return res + elif attr == "intersection_update" and len(args) >= 1: + self.used_builtins.add("py_set_intersection_update") + res = "" + for i, arg in enumerate(args): + call = f"py_set_intersection_update(mut {obj}, {arg})" + if i == 0: res = call + else: res = f"({res}, {call})" + return res + elif attr == "difference_update" and len(args) >= 1: + self.used_builtins.add("py_set_difference_update") + res = "" + for i, arg in enumerate(args): + call = f"py_set_difference_update(mut {obj}, {arg})" + if i == 0: res = call + else: res = f"({res}, {call})" + return res + elif attr == "symmetric_difference_update" and len(args) == 1: + self.used_builtins.add("py_set_xor_update") + return f"py_set_xor_update(mut {obj}, {args[0]})" + elif attr == "issubset" and len(args) == 1: + self.used_builtins.add("py_set_subset") + return f"py_set_subset({obj}, {args[0]})" + elif attr == "issuperset" and len(args) == 1: + self.used_builtins.add("py_set_subset") + return f"py_set_subset({args[0]}, {obj})" + elif attr == "isdisjoint" and len(args) == 1: + self.used_builtins.add("py_set_isdisjoint") + return f"py_set_isdisjoint({obj}, {args[0]})" + return None def _handle_file_methods(self, node: ast.Call, func_node: ast.Attribute, args: list) -> str | None: diff --git a/py2v_transpiler/core/translator/expressions_split/operators.py b/py2v_transpiler/core/translator/expressions_split/operators.py index cf029938..0f9aa364 100644 --- a/py2v_transpiler/core/translator/expressions_split/operators.py +++ b/py2v_transpiler/core/translator/expressions_split/operators.py @@ -362,6 +362,22 @@ def is_none_node(n: ast.AST) -> bool: return f"none !in {right}" return f"!{right}.any(it == none)" + if isinstance(op, (ast.Lt, ast.LtE, ast.Gt, ast.GtE)): + right_type = self._guess_type(node.comparators[0]) + if (left_type.startswith("map[") and left_type.endswith("]bool")) and (right_type.startswith("map[") and right_type.endswith("]bool")): + if isinstance(op, ast.Lt): + self.used_builtins.add("py_set_strict_subset") + return f"py_set_strict_subset({left}, {right})" + elif isinstance(op, ast.LtE): + self.used_builtins.add("py_set_subset") + return f"py_set_subset({left}, {right})" + elif isinstance(op, ast.Gt): + self.used_builtins.add("py_set_strict_subset") + return f"py_set_strict_subset({right}, {left})" + elif isinstance(op, ast.GtE): + self.used_builtins.add("py_set_subset") + return f"py_set_subset({right}, {left})" + return f"{left} {op_str} {right}" parts = [] diff --git a/py2v_transpiler/core/translator/module.py b/py2v_transpiler/core/translator/module.py index 541672ff..68edcf51 100644 --- a/py2v_transpiler/core/translator/module.py +++ b/py2v_transpiler/core/translator/module.py @@ -1275,6 +1275,94 @@ def visit_Module(self, node: ast.Module) -> str: return res }""") + if "py_set_from_list" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_from_list[T](a []Any) T { + mut res := T{} + for x in a { + T is map[string]bool { + res[x.str()] = true + } T is map[int]bool { + res[x as int] = true + } { + // Fallback + } + } + return res +}""") + + if "py_set_remove" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_remove[K](mut s map[K]bool, val K) { + if val !in s { panic('KeyError') } + s.delete(val) +}""") + + if "py_set_pop" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_pop[K](mut s map[K]bool) K { + for k, _ in s { + s.delete(k) + return k + } + panic('KeyError: pop from an empty set') +}""") + + if "py_set_subset" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_subset[K](a map[K]bool, b map[K]bool) bool { + for k, _ in a { + if k !in b { return false } + } + return true +}""") + + if "py_set_strict_subset" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_strict_subset[K](a map[K]bool, b map[K]bool) bool { + return a.len < b.len && py_set_subset(a, b) +}""") + + if "py_set_isdisjoint" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_isdisjoint[K](a map[K]bool, b map[K]bool) bool { + for k, _ in a { + if k in b { return false } + } + return true +}""") + + if "py_set_update" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_update[K](mut a map[K]bool, b map[K]bool) map[K]bool { + for k, _ in b { + a[k] = true + } + return a +}""") + + if "py_set_intersection_update" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_intersection_update[K](mut a map[K]bool, b map[K]bool) map[K]bool { + mut to_delete := []K{} + for k, _ in a { + if k !in b { to_delete << k } + } + for k in to_delete { + a.delete(k) + } + return a +}""") + + if "py_set_difference_update" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_difference_update[K](mut a map[K]bool, b map[K]bool) map[K]bool { + for k, _ in b { + a.delete(k) + } + return a +}""") + + if "py_set_xor_update" in self.used_builtins: + self.emitter.add_helper_function("""fn py_set_xor_update[K](mut a map[K]bool, b map[K]bool) map[K]bool { + for k, _ in b { + if k in a { a.delete(k) } + else { a[k] = true } + } + return a +}""") + if "py_dict_residual" in self.used_builtins: self.emitter.add_helper_function("""fn py_dict_residual[K, V](m map[K]V, exclude []K) map[K]Any { mut res := map[K]Any{} diff --git a/py2v_transpiler/main.py b/py2v_transpiler/main.py index cbc2c682..850c4371 100644 --- a/py2v_transpiler/main.py +++ b/py2v_transpiler/main.py @@ -58,7 +58,7 @@ def generate_all_helpers(output_path: str) -> None: translator.used_list_concat = True translator.used_dict_merge = True - translator.used_builtins = {"sorted", "reversed", "round", "py_subscript", "py_slice", "py_repr", "py_ascii", "py_format"} + translator.used_builtins = {"sorted", "reversed", "round", "py_subscript", "py_slice", "py_repr", "py_ascii", "py_format", "py_set_union", "py_set_intersection", "py_set_difference", "py_set_xor", "py_set_from_list", "py_set_remove", "py_set_pop", "py_set_subset", "py_set_strict_subset", "py_set_isdisjoint", "py_set_update", "py_set_intersection_update", "py_set_difference_update", "py_set_xor_update"} modules = [ "tempfile", "logging", "argparse", "pathlib", "collections", diff --git a/py2v_transpiler/tests/translator/test_set_full_ops.py b/py2v_transpiler/tests/translator/test_set_full_ops.py new file mode 100644 index 00000000..967422a2 --- /dev/null +++ b/py2v_transpiler/tests/translator/test_set_full_ops.py @@ -0,0 +1,93 @@ +import ast +import unittest +from py2v_transpiler.core.translator import VNodeVisitor +from py2v_transpiler.core.analyzer import TypeInference + +class TestSetFullOps(unittest.TestCase): + def setUp(self): + self.ti = TypeInference() + self.translator = VNodeVisitor(self.ti) + + def translate_stmt(self, code, index=-1): + tree = ast.parse(code) + self.ti.analyze(tree) + stmt = tree.body[index] + self.translator.output = [] + self.translator.visit(stmt) + return "\n".join(self.translator.output).strip() + + def translate_expr(self, code): + tree = ast.parse(code) + self.ti.analyze(tree) + last_stmt = tree.body[-1] + if isinstance(last_stmt, ast.Expr): + node = last_stmt.value + elif isinstance(last_stmt, ast.Assign): + node = last_stmt.value + else: + node = last_stmt + return self.translator.visit(node) + + def test_set_init_from_list(self): + code = "s: set[int] = set([1, 2, 2])" + tree = ast.parse(code) + self.ti.analyze(tree) + # Manually set the current assignment type to simulate AnnAssign context + self.translator.current_assignment_type = "map[int]bool" + result = self.translator.visit(tree.body[0].value) + self.assertEqual(result, "py_set_from_list([1, 2, 2])") + self.assertIn("py_set_from_list", self.translator.used_builtins) + + def test_set_add(self): + code = "s = {1}; s.add(2)" + result = self.translate_stmt(code, index=1) + self.assertEqual(result, "s[2] = true") + + def test_set_remove(self): + code = "s = {1}; s.remove(1)" + result = self.translate_stmt(code, index=1) + self.assertEqual(result, "py_set_remove(mut s, 1)") + self.assertIn("py_set_remove", self.translator.used_builtins) + + def test_set_discard(self): + code = "s = {1}; s.discard(1)" + result = self.translate_stmt(code, index=1) + self.assertEqual(result, "s.delete(1)") + + def test_set_pop(self): + code = "s = {1}; s.pop()" + result = self.translate_expr(code) + self.assertEqual(result, "py_set_pop(mut s)") + self.assertIn("py_set_pop", self.translator.used_builtins) + + def test_set_clear(self): + code = "s = {1}; s.clear()" + result = self.translate_stmt(code, index=1) + self.assertEqual(result, "/* s.clear() */ s = {}") + + def test_set_comparison_subset(self): + code = "a = {1}; b = {1, 2}; a <= b" + result = self.translate_expr(code) + self.assertEqual(result, "py_set_subset(a, b)") + self.assertIn("py_set_subset", self.translator.used_builtins) + + def test_set_comparison_strict_subset(self): + code = "a = {1}; b = {1, 2}; a < b" + result = self.translate_expr(code) + self.assertEqual(result, "py_set_strict_subset(a, b)") + self.assertIn("py_set_strict_subset", self.translator.used_builtins) + + def test_set_union_method(self): + code = "a = {1}; b = {2}; a.union(b)" + result = self.translate_expr(code) + self.assertEqual(result, "py_set_union(a, b)") + self.assertIn("py_set_union", self.translator.used_builtins) + + def test_set_update_method(self): + code = "a = {1}; b = {2}; a.update(b)" + result = self.translate_stmt(code, index=2) + self.assertEqual(result, "py_set_update(mut a, b)") + self.assertIn("py_set_update", self.translator.used_builtins) + +if __name__ == "__main__": + unittest.main()