-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This adds a server that acts as a function (exp, env) -> (result, env') meaning the server has to hold no state. The client maintains and transmits the state. This has some bugs right now, such as not being able to serialize recursive functions. Those will be fixed soon. --- This commit was comprised of the following commits: Add initial ScrapMonad Add a notion of relocations We'll need to link native functions, for example, if we take in a serialized environment from the outside. Add small server with JSON API and little HTML page Provide a networked REPL, perhaps as a demo. Add bdecode This is a little more stateful than bencode so add a class and call into the class from a `bdecode` function. Begin deserialization Use some Python nonsense to automatically add deserializer functions to a dict if Object subclasses have them. Add some more deserializers, including first recursive one This is a little painful and I wonder if there is a better way to do it. Some niceties Add deserialization for EnvObject Deserialize env and wrap it in EnvObject We need to pass around entire scrap objects. Persist env in client Serialize/deserialize functions, closures Next up: NativeFunctionRelocation. Type ScrapMonad.__init__ Drop the _ It's cleaner. Fix infinite recursion bug All Objects have deserialize (defined or inheritecd), but all of them *define* it. Check if they define it by looking in `__dict__`. Add support for deserializing NativeFunctionRelocation With this, the web REPL can ship an env back and forth to the idempotent eval server! Fix Python 3.8 Unwrap classmethod and store inner function. Kind of surprised mypy didn't catch this, since I was never storing functions before--always classmethod. Persist env in LocalStorage Persist history in LocalStorage Focus input field on page load Add TODO Throw some errors that do not normally raise Don't double fetch Add title and note Remove begin/end newlines Bound output window height and scroll to bottom Add doctype Embed empty favicon in page Save a request that will always 404. Add a little favicon Thanks, antifavicon. Set 1 hour cache control on eval requests As the server is stateless, we can have some intermediate server cache eval(env, exp) requests. If everyone loads the page and types `123`, for example, we only need receive that request once (per hour). It's not clear if fly.io (for example) does this middle cache thing automatically. Try building image in CI (#19) * Try building image in CI * wip * Add quick check post-build * Don't be interactive * Use tags from meta step to identify image Oops, remove target branch Remove excess env Scroll to bottom after loading history Add space in favicon Add button to clear LocalStorage Add TODO Add multiprocessing server Serialize/deserialize Apply Check in fly.toml Make web repl return result, not _ Report eval errors somewhat opaquely in console Add updateHistory function Use try/catch in client Display server error message in output Move all explicit exception handling into do_index Link to repl page Add charset and viewport Allow reuse of address This means we can C-c the server and re-start it on the same port immediately. Check in scrapscript.org style Adjust color and use <main> for padding Add fonts Render REPL with code/result a little better Bundle style.css with Docker image Fix input background Use prompt as label Remove TODO Don't highlight input box Deserialize bool and record Move html to separate file Expand input into textarea with Shift-Enter Add goatcounter Remove console.log Color textareas too Make the webserver fork Bring Chris's code up to date Print host in debug output
- Loading branch information
1 parent
05d2e00
commit d409f13
Showing
6 changed files
with
611 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# fly.toml app configuration file generated for scrapscript on 2023-11-29T02:06:16-05:00 | ||
# | ||
# See https://fly.io/docs/reference/configuration/ for information about how to use this file. | ||
# | ||
|
||
app = "scrapscript" | ||
primary_region = "ewr" | ||
|
||
[build] | ||
|
||
[http_service] | ||
internal_port = 8000 | ||
force_https = true | ||
auto_stop_machines = true | ||
auto_start_machines = true | ||
min_machines_running = 0 | ||
processes = ["app"] | ||
|
||
[[vm]] | ||
cpu_kind = "shared" | ||
cpus = 1 | ||
memory_mb = 256 | ||
|
||
[experimental] | ||
cmd = ["serve", "--port", "8000", "--debug", "--fork"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<meta charset="utf-8"> | ||
<meta name="viewport" content="width=device-width, initial-scale=1"> | ||
<title>Scrapscript Web REPL</title> | ||
<link rel="shortcut icon" type="image/x-icon" href="data:image/vnd.microsoft.icon;base64,AAABAAEAEBAQAAAAAAAoAQAAFgAAACgAAAAQAAAAIAAAAAEABAAAAAAAgAAAAAAAAAAAAAAAEAAAAAAAAAD/AAAA////AAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAREBEQAAAAAQAQEAEAAAABABAQAQAAAAAREBEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIiIiIiIiIiIiIiIiIiERIiESEiIiIiESEiISIiIhEiISIhEiIiIREiESEhIiIiIiIiIiIiIiIiIiIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"> | ||
<link rel="preconnect" href="https://fonts.googleapis.com" /> | ||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | ||
<link | ||
href="https://fonts.googleapis.com/css2?family=Fira+Code&display=swap" | ||
rel="stylesheet" | ||
/> | ||
<link | ||
href="https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,500;0,700&display=swap" | ||
rel="stylesheet" | ||
/> | ||
<link | ||
href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,wght@0,400;0,700;0,900;1,400&display=swap" | ||
rel="stylesheet" | ||
/> | ||
<link rel="stylesheet" href="/style.css" /> | ||
</head> | ||
<body> | ||
<main> | ||
<div> | ||
<p>See <a href="https://scrapscript.org/">scrapscript.org</a> for a slightly | ||
out of date language reference.</p> | ||
<p>This REPL sends programs to a server to be evaluated, but the server is | ||
completely stateless. All persistence is in the browser.</p> | ||
</div> | ||
<div> | ||
<!-- TODO(max): Add button to save to/load from disk. --> | ||
<button id="clear-local-storage">Clear LocalStorage</button> | ||
</div> | ||
<div id="output" style="height: 400px; overflow: auto;"> | ||
Output: | ||
</div> | ||
<div> | ||
<code>>>> </code><input id="input" type="text" /> | ||
</div> | ||
<script type="module"> | ||
"use strict"; | ||
|
||
function updateHistory(inp, out) { | ||
const pre_inp = document.createElement("pre"); | ||
pre_inp.setAttribute("class", "language-text"); | ||
const code_inp = document.createElement("code"); | ||
code_inp.setAttribute("class", "language-text"); | ||
code_inp.append(`>>> ${inp}`); | ||
pre_inp.append(code_inp); | ||
|
||
const pre_out = document.createElement("pre"); | ||
pre_out.setAttribute("class", "result language-text"); | ||
const code_out = document.createElement("code"); | ||
code_out.setAttribute("class", "language-text"); | ||
code_out.append(`${out}`); | ||
pre_out.append(code_out); | ||
|
||
output.append(pre_inp); | ||
output.append(pre_out); | ||
output.scrollTop = output.scrollHeight; | ||
} | ||
|
||
async function sendRequest(env, exp) { | ||
const params = env === null ? {exp} : {exp, env}; | ||
return fetch("/eval?" + new URLSearchParams(params)); | ||
} | ||
|
||
let input = document.getElementById("input"); | ||
const output = document.getElementById("output"); | ||
const history = []; | ||
const button = document.getElementById("clear-local-storage"); | ||
|
||
function renderHistory() { | ||
for (const [inp, out] of history) { | ||
updateHistory(inp, out); | ||
} | ||
} | ||
|
||
function loadFromLocalStorage() { | ||
history.length = 0; | ||
output.innerHTML = ""; | ||
const hist = window.localStorage.getItem('history'); | ||
if (hist !== null) { | ||
history.push(...JSON.parse(hist)); | ||
} | ||
renderHistory(); | ||
document.env = window.localStorage.getItem('env'); | ||
} | ||
|
||
function expandInput() { | ||
if (input.tagName === "TEXTAREA") { | ||
return; | ||
} | ||
const textarea = document.createElement("textarea"); | ||
textarea.setAttribute("id", "input"); | ||
textarea.setAttribute("rows", "1"); | ||
textarea.setAttribute("style", "height: 1.5em;"); | ||
textarea.value = input.value; | ||
input.replaceWith(textarea); | ||
input = textarea; | ||
input.focus(); | ||
input.addEventListener("keyup", e => inputHandler(e)); | ||
} | ||
|
||
function shrinkInput() { | ||
if (input.tagName === "INPUT") { | ||
return; | ||
} | ||
const newInput = document.createElement("input"); | ||
newInput.setAttribute("id", "input"); | ||
newInput.setAttribute("type", "text"); | ||
newInput.value = input.value; | ||
input.replaceWith(newInput); | ||
input = newInput; | ||
input.focus(); | ||
input.addEventListener("keyup", e => inputHandler(e)); | ||
} | ||
|
||
loadFromLocalStorage(); | ||
input.focus(); | ||
async function inputHandler(e) { | ||
// TODO(max): Make up/down arrow keys navigate history | ||
if (e.key === "Enter") { | ||
if (e.shiftKey && input.tagName === "INPUT") { | ||
// Shift-Enter expands the input to a textarea and does not submit | ||
// input. | ||
expandInput(); | ||
return; | ||
} | ||
if (!e.shiftKey && input.tagName === "TEXTAREA") { | ||
// Normal Enter in a textarea should not submit input. | ||
return; | ||
} | ||
const response = await sendRequest(document.env, input.value); | ||
if (response.ok) { | ||
const {env, result} = await response.json(); | ||
updateHistory(input.value, result); | ||
history.push([input.value, result]); | ||
input.value = ""; | ||
document.env = env; | ||
window.localStorage.setItem('env', env) | ||
window.localStorage.setItem('history', JSON.stringify(history)); | ||
} else { | ||
const msg = await response.text(); | ||
updateHistory(input.value, msg); | ||
input.value = ""; | ||
} | ||
if (input.tagName === "TEXTAREA") { | ||
shrinkInput(); | ||
} | ||
} | ||
} | ||
input.addEventListener("keyup", e => inputHandler(e)); | ||
button.addEventListener("click", () => { | ||
window.localStorage.clear(); | ||
loadFromLocalStorage(); | ||
input.focus(); | ||
}); | ||
</script> | ||
</main> | ||
<script data-goatcounter="https://scrapscript.goatcounter.com/count" | ||
async src="//gc.zgo.at/count.js"></script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.