From 8c8bb103b26bd9a08e3e7f61db7e116d6d16209e Mon Sep 17 00:00:00 2001
From: Parker TenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com>
Date: Sun, 11 Jan 2026 00:30:06 -0500
Subject: [PATCH] moved frontend to event driven bus, added more simulation
stuff
---
web/root/index.html | 32 ++--
web/root/src/automata.ts | 220 +-----------------------
web/root/src/bus.ts | 87 ++++++++++
web/root/src/controls.ts | 100 ++++++-----
web/root/src/editor.ts | 32 ++--
web/root/src/examples.ts | 10 +-
web/root/src/main.ts | 10 +-
web/root/src/simulation.ts | 325 +++++++++++++++++++++++++++++++++++
web/root/src/splitters.ts | 2 +-
web/root/src/terminal.ts | 45 ++---
web/root/src/theme.ts | 10 +-
web/root/src/visualizer.ts | 305 ++++++++++++++++++--------------
web/root/style/controls.scss | 66 ++++++-
web/root/style/editor.scss | 1 -
web/root/style/style.scss | 2 +
web/root/style/terminal.scss | 1 -
web_lib/src/lib.rs | 12 +-
17 files changed, 767 insertions(+), 493 deletions(-)
create mode 100644 web/root/src/bus.ts
create mode 100644 web/root/src/simulation.ts
diff --git a/web/root/index.html b/web/root/index.html
index f25eb13..b6602da 100644
--- a/web/root/index.html
+++ b/web/root/index.html
@@ -30,7 +30,7 @@
-
+
+
+ Status: N/A
+
+
+
+
+
@@ -53,7 +61,7 @@
-
+
Physics: ON
@@ -63,21 +71,25 @@
-
- ⟲ Reset
+
+ ⟲ Reload
-
- ▶ Play
+
+ ✖ Clear
-
- Step
+
+
+ ⏭ Step
+
+
+ ▶ Play
diff --git a/web/root/src/automata.ts b/web/root/src/automata.ts
index 1fb8349..f1b8a12 100644
--- a/web/root/src/automata.ts
+++ b/web/root/src/automata.ts
@@ -1,8 +1,6 @@
-import { network, updateVisualization } from "./visualizer.ts";
-
export type Machine = Fa | Pda | Tm;
-export function machine_from_json(json: string): Machine {
+export function parse_machine_from_json(json: string): Machine {
const machine: Machine = JSON.parse(json);
machine.states = new Map(Object.entries(machine.states));
@@ -202,219 +200,3 @@ export type Tm = {
edges: Map
;
};
-
-export type SimStepResult = "pending" | "accept" | "reject";
-
-export class FaState {
- readonly state: State;
-
- readonly position: number;
- readonly input: string;
- readonly accepted: boolean = false;
- private repr!: string;
-
- constructor(state: State, position: number, input: string){
- this.state=state;
- this.position=position;
- this.input = input;
- }
-
- toString(): string{
- if(!this.repr) this.repr = this.state + " " + this.position;
- return this.repr
- }
-};
-
-export class FaSim {
-
- current_states: Map = new Map();
- accepted: FaState[] = []
-
- step(): SimStepResult {
- return "pending";
- }
-}
-
-export class PdaState {
- readonly state: State;
- readonly stack: Symbol[];
-
- readonly position: number;
- readonly input: string;
- readonly accepted: boolean = false
- private repr!: string;
-
- constructor(state: State, stack: Symbol[], position: number, input: string){
- this.state=state;
- this.stack=stack;
- this.position=position;
- this.input = input;
- }
-
- toString(): string{
- if(!this.repr) this.repr = this.state + " [" + this.stack + "]" + " " + this.position;
- return this.repr
- }
-};
-
-export class PdaSim {
- machine: Pda;
- paths: PdaState[];
- input: string;
-
- current_states: Map = new Map();
- accepted: PdaState[] = []
-
- constructor(machine: Pda, input: string) {
- this.machine = machine;
- this.paths = [new PdaState(machine.initial_state, [machine.initial_stack], 0, input)];
- this.current_states.set(machine.initial_state, [this.paths[0]])
- this.input = input;
- }
-
- step(): SimStepResult {
- if (this.paths.length == 0) return "reject";
- if (this.accepted.length != 0) return "accept";
-
- const paths: PdaState[] = [];
- this.current_states.clear();
-
- const push = (state: PdaState) => {
- paths.push(state);
- if (!this.current_states.has(state.state)) this.current_states.set(state.state, []);
- this.current_states.get(state.state)?.push(state);
-
- if (
- state.position == this.input.length && this.machine.final_states &&
- this.machine.final_states.has(state.state)
- ||
- state.position == this.input.length && !this.machine.final_states &&
- state.stack.length == 1 && state.stack[0] == this.machine.initial_stack
- ) {
-
- // @ts-expect-error sillllyyyy
- state.accepted = true
- this.accepted.push(state);
- }
- };
-
- for (const path of this.paths) {
-
- const stack = path.stack.pop()!;
- const letter_map = this.machine.transitions_components.get(path.state)
- ?.get(stack);
- if (!letter_map) continue;
-
- for (const to of letter_map.get(null) ?? []) {
- push(new PdaState(to.state, path.stack.concat(to.stack), path.position, this.input));
- }
-
- if (path.position >= this.input.length) continue;
-
- const char = this.input.charAt(path.position);
-
- for (const to of letter_map.get(char) ?? []) {
- push(new PdaState(to.state, path.stack.concat(to.stack), path.position+1, this.input));
- }
- }
- this.paths = paths;
-
-
- if (this.paths.length == 0) return "reject";
- if (this.accepted.length != 0) return "accept";
- return "pending"
- }
-}
-
-export class TmState{
- readonly state: State;
- readonly tape: Symbol[];
-
- readonly position: number;
- readonly input: string;
- readonly accepted: boolean = false
- private repr!: string;
-
- constructor(state: State, tape: Symbol[], position: number, input: string){
- this.state=state;
- this.tape = tape;
- this.position=position;
- this.input = input;
- }
-
- toString(): string{
- if(!this.repr) this.repr = this.state + " " + this.position;
- return this.repr
- }
-
-}
-
-export class TmSim {
- current_states: Map = new Map();
- accepted: TmState[] = []
-
- step(): SimStepResult {
- return "pending"
- }
-}
-
-export type Sim = FaSim | PdaSim | TmSim | null
-export let sim: Sim = null;
-
-export let automaton: Machine = {
- type: "fa",
- alphabet: new Map(),
- final_states: new Map(),
- initial_state: "",
- states: new Map(),
- transitions: new Map(),
- transitions_components: new Map(),
- edges: new Map(),
-};
-
-export function clearSimulation(){
- setSimulation(null);
-}
-
-export function setSimulation(sim_: Sim){
- sim = sim_;
- network.redraw()
-}
-
-export function setAutomaton(auto: Machine) {
- automaton = auto;
- sim = null;
- updateVisualization()
-}
-
-export function clearAutomaton() {
- setAutomaton({
- type: "fa",
- alphabet: new Map(),
- final_states: new Map(),
- initial_state: "",
- states: new Map(),
- transitions: new Map(),
- transitions_components: new Map(),
- edges: new Map(),
- });
-}
-
-export function stepSimulation(): void {
- if (sim) {
- console.log(sim.step());
- }
- network.redraw()
-}
-
-export function resetSimulation(): void {
- switch (automaton.type) {
- case "fa":
- break;
- case "pda":
- setSimulation(new PdaSim(automaton as Pda, "aabbaabbaa"));
- break;
- case "tm":
- break;
- }
-}
diff --git a/web/root/src/bus.ts b/web/root/src/bus.ts
new file mode 100644
index 0000000..351e6e8
--- /dev/null
+++ b/web/root/src/bus.ts
@@ -0,0 +1,87 @@
+// deno-lint-ignore-file
+
+import type { Machine } from "./automata.ts";
+import type { Sim, SimStepResult } from "./simulation.ts";
+import type wasm from "./wasm.ts";
+import type { Text } from "npm:@codemirror/state";
+
+type Unsubscribe = () => void;
+
+export class EventBus> {
+ private listeners = new Map void>>();
+
+ on(
+ event: K,
+ handler: (payload: Events[K]) => void,
+ ): Unsubscribe {
+ let set = this.listeners.get(event);
+ if (!set) {
+ set = new Set();
+ this.listeners.set(event, set);
+ }
+ set.add(handler as any);
+
+ return () => this.off(event, handler);
+ }
+
+ once(
+ event: K,
+ handler: (payload: Events[K]) => void,
+ ): Unsubscribe {
+ const off = this.on(event, (payload) => {
+ off();
+ handler(payload);
+ });
+ return off;
+ }
+
+ off(event: K, handler: (payload: Events[K]) => void) {
+ const set = this.listeners.get(event);
+ if (!set) return;
+ set.delete(handler as any);
+ if (set.size === 0) this.listeners.delete(event);
+ }
+
+ emit(event: K, payload: Events[K]) {
+ const set = this.listeners.get(event);
+ if (!set) return;
+
+ // Copy to avoid issues if handlers subscribe/unsubscribe during emit
+ for (const handler of Array.from(set)) {
+ try {
+ (handler as (p: Events[K]) => void)(payload);
+ } catch (e) {
+ console.log(e);
+ }
+ }
+ }
+
+ clear(event?: keyof Events) {
+ if (event) this.listeners.delete(event);
+ else this.listeners.clear();
+ }
+}
+
+type AppEvents = {
+ "begin": void;
+
+ "editor/change": {text: string, doc: Text};
+ "compiled": {log: wasm.CompileLog[], ansi_log: string, machine: string|undefined};
+
+ "automata/sim/update": { simulation: Sim|null };
+ "automata/sim/before_step": { simulation: Sim };
+ "automata/sim/after_step": { simulation: Sim, result: SimStepResult };
+ "automata/update": { automaton: Machine };
+
+ "controls/physics": {enabled: boolean},
+ "controls/reset_network": void,
+
+
+ "controls/step_simulation": void,
+ "controls/reload_simulation": void,
+ "controls/clear_simulation": void,
+
+ "theme/update": void;
+};
+
+export const bus = new EventBus();
diff --git a/web/root/src/controls.ts b/web/root/src/controls.ts
index d54fe85..ef8a8c0 100644
--- a/web/root/src/controls.ts
+++ b/web/root/src/controls.ts
@@ -1,49 +1,70 @@
-import { resetSimulation, stepSimulation } from "./automata.ts";
-import {nodes, edges, network} from "./visualizer.ts"
+import { bus } from "./bus.ts";
-const togglePhysicsBtn = document.getElementById("togglePhysics") as HTMLButtonElement;
-const resetLayoutBtn = document.getElementById("resetLayout") as HTMLButtonElement;
-const playPauseBtn = document.getElementById("playPause") as HTMLButtonElement;
-const stepBtn = document.getElementById("step") as HTMLButtonElement;
-const speedSlider = document.getElementById("speed") as HTMLInputElement;
-const speedLabel = document.getElementById("speedLabel") as HTMLSpanElement;
-const resetSimBtn = document.getElementById("resetSim") as HTMLButtonElement;
+const togglePhysicsBtn = document.getElementById(
+ "togglePhysics",
+) as HTMLButtonElement;
+const resetLayoutBtn = document.getElementById(
+ "resetLayout",
+) as HTMLButtonElement;
+const playPauseBtn = document.getElementById(
+ "playPauseSim",
+) as HTMLButtonElement;
+const stepBtn = document.getElementById("stepSim") as HTMLButtonElement;
+const speedSlider = document.getElementById("speedSim") as HTMLInputElement;
+const speedLabel = document.getElementById("speedSimLabel") as HTMLSpanElement;
+const reloadSimBtn = document.getElementById("reloadSim") as HTMLButtonElement;
+const clearSimBtn = document.getElementById("clearSim") as HTMLButtonElement;
-
-// ---- Physics toggle (styled label) ----
-function setPhysicsButtonUI(enabled: boolean) {
+bus.on("controls/physics", ({ enabled }) => {
togglePhysicsBtn.classList.toggle("active", enabled);
togglePhysicsBtn.textContent = enabled ? "Physics: ON" : "Physics: OFF";
-}
+});
togglePhysicsBtn.onclick = () => {
const enabled = !togglePhysicsBtn.classList.contains("active");
- setPhysicsButtonUI(enabled);
- network.setOptions({ physics: { enabled } });
- network.setOptions({edges: {smooth: enabled}});
+ bus.emit("controls/physics", { enabled });
};
-setPhysicsButtonUI(togglePhysicsBtn.classList.contains("active"));
+bus.emit("controls/physics", {
+ enabled: togglePhysicsBtn.classList.contains("active"),
+});
-resetLayoutBtn.onclick = () => {
- try {
- nodes.forEach((n) => {
- n.physics = true;
- n.x = undefined;
- n.y = undefined;
- });
- network.setData({ nodes, edges });
- } catch {
- // Last resort
- network.setData({ nodes, edges });
+resetLayoutBtn.onclick = () => bus.emit("controls/reset_network", undefined);
+
+clearSimBtn.onclick = () => bus.emit("controls/clear_simulation", undefined);
+
+stepBtn.onclick = () => {
+ bus.emit("controls/step_simulation", undefined);
+};
+
+reloadSimBtn.onclick = () => bus.emit("controls/reload_simulation", undefined);
+
+function updateButtons() {
+ stepBtn.disabled = !simulation_active || running;
+ playPauseBtn.disabled = !simulation_active;
+ clearSimBtn.disabled = !simulation_active;
+}
+
+bus.on("controls/reload_simulation", (_) => {
+ if (running) setRunning(false);
+ updateButtons();
+});
+
+bus.on("automata/sim/update", ({ simulation }) => {
+ simulation_active = !!simulation;
+ if (!simulation) {
+ if (running) setRunning(false);
}
+ updateButtons();
+});
- // If physics button is OFF, keep it OFF (don’t surprise the user)
- const physicsEnabled = togglePhysicsBtn.classList.contains("active");
- network.setOptions({ physics: { enabled: physicsEnabled } });
-};
+bus.on("automata/sim/after_step", ({ result }) => {
+ if (result !== "pending") {
+ if (running) setRunning(false);
+ }
+});
-// ---- Play/Pause + Speed ----
+let simulation_active = false;
let running = false;
let timer: number | null = null;
@@ -74,8 +95,7 @@ function restartTimer() {
const intervalMs = Math.round(1000 / sps);
timer = globalThis.window.setInterval(() => {
- // If your step can throw, keep the interval alive:
- try { stepSimulation(); } catch (e) { console.error(e); }
+ bus.emit("controls/step_simulation", undefined);
}, intervalMs);
}
@@ -93,13 +113,3 @@ function setRunning(on: boolean) {
}
playPauseBtn.onclick = () => setRunning(!running);
-
-stepBtn.onclick = () => {
- stepSimulation();
-};
-
-resetSimBtn.onclick = () => {
- if (running) setRunning(false);
- resetSimulation();
- stepBtn.disabled = false;
-};
\ No newline at end of file
diff --git a/web/root/src/editor.ts b/web/root/src/editor.ts
index 334d0b8..e9d925f 100644
--- a/web/root/src/editor.ts
+++ b/web/root/src/editor.ts
@@ -5,7 +5,6 @@ import {
keymap,
hoverTooltip,
Decoration,
- ViewPlugin,
lineNumbers,
highlightActiveLineGutter,
highlightActiveLine
@@ -18,11 +17,10 @@ import { closeBrackets } from "npm:@codemirror/autocomplete";
import wasm from "./wasm.ts"
-import { terminalPlugin } from "./terminal.ts";
-import { machine_from_json, setAutomaton } from "./automata.ts";
import { sharedText } from "./share.ts";
import { examples } from "./examples.ts";
+import { bus } from "./bus.ts";
function tokenize(text: string) {
@@ -44,31 +42,26 @@ function compile(text: string): wasm.CompileResult {
}
}
-export const analysisField = StateField.define({
+const eventBusConnection = StateField.define({
create(state) {
const text = state.doc.toString();
+ bus.emit("editor/change", {text, doc: state.doc});
return buildAnalysis(text, state.doc);
},
update(value, tr) {
if (!tr.docChanged) return value;
const text = tr.state.doc.toString();
+ bus.emit("editor/change", {text, doc: state.doc});
return buildAnalysis(text, tr.state.doc);
},
provide: (f) => EditorView.decorations.from(f, (v) => v.deco),
});
function buildAnalysis(text: string, doc: Text) {
- save(text);
const tokens = tokenize(text);
- const { log, log_formatted, graph } = compile(text);
+ const { log, ansi_log, machine } = compile(text);
- if (graph){
- try{
- setAutomaton(machine_from_json(graph))
- }catch(e){
- console.log(e);
- }
- }
+ bus.emit("compiled", {log, ansi_log, machine})
const marks = [];
const docLen = doc.length;
@@ -100,7 +93,7 @@ function buildAnalysis(text: string, doc: Text) {
}
const deco = Decoration.set(marks, true);
- return { tokens, log, log_formatted, deco };
+ return { tokens, log, ansi_log, deco };
}
const tokenClass = (t: string) =>
@@ -135,7 +128,7 @@ function sevRank(sev: string) {
// ===================== Hover tooltip (uses cached diags) =====================
const diagHover = hoverTooltip((view, pos) => {
- const { log } = view.state.field(analysisField);
+ const { log } = view.state.field(eventBusConnection);
const hits = log.filter((d) => d.start !== undefined && d.end !== undefined && pos >= d.start && pos <= d.end);
if (hits.length === 0) return null;
@@ -186,7 +179,7 @@ export function getText(): string{
}
const state = EditorState.create({
- doc: sharedText() ?? getSaved() ?? examples[0].machine,
+ doc: "",
extensions: [
lineNumbers(),
highlightActiveLineGutter(),
@@ -197,9 +190,8 @@ const state = EditorState.create({
closeBrackets(),
keymap.of([...defaultKeymap, ...historyKeymap]),
- analysisField,
+ eventBusConnection,
diagHover,
- terminalPlugin,
EditorView.lineWrapping,
],
@@ -208,4 +200,6 @@ const state = EditorState.create({
const editor = new EditorView({
state,
parent: document.getElementById("editor")!,
-});
\ No newline at end of file
+});
+
+bus.on("begin", _ => setText(sharedText() ?? getSaved() ?? examples[0].machine))
\ No newline at end of file
diff --git a/web/root/src/examples.ts b/web/root/src/examples.ts
index f9cb9c4..eb9234b 100644
--- a/web/root/src/examples.ts
+++ b/web/root/src/examples.ts
@@ -10,9 +10,9 @@ export type Category =
| "NTM";
export class Example {
- category: Category;
- title: string;
- machine: string;
+ readonly category: Category;
+ readonly title: string;
+ readonly machine: string;
constructor(category: Category, title: string, machine: string) {
this.category = category;
@@ -21,7 +21,7 @@ export class Example {
}
}
-export const examples: Example[] = [
+export const examples: readonly Example[] = [
new Example(
"Tutorial",
"DFA",
@@ -174,7 +174,7 @@ const CATEGORY_ORDER: Category[] = [
function buildExamplesDropdown(
selectEl: HTMLSelectElement,
- examples: Example[],
+ examples: readonly Example[],
onPick?: (ex: Example) => void,
) {
// Clear everything except the first placeholder option (if present)
diff --git a/web/root/src/main.ts b/web/root/src/main.ts
index 4def64a..c8d68a0 100644
--- a/web/root/src/main.ts
+++ b/web/root/src/main.ts
@@ -1,7 +1,11 @@
-import "./editor.ts"
-import "./visualizer.ts"
+import { bus } from "./bus.ts";
import "./splitters.ts"
import "./controls.ts"
import "./theme.ts"
import "./share.ts"
-import "./examples.ts"
\ No newline at end of file
+import "./examples.ts"
+import "./visualizer.ts"
+import "./editor.ts"
+import "./simulation.ts"
+
+bus.emit("begin", undefined);
\ No newline at end of file
diff --git a/web/root/src/simulation.ts b/web/root/src/simulation.ts
new file mode 100644
index 0000000..c37821b
--- /dev/null
+++ b/web/root/src/simulation.ts
@@ -0,0 +1,325 @@
+import { bus } from "./bus.ts";
+import {
+ Fa,
+ Machine,
+ parse_machine_from_json,
+ Pda,
+ State,
+ Symbol,
+ Tm,
+} from "./automata.ts";
+
+export type SimStepResult = "pending" | "accept" | "reject";
+export type Sim = FaSim | PdaSim | TmSim;
+let simulation: Sim | null = null;
+let automaton: Machine = {
+ type: "fa",
+ alphabet: new Map(),
+ final_states: new Map(),
+ initial_state: "",
+ states: new Map(),
+ transitions: new Map(),
+ transitions_components: new Map(),
+ edges: new Map(),
+};
+
+bus.on("compiled", ({ machine }) => {
+ if (machine) {
+ try {
+ bus.emit("controls/clear_simulation", undefined);
+ automaton = parse_machine_from_json(machine);
+ bus.emit("automata/update", { automaton });
+ } catch (e) {
+ console.log(e);
+ }
+ }
+});
+bus.on("controls/clear_simulation", (_) => {
+ simulation = null;
+ bus.emit("automata/sim/update", { simulation: null });
+});
+bus.on("controls/step_simulation", (_) => {
+ if (simulation) {
+ bus.emit("automata/sim/before_step", { simulation });
+ bus.emit("automata/sim/after_step", {
+ result: simulation.step(),
+ simulation: simulation,
+ });
+ }
+});
+const machineInput = document.getElementById("machineInput") as HTMLInputElement;
+machineInput.addEventListener("input", () => bus.emit("automata/sim/update", {simulation: null}));
+machineInput.addEventListener("keydown", (e) => {
+ if (e.key === "Enter") {
+ bus.emit("controls/reload_simulation", undefined)
+ }
+});
+bus.on("controls/reload_simulation", (_) => {
+ const input = machineInput.value;
+ switch (automaton.type) {
+ case "fa":
+ simulation = new FaSim(automaton as Fa, input);
+ break;
+ case "pda":
+ simulation = new PdaSim(automaton as Pda, input);
+ break;
+ case "tm":
+ simulation = new TmSim(automaton as Tm, input);
+ break;
+ }
+ bus.emit("automata/sim/update", { simulation });
+});
+const simulationStatus = document.getElementById("simulationStatus") as HTMLInputElement;
+bus.on("automata/sim/update", ({simulation}) => {
+ if (!simulation){
+ simulationStatus.innerText = "N/A"
+ simulationStatus.style.color = "var(--fg-2)";
+ }else{
+ simulationStatus.innerText = "Pending"
+ simulationStatus.style.color = "var(--warning)";
+ }
+});
+bus.on("automata/sim/after_step", ({result}) => {
+ if (result === "pending"){
+ simulationStatus.innerText = "Pending"
+ simulationStatus.style.color = "var(--warning)";
+ }else if (result==="accept"){
+ simulationStatus.innerText = "Accepted"
+ simulationStatus.style.color = "var(--success)";
+ }else if (result==="reject"){
+ simulationStatus.innerText = "Rejected"
+ simulationStatus.style.color = "var(--error)";
+ }
+});
+
+export class FaState {
+ readonly state: State;
+
+ readonly position: number;
+ readonly input: string;
+ readonly accepted: boolean = false;
+ private repr!: string;
+
+ constructor(state: State, position: number, input: string) {
+ this.state = state;
+ this.position = position;
+ this.input = input;
+ }
+
+ toString(): string {
+ if (!this.repr) {
+ this.repr = this.state + +" >" + this.input.substring(this.position);
+ }
+ return this.repr;
+ }
+}
+
+export class FaSim {
+ machine: Fa;
+ paths: FaState[];
+ input: string;
+
+ current_states: Map = new Map();
+ accepted: FaState[] = [];
+
+ constructor(machine: Fa, input: string) {
+ this.machine = machine;
+ this.paths = [new FaState(machine.initial_state, 0, input)];
+ this.current_states.set(machine.initial_state, [this.paths[0]]);
+ this.input = input;
+ }
+
+ step(): SimStepResult {
+ if (this.paths.length == 0) return "reject";
+ if (this.accepted.length != 0) return "accept";
+
+ const paths: FaState[] = [];
+ this.current_states.clear();
+
+ const push = (state: FaState) => {
+ paths.push(state);
+ if (!this.current_states.has(state.state)) {
+ this.current_states.set(state.state, []);
+ }
+ this.current_states.get(state.state)?.push(state);
+
+ if (
+ state.position == this.input.length &&
+ this.machine.final_states.has(state.state)
+ ) {
+ // @ts-expect-error sillllyyyy
+ state.accepted = true;
+ this.accepted.push(state);
+ }
+ };
+
+ for (const path of this.paths) {
+ const letter_map = this.machine.transitions_components.get(path.state)!;
+ if (!letter_map) continue;
+
+ for (const to of letter_map.get(null) ?? []) {
+ push(new FaState(to.state, path.position, this.input));
+ }
+
+ if (path.position >= this.input.length) continue;
+
+ const char = this.input.charAt(path.position);
+
+ for (const to of letter_map.get(char) ?? []) {
+ push(new FaState(to.state, path.position + 1, this.input));
+ }
+ }
+ this.paths = paths;
+
+ if (this.paths.length == 0) return "reject";
+ if (this.accepted.length != 0) return "accept";
+ return "pending";
+ }
+}
+
+export class PdaState {
+ readonly state: State;
+ readonly stack: Symbol[];
+
+ readonly position: number;
+ readonly input: string;
+ readonly accepted: boolean = false;
+ private repr!: string;
+
+ constructor(state: State, stack: Symbol[], position: number, input: string) {
+ this.state = state;
+ this.stack = stack;
+ this.position = position;
+ this.input = input;
+ }
+
+ toString(): string {
+ if (!this.repr) {
+ this.repr = this.state + " [" + this.stack + "]" + " >" +
+ this.input.substring(this.position);
+ }
+ return this.repr;
+ }
+}
+
+export class PdaSim {
+ machine: Pda;
+ paths: PdaState[];
+ input: string;
+
+ current_states: Map = new Map();
+ accepted: PdaState[] = [];
+
+ constructor(machine: Pda, input: string) {
+ this.machine = machine;
+ this.paths = [
+ new PdaState(machine.initial_state, [machine.initial_stack], 0, input),
+ ];
+ this.current_states.set(machine.initial_state, [this.paths[0]]);
+ this.input = input;
+ }
+
+ step(): SimStepResult {
+ if (this.paths.length == 0) return "reject";
+ if (this.accepted.length != 0) return "accept";
+
+ const paths: PdaState[] = [];
+ this.current_states.clear();
+
+ const push = (state: PdaState) => {
+ paths.push(state);
+ if (!this.current_states.has(state.state)) {
+ this.current_states.set(state.state, []);
+ }
+ this.current_states.get(state.state)?.push(state);
+
+ if (
+ state.position == this.input.length && this.machine.final_states &&
+ this.machine.final_states.has(state.state) ||
+ state.position == this.input.length && !this.machine.final_states &&
+ state.stack.length == 1 &&
+ state.stack[0] == this.machine.initial_stack
+ ) {
+ // @ts-expect-error sillllyyyy
+ state.accepted = true;
+ this.accepted.push(state);
+ }
+ };
+
+ for (const path of this.paths) {
+ const stack = path.stack.pop()!;
+ const letter_map = this.machine.transitions_components.get(path.state)
+ ?.get(stack);
+ if (!letter_map) continue;
+
+ for (const to of letter_map.get(null) ?? []) {
+ push(
+ new PdaState(
+ to.state,
+ path.stack.concat(to.stack),
+ path.position,
+ this.input,
+ ),
+ );
+ }
+
+ if (path.position >= this.input.length) continue;
+
+ const char = this.input.charAt(path.position);
+
+ for (const to of letter_map.get(char) ?? []) {
+ push(
+ new PdaState(
+ to.state,
+ path.stack.concat(to.stack),
+ path.position + 1,
+ this.input,
+ ),
+ );
+ }
+ }
+ this.paths = paths;
+
+ if (this.paths.length == 0) return "reject";
+ if (this.accepted.length != 0) return "accept";
+ return "pending";
+ }
+}
+
+export class TmState {
+ readonly state: State;
+ readonly tape: Symbol[];
+
+ readonly position: number;
+ readonly input: string;
+ readonly accepted: boolean = false;
+ private repr!: string;
+
+ constructor(state: State, tape: Symbol[], position: number, input: string) {
+ this.state = state;
+ this.tape = tape;
+ this.position = position;
+ this.input = input;
+ }
+
+ toString(): string {
+ if (!this.repr) this.repr = this.state + " " + this.position;
+ return this.repr;
+ }
+}
+
+export class TmSim {
+ machine: Tm;
+ input: string;
+ current_states: Map = new Map();
+ accepted: TmState[] = [];
+
+ constructor(machine: Tm, input: string) {
+ this.machine = machine;
+ this.input = input;
+ }
+
+ step(): SimStepResult {
+ return "pending";
+ }
+}
diff --git a/web/root/src/splitters.ts b/web/root/src/splitters.ts
index 89bc5ba..d6a0e88 100644
--- a/web/root/src/splitters.ts
+++ b/web/root/src/splitters.ts
@@ -63,7 +63,7 @@ function setFlexFill(pane: HTMLElement) {
pane.style.flex = "1 1 auto";
}
-export function enableFlexSplitters() {
+function enableFlexSplitters() {
// Horizontal: A | hSplit | B (top/split/bottom)
for (const splitter of document.querySelectorAll(".hSplit:not(.styleOnly)")) {
const parent = splitter.parentElement as HTMLElement | null;
diff --git a/web/root/src/terminal.ts b/web/root/src/terminal.ts
index fd83210..980dc3c 100644
--- a/web/root/src/terminal.ts
+++ b/web/root/src/terminal.ts
@@ -1,10 +1,14 @@
-// deno-lint-ignore-file
+import { bus } from "./bus.ts";
-import {
- ViewPlugin,
-} from "npm:@codemirror/view";
+bus.on("compiled", ({log, ansi_log}) => {
+ const term = document.getElementById("terminal");
+ if (!term) return;
-import { analysisField } from "./editor.ts";
+ let s = "";
+ s += `\x1b[90m[compile]\x1b[0m ${log.length} diagnostics\n`;
+
+ term.innerHTML = ansiToHtml(s + ansi_log);
+})
function escapeHtml(s: string) {
return s
@@ -77,33 +81,4 @@ function ansiToHtml(input: string) {
out += openSpanIfNeeded(input.slice(lastIndex));
return out;
-}
-
- // @ts-expect-error bad library
-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);
-}
-
-export const terminalPlugin = ViewPlugin.fromClass(
- class {
-
- // @ts-expect-error bad library
- constructor(view) {
- // @ts-expect-error bad library
- this.view = view;
- formatTerminal(view);
- }
- // @ts-expect-error bad library
- update(update) {
- if (update.docChanged) formatTerminal(update.view);
- }
- }
-);
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/web/root/src/theme.ts b/web/root/src/theme.ts
index 8dd0244..8debb35 100644
--- a/web/root/src/theme.ts
+++ b/web/root/src/theme.ts
@@ -1,4 +1,4 @@
-import { updateGraphTheme } from "./visualizer.ts";
+import { bus } from "./bus.ts";
const themeBtn = document.getElementById("themeToggle") as HTMLButtonElement;
@@ -22,11 +22,11 @@ function setTheme(theme: Theme) {
// update button label
themeBtn.textContent = theme === "dark" ? "🌙 Dark" : "☀️ Light";
- updateGraphTheme();
+
+ bus.emit("theme/update", undefined);
}
-
-setTheme(getPreferredTheme());
+bus.on("begin", _ => setTheme(getPreferredTheme()))
themeBtn.addEventListener("click", toggleTheme);
function toggleTheme() {
@@ -34,8 +34,6 @@ function toggleTheme() {
setTheme(current === "dark" ? "light" : "dark");
}
-
-
globalThis.window.matchMedia?.("(prefers-color-scheme: light)")
?.addEventListener("change", () => {
if (localStorage.getItem("theme")) return;
diff --git a/web/root/src/visualizer.ts b/web/root/src/visualizer.ts
index 4b6de48..8c34b15 100644
--- a/web/root/src/visualizer.ts
+++ b/web/root/src/visualizer.ts
@@ -1,143 +1,44 @@
// deno-lint-ignore-file no-unversioned-import
-
// deno-lint-ignore no-import-prefix
import * as vis from "npm:vis-network/standalone";
-import { automaton, setAutomaton, sim } from "./automata.ts";
-export const nodes = new vis.DataSet();
-export const edges = new vis.DataSet();
-
-type Color = string;
-type GraphTheme = {
- bg_0: Color;
- bg_1: Color;
- bg_2: Color;
- fg_0: Color;
- fg_1: Color;
- fg_2: Color;
-
- node_anchor: Color;
- node_border: Color;
- current_node_border: Color;
-
- edge: Color;
- edge_hover: Color;
- edge_active: Color;
-
- font_face: string
-
- node_font_size: number;
- node_font: string,
- node_font_bold: string,
-
- edge_font_size: number;
- edge_font: string,
- edge_font_bold: string,
-};
-
-let _graphTheme: GraphTheme | null = null;
-
-function invalidateGraphThemeCache() {
- _graphTheme = null;
-}
-
-function getGraphTheme(): GraphTheme {
- function cssVar(name: string, fallback = ""): string {
- return getComputedStyle(document.documentElement)
- .getPropertyValue(name)
- .trim() || fallback;
- }
-
- if (_graphTheme) return _graphTheme;
-
- _graphTheme = {
- bg_0: cssVar("--graph-bg-0"),
- bg_1: cssVar("--graph-bg-1"),
- bg_2: cssVar("--graph-bg-2"),
- fg_0: cssVar("--graph-fg-0"),
- fg_1: cssVar("--graph-fg-1"),
- fg_2: cssVar("--graph-fg-2"),
-
- node_anchor: cssVar("--graph-node-anchor"),
- node_border: cssVar("--graph-node-border"),
- current_node_border: cssVar("--graph-current-node-border"),
-
- edge: cssVar("--graph-edge"),
- edge_hover: cssVar("--graph-edge-hover"),
- edge_active: cssVar("--graph-edge-active"),
-
- font_face: cssVar("--graph-font"),
-
- node_font_size: Number(cssVar("--graph-node-font-size")),
- node_font: `${cssVar("--graph-node-font-size")}px ${cssVar("--graph-font")}`,
- node_font_bold: `bold ${cssVar("--graph-node-font-size")}px ${cssVar("--graph-font")}`,
-
- edge_font_size: Number(cssVar("--graph-edge-font-size")),
- edge_font: `${Number(cssVar("--graph-edge-font-size"))}px ${cssVar("--graph-font")}`,
- edge_font_bold: `bold ${Number(cssVar("--graph-edge-font-size"))}px ${cssVar("--graph-font")}`,
- };
-
- return _graphTheme;
-}
-
-export function updateGraphTheme() {
- invalidateGraphThemeCache();
- const gt = getGraphTheme();
-
- network.setOptions({
- nodes: {
- labelHighlightBold: false,
- font: {
- color: gt.fg_0,
- bold: {
- color: gt.fg_1,
- },
- },
- },
- edges: {
- labelHighlightBold: true,
- font: {
- align: "top",
- face: gt.font_face,
- size: gt.edge_font_size,
- color: gt.fg_0,
- strokeColor: gt.bg_0,
- bold: {
- color: gt.fg_1,
- face: gt.font_face,
- size: gt.edge_font_size,
- mod: "bold",
- },
- },
- color: {
- color: gt.edge,
- hover: gt.edge_hover,
- highlight: gt.edge_active,
- },
- shadow: {
- enabled: false,
- },
- },
- });
-
- setAutomaton(automaton)
-}
+import { bus } from "./bus.ts";
+import type { Sim } from "./simulation.ts";
+import type { Machine } from "./automata.ts";
-let _measureCanvas: HTMLCanvasElement | null = null;
+bus.on("controls/physics", ({enabled}) => {
+ network.setOptions({ physics: { enabled } });
+ network.setOptions({edges: {smooth: enabled}});
+});
+bus.on("controls/reset_network", _ => {
+ try {
+ nodes.forEach((n) => {
+ n.physics = true;
+ n.x = undefined;
+ n.y = undefined;
+ });
+ network.setData({ nodes, edges });
+ } catch {
+ // Last resort
+ network.setData({ nodes, edges });
+ }
+});
-export function measureTextWidth(text: string, font: string): number {
- if (!_measureCanvas) {
- _measureCanvas = document.createElement("canvas");
- }
+bus.on("automata/sim/after_step", _ => {
+ network.redraw();
+});
- const ctx = _measureCanvas.getContext("2d")!;
- ctx.font = font;
+let simulation: Sim | null = null;
+bus.on("automata/sim/update", ({simulation: sim}) => {
+ simulation = sim;
+ network.redraw();
+});
- return ctx.measureText(text).width;
-}
+let automaton: Machine
-export function updateVisualization() {
+bus.on("automata/update", ({automaton: auto}) => {
+ automaton = auto;
// Populate nodes
for (const state of automaton.states.keys()) {
@@ -187,17 +88,154 @@ export function updateVisualization() {
}
}
+ // delete old edges
for (const edge_id of edges.getIds()) {
if (!automaton.edges.has(edge_id as string)) {
edges.remove(edge_id);
}
}
+ // delete old nodes
for (const node_id of nodes.getIds()) {
if (!automaton.states.has(node_id as string)) {
nodes.remove(node_id);
}
}
+});
+
+
+const nodes = new vis.DataSet();
+const edges = new vis.DataSet();
+
+
+let _graphTheme: GraphTheme | null = null;
+bus.on("theme/update", _ => {
+ _graphTheme = null;
+ updateGraphTheme()
+});
+
+
+type Color = string;
+type GraphTheme = {
+ bg_0: Color;
+ bg_1: Color;
+ bg_2: Color;
+ fg_0: Color;
+ fg_1: Color;
+ fg_2: Color;
+
+ node_anchor: Color;
+ node_border: Color;
+ current_node_border: Color;
+
+ edge: Color;
+ edge_hover: Color;
+ edge_active: Color;
+
+ font_face: string
+
+ node_font_size: number;
+ node_font: string,
+ node_font_bold: string,
+
+ edge_font_size: number;
+ edge_font: string,
+ edge_font_bold: string,
+};
+
+function getGraphTheme(): GraphTheme {
+ function cssVar(name: string, fallback = ""): string {
+ return getComputedStyle(document.documentElement)
+ .getPropertyValue(name)
+ .trim() || fallback;
+ }
+
+ if (_graphTheme) return _graphTheme;
+
+ _graphTheme = {
+ bg_0: cssVar("--graph-bg-0"),
+ bg_1: cssVar("--graph-bg-1"),
+ bg_2: cssVar("--graph-bg-2"),
+ fg_0: cssVar("--graph-fg-0"),
+ fg_1: cssVar("--graph-fg-1"),
+ fg_2: cssVar("--graph-fg-2"),
+
+ node_anchor: cssVar("--graph-node-anchor"),
+ node_border: cssVar("--graph-node-border"),
+ current_node_border: cssVar("--graph-current-node-border"),
+
+ edge: cssVar("--graph-edge"),
+ edge_hover: cssVar("--graph-edge-hover"),
+ edge_active: cssVar("--graph-edge-active"),
+
+ font_face: cssVar("--graph-font"),
+
+ node_font_size: Number(cssVar("--graph-node-font-size")),
+ node_font: `${cssVar("--graph-node-font-size")}px ${cssVar("--graph-font")}`,
+ node_font_bold: `bold ${cssVar("--graph-node-font-size")}px ${cssVar("--graph-font")}`,
+
+ edge_font_size: Number(cssVar("--graph-edge-font-size")),
+ edge_font: `${Number(cssVar("--graph-edge-font-size"))}px ${cssVar("--graph-font")}`,
+ edge_font_bold: `bold ${Number(cssVar("--graph-edge-font-size"))}px ${cssVar("--graph-font")}`,
+ };
+
+ return _graphTheme;
+}
+
+function updateGraphTheme() {
+ const gt = getGraphTheme();
+
+ network.setOptions({
+ nodes: {
+ labelHighlightBold: false,
+ font: {
+ color: gt.fg_0,
+ bold: {
+ color: gt.fg_1,
+ },
+ },
+ },
+ edges: {
+ labelHighlightBold: true,
+ font: {
+ align: "top",
+ face: gt.font_face,
+ size: gt.edge_font_size,
+ color: gt.fg_0,
+ strokeColor: gt.bg_0,
+ bold: {
+ color: gt.fg_1,
+ face: gt.font_face,
+ size: gt.edge_font_size,
+ mod: "bold",
+ },
+ },
+ color: {
+ color: gt.edge,
+ hover: gt.edge_hover,
+ highlight: gt.edge_active,
+ },
+ shadow: {
+ enabled: false,
+ },
+ },
+ });
+
+ network.redraw();
+}
+
+
+let _measureCanvas: HTMLCanvasElement | null = null;
+
+function measureTextWidth(text: string, font: string): number {
+ if (!_measureCanvas) {
+ _measureCanvas = document.createElement("canvas");
+ }
+
+ const ctx = _measureCanvas.getContext("2d")!;
+ ctx.font = font;
+
+ return ctx.measureText(text).width;
}
function chosen_edge(
@@ -216,7 +254,7 @@ function chosen_node(
) {
}
-export const network: vis.Network = createGraph();
+const network: vis.Network = createGraph();
function createGraph(): vis.Network {
const container = document.getElementById("graph")!;
@@ -278,6 +316,7 @@ function createGraph(): vis.Network {
return network;
}
+
function renderNode({
ctx,
id,
@@ -296,7 +335,7 @@ function renderNode({
const isFinal = automaton.final_states
? automaton.final_states.has(id)
: false;
- const isActive = sim?sim.current_states.has(id):false;
+ const isActive = simulation?simulation.current_states.has(id):false;
const fill = selected ? t.bg_2 : hover ? t.bg_1 : t.bg_0;
const stroke = isActive ? t.current_node_border : t.node_border;
@@ -339,7 +378,7 @@ function renderNode({
}
if (isActive) {
- const paths = sim?.current_states.get(id)!;
+ const paths = simulation?.current_states.get(id)!;
const padX = 8;
const padY = 6;
const lineH = 14;
diff --git a/web/root/style/controls.scss b/web/root/style/controls.scss
index 71af4aa..077d6dc 100644
--- a/web/root/style/controls.scss
+++ b/web/root/style/controls.scss
@@ -52,27 +52,45 @@
}
}
-.btn-blue {
- background: color-mix(in srgb, var(--accent) 14%, transparent);
- border-color: color-mix(in srgb, var(--accent) 40%, transparent);
+.btn-yellow {
+ background: color-mix(in srgb, var(--warning) 14%, transparent);
+ border-color: color-mix(in srgb, var(--warning) 40%, transparent);
&:hover {
- background: color-mix(in srgb, var(--accent) 22%, transparent);
+ background: color-mix(in srgb, var(--warning) 22%, transparent);
+ }
+}
+
+.btn-blue {
+ background: color-mix(in srgb, var(--focus) 14%, transparent);
+ border-color: color-mix(in srgb, var(--focus) 40%, transparent);
+
+ &:hover {
+ background: color-mix(in srgb, var(--focus) 22%, transparent);
}
}
.btn-grey {
- background: color-mix(in srgb, var(--accent) 12%, transparent);
- border-color: color-mix(in srgb, var(--accent) 28%, transparent);
+ background: color-mix(in srgb, var(--ansi-fg-90) 12%, transparent);
+ border-color: color-mix(in srgb, var(--ansi-fg-90) 28%, transparent);
&:hover {
- background: color-mix(in srgb, var(--accent) 18%, transparent);
+ background: color-mix(in srgb, var(--ansi-fg-90) 18%, transparent);
+ }
+}
+
+.btn-red {
+ background: color-mix(in srgb, var(--error) 12%, transparent);
+ border-color: color-mix(in srgb, var(--error) 28%, transparent);
+
+ &:hover {
+ background: color-mix(in srgb, var(--error) 18%, transparent);
}
}
.btn-toggle.active {
- background: color-mix(in srgb, var(--warning) 14%, transparent);
- border-color: color-mix(in srgb, var(--warning) 30%, transparent);
+ background: color-mix(in srgb, var(--success) 14%, transparent);
+ border-color: color-mix(in srgb, var(--success) 30%, transparent);
}
@@ -97,3 +115,33 @@
text-align: right;
opacity: 0.9;
}
+
+
+
+
+.test-input {
+ width: 100%;
+ max-width: 420px;
+
+ align-self: center;
+
+ padding: 10px 14px;
+ border-radius: 12px;
+ border: 1px solid var(--bg-1);
+ background: var(--bg-2);
+
+ font: var(--font-ui);
+ color: var(--fg-0);
+
+ outline: none;
+ transition: border-color 150ms ease, box-shadow 150ms ease;
+}
+
+.test-input::placeholder {
+ color: var(--fg-0);
+}
+
+.test-input:focus {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent);
+}
\ No newline at end of file
diff --git a/web/root/style/editor.scss b/web/root/style/editor.scss
index a09ed24..6cd740a 100644
--- a/web/root/style/editor.scss
+++ b/web/root/style/editor.scss
@@ -31,7 +31,6 @@
.cm-lineNumbers .cm-gutterElement {
padding: 0 10px 0 6px;
font-family: var(--font-mono);
- font-size: 12px;
}
.cm-activeLine {
diff --git a/web/root/style/style.scss b/web/root/style/style.scss
index c516eae..2534d55 100644
--- a/web/root/style/style.scss
+++ b/web/root/style/style.scss
@@ -11,6 +11,7 @@ body {
margin: 0;
color: var(--fg-0);
font-family: var(--font-ui);
+ font-size: 14px;
background: #909090;
}
@@ -103,6 +104,7 @@ body {
border-radius: 12px;
border: 1px solid var(bg-1);
background: var(--bg-2);
+ font: var(--font-ui)
}
.share-toast {
diff --git a/web/root/style/terminal.scss b/web/root/style/terminal.scss
index 1040c5a..ae32e8b 100644
--- a/web/root/style/terminal.scss
+++ b/web/root/style/terminal.scss
@@ -4,7 +4,6 @@
padding: 1em;
margin: 0px;
font-family: var(--font-mono);
- font-size: 12.5px;
line-height: 1.35;
white-space: pre-wrap;
word-break: break-word;
diff --git a/web_lib/src/lib.rs b/web_lib/src/lib.rs
index cd29ba7..e9ec072 100644
--- a/web_lib/src/lib.rs
+++ b/web_lib/src/lib.rs
@@ -152,8 +152,8 @@ pub struct Graph<'a> {
#[wasm_bindgen(getter_with_clone)]
pub struct CompileResult {
pub log: Vec,
- pub log_formatted: String,
- pub graph: Option,
+ pub ansi_log: String,
+ pub machine: Option,
}
#[wasm_bindgen]
@@ -161,10 +161,10 @@ pub fn compile(input: &str) -> CompileResult {
let mut ctx = Context::new(input);
let result = automata::loader::parse_universal(&mut ctx);
- let graph = result.map(|result| serde_json::to_string(&result).unwrap());
+ let machine = result.map(|result| serde_json::to_string(&result).unwrap());
use std::fmt::Write;
- let log_formatted = ctx.logs_display().fold(String::new(), |mut s, e| {
+ let ansi_log = ctx.logs_display().fold(String::new(), |mut s, e| {
write!(&mut s, "{e}").unwrap();
s
});
@@ -190,7 +190,7 @@ pub fn compile(input: &str) -> CompileResult {
CompileResult {
log,
- log_formatted,
- graph,
+ ansi_log,
+ machine,
}
}