From c867f48d6dc555454db69943f0b420cb53676d3d Mon Sep 17 00:00:00 2001 From: Mark Koch <48097969+mark-koch@users.noreply.github.com> Date: Mon, 16 Sep 2024 15:22:32 +0100 Subject: [PATCH] feat!: Hide lists and function tensors behind experimental flag (#501) Closes #437. BREAKING CHANGE: Lists and function tensors are no longer available by default. `guppylang.enable_experimental_features()` must be called before compilation to enable them. --------- Co-authored-by: Alan Lawrence --- guppylang/__init__.py | 1 + guppylang/cfg/builder.py | 2 + guppylang/checker/expr_checker.py | 5 ++ guppylang/experimental.py | 73 +++++++++++++++++++ guppylang/module.py | 5 +- guppylang/tys/builtin.py | 19 ++++- tests/conftest.py | 4 + tests/error/experimental_errors/__init__.py | 0 .../experimental_errors/function_tensor.err | 7 ++ .../experimental_errors/function_tensor.py | 22 ++++++ .../error/experimental_errors/linst_type.err | 6 ++ tests/error/experimental_errors/linst_type.py | 13 ++++ .../list_comprehension.err | 7 ++ .../experimental_errors/list_comprehension.py | 12 +++ .../experimental_errors/list_literal.err | 7 ++ .../error/experimental_errors/list_literal.py | 12 +++ tests/error/experimental_errors/list_type.err | 6 ++ tests/error/experimental_errors/list_type.py | 12 +++ tests/error/test_experimental_errors.py | 21 ++++++ 19 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 guppylang/experimental.py create mode 100644 tests/error/experimental_errors/__init__.py create mode 100644 tests/error/experimental_errors/function_tensor.err create mode 100644 tests/error/experimental_errors/function_tensor.py create mode 100644 tests/error/experimental_errors/linst_type.err create mode 100644 tests/error/experimental_errors/linst_type.py create mode 100644 tests/error/experimental_errors/list_comprehension.err create mode 100644 tests/error/experimental_errors/list_comprehension.py create mode 100644 tests/error/experimental_errors/list_literal.err create mode 100644 tests/error/experimental_errors/list_literal.py create mode 100644 tests/error/experimental_errors/list_type.err create mode 100644 tests/error/experimental_errors/list_type.py create mode 100644 tests/error/test_experimental_errors.py diff --git a/guppylang/__init__.py b/guppylang/__init__.py index e124f4d3..ea5d3178 100644 --- a/guppylang/__init__.py +++ b/guppylang/__init__.py @@ -1,4 +1,5 @@ from guppylang.decorator import guppy +from guppylang.experimental import enable_experimental_features from guppylang.module import GuppyModule from guppylang.prelude import builtins, quantum from guppylang.prelude.builtins import Bool, Float, Int, List, linst, py diff --git a/guppylang/cfg/builder.py b/guppylang/cfg/builder.py index 877b1066..5c1ec8bf 100644 --- a/guppylang/cfg/builder.py +++ b/guppylang/cfg/builder.py @@ -16,6 +16,7 @@ from guppylang.cfg.cfg import CFG from guppylang.checker.core import Globals from guppylang.error import GuppyError, InternalGuppyError +from guppylang.experimental import check_lists_enabled from guppylang.nodes import ( DesugaredGenerator, DesugaredListComp, @@ -304,6 +305,7 @@ def visit_IfExp(self, node: ast.IfExp) -> ast.Name: return make_var(tmp, node) def visit_ListComp(self, node: ast.ListComp) -> ast.AST: + check_lists_enabled(node) # Check for illegal expressions illegals = find_nodes(is_illegal_in_list_comp, node) if illegals: diff --git a/guppylang/checker/expr_checker.py b/guppylang/checker/expr_checker.py index 94599ee1..48bf01df 100644 --- a/guppylang/checker/expr_checker.py +++ b/guppylang/checker/expr_checker.py @@ -57,6 +57,7 @@ GuppyTypeInferenceError, InternalGuppyError, ) +from guppylang.experimental import check_function_tensors_enabled, check_lists_enabled from guppylang.nodes import ( DesugaredGenerator, DesugaredListComp, @@ -214,6 +215,7 @@ def visit_Tuple(self, node: ast.Tuple, ty: Type) -> tuple[ast.expr, Subst]: return node, subst def visit_List(self, node: ast.List, ty: Type) -> tuple[ast.expr, Subst]: + check_lists_enabled(node) if not is_list_type(ty) and not is_linst_type(ty): return self._fail(ty, node) el_ty = get_element_type(ty) @@ -265,6 +267,7 @@ def visit_Call(self, node: ast.Call, ty: Type) -> tuple[ast.expr, Subst]: if isinstance(func_ty, TupleType) and ( function_elements := parse_function_tensor(func_ty) ): + check_function_tensors_enabled(node.func) if any(f.parametrized for f in function_elements): raise GuppyTypeError( "Polymorphic functions in tuples are not supported", node.func @@ -435,6 +438,7 @@ def visit_Tuple(self, node: ast.Tuple) -> tuple[ast.expr, Type]: return node, TupleType([ty for _, ty in elems]) def visit_List(self, node: ast.List) -> tuple[ast.expr, Type]: + check_lists_enabled(node) if len(node.elts) == 0: raise GuppyTypeInferenceError( "Cannot infer type variable in expression of type `list[?T]`", node @@ -602,6 +606,7 @@ def visit_Call(self, node: ast.Call) -> tuple[ast.expr, Type]: elif isinstance(ty, TupleType) and ( function_elems := parse_function_tensor(ty) ): + check_function_tensors_enabled(node.func) if any(f.parametrized for f in function_elems): raise GuppyTypeError( "Polymorphic functions in tuples are not supported", node.func diff --git a/guppylang/experimental.py b/guppylang/experimental.py new file mode 100644 index 00000000..369a27fd --- /dev/null +++ b/guppylang/experimental.py @@ -0,0 +1,73 @@ +from ast import expr +from types import TracebackType + +from guppylang.ast_util import AstNode +from guppylang.error import GuppyError + +EXPERIMENTAL_FEATURES_ENABLED = False + + +class enable_experimental_features: + """Enables experimental Guppy features. + + Can be used as a context manager to enable experimental features in a `with` block. + """ + + def __init__(self) -> None: + global EXPERIMENTAL_FEATURES_ENABLED + self.original = EXPERIMENTAL_FEATURES_ENABLED + EXPERIMENTAL_FEATURES_ENABLED = True + + def __enter__(self) -> None: + pass + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + global EXPERIMENTAL_FEATURES_ENABLED + EXPERIMENTAL_FEATURES_ENABLED = self.original + + +class disable_experimental_features: + """Disables experimental Guppy features. + + Can be used as a context manager to enable experimental features in a `with` block. + """ + + def __init__(self) -> None: + global EXPERIMENTAL_FEATURES_ENABLED + self.original = EXPERIMENTAL_FEATURES_ENABLED + EXPERIMENTAL_FEATURES_ENABLED = False + + def __enter__(self) -> None: + pass + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + global EXPERIMENTAL_FEATURES_ENABLED + EXPERIMENTAL_FEATURES_ENABLED = self.original + + +def check_function_tensors_enabled(node: expr | None = None) -> None: + if not EXPERIMENTAL_FEATURES_ENABLED: + raise GuppyError( + "Function tensors are an experimental feature. Use " + "`guppylang.enable_experimental_features()` to enable them.", + node, + ) + + +def check_lists_enabled(loc: AstNode | None = None) -> None: + if not EXPERIMENTAL_FEATURES_ENABLED: + raise GuppyError( + "Lists are an experimental feature and not fully supported yet. Use " + "`guppylang.enable_experimental_features()` to enable them.", + loc, + ) diff --git a/guppylang/module.py b/guppylang/module.py index c4e2a255..fc7d7537 100644 --- a/guppylang/module.py +++ b/guppylang/module.py @@ -28,6 +28,7 @@ from guppylang.definition.struct import CheckedStructDef from guppylang.definition.ty import TypeDef from guppylang.error import GuppyError, pretty_errors +from guppylang.experimental import enable_experimental_features PyClass = type PyFunc = Callable[..., Any] @@ -89,7 +90,9 @@ def __init__(self, name: str, import_builtins: bool = True): if import_builtins: import guppylang.prelude.builtins as builtins - self.load_all(builtins) + # Std lib is allowed to use experimental features + with enable_experimental_features(): + self.load_all(builtins) def load( self, diff --git a/guppylang/tys/builtin.py b/guppylang/tys/builtin.py index 3bab7ab2..fe40dcfb 100644 --- a/guppylang/tys/builtin.py +++ b/guppylang/tys/builtin.py @@ -10,6 +10,7 @@ from guppylang.definition.common import DefId from guppylang.definition.ty import OpaqueTypeDef, TypeDef from guppylang.error import GuppyError, InternalGuppyError +from guppylang.experimental import check_lists_enabled from guppylang.tys.arg import Argument, ConstArg, TypeArg from guppylang.tys.const import ConstValue from guppylang.tys.param import ConstParam, TypeParam @@ -109,6 +110,7 @@ class _ListTypeDef(OpaqueTypeDef): def check_instantiate( self, args: Sequence[Argument], globals: "Globals", loc: AstNode | None = None ) -> OpaqueType: + check_lists_enabled(loc) if len(args) == 1: [arg] = args if isinstance(arg, TypeArg) and arg.ty.linear: @@ -118,6 +120,21 @@ def check_instantiate( return super().check_instantiate(args, globals, loc) +@dataclass(frozen=True) +class _LinstTypeDef(OpaqueTypeDef): + """Type definition associated with the builtin `linst` type. + + We have a custom definition to disable usage of linsts unless experimental features + are enabled. + """ + + def check_instantiate( + self, args: Sequence[Argument], globals: "Globals", loc: AstNode | None = None + ) -> OpaqueType: + check_lists_enabled(loc) + return super().check_instantiate(args, globals, loc) + + def _list_to_hugr(args: Sequence[Argument]) -> ht.Type: # Type checker ensures that we get a single arg of kind type [arg] = args @@ -163,7 +180,7 @@ def _array_to_hugr(args: Sequence[Argument]) -> ht.Type: float_type_def = _NumericTypeDef( DefId.fresh(), "float", None, NumericType(NumericType.Kind.Float) ) -linst_type_def = OpaqueTypeDef( +linst_type_def = _LinstTypeDef( id=DefId.fresh(), name="linst", defined_at=None, diff --git a/tests/conftest.py b/tests/conftest.py index aac6a94e..9eff323d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,10 @@ import argparse from pathlib import Path +import guppylang + +guppylang.enable_experimental_features() + def pytest_addoption(parser): def dir_path(s): diff --git a/tests/error/experimental_errors/__init__.py b/tests/error/experimental_errors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/error/experimental_errors/function_tensor.err b/tests/error/experimental_errors/function_tensor.err new file mode 100644 index 00000000..4974e110 --- /dev/null +++ b/tests/error/experimental_errors/function_tensor.err @@ -0,0 +1,7 @@ +Guppy compilation failed. Error in file $FILE:19 + +17: @guppy(module) +18: def main() -> tuple[int, int]: +19: return (f, g)(1, 2) + ^^^^^^ +GuppyError: Function tensors are an experimental feature. Use `guppylang.enable_experimental_features()` to enable them. diff --git a/tests/error/experimental_errors/function_tensor.py b/tests/error/experimental_errors/function_tensor.py new file mode 100644 index 00000000..27827f5a --- /dev/null +++ b/tests/error/experimental_errors/function_tensor.py @@ -0,0 +1,22 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule + +module = GuppyModule("test") + + +@guppy(module) +def f(x: int) -> int: + return x + + +@guppy(module) +def g(x: int) -> int: + return x + + +@guppy(module) +def main() -> tuple[int, int]: + return (f, g)(1, 2) + + +module.compile() diff --git a/tests/error/experimental_errors/linst_type.err b/tests/error/experimental_errors/linst_type.err new file mode 100644 index 00000000..b1bcbb40 --- /dev/null +++ b/tests/error/experimental_errors/linst_type.err @@ -0,0 +1,6 @@ +Guppy compilation failed. Error in file $FILE:9 + +7: @guppy(module) +8: def main(x: linst[int]) -> linst[int]: + ^^^^^^^^^^ +GuppyError: Lists are an experimental feature and not fully supported yet. Use `guppylang.enable_experimental_features()` to enable them. diff --git a/tests/error/experimental_errors/linst_type.py b/tests/error/experimental_errors/linst_type.py new file mode 100644 index 00000000..76d917c2 --- /dev/null +++ b/tests/error/experimental_errors/linst_type.py @@ -0,0 +1,13 @@ +from guppylang.decorator import guppy +from guppylang.prelude.builtins import linst +from guppylang.module import GuppyModule + +module = GuppyModule("test") + + +@guppy(module) +def main(x: linst[int]) -> linst[int]: + return x + + +module.compile() diff --git a/tests/error/experimental_errors/list_comprehension.err b/tests/error/experimental_errors/list_comprehension.err new file mode 100644 index 00000000..e2be624e --- /dev/null +++ b/tests/error/experimental_errors/list_comprehension.err @@ -0,0 +1,7 @@ +Guppy compilation failed. Error in file $FILE:9 + +7: @guppy(module) +8: def main() -> None: +9: [i for i in range(10)] + ^^^^^^^^^^^^^^^^^^^^^^ +GuppyError: Lists are an experimental feature and not fully supported yet. Use `guppylang.enable_experimental_features()` to enable them. diff --git a/tests/error/experimental_errors/list_comprehension.py b/tests/error/experimental_errors/list_comprehension.py new file mode 100644 index 00000000..4b352638 --- /dev/null +++ b/tests/error/experimental_errors/list_comprehension.py @@ -0,0 +1,12 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule + +module = GuppyModule("test") + + +@guppy(module) +def main() -> None: + [i for i in range(10)] + + +module.compile() diff --git a/tests/error/experimental_errors/list_literal.err b/tests/error/experimental_errors/list_literal.err new file mode 100644 index 00000000..717c886b --- /dev/null +++ b/tests/error/experimental_errors/list_literal.err @@ -0,0 +1,7 @@ +Guppy compilation failed. Error in file $FILE:9 + +7: @guppy(module) +8: def main() -> None: +9: [1, 2, 3] + ^^^^^^^^^ +GuppyError: Lists are an experimental feature and not fully supported yet. Use `guppylang.enable_experimental_features()` to enable them. diff --git a/tests/error/experimental_errors/list_literal.py b/tests/error/experimental_errors/list_literal.py new file mode 100644 index 00000000..7eb7a891 --- /dev/null +++ b/tests/error/experimental_errors/list_literal.py @@ -0,0 +1,12 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule + +module = GuppyModule("test") + + +@guppy(module) +def main() -> None: + [1, 2, 3] + + +module.compile() diff --git a/tests/error/experimental_errors/list_type.err b/tests/error/experimental_errors/list_type.err new file mode 100644 index 00000000..cf5446ab --- /dev/null +++ b/tests/error/experimental_errors/list_type.err @@ -0,0 +1,6 @@ +Guppy compilation failed. Error in file $FILE:8 + +6: @guppy(module) +7: def main(x: list[int]) -> list[int]: + ^^^^^^^^^ +GuppyError: Lists are an experimental feature and not fully supported yet. Use `guppylang.enable_experimental_features()` to enable them. diff --git a/tests/error/experimental_errors/list_type.py b/tests/error/experimental_errors/list_type.py new file mode 100644 index 00000000..8fa08d61 --- /dev/null +++ b/tests/error/experimental_errors/list_type.py @@ -0,0 +1,12 @@ +from guppylang.decorator import guppy +from guppylang.module import GuppyModule + +module = GuppyModule("test") + + +@guppy(module) +def main(x: list[int]) -> list[int]: + return x + + +module.compile() diff --git a/tests/error/test_experimental_errors.py b/tests/error/test_experimental_errors.py new file mode 100644 index 00000000..16f6c3ee --- /dev/null +++ b/tests/error/test_experimental_errors.py @@ -0,0 +1,21 @@ +import pathlib +import pytest + +from guppylang.experimental import disable_experimental_features +from tests.error.util import run_error_test + +path = pathlib.Path(__file__).parent.resolve() / "experimental_errors" +files = [ + x + for x in path.iterdir() + if x.is_file() and x.suffix == ".py" and x.name != "__init__.py" +] + +# Turn paths into strings, otherwise pytest doesn't display the names +files = [str(f) for f in files] + + +@pytest.mark.parametrize("file", files) +def test_experimental_errors(file, capsys): + with disable_experimental_features(): + run_error_test(file, capsys)