Kernel only attribute annotation (#76) #127

Merged
sb10q merged 8 commits from kernel_only_annotation into master 2021-12-19 11:04:53 +08:00
2 changed files with 86 additions and 16 deletions
Showing only changes of commit 6d65ee90e8 - Show all commits

View File

@ -1,15 +1,15 @@
from inspect import getfullargspec from inspect import getfullargspec, isclass, getmro
from functools import wraps from functools import wraps
from types import SimpleNamespace from types import SimpleNamespace
from numpy import int32, int64 from numpy import int32, int64
from typing import Generic, TypeVar from typing import Generic, TypeVar, get_origin
from math import floor, ceil from math import floor, ceil
import nac3artiq import nac3artiq
__all__ = [ __all__ = [
"KernelInvariant", "virtual", "KernelInvariant", "Kernel", "virtual",
"round64", "floor64", "ceil64", "round64", "floor64", "ceil64",
"extern", "kernel", "portable", "nac3", "extern", "kernel", "portable", "nac3",
"ms", "us", "ns", "ms", "us", "ns",
@ -24,11 +24,13 @@ T = TypeVar('T')
class KernelInvariant(Generic[T]): class KernelInvariant(Generic[T]):
pass pass
class Kernel(Generic[T]):
pass
# The virtual class must exist before nac3artiq.NAC3 is created. # The virtual class must exist before nac3artiq.NAC3 is created.
class virtual(Generic[T]): class virtual(Generic[T]):
pass pass
def round64(x): def round64(x):
return round(x) return round(x)
@ -44,6 +46,7 @@ core_arguments = device_db.device_db["core"]["arguments"]
compiler = nac3artiq.NAC3(core_arguments["target"]) compiler = nac3artiq.NAC3(core_arguments["target"])
allow_registration = True allow_registration = True
allow_kernel_read = False
# Delay NAC3 analysis until all referenced variables are supposed to exist on the CPython side. # Delay NAC3 analysis until all referenced variables are supposed to exist on the CPython side.
registered_functions = set() registered_functions = set()
registered_classes = set() registered_classes = set()
@ -90,7 +93,65 @@ def nac3(cls):
Decorates a class to be analyzed by NAC3. Decorates a class to be analyzed by NAC3.
All classes containing kernels or portable methods must use this decorator. All classes containing kernels or portable methods must use this decorator.
""" """
# python does not allow setting magic method on specific instances
# (https://docs.python.org/3/reference/datamodel.html#special-method-lookup).
# use this set to keep track of those custom class instances that are
# assigned to the `Kernel` fields of a class
cls.__nac3_kernel_only_instances__ = set()
def apply_kernel_only_constraints(val):
kernel_only_set = getattr(type(val), '__nac3_kernel_only_instances__', None)
if kernel_only_set is None:
return
else:
for (_, attr_val) in val.__dict__.items():
if not (attr_val == val):
apply_kernel_only_constraints(attr_val)
kernel_only_set.add(val)
if not isclass(cls):
raise ValueError("nac3 annotation decorator should only be applied to classes")
if not cls.__setattr__ in {base.__setattr__ for base in cls.__bases__}:
raise ValueError("custom __setattr__ is not supported in kernel classes")
Outdated
Review

We renamed them "nac3 classes" to avoid confusion with things like "kernel methods" - a nac3 class can contain a mix of kernel and host items.

We renamed them "nac3 classes" to avoid confusion with things like "kernel methods" - a nac3 class can contain a mix of kernel and host items.
register_class(cls) register_class(cls)
immutable_fields = {
n for b in getmro(cls)
for (n, ty) in b.__dict__.get('__annotations__', {}).items() if get_origin(ty) == Kernel
}
def __setattr__(obj, key, value):
if obj in type(obj).__nac3_kernel_only_instances__:
raise TypeError("attempting to write to kernel only variable")
# should allow init to set value, if no attribute then allow to set attr, then
# recursively apply constraint to all the fields of that specific object,
# regardless of whether they are marked with `Kernel` or not
if key in immutable_fields:
if hasattr(obj, key):
raise TypeError("attempting to write to kernel only variable")
else:
apply_kernel_only_constraints(value)
object.__setattr__(obj, key, value)
def __getattribute__(obj, key):
# need to use `object.__getattribute__` to get attr before checking
# the key in immutable_fields for `__init__`.
# since that in `__init__` when setting a instance variable like `self.a = 3`
# the sequence of internal magic call is still calling cls.__getattribute__(self, 'a')
# first, and if only "AttributeError" is raised, it will then call `__setattr__`
# if we raise `TypeError` too early, python will just halt at this `TypeError`.
attr = object.__getattribute__(obj, key)
if not allow_kernel_read:
if obj in type(obj).__nac3_kernel_only_instances__:
raise TypeError("attempting to read kernel only variable")
if key in immutable_fields:
raise TypeError("attempting to read kernel only variable")
return attr
cls.__setattr__ = __setattr__
cls.__getattribute__ = __getattribute__
return cls return cls
@ -142,6 +203,7 @@ class Core:
self.ref_period = core_arguments["ref_period"] self.ref_period = core_arguments["ref_period"]
def run(self, method, *args, **kwargs): def run(self, method, *args, **kwargs):
global allow_kernel_read
global allow_registration global allow_registration
if allow_registration: if allow_registration:
compiler.analyze(registered_functions, registered_classes) compiler.analyze(registered_functions, registered_classes)
@ -153,8 +215,9 @@ class Core:
else: else:
obj = method obj = method
name = "" name = ""
allow_kernel_read = True
compiler.compile_method_to_file(obj, name, args, "module.elf") compiler.compile_method_to_file(obj, name, args, "module.elf")
allow_kernel_read = False
@kernel @kernel
def reset(self): def reset(self):

View File

@ -1058,20 +1058,27 @@ impl TopLevelComposer {
let dummy_field_type = unifier.get_fresh_var().0; let dummy_field_type = unifier.get_fresh_var().0;
// handle Kernel[T], KernelInvariant[T] // handle Kernel[T], KernelInvariant[T]
let (annotation, mutable) = { let (annotation, mutable) = match &annotation.node {
let mut result = None; ast::ExprKind::Subscript { value, slice, .. }
if let ast::ExprKind::Subscript { value, slice, .. } = &annotation.as_ref().node { if matches!(&value.node, ast::ExprKind::Name { id, .. } if id == &"Kernel".into()) =>
Outdated
Review

Again there is quite some inconsistency with match-by-name vs match-by-id, the virtual class seems to be to some extent match-by-id...

Again there is quite some inconsistency with match-by-name vs match-by-id, the ``virtual`` class seems to be to some extent match-by-id...
if let ast::ExprKind::Name { id, .. } = &value.node { {
result = if id == &"Kernel".into() { match &slice.node {
Some((slice, true)) ast::ExprKind::Subscript { value, .. }
} else if id == &"KernelInvariant".into() { if matches!(&value.node, ast::ExprKind::Name { id, .. } if id == &"list".into()) =>
Some((slice, false)) {
} else { return Err(format!("list is not allowed to be `Kernel` at {}", value.location))
Outdated
Review

This is too restrictive. The restriction is worse than the possibility of bugs from the user mutating the list unexpectedly.

This is too restrictive. The restriction is worse than the possibility of bugs from the user mutating the list unexpectedly.
None }
_ => (slice, true)
} }
} }
ast::ExprKind::Subscript { value, slice, .. }
if matches!(&value.node, ast::ExprKind::Name { id, .. } if id == &"KernelInvariant".into()) => {
(slice, false)
}
_ => {
eprintln!("attributes not annotated with `Kernel` or `KernelInvariants` at {}", &annotation.location);
Outdated
Review

As before, please remove debug prints.
This is also not an error, if it is not annotated with Kernel or KernelInvariant it is simply a host-only attribute.

As before, please remove debug prints. This is also not an error, if it is not annotated with ``Kernel`` or ``KernelInvariant`` it is simply a host-only attribute.
(annotation, true)
} }
result.unwrap_or((annotation, true))
}; };
class_fields_def.push((*attr, dummy_field_type, mutable)); class_fields_def.push((*attr, dummy_field_type, mutable));