From fb2b634c4a90c49bf696ccca050b84051ece62f3 Mon Sep 17 00:00:00 2001 From: David Nadlinger Date: Tue, 17 Dec 2019 19:10:33 +0000 Subject: [PATCH] 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. --- artiq/compiler/embedding.py | 24 +++++++++++--- artiq/language/core.py | 55 +++++++++++++++++++++++++++++++- artiq/test/lit/embedding/eval.py | 18 +++++++++++ 3 files changed, 92 insertions(+), 5 deletions(-) create mode 100644 artiq/test/lit/embedding/eval.py diff --git a/artiq/compiler/embedding.py b/artiq/compiler/embedding.py index 2b5c9a416..fb0a8be39 100644 --- a/artiq/compiler/embedding.py +++ b/artiq/compiler/embedding.py @@ -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, ""), 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 = "" + 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", diff --git a/artiq/language/core.py b/artiq/language/core.py index fe1a7e814..5560398dd 100644 --- a/artiq/language/core.py +++ b/artiq/language/core.py @@ -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( diff --git a/artiq/test/lit/embedding/eval.py b/artiq/test/lit/embedding/eval.py new file mode 100644 index 000000000..d2f91cbf9 --- /dev/null +++ b/artiq/test/lit/embedding/eval.py @@ -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