184 lines
5.6 KiB
Markdown
184 lines
5.6 KiB
Markdown
# NAC3 Specification
|
|
|
|
Specification and discussions about language design.
|
|
|
|
A toy implementation is in `toy-impl`, requires python 3.9.
|
|
|
|
|
|
## Referencing Python Variables
|
|
The kernel is allowed to read Python variables.
|
|
|
|
* 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.)
|
|
|
|
Example code that would be disallowed:
|
|
```py
|
|
from artiq.experiment import *
|
|
counter = 0
|
|
def get_id():
|
|
counter += 1
|
|
return counter
|
|
|
|
class Foo(EnvExperiment):
|
|
@kernel
|
|
def run(self):
|
|
param = get_id()
|
|
# do something...
|
|
result = param
|
|
return result
|
|
```
|
|
|
|
## 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 default parameters are not allowed, as changes to the default value
|
|
would not be kept across kernel calls, and that is a potential source of
|
|
confusion.
|
|
|
|
## Built-in Types
|
|
* Primitive types include:
|
|
* `bool`
|
|
* `byte`
|
|
* `int32`
|
|
* `int64`
|
|
* `uint32`
|
|
* `uint64`
|
|
* `float`
|
|
* `str` (note: fixed length, provide builtin methods?)
|
|
* `bytes` (a list of `byte`, but with more convenient syntax)
|
|
* Collections include:
|
|
* `list`: homogeneous (elements must be of the same type) fixed-size (no
|
|
append) list.
|
|
* `tuple`: inhomogeneous fixed-size list, only pattern
|
|
matching (e.g. `a, b, c = (1, True, 1.2)`) and constant indexing
|
|
is supported:
|
|
```
|
|
t = (1, True)
|
|
# OK
|
|
a, b = t
|
|
# OK
|
|
a = t[0]
|
|
# Not OK
|
|
i = 0
|
|
a = t[i]
|
|
```
|
|
* `range` (over numerical types)
|
|
|
|
### Numerical Types
|
|
* All binary operations expect the values to have the same type, no implicit
|
|
coercion would be performed, explicit casting is required.
|
|
* Casting can be done by `T(v)` where `T` is the target type, and `v` is the
|
|
original value. Examples: `int64(123)`
|
|
* Constant integers are treated as `int32` by default. If the value cannot
|
|
be stored in `int32`, `uint64` would be used if the integer is non-negative,
|
|
and `int64` would be used it the integer is negative.
|
|
* Only `uint32`, `int32` (and range of them) can be used as index.
|
|
|
|
## Generics
|
|
We use [type variable](https://docs.python.org/3/library/typing.html#typing.TypeVar) for denoting generics.
|
|
|
|
Example:
|
|
```py
|
|
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
|
|
```
|
|
|
|
* Type variables can only be used in functions/methods, but not in classes.
|
|
* 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 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.
|
|
* Type variable and union cannot occur alone in the result type.
|
|
|
|
## 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: list[Base]) -> int:
|
|
sum = 0
|
|
for v in x:
|
|
sum += v.foo()
|
|
return sum
|
|
|
|
# incorrect, the type signature of the list is `list[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.
|
|
`virtual[T]` is the type for `T` and its subtypes(derived types).
|
|
|
|
This is mainly for performance consideration, as virtual method table that is
|
|
required for dynamic dispatch would penalize performance, and prohibits function
|
|
inlining etc.
|
|
|
|
Type variables cannot be used inside `virtual[...]`, and type variables would not
|
|
range over `virtual[...]`.
|
|
|
|
|
|
Example:
|
|
```py
|
|
def bar2(x: list[virtual[Base]]) -> int:
|
|
sum = 0
|
|
for v in x:
|
|
sum += v.foo()
|
|
return sum
|
|
# correct
|
|
bar([Base(), Derived()])
|
|
```
|
|
|
|
|