Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions attributes.py.diff

This file was deleted.

111 changes: 0 additions & 111 deletions common.py.diff

This file was deleted.

19 changes: 0 additions & 19 deletions names.py.diff

This file was deleted.

6 changes: 4 additions & 2 deletions py2v_transpiler/core/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,17 @@ def add_constant(self, const_def: str) -> None:

def add_struct(self, struct_def: str) -> None:
"""Adds a struct definition."""
self.structs.append(struct_def)
if struct_def not in self.structs:
self.structs.append(struct_def)

def add_helper_struct(self, struct_def: str) -> None:
"""Adds a struct definition to helpers."""
self.helper_structs.append(struct_def)

def add_function(self, func_def: str) -> None:
"""Adds a function definition."""
self.functions.append(func_def)
if func_def not in self.functions:
self.functions.append(func_def)

def add_helper_function(self, func_def: str) -> None:
"""Adds a function definition to helpers."""
Expand Down
2 changes: 2 additions & 0 deletions py2v_transpiler/core/translator/base_split/type_guessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ def _guess_type_call(self, node: ast.Call) -> str:
if node.args and self._is_literal_string_expr(node.args[0]):
return "LiteralString"
return "string"
if fid == "range":
return "range"
if fid == "int":
return "int"
if fid == "float":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def _register_literal_enum(self, nodes: Sequence[ast.AST]) -> str:
val_map[val] = member_name

enum_lines.append("}")
self.emitter.add_helper_struct("\n".join(enum_lines))
self.emitter.add_struct("\n".join(enum_lines))

# Add .str() method to the enum
str_lines = [f"pub fn (e {enum_name}) str() string {{", " match e {"]
Expand All @@ -101,7 +101,7 @@ def _register_literal_enum(self, nodes: Sequence[ast.AST]) -> str:
str_lines.append(f" .{member} {{ return '{str_val}' }}")
str_lines.append(" }")
str_lines.append("}")
self.emitter.add_helper_struct("\n".join(str_lines))
self.emitter.add_struct("\n".join(str_lines))

self._generated_literal_enums[key] = enum_name
self._literal_enum_values[enum_name] = val_map
Expand Down Expand Up @@ -152,7 +152,7 @@ def clean(s: str) -> str:

pub = "pub " if self.config and getattr(self.config, 'include_all_symbols', False) else ""

self.emitter.add_helper_struct(f"{pub}type {type_name}{gen_decl} = {normalized}")
self.emitter.add_struct(f"{pub}type {type_name}{gen_decl} = {normalized}")

result = f"{type_name}{gen_args}"
self._generated_sum_types[normalized] = result
Expand All @@ -176,7 +176,7 @@ def _register_tuple_struct(self, tuple_types_str: str) -> str:
pub = "pub " if self.config and getattr(self.config, "include_all_symbols", False) else ""
struct_def = f"{pub}struct {struct_name} {{\n" + "\n".join(fields) + "\n}"

self.emitter.add_helper_struct(struct_def)
self.emitter.add_struct(struct_def)

self._generated_tuple_structs[struct_name] = struct_name
return struct_name
26 changes: 9 additions & 17 deletions py2v_transpiler/core/translator/expressions_split/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,17 @@ def visit_Attribute(self, node: ast.Attribute) -> str:
if not (base_type.startswith("?") and base_type[1:] == narrowed_type):
# Ensure narrowed_type is mapped to a V type
v_narrowed_type = self._map_type(narrowed_type)
v_base_type = self._map_type(base_type) if base_type else None

if v_narrowed_type not in ("Any", "void", "unknown"):
# Special case: don't cast from a named struct (NamedTuple/Class)
# to a generic collection/TupleStruct if we are accessing an attribute.
# This avoids breaking field access like (p2 as []string).x
v_base_name = base_type.split('.')[-1]
v_base_name = v_base_type.split('.')[-1] if v_base_type else ""
is_named_struct = v_base_name and v_base_name[0].isupper() and not v_base_name.startswith("TupleStruct_")
is_generic_cast = v_narrowed_type.startswith("[]") or v_narrowed_type.startswith("TupleStruct_") or v_narrowed_type == "Any"

if not (is_named_struct and is_generic_cast):
if not (is_named_struct and is_generic_cast) and v_narrowed_type != v_base_type:
# Emit an explicit cast in V: (obj as NarrowedType)
obj = f"({obj} as {v_narrowed_type})"

Expand Down Expand Up @@ -146,32 +148,22 @@ def visit_Attribute(self, node: ast.Attribute) -> str:
return f"{obj}__{attr_name}"

# Descriptor narrowing check
# If the attribute itself has a narrowed type that's explicitly not generic,
# and it's accessed via a standard property/descriptor pattern, we can
# either emit an explicit cast, or rely on V's static type mapping if properties
# are mapped accurately.
# Since Python properties map to V struct fields (or getters if we had them),
# if the type is explicitly narrowed from a dynamic attribute to a static type,
# we can wrap it in a cast if needed, but usually just returning it is fine unless
# it was typed as Any/void previously.
# To strictly enforce the descriptor narrowing request: "if a descriptor returns a specific type, use it in V."
# If mypy knows `m.desc` is an `int` because of `Descriptor.__get__ -> int`, we should ensure
# that type is used if assigned or passed. We can use an explicit cast if it helps V.
# Just checking if `type_inference` has mapped `StructName.attr_name`.
# First, we need to know the base class name of `obj`.
base_obj_type = self._guess_type(node.value)
if base_obj_type and base_obj_type not in ("Any", "unknown", "void", "int", "f64", "string", "bool"):
desc_key = f"{base_obj_type}.{node.attr}"
narrowed_desc_type = self.type_inference.type_map.get(desc_key)
if narrowed_desc_type and narrowed_desc_type not in ("Any", "unknown", "void"):
attr_name = self._sanitize_name(node.attr)

# Ensure mapped V type
v_narrowed_desc_type = self._map_type(narrowed_desc_type)

# Check if it corresponds to a known function first
if obj in self.function_names:
return f"({obj}__{attr_name} as {narrowed_desc_type})"
return f"({obj}__{attr_name} as {v_narrowed_desc_type})"

# Otherwise, it's a struct field access
return f"({obj}.{attr_name} as {narrowed_desc_type})"
return f"({obj}.{attr_name} as {v_narrowed_desc_type})"

# Handle SCC Attribute access: imported_module.attr -> prefix__attr
if isinstance(node.value, ast.Name) and node.value.id in self.imported_modules:
Expand Down
37 changes: 27 additions & 10 deletions py2v_transpiler/core/translator/expressions_split/calls_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ def _handle_builtin_type_cast(self, node: ast.Call, func_name_str: str, original
# Create a copy and update it
return f"py_dict_update(mut {v_type}({args[0]}).clone(), {kwargs_dict})"

return f"{v_type}({', '.join(args)})"
return f"{v_type}({", ".join(args)})"

# dict.fromkeys()
elif full_func_name == "dict.fromkeys":
Expand All @@ -93,7 +93,8 @@ def _handle_builtin_type_cast(self, node: ast.Call, func_name_str: str, original
v_type = "[]Any"
if len(args) == 0:
return f"{v_type}{{}}"
return f"{v_type}({', '.join(args)})"
self.used_builtins.add("py_list_cast")
return f"py_list_cast<{v_type}>({args[0]})"

# tuple()
elif func_name_str == "tuple" or (original_id == "tuple" and func_name_str == "py_tuple"):
Expand All @@ -102,21 +103,37 @@ def _handle_builtin_type_cast(self, node: ast.Call, func_name_str: str, original
v_type = "[]Any"
if len(args) == 0:
return f"{v_type}{{}}"
return f"{v_type}({', '.join(args)})"
self.used_builtins.add("py_list_cast")
return f"py_list_cast<{v_type}>({args[0]})"

# set()
elif func_name_str == "set" or (original_id == "set" and func_name_str == "py_set"):
v_type = self.current_assignment_type or "map[string]bool"
if not v_type.startswith("map["):
v_type = "map[string]bool"
if "map[Any]" in v_type:
v_type = v_type.replace("map[Any]", "map[string]")
v_assigned_type = self.current_assignment_type
v_type_final = ""
if len(args) == 1:
arg_type = self._guess_type(node.args[0])
if arg_type.startswith("[]"):
guessed_v_type = f"map[{arg_type[2:]}]bool"
if not v_assigned_type or "map[string]bool" in v_assigned_type:
v_type_final = guessed_v_type
elif arg_type == "range":
v_type_final = "map[int]bool"

if not v_type_final:
v_type_final = v_assigned_type or "map[string]bool"

if not v_type_final.startswith("map["):
v_type_final = "map[string]bool"

if "map[Any]" in v_type_final:
v_type_final = v_type_final.replace("map[Any]", "map[string]")
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)})"
return f"{v_type_final}{{}}"
self.used_builtins.add("py_set_from_list")
return f"py_set_from_list<{v_type_final}>({args[0]})"

# int()
elif func_name_str == "int" or (original_id == "int" and func_name_str == "py_int"):
Expand Down
16 changes: 16 additions & 0 deletions py2v_transpiler/core/translator/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -1516,4 +1516,20 @@ def visit_Module(self, node: ast.Module) -> str:
self.emitter.add_helper_struct("struct PyPathSplitExt { root string; ext string }")
self.emitter.add_helper_function("fn py_os_path_splitext(path string) PyPathSplitExt { ext := os.file_ext(path); return PyPathSplitExt{ root: path[..path.len - ext.len], ext: ext } }")

if 'py_set_from_list' in self.used_builtins:
self.emitter.add_helper_function("fn py_set_from_list[M, K](l []K) M { mut res := M{}; for x in l { res[x] = true }; return res }")

if 'py_list_cast' in self.used_builtins:
self.emitter.add_helper_function("""fn py_list_cast[L, T](l []T) L {
mut res := L{cap: l.len}
for x in l {
$if T is Any {
res << x
} $else {
res << x
}
}
return res
}""")

return self.emitter.emit()
4 changes: 2 additions & 2 deletions py2v_transpiler/core/translator/variables_split/names.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ def visit_Name(self, node: ast.Name) -> str:

# If base type is unknown or differs from narrowed, apply cast
if not v_base_type or (v_narrowed_type != v_base_type and not (v_base_type.startswith("?") and v_base_type[1:] == v_narrowed_type)):
# Special case: don't cast from a named struct (NamedTuple/Class)
# Special case: don't cast from a named struct (NamedTuple/Class)
# to a generic collection/TupleStruct or Any.
v_base_name = v_base_type.split('.')[-1] if v_base_type else ""
is_named_struct = v_base_name and v_base_name[0].isupper() and not v_base_name.startswith("TupleStruct_")
is_generic_cast = v_narrowed_type.startswith("[]") or v_narrowed_type.startswith("TupleStruct_") or v_narrowed_type == "Any"

if not (is_named_struct and is_generic_cast):
res = f"({res} as {v_narrowed_type})"

Expand Down
Loading