Updated specification #8
126
README.md
|
@ -5,23 +5,22 @@ Specification and discussions about language design.
|
|||
A toy implementation is in [`toy-impl`](./toy-impl), requires python 3.9.
|
||||
|
||||
|
||||
## Referencing Python Variables
|
||||
The kernel is allowed to read host Python variables, but has to specify with
|
||||
`global` before referencing them. This is to simplify and speed-up
|
||||
implementation, and also warn the user about the variable being global. (prevent
|
||||
calling the interpreter many times during compilation if there are many
|
||||
references to host variables)
|
||||
## Referencing Host Variables from Kernel
|
||||
Host variable to be accessed must be declared as `global` in the kernel
|
||||
pca006132 marked this conversation as resolved
|
||||
function. This is to simplify and speed-up implementation, and also warn the
|
||||
user about the variable being global. (prevent calling the interpreter many
|
||||
times during compilation if there are many references to host variables)
|
||||
|
||||
Only primitive types and tuple/list of primitive types are allowed.
|
||||
Kernel cannot modify host variables, this would be checked by the compiler.
|
||||
Value that can be observed by the kernel would be frozen once the kernel has
|
||||
been compiled, subsequence modification within the host would not affect the
|
||||
sb10q
commented
Ah, this would cause another problem like those in #5 Ah, this would cause another problem like those in https://git.m-labs.hk/M-Labs/nac3-spec/issues/5
But deleting globals or trying to make them immutable in the interpreter sounds messy, and a global being modified in a RPC seems to be a niche case.
So I'm ok with this current proposal, the behavior simply should be documented.
pca006132
commented
Yes, I was thinking about this either, but it seems like there is no easy way around this. Yes, I was thinking about this either, but it seems like there is no easy way around this.
lriesebos
commented
Why don't we force users to type globals used in kernels as Why don't we force users to type globals used in kernels as `Invariant[]`? The semantics are the same, making it clear to the user how this global variable can be used in kernels.
sb10q
commented
It cannot be enforced in the interpreter. It cannot be enforced in the interpreter.
lriesebos
commented
I understand, it would just potentially make it more clear to the user that the global variable is immutable in the kernel. But I am fine with either! I understand, it would just potentially make it more clear to the user that the global variable is immutable in the kernel. But I am fine with either!
pca006132
commented
I think the types that we allow are basically immutable? Int, tuples, string, etc. I think the types that we allow are basically immutable? Int, tuples, string, etc.
Even if they are decorated with `Invariant[T]`, the user can just re-assign them, so hard to enforce.
sb10q
commented
For classes there are special For classes there are special ``__getattr__`` and ``__setattr__`` methods that we can use to hack the interpreter and enforce ``KernelImmutable`` restrictions.
There is no equivalent for modules/globals.
lriesebos
commented
Yes, @sb10q showed a POC for immutable instance variables in #5, and I agree this does not work for global variables. For me, the reasoning was to type global variables as Yes, @sb10q showed a POC for immutable instance variables in #5, and I agree this does not work for global variables.
For me, the reasoning was to type global variables as `KernelImmutable[]` because they are per definition kernel immutable. That would align their type annotation with instance variables. Though since we can not enforce that at runtime, globals will anyway be a special case that needs documentation. So as mentioned before, I am also fine with not typing it `KernelImmutable[]`.
|
||||
kernel.
|
||||
|
||||
The value would be substituted at compile time, subsequent modification in the
|
||||
host would not be known by the kernel.
|
||||
|
||||
Modification in kernel code to the global variables is not allowed.
|
||||
Only types supported in the kernel can be referenced.
|
||||
|
||||
## Class and Functions
|
||||
* Class fields must be annotated:
|
||||
```py
|
||||
* Instance variables must be annotated: (Issue #1)
|
||||
```python
|
||||
class Foo:
|
||||
a: int
|
||||
b: int
|
||||
|
@ -29,24 +28,24 @@ Modification in kernel code to the global variables is not allowed.
|
|||
self.a = a
|
||||
self.b = b
|
||||
```
|
||||
* Three types of instance variables: (Issue #5)
|
||||
* Host only variables: Do not add type annotation for it in the class.
|
||||
lriesebos
commented
Would it still be possible to type host-only variables and class variables?
Would it still be possible to type host-only variables and class variables?
```python
class Foo:
a: int
c: ClassVar[int] # What will this mean?
def __init__(self):
self.a = 1 # Normal / kernel-only variable
self.b: str = 'bar' # Host-only variable, but still typed. Is this possible?
```
pca006132
commented
I did not think about that, maybe we can add some new syntax similar to ClassVar for host only variables? I did not think about that, maybe we can add some new syntax similar to ClassVar for host only variables?
sb10q
commented
Sounds a bit messy and would perhaps negate the advantages of host type annotations (e.g. third-party tools may not understand Sounds a bit messy and would perhaps negate the advantages of host type annotations (e.g. third-party tools may not understand ``ClassVar`` and complain that the type annotation is incorrect).
I propose not supporting host type annotations in kernel classes; anything that is annotated goes on the device.
lriesebos
commented
We type practically everything in our DAX library, and I think it would be good to still allow typing of host code. Maybe you like one of these two ideas: Allow host-only variable typing in methods or by using
Make kernel-only variable typing special using
We type practically everything in [our DAX library](https://gitlab.com/duke-artiq/dax), and I think it would be good to still allow typing of host code. Maybe you like one of these two ideas:
**Allow host-only variable typing in methods or by using `Immutable[]`**
```python
class Foo:
a: int # Kernel-only variable
c: Immutable[ClassVar[int]] # Still allows host-variable typing
def __init__(self):
self.a = 1 # Normal / kernel-only variable
self.b: str = 'bar' # Host-only variable typed in method
```
**Make kernel-only variable typing special using `Kernel[]` and be fully compatible with regular Python typing**
```python
class Foo:
a: Kernel[int] # Use Kernel[] to type kernel-only variables
c: ClassVar[int] # Host-only typing remains vanilla
d: str # Also host-only
def __init__(self):
self.a = 1 # Normal / kernel-only variable
self.b: str = 'bar' # Host-only variable
```
sb10q
commented
That option would be ok, but might need better names than > Make kernel-only variable typing special using Kernel[] and be fully compatible with regular Python typing
That option would be ok, but might need better names than `Kernel` (and `Immutable`).
sb10q
commented
Maybe Maybe `Kernel[]` / `KernelImmutable[]` since the two annotations are similar. And we would use "kernel" terminology in nac3core.
lriesebos
commented
Yes, I think Yes, I think `Kernel[]` and `KernelImmutable[]` is great and I like the fact that it is explicit!
pca006132
commented
Yes, it is fine for me. Did you think about the syntax I mentioned in the screenshot? Using I'm not entirely sure if we can let IDE understand Yes, it is fine for me. Did you think about the syntax I mentioned in the screenshot? Using `Immutable(...)` instead of `Immutable[]`.
I'm not entirely sure if we can let IDE understand `Immutable[T] == T` for completion and checking, but we can do that with `Immutable(T)`. However, one caveat is that it would be harder to let users to define the type elsewhere and use it, as the information of `Immutable()` would be loss. Maybe we should have a look at how typing does it for other generics.
lriesebos
commented
Tbh, I do not think the IDE autocomplete feature should be leading this choice. And besides, IDE's understand that To further motivate my point of view, type checkers such as mypy go totally bad when using parenthesis. See for example this error message for
Tbh, I do not think the IDE autocomplete feature should be leading this choice. And besides, IDE's understand that `ClassVar[T] == T`, so it would not surprise me if we can also make that happen for `Kernel[]` and `KernelImmutable[]`. Brackets are the syntax used for typing in Python, so I would prefer we stick with that.
To further motivate my point of view, type checkers such as mypy go totally bad when using parenthesis. See for example this error message for `TArray()` (ARTIQ 6):
```
dax/modules/rtio_benchmark.py:206: error: Invalid type comment or annotation [valid-type]
dax/modules/rtio_benchmark.py:206: note: Suggestion: use TArray[...] instead of TArray(...)
```
lriesebos
commented
@pca006132 @sb10q did we now decide on the @pca006132 @sb10q did we now decide on the `Kernel[]` and `KernelImmutable[]` annotations? It is not contained in this merge request.
pca006132
commented
Sorry I forgot this one, this is two months ago :(. I think Sorry I forgot this one, this is two months ago :(.
I think `Kernel` is used for kernel only attributes, and `KernelImmutable` is for kernel invariants right? Can `KernelImmutable` be changed by the host? I think it is OK, or we can open another issue If we want further discussion about this.
sb10q
commented
As per the discussion in #5, an attribute with As per the discussion in https://git.m-labs.hk/M-Labs/nac3-spec/issues/5, an attribute with ``KernelImmutable`` can be modified by the host, but only when no kernel is running. In particular, it cannot be modified inside a RPC call, and this would be enforced in the Python interpreter via the ``@kernel`` decorator on the class.
|
||||
* Kernel Invariants: Immutable in the kernel and in the host while the kernel
|
||||
is executing. Type: `KernelInvariant(T)`. The types must be immutable.
|
||||
pca006132 marked this conversation as resolved
Outdated
lriesebos
commented
Should we use brackets instead, just like used for all other typing? Should we use brackets instead, just like used for all other typing? `KernelInvariant[T]`
sb10q
commented
I think so. I think so.
pca006132
commented
Yes, I would fix this later. Yes, I would fix this later.
|
||||
(use tuple instead of list in the host, but the type annotation should still
|
||||
be list?)
|
||||
* Normal Variables: The host can only assign to them in the `__init__`
|
||||
function. Not accessible afterwards.
|
||||
* Functions require full type signature, including type annotation to every
|
||||
parameter and return type.
|
||||
```py
|
||||
```python
|
||||
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, which is a potential source of
|
||||
confusion. (maybe we can allow primitive default types?)
|
||||
* Cannot construct objects within kernel code.
|
||||
|
||||
Questions:
|
||||
* Can we construct objects within kernel code?
|
||||
* Should we support function pointers? What about subtyping with function
|
||||
pointers, and generic types?
|
||||
* Function default parameters must be immutable.
|
||||
* Function pointers are supported, and lambda expression is not supported
|
||||
pca006132 marked this conversation as resolved
Outdated
lriesebos
commented
How do we expect to type function pointers, using the typing library? e.g. How do we expect to type function pointers, using the typing library? e.g. `Call[[int32, int32], int32]`
pca006132
commented
Yes Yes
|
||||
currently. (maybe support lambda after implementing type inference?)
|
||||
|
||||
## Built-in Types
|
||||
* Primitive types include:
|
||||
|
@ -57,14 +56,14 @@ Questions:
|
|||
* `uint32`
|
||||
* `uint64`
|
||||
* `float`
|
||||
* `str` (note: fixed length, provide builtin methods?)
|
||||
* `bytes` (a list of `byte`, but with more convenient syntax)
|
||||
* `str`
|
||||
* `bytes`
|
||||
* 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:
|
||||
* `tuple`: inhomogeneous immutable list, only pattern
|
||||
matching (e.g. `a, b, c = (1, True, 1.2)`) and constant indexing is
|
||||
supported:
|
||||
```
|
||||
t = (1, True)
|
||||
# OK
|
||||
|
@ -75,23 +74,25 @@ Questions:
|
|||
i = 0
|
||||
a = t[i]
|
||||
```
|
||||
* `range` (over numerical types) (not sure if this is really useful)
|
||||
* `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.
|
||||
* All binary operations expect the values to have the same type.
|
||||
* 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.
|
||||
* Integers are treated as `int32` by default. Floating point numbers are double
|
||||
by default.
|
||||
* No implicit coercion, require implicit cast.
|
||||
For integers that don't fit in int32, users should cast them to `int64`
|
||||
explicitly, i.e. `int64(2147483648)`. If the compiler found that the integer
|
||||
does not fit into int32, it would raise an error. (Issue #2)
|
||||
* 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
|
||||
```python
|
||||
from typing import TypeVar
|
||||
lriesebos
commented
Does this just refer to kernel-only classes or is this a general question?
Does this just refer to kernel-only classes or is this a general question?
- In general, `@portable` is a decorator we use very often, so I would like to keep that.
- For kernel-only classes, portable functions sound contradicting
- For portable classes (if existing), that might be useful but I do not have a use case for that right now.
sb10q
commented
Yes, we definitely want Yes, we definitely want ``@portable``.
pca006132
commented
Portable would be for testing the code in the host, similar to the use case of portable functions I guess. Portable would be for testing the code in the host, similar to the use case of portable functions I guess.
sb10q
commented
Not just testing. One use case is converting between SI units (e.g. a floating point number in volts) and chip control words (e.g. integer values to program into a DAC). This allows 1. caching the latter and 2. determination of quantization errors. Not just testing. One use case is converting between SI units (e.g. a floating point number in volts) and chip control words (e.g. integer values to program into a DAC). This allows 1. caching the latter and 2. determination of quantization errors.
pca006132
commented
Perhaps we should give it another name? Kernel only seems a poor naming choice, I chose it previously because the object is created and destroyed in the kernel, the host has no access to it. Actually this is just an ordinary object. Or maybe we should call the object of type Perhaps we should give it another name? Kernel only seems a poor naming choice, I chose it previously because the object is created and destroyed in the kernel, the host has no access to it. Actually this is just an ordinary object.
Or maybe we should call the object of type `EnvExperiment` something like experiment object? I'm not good at naming things...
sb10q
commented
Kernel-only sounds fine to me.
Kernel-only sounds fine to me.
`EnvExperiment` is not specific to the core device (you can perfectly create an ARTIQ experiment with this class, that does not use the core device and the compiler at all) and I had also suggested to [use a decorator instead of inheritance](https://git.m-labs.hk/M-Labs/nac3-spec/issues/5#issuecomment-2022) to have more possibilities to implement immutability.
pca006132
commented
I've updated the explanation, but still I'm not entirely sure if the name should be kernel only class... It does sound weird when we allow it to be portable... I've updated the explanation, but still I'm not entirely sure if the name should be kernel only class... It does sound weird when we allow it to be portable...
lriesebos
commented
So if I understand it correctly now, a kernel-only class is a class decorated with
Regarding the kernel-only name, I am fine with that. So if I understand it correctly now, a kernel-only class is a class decorated with `@kernel` or `@portable`.
- Both can be constructed inside kernels
- `@kernel` classes can only be used in kernels
- `@portable` classes can be constructed in kernels and on the host. Besides, objects can be shared between the kernel and the host
Regarding the kernel-only name, I am fine with that.
sb10q
commented
Or maybe, have a single type of "kernel-capable" class, decorated with Or maybe, have a single type of "kernel-capable" class, decorated with ``@kernel``.
sb10q
commented
That decorator simply registers the class with the compiler, and enforces That decorator simply registers the class with the compiler, and enforces ``Immutable`` in the interpreter. There are no restrictions on the type of methods (kernel, portable, rpc, host-only) that it contains.
lriesebos
commented
Ok great, then I guess I misunderstood the situation when read it and what we discussed in #5 is still valid. So to summarize:
Ok great, then I guess I misunderstood the situation when read it and what we discussed in #5 is still valid. So to summarize:
```python
from artiq.experiment import *
@kernel # Register class with the compiler and more
class Foo(EnvExperiment):
def __init__(self):
pass
def run(self) -> None: # Host-only function
self.kernel_fn() # Runs a kernel
self.rpc_fn() # Runs function on host
self.portable_fn() # Runs function on host
self.host_only_fn() # Runs function on host
@kernel
def kernel_fn(self) -> None: # Kernel function
self.rpc_fn() # RPC call
self.portable_fn() # Portable, so remains in kernel
@rpc
def rpc_fn(self) -> None: # RPC function
pass
@portable
def portable_fn(self) -> None # Portable function
pass
def host_only_fn(self) -> None # Host-only function
pass
```
pca006132
commented
This is exactly what I mean by kernel-only class.
This is different, I don't expect kernel-only class to contain host-only/rpc methods, and the semantic for kernel and portable functions are different. It would not be possible to call a method of a kernel object on the host if it is not portable. I think the kernel-only object is just a simple object in our language, that can be fully-understood by the compiler, without dealing with the complicated stuff like RPC and host functions etc. > So if I understand it correctly now, a kernel-only class is a class decorated with @kernel or @portable.
>
> Both can be constructed inside kernels
> @kernel classes can only be used in kernels
> @portable classes can be constructed in kernels and on the host. Besides, objects can be shared between the kernel and the host
This is exactly what I mean by kernel-only class.
> Or maybe, have a single type of "kernel-capable" class, decorated with @kernel.
> That decorator simply registers the class with the compiler, and enforces Immutable in the interpreter. There are no restrictions on the type of methods (kernel, portable, rpc, host-only) that it contains.
This is different, I don't expect kernel-only class to contain host-only/rpc methods, and the semantic for kernel and portable functions are different. It would not be possible to call a method of a kernel object on the host if it is not portable.
I think the kernel-only object is just a simple object in our language, that can be fully-understood by the compiler, without dealing with the complicated stuff like RPC and host functions etc.
sb10q
commented
What is the advantage of kernel-only classes? We'd need to support classes that mix kernel, portable, rpc, and host-only methods anyway. What is the advantage of kernel-only classes? We'd need to support classes that mix kernel, portable, rpc, and host-only methods anyway.
lriesebos
commented
I agree with @sb10q that "mixed" classes are a must. Kernel-only classes could be interesting to allow construction of objects inside kernels, though tbh at this moment I do not have any use-case for that. I agree with @sb10q that "mixed" classes are a must. Kernel-only classes could be interesting to allow construction of objects inside kernels, though tbh at this moment I do not have any use-case for that.
sb10q
commented
Objects that can be constructed inside kernels could have Objects that can be constructed inside kernels could have ``__init__`` decorated with ``@portable`` or ``@kernel``. But I'm not sure if we want to support them anyway.
pca006132
commented
If the object is stored in the kernel only, RPC would have to pass the object to the host and run the method. But that probably seems fine... If the object is stored in the kernel only, RPC would have to pass the object to the host and run the method. But that probably seems fine...
lriesebos
commented
@pca006132 @sb10q also, I think the proposal in this tread makes sense, which would drop "kernel-only" classes. It is not contained in the current merge request. Should we still add this? @pca006132 @sb10q also, I think the proposal in this tread makes sense, which would drop "kernel-only" classes. It is not contained in the current merge request. Should we still add this?
pca006132
commented
Yes, I think we can drop this, but we should make it explicit that users can create objects in the kernel if the constructor is Yes, I think we can drop this, but we should make it explicit that users can create objects in the kernel if the constructor is `portable` or `kernel`. And maybe we should also document the behavior for RPC when the object only exists on the kernel. What do you think?
sb10q
commented
@pca006132 Sounds good. @pca006132 Sounds good.
|
||||
T = TypeVar('T')
|
||||
|
||||
|
@ -102,16 +103,13 @@ class Foo(EnvExperiment):
|
|||
return a == b
|
||||
```
|
||||
|
||||
* Type variables can only be used in functions/methods, but not in classes.
|
||||
(this can be relaxed, only allow those with type variables fully defined
|
||||
from the constructor)
|
||||
* Type variable can be limited to a fixed set of types.
|
||||
* 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
|
||||
```python
|
||||
def add1(x: Union[int, bool]) -> int:
|
||||
if type(x) == int:
|
||||
# x is int
|
||||
|
@ -131,48 +129,6 @@ Questions:
|
|||
* Would it be better to assert on the type variable directly instead of
|
||||
`type(x)` for type guards?
|
||||
|
||||
## 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, this list cannot be typed (inhomogeneous)
|
||||
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[...]`.
|
||||
|
||||
> Not sure what is the best syntax for `virtual[...]`
|
||||
|
||||
Example:
|
||||
```py
|
||||
def bar2(x: list[virtual[Base]]) -> int:
|
||||
sum = 0
|
||||
for v in x:
|
||||
sum += v.foo()
|
||||
return sum
|
||||
```
|
||||
|
||||
## Lifetime
|
||||
Probably need more discussions...
|
||||
|
||||
|
|
I am still confused what the idea is about the use of 'global'. Is this what you mean?
yes