updated after discussion with sb10q

This commit is contained in:
pca006132 2020-12-15 17:33:31 +08:00 committed by pca006132
parent 1c5795da3d
commit 8aa4fc18a7

199
README.md
View File

@ -3,27 +3,19 @@
Specification and discussions about language design. Specification and discussions about language design.
## Referencing Python Variables ## Referencing Python Variables
> Not decided yet, whether require function annotation or only allow reference
> to variables.
The kernel is allowed to read Python variables. The kernel is allowed to read Python variables.
Unbounded identifiers would be considered as Python variables, no object is * Unbounded identifiers would be considered as Python variables, no object is
allowed. The value would be evaluated at compile time, subsequent modification allowed, only primitive types and tuple/list of allowed types are allowed.
in the host would not be known by the kernel. Basically, only allow lookup, no (not sure how to express the recursive concept neatly in English...)
evaluation. * 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 Example code that would be disallowed:
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:
```py ```py
from artiq.experiment import * from artiq.experiment import *
counter = 0 counter = 0
@ -40,99 +32,106 @@ class Foo(EnvExperiment):
return result return result
``` ```
Example for a totally valid case: ## Class and Functions
* Class fields must be annotated:
```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?
Example:
```py ```py
class Foo: class Foo:
a: int32 a: int
b: int32 b: int
def __init__(self, a: int32, b: int32): def __init__(self, a: int, b: int):
self.a = a self.a = a
self.b = b self.b = b
``` ```
* Functions require full type signature, including type annotation to every
#### Subtyping parameter and return type.
Do we allow subtyping? Or is parametric polymorphism enough?
If subtyping is allowed, we might need virtual method table.
Example where parametric polymorphism is not enough:
```py ```py
class Base: def add(a: int, b: int) -> int:
def foo(); return a + b
return 1
class Foo(Base):
def foo();
return 2
def run_all(l: list[Base]):
return [x.foo() for x in l]
run_all([Base(), Foo()])
``` ```
* 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.
#### Union Type ## Generics
As function overloading is not possible, should we allow union type, and const We use [type variable](https://docs.python.org/3/library/typing.html#typing.TypeVar) for denoting generics.
evaluate all the type checks after monomorphization?
Example: Example:
```py ```py
def foo(x: Union[int, bool]): from typing import TypeVar
if type(x) == int: T = TypeVar('T')
return x
else: class Foo(EnvExperiment):
return 1 if x else 0 @kernel
# type of a is the same as type of b
def run(self, a: T, b: T) -> bool:
return a == b
``` ```
## Function Pointers * Type variable can be limited to a fixed set of types. A shorthand for one-time
- Lambda with no capturing are treated as normal functions. type variable limited to a fixed set of types is [union type & optional type](https://docs.python.org/3/library/typing.html#typing.Union).
- Lambda with capturing: a structure would be created to store *pointers* to e.g. `def run(self, a: Union[int, str])`
captured variables, and the lambda would be a method of the struct. * Type variables support bounded generic, so we can access attributes of the
(Note: Storing pointers to meet the binding behavior of Python lambda) variable that are present in the boundary type, the type instantiated must be
- Method: implemented with fat pointer, i.e. function pointer + object pointer. the subtype of the boundary type. See [PEP484](https://www.python.org/dev/peps/pep-0484/#type-variables-with-an-upper-bound) for details.
Subject to lifetime rules. * 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.
## 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(self) -> int:
return 1
class Derived(Base):
def foo(self) -> int:
return 2
def bar(x: [Base]) -> int:
sum = 0
for v in x:
sum += v.foo()
return sum
# incorrect, the type signature of the list is `[virtual[Base]]`
bar([Base(), Derived()])
```
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 bar2(x: [virtual[Base]]) -> int:
sum = 0
for v in x:
sum += v.foo()
return sum
# correct
bar([Base(), Derived()])
```
Structural subtyping support is not determined yet.