diff --git a/depell/src/icons/download.svg b/depell/src/icons/download.svg index ddb30c3..d187770 100644 --- a/depell/src/icons/download.svg +++ b/depell/src/icons/download.svg @@ -1 +1 @@ - + diff --git a/depell/src/icons/run.svg b/depell/src/icons/run.svg new file mode 100644 index 0000000..07e4b31 --- /dev/null +++ b/depell/src/icons/run.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/depell/src/index.css b/depell/src/index.css index 587ede2..3e19d6b 100644 --- a/depell/src/index.css +++ b/depell/src/index.css @@ -34,6 +34,11 @@ body { } div.preview { + margin: var(--small-gap) 0px; + display: flex; + flex-direction: column; + gap: var(--small-gap); + div.info { display: flex; gap: var(--small-gap); @@ -45,9 +50,26 @@ div.preview { div.stat { display: flex; + + svg { + height: 18px; + } } - margin: var(--small-gap) 0px; + div.code { + position: relative; + + nav { + position: absolute; + right: 0; + padding: var(--small-gap); + + button { + display: flex; + padding: 0; + } + } + } } svg { @@ -86,6 +108,8 @@ pre { font-family: var(--monospace); tab-size: 4; overflow-x: auto; + white-space: pre-wrap; + word-wrap: break-word; } input { diff --git a/depell/src/index.js b/depell/src/index.js index 2d33db5..5225667 100644 --- a/depell/src/index.js +++ b/depell/src/index.js @@ -14,7 +14,7 @@ const stack_pointer_offset = 1 << 20; /** @param {WebAssembly.Instance} instance @param {Post[]} packages @param {number} fuel * @returns {string} */ -function compileCode(instance, packages, fuel) { +function compileCode(instance, packages, fuel = 1) { let { INPUT, INPUT_LEN, LOG_MESSAGES, LOG_MESSAGES_LEN, @@ -34,7 +34,7 @@ function compileCode(instance, packages, fuel) { new DataView(memory.buffer).setUint32(INPUT_LEN.value, codeLength, true); runWasmFunction(instance, compile_and_run, fuel); - return bufToString(memory, LOG_MESSAGES, LOG_MESSAGES_LEN); + return bufToString(memory, LOG_MESSAGES, LOG_MESSAGES_LEN).trim(); } /**@type{WebAssembly.Instance}*/ let fmtInstance; @@ -187,6 +187,61 @@ function loadCachedPackages(code, roots, buf, prevRoots) { } /**@type{Set}*/ const prevRoots = new Set(); +/**@typedef {Object} PackageCtx + * @property {AbortController} [cancelation] + * @property {string[]} keyBuf + * @property {Set} prevParams + * @property {HTMLTextAreaElement} [edit] */ + +/** @param {string} source @param {Set} importDiff @param {HTMLPreElement} errors @param {PackageCtx} ctx */ +async function fetchPackages(source, importDiff, errors, ctx) { + importDiff.clear(); + for (const match of source.matchAll(importRe)) { + if (localStorage["package-" + match[1]]) continue; + importDiff.add(match[1]); + } + + if (importDiff.size !== 0 && (ctx.prevParams.size != importDiff.size + || [...ctx.prevParams.keys()].every(e => importDiff.has(e)))) { + if (ctx.cancelation) ctx.cancelation.abort(); + ctx.prevParams.clear(); + ctx.prevParams = new Set([...importDiff]); + ctx.cancelation = new AbortController(); + + ctx.keyBuf.length = 0; + ctx.keyBuf.push(...importDiff.keys()); + + errors.textContent = "fetching: " + ctx.keyBuf.join(", "); + + await fetch(`/code`, { + method: "POST", + signal: ctx.cancelation.signal, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(ctx.keyBuf), + }).then(async e => { + try { + const json = await e.json(); + if (e.status == 200) { + for (const [key, value] of Object.entries(json)) { + localStorage["package-" + key] = value; + } + const missing = ctx.keyBuf.filter(i => json[i] === undefined); + if (missing.length !== 0) { + errors.textContent = "deps not found: " + missing.join(", "); + } else { + ctx.cancelation = undefined; + ctx.edit?.dispatchEvent(new InputEvent("input")); + } + } + } catch (er) { + errors.textContent = "completely failed to fetch (" + + e.status + "): " + ctx.keyBuf.join(", "); + console.error(e, er); + } + }); + } + +} /** @param {HTMLElement} target */ async function bindCodeEdit(target) { @@ -205,67 +260,24 @@ async function bindCodeEdit(target) { const hbc = await getHbcInstance(), fmt = await getFmtInstance(); let importDiff = new Set(); - const keyBuf = []; /**@type{Post[]}*/ const packages = [{ path: "local.hb", code: "" }]; const debounce = 100; - /**@type{AbortController|undefined}*/ - let cancelation = undefined; let timeout = 0; + const ctx = { keyBuf: [], prevParams: new Set(), edit }; prevRoots.clear(); const onInput = () => { - importDiff.clear(); - for (const match of edit.value.matchAll(importRe)) { - if (localStorage["package-" + match[1]]) continue; - importDiff.add(match[1]); - } + fetchPackages(edit.value, importDiff, errors, ctx); - if (importDiff.size !== 0) { - if (cancelation) cancelation.abort(); - cancelation = new AbortController(); - - keyBuf.length = 0; - keyBuf.push(...importDiff.keys()); - - errors.textContent = "fetching: " + keyBuf.join(", "); - - fetch(`/code`, { - method: "POST", - signal: cancelation.signal, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(keyBuf), - }).then(async e => { - try { - const json = await e.json(); - if (e.status == 200) { - for (const [key, value] of Object.entries(json)) { - localStorage["package-" + key] = value; - } - const missing = keyBuf.filter(i => json[i] === undefined); - if (missing.length !== 0) { - errors.textContent = "deps not found: " + missing.join(", "); - } else { - cancelation = undefined; - edit.dispatchEvent(new InputEvent("input")); - } - } - } catch (er) { - errors.textContent = "completely failed to fetch (" - + e.status + "): " + keyBuf.join(", "); - console.error(e, er); - } - }); - } - - if (cancelation && importDiff.size !== 0) { + if (ctx.cancelation && importDiff.size !== 0) { return; } - loadCachedPackages(edit.value, keyBuf, packages, prevRoots); + loadCachedPackages(edit.value, ctx.keyBuf, packages, prevRoots); - errors.textContent = compileCode(hbc, packages, 1); + errors.textContent = compileCode(hbc, packages); const minified_size = modifyCode(fmt, edit.value, "minify")?.length; if (minified_size) { codeSize.textContent = (MAX_CODE_SIZE - minified_size) + ""; @@ -445,7 +457,7 @@ if (window.location.hostname === 'localhost') { code: "main:=fn():int{return 42}", }]; const res = compileCode(await getHbcInstance(), posts, 1) ?? never(); - const expected = "exit code: 42\n"; + const expected = "exit code: 42"; if (expected != res) console.error(expected, res); } })() @@ -507,6 +519,30 @@ getFmtInstance().then(inst => { Object.assign(window, { filterCodeDeps }); }); +/** @param {HTMLElement} target */ +function runPost(target) { + while (!target.matches("div[class=preview]")) target = target.parentElement ?? never(); + const code = target.querySelector("pre[apply=fmt]"); + if (!(code instanceof HTMLPreElement)) never(); + const output = target.querySelector("pre[id=compiler-output]"); + if (!(output instanceof HTMLPreElement)) never(); + + getHbcInstance().then(async hbc => { + const ctx = { keyBuf: [], prevParams: new Set() }; + await fetchPackages(code.textContent ?? never(), new Set(), output, ctx); + const posts = [{ path: "this", code: "" }]; + loadCachedPackages(code.textContent ?? never(), ctx.keyBuf, posts, new Set()); + output.textContent = compileCode(hbc, posts); + output.hidden = false; + }); + + let author = encodeURIComponent(target.dataset.author ?? never()); + let name = encodeURIComponent(target.dataset.name ?? never()); + fetch(`/post/run?author=${author}&name=${name}`, { method: "POST" }) +} + +Object.assign(window, { runPost }); + updateTab(); wireUp(document.body); diff --git a/depell/src/main.rs b/depell/src/main.rs index fb89163..587cd0a 100644 --- a/depell/src/main.rs +++ b/depell/src/main.rs @@ -4,7 +4,7 @@ use { axum::{ body::Bytes, extract::{DefaultBodyLimit, Path}, - http::{header::COOKIE, request::Parts}, + http::{header::COOKIE, request::Parts, StatusCode}, response::{AppendHeaders, Html}, }, const_format::formatcp, @@ -69,6 +69,7 @@ async fn amain() { .route("/post", get(Post::page)) .route("/post-view", get(Post::get)) .route("/post", post(Post::post)) + .route("/post/run", post(Post::run)) .route("/code", post(fetch_code)) .route("/login", get(Login::page)) .route("/login-view", get(Login::get)) @@ -246,6 +247,12 @@ impl Page for Post { } } +#[derive(Deserialize)] +struct Run { + author: String, + name: String, +} + impl Post { pub fn from_row(r: &rusqlite::Row) -> rusqlite::Result { Ok(Post { @@ -254,10 +261,24 @@ impl Post { timestamp: r.get(2)?, code: r.get(3)?, imports: r.get(4)?, + runs: r.get(5)?, ..Default::default() }) } + async fn run( + session: Session, + axum::extract::Query(run): axum::extract::Query, + ) -> StatusCode { + match db::with(|qes| qes.creata_run.insert((run.name, run.author, session.name))) { + Ok(_) => StatusCode::OK, + Err(e) => { + log::error!("creating run record failed: {e}"); + StatusCode::INTERNAL_SERVER_ERROR + } + } + } + async fn post( session: Session, axum::Form(mut data): axum::Form, @@ -313,7 +334,7 @@ impl Post { impl fmt::Display for Post { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let Self { author, name, timestamp, imports, runs, dependencies, code, .. } = self; - write_html! { f
+ write_html! { f
-
code
+
+ +
code
+
+ if *timestamp == 0 { @@ -772,7 +799,19 @@ mod db { JOIN roots ON import.to_name = roots.name AND import.to_author = roots.author ) SELECT (count(*) - 1) FROM roots - ) AS imports FROM post AS outher WHERE timestamp < ?", + ) AS imports, ( + WITH RECURSIVE roots(name, author, code) AS ( + SELECT name, author, code FROM post WHERE name = outher.name AND author = outher.author + UNION + SELECT post.name, post.author, post.code FROM post + JOIN import ON post.name = import.from_name + AND post.author = import.from_author + JOIN roots ON import.to_name = roots.name + AND import.to_author = roots.author + ) SELECT count(*) FROM roots + JOIN run ON roots.name = run.code_name + AND roots.author = run.code_author + ) as runs FROM post AS outher WHERE timestamp < ?", create_post: "INSERT INTO post (name, author, timestamp, code) VALUES(?, ?, ?, ?)", fetch_deps: " WITH RECURSIVE roots(name, author, code) AS ( @@ -787,6 +826,7 @@ mod db { ", create_import: "INSERT INTO import(to_author, to_name, from_author, from_name) VALUES(?, ?, ?, ?)", + creata_run: "INSERT OR IGNORE INTO run(code_name, code_author, runner) VALUES(?, ?, ?)", } } diff --git a/lang/src/parser.rs b/lang/src/parser.rs index 2d76b47..4b4878d 100644 --- a/lang/src/parser.rs +++ b/lang/src/parser.rs @@ -1597,7 +1597,7 @@ impl ArenaChunk { fn contains(&self, arg: *mut u8) -> bool { (self.base <= arg && unsafe { self.base.add(self.size) } > arg) - || self.next().map_or(false, |s| s.contains(arg)) + || self.next().is_some_and(|s| s.contains(arg)) } pub fn size(&self) -> usize { diff --git a/vm/src/vmrun.rs b/vm/src/vmrun.rs index 65e9c66..3ee19cb 100644 --- a/vm/src/vmrun.rs +++ b/vm/src/vmrun.rs @@ -520,7 +520,7 @@ where if swapped { core::mem::swap(&mut a0, &mut a1); } - self.write_reg(tg, a0.partial_cmp(&a1).map_or(false, |v| v == expected) as u8) + self.write_reg(tg, (a0.partial_cmp(&a1) == Some(expected)) as u8) }); } }