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