diff --git a/py/stencila/schema/__main__.py b/py/stencila/schema/__main__.py index 4fffa753c5..b30117b95c 100644 --- a/py/stencila/schema/__main__.py +++ b/py/stencila/schema/__main__.py @@ -13,6 +13,7 @@ from sys import argv, stderr, stdout from .interpreter import execute_from_cli +from .listener import start_stdio_interpreter def cli_execute(): @@ -25,16 +26,22 @@ def cli_compile(): execute_from_cli(argv[2:], True) +def interpreter_listen(): + start_stdio_interpreter() + + def main(): """The main entry point to this module, read the first CLI arg and call out to the corresponding function.""" command = argv[1] if len(argv) > 1 else '' + logging.basicConfig(stream=stdout, level=logging.DEBUG) + if command == 'execute': - logging.basicConfig(stream=stdout, level=logging.DEBUG) cli_execute() elif command == 'compile': - logging.basicConfig(stream=stdout, level=logging.DEBUG) cli_compile() + elif command == 'listen': + interpreter_listen() else: stderr.write('Unknown command "{}"\n'.format(command)) diff --git a/py/stencila/schema/code_parsing.py b/py/stencila/schema/code_parsing.py index 4bae6affe2..670ebd1c00 100644 --- a/py/stencila/schema/code_parsing.py +++ b/py/stencila/schema/code_parsing.py @@ -111,6 +111,21 @@ def set_code_error(code: typing.Union[CodeChunk, CodeExpression], code.errors.append(error) +def simple_code_chunk_parse(code: CodeChunk) -> CodeChunkExecution: + """ + "Build a CodeChunkExecution from CodeChunk. + + This is the most basic information that is needed to execute a CodeChunk in the interpreter. + """ + parser = CodeChunkParser() + cc_result = parser.parse(code) + + if cc_result.error: + set_code_error(code, cc_result.error) + + return CodeChunkExecution(code, cc_result) + + class CodeChunkParser: """Parse a `CodeChunk` by parsing its `text` into an AST and traversing it.""" diff --git a/py/stencila/schema/listener.py b/py/stencila/schema/listener.py new file mode 100644 index 0000000000..49dc5d1943 --- /dev/null +++ b/py/stencila/schema/listener.py @@ -0,0 +1,106 @@ +import json +import logging +import sys +import typing +from io import BytesIO + +from stencila.schema.code_parsing import simple_code_chunk_parse +from stencila.schema.interpreter import Interpreter +from stencila.schema.types import CodeChunk +from stencila.schema.util import to_json, from_dict + + +def _byte(b: typing.Any) -> bytes: + return bytes((b,)) + + +def encode(number: int) -> bytes: + """Pack `number` into varint bytes""" + buf = b'' + while True: + towrite = number & 0x7f + number >>= 7 + if number: + buf += _byte(towrite | 0x80) + else: + buf += _byte(towrite) + break + return buf + + +def decode_stream(stream: typing.IO) -> int: + """Read a varint from `stream`""" + shift = 0 + result = 0 + while True: + i = _read_one(stream) + result |= (i & 0x7f) << shift + shift += 7 + if not (i & 0x80): + break + + return result + + +def decode_bytes(buf: bytes) -> int: + """Read a varint from from `buf` bytes""" + return decode_stream(BytesIO(buf)) + + +def _read_one(stream: typing.IO) -> int: + """Read a byte from the file (as an integer) + raises EOFError if the stream ends while reading bytes. + """ + if hasattr(stream, 'recv'): + c = stream.recv(1) + elif hasattr(stream, 'buffer'): + c = stream.buffer.read(1) + else: + c = stream.read(1) + if c == b'': + raise EOFError("Unexpected EOF while reading bytes") + return ord(c) + + +class InterpreterListener: + input_stream: typing.IO + output_stream: typing.IO + interpreter: Interpreter + + def __init__(self, input_stream: typing.IO, output_stream: typing.IO, interpreter: Interpreter) -> None: + self.input_stream = input_stream + self.output_stream = output_stream + self.interpreter = interpreter + + def read_message(self) -> typing.Iterable[bytes]: + while True: + message_len = decode_stream(self.input_stream) + yield self.input_stream.read(message_len) + + def write_message(self, s: bytes) -> None: + self.output_stream.write(encode(len(s))) + self.output_stream.write(s) + self.output_stream.flush() + + def run_interpreter(self) -> None: + for message in self.read_message(): + envelope = json.loads(message.decode('utf8')) + + code = from_dict(envelope['body']) + + if isinstance(code, CodeChunk): + code = simple_code_chunk_parse(code) + + self.interpreter.execute([code], {}) + envelope['body'] = code + self.write_message(to_json(envelope).encode('utf8')) + + +def start_stdio_interpreter() -> None: + il = InterpreterListener(sys.stdin.buffer, sys.stdout.buffer, Interpreter()) + il.run_interpreter() + + +if __name__ == '__main__': + logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + start_stdio_interpreter()