Go to file
2020-12-16 16:04:42 +08:00
README.md notes about optional parameter value 2020-12-16 16:04:42 +08:00

NAC3 Specification

Specification and discussions about language design.

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:

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:
    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.
    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.
  • Value of the optional parameter would be created by the caller, and independent between different invokations. The following code would always output 2 1, instead of 1 and 2 for actual Python:
    def test(a = [1]):
        print(a)
        a[0] += 1
    
    test()
    test()
    

Generics

We use type variable for denoting generics.

Example:

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 class fields.
  • 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. 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 works.
    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:

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 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:

def bar2(x: list[virtual[Base]]) -> int:
    sum = 0
    for v in x:
        sum += v.foo()
    return sum
# correct
bar([Base(), Derived()])