Merge branch 'main' into gh-pages

This commit is contained in:
Parker TenBroeck 2026-01-13 18:31:48 -05:00
commit b3e2af0be2
11 changed files with 473 additions and 125 deletions

View file

@ -48,12 +48,45 @@
<div class="hSplit styleOnly" title="Drag to resize canvas height"></div>
<div class="flexCenter sidePadding" style="font-size: calc(16px);font-weight: bold;color: var(--fg-1)">
<span style="margin-right: 0.5em;">Status: </span><span style="color: var(--fg-2)" id="simulationStatus">N/A</span>
<span style="margin-right: 0.5em;">Status: </span><span style="color: var(--fg-2)"
id="simulationStatus">N/A</span>
</div>
<div class="flexCenter sidePadding">
<input id="machineInput" type="text" class="test-input" placeholder="Enter machine input…" />
</div>
<div class="hSplit styleOnly" title="Drag to resize canvas height"></div>
<div class="sidePadding scroll">
<div class="pathsGrid">
<details class="group group-accepted">
<summary class="groupTitle" open>
<span>Accepted</span>
<span class="count" id="acceptedCount">0</span>
</summary>
<div class="groupBody" id="acceptedPaths"></div>
</details>
<details class="group group-running" open>
<summary class="groupTitle">
<span>Running</span>
<span class="count" id="runningCount">0</span>
</summary>
<div class="groupBody" id="runningPaths"></div>
</details>
<details class="group group-rejected" open>
<summary class="groupTitle">
<span>Rejected</span>
<span class="count" id="rejectedCount">0</span>
</summary>
<div class="groupBody" id="rejectedPaths"></div>
</details>
</div>
</div>
</section>
</section>

View file

@ -0,0 +1,2 @@
export const EPSILON: string = "ε"
export const DELTA: string = "δ"

View file

@ -8,5 +8,6 @@ import "./visualizer.ts"
import "./editor.ts"
import "./simulation.ts"
import "./terminal.ts"
import "./paths.ts"
bus.emit("begin", undefined);

147
web/root/src/paths.ts Normal file
View file

@ -0,0 +1,147 @@
import { bus } from "./bus.ts";
import { DELTA } from "./constants.ts";
import type { Sim } from "./simulation.ts";
import type { FaState } from "./simulation/fa.ts";
import type { PdaState } from "./simulation/pda.ts";
import type { TmState } from "./simulation/tm.ts";
type AnyState = {
repr: string;
path: readonly unknown[];
};
function renderFaPath(state: FaState, index: number) {
const details = document.createElement("details");
details.className = "pathItem";
const summary = document.createElement("summary");
summary.className = "pathHeader";
summary.innerHTML = `
<span>${state.repr}</span>
<span class="pathMeta">
<span>steps: ${state.path.length}</span>
</span>
`;
const steps = document.createElement("div");
steps.className = "steps";
for (let i = 0; i < state.path.length; i++) {
const div = document.createElement("div");
div.className = "stepLine";
const step = state.path[i];
div.textContent = `${i + 1}. ${DELTA}(${step.from_state}, ${step.from_letter}) = ${step.state}`;
steps.appendChild(div);
}
details.appendChild(summary);
details.appendChild(steps);
return details;
}
function renderPdaPath(state: PdaState, index: number) {
const details = document.createElement("details");
details.className = "pathItem";
const summary = document.createElement("summary");
summary.className = "pathHeader";
summary.innerHTML = `
<span>${state.repr}</span>
<span class="pathMeta">
<span>steps: ${state.path.length}</span>
</span>
`;
const steps = document.createElement("div");
steps.className = "steps";
for (let i = 0; i < state.path.length; i++) {
const div = document.createElement("div");
div.className = "stepLine";
const step = state.path[i];
div.textContent = `${i + 1}. ${DELTA}(${step.from_state}, ${step.from_letter}, , ${step.from_stack}) = (${step.state}, [ ${step.stack.join(" ")} ])`;
steps.appendChild(div);
}
details.appendChild(summary);
details.appendChild(steps);
return details;
}
function renderTmPath(state: TmState, index: number) {
const details = document.createElement("details");
details.className = "pathItem";
const summary = document.createElement("summary");
summary.className = "pathHeader";
summary.innerHTML = `
<span>${state.repr}</span>
<span class="pathMeta">
<span>steps: ${state.path.length}</span>
</span>
`;
const steps = document.createElement("div");
steps.className = "steps";
for (let i = 0; i < state.path.length; i++) {
const div = document.createElement("div");
div.className = "stepLine";
const step = state.path[i];
div.textContent = `${i + 1}. ${DELTA}(${step.from_state}, ${step.from_symbol}) = (${step.state}, ${step.symbol}, ${step.direction})`;
steps.appendChild(div);
}
details.appendChild(summary);
details.appendChild(steps);
return details;
}
bus.on("automata/sim/after_step", ({simulation}) => {
renderPaths(simulation)
})
bus.on("automata/sim/update", simulation => {
if(simulation){
renderPaths(simulation)
}else{
renderPaths(undefined)
}
})
export function renderPaths(sim: Sim | undefined) {
const acceptedEl = document.getElementById("acceptedPaths")!;
const runningEl = document.getElementById("runningPaths")!;
const rejectedEl = document.getElementById("rejectedPaths")!;
const acceptedCount = document.getElementById("acceptedCount")!;
const runningCount = document.getElementById("runningCount")!;
const rejectedCount = document.getElementById("rejectedCount")!;
acceptedEl.innerHTML = "";
runningEl.innerHTML = "";
rejectedEl.innerHTML = "";
acceptedCount.textContent = String(sim?.accepted.length ?? 0);
runningCount.textContent = String(sim?.paths.length ?? 0);
rejectedCount.textContent = String(sim?.rejected.length ?? 0);
if(!sim)return;
switch (sim.machine.type){
case "fa":
sim.accepted.forEach((s, i) => acceptedEl.appendChild(renderFaPath(s as FaState, i)));
sim.paths.forEach((s, i) => runningEl.appendChild(renderFaPath(s as FaState, i)));
sim.rejected.forEach((s, i) => rejectedEl.appendChild(renderFaPath(s as FaState, i)));
break;
case "pda":
sim.accepted.forEach((s, i) => acceptedEl.appendChild(renderPdaPath(s as PdaState, i)));
sim.paths.forEach((s, i) => runningEl.appendChild(renderPdaPath(s as PdaState, i)));
sim.rejected.forEach((s, i) => rejectedEl.appendChild(renderPdaPath(s as PdaState, i)));
break;
case "tm":
sim.accepted.forEach((s, i) => acceptedEl.appendChild(renderTmPath(s as TmState, i)));
sim.paths.forEach((s, i) => runningEl.appendChild(renderTmPath(s as TmState, i)));
sim.rejected.forEach((s, i) => rejectedEl.appendChild(renderTmPath(s as TmState, i)));
break;
}
}

View file

@ -34,12 +34,13 @@ export let automaton: Machine = {
bus.on("compiled", ({ machine }) => {
if (machine) {
try {
bus.emit("controls/sim/clear", undefined);
automaton = parse_machine_from_json(machine);
bus.emit("automata/update", automaton);
} catch (e) {
console.log(e);
}
}else{
bus.emit("controls/sim/clear", undefined);
}
});
bus.on("controls/sim/clear", (_) => {
@ -83,20 +84,21 @@ bus.on("automata/sim/update", simulation => {
simulationStatus.innerText = "N/A"
simulationStatus.style.color = "var(--fg-2)";
}else{
simulationStatus.innerText = "Pending"
simulationStatus.style.color = "var(--warning)";
update_status(simulation.status())
}
});
bus.on("automata/sim/after_step", ({result}) => {
if (result === "pending"){
update_status(result)
});
function update_status(status: SimStepResult){
if (status === "pending"){
simulationStatus.innerText = "Pending"
simulationStatus.style.color = "var(--warning)";
}else if (result==="accept"){
}else if (status==="accept"){
simulationStatus.innerText = "Accepted"
simulationStatus.style.color = "var(--success)";
}else if (result==="reject"){
}else if (status==="reject"){
simulationStatus.innerText = "Rejected"
simulationStatus.style.color = "var(--error)";
}
});
}

View file

@ -3,8 +3,11 @@ import type {
FaTransTo,
State,
} from "../automata.ts";
import { EPSILON } from "../constants.ts";
import { SimStepResult } from "../simulation.ts";
export type Step = FaTransTo & {from_state: State, from_letter: string}
export type FaState = {
readonly state: State;
readonly position: number;
@ -12,7 +15,7 @@ export type FaState = {
readonly accepted: boolean;
readonly repr: string;
readonly path: readonly FaTransTo[];
readonly path: readonly Step[];
};
type Initializer<T> = { -readonly [P in keyof T]?: T[P] | undefined };
@ -65,19 +68,19 @@ export class FaSim {
this.init_state(state);
}
private transition(from: FaState, to: FaTransTo, consume: boolean) {
private transition(from: FaState, to: FaTransTo, letter: string|undefined) {
const state: Initializer<FaState> = {
state: to.state,
position: from.position + (consume ? 1 : 0),
path: from.path.concat([to]),
position: from.position + (letter ? 1 : 0),
path: from.path.concat([{from_state: from.state, from_letter: letter??EPSILON, ...to}]),
};
this.init_state(state);
}
step(): SimStepResult {
if (this.accepted.length !== 0) return "accept";
if (this.paths.length === 0) return "reject";
const paths = this.paths;
this.paths = [];
@ -93,7 +96,7 @@ export class FaSim {
// epsilon transitions
const eps = letterMap.get(null) ?? [];
for (const to of eps) this.transition(from, to, false);
for (const to of eps) this.transition(from, to, undefined);
// consuming transitions
if (from.position >= this.input.length) {
@ -103,13 +106,17 @@ export class FaSim {
const ch = this.input.charAt(from.position);
const trs = letterMap.get(ch) ?? [];
for (const to of trs) this.transition(from, to, true);
for (const to of trs) this.transition(from, to, ch);
if (eps.length === 0 && trs.length === 0) {
this.rejected.push(from);
}
}
return this.status();
}
status(): SimStepResult {
if (this.accepted.length !== 0) return "accept";
if (this.paths.length === 0) return "reject";
return "pending";

View file

@ -5,7 +5,10 @@ import type {
Symbol
} from "../automata.ts";
import { SimStepResult } from "../simulation.ts";
import { EPSILON } from "../constants.ts";
export type Step = PdaTransTo & {from_state: State, from_letter: string, from_stack: Symbol}
export type PdaState = {
readonly state: State;
readonly stack: Symbol[];
@ -14,7 +17,7 @@ export type PdaState = {
readonly accepted: boolean;
readonly repr: string;
readonly path: readonly PdaTransTo[];
readonly path: readonly Step[];
};
type Initializer<T> = { -readonly [P in keyof T]?: T[P] | undefined };
@ -80,7 +83,7 @@ export class PdaSim {
this.init_state(state);
}
private transition(from: PdaState, to: PdaTransTo, consume: boolean) {
private transition(from: PdaState, to: PdaTransTo, letter: string|undefined) {
const stackCopy = from.stack.slice(0, from.stack.length - 1); // pop off top
const nextStack = stackCopy.concat(to.stack);
if (nextStack.length == 0) {
@ -91,16 +94,14 @@ export class PdaSim {
const state: Initializer<PdaState> = {
state: to.state,
stack: nextStack,
position: from.position + (consume ? 1 : 0),
path: from.path.concat([to]),
position: from.position + (letter ? 1 : 0),
path: from.path.concat([{from_state: from.state, from_letter: letter??EPSILON, from_stack: from.stack[from.stack.length-1], ...to}]),
};
this.init_state(state);
}
step(): SimStepResult {
if (this.accepted.length !== 0) return "accept";
if (this.paths.length === 0) return "reject";
const paths = this.paths;
this.paths = [];
@ -118,7 +119,7 @@ export class PdaSim {
// epsilon transitions
const epsilon_transitions = letterMap.get(null) ?? [];
for (const to of epsilon_transitions) {
this.transition(from, to, false);
this.transition(from, to, undefined);
}
if (from.position >= this.input.length) {
@ -132,13 +133,17 @@ export class PdaSim {
const transitions = letterMap.get(ch) ?? [];
for (const to of transitions) {
this.transition(from, to, true);
this.transition(from, to, ch);
}
if (epsilon_transitions.length == 0 && transitions.length == 0){
this.rejected.push(from);
}
}
return this.status();
}
status(): SimStepResult {
if (this.accepted.length !== 0) return "accept";
if (this.paths.length === 0) return "reject";
return "pending";

View file

@ -7,6 +7,7 @@ import type {
import { SimStepResult } from "../simulation.ts";
export type Step = TmTransTo & {from_state: State, from_symbol: Symbol}
export type TmState = {
readonly state: State;
readonly tape: Symbol[];
@ -15,7 +16,7 @@ export type TmState = {
readonly accepted: boolean;
readonly repr: string;
readonly path: readonly TmTransTo[];
readonly path: readonly Step[];
}
@ -69,7 +70,7 @@ export class TmSim {
state: to.state,
accepted: this.machine.final_states.has(to.state),
path: from.path.concat([to]),
path: from.path.concat([{from_state: from.state, from_symbol: from.tape[from.head], ...to}]),
};
switch (to.direction) {
@ -102,8 +103,6 @@ export class TmSim {
}
step(): SimStepResult {
if (this.accepted.length != 0) return "accept";
if (this.paths.length == 0) return "reject";
const paths: TmState[] = this.paths;
this.paths = [];
@ -122,8 +121,12 @@ export class TmSim {
}
}
if (this.accepted.length != 0) return "accept";
if (this.paths.length == 0) return "reject";
return this.status();
}
status(): SimStepResult {
if (this.accepted.length !== 0) return "accept";
if (this.paths.length === 0) return "reject";
return "pending";
}
}

View file

@ -307,9 +307,7 @@ function createGraph(): vis.Network {
});
network.on('deselectEdge', item => {
console.log(item);
for (const edge of item.previousSelection.edges){
console.log(edge);
dehighlight_from_edge_id(edge.id)
}
});
@ -320,9 +318,7 @@ function createGraph(): vis.Network {
});
network.on('deselectNode', item => {
console.log(item);
for (const node of item.previousSelection.nodes){
console.log(node);
dehighlight_from_node_id(node.id)
}
});

147
web/root/style/paths.scss Normal file
View file

@ -0,0 +1,147 @@
/* ===== Panel ===== */
.panel {
border: 1px solid var(--separator-bg);
border-radius: var(--radius-lg);
background: var(--bg-1);
overflow: hidden;
}
.panelTitle {
cursor: pointer;
list-style: none;
padding: var(--space-3) var(--space-4);
font-weight: 700;
color: var(--fg-0);
background: var(--bg-2);
border-bottom: 1px solid var(--separator-bg);
}
.panelTitle::-webkit-details-marker {
display: none;
}
/* ===== Layout ===== */
.pathsGrid {
display: grid;
gap: var(--space-3);
}
/* ===== Groups ===== */
.group {
border: 1px solid var(--separator-bg);
border-radius: var(--radius-md);
background: var(--bg-1);
overflow: hidden;
}
.groupTitle {
cursor: pointer;
list-style: none;
padding: var(--space-2) var(--space-3);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-2);
font-weight: 600;
color: var(--fg-0);
background: var(--bg-2);
}
.groupTitle::-webkit-details-marker {
display: none;
}
.count {
font-size: 12px;
padding: 2px 8px;
border-radius: 999px;
color: var(--fg-muted);
background: var(--bg-1);
border: 1px solid var(--separator-bg);
}
.groupBody {
padding: var(--space-3);
display: grid;
gap: var(--space-2);
}
/* ===== Path cards ===== */
.pathItem {
border: 1px solid var(--separator-bg);
border-radius: var(--radius-md);
background: var(--bg-1);
overflow: hidden;
}
.pathHeader {
cursor: pointer;
list-style: none;
padding: var(--space-2) var(--space-3);
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--space-2);
color: var(--fg-0);
background: var(--bg-2);
}
.pathHeader::-webkit-details-marker {
display: none;
}
.pathMeta {
display: inline-flex;
gap: var(--space-2);
align-items: center;
font-size: 12px;
color: var(--fg-muted);
}
/* ===== Steps ===== */
.steps {
padding: var(--space-2) var(--space-3) var(--space-3);
display: grid;
gap: var(--space-1);
}
.stepLine {
font: 500 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
color: var(--fg-1);
background: var(--bg-2);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
// overflow-x: auto;
white-space: nowrap;
}
/* ===== Semantic accents ===== */
.group-accepted {
border-color: color-mix(in srgb, var(--success) 35%, var(--separator-bg));
}
.group-running {
border-color: color-mix(in srgb, var(--accent) 35%, var(--separator-bg));
}
.group-rejected {
border-color: color-mix(in srgb, var(--error) 35%, var(--separator-bg));
}

View file

@ -3,6 +3,7 @@
@use "loading.scss";
@use "controls.scss";
@use "themes.scss";
@use "paths.scss";
html,
body {
@ -34,6 +35,10 @@ body {
overflow-y: scroll;
}
.scroll{
overflow: scroll;
}
.flexCol{
display: flex;
flex-direction: column;