diff --git a/asmpython/_compiler/__init__.py b/asmpython/_compiler/__init__.py index 9eb76db35..db77c41c2 100644 --- a/asmpython/_compiler/__init__.py +++ b/asmpython/_compiler/__init__.py @@ -16,10 +16,15 @@ from . import type_parameter_compat_fixes as _type_parameter_compat_fixes from . import field_flow_compat_fixes as _field_flow_compat_fixes from . import container_field_compat_fixes as _container_field_compat_fixes +from . import global_singleton_flow_compat_fixes as _global_singleton_flow_compat_fixes from . import live_definition_compat_fixes as _live_definition_compat_fixes from . import empty_collection_compat_fixes as _empty_collection_compat_fixes from . import ordered_flow_compat_fixes as _ordered_flow_compat_fixes from . import descriptor_precedence_compat_fixes as _descriptor_precedence_compat_fixes from . import return_annotation_precedence_compat_fixes as _return_annotation_precedence_compat_fixes +from . import class_value_compat_fixes as _class_value_compat_fixes +from . import dynamic_value_compat_fixes as _dynamic_value_compat_fixes +from . import concrete_specialization_compat_fixes as _concrete_specialization_compat_fixes +from . import static_method_call_compat_fixes as _static_method_call_compat_fixes __all__ = ["__version__"] diff --git a/asmpython/_compiler/class_value_compat_fixes.py b/asmpython/_compiler/class_value_compat_fixes.py new file mode 100644 index 000000000..3238ee6a1 --- /dev/null +++ b/asmpython/_compiler/class_value_compat_fixes.py @@ -0,0 +1,255 @@ +"""Lower finite class-valued collections before semantic analysis. + +Native whole-program compilation can statically resolve class objects stored +in literal tuples. This pass folds literal tuple indexing and unrolls safe +loops over those tuples, replacing the loop variable with each concrete +class. It also materializes inherited class/static methods on those concrete +classes so backend symbol emission preserves Python's subclass ``cls`` value. +""" + +from __future__ import annotations + +from dataclasses import fields, is_dataclass + +from . import ast_nodes as A +from .sema import SemaAnalyzer + + +_ORIGINAL_ANALYZE = SemaAnalyzer.analyze + + +class _Splice: + def __init__(self, items: list) -> None: + self.items = items + + +def _clone(value): + if isinstance(value, list): + return [_clone(item) for item in value] + if isinstance(value, tuple): + return tuple(_clone(item) for item in value) + if isinstance(value, dict): + return {_clone(key): _clone(item) for key, item in value.items()} + if isinstance(value, set): + return {_clone(item) for item in value} + if is_dataclass(value) and not isinstance(value, type): + cls = type(value) + init_values = {} + deferred = [] + for data_field in fields(value): + cloned_value = _clone(getattr(value, data_field.name)) + if data_field.init: + init_values[data_field.name] = cloned_value + else: + deferred.append((data_field.name, cloned_value)) + cloned = cls(**init_values) + for name, cloned_value in deferred: + setattr(cloned, name, cloned_value) + return cloned + return value + + +def _static_class_tuples(mod: A.Module) -> dict: + class_names = {definition.name for definition in mod.classes} + result = {} + for statement in mod.body: + if not isinstance(statement, A.Assign): + continue + if not isinstance(statement.value, A.TupleLit): + continue + names = [] + valid = True + for element in statement.value.elems: + if not isinstance(element, A.Name) or element.name not in class_names: + valid = False + break + names.append(element.name) + if valid and names: + result[statement.target] = tuple(names) + return result + + +def _materialize_inherited_class_methods(mod: A.Module, class_tuples: dict) -> None: + """Emit subclass symbols for inherited class/static methods. + + Backends key method bodies by ``ClassName__method``. A classmethod invoked + through a concrete subclass must still receive that subclass as ``cls``. + Copying the nearest inherited class/static method onto each finite concrete + class preserves that behavior and gives codegen the symbol it already + expects, without introducing a dynamic metatype runtime. + """ + class_table = {definition.name: definition for definition in mod.classes} + concrete_names = { + class_name + for entries in class_tuples.values() + for class_name in entries + } + for class_name in concrete_names: + owner = class_table.get(class_name) + if owner is None: + continue + existing = {method.name for method in owner.methods} + parent_name = owner.parent + seen = set() + while parent_name and parent_name not in seen: + seen.add(parent_name) + parent = class_table.get(parent_name) + if parent is None: + break + for method in parent.methods: + decorators = set(getattr(method, "decorators", [])) + if method.name in existing: + continue + if not decorators.intersection({"classmethod", "staticmethod"}): + continue + owner.methods.append(_clone(method)) + existing.add(method.name) + parent_name = parent.parent + + +def _contains_loop_control(value) -> bool: + if isinstance(value, (A.Break, A.Continue)): + return True + if isinstance(value, (str, int, float, bool, type(None))): + return False + if isinstance(value, (list, tuple, set)): + return any(_contains_loop_control(item) for item in value) + if isinstance(value, dict): + return any( + _contains_loop_control(key) or _contains_loop_control(item) + for key, item in value.items() + ) + if is_dataclass(value) and not isinstance(value, type): + return any( + _contains_loop_control(getattr(value, data_field.name)) + for data_field in fields(value) + ) + return False + + +def _binds_name(value, name: str) -> bool: + if isinstance(value, A.Assign) and value.target == name: + return True + if isinstance(value, A.MultiAssign) and name in value.targets: + return True + if isinstance(value, A.For): + if value.var == name or name in value.targets: + return True + if isinstance(value, (str, int, float, bool, type(None))): + return False + if isinstance(value, (list, tuple, set)): + return any(_binds_name(item, name) for item in value) + if isinstance(value, dict): + return any( + _binds_name(key, name) or _binds_name(item, name) + for key, item in value.items() + ) + if is_dataclass(value) and not isinstance(value, type): + return any( + _binds_name(getattr(value, data_field.name), name) + for data_field in fields(value) + ) + return False + + +def _rewrite(value, class_tuples: dict, substitutions: dict): + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, list): + rewritten = [] + for item in value: + transformed = _rewrite(item, class_tuples, substitutions) + if isinstance(transformed, _Splice): + rewritten.extend(transformed.items) + else: + rewritten.append(transformed) + return rewritten + if isinstance(value, tuple): + return tuple(_rewrite(item, class_tuples, substitutions) for item in value) + if isinstance(value, dict): + return { + _rewrite(key, class_tuples, substitutions): _rewrite( + item, class_tuples, substitutions + ) + for key, item in value.items() + } + if isinstance(value, set): + return {_rewrite(item, class_tuples, substitutions) for item in value} + + if isinstance(value, A.Name) and value.name in substitutions: + return A.Name(name=substitutions[value.name], pos=value.pos) + + if isinstance(value, A.Call) and value.func in substitutions: + value.func = substitutions[value.func] + + if isinstance(value, A.Subscript): + value.obj = _rewrite(value.obj, class_tuples, substitutions) + value.index = _rewrite(value.index, class_tuples, substitutions) + if ( + isinstance(value.obj, A.Name) + and value.obj.name in class_tuples + and isinstance(value.index, A.IntLit) + ): + entries = class_tuples[value.obj.name] + index = value.index.value + if index < 0: + index += len(entries) + if 0 <= index < len(entries): + return A.Name(name=entries[index], pos=value.pos) + return value + + if isinstance(value, A.For): + value.iter = _rewrite(value.iter, class_tuples, substitutions) + if ( + isinstance(value.iter, A.Name) + and value.iter.name in class_tuples + and not value.targets + and not value.orelse + and not _contains_loop_control(value.body) + and not _binds_name(value.body, value.var) + ): + expanded = [] + for class_name in class_tuples[value.iter.name]: + body = _clone(value.body) + local_substitutions = dict(substitutions) + local_substitutions[value.var] = class_name + transformed = _rewrite(body, class_tuples, local_substitutions) + expanded.extend(transformed) + return _Splice(expanded) + + if is_dataclass(value) and not isinstance(value, type): + for data_field in fields(value): + current = getattr(value, data_field.name) + transformed = _rewrite(current, class_tuples, substitutions) + if isinstance(transformed, _Splice): + raise TypeError( + "class-value loop expansion requires a statement list" + ) + setattr(value, data_field.name, transformed) + return value + + +def _lower_finite_class_values(mod: A.Module) -> None: + if getattr(mod, "_finite_class_values_lowered", False): + return + mod._finite_class_values_lowered = True + class_tuples = _static_class_tuples(mod) + if not class_tuples: + return + _materialize_inherited_class_methods(mod, class_tuples) + mod.body = _rewrite(mod.body, class_tuples, {}) + for function in mod.funcs: + function.body = _rewrite(function.body, class_tuples, {}) + for owner in mod.classes: + for method in owner.methods: + method.body = _rewrite(method.body, class_tuples, {}) + + +def _analyze_with_finite_class_values(self: SemaAnalyzer) -> None: + _lower_finite_class_values(self.mod) + _ORIGINAL_ANALYZE(self) + + +if not getattr(SemaAnalyzer, "_asmpython_finite_class_value_patch", False): + SemaAnalyzer.analyze = _analyze_with_finite_class_values + SemaAnalyzer._asmpython_finite_class_value_patch = True diff --git a/asmpython/_compiler/concrete_specialization_compat_fixes.py b/asmpython/_compiler/concrete_specialization_compat_fixes.py new file mode 100644 index 000000000..f6150f396 --- /dev/null +++ b/asmpython/_compiler/concrete_specialization_compat_fixes.py @@ -0,0 +1,271 @@ +"""Whole-program specialization for concrete class and argument values. + +Finite class lowering may materialize an inherited classmethod on a concrete +subclass so native codegen has a ``Subclass__method`` symbol. Python still +requires ``cls`` inside that body to be the concrete subclass. This pass turns +those copied methods into static, subclass-specialized bodies and folds reads of +concrete class variables to their nearest class-body initializer. + +It also annotates an otherwise-unannotated top-level parameter when every call +site supplies the same statically-known value kind. This keeps ordinary dynamic +Python source while giving native method dispatch the concrete representation it +needs (for example a string parameter used with ``startswith``). +""" + +from __future__ import annotations + +from dataclasses import fields, is_dataclass + +from . import ast_nodes as A +from . import class_value_compat_fixes as class_values +from .metaclass_compat_fixes import _walk_stmts +from .sema import SemaAnalyzer + + +_ORIGINAL_ANALYZE = SemaAnalyzer.analyze + + +def _replace_identifier(value, old: str, new: str) -> None: + if value is None or isinstance(value, (str, int, float, bool)): + return + if isinstance(value, A.Name): + if value.name == old: + value.name = new + return + if isinstance(value, A.Call) and value.func == old: + value.func = new + if isinstance(value, list) or isinstance(value, tuple): + for item in value: + _replace_identifier(item, old, new) + return + if isinstance(value, dict): + for key, item in value.items(): + _replace_identifier(key, old, new) + _replace_identifier(item, old, new) + return + if is_dataclass(value) and not isinstance(value, type): + for data_field in fields(value): + _replace_identifier(getattr(value, data_field.name), old, new) + + +def _nearest_parent_method(owner, method_name: str, class_table: dict): + parent_name = owner.parent + seen = set() + while parent_name and parent_name not in seen: + seen.add(parent_name) + parent = class_table.get(parent_name) + if parent is None: + return None + for method in parent.methods: + if method.name == method_name: + return method + parent_name = parent.parent + return None + + +def _class_variable_initializer(class_name: str, field_name: str, class_table: dict): + current_name = class_name + seen = set() + while current_name and current_name not in seen: + seen.add(current_name) + owner = class_table.get(current_name) + if owner is None: + return None + for name, _annotation, initializer in owner.class_vars: + if name == field_name and initializer is not None: + return class_values._clone(initializer) + current_name = owner.parent + return None + + +def _fold_concrete_class_attributes(value, class_name: str, class_table: dict): + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, list): + return [ + _fold_concrete_class_attributes(item, class_name, class_table) + for item in value + ] + if isinstance(value, tuple): + return tuple( + _fold_concrete_class_attributes(item, class_name, class_table) + for item in value + ) + if isinstance(value, dict): + return { + _fold_concrete_class_attributes(key, class_name, class_table): + _fold_concrete_class_attributes(item, class_name, class_table) + for key, item in value.items() + } + if isinstance(value, A.Attr): + value.obj = _fold_concrete_class_attributes( + value.obj, class_name, class_table + ) + if isinstance(value.obj, A.Name) and value.obj.name == class_name: + initializer = _class_variable_initializer( + class_name, value.name, class_table + ) + if initializer is not None: + return initializer + return value + if is_dataclass(value) and not isinstance(value, type): + for data_field in fields(value): + setattr( + value, + data_field.name, + _fold_concrete_class_attributes( + getattr(value, data_field.name), class_name, class_table + ), + ) + return value + + +def _specialize_materialized_classmethods(mod: A.Module, class_tuples: dict) -> None: + class_table = {owner.name: owner for owner in mod.classes} + concrete_names = { + class_name + for entries in class_tuples.values() + for class_name in entries + } + for class_name in concrete_names: + owner = class_table.get(class_name) + if owner is None: + continue + for method in owner.methods: + decorators = list(getattr(method, "decorators", []) or []) + if "classmethod" not in decorators or not method.params: + continue + inherited = _nearest_parent_method(owner, method.name, class_table) + if inherited is None: + continue + inherited_decorators = list(getattr(inherited, "decorators", []) or []) + # The finite-class pass copies the nearest method verbatim, preserving + # source position. Do not alter an explicit subclass override. + if "classmethod" not in inherited_decorators or method.pos != inherited.pos: + continue + receiver = method.params[0] + _replace_identifier(method.body, receiver, class_name) + method.body = _fold_concrete_class_attributes( + method.body, class_name, class_table + ) + del method.params[0] + if method.defaults: + del method.defaults[0] + if method.param_types: + del method.param_types[0] + method.readonly_params = [ + name for name in method.readonly_params if name != receiver + ] + method.decorators = [ + decorator + for decorator in decorators + if decorator != "classmethod" + ] + if "staticmethod" not in method.decorators: + method.decorators.append("staticmethod") + + +def _literal_annotation(expression, class_names: set): + if isinstance(expression, A.StrLit): + return ("str", None) + if isinstance(expression, A.FloatLit): + return ("float", None) + if isinstance(expression, A.IntLit): + if getattr(expression, "is_none", False): + return None + return ("bool" if getattr(expression, "is_bool", False) else "int", None) + if isinstance(expression, A.ListLit): + return ("list", getattr(expression, "el_type", None)) + if isinstance(expression, A.DictLit): + return ("dict", None) + if isinstance(expression, A.TupleLit): + return ("tuple", None) + if isinstance(expression, A.SetLit): + return ("set", None) + if isinstance(expression, A.Call) and expression.func in class_names: + return (expression.func, None) + if isinstance(expression, A.Name) and expression.name in class_names: + return ("type", None) + return None + + +def _all_calls(mod: A.Module) -> list: + calls = [] + statement_lists = [mod.body] + statement_lists.extend(function.body for function in mod.funcs) + for owner in mod.classes: + statement_lists.extend(method.body for method in owner.methods) + for statements in statement_lists: + for node in _walk_stmts(statements): + if isinstance(node, A.Call): + calls.append(node) + return calls + + +def _bound_argument(call: A.Call, function, parameter_index: int): + if parameter_index < len(call.args): + return call.args[parameter_index] + parameter = function.params[parameter_index] + for name, value in call.kwargs: + if name == parameter: + return value + if parameter_index < len(function.defaults): + return function.defaults[parameter_index] + return None + + +def _specialize_unanimous_function_parameters(mod: A.Module) -> None: + class_names = {owner.name for owner in mod.classes} + definitions = {} + duplicates = set() + for function in mod.funcs: + if function.name in definitions: + duplicates.add(function.name) + else: + definitions[function.name] = function + for name in duplicates: + definitions.pop(name, None) + + calls_by_name = {} + for call in _all_calls(mod): + calls_by_name.setdefault(call.func, []).append(call) + + for name, function in definitions.items(): + calls = calls_by_name.get(name, []) + if not calls: + continue + param_types = list(function.param_types) + while len(param_types) < len(function.params): + param_types.append(None) + changed = False + for parameter_index in range(len(function.params)): + if param_types[parameter_index] is not None: + continue + observed = [] + complete = True + for call in calls: + argument = _bound_argument(call, function, parameter_index) + annotation = _literal_annotation(argument, class_names) + if annotation is None: + complete = False + break + observed.append(annotation) + if complete and observed and all(value == observed[0] for value in observed): + param_types[parameter_index] = observed[0] + changed = True + if changed: + function.param_types = param_types + + +def _analyze_with_concrete_specializations(self: SemaAnalyzer) -> None: + class_values._lower_finite_class_values(self.mod) + class_tuples = class_values._static_class_tuples(self.mod) + if class_tuples: + _specialize_materialized_classmethods(self.mod, class_tuples) + _specialize_unanimous_function_parameters(self.mod) + _ORIGINAL_ANALYZE(self) + + +if not getattr(SemaAnalyzer, "_asmpython_concrete_specialization_patch", False): + SemaAnalyzer.analyze = _analyze_with_concrete_specializations + SemaAnalyzer._asmpython_concrete_specialization_patch = True diff --git a/asmpython/_compiler/container_field_compat_fixes.py b/asmpython/_compiler/container_field_compat_fixes.py index 174d563ae..951204f52 100644 --- a/asmpython/_compiler/container_field_compat_fixes.py +++ b/asmpython/_compiler/container_field_compat_fixes.py @@ -24,6 +24,45 @@ def _annotation_parts(annotation) -> tuple[str, "str | None"]: return "", None +def _literal_kind(expression) -> "str | None": + if isinstance(expression, A.StrLit) or isinstance(expression, A.FString): + return "str" + if isinstance(expression, A.FloatLit): + return "float" + if isinstance(expression, A.IntLit): + if getattr(expression, "is_none", False): + return None + return "bool" if getattr(expression, "is_bool", False) else "int" + if isinstance(expression, A.ListLit): + return "list" + if isinstance(expression, A.DictLit): + return "dict" + if isinstance(expression, A.TupleLit): + return "tuple" + if isinstance(expression, A.SetLit): + return "set" + return None + + +def _literal_collection_element(expression, base: str) -> "str | None": + values = None + if base == "dict" and isinstance(expression, A.DictLit): + values = list(expression.values) + elif base == "list" and isinstance(expression, A.ListLit): + values = list(expression.elems) + if not values: + return None + kinds = [_literal_kind(value) for value in values] + if any(kind is None for kind in kinds): + return None + unique = set(kinds) + if len(unique) == 1: + return kinds[0] + if unique.issubset({"bool", "int", "float"}): + return "float" if "float" in unique else "int" + return "any" + + def _hierarchy_method_names(owner, classes: dict) -> set[str]: result: set[str] = set() current = owner @@ -86,6 +125,109 @@ def _cross_object_self_element(owner, field_name: str) -> "str | None": return None +def _common_class(left: str, right: str, classes: dict) -> str: + if left == right: + return left + left_chain: list[str] = [] + current = left + seen: set[str] = set() + while current in classes and current not in seen: + seen.add(current) + left_chain.append(current) + current = classes[current].parent + right_chain: set[str] = set() + current = right + seen = set() + while current in classes and current not in seen: + seen.add(current) + right_chain.add(current) + current = classes[current].parent + for candidate in left_chain: + if candidate in right_chain: + return candidate + return "any" + + +def _module_instance_types(mod: A.Module, classes: dict) -> dict[str, str]: + result: dict[str, str] = {} + for statement in mod.body: + if ( + isinstance(statement, A.Assign) + and isinstance(statement.target, str) + and isinstance(statement.value, A.Call) + and statement.value.func in classes + ): + result[statement.target] = statement.value.func + return result + + +def _set_initializer_element( + classes: dict, + owner_name: str, + field_name: str, + element: str, +) -> None: + owner = classes.get(owner_name) + if owner is None: + return + for method in owner.methods: + if method.name != "__init__": + continue + for statement in method.body: + if ( + isinstance(statement, A.AttrAssign) + and isinstance(statement.obj, A.Name) + and statement.obj.name == "self" + and statement.name == field_name + ): + base, _old_element = _annotation_parts(statement.annot) + if base == "list": + statement.annot = ("list", element) + return + + +def _refine_module_collection_mutations( + mod: A.Module, + result: dict[tuple[str, str], tuple[str, str]], + classes: dict, +) -> None: + """Infer field element types from whole-program ``obj.field.append(value)``.""" + instances = _module_instance_types(mod, classes) + if not instances: + return + for node in _walk_statements(mod.body): + if not ( + isinstance(node, A.MethodCall) + and node.method == "append" + and len(node.args) == 1 + and isinstance(node.obj, A.Attr) + and isinstance(node.obj.obj, A.Name) + ): + continue + receiver_class = instances.get(node.obj.obj.name) + if receiver_class is None: + continue + argument = node.args[0] + element_class = None + if isinstance(argument, A.Call) and argument.func in classes: + element_class = argument.func + elif isinstance(argument, A.Name): + element_class = instances.get(argument.name) + if element_class is None: + continue + key = (receiver_class, node.obj.name) + metadata = result.get(key) + if metadata is None or metadata[0] != "list": + continue + previous = metadata[1] + if previous in ("any", "int", ""): + refined = element_class + else: + refined = _common_class(previous, element_class, classes) + result[key] = ("list", refined) + _set_initializer_element(classes, receiver_class, node.obj.name, refined) + + def _collection_fields(mod: A.Module) -> dict[tuple[str, str], tuple[str, str]]: result: dict[tuple[str, str], tuple[str, str]] = {} classes = {owner.name: owner for owner in mod.classes} @@ -101,7 +243,13 @@ def _collection_fields(mod: A.Module) -> dict[tuple[str, str], tuple[str, str]]: ): continue base, element = _annotation_parts(statement.annot) - if base not in ("list", "dict") or element is None: + if base not in ("list", "dict"): + continue + literal_element = _literal_collection_element(statement.value, base) + if literal_element is not None and element in (None, "any", "int"): + element = literal_element + statement.annot = (base, element) + if element is None: continue if base == "list" and element == "any": refined = _recursive_object_element(owner, statement.name, classes) @@ -111,9 +259,42 @@ def _collection_fields(mod: A.Module) -> dict[tuple[str, str], tuple[str, str]]: element = refined statement.annot = ("list", element) result[(owner.name, statement.name)] = (base, element) + _refine_module_collection_mutations(mod, result, classes) return result +def _refine_collection_method_returns(mod: A.Module, table: dict) -> None: + """Propagate concrete ``dict.get``/field return kinds into method signatures.""" + for owner in mod.classes: + for method in owner.methods: + observed: list[str] = [] + for statement in _walk_statements(method.body): + if not isinstance(statement, A.Return): + continue + value = statement.value + if ( + isinstance(value, A.MethodCall) + and value.method == "get" + and isinstance(value.obj, A.Attr) + and isinstance(value.obj.obj, A.Name) + and value.obj.obj.name == "self" + ): + metadata = table.get((owner.name, value.obj.name)) + if metadata is not None and metadata[0] == "dict": + observed.append(metadata[1]) + elif ( + isinstance(value, A.Attr) + and isinstance(value.obj, A.Name) + and value.obj.name == "self" + ): + metadata = table.get((owner.name, value.name)) + if metadata is not None: + observed.append(metadata[0]) + concrete = {kind for kind in observed if kind not in ("any", "int", "")} + if len(concrete) == 1: + method.ret_type = (next(iter(concrete)), None) + + def _lookup_field(self: SemaAnalyzer, receiver_type: str, field_name: str): table = getattr(self, "_compat_collection_fields", {}) if receiver_type.startswith("instance:"): @@ -129,12 +310,9 @@ def _lookup_field(self: SemaAnalyzer, receiver_type: str, field_name: str): return None if receiver_type == "any": - # Dynamic Python receivers still carry useful structural information. - # When every class declaring this field agrees on its collection shape, - # that shape is safe to propagate without guessing the receiver class. candidates = { value - for (owner_name, candidate_name), value in table.items() + for (_owner_name, candidate_name), value in table.items() if candidate_name == field_name } if len(candidates) == 1: @@ -175,6 +353,7 @@ def _analyze_with_collection_fields(self: SemaAnalyzer) -> None: _mark_dynamic_parameters(self.mod) _annotate_object_fields(self.mod) self._compat_collection_fields = _collection_fields(self.mod) + _refine_collection_method_returns(self.mod, self._compat_collection_fields) _ORIGINAL_ANALYZE(self) diff --git a/asmpython/_compiler/dynamic_value_compat_fixes.py b/asmpython/_compiler/dynamic_value_compat_fixes.py new file mode 100644 index 000000000..1f551165c --- /dev/null +++ b/asmpython/_compiler/dynamic_value_compat_fixes.py @@ -0,0 +1,98 @@ +"""Compatibility for statically recoverable dynamic Python values. + +The whole-program return pass and semantic checker historically disagreed on a +few common dynamic forms. This module aligns return inference for ``dict.get``, +``getattr`` and ``.__name__`` with the later field-flow pass, and folds direct +``str(UserClass)`` calls to a stable class representation before codegen. +""" + +from __future__ import annotations + +from . import analysis_compat_fixes as analysis +from . import ast_nodes as A +from .sema import SemaAnalyzer + + +_ORIGINAL_EXPRESSION_ANNOTATION = analysis._expression_annotation +_ORIGINAL_CHECK_CALL = SemaAnalyzer._check_call + + +def _expression_annotation_with_dynamic_values( + expression, + environment: dict, + owner_name, + function_returns: dict, + method_returns: dict, + class_names: set, + parents: dict, +) -> str: + if isinstance(expression, A.Attr) and expression.name == "__name__": + return "str" + + if isinstance(expression, A.Call) and expression.func == "getattr": + if len(expression.args) >= 3: + return analysis._expression_annotation( + expression.args[2], + environment, + owner_name, + function_returns, + method_returns, + class_names, + parents, + ) + if ( + len(expression.args) >= 2 + and isinstance(expression.args[1], A.StrLit) + and expression.args[1].value == "__name__" + ): + return "str" + + if ( + isinstance(expression, A.MethodCall) + and expression.method == "get" + and len(expression.args) >= 2 + ): + default_type = analysis._expression_annotation( + expression.args[1], + environment, + owner_name, + function_returns, + method_returns, + class_names, + parents, + ) + if default_type not in (analysis._UNKNOWN, analysis._NONE): + return default_type + + return _ORIGINAL_EXPRESSION_ANNOTATION( + expression, + environment, + owner_name, + function_returns, + method_returns, + class_names, + parents, + ) + + +def _check_call_with_static_class_str(self: SemaAnalyzer, expression, scope) -> None: + if ( + isinstance(expression, A.Call) + and expression.func == "str" + and len(expression.args) == 1 + and isinstance(expression.args[0], A.Name) + and expression.args[0].name in self.classes + ): + class_name = expression.args[0].name + expression.args[0] = A.StrLit( + value="", + pos=expression.args[0].pos, + ) + _ORIGINAL_CHECK_CALL(self, expression, scope) + + +analysis._expression_annotation = _expression_annotation_with_dynamic_values + +if not getattr(SemaAnalyzer, "_asmpython_dynamic_value_patch", False): + SemaAnalyzer._check_call = _check_call_with_static_class_str + SemaAnalyzer._asmpython_dynamic_value_patch = True diff --git a/asmpython/_compiler/global_singleton_flow_compat_fixes.py b/asmpython/_compiler/global_singleton_flow_compat_fixes.py new file mode 100644 index 000000000..6368a2344 --- /dev/null +++ b/asmpython/_compiler/global_singleton_flow_compat_fixes.py @@ -0,0 +1,131 @@ +"""Propagate method return types through module-global singleton objects. + +Whole-program projects commonly create a registry or manager once at module +scope and delegate properties/methods through it:: + + REGISTRY = Registry() + + @property + def type_name(self): + return REGISTRY.type_name(self) + +The concrete class of ``REGISTRY`` is statically known. Resolve such delegated +returns before semantic analysis so native callers use the returned value's +actual representation rather than formatting a string pointer as an integer. +""" + +from __future__ import annotations + +from . import ast_nodes as A +from .container_field_compat_fixes import ( + _collection_fields, + _refine_collection_method_returns, +) +from .dynamic_parameter_compat_fixes import _mark_dynamic_parameters +from .field_flow_compat_fixes import _annotate_object_fields +from .language_compat_fixes import _normalize_method_receivers +from .object_flow_compat_fixes import _walk_statements +from .sema import SemaAnalyzer + + +_ORIGINAL_ANALYZE = SemaAnalyzer.analyze + + +def _annotation_name(annotation) -> "str | None": + if isinstance(annotation, str): + return annotation + if isinstance(annotation, tuple) and annotation: + return annotation[0] if isinstance(annotation[0], str) else None + return None + + +def _global_singletons(mod: A.Module) -> dict[str, str]: + class_names = {owner.name for owner in mod.classes} + result: dict[str, str] = {} + for statement in mod.body: + if ( + isinstance(statement, A.Assign) + and isinstance(statement.target, str) + and isinstance(statement.value, A.Call) + and statement.value.func in class_names + ): + result[statement.target] = statement.value.func + return result + + +def _resolve_method_return( + class_name: str, + method_name: str, + classes: dict, +) -> "str | None": + current = class_name + seen: set[str] = set() + while current in classes and current not in seen: + seen.add(current) + owner = classes[current] + for method in owner.methods: + if method.name != method_name: + continue + value = _annotation_name(method.ret_type) + if value is not None: + return value + current = owner.parent + return None + + +def _refine_global_singleton_returns(mod: A.Module) -> None: + classes = {owner.name: owner for owner in mod.classes} + globals_by_name = _global_singletons(mod) + if not globals_by_name: + return + + # Delegation chains can be several calls deep. A short fixed point mirrors + # the compiler's ordinary unannotated-return inference pass. + for _iteration in range(12): + changed = False + for owner in mod.classes: + for method in owner.methods: + observed: set[str] = set() + for statement in _walk_statements(method.body): + if not isinstance(statement, A.Return): + continue + value = statement.value + if not ( + isinstance(value, A.MethodCall) + and isinstance(value.obj, A.Name) + and value.obj.name in globals_by_name + ): + continue + return_type = _resolve_method_return( + globals_by_name[value.obj.name], + value.method, + classes, + ) + if return_type not in (None, "any"): + observed.add(return_type) + if len(observed) != 1: + continue + inferred = next(iter(observed)) + current = _annotation_name(method.ret_type) + if current != inferred: + method.ret_type = (inferred, None) + changed = True + if not changed: + break + + +def _analyze_with_global_singleton_flow(self: SemaAnalyzer) -> None: + # Re-run the idempotent field passes here so their concrete collection + # metadata is available before resolving singleton delegation. + _normalize_method_receivers(self.mod) + _mark_dynamic_parameters(self.mod) + _annotate_object_fields(self.mod) + table = _collection_fields(self.mod) + _refine_collection_method_returns(self.mod, table) + _refine_global_singleton_returns(self.mod) + _ORIGINAL_ANALYZE(self) + + +if not getattr(SemaAnalyzer, "_asmpython_global_singleton_flow_patch", False): + SemaAnalyzer.analyze = _analyze_with_global_singleton_flow + SemaAnalyzer._asmpython_global_singleton_flow_patch = True diff --git a/asmpython/_compiler/static_method_call_compat_fixes.py b/asmpython/_compiler/static_method_call_compat_fixes.py new file mode 100644 index 000000000..50879004f --- /dev/null +++ b/asmpython/_compiler/static_method_call_compat_fixes.py @@ -0,0 +1,158 @@ +"""Lower class-qualified static/class method calls to direct functions. + +The native backend's historical class-qualified call path passes an implicit +class slot even for ``staticmethod`` and does not preserve Python's concrete +``cls`` semantics for inherited ``classmethod`` calls. Whole-program source +already knows the receiver class, so materialize a normal module function and +rewrite the call before semantic analysis. +""" + +from __future__ import annotations + +from dataclasses import fields, is_dataclass + +from . import ast_nodes as A +from . import class_value_compat_fixes as class_values +from . import concrete_specialization_compat_fixes as concrete +from .sema import SemaAnalyzer + + +_ORIGINAL_ANALYZE = SemaAnalyzer.analyze + + +def _sanitize(value: str) -> str: + return "".join( + character if character.isalnum() or character == "_" else "_" + for character in value + ) + + +def _resolve_class_method(class_name: str, method_name: str, class_table: dict): + current_name = class_name + seen = set() + while current_name and current_name not in seen: + seen.add(current_name) + owner = class_table.get(current_name) + if owner is None: + return None + for method in owner.methods: + if method.name == method_name: + return owner, method + current_name = owner.parent + return None + + +def _prepare_clone(method, receiver_class: str, class_table: dict): + cloned = class_values._clone(method) + decorators = list(getattr(cloned, "decorators", []) or []) + if "classmethod" in decorators: + if not cloned.params: + return None + receiver = cloned.params[0] + concrete._replace_identifier(cloned.body, receiver, receiver_class) + cloned.body = concrete._fold_concrete_class_attributes( + cloned.body, receiver_class, class_table + ) + del cloned.params[0] + if cloned.defaults: + del cloned.defaults[0] + if cloned.param_types: + del cloned.param_types[0] + cloned.readonly_params = [ + name for name in cloned.readonly_params if name != receiver + ] + cloned.decorators = [] + return cloned + + +def _lower_class_qualified_calls(mod: A.Module) -> None: + if getattr(mod, "_class_qualified_calls_lowered", False): + return + mod._class_qualified_calls_lowered = True + + class_values._lower_finite_class_values(mod) + class_tuples = class_values._static_class_tuples(mod) + if class_tuples: + concrete._specialize_materialized_classmethods(mod, class_tuples) + + class_table = {owner.name: owner for owner in mod.classes} + generated = {} + pending_functions = [] + + def ensure_function(receiver_class: str, method_name: str): + key = (receiver_class, method_name) + existing = generated.get(key) + if existing is not None: + return existing + resolved = _resolve_class_method(receiver_class, method_name, class_table) + if resolved is None: + return None + _owner, method = resolved + decorators = set(getattr(method, "decorators", []) or []) + if not decorators.intersection({"staticmethod", "classmethod"}): + return None + cloned = _prepare_clone(method, receiver_class, class_table) + if cloned is None: + return None + symbol = ( + "__asmpy_classcall_" + + _sanitize(receiver_class) + + "_" + + _sanitize(method_name) + ) + cloned.name = symbol + generated[key] = symbol + pending_functions.append(cloned) + return symbol + + def rewrite(value): + if value is None or isinstance(value, (str, int, float, bool)): + return value + if isinstance(value, list): + return [rewrite(item) for item in value] + if isinstance(value, tuple): + return tuple(rewrite(item) for item in value) + if isinstance(value, dict): + return {rewrite(key): rewrite(item) for key, item in value.items()} + if isinstance(value, A.MethodCall): + value.obj = rewrite(value.obj) + value.args = [rewrite(argument) for argument in value.args] + value.kwargs = [(name, rewrite(argument)) for name, argument in value.kwargs] + if isinstance(value.obj, A.Name) and value.obj.name in class_table: + symbol = ensure_function(value.obj.name, value.method) + if symbol is not None: + return A.Call( + func=symbol, + args=value.args, + kwargs=value.kwargs, + pos=value.pos, + ) + return value + if is_dataclass(value) and not isinstance(value, type): + for data_field in fields(value): + setattr(value, data_field.name, rewrite(getattr(value, data_field.name))) + return value + + mod.body = rewrite(mod.body) + for function in mod.funcs: + function.body = rewrite(function.body) + for owner in mod.classes: + for method in owner.methods: + method.body = rewrite(method.body) + + index = 0 + while index < len(pending_functions): + function = pending_functions[index] + index += 1 + function.body = rewrite(function.body) + mod.funcs.append(function) + + +def _analyze_with_direct_class_calls(self: SemaAnalyzer) -> None: + _lower_class_qualified_calls(self.mod) + _ORIGINAL_ANALYZE(self) + + +if not getattr(SemaAnalyzer, "_asmpython_direct_class_call_patch", False): + SemaAnalyzer.analyze = _analyze_with_direct_class_calls + SemaAnalyzer._asmpython_direct_class_call_patch = True diff --git a/asmpython/_runtime/abi_exception_shims_linux.asm b/asmpython/_runtime/abi_exception_shims_linux.asm new file mode 100644 index 000000000..4d8a183da --- /dev/null +++ b/asmpython/_runtime/abi_exception_shims_linux.asm @@ -0,0 +1,23 @@ +; SysV exception ABI shims for the built-in x86-64 backend. +; +; The runtime helpers use asmpython's internal rax/rbx convention, while the +; IR backend emits ordinary SysV calls with arguments in rdi/rsi. + +extern _runtime_setjmp +extern _runtime_raise + +global _abi_setjmp +global _abi_raise + +section .text + +; setjmp(buffer=rdi) -> runtime result +_abi_setjmp: + mov rax, rdi + jmp _runtime_setjmp + +; raise(message=rdi, exception_type_id=rsi) -> does not normally return +_abi_raise: + mov rax, rdi + mov rbx, rsi + jmp _runtime_raise diff --git a/asmpython/_runtime/abi_shims_linux_bundle.asm b/asmpython/_runtime/abi_shims_linux_bundle.asm index e4d853686..3dfd305bd 100644 --- a/asmpython/_runtime/abi_shims_linux_bundle.asm +++ b/asmpython/_runtime/abi_shims_linux_bundle.asm @@ -7,6 +7,7 @@ ; avoids baking a target choice into the shared IR. %include "abi_shims_linux.asm" +%include "abi_exception_shims_linux.asm" extern dlopen extern dlsym diff --git a/provider-runtime-inspection.txt b/provider-runtime-inspection.txt new file mode 100644 index 000000000..a2f5438a5 --- /dev/null +++ b/provider-runtime-inspection.txt @@ -0,0 +1,1882 @@ +=== resolve param types === +3360- if len(group) < 2: +3361- continue +3362- if not all("overload" in getattr(g, "decorators", []) for g in group): +3363- continue +3364- sigs: list = [] +3365- for g in group: +3366- r = self._resolve_annot(g.ret_type) +3367- sigs.append(FuncSig( +3368- name=g.name, +3369- arity=len(g.params), +3370- n_defaults=_count_defaults(g.defaults), +3371- pos=g.pos, +3372- ret_type=(r[0], r[1], r[2]) if r is not None else None, +3373- param_names=list(g.params), +3374- param_defaults=list(g.defaults), +3375: param_types=self._resolve_param_types(g), +3376- vararg=g.vararg, +3377- kwarg=g.kwarg, +3378- decorators=list(getattr(g, "decorators", [])), +3379- )) +3380- self._check_overload_group_distinct(name, sigs, group[0].pos) +3381- self.overload_sets[name] = sigs +3382- # Rename each real FuncDef to its mangled symbol IN PLACE -- +3383- # both codegen backends compile a function under its own +3384- # `.name` attribute directly, so this is what actually +3385- # makes each overload land at a distinct symbol. Sema-side +3386- # dispatch (above) reads the mangled name back via +3387- # _overload_symbol(name, sig) applied to the ORIGINAL name + +3388- # the matched FuncSig, so it must produce the identical +3389- # string this rename uses -- both derive it from the same +3390- # (original name, sig) pair via the one shared helper. +3391- for g, sig in zip(group, sigs): +3392- g.name = _overload_symbol(name, sig) +3393- _overload_funcdef_ids.add(id(g)) +3394- +3395- # First pass: collect function signatures so forward references resolve. +3396- for f in self.mod.funcs: +3397- if id(f) in _overload_funcdef_ids: +3398- # Handled by the pre-pass above -- register only the FIRST +3399- # occurrence into self.funcs (so plain, non-dispatch-aware +3400- # code paths that read self.funcs[name] directly, e.g. +3401- # simple existence checks, still find *something* real; the +3402- # actual multi-signature dispatch reads overload_sets +3403- # instead, never this single entry, for these names) and +3404- # skip the ordinary redefinition guard for every copy. +3405- if f.name not in self.funcs: +3406- r = self._resolve_annot(f.ret_type) +3407- _raw_ret_base = f.ret_type[0] if f.ret_type else None +3408- self.funcs[f.name] = FuncSig( +3409- name=f.name, +3410- arity=len(f.params), +3411- n_defaults=_count_defaults(f.defaults), +3412- pos=f.pos, +3413- ret_type=(r[0], r[1], r[2]) if r is not None else None, +3414- ret_list_tuple_types=(r[3] if r is not None and r[1] == "tuple" else None), +3415- ret_inner_el_type=(r[4] if r is not None and r[1] in ("list", "dict") else None), +3416- param_names=list(f.params), +3417- param_defaults=list(f.defaults), +3418: param_types=self._resolve_param_types(f), +3419- vararg=f.vararg, +3420- kwarg=f.kwarg, +3421- ret_tuple=(r[3] if r is not None and r[0] == "tuple" else None), +3422- ret_bool=(_raw_ret_base == "bool"), +3423- decorators=list(getattr(f, "decorators", [])), +3424- ) +3425- continue +3426- if f.name in self.funcs: +3427- if getattr(f, "is_lifted", False): +3428- # A nested `def` is lifted to a module-level function keyed +3429- # by its bare name (see parser.py's nested-def handling and +3430- # A.ClosureBind) -- valid Python allows the same nested-def +3431- # name in unrelated enclosing functions/methods (e.g. two +3432- # methods each defining their own `def _do(): ...`), but +3433- # this flat lifting scheme can't yet keep them apart. +3434- raise SemaError( +3435- f"nested function {f.name!r} collides with another " +3436- f"nested/module function of the same name after being " +3437- "lifted to module scope -- give it a distinct name " +3438- "(asmpython does not yet mangle nested-function names " +3439- "by enclosing scope)", +3440- f.pos, +3441- ErrorCode.E_REDEFINED_FUNC, +3442- ) +3443- raise SemaError(f"function {f.name!r} redefined", f.pos, ErrorCode.E_REDEFINED_FUNC) +3444- if f.name in BUILTINS and not getattr(f, "is_stdlib", False): +3445- raise SemaError( +3446- f"cannot redefine builtin {f.name!r}", +3447- f.pos, +3448- ErrorCode.E_BUILTIN_REDEFINED, +3449- ) +3450- r = self._resolve_annot(f.ret_type) # type: ignore +3451- _raw_ret_base = f.ret_type[0] if f.ret_type else None +3452- self.funcs[f.name] = FuncSig( +3453- name=f.name, +3454- arity=len(f.params), +3455- n_defaults=_count_defaults(f.defaults), +3456- pos=f.pos, +3457- ret_type=(r[0], r[1], r[2]) if r is not None else None, +3458- ret_list_tuple_types=(r[3] if r is not None and r[1] == "tuple" else None), +3459- ret_inner_el_type=(r[4] if r is not None and r[1] in ("list", "dict") else None), +3460- param_names=list(f.params), +3461- param_defaults=list(f.defaults), +3462: param_types=self._resolve_param_types(f), +3463- vararg=f.vararg, +3464- kwarg=f.kwarg, +3465- ret_tuple=(r[3] if r is not None and r[0] == "tuple" else None), +3466- ret_bool=(_raw_ret_base == "bool"), +3467- decorators=list(getattr(f, "decorators", [])), +3468- ) +3469- +3470- # A lightweight top-level prepass for tuple-return inference. This +3471- # lets `_scan_tuple_return` recognize module constants like +3472- # `MODE_REG` as ints before the full function-body pass. +3473- # +3474- # Deliberately NOT calling `_check_stmt`/`_check_expr` here (an +3475- # earlier version of this prepass did): those mutate shared AST +3476- # nodes in place as a side effect of real checking (`_bind_args` +3477- # rewrites a Call's `.args` list to fill in defaults/pack +3478- # `**kwargs`, `_clone_default_expr` allocates fresh nodes, etc.), +3479- # and this prepass runs over `self.mod.body` a second time before +3480- # the real `_try_check_block(self.mod.body, ...)` pass below -- +3481- # so any call node with side-effecting args (e.g. a function +3482- # taking `**kwargs`) got double-expanded, corrupting its arg count +3483- # and raising a bogus "takes N argument(s), got N+1" error the +3484- # second (real) time it was checked. This only needs a syntax-only, +3485- # non-mutating read of simple module-level constant assignments, so +3486- # `_literal_arg_type` (already used for the same purpose elsewhere) +3487- # is enough -- it never touches call sites at all. +3488- self._tuple_scan_globals = Scope() +3489- self._tuple_scan_globals.add("__name__", "str") +3490- self._tuple_scan_globals.add("__file__", "str") +3491- self._tuple_scan_globals.add("__builtins__", "any") +3492- for stmt in self.mod.body: +3493- if not isinstance(stmt, A.Assign): +3494- continue +3495- lit = self._literal_arg_type(stmt.value) +3496- if lit is None: +3497- continue +3498- ty, el, val, tup = lit +3499- self._tuple_scan_globals.add( +3500- stmt.target, ty, el_type=el, value_type=val, tuple_types=tup +3501- ) +3502- +3503- # Infer which functions return a tuple, and the shape of that tuple, +3504- # so call sites can unpack `q, r = f()`. Done before body analysis so +3505- # forward references and recursion still see the inferred shape. +3506- for f in self.mod.funcs: +3507- f_body_str: list = f.body +3508- sig = self.funcs.get(f.name) +3509- ets = list(sig.ret_tuple) if sig is not None and sig.ret_tuple else self._scan_tuple_return(f_body_str) +3510- if ets is not None: +3511- self.func_ret_tuple[f.name] = ets +3512- +-- +3688- if len(mgroup) < 2: +3689- continue +3690- if not all("overload" in getattr(g, "decorators", []) for g in mgroup): +3691- continue +3692- msigs: list = [] +3693- for g in mgroup: +3694- mr0 = self._resolve_annot(g.ret_type) +3695- msigs.append(FuncSig( +3696- name=g.name, +3697- arity=len(g.params), +3698- n_defaults=_count_defaults(g.defaults), +3699- pos=g.pos, +3700- ret_type=(mr0[0], mr0[1], mr0[2]) if mr0 is not None else None, +3701- param_names=list(g.params), +3702- param_defaults=list(g.defaults), +3703: param_types=self._resolve_param_types(g), +3704- vararg=g.vararg, +3705- kwarg=g.kwarg, +3706- decorators=list(getattr(g, "decorators", [])), +3707- )) +3708- self._check_overload_group_distinct(f"{c.name}.{mname}", msigs, mgroup[0].pos) +3709- self.method_overload_sets[(c.name, mname)] = msigs +3710- for g, msig in zip(mgroup, msigs): +3711- g.name = _overload_symbol(mname, msig) +3712- for m in c.methods: +3713- deco: list[str] = getattr(m, "decorators", []) +3714- is_static = "staticmethod" in deco +3715- is_classm = "classmethod" in deco +3716- if ("private" in deco or "protected" in deco) and not self._ext_active("access"): +3717- raise SemaError( +3718- f"@{'private' if 'private' in deco else 'protected'} on " +3719- f"{c.name}.{m.name} is not supported -- asmpython's " +3720- f"compiler-extension system was withdrawn (see " +3721- f"archived/extensions/)", +3722- m.pos, +3723- ErrorCode.E_DECORATOR_WITHOUT_EXTENSION, +3724- ) +3725- if "final" in deco and not self._ext_active("final"): +3726- raise SemaError( +3727- f"@final on {c.name}.{m.name} is not supported -- " +3728- f"asmpython's compiler-extension system was " +3729- f"withdrawn (see archived/extensions/)", +3730- m.pos, +3731- ErrorCode.E_DECORATOR_WITHOUT_EXTENSION, +3732- ) +3733- if "private" in deco: +3734- sig.access[m.name] = "private" +3735- elif "protected" in deco: +3736- sig.access[m.name] = "protected" +3737- if "final" in deco: +3738- sig.final_methods.add(m.name) +3739- if not (is_static or is_classm): +3740- if not m.params or m.params[0] != "self": +3741- raise SemaError( +3742- f"method {c.name}.{m.name!r} must take 'self' as its first parameter", +3743- m.pos, +3744- ErrorCode.E_MISSING_SELF_PARAM, +3745- ) +3746- mr = self._resolve_annot(m.ret_type) # type: ignore +3747- # `@x.setter` methods are registered under a mangled name +3748- # ("x__setter") so they don't collide in `methods`/codegen +3749- # symbols with the `@property` getter of the same name "x". +3750- # `ClassSig.setters` maps the property name to this mangled +3751- # name so `obj.x = v` can be rewritten to dispatch to it. +3752- setter_prop = None +3753- for d in getattr(m, "decorators", []): +-- +4840- s_body: list = s.body +4841- self._collect_returns(s_body, acc) +4842- elif isinstance(s, A.Try): +4843- st_body: list = s.body +4844- st_handler: list = s.handler +4845- self._collect_returns(st_body, acc) +4846- self._collect_returns(st_handler, acc) +4847- st_extra: list = s.extra_handlers +4848- for _types, _bind, hbody in st_extra: +4849- self._collect_returns(hbody, acc) +4850- st_else: list = s.else_body +4851- st_finally: list = s.finally_body +4852- self._collect_returns(st_else, acc) +4853- self._collect_returns(st_finally, acc) +4854- +4855: def _resolve_param_types(self, f) -> list: +4856- """`overload` extension: resolve each parameter's static type from +4857- its annotation, `"any"` when unannotated (dispatch simply treats +4858- an unannotated param as matching anything -- no attempt to reuse +4859- the default/usage-hint inference machinery here, since that's +4860- keyed to a single concrete function body, not a group of +4861- candidate signatures sharing a name).""" +4862- param_types: list = [] +4863- f_param_types: list = getattr(f, "param_types", []) or [] +4864- for i in range(len(f.params)): +4865- annot = f_param_types[i] if i < len(f_param_types) else None +4866- resolved = self._resolve_annot(annot) +4867- param_types.append(resolved[0] if resolved is not None else "any") +4868- return param_types +4869- +4870- def _check_overload_group_distinct(self, name: str, sigs: list, pos) -> None: +4871- """`overload` extension: reject a group of @overload signatures +4872- that are indistinguishable from each other (same arity AND same +4873- param_types) -- dispatch could never pick between them.""" +4874- seen: set = set() +4875- for sig in sigs: +4876- key = (sig.arity, tuple(sig.param_types)) +4877- if key in seen: +4878- raise SemaError( +4879- f"two @overload signatures for {name!r} are " +4880- f"indistinguishable (same parameter count and types)", +4881- pos, +4882- ErrorCode.E_OVERLOAD_INCOMPATIBLE, +4883- ) +4884- seen.add(key) +4885- +4886- def _resolve_overload(self, name: str, sigs: list, args: list, pos, implicit_self: bool = False): +4887- """`overload` extension: pick the best-matching FuncSig from `sigs` +4888- for a call site's `args`. Filters by arity match first (accounting +4889- for defaults, same as ordinary single-signature arity checking), +4890- then scores the arity-matching candidates by how many parameters +4891- have an EXACT static-type match against the call site's argument +4892- types (an unannotated "any" parameter matches anything but scores +4893- lower than a real match) -- the highest-scoring candidate wins; +4894- a tie or zero arity-matching candidates is an error. Deliberately +4895- simpler than full overload resolution (no covariance/promotion +4896- rules) -- a documented v1 simplification, not every C++/Java +4897- overload-resolution edge case. +4898- +4899- `implicit_self`: True for a method-form overload call, where +4900- `sig.arity`/`sig.param_types` include the `self` receiver +4901- (index 0) but the call-site `args` never do -- every comparison +4902- below is offset by 1 to account for that. +4903- """ +4904- offset = 1 if implicit_self else 0 +4905- n = len(args) +351- # `for row in rows: row[i]` recovers the leaf type. +352- ret_inner_el_type: object = None +353- # Parameter names and their default expressions (parallel to params, +354- # including `self` for methods). Used to bind keyword arguments onto +355- # positions at call sites. +356- param_names: list = field(default_factory=list) +357- param_defaults: list = field(default_factory=list) +358- # `overload` extension: resolved per-parameter static types, parallel to +359- # `param_names` ("int"/"str"/"float"/"any" per slot, "any" for an +360- # unannotated/uninferrable parameter). Populated at registration time +361- # from the same annotation resolution every other FuncSig field already +362- # uses -- previously computed and discarded, never stored, since +363- # ordinary (non-overloaded) call resolution only ever needed arity, not +364- # per-parameter types. Needed here because overload dispatch has to +365- # pick the best-matching signature by argument type, not just count. +366: param_types: list = field(default_factory=list) +367- # Name of the `*args` parameter (the trailing list slot), or None. +368- vararg: Optional[str] = None +369- # Name of the `**kwargs` parameter (the trailing dict slot), or None. +370- kwarg: Optional[str] = None +371- # Per-slot kinds when the body returns a tuple (`return a, b`), so a call +372- # site can unpack `x, y = obj.m()`. None when it doesn't return a tuple. +373- ret_tuple: object = None +374- # Decorator identities for methods (["staticmethod"] / ["classmethod"]). +375- decorators: list = field(default_factory=list) +376- # True when every reachable `return` in the body is a bare `return self` +377- # (and at least one exists), and the method has no explicit return-type +378- # annotation. Lets call sites of e.g. `__enter__` (which conventionally +379- # `return self`) infer `instance:` instead of defaulting to +380- # `int`. Mirrors `ret_tuple`'s body-scanning approach. +381- returns_self: bool = False +382- # True when the function has an explicit `-> bool` annotation (so call +383- # sites can render the return value as True/False in print/str/f-string). +384- ret_bool: bool = False +385- +386- +387-@dataclass +388-class ClassSig: +389- """Compile-time information about a class. +390- +391- `methods` maps method name -> FuncSig (where arity counts `self`). +392- Resolution walks `parent` chains until a method is found. +393- """ +394- +395- name: str +396- parent: Optional[str] +397- methods: dict[str, FuncSig] = field(default_factory=dict) +398- pos: A.SourcePos = None # type: ignore +399- # Field name -> static type ("int"/"str"/"float"/"list"/"dict"/"tuple"/ +400- # "instance:"), inferred from `self.x = ` assignments and +401- # `self.x: T` annotations. Drives the type of `obj.x` reads. Unknown fields +-- +525- +526- +527-# `overload` extension: single canonical symbol-mangling scheme, called +528-# from every resolved-overload-call site (sema, and both codegen backends' +529-# call-emission, which read A.Call.resolved_overload_symbol back rather +530-# than re-deriving it) and from the function-definition-emission side +531-# (codegen/ir_lower, for the actual compiled symbol each @overload def +532-# gets). Suffix is arity plus a short type tag per parameter (i=int, +533-# f=float, s=str, a=any) -- enough to disambiguate same-arity overloads +534-# differing only by parameter type, without a full serialized-signature +535-# mangling scheme this wave's 6 real configs don't need. +536-_OVERLOAD_TYPE_TAG = {"int": "i", "float": "f", "str": "s"} +537- +538- +539-def _overload_symbol(name: str, sig) -> str: +540: tags = "".join(_OVERLOAD_TYPE_TAG.get(t, "a") for t in sig.param_types) +541- return f"{name}__ov{sig.arity}{tags}" +542- +543- +544-def _syntactic_reachable_names(mod: A.Module) -> "tuple[set, set]": +545- """Pre-sema call-graph walk: which top-level functions and class methods +546- are reachable from the module's real entry point (`mod.body`)? +547- +548- Deliberately cheaper and less precise than `ir_lower.py`'s +549- `_reachable_callables` -- that one runs AFTER sema and can key off +550- sema-populated fields (`A.expr_type()`'s `.inferred_type`, +551- `resolved_overload_symbol`, `dunder_call_owner`, etc.) to resolve exactly +552- which class a method call dispatches to. This walker runs BEFORE sema +553- (it has to: it's used to decide whether sema may safely skip a broken +554- body), so it only has the plain-string AST fields the parser already +555- populated: `A.Call.func`, `A.MethodCall.method`, bare `A.Name` refs. It +556- can't tell which class a `MethodCall.method` call targets, so it +557- conservatively marks EVERY class method matching that bare name reachable +558- across all classes -- an over-approximation that only ever marks too +559- much, never too little, which is the safe direction for this walker's +560- one job (deciding what's safe to skip if it errors). +561- """ +562- method_defs: dict = {} +563- methods_by_name: dict = {} +564- for cls in mod.classes: +565- for m in cls.methods: +566- method_defs[(cls.name, m.name)] = m +567- methods_by_name.setdefault(m.name, []).append(cls.name) +568- func_defs = {f.name: f for f in mod.funcs} +569- +570- needed_funcs: set = set() +571- needed_methods: set = set() +572- func_queue: list = [] +573- method_queue: list = [] +574- +575- def add_func(name) -> None: +-- +790- # (mlang_objects), consumed by driver.py's link step. Populated by +791- # _inject_mlang_if_needed, consulted by A.MethodCall's `mlang:` +792- # dispatch above. +793- self.mlang_code_funcs: dict[str, dict] = {} +794- self.mlang_objects: "list[tuple[bytes, str]]" = [] +795- self._tuple_scan_globals: Scope = Scope() +796- # name -> per-slot element kinds for functions that return a tuple +797- # (i.e. have a `return a, b` somewhere). Lets `q, r = f()` recover +798- # the per-target types at the call site. Computed in analyze(). +799- self.func_ret_tuple: dict[str, list[str]] = {} +800- # (qualified_name, param_index) -> (ty, el, val, tup) for parameters +801- # with no annotation and no default, inferred from literal-typed +802- # arguments at call sites. `qualified_name` is a function's plain name, +803- # or "ClassName.method_name" for a method. See +804- # `_infer_unannotated_params`. +805: self.inferred_param_types: dict[str, tuple] = {} +806- # func_name -> list of (ty, el_type, val_type) for each free variable, +807- # populated by _prescan_fv_types() before the main analysis loops. +808- self._fv_types: dict = {} +809- +810- def _ensure_synthetic_func(self, fdef: A.FuncDef, ret_ty: str = "int") -> None: +811- if fdef.name not in self.funcs: +812- self.funcs[fdef.name] = FuncSig( +813- name=fdef.name, +814- arity=len(fdef.params), +815- n_defaults=0, +816- pos=fdef.pos, +817- ret_type=(ret_ty, None, None), +818- param_names=list(fdef.params), +819- param_defaults=[None] * len(fdef.params), +820- ) +821- if not any(f.name == fdef.name for f in self.mod.funcs): +822- self.mod.funcs.append(fdef) +823- +824- def _ensure_builtin_value_func(self, name: str, pos: A.SourcePos) -> str: +825- fname = f"__builtin_value_{name}" +826- if fname in self.funcs: +827- return fname +828- a_name = A.Name(name="a", pos=pos) +829- b_name = A.Name(name="b", pos=pos) +830- cmp = A.Compare(ops=["<" if name == "min" else ">"], operands=[a_name, b_name], pos=pos) +831- body = A.IfExp(test=cmp, body=a_name, orelse=b_name, pos=pos) +832- fdef = A.FuncDef( +833- name=fname, +834- params=["a", "b"], +835- body=[A.Return(value=body, pos=pos)], +836- pos=pos, +837: param_types=[("int", None), ("int", None)], +838- ret_type=("int", None), +839- ) +840- self._ensure_synthetic_func(fdef, "int") +841- return fname +842- +843- def _resolve_class_chain(self, name: str) -> list: +844- """[name, parent, grandparent, ...] for a user-defined class.""" +845- out: list[str] = [] +846- cur = name +847- while cur is not None and cur not in out: +848- out.append(cur) +849- cls: ClassSig = self.classes.get(cur) +850- cur = cls.parent if cls is not None else None +851- return out +852- +853- def _common_class_ancestor(self, a: str, b: str) -> "str | None": +854- """Nearest common ancestor of two user-defined classes, e.g. for +855- `WindowsCodegen` and `Freestanding16Codegen` both descending from +856- `Codegen`. Returns None if they share no modeled ancestor (siblings +857- with only an external/unmodeled base, or unrelated classes).""" +858- chain_a = self._resolve_class_chain(a) +859- chain_b = set(self._resolve_class_chain(b)) +860- for cls_name in chain_a: +861- if cls_name in chain_b: +862- return cls_name +863- return None +864- +865- def _has_external_base(self, class_name: str) -> bool: +866- """True if `class_name` or any ancestor inherits from a base that isn't +867- a user-defined class (a builtin like Exception, or a name imported from +868- another module). Such a base may supply methods/fields asmpython can't +869- see, so member access against it is checked leniently.""" +870- cur = class_name +871- seen: set = set() +872- while cur is not None and cur not in seen: +-- +1305- if el in ("list", "dict") and elval is not None: +1306- sig.field_inner_value_types[cname] = elval +1307- elif el == "tuple" and tup: +1308- sig.field_value_tuple_types[cname] = tup +1309- elif ty == "dict" and val is not None: +1310- sig.field_el_types[cname] = val +1311- if val in ("list", "dict") and elval is not None: +1312- sig.field_inner_value_types[cname] = elval +1313- elif val == "tuple" and tup: +1314- sig.field_value_tuple_types[cname] = tup +1315- for m in c.methods: +1316- # Each param maps to its resolved annotation tuple +1317- # (ty, el, val, tuple) so a `self.x = param` assignment can carry +1318- # the param's element/value kinds onto the field. +1319- pinfo: dict = {} +1320: m_param_types_x: list = m.param_types +1321- m_defaults_x: list = m.defaults +1322- # Explicit `: list` intermediates: m is opaque to sema (external +1323- # FuncDef), so m.params / m.body read back as opaque "any". +1324- # Without the cast, enumerate(m.params) / _scan_field_assigns(m.body) +1325- # use the wrong codegen path (int/dict ops instead of list ops). +1326- m_params_cf: list = m.params +1327- m_body_cf: list = m.body +1328- for i, p in enumerate(m_params_cf): +1329- if i == 0: +1330- continue # self +1331: annot = m_param_types_x[i] if i < len(m_param_types_x) else None +1332- r = self._resolve_annot(annot) # type: ignore +1333- if r is not None: +1334- pinfo[p] = r +1335- elif i < len(m_defaults_x) and m_defaults_x[i] is not None: +1336- # A `=None` default carries no real type of its own +1337- # (its literal is IntLit(0, is_none=True)) -- same +1338- # fix as codegen's param-type setup: "any" instead +1339- # of trusting expr_type's "int", so a later +1340- # `self.x = param` doesn't mistype `self.x` as int +1341- # for an Optional[X]-style parameter. +1342- dty = "any" if A.is_none_expr(m_defaults_x[i]) else A.expr_type(m_defaults_x[i]) # type: ignore +1343- pinfo[p] = (dty, None, None, None, None) +1344- else: +1345: inferred = self.inferred_param_types.get(f"{c.name}.{m.name}:{i}") +1346- if inferred is not None: +1347- pinfo[p] = inferred +1348- else: +1349- # A field assigned from an unannotated parameter +1350- # with no literal call-site signal is genuinely +1351- # dynamic. Keep it opaque rather than pinning the +1352- # field to the numeric "int" fallback, so later +1353- # `self.field.method()` / `self.field[...]` uses +1354- # remain lenient. +1355- pinfo[p] = ("any", None, None, None, None) +1356- self._scan_field_assigns(m_body_cf, sig, pinfo) +1357- +1358- def _scan_field_assigns(self, stmts: list, sig: ClassSig, pinfo: dict) -> None: +1359- for s in stmts: +1360- if ( +1361- isinstance(s, A.AttrAssign) +1362- and isinstance(s.obj, A.Name) +1363- and s.obj.name == "self" +1364- ): +1365- # An explicit declaration annotation (`self.x: T = ...`) wins — +1366- # it carries element/value kinds the initializer (often `{}`/`[]`) +1367- # can't. Otherwise fall back to the value's static type. +1368- r = self._resolve_annot(getattr(s, "annot", None)) # type: ignore +1369- if r is not None: +1370- # Same fix as elsewhere: subscript reads with an +1371- # explicit `ty: str`, not a tuple-unpack. +1372- ty: str = r[0] +1373- el = r[1] +1374- val = r[2] +1375- tup = r[3] +1376- elval = r[4] +1377- else: +1378- raw = self._static_value_info(s.value, pinfo) +1379- ty, el, val, tup = raw[0], raw[1], raw[2], raw[3] +1380- elval = None +-- +1699- # same-named method with conflicting argument types, the +1700- # mismatch just falls back to `int` as before -- no new +1701- # miscompile. +1702- sites = [ +1703- mc for mc in calls +1704- if isinstance(mc, A.MethodCall) and mc.method == m.name +1705- ] +1706- self._infer_call_target_params(f"{c.name}.{m.name}", m, sites, start=1) +1707- +1708- def _infer_call_target_params( +1709- self, qualname: str, fn: A.FuncDef, sites: list, start: int +1710- ) -> None: +1711- """Infer types for `fn`'s parameters at index >= `start` (0 for plain +1712- functions, 1 for methods to skip `self`) from `sites` (the `A.Call`/ +1713- `A.MethodCall` nodes invoking it), storing results in +1714: `self.inferred_param_types`. See `_infer_unannotated_params`.""" +1715: fn_param_types: list = fn.param_types +1716- fn_defaults: list = fn.defaults +1717- for i, p in enumerate(fn.params): +1718- if i < start: +1719- continue +1720: annot = fn_param_types[i] if i < len(fn_param_types) else None +1721- if self._resolve_annot(annot) is not None: # type: ignore +1722- continue +1723- if i < len(fn_defaults) and fn_defaults[i] is not None: +1724- continue +1725- candidates: list = [] +1726- found_any = False +1727- arg_idx = i - start +1728- for site in sites: +1729- args: list = site.args +1730- if arg_idx < len(args): +1731- arg = args[arg_idx] +1732- else: +1733- # Explicit loop, not next((v for n,v in ... if n==p), None): +1734- # asmpython's own codegen has no generator-expression/next() +1735- # support (this is the compiler's own source, self-compiled), +1736- # so that construct silently fell through to an unrelated +1737- # fallback and corrupted state instead of erroring. +1738- site_kwargs: list = site.kwargs +1739- arg = None +1740- for kw_name, kw_val in site_kwargs: +1741- if kw_name == p: +1742- arg = kw_val +1743- break +1744- if arg is None: +1745- continue +1746- lit = self._literal_arg_type(arg) +1747- if lit is None: +1748- continue +1749- found_any = True +1750- if lit not in candidates: +1751- candidates.append(lit) +1752- if found_any and len(candidates) == 1: +1753: self.inferred_param_types[f"{qualname}:{i}"] = candidates[0] +=== type method semantic branch === +8477- e.tuple_elem_types = self._dict_value_tuple_types(e.obj, scope) +8478- elif e.inferred_type == "dict": +8479- e.value_type = self._dict_inner_value_type(e.obj, scope) +8480- elif e.inferred_type == "tuple": +8481- e.tuple_elem_types = self._dict_value_tuple_types(e.obj, scope) +8482- else: +8483- raise SemaError(f"dict has no method {e.method!r}", e.pos, ErrorCode.E_NO_METHOD) +8484- elif obj_t == "str": +8485- self._check_str_method(e, scope) +8486- return +8487- elif obj_t == "int": +8488- if e.method == "to_bytes": +8489- if not (1 <= len(e.args) <= 3): +8490- raise SemaError("int.to_bytes() takes length[, byteorder[, signed]]", e.pos, ErrorCode.E_ARG_COUNT) +8491- if A.expr_type(e.args[0]) not in ("int", "any"): +8492- raise SemaError("int.to_bytes() length must be an int", e.pos, ErrorCode.E_ARG_TYPE) +8493- if len(e.args) >= 2 and A.expr_type(e.args[1]) not in ("str", "any"): +8494- raise SemaError("int.to_bytes() byteorder must be a str", e.pos, ErrorCode.E_ARG_TYPE) +8495- if len(e.args) >= 3 and A.expr_type(e.args[2]) not in ("int", "any"): +8496- raise SemaError("int.to_bytes() signed must be bool/int", e.pos, ErrorCode.E_ARG_TYPE) +8497- e.inferred_type = "list" +8498- e.list_el_type = "int" +8499- return +8500- raise SemaError(f"int has no method {e.method!r}", e.pos, ErrorCode.E_NO_METHOD) +8501- elif ( +8502: obj_t == "type" +8503- or (isinstance(e.obj, A.Name) and e.obj.name == "int") +8504- ): +8505- if isinstance(e.obj, A.Name) and e.obj.name in self.classes: +8506- cls_name = e.obj.name +8507- resolved = self._resolve_method(cls_name, e.method) +8508- if resolved is None: +8509- raise SemaError(f"{cls_name} has no method {e.method!r}", e.pos, ErrorCode.E_NO_METHOD) +8510- sig: FuncSig = resolved[1] +8511- deco: list[str] = getattr(sig, "decorators", []) +8512- if "classmethod" in deco: +8513- expected = sig.arity - 1 # drop implicit cls +8514- elif "staticmethod" in deco: +8515- expected = sig.arity +8516- else: +8517- raise SemaError( +8518- f"{cls_name}.{e.method}() needs an instance " +8519- "(not a @staticmethod or @classmethod)", +8520- e.pos, +8521- ErrorCode.E_METHOD_NEEDS_INSTANCE, +8522- ) +8523- required = expected - sig.n_defaults +8524- if not (required <= len(e.args) <= expected): +8525- raise SemaError( +8526- f"{cls_name}.{e.method}() takes {required}..{expected} " +8527- f"argument(s), got {len(e.args)}", +8528- e.pos, +8529- ErrorCode.E_ARG_COUNT, +8530- ) +8531- if sig.ret_type is not None: +8532- rt7: tuple = sig.ret_type # type: ignore +8533- ty: str = rt7[0] +8534- el = rt7[1] +8535- e.inferred_type = ty +8536- if ty == "list" and el is not None: +8537- e.list_el_type = el +8538- else: +8539- e.inferred_type = "int" +8540- return +8541- if e.method == "from_bytes": +8542- if not (1 <= len(e.args) <= 3): +8543- raise SemaError("int.from_bytes() takes bytes[, byteorder[, signed]]", e.pos, ErrorCode.E_ARG_COUNT) +8544- if A.expr_type(e.args[0]) not in ("list", "any"): +8545- raise SemaError("int.from_bytes() first argument must be bytes/list[int]", e.pos, ErrorCode.E_ARG_TYPE) +8546- if len(e.args) >= 2 and A.expr_type(e.args[1]) not in ("str", "any"): +8547- raise SemaError("int.from_bytes() byteorder must be a str", e.pos, ErrorCode.E_ARG_TYPE) +8548- if len(e.args) >= 3 and A.expr_type(e.args[2]) not in ("int", "any"): +8549- raise SemaError("int.from_bytes() signed must be bool/int", e.pos, ErrorCode.E_ARG_TYPE) +8550- e.inferred_type = "int" +8551- return +8552- raise SemaError(f"{obj_t} has no method {e.method!r}", e.pos, ErrorCode.E_NO_METHOD) +8553- elif obj_t.startswith("super:"): +8554- # super().method(...) — dispatch against the base class. If the +8555- # base is external (e.g. Exception), we can't model it, so the +8556- # call is lenient. +8557- parent = obj_t.split(":", 1)[1] +8558- if parent not in self.classes: +8559- e.inferred_type = "any" +8560- return +8561- resolved = self._resolve_method(parent, e.method) +8562- if resolved is None: +8563- if self._has_external_base(parent): +8564- e.inferred_type = "any" +8565- return +8566- raise SemaError(f"{parent} has no method {e.method!r}", e.pos, ErrorCode.E_NO_METHOD) +8567- sig: FuncSig = resolved[1] +8568- expected = sig.arity - 1 +8569- required = expected - sig.n_defaults +8570- if not (required <= len(e.args) <= expected): +8571- raise SemaError( +8572- f"super().{e.method}() takes {required}..{expected} " +8573- f"argument(s), got {len(e.args)}", +8574- e.pos, +8575- ErrorCode.E_ARG_COUNT, +8576- ) +8577- if sig.ret_type is not None: +8578- rt5: tuple = sig.ret_type # type: ignore +8579- ty: str = rt5[0] +8580- el = rt5[1] +8581- e.inferred_type = ty +8582- if ty == "list" and el is not None: +8583- e.list_el_type = el +8584- if el in ("list", "dict") and sig.ret_inner_el_type: +8585- e.el_value_type = sig.ret_inner_el_type +8586- else: +8587- e.inferred_type = "int" +8588- return +8589- elif obj_t.startswith("instance:"): +8590- class_name = obj_t.split(":", 1)[1] +8591- ov_key = (class_name, e.method) +8592- if ov_key in self.method_overload_sets: +8593- for a in e.args: +8594- self._check_expr(a, scope) +8595- ov_sig = self._resolve_overload( +8596- f"{class_name}.{e.method}", +8597- self.method_overload_sets[ov_key], +8598- e.args, +8599- e.pos, +8600- implicit_self=True, +8601- ) +8602- e.resolved_overload_symbol = _overload_symbol(e.method, ov_sig) +=== str builtin backend lowering === +asmpython/_compiler/sema.py-9916- e.tuple_elem_types = ["int", "int"] +asmpython/_compiler/sema.py-9917- return +asmpython/_compiler/sema.py-9918- # Argument-type sanity for builtins that care. An opaque ("any") +asmpython/_compiler/sema.py-9919- # argument is accepted everywhere — we can't know its real type. +asmpython/_compiler/sema.py-9920- if e.func == "len": +asmpython/_compiler/sema.py-9921- t = A.expr_type(e.args[0]) +asmpython/_compiler/sema.py-9922- if t not in ("str", "list", "dict", "tuple", "set", "any", "int") and not t.startswith("instance:"): +asmpython/_compiler/sema.py-9923- # "int" is the default for unannotated vars — accept leniently +asmpython/_compiler/sema.py-9924- raise SemaError( +asmpython/_compiler/sema.py-9925- "len() requires a string, list, dict, tuple, or set", e.pos, +asmpython/_compiler/sema.py-9926- ErrorCode.E_ARG_TYPE, +asmpython/_compiler/sema.py-9927- ) +asmpython/_compiler/sema.py-9928- elif e.func == "int": +asmpython/_compiler/sema.py-9929- t = A.expr_type(e.args[0]) +asmpython/_compiler/sema.py-9930- # An instance may define __int__ — accepted leniently like +asmpython/_compiler/sema.py-9931- # str()'s __str__/__repr__ dispatch; codegen falls back to +asmpython/_compiler/sema.py-9932- # treating the pointer as a raw int if no __int__ exists. +asmpython/_compiler/sema.py-9933- if t not in ("str", "float", "int", "any") and not t.startswith( +asmpython/_compiler/sema.py-9934- "instance:" +asmpython/_compiler/sema.py-9935- ): +asmpython/_compiler/sema.py-9936- raise SemaError("int() requires str / float / int", e.pos, ErrorCode.E_ARG_TYPE) +asmpython/_compiler/sema.py-9937- elif e.func == "float": +asmpython/_compiler/sema.py-9938- t = A.expr_type(e.args[0]) +asmpython/_compiler/sema.py-9939- if t not in ("str", "int", "float", "any"): +asmpython/_compiler/sema.py-9940- raise SemaError("float() requires str / int / float", e.pos, ErrorCode.E_ARG_TYPE) +asmpython/_compiler/sema.py:9941: elif e.func == "str": +asmpython/_compiler/sema.py-9942- t = A.expr_type(e.args[0]) +asmpython/_compiler/sema.py-9943- # int/float/str convert directly; list/tuple/dict/set stringify +asmpython/_compiler/sema.py-9944- # via their repr; an opaque value or an instance (which may define +asmpython/_compiler/sema.py-9945- # __str__/__repr__) is accepted leniently. All yield a str. +asmpython/_compiler/sema.py-9946- if t not in ( +asmpython/_compiler/sema.py-9947- "int", "float", "str", "any", "list", "tuple", "dict", "set" +asmpython/_compiler/sema.py-9948- ) and not t.startswith("instance:"): +asmpython/_compiler/sema.py-9949- raise SemaError( +asmpython/_compiler/sema.py-9950- "str() requires a scalar, container, or object", e.pos, +asmpython/_compiler/sema.py-9951- ErrorCode.E_ARG_TYPE, +asmpython/_compiler/sema.py-9952- ) +asmpython/_compiler/sema.py-9953- return +asmpython/_compiler/sema.py-9954- if e.func in self.overload_sets: +asmpython/_compiler/sema.py-9955- for a in e.args: +asmpython/_compiler/sema.py-9956- self._check_expr(a, scope) +asmpython/_compiler/sema.py-9957- sig = self._resolve_overload(e.func, self.overload_sets[e.func], e.args, e.pos) +asmpython/_compiler/sema.py-9958- e.resolved_overload_symbol = _overload_symbol(e.func, sig) +asmpython/_compiler/sema.py-9959- self._bind_args( +asmpython/_compiler/sema.py-9960- e, sig.param_names, sig.param_defaults, sig.vararg, e.pos, e.func, +asmpython/_compiler/sema.py-9961- kwarg=sig.kwarg, +asmpython/_compiler/sema.py-9962- ) +asmpython/_compiler/sema.py-9963- if sig.ret_type is not None: +asmpython/_compiler/sema.py-9964- ret_tuple_ov: tuple = sig.ret_type # type: ignore +asmpython/_compiler/sema.py-9965- e.inferred_type = ret_tuple_ov[0] +asmpython/_compiler/sema.py-9966- else: +asmpython/_compiler/sema.py-9967- e.inferred_type = "int" +asmpython/_compiler/sema.py-9968- return +asmpython/_compiler/sema.py-9969- if e.func in self.funcs: +asmpython/_compiler/sema.py-9970- sig = self.funcs[e.func] +asmpython/_compiler/sema.py-9971- self._expand_dstar_kwarg(e, sig.param_names, scope) +asmpython/_compiler/sema.py-9972- # Plain positional calls keep the precise arity diagnostics; calls +asmpython/_compiler/sema.py-9973- # with keyword args or to a `*args` function are validated by the +asmpython/_compiler/sema.py-9974- # binder instead. +asmpython/_compiler/sema.py-9975- if sig.vararg is None and sig.kwarg is None and not e.kwargs: +asmpython/_compiler/sema.py-9976- required = sig.arity - sig.n_defaults +asmpython/_compiler/sema.py-9977- if not (required <= len(e.args) <= sig.arity): +asmpython/_compiler/sema.py-9978- if required == sig.arity: +asmpython/_compiler/sema.py-9979- raise SemaError( +asmpython/_compiler/sema.py-9980- f"{e.func}() takes {sig.arity} argument(s), got {len(e.args)}", +asmpython/_compiler/sema.py-9981- e.pos, +asmpython/_compiler/sema.py-9982- ErrorCode.E_ARG_COUNT, +asmpython/_compiler/sema.py-9983- ) +asmpython/_compiler/sema.py-9984- raise SemaError( +asmpython/_compiler/sema.py-9985- f"{e.func}() takes {required}-{sig.arity} arguments, got {len(e.args)}", +asmpython/_compiler/sema.py-9986- e.pos, +asmpython/_compiler/sema.py-9987- ErrorCode.E_ARG_COUNT, +asmpython/_compiler/sema.py-9988- ) +asmpython/_compiler/sema.py-9989- # Normalize every call to a complete positional argument list +asmpython/_compiler/sema.py-9990- # (defaults filled, keyword args placed, varargs packed) so codegen +asmpython/_compiler/sema.py-9991- # always sees a fixed-shape call. +asmpython/_compiler/sema.py-9992- self._bind_args( +asmpython/_compiler/sema.py-9993- e, sig.param_names, sig.param_defaults, sig.vararg, e.pos, e.func, +asmpython/_compiler/sema.py-9994- kwarg=sig.kwarg, +asmpython/_compiler/sema.py-9995- ) +asmpython/_compiler/sema.py-9996- for a in e.args: +-- +asmpython/_compiler/ir_lower.py-7462- # declares a float parameter (e.g. `sqrt(49)` -- a bare int +asmpython/_compiler/ir_lower.py-7463- # literal into a `("float",)`-typed binding): without this, +asmpython/_compiler/ir_lower.py-7464- # the raw integer bits get passed through unconverted and +asmpython/_compiler/ir_lower.py-7465- # reinterpreted as a double bit pattern on the callee side, +asmpython/_compiler/ir_lower.py-7466- # producing garbage (confirmed: `sqrt(49)` silently returned +asmpython/_compiler/ir_lower.py-7467- # `0` instead of `7`). No reverse case needed -- a real +asmpython/_compiler/ir_lower.py-7468- # Python float literal/expression passed to an int-typed +asmpython/_compiler/ir_lower.py-7469- # parameter isn't valid input this compiler needs to handle +asmpython/_compiler/ir_lower.py-7470- # leniently, unlike this int-into-float direction, which is +asmpython/_compiler/ir_lower.py-7471- # extremely common (any bare int literal argument). +asmpython/_compiler/ir_lower.py-7472- if ( +asmpython/_compiler/ir_lower.py-7473- i < len(fn.arg_types) +asmpython/_compiler/ir_lower.py-7474- and fn.arg_types[i] == "float" +asmpython/_compiler/ir_lower.py-7475- and av.type is not F64 +asmpython/_compiler/ir_lower.py-7476- ): +asmpython/_compiler/ir_lower.py-7477- fv = ctx.tmp(F64) +asmpython/_compiler/ir_lower.py-7478- ctx.emit(IRInstr("sitofp", fv, [av])) +asmpython/_compiler/ir_lower.py-7479- av = fv +asmpython/_compiler/ir_lower.py-7480- args.append(av) +asmpython/_compiler/ir_lower.py-7481- res_ty = F64 if fn.ret_type == "float" else I64 +asmpython/_compiler/ir_lower.py-7482- v = ctx.tmp(res_ty) +asmpython/_compiler/ir_lower.py-7483- ctx.emit(IRInstr("call", v, [c_name] + args)) +asmpython/_compiler/ir_lower.py-7484- return v +asmpython/_compiler/ir_lower.py-7485- +asmpython/_compiler/ir_lower.py-7486- if isinstance(e, A.Call): +asmpython/_compiler/ir_lower.py:7487: if e.func == "str" and len(e.args) == 1: +asmpython/_compiler/ir_lower.py-7488- # Delegates to _lower_expr_as_str, the general str-coercion +asmpython/_compiler/ir_lower.py-7489- # helper f-strings/print() already use -- it covers bool/None +asmpython/_compiler/ir_lower.py-7490- # (this hand-rolled version used to fall through to plain +asmpython/_compiler/ir_lower.py-7491- # decimal conversion for True/False, printing "1"/"0") plus +asmpython/_compiler/ir_lower.py-7492- # tuple/list/dict/set, which this call site never supported at +asmpython/_compiler/ir_lower.py-7493- # all. +asmpython/_compiler/ir_lower.py-7494- return _lower_expr_as_str(ctx, e.args[0]) +asmpython/_compiler/ir_lower.py-7495- if e.func == "int" and len(e.args) in (1, 2): +asmpython/_compiler/ir_lower.py-7496- arg = e.args[0] +asmpython/_compiler/ir_lower.py-7497- arg_t = A.expr_type(arg) +asmpython/_compiler/ir_lower.py-7498- if len(e.args) == 2: +asmpython/_compiler/ir_lower.py-7499- str_v = _lower_expr(ctx, arg) +asmpython/_compiler/ir_lower.py-7500- base_v = _lower_expr(ctx, e.args[1]) +asmpython/_compiler/ir_lower.py-7501- out = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-7502- ctx.emit(IRInstr("call", out, ["_abi_str_to_int_base", str_v, base_v])) +asmpython/_compiler/ir_lower.py-7503- return out +asmpython/_compiler/ir_lower.py-7504- if arg_t == "str": +asmpython/_compiler/ir_lower.py-7505- str_v = _lower_expr(ctx, arg) +asmpython/_compiler/ir_lower.py-7506- out = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-7507- ctx.emit(IRInstr("call", out, ["_abi_str_to_int", str_v])) +asmpython/_compiler/ir_lower.py-7508- return out +asmpython/_compiler/ir_lower.py-7509- if arg_t == "float": +asmpython/_compiler/ir_lower.py-7510- float_v = _lower_expr(ctx, arg) +asmpython/_compiler/ir_lower.py-7511- out = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-7512- ctx.emit(IRInstr("fptosi", out, [float_v])) +asmpython/_compiler/ir_lower.py-7513- return out +asmpython/_compiler/ir_lower.py-7514- if arg_t.startswith("instance:"): +asmpython/_compiler/ir_lower.py-7515- owner = _resolve_method_owner(ctx, arg_t.split(":", 1)[1], "__int__") +asmpython/_compiler/ir_lower.py-7516- if owner is not None: +asmpython/_compiler/ir_lower.py-7517- obj_v = _lower_expr(ctx, arg) +asmpython/_compiler/ir_lower.py-7518- out = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-7519- ctx.emit(IRInstr("call", out, [f"{owner}____int__", obj_v])) +asmpython/_compiler/ir_lower.py-7520- return out +asmpython/_compiler/ir_lower.py-7521- return _lower_expr(ctx, arg) +asmpython/_compiler/ir_lower.py-7522- if e.func == "float" and len(e.args) == 1: +asmpython/_compiler/ir_lower.py-7523- arg = e.args[0] +asmpython/_compiler/ir_lower.py-7524- arg_t = A.expr_type(arg) +asmpython/_compiler/ir_lower.py-7525- # float("nan")/float("inf")/float("-inf") emit the bit +asmpython/_compiler/ir_lower.py-7526- # pattern directly rather than relying on strtod (matches +asmpython/_compiler/ir_lower.py-7527- # codegen.py's own special-case, since UCRT's strtod +asmpython/_compiler/ir_lower.py-7528- # historically had "nan"/"inf" parsing quirks). +asmpython/_compiler/ir_lower.py-7529- if isinstance(arg, A.StrLit): +asmpython/_compiler/ir_lower.py-7530- s = arg.value.strip().lower() +asmpython/_compiler/ir_lower.py-7531- if s == "nan": +asmpython/_compiler/ir_lower.py-7532- out = ctx.tmp(F64) +asmpython/_compiler/ir_lower.py-7533- ctx.emit(IRInstr("const", out, [float("nan")])) +asmpython/_compiler/ir_lower.py-7534- return out +asmpython/_compiler/ir_lower.py-7535- if s in ("inf", "+inf", "infinity", "+infinity"): +asmpython/_compiler/ir_lower.py-7536- out = ctx.tmp(F64) +asmpython/_compiler/ir_lower.py-7537- ctx.emit(IRInstr("const", out, [float("inf")])) +asmpython/_compiler/ir_lower.py-7538- return out +asmpython/_compiler/ir_lower.py-7539- if s in ("-inf", "-infinity"): +asmpython/_compiler/ir_lower.py-7540- out = ctx.tmp(F64) +asmpython/_compiler/ir_lower.py-7541- ctx.emit(IRInstr("const", out, [float("-inf")])) +asmpython/_compiler/ir_lower.py-7542- return out +-- +asmpython/_compiler/codegen.py-13673- f"mov rbx, [rbp{b_slot:+d}]", +asmpython/_compiler/codegen.py-13674- "call _runtime_range_list", +asmpython/_compiler/codegen.py-13675- ) +asmpython/_compiler/codegen.py-13676- return +asmpython/_compiler/codegen.py-13677- if e.func == "len": +asmpython/_compiler/codegen.py-13678- arg = e.args[0] +asmpython/_compiler/codegen.py-13679- self.gen_expr(arg, info) # rax = ptr +asmpython/_compiler/codegen.py-13680- t: str = A.expr_type(arg) +asmpython/_compiler/codegen.py-13681- if t in ("list", "tuple"): +asmpython/_compiler/codegen.py-13682- # Tuples reuse the list layout, so len lives at LIST_LEN_OFF. +asmpython/_compiler/codegen.py-13683- self.emitf(f"mov rax, [rax+{self.LIST_LEN_OFF}]") +asmpython/_compiler/codegen.py-13684- elif t in ("dict", "set"): +asmpython/_compiler/codegen.py-13685- # Sets are dict-backed; len lives at DICT_LEN_OFF. +asmpython/_compiler/codegen.py-13686- self.emitf(f"mov rax, [rax+{self.DICT_LEN_OFF}]") +asmpython/_compiler/codegen.py-13687- elif t.startswith("instance:"): +asmpython/_compiler/codegen.py-13688- cls_name = t.split(":", 1)[1] +asmpython/_compiler/codegen.py-13689- owner = self._resolve_method_owner(cls_name, "__len__") +asmpython/_compiler/codegen.py-13690- if owner is not None: +asmpython/_compiler/codegen.py-13691- self.emitf(f"mov {self._arg_reg(0)}, rax") +asmpython/_compiler/codegen.py-13692- self.emit_call(self._method_symbol(owner, "__len__")) +asmpython/_compiler/codegen.py-13693- else: +asmpython/_compiler/codegen.py-13694- self.emitf("xor rax, rax") +asmpython/_compiler/codegen.py-13695- else: +asmpython/_compiler/codegen.py-13696- self._emit_strlen() # rax = length (string) +asmpython/_compiler/codegen.py-13697- return +asmpython/_compiler/codegen.py:13698: if e.func == "str": +asmpython/_compiler/codegen.py-13699- arg_t: str = A.expr_type(e.args[0]) +asmpython/_compiler/codegen.py-13700- self.gen_expr(e.args[0], info) +asmpython/_compiler/codegen.py-13701- # int/float conversions land in the shared static itoa buffer; str() +asmpython/_compiler/codegen.py-13702- # results are commonly stored (e.g. `[str(x) for x in xs]`), so copy +asmpython/_compiler/codegen.py-13703- # out to a fresh allocation to avoid every result aliasing the buffer. +asmpython/_compiler/codegen.py-13704- if arg_t in ("list", "tuple", "dict", "set"): +asmpython/_compiler/codegen.py-13705- # str(container) == repr(container) for these built-ins. +asmpython/_compiler/codegen.py-13706- self._emit_container_repr(e.args[0], arg_t) +asmpython/_compiler/codegen.py-13707- elif arg_t == "float": +asmpython/_compiler/codegen.py-13708- # xmm0 has the value; print into our int_to_str buffer via sprintf. +asmpython/_compiler/codegen.py-13709- self._emit_float_to_str() # rax = ptr (shared buffer) +asmpython/_compiler/codegen.py-13710- self.emitf("call _runtime_str_concat_dup") +asmpython/_compiler/codegen.py-13711- elif arg_t == "str": +asmpython/_compiler/codegen.py-13712- pass # already a str ptr in rax +asmpython/_compiler/codegen.py-13713- elif arg_t.startswith("instance:"): +asmpython/_compiler/codegen.py-13714- resolved = self._resolve_str_dunder(arg_t.split(":", 1)[1]) +asmpython/_compiler/codegen.py-13715- if resolved is not None: +asmpython/_compiler/codegen.py-13716- owner, method = resolved +asmpython/_compiler/codegen.py-13717- self.emitf(f"mov {self._arg_reg(0)}, rax") +asmpython/_compiler/codegen.py-13718- self.emit_call(self._method_symbol(owner, method)) +asmpython/_compiler/codegen.py-13719- else: +asmpython/_compiler/codegen.py-13720- self._emit_int_to_str() # rax = ptr to ASCII +asmpython/_compiler/codegen.py-13721- self.emitf("call _runtime_str_concat_dup") +asmpython/_compiler/codegen.py-13722- elif arg_t == "int" and A.is_bool_expr(e.args[0]): +asmpython/_compiler/codegen.py-13723- self._emit_bool_to_str() +asmpython/_compiler/codegen.py-13724- self.emitf("call _runtime_str_concat_dup") +asmpython/_compiler/codegen.py-13725- elif arg_t == "int" and A.is_none_expr(e.args[0]): +asmpython/_compiler/codegen.py-13726- self.emitf( +asmpython/_compiler/codegen.py-13727- "lea rax, [_runtime_none_str]", "call _runtime_str_concat_dup" +asmpython/_compiler/codegen.py-13728- ) +asmpython/_compiler/codegen.py-13729- else: +asmpython/_compiler/codegen.py-13730- self._emit_int_to_str() # rax = ptr to ASCII (shared buffer) +asmpython/_compiler/codegen.py-13731- self.emitf("call _runtime_str_concat_dup") +asmpython/_compiler/codegen.py-13732- return +asmpython/_compiler/codegen.py-13733- if e.func == "int": +asmpython/_compiler/codegen.py-13734- arg_t = A.expr_type(e.args[0]) +asmpython/_compiler/codegen.py-13735- if len(e.args) == 2: +asmpython/_compiler/codegen.py-13736- # int(s, base): parse the string in the given radix via strtoll. +asmpython/_compiler/codegen.py-13737- # base 0 auto-detects 0x / 0o / 0b prefixes (matches CPython). +asmpython/_compiler/codegen.py-13738- base_slot = info.locals_[f"__int_base_{id(e)}"] +asmpython/_compiler/codegen.py-13739- self.gen_expr(e.args[1], info) # rax = base (int) +asmpython/_compiler/codegen.py-13740- self.emitf(f"mov [rbp{base_slot:+d}], rax") +asmpython/_compiler/codegen.py-13741- self.gen_expr(e.args[0], info) # rax = string ptr +asmpython/_compiler/codegen.py-13742- self.emitf(f"mov rbx, [rbp{base_slot:+d}]") +asmpython/_compiler/codegen.py-13743- self._emit_str_to_int_base() # rax = parsed int +asmpython/_compiler/codegen.py-13744- return +asmpython/_compiler/codegen.py-13745- self.gen_expr(e.args[0], info) +asmpython/_compiler/codegen.py-13746- if arg_t == "str": +asmpython/_compiler/codegen.py-13747- self._emit_str_to_int() +asmpython/_compiler/codegen.py-13748- elif arg_t == "float": +asmpython/_compiler/codegen.py-13749- # Truncate toward zero (Python's int(float) semantics). +asmpython/_compiler/codegen.py-13750- self.emitf("cvttsd2si rax, xmm0") +asmpython/_compiler/codegen.py-13751- elif arg_t.startswith("instance:"): +asmpython/_compiler/codegen.py-13752- resolved = self._resolve_int_dunder(arg_t.split(":", 1)[1]) +asmpython/_compiler/codegen.py-13753- if resolved is not None: +=== class ids and type values === +asmpython/_compiler/sema.py-212- "AttributeError", +asmpython/_compiler/sema.py-213- "KeyError", +asmpython/_compiler/sema.py-214- "IndexError", +asmpython/_compiler/sema.py-215- "LookupError", +asmpython/_compiler/sema.py-216- "StopIteration", +asmpython/_compiler/sema.py-217- "ArithmeticError", +asmpython/_compiler/sema.py-218- "ZeroDivisionError", +asmpython/_compiler/sema.py-219- "OverflowError", +asmpython/_compiler/sema.py-220- "AssertionError", +asmpython/_compiler/sema.py-221- "ImportError", +asmpython/_compiler/sema.py-222- "OSError", +asmpython/_compiler/sema.py-223- "IOError", +asmpython/_compiler/sema.py-224- "FileNotFoundError", +asmpython/_compiler/sema.py-225-}) +asmpython/_compiler/sema.py-226- +asmpython/_compiler/sema.py-227-# Builtin scalar/container type names usable as a bare *value* (not just a +asmpython/_compiler/sema.py-228-# call target or an annotation), e.g. `{"type": str}` mimicking argparse's +asmpython/_compiler/sema.py-229-# `add_argument(type=str)` convention. asmpython has no first-class type +asmpython/_compiler/sema.py-230-# objects -- like a user class or builtin exception used as a value, this +asmpython/_compiler/sema.py-231-# loads a stable per-name RTTI id the program never actually inspects (see +asmpython/_compiler/sema.py:232:# codegen.py's BUILTIN_TYPE_IDS and class_ids). +asmpython/_compiler/sema.py-233-BUILTIN_TYPE_NAMES: frozenset[str] = frozenset({ +asmpython/_compiler/sema.py-234- "int", "float", "str", "bool", "list", "dict", "tuple", "set", +asmpython/_compiler/sema.py-235-}) +asmpython/_compiler/sema.py-236- +asmpython/_compiler/sema.py-237- +asmpython/_compiler/sema.py-238-# Interpreter-only `.` calls: features that require a live +asmpython/_compiler/sema.py-239-# Python interpreter (dynamic import / code execution by string) and so cannot +asmpython/_compiler/sema.py-240-# be compiled to native code. These are in the excluded 0.1% of the language. +asmpython/_compiler/sema.py-241-# Rejected with a clear, located message rather than letting them slip through +asmpython/_compiler/sema.py-242-# the module-leniency path and explode in codegen with a raw traceback. +asmpython/_compiler/sema.py-243-INTERPRETER_ONLY_METHODS: frozenset[tuple[str, str]] = frozenset({ +asmpython/_compiler/sema.py-244- ("importlib", "import_module"), +asmpython/_compiler/sema.py-245- ("importlib", "reload"), +asmpython/_compiler/sema.py-246- ("imp", "load_module"), +asmpython/_compiler/sema.py-247-}) +asmpython/_compiler/sema.py-248- +asmpython/_compiler/sema.py-249-# Interpreter-only *builtins* (bare calls, not module methods). +asmpython/_compiler/sema.py-250-INTERPRETER_ONLY_BUILTINS: frozenset[str] = frozenset({ +asmpython/_compiler/sema.py-251- "eval", +asmpython/_compiler/sema.py-252- "exec", +asmpython/_compiler/sema.py-253- "compile", +asmpython/_compiler/sema.py-254- "__import__", +asmpython/_compiler/sema.py-255- "globals", +asmpython/_compiler/sema.py-256- "locals", +asmpython/_compiler/sema.py-257- "vars", +asmpython/_compiler/sema.py-258-}) +asmpython/_compiler/sema.py-259- +asmpython/_compiler/sema.py-260-# Binary operator -> (forward dunder, reflected dunder). A user class can +asmpython/_compiler/sema.py-261-# overload `a b` by defining the forward method on `a`'s class, or the +asmpython/_compiler/sema.py-262-# reflected method on `b`'s class (used when `a`'s type doesn't define the +asmpython/_compiler/sema.py-263-# forward method, mirroring Python's `NotImplemented` fallback — but since +asmpython/_compiler/sema.py-264-# asmpython doesn't model `NotImplemented`, the forward method wins whenever +asmpython/_compiler/sema.py-265-# it exists). E.g. `Path("a") / "b"` resolves `Path.__truediv__`. +asmpython/_compiler/sema.py-266-DUNDER_BINOP: dict[str, tuple[str, str]] = { +asmpython/_compiler/sema.py-267- "+": ("__add__", "__radd__"), +asmpython/_compiler/sema.py-268- "-": ("__sub__", "__rsub__"), +asmpython/_compiler/sema.py-269- "*": ("__mul__", "__rmul__"), +asmpython/_compiler/sema.py-270- "/": ("__truediv__", "__rtruediv__"), +asmpython/_compiler/sema.py-271- "//": ("__floordiv__", "__rfloordiv__"), +asmpython/_compiler/sema.py-272- "%": ("__mod__", "__rmod__"), +asmpython/_compiler/sema.py-273- "**": ("__pow__", "__rpow__"), +asmpython/_compiler/sema.py-274- "&": ("__and__", "__rand__"), +asmpython/_compiler/sema.py-275- "|": ("__or__", "__ror__"), +asmpython/_compiler/sema.py-276- "^": ("__xor__", "__rxor__"), +asmpython/_compiler/sema.py-277- "<<": ("__lshift__", "__rlshift__"), +-- +asmpython/_compiler/ir_lower.py-22-""" +asmpython/_compiler/ir_lower.py-23- +asmpython/_compiler/ir_lower.py-24-from __future__ import annotations +asmpython/_compiler/ir_lower.py-25- +asmpython/_compiler/ir_lower.py-26-from dataclasses import fields, is_dataclass +asmpython/_compiler/ir_lower.py-27- +asmpython/_compiler/ir_lower.py-28-from . import ast_nodes as A +asmpython/_compiler/ir_lower.py-29-from .ir import ( +asmpython/_compiler/ir_lower.py-30- IRBlock, +asmpython/_compiler/ir_lower.py-31- IRFunc, +asmpython/_compiler/ir_lower.py-32- IRGlobal, +asmpython/_compiler/ir_lower.py-33- IRInstr, +asmpython/_compiler/ir_lower.py-34- IRModule, +asmpython/_compiler/ir_lower.py-35- IRType, +asmpython/_compiler/ir_lower.py-36- IRValue, +asmpython/_compiler/ir_lower.py-37- F64, +asmpython/_compiler/ir_lower.py-38- I64, +asmpython/_compiler/ir_lower.py-39- PTR, +asmpython/_compiler/ir_lower.py-40- ir_type_for, +asmpython/_compiler/ir_lower.py-41-) +asmpython/_compiler/ir_lower.py:42:from .codegen import BUILTIN_EXC_IDS, BUILTIN_EXC_PARENTS, BUILTIN_TYPE_IDS, EXC_ANY +asmpython/_compiler/ir_lower.py-43- +asmpython/_compiler/ir_lower.py-44- +asmpython/_compiler/ir_lower.py-45-class LowerError(Exception): +asmpython/_compiler/ir_lower.py-46- pass +asmpython/_compiler/ir_lower.py-47- +asmpython/_compiler/ir_lower.py-48- +asmpython/_compiler/ir_lower.py-49-U8 = IRType("u8") +asmpython/_compiler/ir_lower.py-50- +asmpython/_compiler/ir_lower.py-51-# jmp_buf layout (mirrors _runtime_setjmp in codegen.py): rbx/rbp/r12-r15/ +asmpython/_compiler/ir_lower.py-52-# rsp/retaddr in the first 8 slots (0-56), rsi/rdi in slots 8-9 (64, 72) -- +asmpython/_compiler/ir_lower.py-53-# 10 regs * 8 bytes = 80 bytes. rsi/rdi were added after the first 8 slots +asmpython/_compiler/ir_lower.py-54-# were already established elsewhere; kept at the end rather than +asmpython/_compiler/ir_lower.py-55-# renumbering to avoid touching every other offset in this file. +asmpython/_compiler/ir_lower.py-56-_JMP_BUF_SIZE = 80 +asmpython/_compiler/ir_lower.py-57- +asmpython/_compiler/ir_lower.py-58-# Build parent-id map once at module level from BUILTIN_EXC_PARENTS. +asmpython/_compiler/ir_lower.py-59-_EXC_PARENT_OF: dict[int, int] = { +asmpython/_compiler/ir_lower.py-60- BUILTIN_EXC_IDS[k]: (BUILTIN_EXC_IDS[v] if v else -1) +asmpython/_compiler/ir_lower.py-61- for k, v in BUILTIN_EXC_PARENTS.items() +asmpython/_compiler/ir_lower.py-62- if k in BUILTIN_EXC_IDS +asmpython/_compiler/ir_lower.py-63-} +asmpython/_compiler/ir_lower.py-64- +asmpython/_compiler/ir_lower.py-65- +asmpython/_compiler/ir_lower.py-66-def _exc_raise_type_id_ir(value) -> int: +asmpython/_compiler/ir_lower.py-67- """Type id for `raise value` -- mirrors codegen._exc_raise_type_id.""" +asmpython/_compiler/ir_lower.py-68- name = None +asmpython/_compiler/ir_lower.py-69- if isinstance(value, A.Call): +asmpython/_compiler/ir_lower.py-70- name = value.func +asmpython/_compiler/ir_lower.py-71- elif isinstance(value, A.Name): +asmpython/_compiler/ir_lower.py-72- name = value.name +asmpython/_compiler/ir_lower.py-73- if name is not None and name in BUILTIN_EXC_IDS: +asmpython/_compiler/ir_lower.py-74- return BUILTIN_EXC_IDS[name] +asmpython/_compiler/ir_lower.py-75- return EXC_ANY +asmpython/_compiler/ir_lower.py-76- +asmpython/_compiler/ir_lower.py-77- +asmpython/_compiler/ir_lower.py-78-def _exc_matching_ids_ir(types: list) -> list: +asmpython/_compiler/ir_lower.py-79- """Full set of type ids an `except (T1, T2, ...):` catches, +asmpython/_compiler/ir_lower.py-80- including subtypes and EXC_ANY -- mirrors codegen._exc_matching_ids.""" +asmpython/_compiler/ir_lower.py-81- ancestor_ids = [BUILTIN_EXC_IDS[t] for t in types if t in BUILTIN_EXC_IDS] +asmpython/_compiler/ir_lower.py-82- matches: list = [EXC_ANY] +asmpython/_compiler/ir_lower.py-83- for a in ancestor_ids: +asmpython/_compiler/ir_lower.py-84- if a not in matches: +asmpython/_compiler/ir_lower.py-85- matches.append(a) +asmpython/_compiler/ir_lower.py-86- for tid in BUILTIN_EXC_IDS.values(): +asmpython/_compiler/ir_lower.py-87- for a in ancestor_ids: +-- +asmpython/_compiler/ir_lower.py-134- # to a compile-time-known scalar value (stdlib.Const), NOT a real +asmpython/_compiler/ir_lower.py-135- # runtime global. `_lower_expr`'s A.Name case checks this BEFORE +asmpython/_compiler/ir_lower.py-136- # falling back to the generic slot/global lookup (see there for +asmpython/_compiler/ir_lower.py-137- # why: without this, the name silently became an uninitialized +asmpython/_compiler/ir_lower.py-138- # local defaulting to I64, corrupting anything that read it). +asmpython/_compiler/ir_lower.py-139- self.ffi_consts = ffi_consts or {} +asmpython/_compiler/ir_lower.py-140- self.imported_modules = imported_modules or {} +asmpython/_compiler/ir_lower.py-141- # import_binary()/.imported dynamic-loading (see the "import_binary +asmpython/_compiler/ir_lower.py-142- # dynamic DLL loading" section near the end of this file): handle +asmpython/_compiler/ir_lower.py-143- # variable name -> list of (func_name, FuncDef) for every top-level +asmpython/_compiler/ir_lower.py-144- # `@.imported` stub decorated for it. Built once in +asmpython/_compiler/ir_lower.py-145- # lower_module from the whole-program mod.funcs, mirroring +asmpython/_compiler/ir_lower.py-146- # codegen.py's Codegen.imported_funcs exactly (same dict shape, same +asmpython/_compiler/ir_lower.py-147- # ".imported" decorator-suffix scan) so both backends resolve the +asmpython/_compiler/ir_lower.py-148- # same set of dynamically-imported functions per handle. +asmpython/_compiler/ir_lower.py-149- self.imported_funcs: dict[str, list[tuple[str, "A.FuncDef"]]] = imported_funcs or {} +asmpython/_compiler/ir_lower.py-150- self.classes_sig = classes_sig or {} +asmpython/_compiler/ir_lower.py-151- self.global_types = global_types or {} +asmpython/_compiler/ir_lower.py-152- self.global_names = frozenset(self.global_types) +asmpython/_compiler/ir_lower.py-153- self.global_list_el_ty = global_list_el_ty or {} +asmpython/_compiler/ir_lower.py:154: self.class_ids: dict[str, int] = { +asmpython/_compiler/ir_lower.py-155- name: i for i, name in enumerate(sorted(class_names)) +asmpython/_compiler/ir_lower.py-156- } +asmpython/_compiler/ir_lower.py-157- # `class C: x = 5` (plain class, not @dataclass -- a dataclass's +asmpython/_compiler/ir_lower.py-158- # class vars are per-instance fields, handled entirely differently) +asmpython/_compiler/ir_lower.py-159- # static class-level variables: `ClassName.attr` reads/writes a +asmpython/_compiler/ir_lower.py-160- # real dedicated global, one per (class, var) pair, initialized +asmpython/_compiler/ir_lower.py-161- # from its default expression at module-init time. Mirrors +asmpython/_compiler/ir_lower.py-162- # codegen.py's `class_var_labels`/`class_var_defaults` exactly +asmpython/_compiler/ir_lower.py-163- # (`__cv___` label convention) -- this was previously +asmpython/_compiler/ir_lower.py-164- # entirely unimplemented on this backend: `ClassName.attr` fell +asmpython/_compiler/ir_lower.py-165- # through to the generic instance-attribute dict-lookup fallback, +asmpython/_compiler/ir_lower.py-166- # which treated the class's raw RTTI id (a small integer, e.g. 0) +asmpython/_compiler/ir_lower.py-167- # as if it were a real object pointer and dereferenced it, +asmpython/_compiler/ir_lower.py-168- # crashing (confirmed via gdb: SIGSEGV reading near address 0). +asmpython/_compiler/ir_lower.py-169- self.class_var_labels: dict[tuple[str, str], str] = {} +asmpython/_compiler/ir_lower.py-170- self.class_var_defaults: list[tuple[str, "A.Expr"]] = [] +asmpython/_compiler/ir_lower.py-171- for cls in classes or []: +asmpython/_compiler/ir_lower.py-172- if getattr(cls, "is_dataclass", False): +asmpython/_compiler/ir_lower.py-173- continue +asmpython/_compiler/ir_lower.py-174- for cv in getattr(cls, "class_vars", []) or []: +asmpython/_compiler/ir_lower.py-175- cvname, _annot, cvdefault = cv +asmpython/_compiler/ir_lower.py-176- if cvdefault is None: +asmpython/_compiler/ir_lower.py-177- continue +asmpython/_compiler/ir_lower.py-178- label = f"__cv_{cls.name}__{cvname}" +asmpython/_compiler/ir_lower.py-179- self.class_var_labels[(cls.name, cvname)] = label +asmpython/_compiler/ir_lower.py-180- self.class_var_defaults.append((label, cvdefault)) +asmpython/_compiler/ir_lower.py-181- self._str_names: dict[str, str] = {} +asmpython/_compiler/ir_lower.py-182- self._n = 0 +asmpython/_compiler/ir_lower.py-183- +asmpython/_compiler/ir_lower.py-184- def intern_str(self, value: str) -> str: +asmpython/_compiler/ir_lower.py-185- if value in self._str_names: +asmpython/_compiler/ir_lower.py-186- return self._str_names[value] +asmpython/_compiler/ir_lower.py-187- self._n += 1 +asmpython/_compiler/ir_lower.py-188- name = f"__str_{self._n}" +asmpython/_compiler/ir_lower.py-189- self.data.append(IRGlobal(name=name, type=PTR, value=value)) +asmpython/_compiler/ir_lower.py-190- self._str_names[value] = name +asmpython/_compiler/ir_lower.py-191- return name +asmpython/_compiler/ir_lower.py-192- +asmpython/_compiler/ir_lower.py-193- +asmpython/_compiler/ir_lower.py-194-class _FuncCtx: +asmpython/_compiler/ir_lower.py-195- def __init__( +asmpython/_compiler/ir_lower.py-196- self, +asmpython/_compiler/ir_lower.py-197- mctx: _ModuleCtx, +asmpython/_compiler/ir_lower.py-198- *, +asmpython/_compiler/ir_lower.py-199- local_names: set[str] | None = None, +-- +asmpython/_compiler/ir_lower.py-2212- return "staticmethod" in getattr(msig, "decorators", []) +asmpython/_compiler/ir_lower.py-2213- +asmpython/_compiler/ir_lower.py-2214- +asmpython/_compiler/ir_lower.py-2215-def _resolve_method_owner(ctx: _FuncCtx, class_name: str, method: str) -> str | None: +asmpython/_compiler/ir_lower.py-2216- for cname in _resolve_class_chain(ctx, class_name): +asmpython/_compiler/ir_lower.py-2217- sig = ctx.mctx.classes_sig.get(cname) +asmpython/_compiler/ir_lower.py-2218- if sig is not None and method in sig.methods: +asmpython/_compiler/ir_lower.py-2219- return cname +asmpython/_compiler/ir_lower.py-2220- return None +asmpython/_compiler/ir_lower.py-2221- +asmpython/_compiler/ir_lower.py-2222- +asmpython/_compiler/ir_lower.py-2223-def _virtual_dispatch_rows(ctx: _FuncCtx, class_name: str, method: str) -> list[tuple[int, str]]: +asmpython/_compiler/ir_lower.py-2224- """[(class_id, owner)] for every user class that is `class_name` or +asmpython/_compiler/ir_lower.py-2225- descends from it and resolves `method` somewhere on its chain -- +asmpython/_compiler/ir_lower.py-2226- mirrors codegen.py's _virtual_dispatch_rows exactly. A method call on a +asmpython/_compiler/ir_lower.py-2227- `class_name`-typed receiver can bind statically only when every row +asmpython/_compiler/ir_lower.py-2228- shares one owner; with overrides in play the call must dispatch on the +asmpython/_compiler/ir_lower.py-2229- instance's runtime __class__ id instead, since the static type names +asmpython/_compiler/ir_lower.py-2230- the base but the receiver at runtime may be a subclass.""" +asmpython/_compiler/ir_lower.py-2231- rows: list[tuple[int, str]] = [] +asmpython/_compiler/ir_lower.py:2232: for cname, cid in ctx.mctx.class_ids.items(): +asmpython/_compiler/ir_lower.py-2233- if class_name not in _resolve_class_chain(ctx, cname): +asmpython/_compiler/ir_lower.py-2234- continue +asmpython/_compiler/ir_lower.py-2235- owner = _resolve_method_owner(ctx, cname, method) +asmpython/_compiler/ir_lower.py-2236- if owner is not None: +asmpython/_compiler/ir_lower.py-2237- rows.append((cid, owner)) +asmpython/_compiler/ir_lower.py-2238- return rows +asmpython/_compiler/ir_lower.py-2239- +asmpython/_compiler/ir_lower.py-2240- +asmpython/_compiler/ir_lower.py:2241:def _subclass_ids(ctx: _FuncCtx, target: str) -> list[int]: +asmpython/_compiler/ir_lower.py-2242- ids: list[int] = [] +asmpython/_compiler/ir_lower.py:2243: for name, cid in ctx.mctx.class_ids.items(): +asmpython/_compiler/ir_lower.py-2244- cur = name +asmpython/_compiler/ir_lower.py-2245- seen: list[str] = [] +asmpython/_compiler/ir_lower.py-2246- while cur and cur not in seen: +asmpython/_compiler/ir_lower.py-2247- if cur == target: +asmpython/_compiler/ir_lower.py-2248- ids.append(cid) +asmpython/_compiler/ir_lower.py-2249- break +asmpython/_compiler/ir_lower.py-2250- seen.append(cur) +asmpython/_compiler/ir_lower.py-2251- sig = ctx.mctx.classes_sig.get(cur) +asmpython/_compiler/ir_lower.py-2252- cur = sig.parent if sig is not None else None +asmpython/_compiler/ir_lower.py-2253- return ids +asmpython/_compiler/ir_lower.py-2254- +asmpython/_compiler/ir_lower.py-2255- +asmpython/_compiler/ir_lower.py-2256-def _resolve_str_dunder(ctx: _FuncCtx, class_name: str, repr_first: bool = False) -> tuple[str, str] | None: +asmpython/_compiler/ir_lower.py-2257- methods = ("__repr__", "__str__") if repr_first else ("__str__", "__repr__") +asmpython/_compiler/ir_lower.py-2258- for method in methods: +asmpython/_compiler/ir_lower.py-2259- owner = _resolve_method_owner(ctx, class_name, method) +asmpython/_compiler/ir_lower.py-2260- if owner is not None: +asmpython/_compiler/ir_lower.py-2261- return owner, method +asmpython/_compiler/ir_lower.py-2262- return None +asmpython/_compiler/ir_lower.py-2263- +asmpython/_compiler/ir_lower.py-2264- +asmpython/_compiler/ir_lower.py-2265-def _value_repr_kind(t: str) -> int: +asmpython/_compiler/ir_lower.py-2266- if t == "str": +asmpython/_compiler/ir_lower.py-2267- return 1 +asmpython/_compiler/ir_lower.py-2268- if t == "float": +asmpython/_compiler/ir_lower.py-2269- return 2 +asmpython/_compiler/ir_lower.py-2270- return 0 +asmpython/_compiler/ir_lower.py-2271- +asmpython/_compiler/ir_lower.py-2272- +asmpython/_compiler/ir_lower.py-2273-def _composite_repr_kind(t: str, inner: str) -> int: +asmpython/_compiler/ir_lower.py-2274- if t == "list": +asmpython/_compiler/ir_lower.py-2275- return 3 | (_value_repr_kind(inner) << 4) +asmpython/_compiler/ir_lower.py-2276- if t == "dict": +asmpython/_compiler/ir_lower.py-2277- return 4 | (_value_repr_kind(inner) << 4) +asmpython/_compiler/ir_lower.py-2278- if t == "tuple": +asmpython/_compiler/ir_lower.py-2279- return 5 +asmpython/_compiler/ir_lower.py-2280- return _value_repr_kind(t) +asmpython/_compiler/ir_lower.py-2281- +asmpython/_compiler/ir_lower.py-2282- +asmpython/_compiler/ir_lower.py-2283-def _list_repr_kind(e: A.Expr) -> int: +asmpython/_compiler/ir_lower.py-2284- el = getattr(e, "list_el_type", "int") or "int" +asmpython/_compiler/ir_lower.py-2285- inner = getattr(e, "list_el_value_type", "int") or "int" +asmpython/_compiler/ir_lower.py-2286- if isinstance(e, A.ListLit): +asmpython/_compiler/ir_lower.py-2287- el = e.el_type or "int" +asmpython/_compiler/ir_lower.py-2288- inner = getattr(e, "el_value_type", "int") or "int" +-- +asmpython/_compiler/ir_lower.py-3786- arg0_t = A.expr_type(arg0) +asmpython/_compiler/ir_lower.py-3787- has_prim_target = False +asmpython/_compiler/ir_lower.py-3788- prim_match = False +asmpython/_compiler/ir_lower.py-3789- for t in targets: +asmpython/_compiler/ir_lower.py-3790- if t in prim_map: +asmpython/_compiler/ir_lower.py-3791- has_prim_target = True +asmpython/_compiler/ir_lower.py-3792- if arg0_t in prim_map[t]: +asmpython/_compiler/ir_lower.py-3793- prim_match = True +asmpython/_compiler/ir_lower.py-3794- if t == "int" and A.is_bool_expr(arg0): +asmpython/_compiler/ir_lower.py-3795- prim_match = True +asmpython/_compiler/ir_lower.py-3796- if t == "bool" and A.is_bool_expr(arg0): +asmpython/_compiler/ir_lower.py-3797- prim_match = True +asmpython/_compiler/ir_lower.py-3798- if has_prim_target: +asmpython/_compiler/ir_lower.py-3799- _lower_expr(ctx, arg0) +asmpython/_compiler/ir_lower.py-3800- out = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-3801- ctx.emit(IRInstr("const", out, [1 if prim_match else 0])) +asmpython/_compiler/ir_lower.py-3802- return out +asmpython/_compiler/ir_lower.py-3803- +asmpython/_compiler/ir_lower.py-3804- accept: list[int] = [] +asmpython/_compiler/ir_lower.py-3805- for t in targets: +asmpython/_compiler/ir_lower.py:3806: for cid in _subclass_ids(ctx, t): +asmpython/_compiler/ir_lower.py-3807- if cid not in accept: +asmpython/_compiler/ir_lower.py-3808- accept.append(cid) +asmpython/_compiler/ir_lower.py-3809- +asmpython/_compiler/ir_lower.py-3810- obj_v = _lower_expr(ctx, arg0) +asmpython/_compiler/ir_lower.py-3811- zero = ctx.tmp(PTR if obj_v.type == PTR else I64) +asmpython/_compiler/ir_lower.py-3812- ctx.emit(IRInstr("const", zero, [0])) +asmpython/_compiler/ir_lower.py-3813- out_ptr = ctx.ensure_slot(f"__isinst_out_{id(e)}", I64) +asmpython/_compiler/ir_lower.py-3814- none_b = ctx.new_block("isinstnone") +asmpython/_compiler/ir_lower.py-3815- live_b = ctx.new_block("isinstlive") +asmpython/_compiler/ir_lower.py-3816- end_b = ctx.new_block("isinstend") +asmpython/_compiler/ir_lower.py-3817- is_none = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-3818- ctx.emit(IRInstr("icmp.eq", is_none, [obj_v, zero])) +asmpython/_compiler/ir_lower.py-3819- ctx.emit(IRInstr("br.t", None, [is_none, none_b.label, live_b.label])) +asmpython/_compiler/ir_lower.py-3820- +asmpython/_compiler/ir_lower.py-3821- ctx.switch_to(none_b) +asmpython/_compiler/ir_lower.py-3822- none_v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-3823- ctx.emit(IRInstr("const", none_v, [0])) +asmpython/_compiler/ir_lower.py-3824- ctx.emit(IRInstr("store", None, [none_v, out_ptr])) +asmpython/_compiler/ir_lower.py-3825- ctx.emit(IRInstr("br", None, [end_b.label])) +asmpython/_compiler/ir_lower.py-3826- +asmpython/_compiler/ir_lower.py-3827- ctx.switch_to(live_b) +asmpython/_compiler/ir_lower.py-3828- key_sym = ctx.mctx.intern_str("__class__") +asmpython/_compiler/ir_lower.py-3829- key_v = ctx.tmp(PTR) +asmpython/_compiler/ir_lower.py-3830- ctx.emit(IRInstr("global_addr", key_v, [key_sym])) +asmpython/_compiler/ir_lower.py-3831- miss_v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-3832- ctx.emit(IRInstr("const", miss_v, [-1])) +asmpython/_compiler/ir_lower.py-3833- class_id = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-3834- ctx.emit(IRInstr("call", class_id, ["_abi_dict_get_default", obj_v, key_v, miss_v])) +asmpython/_compiler/ir_lower.py-3835- match_v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-3836- ctx.emit(IRInstr("const", match_v, [0])) +asmpython/_compiler/ir_lower.py-3837- for cid in accept: +asmpython/_compiler/ir_lower.py-3838- cid_v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-3839- ctx.emit(IRInstr("const", cid_v, [cid])) +asmpython/_compiler/ir_lower.py-3840- eq_v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-3841- ctx.emit(IRInstr("icmp.eq", eq_v, [class_id, cid_v])) +asmpython/_compiler/ir_lower.py-3842- next_v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-3843- ctx.emit(IRInstr("ior", next_v, [match_v, eq_v])) +asmpython/_compiler/ir_lower.py-3844- match_v = next_v +asmpython/_compiler/ir_lower.py-3845- ctx.emit(IRInstr("store", None, [match_v, out_ptr])) +asmpython/_compiler/ir_lower.py-3846- ctx.emit(IRInstr("br", None, [end_b.label])) +asmpython/_compiler/ir_lower.py-3847- +asmpython/_compiler/ir_lower.py-3848- ctx.switch_to(end_b) +asmpython/_compiler/ir_lower.py-3849- out = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-3850- ctx.emit(IRInstr("load", out, [out_ptr])) +asmpython/_compiler/ir_lower.py-3851- return out +-- +asmpython/_compiler/ir_lower.py-4460- _emit_instance_field_set(ctx, obj_v, "_alive", zero_v) +asmpython/_compiler/ir_lower.py-4461- return obj_v +asmpython/_compiler/ir_lower.py-4462- +asmpython/_compiler/ir_lower.py-4463- +asmpython/_compiler/ir_lower.py-4464-def _lower_expr(ctx: _FuncCtx, e: A.Expr) -> IRValue: +asmpython/_compiler/ir_lower.py-4465- if isinstance(e, A.IntLit): +asmpython/_compiler/ir_lower.py-4466- v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-4467- ctx.emit(IRInstr("const", v, [int(e.value)])) +asmpython/_compiler/ir_lower.py-4468- return v +asmpython/_compiler/ir_lower.py-4469- +asmpython/_compiler/ir_lower.py-4470- if isinstance(e, A.FloatLit): +asmpython/_compiler/ir_lower.py-4471- v = ctx.tmp(F64) +asmpython/_compiler/ir_lower.py-4472- ctx.emit(IRInstr("const", v, [float(e.value)])) +asmpython/_compiler/ir_lower.py-4473- return v +asmpython/_compiler/ir_lower.py-4474- +asmpython/_compiler/ir_lower.py-4475- if isinstance(e, A.Name): +asmpython/_compiler/ir_lower.py-4476- if e.name in ctx.mctx.func_names and e.name not in ctx.slot_ty: +asmpython/_compiler/ir_lower.py-4477- v = ctx.tmp(PTR) +asmpython/_compiler/ir_lower.py-4478- ctx.emit(IRInstr("global_addr", v, [e.name])) +asmpython/_compiler/ir_lower.py-4479- return v +asmpython/_compiler/ir_lower.py:4480: if e.name in ctx.mctx.class_ids: +asmpython/_compiler/ir_lower.py-4481- v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py:4482: ctx.emit(IRInstr("const", v, [ctx.mctx.class_ids[e.name]])) +asmpython/_compiler/ir_lower.py-4483- return v +asmpython/_compiler/ir_lower.py:4484: if e.name in BUILTIN_TYPE_IDS: +asmpython/_compiler/ir_lower.py-4485- v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py:4486: ctx.emit(IRInstr("const", v, [BUILTIN_TYPE_IDS[e.name]])) +asmpython/_compiler/ir_lower.py-4487- return v +asmpython/_compiler/ir_lower.py-4488- if e.name in ctx.mctx.ffi_consts and e.name not in ctx.slot_ty: +asmpython/_compiler/ir_lower.py-4489- # `from math import pi` -- a bare name bound to a stdlib.Const, +asmpython/_compiler/ir_lower.py-4490- # not a real runtime global at all (the binding table itself +asmpython/_compiler/ir_lower.py-4491- # IS the value, resolved at COMPILE time). Was entirely +asmpython/_compiler/ir_lower.py-4492- # unhandled: fell through to the generic slot/global fallback +asmpython/_compiler/ir_lower.py-4493- # below, which allocated a fresh, never-initialized local +asmpython/_compiler/ir_lower.py-4494- # slot defaulting to I64 and read GARBAGE stack memory as the +asmpython/_compiler/ir_lower.py-4495- # constant's value -- confirmed via a real repro (`from math +asmpython/_compiler/ir_lower.py-4496- # import pi, sqrt; print(int(sqrt(pi * pi)))`) crashing the +asmpython/_compiler/ir_lower.py-4497- # COMPILER itself (not the compiled binary): the garbage +asmpython/_compiler/ir_lower.py-4498- # I64-typed value flowed into `pi * pi`'s `fmul`, and +asmpython/_compiler/ir_lower.py-4499- # regalloc allocated it a GP register to match its wrong +asmpython/_compiler/ir_lower.py-4500- # type, so codegen's XMM-only binop path hit a `RegLoc` +asmpython/_compiler/ir_lower.py-4501- # where it expected a `StackLoc`. Mirrors the existing +asmpython/_compiler/ir_lower.py-4502- # `module.CONST`-style `A.Attr` FFI-const handling elsewhere +asmpython/_compiler/ir_lower.py-4503- # in this file (see its own much longer comment for the +asmpython/_compiler/ir_lower.py-4504- # `value_windows` override rationale) -- same value +asmpython/_compiler/ir_lower.py-4505- # resolution, just for the from-import bare-name spelling +asmpython/_compiler/ir_lower.py-4506- # instead of the module-attribute spelling. `e.name not in +asmpython/_compiler/ir_lower.py-4507- # ctx.slot_ty` guards against a local variable shadowing the +asmpython/_compiler/ir_lower.py-4508- # imported constant's name (rare but real -- a function-local +asmpython/_compiler/ir_lower.py-4509- # `pi = 3` inside code that also imported `math.pi` at module +asmpython/_compiler/ir_lower.py-4510- # scope must read the LOCAL, not the FFI constant). +asmpython/_compiler/ir_lower.py-4511- b = ctx.mctx.ffi_consts[e.name] +asmpython/_compiler/ir_lower.py-4512- value = getattr(b, "value_windows", None) +asmpython/_compiler/ir_lower.py-4513- if value is None: +asmpython/_compiler/ir_lower.py-4514- value = getattr(b, "value", None) +asmpython/_compiler/ir_lower.py-4515- if b.ty == "str" and isinstance(value, str): +asmpython/_compiler/ir_lower.py-4516- name = ctx.mctx.intern_str(value) +asmpython/_compiler/ir_lower.py-4517- v = ctx.tmp(PTR) +asmpython/_compiler/ir_lower.py-4518- ctx.emit(IRInstr("global_addr", v, [name])) +asmpython/_compiler/ir_lower.py-4519- return v +asmpython/_compiler/ir_lower.py-4520- if b.ty == "int" and isinstance(value, (int, bool)): +asmpython/_compiler/ir_lower.py-4521- v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-4522- ctx.emit(IRInstr("const", v, [int(value)])) +asmpython/_compiler/ir_lower.py-4523- return v +asmpython/_compiler/ir_lower.py-4524- if b.ty == "float" and isinstance(value, (int, float)): +asmpython/_compiler/ir_lower.py-4525- v = ctx.tmp(F64) +asmpython/_compiler/ir_lower.py-4526- ctx.emit(IRInstr("const", v, [float(value)])) +asmpython/_compiler/ir_lower.py-4527- return v +asmpython/_compiler/ir_lower.py-4528- # Anything else (list-typed constants, etc.) falls through to +asmpython/_compiler/ir_lower.py-4529- # the generic path unchanged -- a separate, smaller, +asmpython/_compiler/ir_lower.py-4530- # not-yet-scoped gap, same as the module.CONST case's own +asmpython/_compiler/ir_lower.py-4531- # matching fallthrough note. +-- +asmpython/_compiler/ir_lower.py-7403- ctx.emit(IRInstr("const", zero, [0])) +asmpython/_compiler/ir_lower.py-7404- v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-7405- ctx.emit(IRInstr("call", v, ["_abi_dict_get_default", obj_val, key_ptr, zero])) +asmpython/_compiler/ir_lower.py-7406- if A.expr_type(e) == "float": +asmpython/_compiler/ir_lower.py-7407- # Every dict/instance-attribute cell is a plain 8-byte int slot +asmpython/_compiler/ir_lower.py-7408- # (_abi_dict_set/get_default only ever move GP-sized values); +asmpython/_compiler/ir_lower.py-7409- # a float attribute's bits went in via bitcast_f2i on write +asmpython/_compiler/ir_lower.py-7410- # (see A.AttrAssign below) and must come back out the same way, +asmpython/_compiler/ir_lower.py-7411- # not as a numeric int->float conversion (sitofp would treat +asmpython/_compiler/ir_lower.py-7412- # the raw bit pattern as an integer value, corrupting it). +asmpython/_compiler/ir_lower.py-7413- fv = ctx.tmp(F64) +asmpython/_compiler/ir_lower.py-7414- ctx.emit(IRInstr("bitcast_i2f", fv, [v])) +asmpython/_compiler/ir_lower.py-7415- return fv +asmpython/_compiler/ir_lower.py-7416- return v +asmpython/_compiler/ir_lower.py-7417- +asmpython/_compiler/ir_lower.py-7418- if isinstance(e, A.Call) and e.func in ctx.mctx.class_names: +asmpython/_compiler/ir_lower.py-7419- if e.func == "Thread": +asmpython/_compiler/ir_lower.py-7420- return _lower_thread_ctor(ctx, e) +asmpython/_compiler/ir_lower.py-7421- v = ctx.tmp(PTR) +asmpython/_compiler/ir_lower.py-7422- ctx.emit(IRInstr("call", v, ["_abi_new_instance"])) +asmpython/_compiler/ir_lower.py:7423: cid = ctx.mctx.class_ids.get(e.func) +asmpython/_compiler/ir_lower.py-7424- if cid is not None: +asmpython/_compiler/ir_lower.py-7425- key_name = ctx.mctx.intern_str("__class__") +asmpython/_compiler/ir_lower.py-7426- key_v = ctx.tmp(PTR) +asmpython/_compiler/ir_lower.py-7427- cid_v = ctx.tmp(I64) +asmpython/_compiler/ir_lower.py-7428- ctx.emit(IRInstr("global_addr", key_v, [key_name])) +asmpython/_compiler/ir_lower.py-7429- ctx.emit(IRInstr("const", cid_v, [cid])) +asmpython/_compiler/ir_lower.py-7430- ctx.emit(IRInstr("call", None, ["_abi_dict_set", v, key_v, cid_v])) +asmpython/_compiler/ir_lower.py-7431- owner = _resolve_method_owner(ctx, e.func, "__init__") +asmpython/_compiler/ir_lower.py-7432- if owner is not None: +asmpython/_compiler/ir_lower.py-7433- init_args = [v] +asmpython/_compiler/ir_lower.py-7434- for arg in e.args: +asmpython/_compiler/ir_lower.py-7435- init_args.append(_lower_expr(ctx, arg)) +asmpython/_compiler/ir_lower.py-7436- ctx.emit(IRInstr("call", None, [f"{owner}____init__", *init_args])) +asmpython/_compiler/ir_lower.py-7437- return v +asmpython/_compiler/ir_lower.py-7438- +asmpython/_compiler/ir_lower.py-7439- if isinstance(e, A.Call) and e.func in ctx.mctx.ffi_funcs: +asmpython/_compiler/ir_lower.py-7440- # A bound stdlib FFI function (e.g. asmlib.hardware.in_byte/cpuid/ +asmpython/_compiler/ir_lower.py-7441- # disable_interrupts, or a real libm export like math.sqrt): call +asmpython/_compiler/ir_lower.py-7442- # its real c_name symbol, not the asmpython-level name. Argument +asmpython/_compiler/ir_lower.py-7443- # marshaling is exactly what a normal "call" IR op already does +asmpython/_compiler/ir_lower.py-7444- # (the same standard-ABI argument passing _gen_ffi_call does by +asmpython/_compiler/ir_lower.py-7445- # hand in the legacy codegen.py for the same bindings) -- but the +asmpython/_compiler/ir_lower.py-7446- # RESULT type must follow the binding's own declared `ret_type`, +asmpython/_compiler/ir_lower.py-7447- # not be hardcoded I64: hardware.py's bindings are genuinely all +asmpython/_compiler/ir_lower.py-7448- # int, but math.py's aren't (sqrt/sin/cos/... return float, in +asmpython/_compiler/ir_lower.py-7449- # XMM0, not RAX). Was hardcoded I64 unconditionally -- confirmed +asmpython/_compiler/ir_lower.py-7450- # via a real repro (`int(sqrt(49))`): the call's result got typed +asmpython/_compiler/ir_lower.py-7451- # I64 despite the real ABI return coming back in XMM0, so the +asmpython/_compiler/ir_lower.py-7452- # immediately-following `fptosi` (which expects an F64 SOURCE +asmpython/_compiler/ir_lower.py-7453- # location) fed it a value the allocator had placed in a GP +asmpython/_compiler/ir_lower.py-7454- # register, tripping codegen's `_dst_xmm`-style location-kind +asmpython/_compiler/ir_lower.py-7455- # assert (`'RegLoc' object has no attribute 'offset'`). +asmpython/_compiler/ir_lower.py-7456- fn = ctx.mctx.ffi_funcs[e.func] +asmpython/_compiler/ir_lower.py-7457- c_name = getattr(fn, "c_name_windows", None) or fn.c_name +asmpython/_compiler/ir_lower.py-7458- args = [] +asmpython/_compiler/ir_lower.py-7459- for i, a in enumerate(e.args): +asmpython/_compiler/ir_lower.py-7460- av = _lower_expr(ctx, a) +asmpython/_compiler/ir_lower.py-7461- # Coerce an int-typed argument to float when the binding +asmpython/_compiler/ir_lower.py-7462- # declares a float parameter (e.g. `sqrt(49)` -- a bare int +asmpython/_compiler/ir_lower.py-7463- # literal into a `("float",)`-typed binding): without this, +asmpython/_compiler/ir_lower.py-7464- # the raw integer bits get passed through unconverted and +asmpython/_compiler/ir_lower.py-7465- # reinterpreted as a double bit pattern on the callee side, +asmpython/_compiler/ir_lower.py-7466- # producing garbage (confirmed: `sqrt(49)` silently returned +asmpython/_compiler/ir_lower.py-7467- # `0` instead of `7`). No reverse case needed -- a real +asmpython/_compiler/ir_lower.py-7468- # Python float literal/expression passed to an int-typed +-- +asmpython/_compiler/ir_lower.py-9352- def add_resolved(class_name: str, method: str) -> None: +asmpython/_compiler/ir_lower.py-9353- add(_resolve_method_owner_in_sigs(classes_sig, class_name, method), method) +asmpython/_compiler/ir_lower.py-9354- +asmpython/_compiler/ir_lower.py-9355- def add_resolved_virtual(class_name: str, method: str) -> None: +asmpython/_compiler/ir_lower.py-9356- # Walker-side counterpart to ir_lower.py's own `_virtual_dispatch_ +asmpython/_compiler/ir_lower.py-9357- # rows` (used at LOWERING time to emit a runtime __class__-id +asmpython/_compiler/ir_lower.py-9358- # dispatch chain whenever more than one subclass overrides a +asmpython/_compiler/ir_lower.py-9359- # method) -- that function makes lowering correctly CALL every +asmpython/_compiler/ir_lower.py-9360- # subclass override's real symbol, but nothing previously marked +asmpython/_compiler/ir_lower.py-9361- # those overrides reachable for THIS walker, which only ever +asmpython/_compiler/ir_lower.py-9362- # looked upward from the receiver's own static type +asmpython/_compiler/ir_lower.py-9363- # (add_resolved/_resolve_method_owner_in_sigs). Any user class +asmpython/_compiler/ir_lower.py-9364- # that is `class_name` or descends from it and resolves `method` +asmpython/_compiler/ir_lower.py-9365- # anywhere on its own chain needs its owner marked reachable too +asmpython/_compiler/ir_lower.py-9366- # -- not just the statically-resolved owner -- since the +asmpython/_compiler/ir_lower.py-9367- # receiver's runtime __class__ may be any of them. Same +asmpython/_compiler/ir_lower.py-9368- # "lowering fixed, walker not fixed" shape as every other +asmpython/_compiler/ir_lower.py-9369- # dunder/lambda/super() dispatch gap fixed this session; mirrors +asmpython/_compiler/ir_lower.py-9370- # `_virtual_dispatch_rows`'s downward scan but reimplemented +asmpython/_compiler/ir_lower.py-9371- # against the module-level `classes_sig` dict (no live `ctx` / +asmpython/_compiler/ir_lower.py:9372: # `class_ids` exists yet at this walker's point in the pipeline). +asmpython/_compiler/ir_lower.py-9373- add_resolved(class_name, method) +asmpython/_compiler/ir_lower.py-9374- for cname in classes_sig: +asmpython/_compiler/ir_lower.py-9375- seen: set[str] = set() +asmpython/_compiler/ir_lower.py-9376- cur = cname +asmpython/_compiler/ir_lower.py-9377- descends = False +asmpython/_compiler/ir_lower.py-9378- while cur is not None and cur not in seen: +asmpython/_compiler/ir_lower.py-9379- seen.add(cur) +asmpython/_compiler/ir_lower.py-9380- if cur == class_name: +asmpython/_compiler/ir_lower.py-9381- descends = True +asmpython/_compiler/ir_lower.py-9382- break +asmpython/_compiler/ir_lower.py-9383- sig = classes_sig.get(cur) +asmpython/_compiler/ir_lower.py-9384- cur = sig.parent if sig is not None else None +asmpython/_compiler/ir_lower.py-9385- if descends: +asmpython/_compiler/ir_lower.py-9386- add_resolved(cname, method) +asmpython/_compiler/ir_lower.py-9387- +asmpython/_compiler/ir_lower.py-9388- def visit(node) -> None: +asmpython/_compiler/ir_lower.py-9389- if node is None or isinstance(node, (str, int, float, bool)): +asmpython/_compiler/ir_lower.py-9390- return +asmpython/_compiler/ir_lower.py-9391- if isinstance(node, A.MethodCall): +asmpython/_compiler/ir_lower.py-9392- obj_ty = A.expr_type(node.obj) +asmpython/_compiler/ir_lower.py-9393- # `overload` extension: a resolved overload method call's real +asmpython/_compiler/ir_lower.py-9394- # target is its mangled method name (node.method is the +asmpython/_compiler/ir_lower.py-9395- # original, ambiguous, never-actually-emitted bare name -- +asmpython/_compiler/ir_lower.py-9396- # every source-level @overload method was renamed to its own +asmpython/_compiler/ir_lower.py-9397- # mangled name during sema's per-class overload pre-pass). +asmpython/_compiler/ir_lower.py-9398- resolved_ov_m = getattr(node, "resolved_overload_symbol", None) +asmpython/_compiler/ir_lower.py-9399- dispatch_method = resolved_ov_m if resolved_ov_m is not None else node.method +asmpython/_compiler/ir_lower.py-9400- if obj_ty.startswith("instance:"): +asmpython/_compiler/ir_lower.py-9401- add_resolved_virtual(obj_ty.split(":", 1)[1], dispatch_method) +asmpython/_compiler/ir_lower.py-9402- elif obj_ty.startswith("super:"): +asmpython/_compiler/ir_lower.py-9403- # super().method(...): dispatches statically to the base +asmpython/_compiler/ir_lower.py-9404- # class's OWN method (never a subclass override -- see +asmpython/_compiler/ir_lower.py-9405- # ir_lower.py's `super:` MethodCall lowering), so mark +asmpython/_compiler/ir_lower.py-9406- # exactly that resolved owner reachable. Same +asmpython/_compiler/ir_lower.py-9407- # "lowering fixed, walker not fixed" shape as every other +asmpython/_compiler/ir_lower.py-9408- # dunder/lambda dispatch gap this session -- add_resolved +asmpython/_compiler/ir_lower.py-9409- # already handles the None-owner (non-user-class base) +asmpython/_compiler/ir_lower.py-9410- # case as a no-op. +asmpython/_compiler/ir_lower.py-9411- add_resolved(obj_ty.split(":", 1)[1], node.method) +asmpython/_compiler/ir_lower.py-9412- elif obj_ty == "type" and isinstance(node.obj, A.Name): +asmpython/_compiler/ir_lower.py-9413- add_resolved(node.obj.name, node.method) +asmpython/_compiler/ir_lower.py-9414- # `node.obj` (the receiver) is visited (or deliberately +asmpython/_compiler/ir_lower.py-9415- # skipped, for a bare Name) once, uniformly for both +asmpython/_compiler/ir_lower.py-9416- # MethodCall and Attr, in the generic dataclass-field +asmpython/_compiler/ir_lower.py-9417- # recursion below -- see its own comment for why. +-- +asmpython/_compiler/codegen.py-68- "ZeroDivisionError": 6, +asmpython/_compiler/codegen.py-69- "OverflowError": 7, +asmpython/_compiler/codegen.py-70- "LookupError": 8, +asmpython/_compiler/codegen.py-71- "IndexError": 9, +asmpython/_compiler/codegen.py-72- "KeyError": 10, +asmpython/_compiler/codegen.py-73- "NameError": 11, +asmpython/_compiler/codegen.py-74- "AttributeError": 12, +asmpython/_compiler/codegen.py-75- "TypeError": 13, +asmpython/_compiler/codegen.py-76- "ValueError": 14, +asmpython/_compiler/codegen.py-77- "RuntimeError": 15, +asmpython/_compiler/codegen.py-78- "NotImplementedError": 16, +asmpython/_compiler/codegen.py-79- "AssertionError": 17, +asmpython/_compiler/codegen.py-80- "ImportError": 18, +asmpython/_compiler/codegen.py-81- "OSError": 19, +asmpython/_compiler/codegen.py-82- "FileNotFoundError": 20, +asmpython/_compiler/codegen.py-83- "StopIteration": 21, +asmpython/_compiler/codegen.py-84- "IOError": 19, # alias for OSError (same id) +asmpython/_compiler/codegen.py-85-} +asmpython/_compiler/codegen.py-86- +asmpython/_compiler/codegen.py-87-# A bare builtin scalar/container type name used as a value (`{"type": str}`, +asmpython/_compiler/codegen.py:88:# mimicking argparse's `type=str`) -- same RTTI-id trick as class_ids / +asmpython/_compiler/codegen.py-89-# BUILTIN_EXC_IDS: asmpython has no first-class type objects, so this is just +asmpython/_compiler/codegen.py-90-# a stable, unique-per-name placeholder the program never actually inspects. +asmpython/_compiler/codegen.py:91:# Negative so it can never collide with class_ids (which starts at 0). +asmpython/_compiler/codegen.py:92:BUILTIN_TYPE_IDS: dict[str, int] = { +asmpython/_compiler/codegen.py-93- "int": -1, +asmpython/_compiler/codegen.py-94- "float": -2, +asmpython/_compiler/codegen.py-95- "str": -3, +asmpython/_compiler/codegen.py-96- "bool": -4, +asmpython/_compiler/codegen.py-97- "list": -5, +asmpython/_compiler/codegen.py-98- "dict": -6, +asmpython/_compiler/codegen.py-99- "tuple": -7, +asmpython/_compiler/codegen.py-100- "set": -8, +asmpython/_compiler/codegen.py-101-} +asmpython/_compiler/codegen.py-102- +asmpython/_compiler/codegen.py-103- +asmpython/_compiler/codegen.py-104-# --- Function metadata -------------------------------------------------------- +asmpython/_compiler/codegen.py-105- +asmpython/_compiler/codegen.py-106- +asmpython/_compiler/codegen.py-107-@dataclass +asmpython/_compiler/codegen.py-108-class FuncInfo: +asmpython/_compiler/codegen.py-109- name: str +asmpython/_compiler/codegen.py-110- params: list[str] +asmpython/_compiler/codegen.py-111- locals_: dict[str, int] = field( +asmpython/_compiler/codegen.py-112- default_factory=dict +asmpython/_compiler/codegen.py-113- ) # name -> RBP offset (negative) +asmpython/_compiler/codegen.py-114- local_types: dict[str, str] = field( +asmpython/_compiler/codegen.py-115- default_factory=dict +asmpython/_compiler/codegen.py-116- ) # name -> 'int'|'float'|'str'|'list' +asmpython/_compiler/codegen.py-117- frame_size: int = 0 # bytes to subtract from RSP +asmpython/_compiler/codegen.py-118- # Default value expressions, one per param (None for required). +asmpython/_compiler/codegen.py-119- defaults: list = field(default_factory=list) +asmpython/_compiler/codegen.py-120- # Running RBP offset (negative) used while collecting locals, and whether +asmpython/_compiler/codegen.py-121- # this is the synthetic module-entry frame. Carried on the FuncInfo so the +asmpython/_compiler/codegen.py-122- # local-collection helpers can be plain instance methods (no closures / +asmpython/_compiler/codegen.py-123- # nonlocal) — a self-host requirement. +asmpython/_compiler/codegen.py-124- offset: int = 0 +asmpython/_compiler/codegen.py-125- is_main: bool = False +asmpython/_compiler/codegen.py-126- # Names declared `global` in this function: skip frame-slot allocation and +asmpython/_compiler/codegen.py-127- # access them via the module-global .bss slot instead. +asmpython/_compiler/codegen.py-128- global_names: set = field(default_factory=set) +asmpython/_compiler/codegen.py-129- # Nonlocal vars: maps var name -> box-ptr slot name (__nl_box_). +asmpython/_compiler/codegen.py-130- # Reads/writes to these names go through one pointer indirection. +asmpython/_compiler/codegen.py-131- nonlocal_boxes: dict = field(default_factory=dict) +asmpython/_compiler/codegen.py-132- # True when the function's declared return annotation is `-> float`. +asmpython/_compiler/codegen.py-133- # Lets `return ` promote a non-float result (e.g. an `any`/`int` +asmpython/_compiler/codegen.py-134- # element read out of an unannotated `list`) to xmm0, since callers of a +asmpython/_compiler/codegen.py-135- # float-returning function read the result from xmm0. +asmpython/_compiler/codegen.py-136- ret_is_float: bool = False +asmpython/_compiler/codegen.py-137- +-- +asmpython/_compiler/codegen.py-288- and isinstance(s.target, str) +asmpython/_compiler/codegen.py-289- and s.target not in bound_in_frame +asmpython/_compiler/codegen.py-290- ): +asmpython/_compiler/codegen.py-291- self.global_vars[s.target] = A.expr_type(s.value) +asmpython/_compiler/codegen.py-292- elif ( +asmpython/_compiler/codegen.py-293- isinstance(s, A.ConstDecl) +asmpython/_compiler/codegen.py-294- and s.name not in bound_in_frame +asmpython/_compiler/codegen.py-295- ): +asmpython/_compiler/codegen.py-296- # const NAME = value is module-scope-only (enforced by the +asmpython/_compiler/codegen.py-297- # parser), so it's always a global slot, never a frame local +asmpython/_compiler/codegen.py-298- # -- same collection as a plain top-level A.Assign. +asmpython/_compiler/codegen.py-299- self.global_vars[s.name] = A.expr_type(s.value) +asmpython/_compiler/codegen.py-300- elif isinstance(s, A.If): +asmpython/_compiler/codegen.py-301- self._collect_if_globals(s, bound_in_frame, self.global_vars) +asmpython/_compiler/codegen.py-302- # RTTI: each user class gets a small integer id. Instances are tagged +asmpython/_compiler/codegen.py-303- # with their id (a hidden `__class__` dict entry) at construction, and +asmpython/_compiler/codegen.py-304- # isinstance walks the `__class_parents` table to honour inheritance. +asmpython/_compiler/codegen.py-305- # Built with an explicit loop rather than a dict comprehension over +asmpython/_compiler/codegen.py-306- # enumerate(...) so this stays self-compilable (asmpython comprehensions +asmpython/_compiler/codegen.py-307- # don't take enumerate() iterables or tuple targets). +asmpython/_compiler/codegen.py:308: self.class_ids: dict[str, int] = {} +asmpython/_compiler/codegen.py-309- cid = 0 +asmpython/_compiler/codegen.py-310- for cls in mod.classes: +asmpython/_compiler/codegen.py:311: self.class_ids[cls.name] = cid +asmpython/_compiler/codegen.py-312- cid += 1 +asmpython/_compiler/codegen.py-313- # Lazily-built .rodata table mapping each class id above to a +asmpython/_compiler/codegen.py-314- # "" string, for type(instance) (see +asmpython/_compiler/codegen.py-315- # _type_name_table_label). None until first requested. +asmpython/_compiler/codegen.py-316- self.type_name_table: list[str] | None = None +asmpython/_compiler/codegen.py-317- # Class-level variables that act as static constants: `class C: x = 5`. +asmpython/_compiler/codegen.py-318- # Each maps "." -> (label, default_expr). Emitted as bss +asmpython/_compiler/codegen.py-319- # globals, initialized at startup, and read/written via ClassName.attr +asmpython/_compiler/codegen.py-320- # and cls.attr. Only plain class bodies (not @dataclass, whose class +asmpython/_compiler/codegen.py-321- # vars are per-instance fields) contribute, and only literal/simple +asmpython/_compiler/codegen.py-322- # defaults that the startup initializer can evaluate. +asmpython/_compiler/codegen.py-323- self.class_var_labels: dict[str, str] = {} +asmpython/_compiler/codegen.py-324- self.class_var_defaults: list = [] # (label, default_expr) in emit order +asmpython/_compiler/codegen.py-325- for cls in mod.classes: +asmpython/_compiler/codegen.py-326- if getattr(cls, "is_dataclass", False): +asmpython/_compiler/codegen.py-327- continue +asmpython/_compiler/codegen.py-328- for cv in getattr(cls, "class_vars", []) or []: +asmpython/_compiler/codegen.py-329- cvname, _annot, cvdefault = cv +asmpython/_compiler/codegen.py-330- if cvdefault is None: +asmpython/_compiler/codegen.py-331- continue +asmpython/_compiler/codegen.py-332- label = f"__cv_{cls.name}__{cvname}" +asmpython/_compiler/codegen.py-333- self.class_var_labels[f"{cls.name}.{cvname}"] = label +asmpython/_compiler/codegen.py-334- self.class_var_defaults.append((label, cvdefault)) +asmpython/_compiler/codegen.py-335- # import_binary()/.imported dynamic-loading: map each handle variable +asmpython/_compiler/codegen.py-336- # name to the list of (func_name, FuncDef) decorated `@handle.imported` +asmpython/_compiler/codegen.py-337- # for it. A handle's import_binary(path) call site resolves every +asmpython/_compiler/codegen.py-338- # function in its list via GetProcAddress/dlsym immediately, storing +asmpython/_compiler/codegen.py-339- # each pointer keyed by name on the handle instance — see +asmpython/_compiler/codegen.py-340- # _gen_constructor-adjacent dynamic-import codegen. +asmpython/_compiler/codegen.py-341- # +asmpython/_compiler/codegen.py-342- # Also scans class methods (mod.classes[*].methods), not just +asmpython/_compiler/codegen.py-343- # top-level mod.funcs: a class can wrap a set of GL bindings behind +asmpython/_compiler/codegen.py-344- # its own API (e.g. pugtk's GLRenderer3D) instead of forcing every +asmpython/_compiler/codegen.py-345- # caller to hand-declare the same ~20 top-level @glfns.imported +asmpython/_compiler/codegen.py-346- # stubs. `handle` (the `@.imported` decorator's receiver) +asmpython/_compiler/codegen.py-347- # still has to be a name resolvable where the decorator itself is +asmpython/_compiler/codegen.py-348- # evaluated -- a module-level `glfns = gl_import()`, since Python +asmpython/_compiler/codegen.py-349- # evaluates a class's decorators once, at class-definition time, +asmpython/_compiler/codegen.py-350- # not per-instance. Methods compile with `self` as their first +asmpython/_compiler/codegen.py-351- # parameter; _gen_dynamic_call's plain_params helper below strips +asmpython/_compiler/codegen.py-352- # it before marshalling so `self` is never sent to the GL call. +asmpython/_compiler/codegen.py-353- self.imported_funcs: dict[str, list[tuple[str, A.FuncDef]]] = {} +asmpython/_compiler/codegen.py-354- for f in mod.funcs: +asmpython/_compiler/codegen.py-355- for deco in f.decorators: +asmpython/_compiler/codegen.py-356- if deco.endswith(".imported"): +-- +asmpython/_compiler/codegen.py-635- +asmpython/_compiler/codegen.py-636- def intern_string(self, s: str) -> tuple[str, int]: +asmpython/_compiler/codegen.py-637- """Add a string to .data, return (label, byte_length). +asmpython/_compiler/codegen.py-638- +asmpython/_compiler/codegen.py-639- Emits the string as a comma-separated list of byte values for NASM. +asmpython/_compiler/codegen.py-640- We walk the characters and take `ord(ch)` rather than `s.encode()` so +asmpython/_compiler/codegen.py-641- this stays self-compilable (asmpython has no bytes type). For the ASCII +asmpython/_compiler/codegen.py-642- source the compiler emits this is exact; `ord` on a 1-char asmpython str +asmpython/_compiler/codegen.py-643- already yields its byte value. +asmpython/_compiler/codegen.py-644- """ +asmpython/_compiler/codegen.py-645- label = f"str_{len(self.strings)}" +asmpython/_compiler/codegen.py-646- parts: list = [] +asmpython/_compiler/codegen.py-647- for ch in s: +asmpython/_compiler/codegen.py-648- parts.append(str(ord(ch))) +asmpython/_compiler/codegen.py-649- body = ",".join(parts) if parts else "0" +asmpython/_compiler/codegen.py-650- self.strings.append((label, body)) +asmpython/_compiler/codegen.py-651- return label, len(parts) +asmpython/_compiler/codegen.py-652- +asmpython/_compiler/codegen.py-653- def _type_name_table_label(self) -> str: +asmpython/_compiler/codegen.py-654- """Lazily build a .rodata table mapping each user class's RTTI id +asmpython/_compiler/codegen.py:655: (see self.class_ids) to a "" string, so +asmpython/_compiler/codegen.py-656- type(instance) can index into it by runtime class id.""" +asmpython/_compiler/codegen.py-657- if self.type_name_table is None: +asmpython/_compiler/codegen.py-658- table = [] +asmpython/_compiler/codegen.py-659- _tnt_i = 0 +asmpython/_compiler/codegen.py:660: while _tnt_i < len(self.class_ids): +asmpython/_compiler/codegen.py-661- table.append("") +asmpython/_compiler/codegen.py-662- _tnt_i += 1 +asmpython/_compiler/codegen.py:663: for name, cid in self.class_ids.items(): +asmpython/_compiler/codegen.py-664- label, _ = self.intern_string(f"") +asmpython/_compiler/codegen.py-665- table[cid] = label +asmpython/_compiler/codegen.py-666- self.type_name_table = table +asmpython/_compiler/codegen.py-667- return "__type_name_table" +asmpython/_compiler/codegen.py-668- +asmpython/_compiler/codegen.py-669- # ---- driver ------------------------------------------------------------- +asmpython/_compiler/codegen.py-670- +asmpython/_compiler/codegen.py-671- def generate(self) -> str: +asmpython/_compiler/codegen.py-672- self.emit(f"; asmpython generated for target = {self.target_name}") +asmpython/_compiler/codegen.py-673- self.emit("BITS 64") +asmpython/_compiler/codegen.py-674- self.emit("default rel") +asmpython/_compiler/codegen.py-675- before = len(self.lines) +asmpython/_compiler/codegen.py-676- self.emit_externs() +asmpython/_compiler/codegen.py-677- # Avoid duplicate `extern foo` declarations: the target subclass +asmpython/_compiler/codegen.py-678- # already emits some. Collect their symbol names (the last token of each +asmpython/_compiler/codegen.py-679- # `extern ...` line). Built as a list with an explicit loop rather than +asmpython/_compiler/codegen.py-680- # a set comprehension so this stays self-compilable (asmpython has no +asmpython/_compiler/codegen.py-681- # set runtime); `already` is only used for membership below. +asmpython/_compiler/codegen.py-682- already: list = [] +asmpython/_compiler/codegen.py-683- for line in self.lines[before:]: +asmpython/_compiler/codegen.py-684- stripped = line.strip() +asmpython/_compiler/codegen.py-685- if stripped.startswith("extern"): +asmpython/_compiler/codegen.py-686- already.append(stripped.split()[-1]) +asmpython/_compiler/codegen.py-687- # Symbols defined inline by emit_asmlib_runtime must not also be +asmpython/_compiler/codegen.py-688- # declared `extern` — that would conflict with their label definition. +asmpython/_compiler/codegen.py-689- inline = self._asmlib_inline_syms() +asmpython/_compiler/codegen.py-690- for sym in sorted(self.ffi_externs): +asmpython/_compiler/codegen.py-691- if sym not in already and sym not in inline: +asmpython/_compiler/codegen.py-692- self.emit(f"extern {sym}") +asmpython/_compiler/codegen.py-693- self.emit(self.section_text) +asmpython/_compiler/codegen.py-694- self.emit_entry() +asmpython/_compiler/codegen.py-695- # Whole-program compilation (program.py's load_program) merges EVERY +asmpython/_compiler/codegen.py-696- # function/method from every imported stdlib module unconditionally, +asmpython/_compiler/codegen.py-697- # whether or not the program actually calls it -- sema.py tolerates +asmpython/_compiler/codegen.py-698- # (doesn't hard-fail on) a semantic error in one of these if it's +asmpython/_compiler/codegen.py-699- # unreachable, so a name that's merged-but-broken can reach this +asmpython/_compiler/codegen.py-700- # point at all. The x86-64 backend (ir_lower.py) already prunes these +asmpython/_compiler/codegen.py-701- # via `_reachable_callables` before lowering; do the same filtering +asmpython/_compiler/codegen.py-702- # here so the legacy backend doesn't try to emit a call into a +asmpython/_compiler/codegen.py-703- # skipped, potentially-half-checked body (e.g. a reference to the +asmpython/_compiler/codegen.py-704- # undefined symbol `property`, from collections.py's `namedtuple()`) +asmpython/_compiler/codegen.py-705- # for code that never runs. Reused as-is: it's a pure function of the +asmpython/_compiler/codegen.py-706- # already-fully-typed `self.mod` with no IR-specific state. +asmpython/_compiler/codegen.py-707- from .ir_lower import _reachable_callables +asmpython/_compiler/codegen.py-708- +-- +asmpython/_compiler/codegen.py-2214- self._cl_define(info, f"__dm_new_{id(expr)}") +asmpython/_compiler/codegen.py-2215- if obj_t == "set" and expr.method in ( +asmpython/_compiler/codegen.py-2216- "union", +asmpython/_compiler/codegen.py-2217- "intersection", +asmpython/_compiler/codegen.py-2218- "difference", +asmpython/_compiler/codegen.py-2219- ): +asmpython/_compiler/codegen.py-2220- self._cl_define(info, f"__sm_other_{id(expr)}") +asmpython/_compiler/codegen.py-2221- self._cl_define(info, f"__sm_new_{id(expr)}") +asmpython/_compiler/codegen.py-2222- self._cl_define(info, f"__sm_keys_{id(expr)}") +asmpython/_compiler/codegen.py-2223- self._cl_define(info, f"__sm_idx_{id(expr)}") +asmpython/_compiler/codegen.py-2224- self._cl_define(info, f"__sm_key_{id(expr)}") +asmpython/_compiler/codegen.py-2225- if obj_t.startswith("instance:"): +asmpython/_compiler/codegen.py-2226- self._cl_define(info, f"__callself_{id(expr)}") +asmpython/_compiler/codegen.py-2227- # Module-qualified call to a merged project function +asmpython/_compiler/codegen.py-2228- # (`A.expr_type(x)`) lowers to a plain call, so it needs the +asmpython/_compiler/codegen.py-2229- # same per-arg slots as an instance/super call. +asmpython/_compiler/codegen.py-2230- is_module_fn = obj_t == "module" and expr.method in self.funcs +asmpython/_compiler/codegen.py-2231- # `ClassName.staticmethod(args)` / `.classmethod(args)`: a plain +asmpython/_compiler/codegen.py-2232- # call to the method symbol, so it needs per-arg slots too. +asmpython/_compiler/codegen.py-2233- is_class_static = ( +asmpython/_compiler/codegen.py:2234: isinstance(expr.obj, A.Name) and expr.obj.name in self.class_ids +asmpython/_compiler/codegen.py-2235- ) +asmpython/_compiler/codegen.py-2236- if ( +asmpython/_compiler/codegen.py-2237- obj_t.startswith("instance:") +asmpython/_compiler/codegen.py-2238- or obj_t.startswith("super:") +asmpython/_compiler/codegen.py-2239- or is_module_fn +asmpython/_compiler/codegen.py-2240- or is_class_static +asmpython/_compiler/codegen.py-2241- ): +asmpython/_compiler/codegen.py-2242- for k in range(len(expr.args)): +asmpython/_compiler/codegen.py-2243- self._cl_define(info, f"__callarg_{id(expr)}_{k}") +asmpython/_compiler/codegen.py-2244- self._cl_walk_expr(info, expr.obj) +asmpython/_compiler/codegen.py-2245- for a in expr.args: +asmpython/_compiler/codegen.py-2246- self._cl_walk_expr(info, a) +asmpython/_compiler/codegen.py-2247- for _kn, kv in getattr(expr, "kwargs", []) or []: +asmpython/_compiler/codegen.py-2248- self._cl_walk_expr(info, kv) +asmpython/_compiler/codegen.py-2249- elif isinstance(expr, A.Subscript): +asmpython/_compiler/codegen.py-2250- if isinstance(expr.index, A.Slice): +asmpython/_compiler/codegen.py-2251- # slice needs scratch slots for obj/start (always) plus +asmpython/_compiler/codegen.py-2252- # stop/step when the step-aware path is used. +asmpython/_compiler/codegen.py-2253- self._cl_define(info, f"__strsl_obj_{id(expr)}") +asmpython/_compiler/codegen.py-2254- self._cl_define(info, f"__strsl_start_{id(expr)}") +asmpython/_compiler/codegen.py-2255- if expr.index.step is not None: +asmpython/_compiler/codegen.py-2256- self._cl_define(info, f"__strsl_stop_{id(expr)}") +asmpython/_compiler/codegen.py-2257- self._cl_define(info, f"__strsl_step_{id(expr)}") +asmpython/_compiler/codegen.py-2258- # List slicing dispatches through a different helper; the +asmpython/_compiler/codegen.py-2259- # codegen path uses its own pair of slots. +asmpython/_compiler/codegen.py-2260- if A.expr_type(expr.obj) == "list": +asmpython/_compiler/codegen.py-2261- self._cl_define(info, f"__lstsl_obj_{id(expr)}") +asmpython/_compiler/codegen.py-2262- self._cl_define(info, f"__lstsl_start_{id(expr)}") +asmpython/_compiler/codegen.py-2263- if expr.index.step is not None: +asmpython/_compiler/codegen.py-2264- self._cl_define(info, f"__lstsl_step_{id(expr)}") +asmpython/_compiler/codegen.py-2265- self._cl_walk_expr(info, expr.obj) +asmpython/_compiler/codegen.py-2266- if expr.index.start is not None: +asmpython/_compiler/codegen.py-2267- self._cl_walk_expr(info, expr.index.start) +asmpython/_compiler/codegen.py-2268- if expr.index.stop is not None: +asmpython/_compiler/codegen.py-2269- self._cl_walk_expr(info, expr.index.stop) +asmpython/_compiler/codegen.py-2270- if expr.index.step is not None: +asmpython/_compiler/codegen.py-2271- self._cl_walk_expr(info, expr.index.step) +asmpython/_compiler/codegen.py-2272- elif A.expr_type(expr.obj) == "str": +asmpython/_compiler/codegen.py-2273- self._cl_define(info, f"__stridx_{id(expr)}") +asmpython/_compiler/codegen.py-2274- self._cl_walk_expr(info, expr.obj) +asmpython/_compiler/codegen.py-2275- self._cl_walk_expr(info, expr.index) +asmpython/_compiler/codegen.py-2276- elif getattr(expr, "_getitem_class", None) is not None: +asmpython/_compiler/codegen.py-2277- # Instance __getitem__: synthesized method call needs receiver +asmpython/_compiler/codegen.py-2278- # slot and one arg slot (the index). We park them under +asmpython/_compiler/codegen.py-2279- # __gi_self_ / __gi_arg_ so they don't collide with +-- +asmpython/_compiler/codegen.py-4010- self._gen_const_load(self.ffi_consts[expr.name]) +asmpython/_compiler/codegen.py-4011- return +asmpython/_compiler/codegen.py-4012- # Module dunders the runtime provides as string constants. A +asmpython/_compiler/codegen.py-4013- # compiled program is its own entry point, so __name__ is +asmpython/_compiler/codegen.py-4014- # "__main__"; __file__ is the entry source file's resolved path +asmpython/_compiler/codegen.py-4015- # (threaded in from the driver), or "" if compiling from a +asmpython/_compiler/codegen.py-4016- # string with no real file. +asmpython/_compiler/codegen.py-4017- if expr.name == "__name__": +asmpython/_compiler/codegen.py-4018- label, _ = self.intern_string("__main__") +asmpython/_compiler/codegen.py-4019- self.emitf(f"lea rax, [{label}]") +asmpython/_compiler/codegen.py-4020- return +asmpython/_compiler/codegen.py-4021- if expr.name == "__file__": +asmpython/_compiler/codegen.py-4022- label, _ = self.intern_string(self.entry_path or "") +asmpython/_compiler/codegen.py-4023- self.emitf(f"lea rax, [{label}]") +asmpython/_compiler/codegen.py-4024- return +asmpython/_compiler/codegen.py-4025- # A bare class name used as a value (`Stmt = Assign | AugAssign`, +asmpython/_compiler/codegen.py-4026- # `isinstance(x, Token)`, storing a class object). asmpython has no +asmpython/_compiler/codegen.py-4027- # first-class type objects; we load the class's RTTI id so the value +asmpython/_compiler/codegen.py-4028- # is stable and unique per class. (These appear in type-alias +asmpython/_compiler/codegen.py-4029- # expressions the compiled program never actually inspects.) +asmpython/_compiler/codegen.py:4030: if expr.name in self.class_ids: +asmpython/_compiler/codegen.py:4031: self.emitf(f"mov rax, {self.class_ids[expr.name]}") +asmpython/_compiler/codegen.py-4032- return +asmpython/_compiler/codegen.py:4033: if expr.name in BUILTIN_TYPE_IDS: +asmpython/_compiler/codegen.py-4034- # A bare builtin type name as a value (`{"type": str}`, +asmpython/_compiler/codegen.py-4035- # mimicking argparse's `type=str`) -- same RTTI-id trick, +asmpython/_compiler/codegen.py:4036: # see BUILTIN_TYPE_IDS. +asmpython/_compiler/codegen.py:4037: self.emitf(f"mov rax, {BUILTIN_TYPE_IDS[expr.name]}") +asmpython/_compiler/codegen.py-4038- return +asmpython/_compiler/codegen.py-4039- if expr.name in BUILTIN_EXCEPTIONS: +asmpython/_compiler/codegen.py-4040- # A bare builtin-exception name as a value (`raise X` without +asmpython/_compiler/codegen.py-4041- # parens, storing the class): exceptions are message strings, +asmpython/_compiler/codegen.py-4042- # so the class-as-value is its interned name. +asmpython/_compiler/codegen.py-4043- lbl, _ = self.intern_string(expr.name) +asmpython/_compiler/codegen.py-4044- self.emitf(f"lea rax, [{lbl}]") +asmpython/_compiler/codegen.py-4045- return +asmpython/_compiler/codegen.py-4046- if expr.name not in info.locals_ and expr.name not in self.global_vars: +asmpython/_compiler/codegen.py-4047- # A bare reference to a top-level function (not a call): used +asmpython/_compiler/codegen.py-4048- # as a callback value, e.g. `atexit.register(my_handler, ...)` +asmpython/_compiler/codegen.py-4049- # or `signal.signal(SIGINT, handler)`. Evaluate to the +asmpython/_compiler/codegen.py-4050- # function's address so it can be stored in an int slot and +asmpython/_compiler/codegen.py-4051- # called indirectly later. +asmpython/_compiler/codegen.py-4052- if any(f.name == expr.name for f in self.mod.funcs): +asmpython/_compiler/codegen.py-4053- self.emitf(f"lea rax, [rel {self._user_symbol(expr.name)}]") +asmpython/_compiler/codegen.py-4054- return +asmpython/_compiler/codegen.py-4055- # A module name (from `import X`) isn't a real heap variable — +asmpython/_compiler/codegen.py-4056- # represent it as null (0). Attribute access on it falls through +asmpython/_compiler/codegen.py-4057- # to the lenient module/any path in _gen_attr. +asmpython/_compiler/codegen.py-4058- if ( +asmpython/_compiler/codegen.py-4059- expr.inferred_type in ("module", "any") +asmpython/_compiler/codegen.py-4060- or expr.name in self.mod.imported_modules +asmpython/_compiler/codegen.py-4061- ): +asmpython/_compiler/codegen.py-4062- self.emitf("xor rax, rax") +asmpython/_compiler/codegen.py-4063- return +asmpython/_compiler/codegen.py-4064- raise NameError(f"undefined variable {expr.name}") +asmpython/_compiler/codegen.py-4065- mem = self._var_mem(expr.name, info) +asmpython/_compiler/codegen.py-4066- ty = self._var_type(expr.name, info) +asmpython/_compiler/codegen.py-4067- if expr.name in info.nonlocal_boxes: +asmpython/_compiler/codegen.py-4068- # Nonlocal: param slot holds a box ptr; deref to get the value. +asmpython/_compiler/codegen.py-4069- self.emitf(f"mov rax, {mem}", "mov rax, [rax]") +asmpython/_compiler/codegen.py-4070- elif ty == "float": +asmpython/_compiler/codegen.py-4071- self.emitf(f"movsd xmm0, {mem}") +asmpython/_compiler/codegen.py-4072- else: +asmpython/_compiler/codegen.py-4073- self.emitf(f"mov rax, {mem}") diff --git a/tests/cases/468_provider_type_runtime.py b/tests/cases/468_provider_type_runtime.py new file mode 100644 index 000000000..240beb0a2 --- /dev/null +++ b/tests/cases/468_provider_type_runtime.py @@ -0,0 +1,44 @@ +# expect: +# True +# False +# True +# True +# True +# True +# Finite class tuples must lower without a dynamic metatype runtime. +# Dynamic values must preserve string behavior. +# Python-boolean verification generation 1. + +class Provider: + runtime_realms = ("server", "client") + + @classmethod + def supports_realm(cls, realm: str) -> bool: + return realm in cls.runtime_realms + + +class ServerProvider(Provider): + runtime_realms = ("server",) + + +class ClientProvider(Provider): + runtime_realms = ("client",) + + +class StaticProbe: + @staticmethod + def contains_server(realm: str) -> bool: + return realm in ("server",) + + +def starts_with_somnia(value) -> bool: + return value.startswith("somnia.") + + +provider_types = (ServerProvider, ClientProvider) +print(provider_types[0].supports_realm("server")) +print(provider_types[1].supports_realm("server")) +print(starts_with_somnia("somnia.Scene")) +print("Provider" in str(Provider)) +print("server" in ("server",)) +print(StaticProbe.contains_server("server")) diff --git a/tests/cases/469_property_generator_string.py b/tests/cases/469_property_generator_string.py new file mode 100644 index 000000000..061b5aefa --- /dev/null +++ b/tests/cases/469_property_generator_string.py @@ -0,0 +1,47 @@ +# expect: +# 2 +# somnia.Root +# Property-flow verification generation 3. + + +class Registry: + def __init__(self): + self.names = { + "Root": "somnia.Root", + "Child": "somnia.Child", + } + + def type_name(self, value): + return self.names.get( + value.name, + getattr(value, "fallback_name", value.name), + ) + + +REGISTRY = Registry() + + +class Node: + def __init__(self, name): + self.name = name + self.children = [] + + @property + def type_name(self): + return REGISTRY.type_name(self) + + def walk(self): + yield self + for child in self.children: + yield from child.walk() + + +root = Node("Root") +root.children.append(Node("Child")) +values = [ + obj.type_name + for obj in root.walk() + if obj.type_name.startswith("somnia.") +] +print(len(values)) +print(values[0]) diff --git a/tests/cases/470_property_string_only.py b/tests/cases/470_property_string_only.py new file mode 100644 index 000000000..e963141d1 --- /dev/null +++ b/tests/cases/470_property_string_only.py @@ -0,0 +1,27 @@ +# expect: +# somnia.Root +# Method/property isolation generation 1. + + +class Registry: + def __init__(self): + self.names = {"Root": "somnia.Root"} + + def type_name(self, value): + return self.names.get(value.name, value.name) + + +REGISTRY = Registry() + + +class Node: + def __init__(self, name): + self.name = name + + @property + def type_name(self): + return REGISTRY.type_name(self) + + +root = Node("Root") +print(root.type_name) diff --git a/tests/cases/471_recursive_generator_only.py b/tests/cases/471_recursive_generator_only.py new file mode 100644 index 000000000..4c2f87e79 --- /dev/null +++ b/tests/cases/471_recursive_generator_only.py @@ -0,0 +1,18 @@ +# expect: +# 2 +# Generator isolation generation 4. + + +class Node: + def __init__(self): + self.children = [] + + def walk(self): + yield self + for child in self.children: + yield from child.walk() + + +root = Node() +root.children.append(Node()) +print(len(root.walk())) diff --git a/tests/cases/472_property_comprehension_only.py b/tests/cases/472_property_comprehension_only.py new file mode 100644 index 000000000..9101f3318 --- /dev/null +++ b/tests/cases/472_property_comprehension_only.py @@ -0,0 +1,18 @@ +# expect: +# 2 +# somnia.Root + + +class Node: + def __init__(self, type_name): + self.type_name = type_name + + +nodes = [Node("somnia.Root"), Node("somnia.Child")] +values = [ + node.type_name + for node in nodes + if node.type_name.startswith("somnia.") +] +print(len(values)) +print(values[0]) diff --git a/tests/cases/473_dict_method_string.py b/tests/cases/473_dict_method_string.py new file mode 100644 index 000000000..74e69320c --- /dev/null +++ b/tests/cases/473_dict_method_string.py @@ -0,0 +1,20 @@ +# expect: +# somnia.Root +# Dict-method diagnostic generation 1. + + +class Value: + def __init__(self, name): + self.name = name + + +class Registry: + def __init__(self): + self.names = {"Root": "somnia.Root"} + + def type_name(self, value): + return self.names.get(value.name, value.name) + + +registry = Registry() +print(registry.type_name(Value("Root"))) diff --git a/tests/cases/474_literal_property_string.py b/tests/cases/474_literal_property_string.py new file mode 100644 index 000000000..e6599d0ec --- /dev/null +++ b/tests/cases/474_literal_property_string.py @@ -0,0 +1,11 @@ +# expect: +# somnia.Root + + +class Node: + @property + def type_name(self): + return "somnia.Root" + + +print(Node().type_name) diff --git a/tests/cases/475_simple_generator_self.py b/tests/cases/475_simple_generator_self.py new file mode 100644 index 000000000..007730a57 --- /dev/null +++ b/tests/cases/475_simple_generator_self.py @@ -0,0 +1,10 @@ +# expect: +# 1 + + +class Node: + def walk(self): + yield self + + +print(len(Node().walk())) diff --git a/tests/cases/476_generator_child_iteration.py b/tests/cases/476_generator_child_iteration.py new file mode 100644 index 000000000..b6354fbd4 --- /dev/null +++ b/tests/cases/476_generator_child_iteration.py @@ -0,0 +1,17 @@ +# expect: +# 2 + + +class Node: + def __init__(self): + self.children = [] + + def walk(self): + yield self + for child in self.children: + yield child + + +root = Node() +root.children.append(Node()) +print(len(root.walk()))