forked from M-Labs/artiq
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:
parent
6ee15fbcae
commit
fb2b634c4a
|
@ -765,6 +765,9 @@ class Stitcher:
|
|||
if hasattr(function, 'artiq_embedded') and 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
|
||||
line = function.__code__.co_firstlineno
|
||||
name = function.__code__.co_name
|
||||
|
@ -848,10 +851,20 @@ class Stitcher:
|
|||
|
||||
# Extract function source.
|
||||
embedded_function = host_function.artiq_embedded.function
|
||||
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
|
||||
if isinstance(embedded_function, str):
|
||||
# This is a function to be eval'd from the given source code in string form.
|
||||
# Mangle the host function's id() into the fully qualified name to make sure
|
||||
# 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.
|
||||
signature = inspect.signature(embedded_function)
|
||||
|
@ -937,6 +950,9 @@ class Stitcher:
|
|||
return function_node
|
||||
|
||||
def _extract_annot(self, function, annot, kind, call_loc, fn_kind):
|
||||
if annot is None:
|
||||
annot = builtins.TNone()
|
||||
|
||||
if not isinstance(annot, types.Type):
|
||||
diag = diagnostic.Diagnostic("error",
|
||||
"type annotation for {kind}, '{annot}', is not an ARTIQ type",
|
||||
|
|
|
@ -8,7 +8,7 @@ import numpy
|
|||
|
||||
|
||||
__all__ = ["kernel", "portable", "rpc", "syscall", "host_only",
|
||||
"set_time_manager", "set_watchdog_factory",
|
||||
"kernel_from_string", "set_time_manager", "set_watchdog_factory",
|
||||
"TerminationRequested"]
|
||||
|
||||
# global namespace for kernels
|
||||
|
@ -140,6 +140,59 @@ def host_only(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:
|
||||
def _not_implemented(self, *args, **kwargs):
|
||||
raise NotImplementedError(
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue