From 10eac8a64510ce1ba281375f3967143ca4b8cc8b Mon Sep 17 00:00:00 2001 From: Mark Koch Date: Mon, 9 Dec 2024 09:20:55 +0000 Subject: [PATCH 1/3] feat: Add Option type to standard library --- guppylang/decorator.py | 13 +++-- guppylang/definition/ty.py | 8 ++- guppylang/std/_internal/compiler/option.py | 61 ++++++++++++++++++++++ guppylang/std/option.py | 61 ++++++++++++++++++++++ tests/integration/test_option.py | 36 +++++++++++++ 5 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 guppylang/std/_internal/compiler/option.py create mode 100644 guppylang/std/option.py create mode 100644 tests/integration/test_option.py diff --git a/guppylang/decorator.py b/guppylang/decorator.py index b0ea43fe..c978a878 100644 --- a/guppylang/decorator.py +++ b/guppylang/decorator.py @@ -1,6 +1,6 @@ import ast import inspect -from collections.abc import Callable, KeysView +from collections.abc import Callable, KeysView, Sequence from dataclasses import dataclass, field from pathlib import Path from types import ModuleType @@ -48,6 +48,8 @@ sphinx_running, ) from guppylang.span import SourceMap +from guppylang.tys.arg import Argument +from guppylang.tys.param import Parameter from guppylang.tys.subst import Inst from guppylang.tys.ty import NumericType @@ -197,10 +199,11 @@ def dec(c: type) -> type: @pretty_errors def type( self, - hugr_ty: ht.Type, + hugr_ty: ht.Type | Callable[[Sequence[Argument]], ht.Type], name: str = "", linear: bool = False, bound: ht.TypeBound | None = None, + params: Sequence[Parameter] | None = None, module: GuppyModule | None = None, ) -> OpaqueTypeDecorator: """Decorator to annotate a class definitions as Guppy types. @@ -212,14 +215,16 @@ def type( mod = module or self.get_module() mod._instance_func_buffer = {} + mk_hugr_ty = (lambda _: hugr_ty) if isinstance(hugr_ty, ht.Type) else hugr_ty + def dec(c: type) -> OpaqueTypeDef: defn = OpaqueTypeDef( DefId.fresh(mod), name or c.__name__, None, - [], + params or [], linear, - lambda _: hugr_ty, + mk_hugr_ty, bound, ) mod.register_def(defn) diff --git a/guppylang/definition/ty.py b/guppylang/definition/ty.py index e50fe4fd..123b99c6 100644 --- a/guppylang/definition/ty.py +++ b/guppylang/definition/ty.py @@ -1,7 +1,7 @@ from abc import abstractmethod from collections.abc import Callable, Sequence from dataclasses import dataclass, field -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from hugr import tys @@ -41,6 +41,12 @@ class OpaqueTypeDef(TypeDef, CompiledDef): to_hugr: Callable[[Sequence[Argument]], tys.Type] bound: tys.TypeBound | None = None + def __getitem__(self, item: Any) -> "OpaqueTypeDef": + """Dummy implementation to allow generic instantiations in type signatures that + are evaluated by the Python interpreter. + """ + return self + def check_instantiate( self, args: Sequence[Argument], globals: "Globals", loc: AstNode | None = None ) -> OpaqueType: diff --git a/guppylang/std/_internal/compiler/option.py b/guppylang/std/_internal/compiler/option.py new file mode 100644 index 00000000..3a2b5840 --- /dev/null +++ b/guppylang/std/_internal/compiler/option.py @@ -0,0 +1,61 @@ +from abc import ABC + +from hugr import Wire, ops +from hugr import tys as ht +from hugr import val as hv + +from guppylang.definition.custom import CustomCallCompiler, CustomInoutCallCompiler +from guppylang.definition.value import CallReturnWires +from guppylang.error import InternalGuppyError +from guppylang.std._internal.compiler.prelude import build_unwrap +from guppylang.tys.arg import TypeArg + + +class OptionCompiler(CustomInoutCallCompiler, ABC): + """Abstract base class for compilers for `Option` methods.""" + + @property + def option_ty(self) -> ht.Option: + match self.type_args: + case [TypeArg(ty)]: + return ht.Option(ty.to_hugr()) + case _: + raise InternalGuppyError("Invalid type args for Option op") + + +class OptionConstructor(OptionCompiler, CustomCallCompiler): + """Compiler for the `Option` constructors `none` and `some`.""" + + def __init__(self, tag: int): + self.tag = tag + + def compile(self, args: list[Wire]) -> list[Wire]: + return [self.builder.add_op(ops.Tag(self.tag, self.option_ty), *args)] + + +class OptionTestCompiler(OptionCompiler): + """Compiler for the `Option.is_none` and `Option.is_none` methods.""" + + def __init__(self, tag: int): + self.tag = tag + + def compile_with_inouts(self, args: list[Wire]) -> CallReturnWires: + [opt] = args + cond = self.builder.add_conditional(opt) + for i in [0, 1]: + with cond.add_case(i) as case: + val = hv.TRUE if i == self.tag else hv.FALSE + opt = case.add_op(ops.Tag(i, self.option_ty), *case.inputs()) + case.set_outputs(case.load(val), opt) + [res, opt] = cond.outputs() + return CallReturnWires(regular_returns=[res], inout_returns=[opt]) + + +class OptionUnwrapCompiler(OptionCompiler, CustomCallCompiler): + """Compiler for the `Option.unwrap` method.""" + + def compile(self, args: list[Wire]) -> list[Wire]: + [opt] = args + return list( + build_unwrap(self.builder, opt, "Option.unwrap: value is `None`").outputs() + ) diff --git a/guppylang/std/option.py b/guppylang/std/option.py new file mode 100644 index 00000000..dd4f43e7 --- /dev/null +++ b/guppylang/std/option.py @@ -0,0 +1,61 @@ +from collections.abc import Sequence +from typing import Generic, no_type_check + +import hugr.tys as ht + +from guppylang.decorator import guppy +from guppylang.error import InternalGuppyError +from guppylang.std._internal.compiler.option import ( + OptionConstructor, + OptionTestCompiler, + OptionUnwrapCompiler, +) +from guppylang.std.builtins import owned +from guppylang.tys.arg import Argument, TypeArg +from guppylang.tys.param import TypeParam + + +def _option_to_hugr(args: Sequence[Argument]) -> ht.Type: + match args: + case [TypeArg(ty)]: + return ht.Option(ty.to_hugr()) + case _: + raise InternalGuppyError("Invalid type args for Option") + + +T = guppy.type_var("T", linear=True) + + +@guppy.type(_option_to_hugr, params=[TypeParam(0, "T", can_be_linear=True)]) +class Option(Generic[T]): # type: ignore[misc] + """Represents an optional value.""" + + @guppy.custom(OptionTestCompiler(0)) + @no_type_check + def is_none(self: "Option[T]") -> bool: + """Returns `True` if the option is a `none` value.""" + + @guppy.custom(OptionTestCompiler(1)) + @no_type_check + def is_some(self: "Option[T]") -> bool: + """Returns `True` if the option is a `some` value.""" + + @guppy.custom(OptionUnwrapCompiler()) + @no_type_check + def unwrap(self: "Option[T]" @ owned) -> T: + """Returns the contained `some` value, consuming `self`. + + Panics if the option is a `none` value. + """ + + +@guppy.custom(OptionConstructor(0)) +@no_type_check +def none() -> Option[T]: + """Constructs a `none` optional value.""" + + +@guppy.custom(OptionConstructor(1)) +@no_type_check +def some(value: T @ owned) -> Option[T]: + """Constructs a `some` optional value.""" diff --git a/tests/integration/test_option.py b/tests/integration/test_option.py new file mode 100644 index 00000000..1281163b --- /dev/null +++ b/tests/integration/test_option.py @@ -0,0 +1,36 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule +from guppylang.std.option import Option, none, some + + +def test_none(validate, run_int_fn): + module = GuppyModule("test_range") + module.load(Option, none) + + @guppy(module) + def main() -> int: + x: Option[int] = none() + is_none = 10 if x.is_none() else 0 + is_some = 1 if x.is_some() else 0 + return is_none + is_some + + compiled = module.compile() + validate(compiled) + run_int_fn(compiled, expected=10) + + +def test_some_unwrap(validate, run_int_fn): + module = GuppyModule("test_range") + module.load(Option, some) + + @guppy(module) + def main() -> int: + x: Option[int] = some(42) + is_none = 1 if x.is_none() else 0 + is_some = x.unwrap() if x.is_some() else 0 + return is_none + is_some + + compiled = module.compile() + validate(compiled) + run_int_fn(compiled, expected=42) + From c803b35e11dbc5fb8cfe4a9344464b1efcb871d1 Mon Sep 17 00:00:00 2001 From: Mark Koch Date: Tue, 10 Dec 2024 09:17:02 +0000 Subject: [PATCH 2/3] Explain callable in docstring --- guppylang/decorator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guppylang/decorator.py b/guppylang/decorator.py index c978a878..5b7d6361 100644 --- a/guppylang/decorator.py +++ b/guppylang/decorator.py @@ -211,6 +211,10 @@ def type( Requires the static Hugr translation of the type. Additionally, the type can be marked as linear. All `@guppy` annotated functions on the class are turned into instance functions. + + For non-generic types, the Hugr representation can be passed as a static value. + For generic types, a callable may be passed that takes the type arguments of a + concrete instantiation. """ mod = module or self.get_module() mod._instance_func_buffer = {} From a9fdefb406e7f789dd189aaa16b5762f87ece567 Mon Sep 17 00:00:00 2001 From: Mark Koch Date: Tue, 10 Dec 2024 09:28:40 +0000 Subject: [PATCH 3/3] Rename none to nothing --- guppylang/std/_internal/compiler/option.py | 9 ++++----- guppylang/std/option.py | 10 +++++----- tests/integration/test_option.py | 10 +++++----- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/guppylang/std/_internal/compiler/option.py b/guppylang/std/_internal/compiler/option.py index 3a2b5840..3ec5c118 100644 --- a/guppylang/std/_internal/compiler/option.py +++ b/guppylang/std/_internal/compiler/option.py @@ -24,7 +24,7 @@ def option_ty(self) -> ht.Option: class OptionConstructor(OptionCompiler, CustomCallCompiler): - """Compiler for the `Option` constructors `none` and `some`.""" + """Compiler for the `Option` constructors `nothing` and `some`.""" def __init__(self, tag: int): self.tag = tag @@ -34,7 +34,7 @@ def compile(self, args: list[Wire]) -> list[Wire]: class OptionTestCompiler(OptionCompiler): - """Compiler for the `Option.is_none` and `Option.is_none` methods.""" + """Compiler for the `Option.is_nothing` and `Option.is_some` methods.""" def __init__(self, tag: int): self.tag = tag @@ -56,6 +56,5 @@ class OptionUnwrapCompiler(OptionCompiler, CustomCallCompiler): def compile(self, args: list[Wire]) -> list[Wire]: [opt] = args - return list( - build_unwrap(self.builder, opt, "Option.unwrap: value is `None`").outputs() - ) + err = "Option.unwrap: value is `Nothing`" + return list(build_unwrap(self.builder, opt, err).outputs()) diff --git a/guppylang/std/option.py b/guppylang/std/option.py index dd4f43e7..9359e168 100644 --- a/guppylang/std/option.py +++ b/guppylang/std/option.py @@ -32,8 +32,8 @@ class Option(Generic[T]): # type: ignore[misc] @guppy.custom(OptionTestCompiler(0)) @no_type_check - def is_none(self: "Option[T]") -> bool: - """Returns `True` if the option is a `none` value.""" + def is_nothing(self: "Option[T]") -> bool: + """Returns `True` if the option is a `nothing` value.""" @guppy.custom(OptionTestCompiler(1)) @no_type_check @@ -45,14 +45,14 @@ def is_some(self: "Option[T]") -> bool: def unwrap(self: "Option[T]" @ owned) -> T: """Returns the contained `some` value, consuming `self`. - Panics if the option is a `none` value. + Panics if the option is a `nothing` value. """ @guppy.custom(OptionConstructor(0)) @no_type_check -def none() -> Option[T]: - """Constructs a `none` optional value.""" +def nothing() -> Option[T]: + """Constructs a `nothing` optional value.""" @guppy.custom(OptionConstructor(1)) diff --git a/tests/integration/test_option.py b/tests/integration/test_option.py index 1281163b..db491643 100644 --- a/tests/integration/test_option.py +++ b/tests/integration/test_option.py @@ -1,16 +1,16 @@ from guppylang.decorator import guppy from guppylang.module import GuppyModule -from guppylang.std.option import Option, none, some +from guppylang.std.option import Option, nothing, some def test_none(validate, run_int_fn): module = GuppyModule("test_range") - module.load(Option, none) + module.load(Option, nothing) @guppy(module) def main() -> int: - x: Option[int] = none() - is_none = 10 if x.is_none() else 0 + x: Option[int] = nothing() + is_none = 10 if x.is_nothing() else 0 is_some = 1 if x.is_some() else 0 return is_none + is_some @@ -26,7 +26,7 @@ def test_some_unwrap(validate, run_int_fn): @guppy(module) def main() -> int: x: Option[int] = some(42) - is_none = 1 if x.is_none() else 0 + is_none = 1 if x.is_nothing() else 0 is_some = x.unwrap() if x.is_some() else 0 return is_none + is_some