341 lines
12 KiB
Python
341 lines
12 KiB
Python
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
# See https://llvm.org/LICENSE.txt for license information.
|
|
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
from ipykernel.kernelbase import Kernel
|
|
|
|
__version__ = "0.0.1"
|
|
|
|
|
|
class TableGenKernelException(Exception):
|
|
pass
|
|
|
|
|
|
class TableGenKernel(Kernel):
|
|
"""Kernel using llvm-tblgen inside jupyter.
|
|
|
|
All input is treated as TableGen unless the first non whitespace character
|
|
is "%" in which case it is a "magic" line.
|
|
|
|
The supported cell magic is:
|
|
* %args - to set the arguments passed to llvm-tblgen.
|
|
* %reset - to reset the cached code and magic state.
|
|
* %noreset - to not reset the cached code and magic state
|
|
(useful when you have changed the default to always
|
|
reset the cache).
|
|
|
|
These are "cell magic" meaning it applies to the whole cell. Therefore
|
|
it must be the first line, or part of a run of magic lines starting
|
|
from the first line.
|
|
|
|
The following are global magic (that applies to all cells going
|
|
forward):
|
|
* %config - to change the behaviour of the kernel overall, including
|
|
changing defaults for things like resets.
|
|
|
|
Global magic must be written in the same way as cell magic.
|
|
|
|
```tablgen
|
|
%args
|
|
%reset
|
|
%args --print-records --print-detailed-records
|
|
class Stuff {
|
|
string Name;
|
|
}
|
|
|
|
def a_thing : Stuff {}
|
|
```
|
|
|
|
"""
|
|
|
|
implementation = "tablegen"
|
|
implementation_version = __version__
|
|
|
|
language_version = __version__
|
|
language = "tablegen"
|
|
language_info = {
|
|
"name": "tablegen",
|
|
"mimetype": "text/x-tablegen",
|
|
"file_extension": ".td",
|
|
"pygments_lexer": "text",
|
|
}
|
|
|
|
def __init__(self, **kwargs):
|
|
Kernel.__init__(self, **kwargs)
|
|
self._executable = None
|
|
# A list of (code, magic) tuples.
|
|
# All the previous cell's code we have run since the last reset.
|
|
# This emulates a persistent state like a Python interpreter would have.
|
|
self._previous_code = ""
|
|
# The most recent set of magic since the last reset.
|
|
self._previous_magic = {}
|
|
# The default cache reset behaviour. True means do not cache anything
|
|
# between cells.
|
|
self._cell_reset = False
|
|
|
|
@property
|
|
def banner(self):
|
|
return "llvm-tblgen kernel %s" % __version__
|
|
|
|
def get_executable(self):
|
|
"""If this is the first run, search for llvm-tblgen.
|
|
Otherwise return the cached path to it."""
|
|
if self._executable is None:
|
|
path = os.environ.get("LLVM_TBLGEN_EXECUTABLE")
|
|
if path is not None and os.path.isfile(path) and os.access(path, os.X_OK):
|
|
self._executable = path
|
|
else:
|
|
path = shutil.which("llvm-tblgen")
|
|
if path is None:
|
|
raise OSError(
|
|
"llvm-tblgen not found. Put it on your PATH or set the"
|
|
" environment variable LLVM_TBLGEN_EXECUTABLE to point to it."
|
|
)
|
|
self._executable = path
|
|
|
|
return self._executable
|
|
|
|
def parse_config_magic(self, config):
|
|
"""Config should be a list of parameters given to the %config command.
|
|
We allow only one setting per %config line and that setting can only
|
|
have one value.
|
|
|
|
Assuming the parameters are valid, update the kernel's setting with
|
|
the new value.
|
|
|
|
If there is an error, raise a TableGenKernelException.
|
|
|
|
>>> k.parse_config_magic([])
|
|
Traceback (most recent call last):
|
|
...
|
|
TableGenKernelException: Incorrect number of parameters to %config. Expected %config <setting> <value>.
|
|
>>> k._cell_reset
|
|
False
|
|
>>> k.parse_config_magic(["a", "b", "c"])
|
|
Traceback (most recent call last):
|
|
...
|
|
TableGenKernelException: Incorrect number of parameters to %config. Expected %config <setting> <value>.
|
|
>>> k.parse_config_magic(["notasetting", "..."])
|
|
Traceback (most recent call last):
|
|
...
|
|
TableGenKernelException: Unknown kernel setting "notasetting". Possible settings are: "cellreset".
|
|
>>> k.parse_config_magic(["cellreset", "food"])
|
|
Traceback (most recent call last):
|
|
...
|
|
TableGenKernelException: Invalid value for setting "cellreset", expected "on" or "off".
|
|
>>> k.parse_config_magic(["cellreset", "on"])
|
|
>>> k._cell_reset
|
|
True
|
|
>>> k.parse_config_magic(["cellreset", "off"])
|
|
>>> k._cell_reset
|
|
False
|
|
"""
|
|
if len(config) != 2:
|
|
raise TableGenKernelException(
|
|
"Incorrect number of parameters to %config. Expected %config <setting> <value>."
|
|
)
|
|
|
|
name, value = config
|
|
if name != "cellreset":
|
|
raise TableGenKernelException(
|
|
'Unknown kernel setting "{}". '
|
|
'Possible settings are: "cellreset".'.format(name)
|
|
)
|
|
|
|
try:
|
|
self._cell_reset = {"on": True, "off": False}[value.lower()]
|
|
except KeyError:
|
|
raise TableGenKernelException(
|
|
'Invalid value for setting "{}", '
|
|
'expected "on" or "off".'.format(name)
|
|
)
|
|
|
|
def get_magic(self, code):
|
|
"""Given a block of code remove the magic lines from it.
|
|
Returns a tuple of newline joined code lines and a dictionary of magic.
|
|
Where the key is the magic name (minus the %) and the values are lists
|
|
of the arguments to the magic.
|
|
|
|
Currently we only look for "cell magic" which must be at the start of
|
|
the cell. Meaning the first line, or a set of lines beginning with %
|
|
that come before the first non-magic line.
|
|
|
|
>>> k.get_magic("")
|
|
('', {})
|
|
>>> k.get_magic("Hello World.\\nHello again.")
|
|
('Hello World.\\nHello again.', {})
|
|
>>> k.get_magic(" %foo a b c")
|
|
('', {'foo': ['a', 'b', 'c']})
|
|
>>> k.get_magic("%foo\\n %foo a b c\\nFoo")
|
|
('Foo', {'foo': ['a', 'b', 'c']})
|
|
>>> k.get_magic("%foo\\n%bar\\nFoo")
|
|
('Foo', {'foo': [], 'bar': []})
|
|
>>> k.get_magic("Foo\\n%foo\\nFoo")
|
|
('Foo\\n%foo\\nFoo', {})
|
|
>>> k.get_magic("%bar\\n\\n%foo")
|
|
('\\n%foo', {'bar': []})
|
|
>>> k.get_magic("%foo a b\\n Foo\\n%foo c d")
|
|
(' Foo\\n%foo c d', {'foo': ['a', 'b']})
|
|
>>> k.get_magic("%foo a b\\n \\n%foo c d")
|
|
(' \\n%foo c d', {'foo': ['a', 'b']})
|
|
"""
|
|
magic = {}
|
|
code_lines = []
|
|
|
|
lines = code.splitlines()
|
|
while lines:
|
|
line = lines.pop(0)
|
|
possible_magic = line.lstrip()
|
|
if possible_magic.startswith("%"):
|
|
magic_parts = possible_magic.split()
|
|
# Key has the % removed
|
|
magic[magic_parts[0][1:]] = magic_parts[1:]
|
|
else:
|
|
code_lines = [line, *lines]
|
|
break
|
|
|
|
return "\n".join(code_lines), magic
|
|
|
|
def should_reset(self, magic):
|
|
"""Return true if we should reset the cache, based on the default
|
|
setting and the current cell's magic %reset and/or %noreset.
|
|
|
|
>>> k._cell_reset = False
|
|
>>> k.should_reset({})
|
|
False
|
|
>>> k.should_reset({'reset': [], 'noreset': []})
|
|
Traceback (most recent call last):
|
|
...
|
|
TableGenKernelException: %reset and %noreset in the same cell is not allowed. Use only one, or neither.
|
|
>>> k.should_reset({'reset': []})
|
|
True
|
|
>>> k.should_reset({'noreset': []})
|
|
False
|
|
>>> k._cell_reset = True
|
|
>>> k.should_reset({})
|
|
True
|
|
>>> k.should_reset({'reset': [], 'noreset': []})
|
|
Traceback (most recent call last):
|
|
...
|
|
TableGenKernelException: %reset and %noreset in the same cell is not allowed. Use only one, or neither.
|
|
>>> k.should_reset({'reset': []})
|
|
True
|
|
>>> k.should_reset({'noreset': []})
|
|
False
|
|
"""
|
|
# Cell reset is the default unless told otherwise.
|
|
should_reset = self._cell_reset
|
|
# Magic reset commands always win if present.
|
|
reset = magic.get("reset") is not None
|
|
noreset = magic.get("noreset") is not None
|
|
|
|
if reset and not noreset:
|
|
should_reset = True
|
|
elif noreset and not reset:
|
|
should_reset = False
|
|
elif noreset and reset:
|
|
raise TableGenKernelException(
|
|
"%reset and %noreset in the same cell is not allowed. Use only one, or neither."
|
|
)
|
|
# else neither are set so use the default.
|
|
|
|
return should_reset
|
|
|
|
def get_code_and_args(self, new_code):
|
|
"""Get the code that do_execute should use, taking into account
|
|
the code from any cached cells.
|
|
|
|
Returns the code to compile and the arguments to use to do so.
|
|
|
|
>>> k._previous_code = ""
|
|
>>> k._previous_magic = {}
|
|
>>> k.get_code_and_args("")
|
|
('', [])
|
|
>>> k.get_code_and_args("%args 1\\nSome code")
|
|
('Some code', ['1'])
|
|
>>> k.get_code_and_args("%args 2\\nSome more code")
|
|
('Some code\\nSome more code', ['2'])
|
|
>>> k.get_code_and_args("%reset\\n%args 3 4\\nSome new code")
|
|
('Some new code', ['3', '4'])
|
|
>>> k.get_code_and_args("%reset\\nSome new code")
|
|
('Some new code', [])
|
|
"""
|
|
new_code, new_magic = self.get_magic(new_code)
|
|
|
|
# Update kernel configuration first, if needed.
|
|
config_magic = new_magic.get("config")
|
|
if config_magic is not None:
|
|
self.parse_config_magic(config_magic)
|
|
|
|
if self.should_reset(new_magic):
|
|
self._previous_code = new_code
|
|
self._previous_magic = new_magic
|
|
else:
|
|
self._previous_code += ("\n" if self._previous_code else "") + new_code
|
|
self._previous_magic.update(new_magic)
|
|
|
|
return self._previous_code, self._previous_magic.get("args", [])
|
|
|
|
def make_status(self):
|
|
return {
|
|
"status": "ok",
|
|
"execution_count": self.execution_count,
|
|
"payload": [],
|
|
"user_expressions": {},
|
|
}
|
|
|
|
def send_stream(self, name, content):
|
|
self.send_response(self.iopub_socket, "stream", {"name": name, "text": content})
|
|
|
|
return self.make_status()
|
|
|
|
def send_stderr(self, stderr):
|
|
return self.send_stream("stderr", stderr)
|
|
|
|
def send_stdout(self, stdout):
|
|
return self.send_stream("stdout", stdout)
|
|
|
|
def do_execute(
|
|
self, code, silent, store_history=True, user_expressions=None, allow_stdin=False
|
|
):
|
|
"""Execute user code using llvm-tblgen binary."""
|
|
try:
|
|
all_code, args = self.get_code_and_args(code)
|
|
except TableGenKernelException as e:
|
|
return self.send_stderr(str(e))
|
|
|
|
# If we cannot find llvm-tblgen, propogate the error to the notebook.
|
|
# (in case the user is not able to see the output from the Jupyter server)
|
|
try:
|
|
executable = self.get_executable()
|
|
except Exception as e:
|
|
return self.send_stderr(str(e))
|
|
|
|
with tempfile.TemporaryFile("w+") as f:
|
|
f.write(all_code)
|
|
f.seek(0)
|
|
got = subprocess.run(
|
|
[executable, *args],
|
|
stdin=f,
|
|
stderr=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
universal_newlines=True,
|
|
)
|
|
|
|
if not silent:
|
|
if got.stderr:
|
|
return self.send_stderr(got.stderr)
|
|
else:
|
|
return self.send_stdout(got.stdout)
|
|
else:
|
|
return self.make_status()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import doctest
|
|
|
|
doctest.testmod(extraglobs={"k": TableGenKernel()})
|