diff --git a/automata/src/loader/lexer.rs b/automata/src/loader/lexer.rs
index 4023531..183ecde 100644
--- a/automata/src/loader/lexer.rs
+++ b/automata/src/loader/lexer.rs
@@ -96,7 +96,7 @@ fn begin_ident(c: char) -> bool {
}
fn continue_ident(c: char) -> bool {
- c.is_alphanumeric() || c == '_' || (!c.is_ascii() && !c.is_control() && !c.is_whitespace())
+ c.is_alphanumeric() || c == '_' || c=='\'' || (!c.is_ascii() && !c.is_control() && !c.is_whitespace())
}
impl<'a> std::iter::Iterator for Lexer<'a> {
diff --git a/web/root/index.html b/web/root/index.html
index 7964d62..f25eb13 100644
--- a/web/root/index.html
+++ b/web/root/index.html
@@ -28,10 +28,24 @@
-
-
+
+
+
+
+
+
+
+
+
diff --git a/web/root/src/automata.ts b/web/root/src/automata.ts
index c8c8517..685edde 100644
--- a/web/root/src/automata.ts
+++ b/web/root/src/automata.ts
@@ -19,11 +19,19 @@ export function machine_from_json(json: string): Machine {
}
machine.edges = new Map();
+ machine.transitions_components = new Map();
switch (machine.type) {
case "fa":
{
for (const [from, tos] of machine.transitions) {
for (const to of tos) {
+ const layer_0 = machine.transitions_components;
+ if(!layer_0.has(from.state)) layer_0.set(from.state, new Map());
+ const layer_1 = machine.transitions_components.get(from.state)!;
+ if(!layer_1.has(from.letter)) layer_1.set(from.letter, []);
+ const layer_2 = layer_1.get(from.letter)!;
+ layer_2.push(to);
+
const edge = from.state + "#" + to.state;
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
machine.edges.get(edge)?.push({
@@ -40,6 +48,15 @@ export function machine_from_json(json: string): Machine {
machine.symbols = new Map(Object.entries(machine.symbols));
for (const [from, tos] of machine.transitions) {
for (const to of tos) {
+ const layer_0 = machine.transitions_components;
+ if(!layer_0.has(from.state)) layer_0.set(from.state, new Map());
+ const layer_1 = machine.transitions_components.get(from.state)!;
+ if(!layer_1.has(from.symbol)) layer_1.set(from.symbol, new Map());
+ const layer_2 = layer_1.get(from.symbol)!;
+ if(!layer_2.has(from.letter)) layer_2.set(from.letter, []);
+ const layer_3 = layer_2.get(from.letter)!;
+ layer_3.push(to);
+
const edge = from.state + "#" + to.state;
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
machine.edges.get(edge)?.push({
@@ -56,6 +73,13 @@ export function machine_from_json(json: string): Machine {
machine.symbols = new Map(Object.entries(machine.symbols));
for (const [from, tos] of machine.transitions) {
for (const to of tos) {
+ const layer_0 = machine.transitions_components;
+ if(!layer_0.has(from.state)) layer_0.set(from.state, new Map());
+ const layer_1 = machine.transitions_components.get(from.state)!;
+ if(!layer_1.has(from.symbol)) layer_1.set(from.symbol, []);
+ const layer_2 = layer_1.get(from.symbol)!;
+ layer_2.push(to);
+
const edge = from.state + "#" + to.state;
if (!machine.edges.has(edge)) machine.edges.set(edge, []);
machine.edges.get(edge)?.push({
@@ -108,6 +132,7 @@ export type Fa = {
final_states: Map;
transitions: Map;
+ transitions_components: Map>;
edges: Map;
};
@@ -137,6 +162,7 @@ export type Pda = {
final_states: Map | null;
transitions: Map;
+ transitions_components: Map>>;
edges: Map;
};
@@ -166,6 +192,8 @@ export type Tm = {
final_states: Map;
transitions: Map;
+ transitions_components: Map>;
edges: Map;
};
+
diff --git a/web/root/src/editor.ts b/web/root/src/editor.ts
index a45ccef..344ca61 100644
--- a/web/root/src/editor.ts
+++ b/web/root/src/editor.ts
@@ -22,6 +22,8 @@ import { terminalPlugin } from "./terminal.ts";
import { setAutomaton } from "./visualizer.ts";
import { machine_from_json } from "./automata.ts";
+import { sharedText } from "./share.ts";
+import { examples } from "./examples.ts";
function tokenize(text: string) {
@@ -43,38 +45,21 @@ function compile(text: string): wasm.CompileResult {
}
}
-const tokenClass = (t: string) =>
-({
- 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");
-
-
-function severityClass(sev: string) {
- 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: string) {
- if (sev === "error") return 3;
- if (sev === "warning") return 2;
- return 1;
-}
-
+export 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),
+});
function buildAnalysis(text: string, doc: Text) {
+ save(text);
const tokens = tokenize(text);
const { log, log_formatted, graph } = compile(text);
@@ -86,7 +71,6 @@ function buildAnalysis(text: string, doc: Text) {
}
}
- // Build ONE Decoration set: syntax + diagnostics
const marks = [];
const docLen = doc.length;
@@ -120,18 +104,35 @@ function buildAnalysis(text: string, doc: Text) {
return { tokens, log, log_formatted, deco };
}
-export 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),
-});
+const tokenClass = (t: string) =>
+({
+ 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");
+
+
+function severityClass(sev: string) {
+ 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: string) {
+ if (sev === "error") return 3;
+ if (sev === "warning") return 2;
+ return 1;
+}
// ===================== Hover tooltip (uses cached diags) =====================
const diagHover = hoverTooltip((view, pos) => {
@@ -169,32 +170,24 @@ const diagHover = hoverTooltip((view, pos) => {
};
});
+function save(text: string){
+ globalThis.localStorage.save = text;
+}
-const initialText = `type=NPDA
-Q = {q0, q1} // states
-E = {a, b} // alphabet
-T = {z0, A, B} // stack
-q0 = q0
-z0 = z0
+function getSaved(): string | undefined{
+ return globalThis.localStorage.save;
+}
-// 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]) }
+export function setText(text: string){
+ editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: text } });
+}
-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) }
-`;
+export function getText(): string{
+ return editor.state.doc.toString()
+}
const state = EditorState.create({
- doc: initialText,
+ doc: sharedText() ?? getSaved() ?? examples[0].machine,
extensions: [
lineNumbers(),
highlightActiveLineGutter(),
diff --git a/web/root/src/examples.ts b/web/root/src/examples.ts
new file mode 100644
index 0000000..7987d8e
--- /dev/null
+++ b/web/root/src/examples.ts
@@ -0,0 +1,215 @@
+import { setText } from "./editor.ts";
+
+export type Category =
+ | "Tutorial"
+ | "DFA"
+ | "NFA"
+ | "DPDA"
+ | "NPDA"
+ | "TM"
+ | "NTM";
+
+export class Example {
+ category: Category;
+ title: string;
+ machine: string;
+
+ constructor(category: Category, title: string, machine: string) {
+ this.category = category;
+ this.title = title;
+ this.machine = machine;
+ }
+}
+
+export const examples: Example[] = [
+ new Example(
+ "Tutorial",
+ "DFA",
+ `// strings over a,b which start and end with different letters
+
+type = DFA // type of machine DFA, NFA, DPDA, NPDA, DTM, NTM
+Q = {q0, qa, qa', qb, qb'} // set of states
+E = {a, b} // alphabet
+F = {qa', qb'} // set of final states
+q0 = q0 // initial state
+
+// transition function (state, letter) -> state
+d(q0, a) = qa
+d(q0, b) = qb
+
+d(qa, a) = qa
+d(qa, b) = qa'
+
+d(qa', a) = qa
+d(qa', b) = qa'
+
+d(qb, a) = qb'
+d(qb, b) = qb
+
+d(qb', a) = qb'
+d(qb', b) = qb`,
+ ),
+
+ new Example(
+ "DFA",
+ "modulo",
+ `type=DFA
+E={1,2,3}
+Q={q0, q1, q2, q3, q4}
+F = {q0}
+q0=q0
+
+d(q0, 1) = q1
+d(q1, 1) = q2
+d(q2, 1) = q3
+d(q3, 1) = q4
+d(q4, 1) = q0
+
+d(q0, 2) = q2
+d(q1, 2) = q3
+d(q2, 2) = q4
+d(q3, 2) = q0
+d(q4, 2) = q1
+
+d(q0, 3) = q3
+d(q1, 3) = q4
+d(q2, 3) = q0
+d(q3, 3) = q1
+d(q4, 3) = q2`,
+ ),
+
+ new Example(
+ "DPDA",
+ "unequal",
+ `type=DPDA
+Q = {q0, qas, qeq, qmb, qlb} // states
+E = {a, b} // alphabet
+T = {z0, A} // stack
+F = {qmb, qlb} // final states
+q0 = q0
+z0 = z0
+
+d(q0, a, z0) = (qas, z0)
+
+d(qas, a, z0) = (qas, [A z0])
+d(qas, b, z0) = (qeq, z0)
+d(qas, a, A) = (qas, [A A])
+d(qas, b, A) = (qlb, ~)
+
+d(qlb, b, A) = (qeq, ~)
+d(qlb, b, z0) = (qeq, z0)
+
+d(qeq, b, z0) = (qmb, z0)
+
+d(qmb, b, z0) = (qmb, z0)`,
+ ),
+
+ new Example(
+ "NPDA",
+ "unequal",
+ `type=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 CATEGORY_ORDER: Category[] = [
+ "Tutorial",
+ "DFA",
+ "NFA",
+ "DPDA",
+ "NPDA",
+ "TM",
+ "NTM",
+];
+
+function buildExamplesDropdown(
+ selectEl: HTMLSelectElement,
+ examples: Example[],
+ onPick?: (ex: Example) => void,
+) {
+ // Clear everything except the first placeholder option (if present)
+ const keepFirstPlaceholder = selectEl.options.length > 0 &&
+ selectEl.options[0].disabled && selectEl.options[0].value === "";
+
+ selectEl.innerHTML = "";
+ if (keepFirstPlaceholder) {
+ const placeholder = document.createElement("option");
+ placeholder.value = "";
+ placeholder.disabled = true;
+ placeholder.selected = true;
+ placeholder.textContent = "Choose an example…";
+ selectEl.appendChild(placeholder);
+ }
+
+ // Group examples by category
+ const grouped = new Map();
+ for (const ex of examples) {
+ if (!grouped.has(ex.category)) grouped.set(ex.category, []);
+ grouped.get(ex.category)!.push(ex);
+ }
+
+ // Optional: sort titles within each group
+ for (const [cat, list] of grouped) {
+ list.sort((a, b) => a.title.localeCompare(b.title));
+ grouped.set(cat, list);
+ }
+
+ // Create optgroups in your preferred order (and then any extras)
+ const categoriesToRender: Category[] = [
+ ...CATEGORY_ORDER.filter((c) => grouped.has(c)),
+ ...Array.from(grouped.keys()).filter((c) => !CATEGORY_ORDER.includes(c))
+ .sort(),
+ ];
+
+ // We'll store a stable reference via an index into the examples array
+ // (simplest + avoids encoding large machine strings into