From 6a038fcdde776f1afb7646416d8a09b5e16f4889 Mon Sep 17 00:00:00 2001 From: stupidcomputer Date: Sun, 16 Jun 2024 18:18:28 -0500 Subject: [PATCH] massive project restructure relocated everything in its own module, `cli`, and split the server code such that it's more component based, if you will also, added a working test suite, which is somewhat exciting. --- desmosisa/asm.py => cli/__init__.py | 0 cli/__main__.py | 4 + cli/cli.py | 30 +++ cli/data/__init__.py | 0 cli/data/computer.py | 118 ++++++++++ data/testing.desmos => cli/data/testing.py | 4 +- cli/lib/__init__.py | 0 cli/lib/clientside.py | 87 +++++++ cli/lib/graphparser.py | 252 +++++++++++++++++++++ cli/lib/server.py | 106 +++++++++ data/computer.desmos => cli/lib/testing | 0 cli/tests/__init__.py | 0 cli/tests/isa.py | 84 +++++++ console.js | 46 ---- desmosisa/__init__.py | 42 ---- desmosisa/__main__.py | 4 - desmosisa/parser.py | 176 -------------- desmosisa/server.py | 109 --------- shell.nix | 2 +- test.override | 1 - 20 files changed, 685 insertions(+), 380 deletions(-) rename desmosisa/asm.py => cli/__init__.py (100%) create mode 100644 cli/__main__.py create mode 100644 cli/cli.py create mode 100644 cli/data/__init__.py create mode 100644 cli/data/computer.py rename data/testing.desmos => cli/data/testing.py (59%) create mode 100644 cli/lib/__init__.py create mode 100644 cli/lib/clientside.py create mode 100644 cli/lib/graphparser.py create mode 100644 cli/lib/server.py rename data/computer.desmos => cli/lib/testing (100%) create mode 100644 cli/tests/__init__.py create mode 100644 cli/tests/isa.py delete mode 100644 console.js delete mode 100644 desmosisa/__init__.py delete mode 100644 desmosisa/__main__.py delete mode 100644 desmosisa/parser.py delete mode 100644 desmosisa/server.py delete mode 100644 test.override diff --git a/desmosisa/asm.py b/cli/__init__.py similarity index 100% rename from desmosisa/asm.py rename to cli/__init__.py diff --git a/cli/__main__.py b/cli/__main__.py new file mode 100644 index 0000000..9ae637f --- /dev/null +++ b/cli/__main__.py @@ -0,0 +1,4 @@ +from .cli import main + +if __name__ == "__main__": + main() diff --git a/cli/cli.py b/cli/cli.py new file mode 100644 index 0000000..0cf6204 --- /dev/null +++ b/cli/cli.py @@ -0,0 +1,30 @@ +from .lib.server import DesmosGraphServer +from .lib.graphparser import DesmosGraph, DesmosGraphOverride + +from .lib.clientside import payload as JSGraphPayload +from .tests.isa import test_entry_point + +def main(): +# graph = DesmosGraph.from_file("data/computer.desmos") +# override = DesmosGraphOverride.from_file("test.override") +# +# graph.include_override(override) +# server = DesmosGraphServer() +# server.append_inst({ +# "type": "insert_graph", +# "graph": graph, +# }) +# server.append_inst({ +# "type": "test_graph", +# "graph": graph, +# "name": "test and assert addition", +# "expectedOutput": [1, 4, 6, 0, 0, 4], +# "expression": "B", +# }) +# server.start() +# print(server.outputs) +# + test_entry_point() + +if __name__ == "__main__": + main() diff --git a/cli/data/__init__.py b/cli/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/data/computer.py b/cli/data/computer.py new file mode 100644 index 0000000..cf5597d --- /dev/null +++ b/cli/data/computer.py @@ -0,0 +1,118 @@ +payload = """ +ticker 1 : main +# Main memory structure +# This should be filled in with an override +id testing : B = [] +# Operations on memory +: q(l, v, i)=ifval(l, i, [1...length(l)], v) +: setlistval(index, v) = B -> q(B, v, index) +: incx(index, v) = setlistval(index, B[index] + v) +: ifval(l, index, c, v) = {c = index : v, l[c]} + +# Operations +: oadd(x, y) = x + y +: osub(x, y) = x - y +: odiv(x, y) = x / y +: omul(x, y) = x * y + +# Instruction implementation +: ijmp(a, b, c) = ip -> a, jumped -> 1 +: iadd(a, b, t_o) = setlistval(t_o, oadd(B[a], B[b])) +: isub(a, b, t_o) = setlistval(t_o, osub(B[a], B[b])) +: idiv(a, b, t_o) = setlistval(t_o, odiv(B[a], B[b])) +: imul(a, b, t_o) = setlistval(t_o, omul(B[a], B[b])) + +: icmp(a, b, z) = { \ + a = b : equals -> 1 , \ + a > b : greater -> 1 , \ + a < b : less -> 1 \ + } +: irst(a, b, c) = equals -> 0, greater -> 0, less -> 0 +: ield(addr, b) = setlistval(addr, equals) +: igld(addr, b) = setlistval(addr, greater) +: illd(addr, b) = setlistval(addr, less) + + +: ibe(addr, b, c) = {equals = 1 : ijmp(addr, 0, 0), jumped -> 1} +: ibne(addr, b, c) = {equals = 0 : ijmp(addr, 0, 0), jumped -> 1} +: ibg(addr, b, c) = {greater = 1 : ijmp(addr, 0, 0), jumped -> 1} +: ibl(addr, b, c) = {less = 1 : ijmp(addr, 0, 0), jumped -> 1} + +: isto(v, addr) = setlistval(addr, v) +: imov(from, target) = setlistval(target, B[from]) +: ipsto(value, ptr) = setlistval(B[ptr], value) + +# registers +# instruction pointer +: ip = 1 + +# is the result of icmp equal? +: equals = 0 + +# ditto for greater than +: greater = 0 + +# ditto for less than +: less = 0 + +# instruction loading areas +: inst = 0 + +# next three values after the instruction +: paramone = 0 +: paramtwo = 0 +: paramthree = 0 + +# main execution flows +: load(addr) = jumped -> 0, inst -> B[addr], \ + paramone -> B[addr + 1], \ + paramtwo -> B[addr + 2], \ + paramthree -> B[addr + 3] +: exec = { \ + inst = sto : isto(paramone, paramtwo), \ + inst = psto : ipsto(paramone, paramtwo), \ + inst = mov : imov(paramone, paramtwo), \ + inst = add : iadd(paramone, paramtwo, paramthree), \ + inst = cmp : icmp(paramone, paramtwo, paramthree), \ + inst = eld : ield(paramone, paramtwo), \ + inst = gld : igld(paramone, paramtwo), \ + inst = lld : illd(paramone, paramtwo), \ + inst = jmp : ijmp(paramone, paramtwo, paramthree), \ + inst = be : ibe(paramone, paramtwo, paramthree), \ + inst = bne : ibne(paramone, paramtwo, paramthree), \ + inst = bg : ibg(paramone, paramtwo, paramthree), \ + inst = bl : ibl(paramone, paramtwo, paramthree), \ + inst = sub : isub(paramone, paramtwo, paramthree), \ + inst = mul : imul(paramone, paramtwo, paramthree), \ + inst = div : idiv(paramone, paramtwo, paramthree) \ +} +: incip = {jumped = 0 : ip -> ip + instwidth[inst] + 1} + +# execution occurs here +: execution = 0 +: jumped = 0 + +: loop = {execution = 0 : execution -> 1, execution = 1 : execution -> 2, execution = 2 : execution -> 0} +: loopaction = {execution = 0 : load(ip), execution = 1 : exec, execution = 2 : incip} +: main = loopaction, loop + +: sto = 1 +: psto = 16 +: mov = 17 +: add = 2 +: cmp = 3 +: eld = 4 +: gld = 5 +: lld = 6 +: jmp = 7 +: be = 8 +: bne = 9 +: bg = 10 +: bl = 11 +: sub = 12 +: mul = 13 +: div = 14 +: rst = 15 + +: instwidth = [2,3,1,1,1,1,1,1,1,1,3,3,3,3,0,2,2] +""" diff --git a/data/testing.desmos b/cli/data/testing.py similarity index 59% rename from data/testing.desmos rename to cli/data/testing.py index b6afd5d..ec1016c 100644 --- a/data/testing.desmos +++ b/cli/data/testing.py @@ -1,6 +1,8 @@ +payload = """ : testing = 32 : b = testing : m = 3 : y = m * x + b : y = 2m * x + b -: \frac{x^{2+3}}{3}*4*testing \ No newline at end of file +: \frac{x^{2+3}}{3}*4*testing +""" diff --git a/cli/lib/__init__.py b/cli/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/lib/clientside.py b/cli/lib/clientside.py new file mode 100644 index 0000000..be51379 --- /dev/null +++ b/cli/lib/clientside.py @@ -0,0 +1,87 @@ +payload = """ +// https://stackoverflow.com/questions/3115982/how-to-check-if-two-arrays-are-equal-with-javascript +function arraysEqual(a, b) { + if (a === b) return true; + if (a == null || b == null) return false; + if (a.length !== b.length) return false; + + for (var i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) return false; + } + return true; +} + +function main() { + let socket = new WebSocket("ws://localhost:8764"); + var toCompare = ""; + + socket.onopen = function(e) { + console.log("[LOG] sending client ping") + socket.send("client ping"); + } + + socket.onclose = function(e) { + setTimeout(function() { + main(); + }, 1000); + } + + socket.onmessage = function(e) { + var message = JSON.parse(e.data); + + console.log(message.message) + if (message.message === "clear") { + console.log("[LOG] removing expressions from the graph"); + Calc.getExpressions().map((i) => { + return i.id; + console.log(`[LOG] removing expression ${i.id}`); + }).map((i) => { + Calc.removeExpression({ id: i }); + }); + } else if (message.message === "expression") { + console.log(`[LOG] adding expression ${message.payload} as id ${message.id}`); + Calc.setExpression({ + type: "expression", + latex: message.payload, + id: message.id, + }) + } else if (message.message === "ticker") { + var state = Calc.getState(); + + state.expressions.ticker = { + handlerLatex: message.payload, + minStepLatex: message.rate, + open: true, + }; + + Calc.setState(JSON.stringify(state)) + } else if (message.message === "eval") { + toCompare = eval(message.expectedOutput); + var exp = Calc.HelperExpression({ + latex: message.expression + }) + var timeoutid = setTimeout(function() { + console.log("[LOG] failing because timeout occured") + socket.send(JSON.stringify({ + "name": message.expression, + "output": "false" + })) + exp.unobserve('listValue') + }, 5000); + exp.observe('listValue', function () { + console.log(exp.listValue); + if(arraysEqual(exp.listValue, toCompare)) { + socket.send(JSON.stringify({ + "name": message.expression, + "output": "true" + })) + clearTimeout(timeoutid); + } + }) + Calc.controller.listModel.ticker.playing = true; + } else { + console.log(`[LOG] couldn't parse message ${e.data}`) + } + } +} main(); +""" diff --git a/cli/lib/graphparser.py b/cli/lib/graphparser.py new file mode 100644 index 0000000..bf06017 --- /dev/null +++ b/cli/lib/graphparser.py @@ -0,0 +1,252 @@ +from dataclasses import dataclass +from typing import Callable, ClassVar, Self, Any, IO, List, Dict +from json import loads + +reserved_keywords = "sin cos tan csc sec cot mean median min max quertile quantile stdev stdevp var mad cov covp corr spearman stats count total join sort shuffle unique for histogram dotplot boxplot normaldist tdist poissondist binomialdist uniformdist pdf cdf inversecdf random ttest tscore ittest frac sinh cosh tanh csch sech coth polygon distance midpoint rgb hsv lcm gcd mod ceil floor round sign nPr nCr log with cdot to in length left right operatorname" +reserved_keywords = reserved_keywords.split(' ') + +def continue_lines(string: str) -> str: + # if there's a '\' and then a newline, ignore the newline. + return string.replace('\\\n', '') + +def replace_multiplication(string: str) -> str: + return string.replace('*', '\\cdot ') + +def replace_actions(string: str) -> str: + return string.replace(' ->', '\\to ').replace('->', '\\to ') + +def replace_tabs(string: str) -> str: + return string.replace('\t', ' ') + +def merge_multiple_spaces(string: str) -> str: + return ' '.join(string.split()) + +def curly_brackets(string: str) -> str: + return string.replace('{', '\\left\\{').replace('}', '\\right\\}') + +def parens(string: str) -> str: + return string.replace('(', '\\left(').replace(')', '\\right)') + +def for_fix(string: str) -> str: + return string \ + .replace('for\\ ', "\\operatorname{for}") \ + .replace('for', "\\operatorname{for}") \ + .replace('length\\ ', "\\operatorname{length}") \ + .replace('length', "\\operatorname{length}") + +def make_spaces_permanant(string: str) -> str: + return string.replace(' ', '\ ') + +def change_square_brackets(string: str) -> str: + return string.replace('[', '\\left[').replace(']', '\\right]') + +def subscriptize(string: str) -> str: + output = "" + buffer = "" + for char in (string + "\n"): # add a newline so there's always a non-alpha char at the end + if char.isalpha(): + buffer += char + + else: + if buffer: + # check if our buffer is a reserved word; if so, do + # not expand. + if not buffer in reserved_keywords: + if len(buffer) > 1: + output += "{}_{{{}}}".format(buffer[0], buffer[1:]) + else: + output += buffer[0] + else: + output += buffer + + buffer = "" + output += char + + return output.rstrip() + +@dataclass +class DesmosGraphOverride: + """ + Container for Desmos Overrides. + + This is the method by which machine code compiled by assembler or otherwise, is inserted. + + It's sort of like a fancy substitution macro system thing. It's still prone to stupidity. + """ + + # XXX: If you use the Dict[str, str] type annotation, dataclass throws an error: + # ValueError: mutable default for field payload is not allowed: use default_factory + # Ask a question about this + payload: Any = None + + @classmethod + def from_file(cls, filename: str) -> Self: + """ + Read a Override from the filename filename. + """ + fd = open(filename, "r") + + return cls.from_file_object(fd) + + @classmethod + def from_file_object(cls, io_obj: IO) -> Self: + """ + Read a Override from fileobj fileobj. + """ + text = io_obj.read() + + return cls.from_text(text) + + @classmethod + def from_text(cls, text: str) -> Self: + return cls(loads(text)) + +@dataclass +class DesmosGraphStatement: + """ + This class contains one Desmos graph statement -- that is, one line in the editor. + + You probably shouldn't create this class manually; it's used in .DesmosGraph. + """ + commands: str + latex: str + + def __getitem__(self, item) -> str: + try: + return self.commands[item] + except KeyError: + return None + + def __repr__(self) -> str: + return "{} : {}".format(str(self.commands), self.latex) + + @classmethod + def from_line(cls, line) -> Self: + if not line: + return None + + if line[0] == "#": # ignore comments + return None + + splitted = line.split(':') + commands = splitted[0] + latex = ':'.join(splitted[1:]) + + commands = commands.split(' ') + iterator = iter(commands) + commands = dict(zip(iterator, iterator)) + + try: + del commands[''] + except KeyError: + pass + + return cls(commands, latex) + + @classmethod + def from_lines(cls, lines) -> List[str]: + output = [] + for line in lines: + output.append(cls.from_line(line)) + + return list( + filter( + lambda item: item is not None, + output + ) + ) + +@dataclass +class DesmosGraph: + """ + This class represents a Desmos Graph. That is, the thing that you interact with when you use Desmos. That thing. + + You want to create one through a series of Desmos statements. This is a cobbled together DSL that uses .replace and .split regularly. Beware! + """ + + text_preprocessors: ClassVar[List[Callable[[str], str]]] = [ + continue_lines, + replace_multiplication, + replace_actions, + replace_tabs + ] + line_preprocessors: ClassVar[List[Any]] = [ + DesmosGraphStatement.from_lines + ] + latex_preprocessors: ClassVar[List[Callable[[str], str]]] = [ + merge_multiple_spaces, + curly_brackets, + parens, + subscriptize, + change_square_brackets, + for_fix, + make_spaces_permanant + ] + + text: str + + def __post_init__(self): + self.ast: List[DesmosGraphStatement] = [] + self._parse() + + def _parse(self) -> None: + text: str = self.text + for preprocessor in self.text_preprocessors: + text = preprocessor(text) + + lines = text.split('\n') + for preprocessor in self.line_preprocessors: + lines = preprocessor(lines) + + for index, line in enumerate(lines): + if not line["comment"]: + for preprocessor in self.latex_preprocessors: + lines[index].latex = preprocessor(line.latex) + + self.ast = lines + + def to_file(self, filename: str) -> None: + """ + Write the Graph to the filename filename. + """ + fd = open(filename, "w") + self.to_fileobj(fd) + + def to_fileobj(self, io_obj: IO) -> None: + """ + Write the Graph to the fileobj fileobj. + """ + io_obj.write(self.text) + + @classmethod + def from_file(cls, filename: str) -> Self: + """ + Read a Graph from the filename filename. + """ + fd = open(filename, "r") + + return cls.from_file_object(fd) + + @classmethod + def from_file_object(cls, io_obj: IO) -> Self: + """ + Read a Graph from fileobj fileobj. + """ + text = io_obj.read() + + return cls(text) + + def include_override(self, override: DesmosGraphOverride) -> None: + """ + Overrides are the way to include changes in a Desmos Graph. This is changes in the instructions and stuff. + + If you're writing an assembler, you can use an override for computer.desmos to include the compiled machine code. + """ + # XXX: This is O(n^2). Or somewhat inefficient. Not a computer science major. + for key in override.payload.keys(): + for statement in self.ast: + try: + if statement.commands["id"] == key: + statement.latex = override.payload[key] + except KeyError: + pass diff --git a/cli/lib/server.py b/cli/lib/server.py new file mode 100644 index 0000000..304e03f --- /dev/null +++ b/cli/lib/server.py @@ -0,0 +1,106 @@ +import asyncio +from websockets.server import serve +from websockets.sync.client import connect + +import json +import random + +class DesmosGraphServer: + instructions_to_run = [] + outputs = [] + + async def _reset(self, websocket): + await websocket.send( + json.dumps( + { + "message": "clear", + "payload": "none", + } + ) + ) + + async def _send_graph(self, websocket, graph): + for line in graph.ast: + if line["ticker"]: + await websocket.send( + json.dumps( + { + "message": "ticker", + "rate": line.commands["ticker"], + "payload": line.latex, + } + ) + ) + + continue + + # if the line has been assigned an id, make it so + if line["id"]: + ident = line.commands["id"] + else: + # else just choose a safe option + ident = "placeholder" + str(random.randint(1, 100100)) + + await websocket.send( + json.dumps( + { + "message": "expression", + "id": ident, + "payload": line.latex, + } + ) + ) + + async def _check_for_thing(self, websocket, expectedOutput, expression): + await websocket.send( + json.dumps( + { + "message": "eval", + "expectedOutput": expectedOutput, + "expression": expression, + } + ) + ) + + async def ws_main(self, websocket): + message = await websocket.recv() # eat the client ping + + for instruction in self.instructions_to_run: + await self._reset(websocket) + + if instruction["type"] == "insert_graph": + await self._send_graph(websocket, instruction["graph"]) + + elif instruction["type"] == "test_graph": + await self._send_graph(websocket, instruction["graph"]) + await self._check_for_thing( + websocket, + instruction["expectedOutput"], + instruction["expression"] + ) + + message = await websocket.recv() + jsonified = json.loads(message) + result = jsonified["output"] + self.outputs.append( + { + "name": instruction["name"], + "output": result, + } + ) + + self.stop.set_result("sotp please!!!!") + + async def main(self, stop): + async with serve(self.ws_main, "localhost", 8764): + self.stop = stop + await stop + + def start(self): + loop = asyncio.get_event_loop() + stop = loop.create_future() + + loop.run_until_complete(self.main(stop)) + + def append_inst(self, inst): + self.instructions_to_run.append(inst) diff --git a/data/computer.desmos b/cli/lib/testing similarity index 100% rename from data/computer.desmos rename to cli/lib/testing diff --git a/cli/tests/__init__.py b/cli/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cli/tests/isa.py b/cli/tests/isa.py new file mode 100644 index 0000000..728d55f --- /dev/null +++ b/cli/tests/isa.py @@ -0,0 +1,84 @@ +import unittest +import timeout_decorator +import time + +from cli.lib.server import DesmosGraphServer +from cli.lib.graphparser import DesmosGraph, DesmosGraphOverride +from cli.data.computer import payload as computer_graph_payload + +def arr_to_override_text(arr): + arr = [str(i) for i in arr] + return "B = \\left[{}\\right]".format( + ", ".join(arr) + ) + +def instruction_test_helper(override_text, expected_output): + graph = DesmosGraph(computer_graph_payload) + override = DesmosGraphOverride({ + "testing": arr_to_override_text(override_text) + }) + + graph.include_override(override) + + server = DesmosGraphServer() + server.instructions_to_run = [] + server.append_inst({ + "type": "test_graph", + "graph": graph, + "name": "", + "expectedOutput": expected_output, + "expression": "B", + }) + server.start() + + time.sleep(1) + return server.outputs[-1]["output"] == "true" + +class ISATest(unittest.TestCase): + def test_store_positive(self): + self.assertTrue( + instruction_test_helper( + [1, 4, 6, 0, 0, 0], # store lit. 4 to address 6 + [1, 4, 6, 0, 0, 4] + ) + ) + + def test_addition(self): + self.assertTrue( + instruction_test_helper( + [2, 1, 1, 5, 0], # add addresses 1 and 1 to cell 5 + [2, 1, 1, 5, 4] + ) + ) + + def test_subtraction(self): + self.assertTrue( + instruction_test_helper( + [12, 1, 2, 5, 0], # 12 - 1 into cell 5 + [12, 1, 2, 5, 11] + ) + ) + + def test_multiplication(self): + self.assertTrue( + instruction_test_helper( + [13, 1, 3, 5, 0], # 13 * 3 + [13, 1, 3, 5, 39], + ) + ) + + def test_division(self): + self.assertTrue( + instruction_test_helper( + [14, 1, 6, 5, 0, 7], # 14 / 7 + [14, 1, 6, 5, 2, 7], + ) + ) + + def test_division_with_decimal(self): + self.assertTrue( + instruction_test_helper( + [14, 1, 6, 5, 0, 4], # 14 / 4 = 3.5 + [14, 1, 6, 5, 3.5, 4], + ) + ) diff --git a/console.js b/console.js deleted file mode 100644 index 935c419..0000000 --- a/console.js +++ /dev/null @@ -1,46 +0,0 @@ -function main() { - let socket = new WebSocket("ws://localhost:8764"); - var ids = []; - - socket.onopen = function(e) { - console.log("[LOG] sending client ping") - socket.send("client ping"); - } - - socket.onmessage = function(e) { - var message = JSON.parse(e.data); - - console.log(message.message) - if (message.message === "clear") { - console.log("[LOG] removing expressions from the graph"); - for(i in ids) { - console.log(`[LOG] removing expression ${ids[i]}`) - Calc.removeExpression({ - id: ids[i], - }) - } - - ids = []; - } else if (message.message === "expression") { - console.log(`[LOG] adding expression ${message.payload} as id ${message.id}`); - Calc.setExpression({ - type: "expression", - latex: message.payload, - id: message.id, - }) - ids.push(message.id) - } else if (message.message === "ticker") { - var state = Calc.getState(); - - state.expressions.ticker = { - handlerLatex: message.payload, - minStepLatex: message.rate, - open: true, - }; - - Calc.setState(JSON.stringify(state)) - } else { - console.log(`[LOG] couldn't parse message ${e.data}`) - } - } -} main(); diff --git a/desmosisa/__init__.py b/desmosisa/__init__.py deleted file mode 100644 index b2d4638..0000000 --- a/desmosisa/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -from .server import main -import argparse -import pyperclip -import json - -def get_overrides(file): - fd = open(file, "r") - data = json.loads(fd.read()) - fd.close() - return data - -def define_args(parser): - parser.add_argument('--copy', action="store_true", help="copy the client side JS to clipboard") - parser.add_argument('--assemble', nargs=2, help="assemble the file INFILE and write the resultant overrides to OUTFILE") - parser.add_argument('--run', help="specify file to start the desmos server for") - parser.add_argument('--overrides', help="specify file that contains overrides for desmos expressions") - - -def entry(): - parser = argparse.ArgumentParser( - prog="desmosisa", - description="a smörgåsbord of utilities for desmos, including some implementations of an desmos-based isa", - ) - - define_args(parser) - - args = parser.parse_args() - if args.overrides: - args.overrides = get_overrides(args.overrides) - - if args.run: - main(args.run, args.overrides if args.overrides else {}) - elif args.copy: - fd = open("console.js", "r") - buffer = fd.read() - pyperclip.copy(buffer) - print("copied") - else: - parser.print_help() - -if __name__ == "__main__": - entry() diff --git a/desmosisa/__main__.py b/desmosisa/__main__.py deleted file mode 100644 index 0bb2e7e..0000000 --- a/desmosisa/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -from . import entry - -if __name__ == "__main__": - entry() diff --git a/desmosisa/parser.py b/desmosisa/parser.py deleted file mode 100644 index 5c454f1..0000000 --- a/desmosisa/parser.py +++ /dev/null @@ -1,176 +0,0 @@ -# desmos reserved keywords. think function names, and other verbs -# like with, etc. -reserved_keywords = "sin cos tan csc sec cot mean median min max quertile quantile stdev stdevp var mad cov covp corr spearman stats count total join sort shuffle unique for histogram dotplot boxplot normaldist tdist poissondist binomialdist uniformdist pdf cdf inversecdf random ttest tscore ittest frac sinh cosh tanh csch sech coth polygon distance midpoint rgb hsv lcm gcd mod ceil floor round sign nPr nCr log with cdot to in length left right operatorname" -reserved_keywords = reserved_keywords.split(' ') - -def continue_lines(string): - # if there's a '\' and then a newline, ignore the newline. - return string.replace('\\\n', '') - -def replace_multiplication(string): - return string.replace('*', '\\cdot ') - -def replace_actions(string): - return string.replace(' ->', '\\to ').replace('->', '\\to ') - -def replace_tabs(string): - return string.replace('\t', ' ') - -def split_linewise(string): - return string.split('\n') - -def merge_multiple_spaces(string): - return ' '.join(string.split()) - -def curly_brackets(string): - return string.replace('{', '\\left\\{').replace('}', '\\right\\}') - -def parens(string): - return string.replace('(', '\\left(').replace(')', '\\right)') - -def for_fix(string): - return string \ - .replace('for\\ ', "\\operatorname{for}") \ - .replace('for', "\\operatorname{for}") \ - .replace('length\\ ', "\\operatorname{length}") \ - .replace('length', "\\operatorname{length}") - -def make_spaces_permanant(string): - return string.replace(' ', '\ ') - -def change_square_brackets(string): - return string.replace('[', '\\left[').replace(']', '\\right]') - -def subscriptize(string): - output = "" - buffer = "" - for char in (string + "\n"): # add a newline so there's always a non-alpha char at the end - if char.isalpha(): - buffer += char - - else: - if buffer: - # check if our buffer is a reserved word; if so, do - # not expand. - if not buffer in reserved_keywords: - if len(buffer) > 1: - output += "{}_{{{}}}".format(buffer[0], buffer[1:]) - else: - output += buffer[0] - else: - output += buffer - - buffer = "" - output += char - - return output.rstrip() - -class Statement: - def __init__(self, commands, latex): - self.commands = commands - self.latex = latex - - def __getitem__(self, item): - try: - return self.commands[item] - except KeyError: - return None - - def __repr__(self): - return "{} : {}".format(str(self.commands), self.latex) - - @classmethod - def from_line(cls, line): - if not line: - return None - - if line[0] == "#": # ignore comments - return None - - splitted = line.split(':') - commands = splitted[0] - latex = ':'.join(splitted[1:]) - - commands = commands.split(' ') - iterator = iter(commands) - commands = dict(zip(iterator, iterator)) - - try: - del commands[''] - except KeyError: - pass - - return cls(commands, latex) - - @classmethod - def from_lines(cls, lines): - output = [] - for line in lines: - output.append(cls.from_line(line)) - - return list( - filter( - lambda item: item is not None, - output - ) - ) - - -class Parser: - """ - Implements the parsing of the Desmos local DSL. - - General syntax: - option1 value1 option2 value2 option3 value3 : latex - - in latex statements, multiplication between variables is *not* implicit. - this is because - `testing` - becomes - `t_{esting}`. - - In order to multiple two variables together, use - `a * b` - instead of - `ab`. - - """ - def __init__(self, file): - self.file = file - self.ast = [] - - def parse(self): - with open(self.file, "r") as f: - text = f.read() - - text = continue_lines(text) - text = replace_multiplication(text) - text = replace_actions(text) - text = replace_tabs(text) - lines = split_linewise(text) - lines = Statement.from_lines(lines) - - for index, line in enumerate(lines): - if not line["comment"]: - # now, remove multiple spaces in lines, replacing them with one. - lines[index].latex = merge_multiple_spaces(line.latex) - - # replace curly brackets and parens - lines[index].latex = curly_brackets(line.latex) - lines[index].latex = parens(line.latex) - - # convert things like testing to t_{esting} - lines[index].latex = subscriptize(line.latex) - - # change square brackets to \\left[ and \\right] - lines[index].latex = change_square_brackets(line.latex) - - print(lines[index].latex) - # replace for with \\operatorname{for} - lines[index].latex = for_fix(line.latex) - print(lines[index].latex) - - # make the spaces escaped and 'permanant' - lines[index].latex = make_spaces_permanant(line.latex) - - self.ast = lines \ No newline at end of file diff --git a/desmosisa/server.py b/desmosisa/server.py deleted file mode 100644 index cf32b57..0000000 --- a/desmosisa/server.py +++ /dev/null @@ -1,109 +0,0 @@ -from websockets.server import serve -import asyncio -import os -import time -import json -import random -import queue -import functools -from .parser import Parser, Statement -from watchdog.events import FileSystemEventHandler -from watchdog.events import FileModifiedEvent -from watchdog.observers import Observer - -class FSEHandler(FileSystemEventHandler): - def __init__(self, queue, *args, **kwargs): - self.queue = queue - super().__init__(*args, **kwargs) - - def on_modified(self, event): - self.queue.put("") - -async def serv(websocket, file, overrides={}): - message = await websocket.recv() - lmtime = 0 - epsilon = 0.25 # tweak this to what makes sense. 0.25 seconds makes sense to me. - q = queue.Queue() - - # make it read the file initially - q.put("") - - # setup the watchdog - observer = Observer() - event_handler = FSEHandler(q) - observer.schedule(event_handler, file) - observer.start() - - print("client connected") - - while True: - # there are sometimes multiple writes bundled close together -- so - # debounce them. - q.get() - if time.time() - lmtime < epsilon: - continue - else: - lmtime = time.time() - - parser = Parser(file) - parser.parse() - - await websocket.send( - json.dumps( - { - "message": "clear", - "payload": "none", - } - ) - ) - - for line in parser.ast: - if line["ticker"]: - await websocket.send( - json.dumps( - { - "message": "ticker", - "rate": line.commands["ticker"], - "payload": line.latex, - } - ) - ) - - continue - - # if the line has been assigned an id, make it so - if line["id"]: - ident = line.commands["id"] - print("performing substitution") - else: - # else just choose a safe option - ident = "placeholder" + str(random.randint(1, 100100)) - - if ident in overrides.keys(): - to_send = overrides[ident] - else: - to_send = line.latex - - - await websocket.send( - json.dumps( - { - "message": "expression", - "id": ident, - "payload": to_send, - } - ) - ) - -async def start_server(file, overrides): - print("starting server") - wrapper = functools.partial(serv, file=file, overrides=overrides) - async with serve(wrapper, "localhost", 8764): - print("starting server for realz") - await asyncio.Future() - -def main(file, overrides): - asyncio.run(start_server(file, overrides)) - -if __name__ == "__main__": - main("data/testing.desmos") diff --git a/shell.nix b/shell.nix index e2689da..cd833d9 100644 --- a/shell.nix +++ b/shell.nix @@ -1,4 +1,4 @@ { pkgs ? import {} }: pkgs.mkShell { - nativeBuildInputs = with pkgs.python311Packages; [ websockets watchdog pyperclip ]; + nativeBuildInputs = with pkgs.python311Packages; [ websockets watchdog pyperclip timeout-decorator ]; } diff --git a/test.override b/test.override deleted file mode 100644 index 4f130c2..0000000 --- a/test.override +++ /dev/null @@ -1 +0,0 @@ -{"testing": "\\left[1,2,3,4,5,1\\right]"}