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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from contextlib import suppress
from dataclasses import replace
from types import ModuleType
from typing import TYPE_CHECKING, Any, NoReturn
from typing import TYPE_CHECKING, Any, NoReturn, cast

from guppylang_internals.ast_util import (
AstNode,
Expand Down Expand Up @@ -88,11 +88,11 @@
UnaryOperatorNotDefinedError,
WrongNumberOfArgsError,
)
from guppylang_internals.definition.common import Definition
from guppylang_internals.definition.common import Definition, ParsedDef
from guppylang_internals.definition.parameter import ParamDef
from guppylang_internals.definition.ty import TypeDef
from guppylang_internals.definition.value import CallableDef, ValueDef
from guppylang_internals.engine import ENGINE
from guppylang_internals.engine import DEF_STORE, ENGINE
from guppylang_internals.error import (
GuppyComptimeError,
GuppyError,
Expand Down Expand Up @@ -580,7 +580,23 @@ def visit_Attribute(self, node: ast.Attribute) -> tuple[ast.expr, Type]:
# Name can be a EnumDef only if it is in a attribute access, thus we
# manually need to call the helper instead of relying on the standard
# visit_Name (that is called through synthesize)

# visit node for case of staticmethods on a non-instantiated type
ty = get_type_opt(node.value)
if node.value.id in self.ctx.globals:
defn = cast("ParsedDef", self.ctx.globals[node.value.id])
if not isinstance(defn, PythonObject):
Comment on lines +587 to +588
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
defn = cast("ParsedDef", self.ctx.globals[node.value.id])
if not isinstance(defn, PythonObject):
defn_or_python_object = self.ctx.globals[node.value.id]
if isinstance(defn_or_python_object, TypeDef):

ty_def = ENGINE.parsed[defn.id]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should already be ParsedDef?

if (
node.attr in DEF_STORE.type_members[ty_def.id]
and isinstance(ty_def, TypeDef)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isinstance(ty_def, TypeDef) could be removed if you go with the refactor above

and (func := ENGINE.get_instance_func(ty_def, node.attr))
and DEF_STORE.type_members[ty_def.id][node.attr].is_static
Copy link
Copy Markdown
Collaborator

@mark-koch mark-koch Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need this static check? Imo type.func would also be fine if func is not static, similar to Python:

x = int.__add__(1, 2)

):
return with_loc(
node, GlobalName(id=node.attr, def_id=func.id)
), func.ty
Comment on lines +586 to +598
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this fail when the thing that we are accessing on is not in globals? Can this even happen? I am confused what this does, and what all the if clauses mean. Perhaps @mark-koch has thoughts on this.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could definitely be cleaner / wrapped up into a function.
Also the cast / isinstances were also a result of my fight with mypy due to my lack of experience with the compiler types.
Essentially I was going for, "if this is a type that has has a type_member of this name and it's static, then return the staticmethod"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused in particular about the if not isinstance(defn, PythonObject):. What does it do?

Also, I guess ENGINE.get_instance_func(...) is not necessarily the right name anymore.

Let us wait for a comment from Mark on this one.


if ty is None:
node.value, ty = self._check_name_id(
node.value.id, node.value, allow_enum=True
Expand Down Expand Up @@ -642,16 +658,28 @@ def visit_Attribute(self, node: ast.Attribute) -> tuple[ast.expr, Type]:

def _check_method(
self, ty: Type, node: ast.Attribute
) -> tuple[PartialApply, FunctionType] | None:
) -> tuple[ast.expr, FunctionType] | None:
"""Helper method to check if an attribute access corresponds to a method call"""
if func := ENGINE.get_instance_func(ty, node.attr):
name = with_type(
func.ty, with_loc(node, GlobalName(id=func.name, def_id=func.id))
)
# Make a closure by partially applying the `self` argument
# TODO: Try to infer some type args based on `self`
result_ty = FunctionType(func.ty.inputs[1:], func.ty.output, func.ty.params)
return with_loc(node, PartialApply(func=name, args=[node.value])), result_ty
ty_id = DEF_STORE.type_member_parents[func.id]

if (
impl_def := DEF_STORE.type_members[ty_id].get(node.attr)
) and impl_def.is_static:
Comment on lines +669 to +671
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be clearer if we introduced a new ENGINE.get_type_member function that returns the TypeMember:

if member := ENGINE.get_type_member(ty, node.attr):
    if member.is_static:
        ...
    else:
        ...

# if this is a staticmethod do not partially apply `self`
return with_loc(node, GlobalName(id=node.attr, def_id=func.id)), func.ty
else:
# Make a closure by partially applying the `self` argument
# TODO: Try to infer some type args based on `self`
result_ty = FunctionType(
func.ty.inputs[1:], func.ty.output, func.ty.params
)
return with_loc(
node, PartialApply(func=name, args=[node.value])
), result_ty
else:
return None

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,19 +317,22 @@ def check_signature(
param = parse_parameter(param_node, i, globals, param_var_mapping)
param_var_mapping[param.name] = param

is_static = False
# Figure out if this is a method
self_defn: TypeDef | None = None
if def_id is not None and def_id in DEF_STORE.type_member_parents:
self_def_id = DEF_STORE.type_member_parents[def_id]
self_defn = cast("TypeDef", ENGINE.get_checked(self_def_id, mono_args=()))
assert isinstance(self_defn, TypeDef)
# Figure out if this is a staticmethod
is_static = DEF_STORE.type_members[self_def_id][func_def.name].is_static

inputs = []
ctx = TypeParsingCtx(globals, param_var_mapping, allow_free_vars=True)
for i, inp in enumerate(func_def.args.args):
# Special handling for `self` arguments. Note that `__new__` is excluded here
# since it's not a method so doesn't take `self`.
if self_defn and i == 0 and func_def.name != "__new__":
# Special handling for `self` arguments. Note that `__new__` and staticmethods
# are excluded here since they do not take `self`.
if self_defn and i == 0 and func_def.name != "__new__" and not is_static:
Comment on lines +333 to +335
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once this PR lands, we should turn __new__ into a static method, then we won't need this hack anymore 🎉

input = parse_self_arg(inp, self_defn, ctx)
ctx = replace(ctx, self_ty=input.ty)
else:
Expand Down
52 changes: 48 additions & 4 deletions guppylang-internals/src/guppylang_internals/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@
OpCompiler,
RawCustomFunctionDef,
)
from guppylang_internals.definition.declaration import RawFunctionDecl
from guppylang_internals.definition.function import RawFunctionDef
from guppylang_internals.definition.overloaded import OverloadedFunctionDef
from guppylang_internals.definition.traced import RawTracedFunctionDef
from guppylang_internals.definition.ty import OpaqueTypeDef, TypeDef
from guppylang_internals.definition.wasm import RawWasmFunctionDef
from guppylang_internals.dummy_decorator import _dummy_custom_decorator, sphinx_running
from guppylang_internals.engine import DEF_STORE
from guppylang_internals.error import GuppyError, pretty_errors
from guppylang_internals.error import GuppyError, InternalGuppyError, pretty_errors
from guppylang_internals.std._internal.checker import WasmCallChecker
from guppylang_internals.std._internal.compiler.wasm import (
WasmModuleCallCompiler,
Expand Down Expand Up @@ -158,12 +162,47 @@ def extend_type(defn: TypeDef, return_class: bool = False) -> Callable[[type], t
def dec(c: type) -> type:
for val in c.__dict__.values():
if isinstance(val, GuppyDefinition):
DEF_STORE.register_type_member(defn.id, val.wrapped.name, val.id)
DEF_STORE.register_type_member(
defn.id, val.wrapped.name, val.id, is_static=determine_static(val)
)
return c if return_class else GuppyDefinition(defn) # type: ignore[return-value]

return dec


def determine_static(defn: GuppyDefinition) -> bool:
"""If defn corresponds to a function, check if it is static."""
if not isinstance(defn, GuppyFunctionDefinition):
return False
match defn.wrapped:
case RawFunctionDef() | RawCustomFunctionDef() | RawFunctionDecl():
return isinstance(defn.wrapped.python_func, staticmethod)
# comptime methods not yet supported
case RawTracedFunctionDef():
if isinstance(defn.wrapped.python_func, staticmethod):
# TODO guppy error handling
raise TypeError("static comptime func")
else:
return False
Comment on lines +180 to +186
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the issue with comptime functions, I'd assume everything should just work?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not looked into this, but I get 'staticmethod' object has no attribute '__globals__' in mock_builtins

case OverloadedFunctionDef():
# check all the methods in the overload are also static
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the current implementation doesn't do that. You just check if any overload is static and then raise an error? I would expect something like

is_static = [determine_static(overload_id) for overload_id in defn.wrapped.func_ids]
if all(is_static):
    return True
elif not any(is_static):
    return False:
else:
    raise TypeError("Some static, some not")

Note that this would refquire refactoring determine_static to take ids instead of GuppyDefinitions

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, maybe we should just look for @staticmethod on the @guppy.overloaded function instead?

for func_id in defn.wrapped.func_ids:
func_def = DEF_STORE.raw_defs[func_id]
if not isinstance(func_def, RawFunctionDef):
return False
Comment on lines +191 to +192
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could also be a RawCustomFunctionDef, RawFunctionDecl, RawTracedFunctionDef, or even another OverloadedFunctionDef

if not isinstance(func_def.python_func, staticmethod):
# TODO guppy error handling
raise TypeError(
"one of the functions in this overload is not static"
)
Comment on lines +194 to +197
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a vanilla Python error is ok here since it raised while decorating, not during compilation.

However, the message could be improved a bit, for example mentioning which overload is static and which isn't. Also could you add an error test for this case?

return True
case _:
raise InternalGuppyError(
f"Cannot determine staticness of GuppyFunctionDefinition wrapping \
{type(defn.wrapped)}"
)


def custom_type(
hugr_ty: ht.Type | Callable[[Sequence[Argument], CompilerContext], ht.Type],
name: str = "",
Expand Down Expand Up @@ -202,7 +241,9 @@ def dec(c: type[T]) -> type[T]:
DEF_STORE.register_def(defn, get_calling_frame())
for val in c.__dict__.values():
if isinstance(val, GuppyDefinition):
DEF_STORE.register_type_member(defn.id, val.wrapped.name, val.id)
DEF_STORE.register_type_member(
defn.id, val.wrapped.name, val.id, is_static=determine_static(val)
)
# We're pretending to return the class unchanged, but in fact we return
# a `GuppyDefinition` that handles the comptime logic
return GuppyDefinition(defn) # type: ignore[return-value]
Expand Down Expand Up @@ -300,7 +341,10 @@ def dec(cls: builtins.type[T]) -> GuppyDefinition:
for val in cls.__dict__.values():
if isinstance(val, GuppyDefinition):
DEF_STORE.register_type_member(
ext_module.id, val.wrapped.name, val.id
ext_module.id,
val.wrapped.name,
val.id,
is_static=determine_static(val),
)
wasm_def: RawWasmFunctionDef
if isinstance(val, GuppyFunctionDefinition) and isinstance(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,9 @@ def compile_call(


def parse_py_func(f: PyFunc, sources: SourceMap) -> tuple[ast.FunctionDef, str | None]:
# get the wrapped function if a staticmethod
if isinstance(f, staticmethod):
f = f.__func__
Comment on lines +328 to +330
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that the saved self.python_func is wrapped may be breaking as well (depending on if the wrapper copies or forwards key attributes such as __module__.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mark-koch and I briefly discussed this but I would appreciate any more thoughts / discussion to make sure this is ok

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently there is no guarantees made by the python_func about its type (because we do not have a proper type for it). However we do access things like __module__ on it, which is defined with regular functions. I guess if you add the link_name tests (and any other paths that may trigger attribute accesses) we should be fine.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consider storing the wrapped function directly instead of unwrapping here. That might be cleaner?

source_lines, line_offset = inspect.getsourcelines(f)
source, func_ast, line_offset = parse_source(source_lines, line_offset)
file = inspect.getsourcefile(f)
Expand Down
17 changes: 12 additions & 5 deletions guppylang-internals/src/guppylang_internals/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from contextlib import suppress
from dataclasses import dataclass
from types import FrameType
from typing import ClassVar, cast
from typing import ClassVar, NamedTuple, cast

import hugr
import hugr.build.function as hf
Expand Down Expand Up @@ -102,6 +102,11 @@
MonoDefId = tuple[DefId, Inst]


class TypeMember(NamedTuple):
id: DefId
is_static: bool


class DefinitionStore:
"""Storage class holding references to all Guppy definitions created in the current
interpreter session.
Expand All @@ -110,7 +115,7 @@ class DefinitionStore:
"""

raw_defs: dict[DefId, RawDef]
type_members: defaultdict[DefId, dict[str, DefId]]
type_members: defaultdict[DefId, dict[str, TypeMember]]
type_member_parents: dict[DefId, DefId]
wasm_functions: dict[DefId, FunctionType]
frames: dict[DefId, FrameType]
Expand All @@ -128,9 +133,11 @@ def register_def(self, defn: RawDef, frame: FrameType) -> None:
self.raw_defs[defn.id] = defn
self.frames[defn.id] = frame

def register_type_member(self, ty_id: DefId, name: str, member_id: DefId) -> None:
def register_type_member(
self, ty_id: DefId, name: str, member_id: DefId, *, is_static: bool = False
) -> None:
assert member_id not in self.type_member_parents, "Already a type member"
self.type_members[ty_id][name] = member_id
self.type_members[ty_id][name] = TypeMember(member_id, is_static)
self.type_member_parents[member_id] = ty_id
# Update the frame of the definition to the frame of the defining class
if member_id in self.frames:
Expand Down Expand Up @@ -362,7 +369,7 @@ def get_instance_func(self, ty: Type | TypeDef, name: str) -> CallableDef | None
type_defn.id in DEF_STORE.type_members
and name in DEF_STORE.type_members[type_defn.id]
):
def_id = DEF_STORE.type_members[type_defn.id][name]
def_id = DEF_STORE.type_members[type_defn.id][name].id
defn = ENGINE.get_parsed(def_id)
if isinstance(defn, CallableDef):
return defn
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -559,9 +559,9 @@ def __call__(self, *args: Any) -> Any:
# Definition is non-generic, so we can use `mono_args=()` here
defn = ENGINE.get_checked(self.wrapped.id, mono_args=())
if isinstance(defn, TypeDef) and (
constructor_id := DEF_STORE.type_members[defn.id].get("__new__")
constructor_def := DEF_STORE.type_members[defn.id].get("__new__")
):
return TracingDefMixin(DEF_STORE.raw_defs[constructor_id])(*args)
return TracingDefMixin(DEF_STORE.raw_defs[constructor_def.id])(*args)
err = f"{defn.description.capitalize()} `{defn.name}` is not callable"
raise GuppyComptimeError(err)

Expand Down Expand Up @@ -608,7 +608,7 @@ def to_guppy_object(self) -> GuppyObject:
and "__new__" in DEF_STORE.type_members[defn.id]
):
constructor = DEF_STORE.raw_defs[
DEF_STORE.type_members[defn.id]["__new__"]
DEF_STORE.type_members[defn.id]["__new__"].id
]
return TracingDefMixin(constructor).to_guppy_object()
err = f"{defn.description.capitalize()} `{defn.name}` is not a value"
Expand Down
15 changes: 13 additions & 2 deletions guppylang/src/guppylang/decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from guppylang_internals.decorator import (
custom_function,
custom_type,
determine_static,
hugr_op,
)
from guppylang_internals.definition.common import DefId
Expand Down Expand Up @@ -247,7 +248,12 @@ def decorator(
DEF_STORE.register_def(defn, frame)
for val in cls.__dict__.values():
if isinstance(val, GuppyDefinition):
DEF_STORE.register_type_member(defn.id, val.wrapped.name, val.id)
DEF_STORE.register_type_member(
defn.id,
val.wrapped.name,
val.id,
is_static=determine_static(val),
)
# Prior to Python 3.13, the `__firstlineno__` attribute on classes is not
# set. However, we need this information to precisely look up the source for
# the class later. If it's not there, we can set it from the calling frame:
Expand Down Expand Up @@ -291,7 +297,12 @@ def decorator(
DEF_STORE.register_def(defn, frame)
for val in cls.__dict__.values():
if isinstance(val, GuppyDefinition):
DEF_STORE.register_type_member(defn.id, val.wrapped.name, val.id)
DEF_STORE.register_type_member(
defn.id,
val.wrapped.name,
val.id,
is_static=determine_static(val),
)
# Prior to Python 3.13, the `__firstlineno__` attribute on classes is not
# set. However, we need this information to precisely look up the source for
# the class later. If it's not there, we can set it from the calling frame:
Expand Down
10 changes: 5 additions & 5 deletions guppylang/src/guppylang/defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@ def __getattr__(self, name: str) -> Any:
defn = ENGINE.get_checked(self.wrapped.id, mono_args=())
assert isinstance(defn, CheckedEnumDef)
if (
# We can only access the variants of the enum from the enum class,
# not methods
name in defn.variants
and defn.id in DEF_STORE.type_members
# We can only access enum variants or static methods from the enum class
defn.id in DEF_STORE.type_members
and name in DEF_STORE.type_members[defn.id]
) and (
name in defn.variants or DEF_STORE.type_members[defn.id][name].is_static
):
member_def = DEF_STORE.raw_defs[DEF_STORE.type_members[defn.id][name]]
member_def = DEF_STORE.raw_defs[DEF_STORE.type_members[defn.id][name].id]
return TracingDefMixin(member_def)
raise AttributeError(
f"{defn.description.capitalize()} `{defn.name}` has no attribute `{name}`"
Expand Down
1 change: 0 additions & 1 deletion guppylang/src/guppylang/std/builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
setattr,
slice,
sorted,
staticmethod,
sum,
super,
type,
Expand Down
4 changes: 0 additions & 4 deletions guppylang/src/guppylang/std/unsupported.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,6 @@ def slice(x): ...
def sorted(x): ...


@custom_function(checker=UnsupportedChecker(), higher_order_value=False)
def staticmethod(x): ...


@custom_function(checker=UnsupportedChecker(), higher_order_value=False)
def sum(x): ...

Expand Down
Loading
Loading