# NAC3 Specification 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) Only primitive types and tuple/list of primitive types are allowed. 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. ## 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, 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? ## 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) (not sure if this is really useful) ### 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. (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 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 cannot occur alone in the result type, i.e. must be bound to the input parameters. Questions: * Should we support things like optional type? (like the one in rust) * 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 ```