diff --git a/default.nix b/default.nix index 50ae5c0..15121cd 100644 --- a/default.nix +++ b/default.nix @@ -5,9 +5,10 @@ # Replace llvmPackages with llvmPackages_X, where X is the latest LLVM version (at the time of writing, 16) llvmPackages.bintools rustup - wasm-bindgen-cli_0_2_100 + # wasm-bindgen-cli_0_2_100 wasm-pack binaryen + simple-http-server ]; RUSTC_VERSION = "nightly"; # https://github.com/rust-lang/rust-bindgen#environment-variables diff --git a/src/automata/npda.rs b/src/automata/npda.rs index baf0ff1..b537153 100644 --- a/src/automata/npda.rs +++ b/src/automata/npda.rs @@ -353,10 +353,12 @@ impl TransitionTable { Some(symbol) }).collect(); - transitions_map + if !transitions_map .entry((state, char, stack_symbol)) - .or_insert(Vec::new()) - .push((next_state, stack)) + .or_insert(HashSet::new()) + .insert((next_state, stack)) { + logs.emit_warning("duplicate transition", item.1); + } } } TL::Assignment(S(Dest::Function(S(name, _), _), dest_s), _) => { @@ -403,7 +405,7 @@ impl TransitionTable { let initial_state = match initial_state { Some(some) => some, None => { - if let Some(initial) = states.get("z0") { + if let Some(initial) = states.get("q0") { logs.emit_warning_locless("initial state not defined, defaulting to 'q0'"); *initial } else { diff --git a/src/loader/log.rs b/src/loader/log.rs index 8498828..d975241 100644 --- a/src/loader/log.rs +++ b/src/loader/log.rs @@ -72,6 +72,18 @@ impl<'a> Logs<'a> { entry, }) } + + pub fn entries(&self) -> &[LogEntry]{ + &self.logs + } + + pub fn into_entries(self) -> impl Iterator{ + self.logs.into_iter() + } + + pub fn src(&self) -> &str{ + &self.src + } } pub enum LogLevel { diff --git a/src/loader/parser.rs b/src/loader/parser.rs index e5ef7be..a002bcd 100644 --- a/src/loader/parser.rs +++ b/src/loader/parser.rs @@ -44,7 +44,7 @@ impl<'a> Parser<'a> { } fn expect_token(&mut self, expected: Token<'a>) -> (bool, Span) { - if let Some(Spanned(token, span)) = self.next_token() { + if let Some(Spanned(token, span)) = self.peek_token() { if token != expected { self.logs.emit_error( format!("unexpected token {:#}, expected {:}", token, expected), @@ -52,6 +52,7 @@ impl<'a> Parser<'a> { ); (false, span) } else { + self.next_token(); (true, span) } } else { @@ -212,27 +213,26 @@ impl<'a> Parser<'a> { pub fn parse_elements(mut self) -> (Vec>>, Logs<'a>) { let mut result = Vec::new(); - loop { - let Some(next) = self.next_token() else { break }; + while let Some(next) = self.next_token() { match (next, self.peek_token()) { (Spanned(Token::Ident(ident), start), Some(Spanned(Token::LPar, _))) => { let tuple = self.parse_tupple(); let span = start.join(tuple.1); let dest = Spanned(Dest::Function(Spanned(ident, start), tuple), span); - self.expect_token(Token::Eq); + if !self.expect_token(Token::Eq).0{continue;} let item = self.parse_item(); let span = start.join(item.1); result.push(Spanned(TopLevel::Assignment(dest, item), span)); } ( - Spanned(Token::Ident(_), _), - Some(Spanned(Token::LSmallArrow | Token::Ident(_), _)), + Spanned(Token::Ident(_), start), + Some(Spanned(Token::LSmallArrow, end)), ) => { - todo!() + self.logs.emit_error("Production rules are not yet supported", start.join(end)); } (Spanned(Token::Ident(ident), start), _) => { let dest = Spanned(Dest::Ident(ident), start); - self.expect_token(Token::Eq); + if !self.expect_token(Token::Eq).0{continue;} let item = self.parse_item(); let span = start.join(item.1); result.push(Spanned(TopLevel::Assignment(dest, item), span)); diff --git a/web/Cargo.toml b/web/Cargo.toml index a2c1580..e566b50 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -3,6 +3,9 @@ name = "automata-web" version = "0.1.0" edition = "2024" +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] automata = {path=".."} console_error_panic_hook = "0.1.7" diff --git a/web/build.sh b/web/build.sh index 14256f9..37727a3 100755 --- a/web/build.sh +++ b/web/build.sh @@ -1,15 +1,2 @@ - -CRATE_NAME="automata-web" -TARGET_NAME="automata-web" -OUT_FILE_NAME="./root/automata.wasm" -mkdir root -TARGET="../target" - - -cargo build --package automata-web --target wasm32-unknown-unknown --release -TARGET_NAME="${CRATE_NAME}.wasm" -WASM_PATH="${TARGET}/wasm32-unknown-unknown/release/$TARGET_NAME" - -wasm-bindgen ${WASM_PATH} --out-dir root --out-name ${OUT_FILE_NAME} --no-modules --no-typescript -wasm-opt ${OUT_FILE_NAME} -O2 --fast-math -g -o ${OUT_FILE_NAME} \ No newline at end of file +wasm-pack build --release --target web --no-typescript --out-dir root/automata --no-pack \ No newline at end of file diff --git a/web/root/editor.css b/web/root/editor.css new file mode 100644 index 0000000..0c80295 --- /dev/null +++ b/web/root/editor.css @@ -0,0 +1,372 @@ +html, +body { + height: 100%; + width: 100%; + margin: 0; + font-family: system-ui, sans-serif; + background: #909090; +} + +.wrap { + height: 100vh; + width: 100vw; + display: grid; + grid-template-rows: var(--topH, 50vh) 8px 1fr; + /* top pane, splitter, editor */ + overflow: hidden; +} + +/* Top pane: terminal area */ +.topPane { + display: grid; + grid-template-columns: var(--termW, 50vw) 8px 1fr; + /* terminal, splitter, filler */ + min-height: 80px; + overflow: hidden; +} + +/* Bottom pane: editor */ +.bottomPane { + min-height: 120px; + overflow: hidden; +} + +/* Make editor fill its pane */ +#editor { + height: 100%; + width: 100%; +} + + +.terminal { + background: #0b0f14; + color: #c9d1d9; + padding: 1em; + margin: 0px; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; + font-size: 12.5px; + line-height: 1.35; + white-space: pre-wrap; + word-break: break-word; +} + +/* ANSI text styles */ +.ansi-bold { font-weight: 700; } +.ansi-dim { opacity: 0.7; } + +/* Foreground colors (standard + bright) */ +.ansi-fg-30 { color: #0b0f14; } /* black */ +.ansi-fg-31 { color: #ff7b72; } /* red */ +.ansi-fg-32 { color: #7ee787; } /* green */ +.ansi-fg-33 { color: #f2cc60; } /* yellow */ +.ansi-fg-34 { color: #79c0ff; } /* blue */ +.ansi-fg-35 { color: #d2a8ff; } /* magenta */ +.ansi-fg-36 { color: #a5d6ff; } /* cyan */ +.ansi-fg-37 { color: #c9d1d9; } /* white */ + +.ansi-fg-90 { color: #6e7681; } /* bright black / gray */ +.ansi-fg-91 { color: #ffa198; } +.ansi-fg-92 { color: #a6f3a6; } +.ansi-fg-93 { color: #ffe082; } +.ansi-fg-94 { color: #a5d6ff; } +.ansi-fg-95 { color: #e3b8ff; } +.ansi-fg-96 { color: #c7f0ff; } +.ansi-fg-97 { color: #ffffff; } + +/* Background colors (optional) */ +.ansi-bg-40 { background: #0b0f14; } +.ansi-bg-41 { background: rgba(255, 123, 114, 0.22); } +.ansi-bg-42 { background: rgba(126, 231, 135, 0.18); } +.ansi-bg-43 { background: rgba(242, 204, 96, 0.18); } +.ansi-bg-44 { background: rgba(121, 192, 255, 0.18); } +.ansi-bg-45 { background: rgba(210, 168, 255, 0.18); } +.ansi-bg-46 { background: rgba(165, 214, 255, 0.18); } +.ansi-bg-47 { background: rgba(201, 209, 217, 0.10); } + +html, +body { + height: 100%; + margin: 0; + overflow: hidden; +} + +/* App layout */ +.app { + height: 100vh; + width: 100vw; + display: grid; + grid-template-rows: var(--canvasH, 35vh) 8px 1fr; + overflow: hidden; +} + +/* ---------- Canvas ---------- */ +.canvasPane { + position: relative; + overflow: hidden; + background: #111; +} + +#canvas { + width: 100%; + height: 100%; + display: block; +} + +/* ---------- Bottom area (terminal + editor) ---------- */ +/* Bottom area (terminal + editor) */ +.bottomPane { + height: 100%; + display: grid; + grid-template-columns: 1fr 8px var(--termW, 40vw); + overflow: hidden; + /* IMPORTANT */ +} + +/* Terminal side */ +.terminalPane { + height: 100%; + overflow: hidden; +} + +.terminal { + height: 100%; + width: 100%; + overflow-y: auto; + /* terminal scrolls */ + overflow-x: auto; +} + +/* Editor side */ +.editorPane { + height: 100%; + overflow: hidden; + /* let CodeMirror scroll */ +} + +/* CodeMirror mount point */ +#editor { + height: 100%; + width: 100%; +} + +/* VERY IMPORTANT: force CodeMirror to respect container height */ +.cm-editor { + height: 100%; +} + +/* CodeMirror’s internal scroller (this is where the scrollbar lives) */ +.cm-scroller { + overflow-y: auto !important; +} + +/* ---------- Splitters ---------- */ +.hSplit { + cursor: row-resize; + background: rgba(255, 255, 255, 0.06); +} + +.vSplit { + cursor: col-resize; + background: rgba(255, 255, 255, 0.06); +} + +.hSplit:hover, +.vSplit:hover { + background: rgba(121, 192, 255, 0.25); +} + + + + + + + + + + + + + +.diag { + margin: 0; + padding-left: 18px; +} + +.diag li { + margin: 6px 0; +} + +/* --- Syntax colors via CSS classes applied by decorations --- */ +.tok-comment { + color: #1a7b24; +} + +.tok-keyword { + color: #b99400; + font-weight: 600; +} + +.tok-error { + color: #ff0505; + font-weight: 1000; +} + +.tok-ident { + color: #90d4e0; +} + +.tok-brace { + color: #d73a49; + font-weight: 600; +} + +.tok-punc { + color: #ffffff; +} + +.tok-string { + color: #03621e; +} + +/* Rainbow bracket depth classes */ +.rb-0 { + color: #a35; + font-weight: 700; +} + +.rb-1 { + color: #ed0; + font-weight: 700; +} + +.rb-2 { + color: #9d5; + font-weight: 700; +} + +.rb-3 { + color: #2cb; + font-weight: 700; +} + +.rb-4 { + color: #36b; + font-weight: 700; +} + +.rb-5 { + color: #639; + font-weight: 700; +} + +/* Severity underline styles */ +.cm-diag-error { + text-decoration: underline wavy #d73a49; + /* red */ + text-underline-offset: 2px; +} + +.cm-diag-warning { + text-decoration: underline wavy #ffd33d; + /* yellow */ + text-underline-offset: 2px; +} + +.cm-diag-info { + text-decoration: underline wavy #79c0ff; + /* cyan-ish */ + text-underline-offset: 2px; +} + +/* Tooltip title coloring by severity */ +.tipTitle.error { + color: #d73a49; +} + +.tipTitle.warning { + color: #ffd33d; +} + +.tipTitle.info { + color: #79c0ff; +} + +/* Optional: diagnostics panel coloring */ +.diag li.error { + color: #d73a49; +} + +.diag li.warning { + color: #b08800; +} + +.diag li.info { + color: #0366d6; +} + +/* Tooltip styling */ +.cm-tooltip.cm-tooltip-hover { + border: 1px solid #ddd; + background: black; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12); + border-radius: 10px; + padding: 8px 10px; + max-width: 420px; + font-size: 13px; + line-height: 1.35; +} + +.tipTitle { + font-weight: 700; + margin-bottom: 4px; +} + +.tipBody { + white-space: pre-wrap; +} + + + +/* Loading screen */ + +.centered { + margin-right: auto; + margin-left: auto; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: #f0f0f0; + font-size: 24px; + font-family: Ubuntu-Light, Helvetica, sans-serif; + text-align: center; +} + +.lds-dual-ring { + display: inline-block; + width: 24px; + height: 24px; +} + +.lds-dual-ring:after { + content: " "; + display: block; + width: 24px; + height: 24px; + margin: 0px; + border-radius: 50%; + border: 3px solid #fff; + border-color: #fff transparent #fff transparent; + animation: lds-dual-ring 1.2s linear infinite; +} + +@keyframes lds-dual-ring { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/web/root/editor.js b/web/root/editor.js new file mode 100644 index 0000000..c4a162d --- /dev/null +++ b/web/root/editor.js @@ -0,0 +1,400 @@ +import { EditorView, keymap, hoverTooltip, Decoration, ViewPlugin } from "https://esm.sh/@codemirror/view"; +import { EditorState, StateField } from "https://esm.sh/@codemirror/state"; +import { defaultKeymap, history, historyKeymap } from "https://esm.sh/@codemirror/commands"; +import { lineNumbers, highlightActiveLineGutter } from "https://esm.sh/@codemirror/view"; +import { bracketMatching, indentOnInput } from "https://esm.sh/@codemirror/language"; +import { closeBrackets } from "https://esm.sh/@codemirror/autocomplete"; +import { oneDark } from "https://esm.sh/@codemirror/theme-one-dark"; + +import wasm from "./wasm.js" + +function tokenize(text) { + try{ + return wasm.lex(text); + }catch(e){ + console.log(e) + return [] + } +} + +function compile(text) { + try{ + return wasm.compile(text); + }catch(e){ + console.log(e) + return [] + } +} +/* ===================================================================== */ + +// Map token types -> CSS classes +const tokenClass = (t) => + ({ + comment: "tok-comment", + keyword: "tok-keyword", + error: "tok-error", + ident: "tok-ident", + punc: "tok-punc", + string: "tok-string", + lpar: "rb-", + lbrace: "rb-", + lbracket: "rb-", + + rpar: "rb-", + rbrace: "rb-", + rbracket: "rb-", + }[t] || "tok-ident"); + +// ===================== Diagnostics helpers ===================== +function severityClass(sev) { + const s = (sev || "error").toLowerCase(); + if (s === "warning") return "cm-diag-warning"; + if (s === "info") return "cm-diag-info"; + return "cm-diag-error"; +} +function sevRank(sev) { + if (sev === "error") return 3; + if (sev === "warning") return 2; + return 1; +} +function sevLabel(sev) { + if (sev === "warning") return "warning"; + if (sev === "info") return "info"; + return "error"; +} +function sevAnsiColorClass(sev) { + if (sev === "warning") return "ansi-yellow"; + if (sev === "info") return "ansi-cyan"; + return "ansi-red"; +} + + +function buildAnalysis(text, doc) { + const tokens = tokenize(text); + const {log, log_formatted} = compile(text); + + // Build ONE Decoration set: syntax + diagnostics + const marks = []; + const docLen = doc.length; + + for (const tok of tokens) { + const start = Math.max(0, Math.min(docLen, tok.start)); + const end = Math.max(start, Math.min(docLen, tok.end)); + var tc = tokenClass(tok.kind); + if (tc === "rb-"){ + tc += tok.scope_level.toString(); + } + if (end > start) { + marks.push(Decoration.mark({ class: tc }).range(start, end)); + } + } + + for (const d of log) { + if (d.start === undefined || d.end === undefined)continue; + const start = Math.max(0, Math.min(docLen, d.start)); + const endRaw = d.end == null ? d.start : d.end; + const end = Math.max(start, Math.min(docLen, endRaw)); + const cls = severityClass(d.level); + if (end > start) { + marks.push(Decoration.mark({ class: cls }).range(start, end)); + } else { + const end = Math.min(docLen, start + 1); + if (end > start) marks.push(Decoration.mark({ class: cls }).range(start, end)); + } + } + + const deco = Decoration.set(marks, true); + return { tokens, log, log_formatted, deco }; +} + +const analysisField = StateField.define({ + create(state) { + const text = state.doc.toString(); + return buildAnalysis(text, state.doc); + }, + update(value, tr) { + if (!tr.docChanged) return value; + const text = tr.state.doc.toString(); + return buildAnalysis(text, tr.state.doc); + }, + provide: (f) => EditorView.decorations.from(f, (v) => v.deco), +}); + +// ===================== Hover tooltip (uses cached diags) ===================== +const diagHover = hoverTooltip((view, pos) => { + const { log } = view.state.field(analysisField); + const hits = log.filter((d) => pos >= d.start && pos <= d.end); + if (hits.length === 0) return null; + + const top = hits.reduce((a, b) => (sevRank(b.level) > sevRank(a.level) ? b : a), hits[0]); + + return { + pos, + end: pos, + above: true, + create() { + const dom = document.createElement("div"); + dom.className = "cm-tooltip cm-tooltip-hover"; + + const title = document.createElement("div"); + title.className = `tipTitle ${top.level}`; + title.textContent = + hits.length === 1 ? top.level.toUpperCase() : `${top.level.toUpperCase()} (${hits.length})`; + + const body = document.createElement("div"); + body.className = "tipBody"; + body.textContent = hits + .slice() + .sort((a, b) => sevRank(b.level) - sevRank(a.level)) + .map((h) => `[${h.level.toUpperCase()}] ${h.message}`) + .join("\n"); + + dom.appendChild(title); + dom.appendChild(body); + return { dom }; + }, + }; +}); + + +function escapeHtml(s) { + return String(s) + .replace(/&/g, "&") + .replace(//g, ">"); +} + + +function ansiToHtml(input) { + const ESC_RE = /\x1b\[([0-9;]*)m/g; + + let out = ""; + let lastIndex = 0; + + // current style state + let fg = null; // e.g. 31, 92 + let bg = null; // e.g. 41 + let bold = false; + let dim = false; + + function openSpanIfNeeded(text) { + if (text.length === 0) return ""; + const classes = []; + if (bold) classes.push("ansi-bold"); + if (dim) classes.push("ansi-dim"); + if (fg != null) classes.push(`ansi-fg-${fg}`); + if (bg != null) classes.push(`ansi-bg-${bg}`); + if (classes.length === 0) return escapeHtml(text); + return `${escapeHtml(text)}`; + } + + function applyCodes(codes) { + if (codes.length === 0) codes = [0]; + for (const c of codes) { + const code = Number(c); + if (Number.isNaN(code)) continue; + + if (code === 0) { + fg = null; bg = null; bold = false; dim = false; + } else if (code === 1) { + bold = true; + } else if (code === 2) { + dim = true; + } else if (code === 22) { + bold = false; dim = false; + } else if (code === 39) { + fg = null; + } else if (code === 49) { + bg = null; + } else if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) { + fg = code; + } else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) { + bg = code; + } + } + } + + let m; + while ((m = ESC_RE.exec(input)) !== null) { + const chunk = input.slice(lastIndex, m.index); + out += openSpanIfNeeded(chunk); + + const codes = m[1] ? m[1].split(";") : []; + applyCodes(codes); + + lastIndex = ESC_RE.lastIndex; + } + + out += openSpanIfNeeded(input.slice(lastIndex)); + return out; +} + +function formatTerminal(view) { + const term = document.getElementById("terminal"); + if (!term) return; + + const { log, log_formatted } = view.state.field(analysisField); + + let s = ""; + s += `\x1b[90m[compile]\x1b[0m ${log.length} diagnostics\n`; + + term.innerHTML = ansiToHtml(s+log_formatted); +} + +const terminalPlugin = ViewPlugin.fromClass( + class { + constructor(view) { + this.view = view; + formatTerminal(view); + } + update(update) { + if (update.docChanged) formatTerminal(update.view); + } + } +); + +// ===================== Build editor ===================== +const initialText = `machine=NPDA +Q = {q0, q1} // states +E = {a, b} // alphabet +T = {z0, A, B} // stack +q0 = q0 +z0 = z0 + +// construct all possible permutations of A's and B's +d(q0, epsilon, z0) = { (q0, [A z0]), (q0, [B z0]) } +d(q0, epsilon, A) = { (q0, [A A]), (q0, [B A]) } + +d(q0, epsilon, B) = { (q0, [A B]), (q0, [B B]) } + +// transition to q1 +d(q0, epsilon, z0) = { (q1, z0) } +d(q0, epsilon, A) = { (q1, A) } +d(q0, epsilon, B) = { (q1, B) } + +// consume stack until empty +d(q1, a, A) = { (q1, epsilon) } +d(q1, b, B) = { (q1, epsilon) } +`; + +const state = EditorState.create({ + doc: initialText, + extensions: [ + lineNumbers(), + highlightActiveLineGutter(), + history(), + indentOnInput(), + bracketMatching(), + closeBrackets(), + keymap.of([...defaultKeymap, ...historyKeymap]), + oneDark, + + analysisField, + diagHover, + terminalPlugin, + + EditorView.lineWrapping, + ], +}); + +window.editor = new EditorView({ + state, + parent: document.getElementById("editor"), +}); + + +function setDefaultLayoutWeights() { + const vh = window.innerHeight; + const vw = window.innerWidth; + + // Canvas: 30% of screen height + const canvasH = Math.round(vh * 0.60); + + // Terminal: 35% of width + const termW = Math.round(vw * 0.30); + + const app = document.getElementById("app"); + app.style.setProperty("--canvasH", `${canvasH}px`); + app.style.setProperty("--termW", `${termW}px`); +} + +setDefaultLayoutWeights(); + + +(function enableLayoutSplitters() { + const app = document.getElementById("app"); + const hSplit = document.getElementById("hSplit"); + const vSplit = document.getElementById("vSplit"); + const canvas = document.getElementById("canvas"); + const canvasPane = document.getElementById("canvasPane"); + + let draggingH = false; + let draggingV = false; + + // --- Canvas height splitter --- + hSplit.addEventListener("mousedown", (e) => { + draggingH = true; + document.body.style.cursor = "row-resize"; + e.preventDefault(); + }); + + // --- Terminal/editor width splitter --- + vSplit.addEventListener("mousedown", (e) => { + draggingV = true; + document.body.style.cursor = "col-resize"; + e.preventDefault(); + }); + + function resizeCanvasToPane() { + // Keep canvas resolution in sync with CSS size (crisp rendering) + const rect = canvasPane.getBoundingClientRect(); + const dpr = window.devicePixelRatio || 1; + const w = Math.max(1, Math.floor(rect.width * dpr)); + const h = Math.max(1, Math.floor(rect.height * dpr)); + if (canvas.width !== w) canvas.width = w; + if (canvas.height !== h) canvas.height = h; + + // Optional: demo draw + const ctx = canvas.getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#1f6feb"; + ctx.fillRect(20 * dpr, 20 * dpr, 140 * dpr, 60 * dpr); + ctx.fillStyle = "#c9d1d9"; + ctx.font = `${14 * dpr}px ui-monospace, monospace`; + ctx.fillText("Canvas area", 30 * dpr, 55 * dpr); + } + + window.addEventListener("mousemove", (e) => { + const rect = app.getBoundingClientRect(); + + if (draggingH) { + const y = e.clientY - rect.top; + const minCanvas = 80; + const minBottom = 180; + const maxCanvas = rect.height - 8 - minBottom; + const canvasH = Math.max(minCanvas, Math.min(maxCanvas, y)); + app.style.setProperty("--canvasH", `${canvasH}px`); + resizeCanvasToPane(); + } + + if (draggingV) { + const bottomPane = document.getElementById("bottomPane"); + const r = bottomPane.getBoundingClientRect(); + const x = e.clientX - r.left; + const minTerm = 220; + const maxTerm = r.width - 8 - 220; + const termW = Math.max(minTerm, Math.min(maxTerm, r.width-x)); + app.style.setProperty("--termW", `${termW}px`); + } + }); + + window.addEventListener("mouseup", () => { + draggingH = false; + draggingV = false; + document.body.style.cursor = ""; + }); + + // Keep canvas crisp on window resize too + window.addEventListener("resize", resizeCanvasToPane); + resizeCanvasToPane(); +})(); \ No newline at end of file diff --git a/web/root/index.html b/web/root/index.html new file mode 100644 index 0000000..98613ef --- /dev/null +++ b/web/root/index.html @@ -0,0 +1,45 @@ + + + + + + + Automata + + + + +
+

+ Loading… +

+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/web/root/index.js b/web/root/index.js new file mode 100644 index 0000000..208123c --- /dev/null +++ b/web/root/index.js @@ -0,0 +1,3 @@ +import wasm from "./wasm.js" +import "./editor.js" + diff --git a/web/root/package.json b/web/root/package.json new file mode 100644 index 0000000..1bdc95e --- /dev/null +++ b/web/root/package.json @@ -0,0 +1,15 @@ +{ + "name": "automata-web", + "type": "module", + "version": "0.1.0", + "files": [ + "automata_bg.wasm", + "automata.js", + "automata_bg.js" + ], + "main": "automata.js", + "sideEffects": [ + "./automata.js", + "./snippets/*" + ] +} \ No newline at end of file diff --git a/web/root/wasm.js b/web/root/wasm.js new file mode 100644 index 0000000..d45e127 --- /dev/null +++ b/web/root/wasm.js @@ -0,0 +1,26 @@ +console.debug("Loading wasm…"); +import init, * as wasm from "./automata/automata_web.js"; +try{ + console.debug("Wasm loaded. Starting app…"); + window.wasm = wasm; + await init(); + console.debug("App started."); + document.getElementById("center_text").innerHTML = ''; + document.getElementById("app").style.display = ''; + wasm.init(); +}catch(e){ + console.error("Failed to start: " + error); + document.getElementById("the_canvas_id").remove(); + document.getElementById("center_text").innerHTML = ` +

+ An error occurred during loading: +

+

+ ${error} +

+

+ Make sure you use a modern browser with WebGL and WASM enabled. +

`; +} + +export default wasm \ No newline at end of file diff --git a/web/run.sh b/web/run.sh new file mode 100755 index 0000000..08eb2f1 --- /dev/null +++ b/web/run.sh @@ -0,0 +1,2 @@ +cd root +simple-http-server \ No newline at end of file diff --git a/web/src/lib.rs b/web/src/lib.rs new file mode 100644 index 0000000..34094b8 --- /dev/null +++ b/web/src/lib.rs @@ -0,0 +1,219 @@ +use automata::{ + automata::npda::{self, NPDA}, + loader::{self, Span, Spanned, lexer::Lexer}, +}; + +use wasm_bindgen::prelude::wasm_bindgen; + +#[wasm_bindgen] +pub fn test() { + panic!() +} + +#[wasm_bindgen(start)] +pub fn main() {} + +#[wasm_bindgen] +pub fn init() { + console_error_panic_hook::set_once(); +} + +#[wasm_bindgen] +pub fn silly(machine: &str, input: &str) { + let table = match npda::TransitionTable::load_table(machine) { + Ok((ok, logs)) => { + for log in logs.displayable() { + println!("{log}") + } + ok + } + Err(logs) => { + for log in logs.displayable() { + println!("{log}") + } + return; + } + }; + + println!("running on: '{input}'"); + let mut simulator = npda::Simulator::begin(input, table); + loop { + match simulator.step() { + npda::SimulatorResult::Pending => {} + npda::SimulatorResult::Reject => { + println!("REJECTED"); + break; + } + npda::SimulatorResult::Accept(npda) => { + println!("ACCEPT: {npda:?}"); + break; + } + } + } +} + +#[wasm_bindgen] +#[derive(Clone, Copy)] +pub enum Kind { + Ident = "ident", + Keyword = "keyword", + Error = "error", + Comment = "comment", + Punc = "punc", + + LPar = "lpar", + LBrace = "lbrace", + LBracket = "lbracket", + + RPar = "rpar", + RBrace = "rbrace", + RBracket = "rbracket", +} + +#[wasm_bindgen] +#[derive(Clone, Copy)] +pub struct Tok { + pub start: usize, + pub end: usize, + pub scope_level: usize, + pub kind: Kind, +} + +#[wasm_bindgen] +pub fn lex(input: &str) -> Vec { + let mut scope_level = 0; + let mut index_utf16 = 0; + let mut index_utf8 = 0; + Lexer::new(input) + .map(|Spanned(tok, Span(start_utf8, end_utf8))| { + let since_last = &input[index_utf8..start_utf8]; + let since_start = &input[start_utf8..end_utf8]; + + index_utf8 = end_utf8; + let start = index_utf16 + since_last.chars().map(char::len_utf16).sum::(); + let end = start + since_start.chars().map(char::len_utf16).sum::(); + index_utf16 = end; + + let Ok(tok) = tok else { + return Tok { + start, + end, + kind: Kind::Error, + scope_level, + }; + }; + use automata::loader::lexer::Token; + let kind = match tok { + Token::LPar => Kind::LPar, + Token::RPar => Kind::RPar, + Token::LBrace => Kind::LBrace, + Token::RBrace => Kind::RBrace, + Token::LBracket => Kind::LBracket, + Token::RBracket => Kind::RBracket, + Token::Tilde => Kind::Keyword, + Token::Eq => Kind::Punc, + Token::Comma => Kind::Punc, + Token::Or => Kind::Punc, + Token::Plus => Kind::Punc, + Token::Star => Kind::Punc, + Token::And => Kind::Punc, + Token::LSmallArrow => Kind::Punc, + Token::LBigArrow => Kind::Punc, + Token::Comment(_) => Kind::Comment, + Token::Ident(_) + if input[..start_utf8] + .split("\n") + .last() + .unwrap_or_default() + .trim() + .is_empty() => + { + Kind::Keyword + } + Token::Ident( + loader::EPSILON_LOWER + | "epsilon" + | loader::DELTA_LOWER + | "delta" + | loader::GAMMA_UPPER + | "gamma" + | loader::GAMMA_LOWER + | loader::SIGMA_UPPER + | "sigma", + ) => Kind::Keyword, + Token::Ident(_) => Kind::Ident, + }; + + let scope_level = match kind { + Kind::LPar | Kind::LBrace | Kind::LBracket => { + scope_level = scope_level.saturating_add(1); + scope_level.saturating_sub(1) + } + Kind::RPar | Kind::RBrace | Kind::RBracket => { + scope_level = scope_level.saturating_sub(1); + scope_level + } + _ => scope_level, + }; + Tok { + start, + end, + kind, + scope_level, + } + }) + .collect() +} + +#[wasm_bindgen] +#[derive(Clone, Copy)] +pub enum LogLevel { + Info = "info", + Warning = "warning", + Error = "error", +} + +#[wasm_bindgen(getter_with_clone)] +#[derive(Clone)] +pub struct CompileLog { + pub level: LogLevel, + pub message: String, + pub start: Option, + pub end: Option, +} + +#[wasm_bindgen(getter_with_clone)] +pub struct CompileResult{ + pub log: Vec, + pub log_formatted: String, +} + +#[wasm_bindgen] +pub fn compile(input: &str) -> CompileResult { + let log = match npda::TransitionTable::load_table(input) { + Ok((_, logs)) => logs, + Err(logs) => logs, + }; + + use std::fmt::Write; + let log_formatted = log.displayable().fold(String::new(), |mut s, e|{write!(&mut s, "{e}").unwrap(); s}); + + let log = log.into_entries() + .map(|e| CompileLog { + level: match e.level { + loader::log::LogLevel::Info => LogLevel::Info, + loader::log::LogLevel::Warning => LogLevel::Warning, + loader::log::LogLevel::Error => LogLevel::Error, + }, + message: e.message, + start: e + .span + .map(|span| input[..span.0].chars().map(char::len_utf16).count()), + end: e + .span + .map(|span| input[..span.1].chars().map(char::len_utf16).count()), + }) + .collect(); + + CompileResult { log, log_formatted } +} diff --git a/web/src/main.rs b/web/src/main.rs deleted file mode 100644 index 1b849a3..0000000 --- a/web/src/main.rs +++ /dev/null @@ -1,51 +0,0 @@ -use automata::automata::npda; - -use web_sys::window; - -fn main() { - console_error_panic_hook::set_once(); - - let document = window() - .and_then(|win| win.document()) - .expect("Could not access the document"); - let body = document.body().expect("Could not access document.body"); - let text_node = document.create_text_node("Hello, world from Vanilla Rust!"); - body.append_child(text_node.as_ref()) - .expect("Failed to append text"); -} - -// pub fn main() { -// let input = include_str!("../../example.npda"); - -// let table = match npda::TransitionTable::load_table(input) { -// Ok((ok, logs)) => { -// for log in logs.displayable() { -// println!("{log}") -// } -// ok -// } -// Err(logs) => { -// for log in logs.displayable() { -// println!("{log}") -// } -// return; -// } -// }; - -// let input = "aababaab"; -// println!("running on: '{input}'"); -// let mut simulator = npda::Simulator::begin(input, table); -// loop { -// match simulator.step(){ -// npda::SimulatorResult::Pending => {}, -// npda::SimulatorResult::Reject => { -// println!("REJECTED"); -// break; -// }, -// npda::SimulatorResult::Accept(npda) => { -// println!("ACCEPT: {npda:?}"); -// break; -// }, -// } -// } -// }