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