xref: /llvm-project/llvm/utils/TableGen/jupyter/tablegen_kernel/kernel.py (revision f8559751fc2b15b45ac417be9abe865085af45ad)
1210a383eSDavid Spickett# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2210a383eSDavid Spickett# See https://llvm.org/LICENSE.txt for license information.
3210a383eSDavid Spickett# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4210a383eSDavid Spickett
5210a383eSDavid Spickettimport os
6210a383eSDavid Spickettimport shutil
7210a383eSDavid Spickettimport subprocess
8210a383eSDavid Spickettimport tempfile
9210a383eSDavid Spickettfrom ipykernel.kernelbase import Kernel
10210a383eSDavid Spickett
11210a383eSDavid Spickett__version__ = "0.0.1"
12210a383eSDavid Spickett
13210a383eSDavid Spickett
14fbec83cbSDavid Spickettclass TableGenKernelException(Exception):
15fbec83cbSDavid Spickett    pass
16fbec83cbSDavid Spickett
17fbec83cbSDavid Spickett
18210a383eSDavid Spickettclass TableGenKernel(Kernel):
19210a383eSDavid Spickett    """Kernel using llvm-tblgen inside jupyter.
20210a383eSDavid Spickett
21210a383eSDavid Spickett    All input is treated as TableGen unless the first non whitespace character
22210a383eSDavid Spickett    is "%" in which case it is a "magic" line.
23210a383eSDavid Spickett
24fbec83cbSDavid Spickett    The supported cell magic is:
2592787e3eSDavid Spickett    * %args    - to set the arguments passed to llvm-tblgen.
2692787e3eSDavid Spickett    * %reset   - to reset the cached code and magic state.
27fbec83cbSDavid Spickett    * %noreset - to not reset the cached code and magic state
28fbec83cbSDavid Spickett                 (useful when you have changed the default to always
29fbec83cbSDavid Spickett                  reset the cache).
30210a383eSDavid Spickett
3192787e3eSDavid Spickett    These are "cell magic" meaning it applies to the whole cell. Therefore
32210a383eSDavid Spickett    it must be the first line, or part of a run of magic lines starting
33210a383eSDavid Spickett    from the first line.
34210a383eSDavid Spickett
35fbec83cbSDavid Spickett    The following are global magic (that applies to all cells going
36fbec83cbSDavid Spickett    forward):
37fbec83cbSDavid Spickett    * %config  - to change the behaviour of the kernel overall, including
38fbec83cbSDavid Spickett                 changing defaults for things like resets.
39fbec83cbSDavid Spickett
40fbec83cbSDavid Spickett    Global magic must be written in the same way as cell magic.
41fbec83cbSDavid Spickett
42210a383eSDavid Spickett    ```tablgen
43210a383eSDavid Spickett    %args
4492787e3eSDavid Spickett    %reset
45210a383eSDavid Spickett    %args --print-records --print-detailed-records
46210a383eSDavid Spickett    class Stuff {
47210a383eSDavid Spickett      string Name;
48210a383eSDavid Spickett    }
49210a383eSDavid Spickett
50210a383eSDavid Spickett    def a_thing : Stuff {}
51210a383eSDavid Spickett    ```
52210a383eSDavid Spickett
53210a383eSDavid Spickett    """
54210a383eSDavid Spickett
55210a383eSDavid Spickett    implementation = "tablegen"
56210a383eSDavid Spickett    implementation_version = __version__
57210a383eSDavid Spickett
58210a383eSDavid Spickett    language_version = __version__
59210a383eSDavid Spickett    language = "tablegen"
60210a383eSDavid Spickett    language_info = {
61210a383eSDavid Spickett        "name": "tablegen",
62210a383eSDavid Spickett        "mimetype": "text/x-tablegen",
63210a383eSDavid Spickett        "file_extension": ".td",
64210a383eSDavid Spickett        "pygments_lexer": "text",
65210a383eSDavid Spickett    }
66210a383eSDavid Spickett
67210a383eSDavid Spickett    def __init__(self, **kwargs):
68210a383eSDavid Spickett        Kernel.__init__(self, **kwargs)
69210a383eSDavid Spickett        self._executable = None
7092787e3eSDavid Spickett        # A list of (code, magic) tuples.
7192787e3eSDavid Spickett        # All the previous cell's code we have run since the last reset.
7292787e3eSDavid Spickett        # This emulates a persistent state like a Python interpreter would have.
7392787e3eSDavid Spickett        self._previous_code = ""
7492787e3eSDavid Spickett        # The most recent set of magic since the last reset.
7592787e3eSDavid Spickett        self._previous_magic = {}
76fbec83cbSDavid Spickett        # The default cache reset behaviour. True means do not cache anything
77fbec83cbSDavid Spickett        # between cells.
78fbec83cbSDavid Spickett        self._cell_reset = False
79210a383eSDavid Spickett
80210a383eSDavid Spickett    @property
81210a383eSDavid Spickett    def banner(self):
82210a383eSDavid Spickett        return "llvm-tblgen kernel %s" % __version__
83210a383eSDavid Spickett
84fdd18e86SDavid Spickett    def get_executable(self):
85210a383eSDavid Spickett        """If this is the first run, search for llvm-tblgen.
86210a383eSDavid Spickett        Otherwise return the cached path to it."""
87210a383eSDavid Spickett        if self._executable is None:
88210a383eSDavid Spickett            path = os.environ.get("LLVM_TBLGEN_EXECUTABLE")
89210a383eSDavid Spickett            if path is not None and os.path.isfile(path) and os.access(path, os.X_OK):
90210a383eSDavid Spickett                self._executable = path
91210a383eSDavid Spickett            else:
92210a383eSDavid Spickett                path = shutil.which("llvm-tblgen")
93210a383eSDavid Spickett                if path is None:
94fdd18e86SDavid Spickett                    raise OSError(
95fdd18e86SDavid Spickett                        "llvm-tblgen not found. Put it on your PATH or set the"
96fdd18e86SDavid Spickett                        " environment variable LLVM_TBLGEN_EXECUTABLE to point to it."
97fdd18e86SDavid Spickett                    )
98210a383eSDavid Spickett                self._executable = path
99210a383eSDavid Spickett
100210a383eSDavid Spickett        return self._executable
101210a383eSDavid Spickett
102fbec83cbSDavid Spickett    def parse_config_magic(self, config):
103fbec83cbSDavid Spickett        """Config should be a list of parameters given to the %config command.
104fbec83cbSDavid Spickett        We allow only one setting per %config line and that setting can only
105fbec83cbSDavid Spickett        have one value.
106fbec83cbSDavid Spickett
107fbec83cbSDavid Spickett        Assuming the parameters are valid, update the kernel's setting with
108fbec83cbSDavid Spickett        the new value.
109fbec83cbSDavid Spickett
110fbec83cbSDavid Spickett        If there is an error, raise a TableGenKernelException.
111fbec83cbSDavid Spickett
112fbec83cbSDavid Spickett        >>> k.parse_config_magic([])
113fbec83cbSDavid Spickett        Traceback (most recent call last):
114fbec83cbSDavid Spickett         ...
115fbec83cbSDavid Spickett        TableGenKernelException: Incorrect number of parameters to %config. Expected %config <setting> <value>.
116fbec83cbSDavid Spickett        >>> k._cell_reset
117fbec83cbSDavid Spickett        False
118fbec83cbSDavid Spickett        >>> k.parse_config_magic(["a", "b", "c"])
119fbec83cbSDavid Spickett        Traceback (most recent call last):
120fbec83cbSDavid Spickett         ...
121fbec83cbSDavid Spickett        TableGenKernelException: Incorrect number of parameters to %config. Expected %config <setting> <value>.
122fbec83cbSDavid Spickett        >>> k.parse_config_magic(["notasetting", "..."])
123fbec83cbSDavid Spickett        Traceback (most recent call last):
124fbec83cbSDavid Spickett         ...
125fbec83cbSDavid Spickett        TableGenKernelException: Unknown kernel setting "notasetting". Possible settings are: "cellreset".
126fbec83cbSDavid Spickett        >>> k.parse_config_magic(["cellreset", "food"])
127fbec83cbSDavid Spickett        Traceback (most recent call last):
128fbec83cbSDavid Spickett         ...
129fbec83cbSDavid Spickett        TableGenKernelException: Invalid value for setting "cellreset", expected "on" or "off".
130fbec83cbSDavid Spickett        >>> k.parse_config_magic(["cellreset", "on"])
131fbec83cbSDavid Spickett        >>> k._cell_reset
132fbec83cbSDavid Spickett        True
133fbec83cbSDavid Spickett        >>> k.parse_config_magic(["cellreset", "off"])
134fbec83cbSDavid Spickett        >>> k._cell_reset
135fbec83cbSDavid Spickett        False
136fbec83cbSDavid Spickett        """
137fbec83cbSDavid Spickett        if len(config) != 2:
138fbec83cbSDavid Spickett            raise TableGenKernelException(
139fbec83cbSDavid Spickett                "Incorrect number of parameters to %config. Expected %config <setting> <value>."
140fbec83cbSDavid Spickett            )
141fbec83cbSDavid Spickett
142fbec83cbSDavid Spickett        name, value = config
143fbec83cbSDavid Spickett        if name != "cellreset":
144fbec83cbSDavid Spickett            raise TableGenKernelException(
145fbec83cbSDavid Spickett                'Unknown kernel setting "{}". '
146fbec83cbSDavid Spickett                'Possible settings are: "cellreset".'.format(name)
147fbec83cbSDavid Spickett            )
148fbec83cbSDavid Spickett
149fbec83cbSDavid Spickett        try:
150fbec83cbSDavid Spickett            self._cell_reset = {"on": True, "off": False}[value.lower()]
151fbec83cbSDavid Spickett        except KeyError:
152fbec83cbSDavid Spickett            raise TableGenKernelException(
153fbec83cbSDavid Spickett                'Invalid value for setting "{}", '
154fbec83cbSDavid Spickett                'expected "on" or "off".'.format(name)
155fbec83cbSDavid Spickett            )
156fbec83cbSDavid Spickett
157210a383eSDavid Spickett    def get_magic(self, code):
15892787e3eSDavid Spickett        """Given a block of code remove the magic lines from it.
15992787e3eSDavid Spickett        Returns a tuple of newline joined code lines and a dictionary of magic.
16092787e3eSDavid Spickett        Where the key is the magic name (minus the %) and the values are lists
16192787e3eSDavid Spickett        of the arguments to the magic.
162210a383eSDavid Spickett
163210a383eSDavid Spickett        Currently we only look for "cell magic" which must be at the start of
164210a383eSDavid Spickett        the cell. Meaning the first line, or a set of lines beginning with %
165210a383eSDavid Spickett        that come before the first non-magic line.
166210a383eSDavid Spickett
167210a383eSDavid Spickett        >>> k.get_magic("")
16892787e3eSDavid Spickett        ('', {})
169210a383eSDavid Spickett        >>> k.get_magic("Hello World.\\nHello again.")
17092787e3eSDavid Spickett        ('Hello World.\\nHello again.', {})
171210a383eSDavid Spickett        >>> k.get_magic("   %foo a b c")
17292787e3eSDavid Spickett        ('', {'foo': ['a', 'b', 'c']})
17392787e3eSDavid Spickett        >>> k.get_magic("%foo\\n   %foo a b c\\nFoo")
17492787e3eSDavid Spickett        ('Foo', {'foo': ['a', 'b', 'c']})
17592787e3eSDavid Spickett        >>> k.get_magic("%foo\\n%bar\\nFoo")
17692787e3eSDavid Spickett        ('Foo', {'foo': [], 'bar': []})
177210a383eSDavid Spickett        >>> k.get_magic("Foo\\n%foo\\nFoo")
17892787e3eSDavid Spickett        ('Foo\\n%foo\\nFoo', {})
17992787e3eSDavid Spickett        >>> k.get_magic("%bar\\n\\n%foo")
18092787e3eSDavid Spickett        ('\\n%foo', {'bar': []})
18192787e3eSDavid Spickett        >>> k.get_magic("%foo a b\\n   Foo\\n%foo c d")
18292787e3eSDavid Spickett        ('   Foo\\n%foo c d', {'foo': ['a', 'b']})
18392787e3eSDavid Spickett        >>> k.get_magic("%foo a b\\n \\n%foo c d")
18492787e3eSDavid Spickett        (' \\n%foo c d', {'foo': ['a', 'b']})
185210a383eSDavid Spickett        """
18692787e3eSDavid Spickett        magic = {}
187210a383eSDavid Spickett        code_lines = []
188210a383eSDavid Spickett
189210a383eSDavid Spickett        lines = code.splitlines()
190210a383eSDavid Spickett        while lines:
191210a383eSDavid Spickett            line = lines.pop(0)
192210a383eSDavid Spickett            possible_magic = line.lstrip()
193210a383eSDavid Spickett            if possible_magic.startswith("%"):
19492787e3eSDavid Spickett                magic_parts = possible_magic.split()
19592787e3eSDavid Spickett                # Key has the % removed
19692787e3eSDavid Spickett                magic[magic_parts[0][1:]] = magic_parts[1:]
197210a383eSDavid Spickett            else:
198210a383eSDavid Spickett                code_lines = [line, *lines]
199210a383eSDavid Spickett                break
200210a383eSDavid Spickett
20192787e3eSDavid Spickett        return "\n".join(code_lines), magic
20292787e3eSDavid Spickett
203fbec83cbSDavid Spickett    def should_reset(self, magic):
204fbec83cbSDavid Spickett        """Return true if we should reset the cache, based on the default
205fbec83cbSDavid Spickett        setting and the current cell's magic %reset and/or %noreset.
206fbec83cbSDavid Spickett
207fbec83cbSDavid Spickett        >>> k._cell_reset = False
208fbec83cbSDavid Spickett        >>> k.should_reset({})
209fbec83cbSDavid Spickett        False
210fbec83cbSDavid Spickett        >>> k.should_reset({'reset': [], 'noreset': []})
211fbec83cbSDavid Spickett        Traceback (most recent call last):
212fbec83cbSDavid Spickett        ...
213fbec83cbSDavid Spickett        TableGenKernelException: %reset and %noreset in the same cell is not allowed. Use only one, or neither.
214fbec83cbSDavid Spickett        >>> k.should_reset({'reset': []})
215fbec83cbSDavid Spickett        True
216fbec83cbSDavid Spickett        >>> k.should_reset({'noreset': []})
217fbec83cbSDavid Spickett        False
218fbec83cbSDavid Spickett        >>> k._cell_reset = True
219fbec83cbSDavid Spickett        >>> k.should_reset({})
220fbec83cbSDavid Spickett        True
221fbec83cbSDavid Spickett        >>> k.should_reset({'reset': [], 'noreset': []})
222fbec83cbSDavid Spickett        Traceback (most recent call last):
223fbec83cbSDavid Spickett        ...
224fbec83cbSDavid Spickett        TableGenKernelException: %reset and %noreset in the same cell is not allowed. Use only one, or neither.
225fbec83cbSDavid Spickett        >>> k.should_reset({'reset': []})
226fbec83cbSDavid Spickett        True
227fbec83cbSDavid Spickett        >>> k.should_reset({'noreset': []})
228fbec83cbSDavid Spickett        False
229fbec83cbSDavid Spickett        """
230fbec83cbSDavid Spickett        # Cell reset is the default unless told otherwise.
231fbec83cbSDavid Spickett        should_reset = self._cell_reset
232fbec83cbSDavid Spickett        # Magic reset commands always win if present.
233fbec83cbSDavid Spickett        reset = magic.get("reset") is not None
234fbec83cbSDavid Spickett        noreset = magic.get("noreset") is not None
235fbec83cbSDavid Spickett
236fbec83cbSDavid Spickett        if reset and not noreset:
237fbec83cbSDavid Spickett            should_reset = True
238fbec83cbSDavid Spickett        elif noreset and not reset:
239fbec83cbSDavid Spickett            should_reset = False
240fbec83cbSDavid Spickett        elif noreset and reset:
241fbec83cbSDavid Spickett            raise TableGenKernelException(
242fbec83cbSDavid Spickett                "%reset and %noreset in the same cell is not allowed. Use only one, or neither."
243fbec83cbSDavid Spickett            )
244fbec83cbSDavid Spickett        # else neither are set so use the default.
245fbec83cbSDavid Spickett
246fbec83cbSDavid Spickett        return should_reset
247fbec83cbSDavid Spickett
24892787e3eSDavid Spickett    def get_code_and_args(self, new_code):
24992787e3eSDavid Spickett        """Get the code that do_execute should use, taking into account
25092787e3eSDavid Spickett        the code from any cached cells.
25192787e3eSDavid Spickett
25292787e3eSDavid Spickett        Returns the code to compile and the arguments to use to do so.
25392787e3eSDavid Spickett
25492787e3eSDavid Spickett        >>> k._previous_code = ""
25592787e3eSDavid Spickett        >>> k._previous_magic = {}
25692787e3eSDavid Spickett        >>> k.get_code_and_args("")
25792787e3eSDavid Spickett        ('', [])
25892787e3eSDavid Spickett        >>> k.get_code_and_args("%args 1\\nSome code")
25992787e3eSDavid Spickett        ('Some code', ['1'])
26092787e3eSDavid Spickett        >>> k.get_code_and_args("%args 2\\nSome more code")
26192787e3eSDavid Spickett        ('Some code\\nSome more code', ['2'])
26292787e3eSDavid Spickett        >>> k.get_code_and_args("%reset\\n%args 3 4\\nSome new code")
26392787e3eSDavid Spickett        ('Some new code', ['3', '4'])
26492787e3eSDavid Spickett        >>> k.get_code_and_args("%reset\\nSome new code")
26592787e3eSDavid Spickett        ('Some new code', [])
26692787e3eSDavid Spickett        """
26792787e3eSDavid Spickett        new_code, new_magic = self.get_magic(new_code)
26892787e3eSDavid Spickett
269fbec83cbSDavid Spickett        # Update kernel configuration first, if needed.
270fbec83cbSDavid Spickett        config_magic = new_magic.get("config")
271fbec83cbSDavid Spickett        if config_magic is not None:
272fbec83cbSDavid Spickett            self.parse_config_magic(config_magic)
273fbec83cbSDavid Spickett
274fbec83cbSDavid Spickett        if self.should_reset(new_magic):
27592787e3eSDavid Spickett            self._previous_code = new_code
27692787e3eSDavid Spickett            self._previous_magic = new_magic
27792787e3eSDavid Spickett        else:
27892787e3eSDavid Spickett            self._previous_code += ("\n" if self._previous_code else "") + new_code
27992787e3eSDavid Spickett            self._previous_magic.update(new_magic)
28092787e3eSDavid Spickett
28192787e3eSDavid Spickett        return self._previous_code, self._previous_magic.get("args", [])
282210a383eSDavid Spickett
283fdd18e86SDavid Spickett    def make_status(self):
284fdd18e86SDavid Spickett        return {
285fdd18e86SDavid Spickett            "status": "ok",
286fdd18e86SDavid Spickett            "execution_count": self.execution_count,
287fdd18e86SDavid Spickett            "payload": [],
288fdd18e86SDavid Spickett            "user_expressions": {},
289fdd18e86SDavid Spickett        }
290fdd18e86SDavid Spickett
291fdd18e86SDavid Spickett    def send_stream(self, name, content):
292fdd18e86SDavid Spickett        self.send_response(self.iopub_socket, "stream", {"name": name, "text": content})
293fdd18e86SDavid Spickett
294fdd18e86SDavid Spickett        return self.make_status()
295fdd18e86SDavid Spickett
296fdd18e86SDavid Spickett    def send_stderr(self, stderr):
297fdd18e86SDavid Spickett        return self.send_stream("stderr", stderr)
298fdd18e86SDavid Spickett
299fdd18e86SDavid Spickett    def send_stdout(self, stdout):
300fdd18e86SDavid Spickett        return self.send_stream("stdout", stdout)
301fdd18e86SDavid Spickett
302210a383eSDavid Spickett    def do_execute(
303210a383eSDavid Spickett        self, code, silent, store_history=True, user_expressions=None, allow_stdin=False
304210a383eSDavid Spickett    ):
305210a383eSDavid Spickett        """Execute user code using llvm-tblgen binary."""
306fbec83cbSDavid Spickett        try:
30792787e3eSDavid Spickett            all_code, args = self.get_code_and_args(code)
308fbec83cbSDavid Spickett        except TableGenKernelException as e:
309fbec83cbSDavid Spickett            return self.send_stderr(str(e))
310210a383eSDavid Spickett
311*f8559751SJay Foad        # If we cannot find llvm-tblgen, propagate the error to the notebook.
312fdd18e86SDavid Spickett        # (in case the user is not able to see the output from the Jupyter server)
313fdd18e86SDavid Spickett        try:
314fdd18e86SDavid Spickett            executable = self.get_executable()
315fdd18e86SDavid Spickett        except Exception as e:
316fdd18e86SDavid Spickett            return self.send_stderr(str(e))
317fdd18e86SDavid Spickett
318210a383eSDavid Spickett        with tempfile.TemporaryFile("w+") as f:
31992787e3eSDavid Spickett            f.write(all_code)
320210a383eSDavid Spickett            f.seek(0)
321210a383eSDavid Spickett            got = subprocess.run(
322fdd18e86SDavid Spickett                [executable, *args],
323210a383eSDavid Spickett                stdin=f,
324210a383eSDavid Spickett                stderr=subprocess.PIPE,
325210a383eSDavid Spickett                stdout=subprocess.PIPE,
326210a383eSDavid Spickett                universal_newlines=True,
327210a383eSDavid Spickett            )
328210a383eSDavid Spickett
329210a383eSDavid Spickett        if not silent:
330210a383eSDavid Spickett            if got.stderr:
331fdd18e86SDavid Spickett                return self.send_stderr(got.stderr)
332210a383eSDavid Spickett            else:
333fdd18e86SDavid Spickett                return self.send_stdout(got.stdout)
334fdd18e86SDavid Spickett        else:
335fdd18e86SDavid Spickett            return self.make_status()
336210a383eSDavid Spickett
337210a383eSDavid Spickett
338210a383eSDavid Spickettif __name__ == "__main__":
339210a383eSDavid Spickett    import doctest
340210a383eSDavid Spickett
341210a383eSDavid Spickett    doctest.testmod(extraglobs={"k": TableGenKernel()})
342