compiler, language: Implement @kernel_from_string

With support for polymorphism (or type erasure on pointers to
member functions) being absent in the ARTIQ compiler, code
generation is vital to be able to implement abstractions that
work with user-provided lists/trees of objects with uniform
interfaces (e.g. a common base class, or duck typing), but
different concrete types.

@kernel_from_string has been in production use for exactly
this use case in Oxford for the better part of a year now
(various places in ndscan).

GitHub: Fixes #1089.
This commit is contained in:
David Nadlinger 2019-12-17 19:10:33 +00:00 committed by Sébastien Bourdeauducq
parent 6ee15fbcae
commit fb2b634c4a
3 changed files with 92 additions and 5 deletions

View File

@ -765,6 +765,9 @@ class Stitcher:
if hasattr(function, 'artiq_embedded') and function.artiq_embedded.function: if hasattr(function, 'artiq_embedded') and function.artiq_embedded.function:
function = function.artiq_embedded.function function = function.artiq_embedded.function
if isinstance(function, str):
return source.Range(source.Buffer(function, "<string>"), 0, 0)
filename = function.__code__.co_filename filename = function.__code__.co_filename
line = function.__code__.co_firstlineno line = function.__code__.co_firstlineno
name = function.__code__.co_name name = function.__code__.co_name
@ -848,10 +851,20 @@ class Stitcher:
# Extract function source. # Extract function source.
embedded_function = host_function.artiq_embedded.function embedded_function = host_function.artiq_embedded.function
source_code = inspect.getsource(embedded_function) if isinstance(embedded_function, str):
filename = embedded_function.__code__.co_filename # This is a function to be eval'd from the given source code in string form.
module_name = embedded_function.__globals__['__name__'] # Mangle the host function's id() into the fully qualified name to make sure
first_line = embedded_function.__code__.co_firstlineno # there are no collisions.
source_code = embedded_function
embedded_function = host_function
filename = "<string>"
module_name = "__eval_{}".format(id(host_function))
first_line = 1
else:
source_code = inspect.getsource(embedded_function)
filename = embedded_function.__code__.co_filename
module_name = embedded_function.__globals__['__name__']
first_line = embedded_function.__code__.co_firstlineno
# Extract function annotation. # Extract function annotation.
signature = inspect.signature(embedded_function) signature = inspect.signature(embedded_function)
@ -937,6 +950,9 @@ class Stitcher:
return function_node return function_node
def _extract_annot(self, function, annot, kind, call_loc, fn_kind): def _extract_annot(self, function, annot, kind, call_loc, fn_kind):
if annot is None:
annot = builtins.TNone()
if not isinstance(annot, types.Type): if not isinstance(annot, types.Type):
diag = diagnostic.Diagnostic("error", diag = diagnostic.Diagnostic("error",
"type annotation for {kind}, '{annot}', is not an ARTIQ type", "type annotation for {kind}, '{annot}', is not an ARTIQ type",

View File

@ -8,7 +8,7 @@ import numpy
__all__ = ["kernel", "portable", "rpc", "syscall", "host_only", __all__ = ["kernel", "portable", "rpc", "syscall", "host_only",
"set_time_manager", "set_watchdog_factory", "kernel_from_string", "set_time_manager", "set_watchdog_factory",
"TerminationRequested"] "TerminationRequested"]
# global namespace for kernels # global namespace for kernels
@ -140,6 +140,59 @@ def host_only(function):
return function return function
def kernel_from_string(parameters, body_code, decorator=kernel):
"""Build a kernel function from the supplied source code in string form,
similar to ``exec()``/``eval()``.
Operating on pieces of source code as strings is a very brittle form of
metaprogramming; kernels generated like this are hard to debug, and
inconvenient to write. Nevertheless, this can sometimes be useful to work
around restrictions in ARTIQ Python. In that instance, care should be taken
to keep string-generated code to a minimum and cleanly separate it from
surrounding code.
The resulting function declaration is also evaluated using ``exec()`` for
use from host Python code. To encourage a modicum of code hygiene, no
global symbols are available by default; any objects accessed by the
function body must be passed in explicitly as parameters.
:param parameters: A list of parameter names the generated functions
accepts. Each entry can either be a string or a tuple of two strings;
if the latter, the second element specifies the type annotation.
:param body_code: The code for the function body, in string form.
``return`` statements can be used to return values, as usual.
:param decorator: One of ``kernel`` or ``portable`` (optionally with
parameters) to specify how the function will be executed.
:return: The function generated from the arguments.
"""
# Build complete function declaration.
decl = "def kernel_from_string_fn("
for p in parameters:
type_annotation = ""
if isinstance(p, tuple):
name, typ = p
type_annotation = ": " + typ
else:
name = p
decl += name + type_annotation + ","
decl += "):\n"
decl += "\n".join(" " + line for line in body_code.split("\n"))
# Evaluate to get host-side function declaration.
context = {}
try:
exec(decl, context)
except SyntaxError:
raise SyntaxError("Error parsing kernel function: '{}'".format(decl))
fn = decorator(context["kernel_from_string_fn"])
# Save source code for the compiler to pick up later.
fn.artiq_embedded = fn.artiq_embedded._replace(function=decl)
return fn
class _DummyTimeManager: class _DummyTimeManager:
def _not_implemented(self, *args, **kwargs): def _not_implemented(self, *args, **kwargs):
raise NotImplementedError( raise NotImplementedError(

View File

@ -0,0 +1,18 @@
# RUN: %python -m artiq.compiler.testbench.embedding %s
from artiq.language.core import *
def make_incrementer(increment):
return kernel_from_string(["a"], "return a + {}".format(increment),
portable)
foo = make_incrementer(1)
bar = make_incrementer(2)
@kernel
def entrypoint():
assert foo(4) == 5
assert bar(4) == 6