def ensure_poly_running(self): """Starts the Poly/ML process if it is not already running.""" if self.process == None or not self.process.is_alive(): # reset state, in case poly just died self.compile_in_progress = False self._parse_trees = {} self.process = PolyProcess(self.poly_bin)
class Poly: """The core class for interacting with a Poly/ML instance. poly_bin -- the path to the Poly/ML binary; only used when creating self.process process -- the Poly/ML process (lazily created) compile_in_progress -- whether there is currently an async compilation happening """ def __init__(self, poly_bin="poly"): self.poly_bin = poly_bin self.process = None self.compile_in_progress = False self._parse_trees = {} # for _clean_text() import re self._clean_rexp = re.compile(r"\s+") def has_built(self, path): """Return whether a file has been compiled.""" return path in self._parse_trees.keys() def ensure_poly_running(self): """Starts the Poly/ML process if it is not already running.""" if self.process == None or not self.process.is_alive(): # reset state, in case poly just died self.compile_in_progress = False self._parse_trees = {} self.process = PolyProcess(self.poly_bin) def node_for_position(self, path, position): """Get the PolyNode at a given position. This will return None if the file has not been compiled. The information is only accurate for the last successful compile of that file. path -- the path of the file, as passed to compile or compile_sync position -- a zero-indexed offset in the ml code last passed to compile or compile_sync for path returns a PolyNode object, or None raises poly.process.Timeout if the request to Poly/ML times out raises poly.process.ProtocolError if communication with Poly/ML failed """ if path in self._parse_trees.keys(): node = PolyNode() node.file_name = path p = self.process.sync_request("O", [self._parse_trees[path], position, position]) p.popcode("O") p.pop() # ignire RID p.popcode(",") node.parse_tree = p.pop() p.popcode(",") node.start = p.popint() p.popcode(",") node.end = p.popint() while p.popcode().code == ",": node.commands.append(p.popstr()) return node else: return None def type_for_node(self, node): """Get the type of a PolyNode This will return None if the node does not have a type. The information is only accurate if the file has not been recompiled since the PolyNode object was created. node -- a PolyNode, as returned by node_for_position or declaration_for_node returns a string giving the type, or None raises poly.process.Timeout if the request to Poly/ML times out raises poly.process.ProtocolError if communication with Poly/ML failed """ if node and "T" in node.commands: p = self.process.sync_request("T", [node.parse_tree, node.start, node.end]) p.popcode("T") for i in range(7): p.pop() # ignore info about the ident. if p.popcode().code == ",": return p.popstr().strip() else: return None else: return None def declaration_for_node(self, node): """Get the PolyNode for the declaration of a PolyNode This will return None if the node does not have a declaration. The information is only accurate if the file has not been recompiled since the PolyLocation object was created. node -- a PolyNode, as returned by node_for_position Returns a PolyLocation for the declaration, or None. This may be a PolyNode. Due to a shortcoming of Poly/ML, the location filename will not include the path to the file (FIXME: need some way to specify the search path?) raises poly.process.Timeout if the request to Poly/ML times out raises poly.process.ProtocolError if communication with Poly/ML failed """ if node and "I" in node.commands: p = self.process.sync_request("I", [node.parse_tree, node.start, node.end, "I"]) p.popcode("I") for i in range(7): p.pop() # ignore info about the ident. if p.popcode().code == ",": file_name = p.popstr() p.popcode(",") line = p.popint() p.popcode(",") start = p.popint() p.popcode(",") end = p.popint() if file_name in self._parse_trees.keys(): if line: print("Got line number ({0}) for self-compiled file!".format(line)) return PolyLocation(file_name, line, start, end) else: return PolyNode(file_name, start, end, self._parse_trees[file_name]) else: return PolyLocation(file_name, line, start, end) return dnode else: return None else: return None def _clean_text(self, text): """Removes excess whitespace from a string. Returns the given text with all contiguous whitespace replaced by a single space, and any whitespace at the start or end removed. Poly/ML puts newlines into its messages; this allows us to strip them out. """ return self._clean_rexp.sub(" ", text.strip()) def _pop_d_message(self, p): """Reads a D message from a Packet Returns a pair of PolyLocation and identifier """ p.popcode("D") file_name = p.popstr() p.popcode(",") line = p.popint() if not line: line = None p.popcode(",") start = p.popint() p.popcode(",") end = p.popint() p.popcode(";") text = p.popstr() p.popcode("d") return PolyLocation(file_name, line, start, end), text def _pop_output_until_code(self, p, closing_code): """Reads some output, possibly including markup, up to (and including) the next occurrence of closing_code (which should be lower case) Returns the output, and the last found location """ text = "" location = None if not p.nextiscode(): text = p.popstr() code = p.popcode().code while code != closing_code: if code == "D": p.pushcode("D") location, t = self._pop_d_message(p) if len(text): text += " " text += t else: p.popuntilcode(";") text += " " + p.popstr().strip() p.popcode(code.lower()) if not p.nextiscode(): if len(text): text += " " n = p.popstr() text += n code = p.popcode().code return location, text def _pop_compile_result_header(self, p, file): """Reads an R response and returns the result code as a string Also saves the parse tree ID in self._parse_trees. p -- a poly.process.Packet containing a compilation result block file -- the key to use for saving the parse tree ID """ p.popcode("R") # pop off leading p code p.pop() # ignore RID p.popcode(",") self._parse_trees[file] = p.pop() # save parse tree ID p.popcode(",") result_code = p.popstr() p.popcode(",") p.popint() # ignore final offset p.popcode(";") return result_code def _pop_compile_exception_message(self, p): """Reads an exception message from an R response p -- a poly.process.Packet containing a compilation result block, that has already had _pop_compile_result_header called on it, and had the result 'X' Returns a PolyException message """ p.pop_until_nonempty() p.popcode("X") # pop off leading p code exp = PolyException("") exp.location, message = self._pop_output_until_code(p, "x") exp.text = self._clean_text(message) return exp def _pop_compile_error_messages(self, p): """Reads the error list from an R response p -- a poly.process.Packet containing a compilation result block, that has already had _pop_compile_result_header called on it (and _pop_compile_exception_message if the result was 'X') Returns a list of PolyErrorMessage objects. """ messages = [] p.pop_until_nonempty() while p.popcode().code == "E": message_code = p.popstr() p.popcode(",") file_name = p.popstr() p.popcode(",") line = p.popint() if not line: line = None p.popcode(",") start_pos = p.popint() p.popcode(",") end_pos = p.popint() p.popcode(";") location, text = self._pop_output_until_code(p, "e") p.popempty() # empty string between escape codes text = self._clean_text(text) messages.append(PolyErrorMessage(message_code, file_name, line, start_pos, end_pos, text)) return messages def _read_compile_response(self, p, file): """Parses the response packet for a compilation p -- a poly.process.Packet containing a compilation result block file -- the file name for the compilation Returns a pair of result code (a single-character string) and a list of PolyMessage objects. """ result_code = self._pop_compile_result_header(p, file) messages = [] if result_code == "L": location, text = self._pop_output_until_code(p, "r") # this clearly counts as an error messages = [PolyMessage("E", text)] else: if result_code == "X": messages = [self._pop_compile_exception_message(p)] messages += self._pop_compile_error_messages(p) return result_code, messages def compile_sync(self, file, prelude, source, timeout=10): """Sends ML code for compilation, and waits for the result file -- the file name for the compilation prelude -- ML code to set up the compilation state (eg: loading a saved state) source -- the ML code to compile timeout -- (optional) how long to wait, in seconds (default: 10) Returns a pair of result code (a single-character string) and a list of PolyMessage objects. If the result code is 'X', the first will be a PolyException and the remainder PolyErrorMessage objects, otherwise they will all be PolyErrorMessage objects. """ if self.compile_in_progress: return None, [] self.ensure_poly_running() p = self.process.sync_request("R", [file, 0, len(prelude), len(source), prelude, source], timeout) return self._read_compile_response(p, file) def compile(self, file, prelude, source, handler): """Sends ML code for compilation file -- the file name for the compilation prelude -- ML code to set up the compilation state (eg: loading a saved state) source -- the ML code to compile handler -- a method to call when the compilation has finished The handler will be passed two arguments: the result code (a single-character string) and a list of PolyMessage objects. If the result code is 'X', the first will be a PolyException and the remainder PolyErrorMessage objects, otherwise they will all be PolyErrorMessage objects. Returns the request ID (an integer). """ if self.compile_in_progress: return -1 self.ensure_poly_running() self.compile_in_progress = True def run_handler(p): self.compile_in_progress = False result_code, messages = self._read_compile_response(p, file) handler(result_code, messages) rid = self.process.send_request("R", [file, 0, len(prelude), len(source), prelude, source], run_handler) return rid def cancel_compile(self, rid): """Cancels a compilation in progress rid -- the request id, as returned by compile() """ self.process.send_request("K", [rid])