342 lines · python
1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.2# See https://llvm.org/LICENSE.txt for license information.3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception4 5import os6import shutil7import subprocess8import tempfile9from ipykernel.kernelbase import Kernel10 11__version__ = "0.0.1"12 13 14class TableGenKernelException(Exception):15 pass16 17 18class TableGenKernel(Kernel):19 """Kernel using llvm-tblgen inside jupyter.20 21 All input is treated as TableGen unless the first non whitespace character22 is "%" in which case it is a "magic" line.23 24 The supported cell magic is:25 * %args - to set the arguments passed to llvm-tblgen.26 * %reset - to reset the cached code and magic state.27 * %noreset - to not reset the cached code and magic state28 (useful when you have changed the default to always29 reset the cache).30 31 These are "cell magic" meaning it applies to the whole cell. Therefore32 it must be the first line, or part of a run of magic lines starting33 from the first line.34 35 The following are global magic (that applies to all cells going36 forward):37 * %config - to change the behaviour of the kernel overall, including38 changing defaults for things like resets.39 40 Global magic must be written in the same way as cell magic.41 42 ```tablgen43 %args44 %reset45 %args --print-records --print-detailed-records46 class Stuff {47 string Name;48 }49 50 def a_thing : Stuff {}51 ```52 53 """54 55 implementation = "tablegen"56 implementation_version = __version__57 58 language_version = __version__59 language = "tablegen"60 language_info = {61 "name": "tablegen",62 "mimetype": "text/x-tablegen",63 "file_extension": ".td",64 "pygments_lexer": "text",65 }66 67 def __init__(self, **kwargs):68 Kernel.__init__(self, **kwargs)69 self._executable = None70 # A list of (code, magic) tuples.71 # All the previous cell's code we have run since the last reset.72 # This emulates a persistent state like a Python interpreter would have.73 self._previous_code = ""74 # The most recent set of magic since the last reset.75 self._previous_magic = {}76 # The default cache reset behaviour. True means do not cache anything77 # between cells.78 self._cell_reset = False79 80 @property81 def banner(self):82 return "llvm-tblgen kernel %s" % __version__83 84 def get_executable(self):85 """If this is the first run, search for llvm-tblgen.86 Otherwise return the cached path to it."""87 if self._executable is None:88 path = os.environ.get("LLVM_TBLGEN_EXECUTABLE")89 if path is not None and os.path.isfile(path) and os.access(path, os.X_OK):90 self._executable = path91 else:92 path = shutil.which("llvm-tblgen")93 if path is None:94 raise OSError(95 "llvm-tblgen not found. Put it on your PATH or set the"96 " environment variable LLVM_TBLGEN_EXECUTABLE to point to it."97 )98 self._executable = path99 100 return self._executable101 102 def parse_config_magic(self, config):103 """Config should be a list of parameters given to the %config command.104 We allow only one setting per %config line and that setting can only105 have one value.106 107 Assuming the parameters are valid, update the kernel's setting with108 the new value.109 110 If there is an error, raise a TableGenKernelException.111 112 >>> k.parse_config_magic([])113 Traceback (most recent call last):114 ...115 TableGenKernelException: Incorrect number of parameters to %config. Expected %config <setting> <value>.116 >>> k._cell_reset117 False118 >>> k.parse_config_magic(["a", "b", "c"])119 Traceback (most recent call last):120 ...121 TableGenKernelException: Incorrect number of parameters to %config. Expected %config <setting> <value>.122 >>> k.parse_config_magic(["notasetting", "..."])123 Traceback (most recent call last):124 ...125 TableGenKernelException: Unknown kernel setting "notasetting". Possible settings are: "cellreset".126 >>> k.parse_config_magic(["cellreset", "food"])127 Traceback (most recent call last):128 ...129 TableGenKernelException: Invalid value for setting "cellreset", expected "on" or "off".130 >>> k.parse_config_magic(["cellreset", "on"])131 >>> k._cell_reset132 True133 >>> k.parse_config_magic(["cellreset", "off"])134 >>> k._cell_reset135 False136 """137 if len(config) != 2:138 raise TableGenKernelException(139 "Incorrect number of parameters to %config. Expected %config <setting> <value>."140 )141 142 name, value = config143 if name != "cellreset":144 raise TableGenKernelException(145 'Unknown kernel setting "{}". '146 'Possible settings are: "cellreset".'.format(name)147 )148 149 try:150 self._cell_reset = {"on": True, "off": False}[value.lower()]151 except KeyError:152 raise TableGenKernelException(153 'Invalid value for setting "{}", '154 'expected "on" or "off".'.format(name)155 )156 157 def get_magic(self, code):158 """Given a block of code remove the magic lines from it.159 Returns a tuple of newline joined code lines and a dictionary of magic.160 Where the key is the magic name (minus the %) and the values are lists161 of the arguments to the magic.162 163 Currently we only look for "cell magic" which must be at the start of164 the cell. Meaning the first line, or a set of lines beginning with %165 that come before the first non-magic line.166 167 >>> k.get_magic("")168 ('', {})169 >>> k.get_magic("Hello World.\\nHello again.")170 ('Hello World.\\nHello again.', {})171 >>> k.get_magic(" %foo a b c")172 ('', {'foo': ['a', 'b', 'c']})173 >>> k.get_magic("%foo\\n %foo a b c\\nFoo")174 ('Foo', {'foo': ['a', 'b', 'c']})175 >>> k.get_magic("%foo\\n%bar\\nFoo")176 ('Foo', {'foo': [], 'bar': []})177 >>> k.get_magic("Foo\\n%foo\\nFoo")178 ('Foo\\n%foo\\nFoo', {})179 >>> k.get_magic("%bar\\n\\n%foo")180 ('\\n%foo', {'bar': []})181 >>> k.get_magic("%foo a b\\n Foo\\n%foo c d")182 (' Foo\\n%foo c d', {'foo': ['a', 'b']})183 >>> k.get_magic("%foo a b\\n \\n%foo c d")184 (' \\n%foo c d', {'foo': ['a', 'b']})185 """186 magic = {}187 code_lines = []188 189 lines = code.splitlines()190 while lines:191 line = lines.pop(0)192 possible_magic = line.lstrip()193 if possible_magic.startswith("%"):194 magic_parts = possible_magic.split()195 # Key has the % removed196 magic[magic_parts[0][1:]] = magic_parts[1:]197 else:198 code_lines = [line, *lines]199 break200 201 return "\n".join(code_lines), magic202 203 def should_reset(self, magic):204 """Return true if we should reset the cache, based on the default205 setting and the current cell's magic %reset and/or %noreset.206 207 >>> k._cell_reset = False208 >>> k.should_reset({})209 False210 >>> k.should_reset({'reset': [], 'noreset': []})211 Traceback (most recent call last):212 ...213 TableGenKernelException: %reset and %noreset in the same cell is not allowed. Use only one, or neither.214 >>> k.should_reset({'reset': []})215 True216 >>> k.should_reset({'noreset': []})217 False218 >>> k._cell_reset = True219 >>> k.should_reset({})220 True221 >>> k.should_reset({'reset': [], 'noreset': []})222 Traceback (most recent call last):223 ...224 TableGenKernelException: %reset and %noreset in the same cell is not allowed. Use only one, or neither.225 >>> k.should_reset({'reset': []})226 True227 >>> k.should_reset({'noreset': []})228 False229 """230 # Cell reset is the default unless told otherwise.231 should_reset = self._cell_reset232 # Magic reset commands always win if present.233 reset = magic.get("reset") is not None234 noreset = magic.get("noreset") is not None235 236 if reset and not noreset:237 should_reset = True238 elif noreset and not reset:239 should_reset = False240 elif noreset and reset:241 raise TableGenKernelException(242 "%reset and %noreset in the same cell is not allowed. Use only one, or neither."243 )244 # else neither are set so use the default.245 246 return should_reset247 248 def get_code_and_args(self, new_code):249 """Get the code that do_execute should use, taking into account250 the code from any cached cells.251 252 Returns the code to compile and the arguments to use to do so.253 254 >>> k._previous_code = ""255 >>> k._previous_magic = {}256 >>> k.get_code_and_args("")257 ('', [])258 >>> k.get_code_and_args("%args 1\\nSome code")259 ('Some code', ['1'])260 >>> k.get_code_and_args("%args 2\\nSome more code")261 ('Some code\\nSome more code', ['2'])262 >>> k.get_code_and_args("%reset\\n%args 3 4\\nSome new code")263 ('Some new code', ['3', '4'])264 >>> k.get_code_and_args("%reset\\nSome new code")265 ('Some new code', [])266 """267 new_code, new_magic = self.get_magic(new_code)268 269 # Update kernel configuration first, if needed.270 config_magic = new_magic.get("config")271 if config_magic is not None:272 self.parse_config_magic(config_magic)273 274 if self.should_reset(new_magic):275 self._previous_code = new_code276 self._previous_magic = new_magic277 else:278 self._previous_code += ("\n" if self._previous_code else "") + new_code279 self._previous_magic.update(new_magic)280 281 return self._previous_code, self._previous_magic.get("args", [])282 283 def make_status(self):284 return {285 "status": "ok",286 "execution_count": self.execution_count,287 "payload": [],288 "user_expressions": {},289 }290 291 def send_stream(self, name, content):292 self.send_response(self.iopub_socket, "stream", {"name": name, "text": content})293 294 return self.make_status()295 296 def send_stderr(self, stderr):297 return self.send_stream("stderr", stderr)298 299 def send_stdout(self, stdout):300 return self.send_stream("stdout", stdout)301 302 def do_execute(303 self, code, silent, store_history=True, user_expressions=None, allow_stdin=False304 ):305 """Execute user code using llvm-tblgen binary."""306 try:307 all_code, args = self.get_code_and_args(code)308 except TableGenKernelException as e:309 return self.send_stderr(str(e))310 311 # If we cannot find llvm-tblgen, propagate the error to the notebook.312 # (in case the user is not able to see the output from the Jupyter server)313 try:314 executable = self.get_executable()315 except Exception as e:316 return self.send_stderr(str(e))317 318 with tempfile.TemporaryFile("w+") as f:319 f.write(all_code)320 f.seek(0)321 got = subprocess.run(322 [executable, *args],323 stdin=f,324 stderr=subprocess.PIPE,325 stdout=subprocess.PIPE,326 universal_newlines=True,327 )328 329 if not silent:330 if got.stderr:331 return self.send_stderr(got.stderr)332 else:333 return self.send_stdout(got.stdout)334 else:335 return self.make_status()336 337 338if __name__ == "__main__":339 import doctest340 341 doctest.testmod(extraglobs={"k": TableGenKernel()})342