From 8aa4fc18a7b7076a80450150fa19b7fdb35d28c4 Mon Sep 17 00:00:00 2001 From: pca006132 Date: Tue, 15 Dec 2020 17:33:31 +0800 Subject: [PATCH] updated after discussion with sb10q --- README.md | 183 +++++++++++++++++++++++++++--------------------------- 1 file changed, 91 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index e0259a3..4fb72d8 100644 --- a/README.md +++ b/README.md @@ -3,27 +3,19 @@ Specification and discussions about language design. ## Referencing Python Variables -> Not decided yet, whether require function annotation or only allow reference -> to variables. - The kernel is allowed to read Python variables. -Unbounded identifiers would be considered as Python variables, no object is -allowed. The value would be evaluated at compile time, subsequent modification -in the host would not be known by the kernel. Basically, only allow lookup, no -evaluation. +* Unbounded identifiers would be considered as Python variables, no object is + allowed, only primitive types and tuple/list of allowed types are allowed. + (not sure how to express the recursive concept neatly in English...) +* The value would be evaluated at compile time, subsequent modification + in the host would not be known by the kernel. +* Modification of global variable from within the kernel would be considered as + error. +* Calling non-RPC host function would be considered as an error. (RPC functions + must be annotated.) -(Bad) Alternative: Evaluate the unbounded identifier, allowing functions and -objects. It would easier to write the program as we don't have to manually -remove function calls in the kernel and store the result in a global variable. -However, this would potentially cause confusion, as the possibly side-effectful -function would be evaluated only once during compilation. - -(Better) Alternative: only evaluate functions marked as pure? Not sure if we can -access custom function annotation. We have to rely on the user to uphold the -guarantee. - -Example for a potentially confusing case: +Example code that would be disallowed: ```py from artiq.experiment import * counter = 0 @@ -40,99 +32,106 @@ class Foo(EnvExperiment): return result ``` -Example for a totally valid case: +## Class and Functions +* Class fields must be annotated: + ```py + class Foo: + a: int + b: int + def __init__(self, a: int, b: int): + self.a = a + self.b = b + ``` +* Functions require full type signature, including type annotation to every + parameter and return type. + ```py + def add(a: int, b: int) -> int: + return a + b + ``` +* No implicit coercion, require implicit cast. Integers are int32 by default, + floating point numbers are double by default. +* RPCs: optional parameter type signature, require return type signature. +* Function pointer is supported. +* Method pointer is a fat pointer (function pointer + object pointer), and + subject to lifetime check. -```py -from artiq.experiment import * -def get_param(x): - return x**2 - -class Foo(EnvExperiment): - @kernel - def run(self): - # unbounded function call disallowed - param = get_param(123) - # do something... - result = param - return result -``` - -This would not be allowed, and must be translated into this - -```py -from artiq.experiment import * -def get_param(x): - return x**2 - -param_123 = get_param(123) -class Foo(EnvExperiment): - @kernel - def run(self): - param = param_123 - # do something... - result = param - return result -``` - -## Type -### Decided -* Parametric polymorphism: use Python type variable. -* Normal functions: require full type signature. -* RPC: optional parameter type signature, require return type signature. -* No implicit coercion - -### Undecided -#### Class Fields -Should we require the user to declare all class fields first? +## Generics +We use [type variable](https://docs.python.org/3/library/typing.html#typing.TypeVar) for denoting generics. Example: ```py -class Foo: - a: int32 - b: int32 - def __init__(self, a: int32, b: int32): - self.a = a - self.b = b +from typing import TypeVar +T = TypeVar('T') + +class Foo(EnvExperiment): + @kernel + # type of a is the same as type of b + def run(self, a: T, b: T) -> bool: + return a == b ``` -#### Subtyping -Do we allow subtyping? Or is parametric polymorphism enough? -If subtyping is allowed, we might need virtual method table. +* Type variable can be limited to a fixed set of types. A shorthand for one-time + type variable limited to a fixed set of types is [union type & optional type](https://docs.python.org/3/library/typing.html#typing.Union). + e.g. `def run(self, a: Union[int, str])` +* Type variables support bounded generic, so we can access attributes of the + variable that are present in the boundary type, the type instantiated must be + the subtype of the boundary type. See [PEP484](https://www.python.org/dev/peps/pep-0484/#type-variables-with-an-upper-bound) for details. +* Type variables are invariant, same as the default in Python. We disallow + covariant or contravariant. The compiler should mark as error if it encounters + a type variable used in kernel that is declared covariant or contravariant. +* Code region protected by a type check, such as `if type(x) == int:`, would + treat `x` as `int`, similar to how [typescript type guard](https://www.typescripttutorial.net/typescript-tutorial/typescript-type-guards/) works. + ```py + def add1(x: Union[int, bool]) -> int: + if type(x) == int: + # x is int + return x + 1 + else: + # x must be bool + return 2 if x else 1 + ``` +* Generics are instantiated at compile time, all the type checks like + `type(x) == int` would be evaluated as constants. Type checks are not allowed + in area outside generics. -Example where parametric polymorphism is not enough: +## Dynamic Dispatch +Type annotations are invariant, so subtype (derived types) cannot be used +when the base type is expected. Example: ```py class Base: - def foo(); + def foo(self) -> int: return 1 -class Foo(Base): - def foo(); +class Derived(Base): + def foo(self) -> int: return 2 -def run_all(l: list[Base]): - return [x.foo() for x in l] +def bar(x: [Base]) -> int: + sum = 0 + for v in x: + sum += v.foo() + return sum -run_all([Base(), Foo()]) +# incorrect, the type signature of the list is `[virtual[Base]]` +bar([Base(), Derived()]) ``` -#### Union Type -As function overloading is not possible, should we allow union type, and const -evaluate all the type checks after monomorphization? +Dynamic dispatch is supported, but requires explicit annotation, similar to +[trait object](https://doc.rust-lang.org/book/ch17-02-trait-objects.html) in rust. +This is mainly for performance consideration, as virtual method table that is +required for dynamic dispatch would penalize performance, and prohibits function +inlining etc. Example: ```py -def foo(x: Union[int, bool]): - if type(x) == int: - return x - else: - return 1 if x else 0 +def bar2(x: [virtual[Base]]) -> int: + sum = 0 + for v in x: + sum += v.foo() + return sum +# correct +bar([Base(), Derived()]) ``` -## Function Pointers -- Lambda with no capturing are treated as normal functions. -- Lambda with capturing: a structure would be created to store *pointers* to - captured variables, and the lambda would be a method of the struct. - (Note: Storing pointers to meet the binding behavior of Python lambda) -- Method: implemented with fat pointer, i.e. function pointer + object pointer. - Subject to lifetime rules. +Structural subtyping support is not determined yet.