updated after discussion with sb10q
This commit is contained in:
parent
1c5795da3d
commit
8aa4fc18a7
199
README.md
199
README.md
@ -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.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user