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.
This commit is contained in:
parent
b7c983d3d6
commit
6a038fcdde
4
cli/__main__.py
Normal file
4
cli/__main__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
30
cli/cli.py
Normal file
30
cli/cli.py
Normal file
@ -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()
|
0
cli/data/__init__.py
Normal file
0
cli/data/__init__.py
Normal file
118
cli/data/computer.py
Normal file
118
cli/data/computer.py
Normal file
@ -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]
|
||||
"""
|
@ -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
|
||||
"""
|
0
cli/lib/__init__.py
Normal file
0
cli/lib/__init__.py
Normal file
87
cli/lib/clientside.py
Normal file
87
cli/lib/clientside.py
Normal file
@ -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();
|
||||
"""
|
252
cli/lib/graphparser.py
Normal file
252
cli/lib/graphparser.py
Normal file
@ -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 <class 'dict'> 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
|
106
cli/lib/server.py
Normal file
106
cli/lib/server.py
Normal file
@ -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)
|
0
cli/tests/__init__.py
Normal file
0
cli/tests/__init__.py
Normal file
84
cli/tests/isa.py
Normal file
84
cli/tests/isa.py
Normal file
@ -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],
|
||||
)
|
||||
)
|
46
console.js
46
console.js
@ -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();
|
@ -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()
|
@ -1,4 +0,0 @@
|
||||
from . import entry
|
||||
|
||||
if __name__ == "__main__":
|
||||
entry()
|
@ -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
|
@ -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")
|
@ -1,4 +1,4 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
nativeBuildInputs = with pkgs.python311Packages; [ websockets watchdog pyperclip ];
|
||||
nativeBuildInputs = with pkgs.python311Packages; [ websockets watchdog pyperclip timeout-decorator ];
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
{"testing": "\\left[1,2,3,4,5,1\\right]"}
|
Loading…
Reference in New Issue
Block a user