From 2e6330196db0137207b75a78ca5935c2f39fa99f Mon Sep 17 00:00:00 2001 From: Parker TenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Sun, 11 Jan 2026 11:40:54 -0500 Subject: [PATCH 1/2] organizing frontend --- web/root/src/bus.ts | 16 ++-- web/root/src/controls.ts | 162 +++++++++++++++++-------------------- web/root/src/editor.ts | 94 ++++++++++++--------- web/root/src/examples.ts | 6 +- web/root/src/share.ts | 74 +++++++++-------- web/root/src/simulation.ts | 14 ++-- web/root/src/visualizer.ts | 20 +++-- 7 files changed, 203 insertions(+), 183 deletions(-) diff --git a/web/root/src/bus.ts b/web/root/src/bus.ts index 351e6e8..1c56cd0 100644 --- a/web/root/src/bus.ts +++ b/web/root/src/bus.ts @@ -1,6 +1,7 @@ // deno-lint-ignore-file import type { Machine } from "./automata.ts"; +import type { Example } from "./examples.ts"; import type { Sim, SimStepResult } from "./simulation.ts"; import type wasm from "./wasm.ts"; import type { Text } from "npm:@codemirror/state"; @@ -73,13 +74,16 @@ type AppEvents = { "automata/sim/after_step": { simulation: Sim, result: SimStepResult }; "automata/update": { automaton: Machine }; - "controls/physics": {enabled: boolean}, - "controls/reset_network": void, - + "example/selected": {example: Example}; - "controls/step_simulation": void, - "controls/reload_simulation": void, - "controls/clear_simulation": void, + "controls/editor/set_text": {text: string}; + + "controls/vis/physics": {enabled: boolean}; + "controls/vis/reset_network": void; + + "controls/sim/step": void; + "controls/sim/reload": void; + "controls/sim/clear": void; "theme/update": void; }; diff --git a/web/root/src/controls.ts b/web/root/src/controls.ts index ef8a8c0..16980f3 100644 --- a/web/root/src/controls.ts +++ b/web/root/src/controls.ts @@ -15,59 +15,6 @@ const speedLabel = document.getElementById("speedSimLabel") as HTMLSpanElement; const reloadSimBtn = document.getElementById("reloadSim") as HTMLButtonElement; const clearSimBtn = document.getElementById("clearSim") as HTMLButtonElement; -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"); - bus.emit("controls/physics", { enabled }); -}; - -bus.emit("controls/physics", { - enabled: togglePhysicsBtn.classList.contains("active"), -}); - -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(); -}); - -bus.on("automata/sim/after_step", ({ result }) => { - if (result !== "pending") { - if (running) setRunning(false); - } -}); - -let simulation_active = false; -let running = false; -let timer: number | null = null; - // speed slider is "steps per second" function getStepsPerSecond() { return Math.max(1, Math.min(60, Number(speedSlider.value) || 10)); @@ -77,39 +24,82 @@ function updateSpeedUI() { } updateSpeedUI(); -speedSlider.addEventListener("input", () => { - updateSpeedUI(); - if (running) restartTimer(); -}); +class Controls { + static simulation_active = false; + static running = false; + static timer: number | null = null; -function stopTimer() { - if (timer !== null) { - clearInterval(timer); - timer = null; + static updateButtons() { + stepBtn.disabled = !Controls.simulation_active || Controls.running; + playPauseBtn.disabled = !Controls.simulation_active; + clearSimBtn.disabled = !Controls.simulation_active; + } + static setRunning(on: boolean) { + Controls.running = on; + playPauseBtn.textContent = Controls.running ? "⏸ Pause" : "▶ Play"; + playPauseBtn.classList.toggle("btn-primary", !Controls.running); + playPauseBtn.classList.toggle("btn-secondary", Controls.running); + + if (Controls.running) Controls.restartTimer(); + else Controls.stopTimer(); + Controls.updateButtons(); + } + static stop() { + if (Controls.running) Controls.setRunning(false); + } + static stopTimer() { + if (Controls.timer !== null) { + clearInterval(Controls.timer); + Controls.timer = null; + } + } + + static restartTimer() { + Controls.stopTimer(); + const sps = getStepsPerSecond(); + const intervalMs = Math.round(1000 / sps); + + Controls.timer = globalThis.window.setInterval(() => { + bus.emit("controls/sim/step", undefined); + }, intervalMs); + } + + static { + speedSlider.addEventListener("input", () => { + updateSpeedUI(); + if (Controls.running) Controls.restartTimer(); + }); + playPauseBtn.onclick = () => Controls.setRunning(!Controls.running); + resetLayoutBtn.onclick = () => + bus.emit("controls/vis/reset_network", undefined); + clearSimBtn.onclick = () => bus.emit("controls/sim/clear", undefined); + stepBtn.onclick = () => bus.emit("controls/sim/step", undefined); + reloadSimBtn.onclick = () => bus.emit("controls/sim/reload", undefined); + togglePhysicsBtn.onclick = () => { + const enabled = !togglePhysicsBtn.classList.contains("active"); + bus.emit("controls/vis/physics", { enabled }); + }; + + bus.on("controls/vis/physics", ({ enabled }) => { + togglePhysicsBtn.classList.toggle("active", enabled); + togglePhysicsBtn.textContent = enabled ? "Physics: ON" : "Physics: OFF"; + }); + + bus.on("controls/sim/reload", (_) => { + if (Controls.running) Controls.setRunning(false); + }); + + bus.on("automata/sim/update", ({ simulation }) => { + Controls.simulation_active = !!simulation; + if (!simulation) Controls.stop(); + }); + + bus.on("automata/sim/after_step", ({ result }) => { + if (result !== "pending") Controls.stop(); + }); + + bus.emit("controls/vis/physics", { + enabled: togglePhysicsBtn.classList.contains("active"), + }); } } - -function restartTimer() { - stopTimer(); - const sps = getStepsPerSecond(); - const intervalMs = Math.round(1000 / sps); - - timer = globalThis.window.setInterval(() => { - bus.emit("controls/step_simulation", undefined); - }, intervalMs); -} - -function setRunning(on: boolean) { - running = on; - playPauseBtn.textContent = running ? "⏸ Pause" : "▶ Play"; - playPauseBtn.classList.toggle("btn-primary", !running); - playPauseBtn.classList.toggle("btn-secondary", running); - - // Disable step while running (optional, but feels nice) - stepBtn.disabled = running; - - if (running) restartTimer(); - else stopTimer(); -} - -playPauseBtn.onclick = () => setRunning(!running); diff --git a/web/root/src/editor.ts b/web/root/src/editor.ts index e9d925f..d3594be 100644 --- a/web/root/src/editor.ts +++ b/web/root/src/editor.ts @@ -1,67 +1,71 @@ // deno-lint-ignore-file import { - EditorView, - keymap, - hoverTooltip, Decoration, - lineNumbers, + EditorView, + highlightActiveLine, highlightActiveLineGutter, - highlightActiveLine + hoverTooltip, + keymap, + lineNumbers, } from "npm:@codemirror/view"; import { EditorState, StateField, Text } from "npm:@codemirror/state"; -import { defaultKeymap, history, historyKeymap } from "npm:@codemirror/commands"; +import { + defaultKeymap, + history, + historyKeymap, +} from "npm:@codemirror/commands"; import { bracketMatching, indentOnInput } from "npm:@codemirror/language"; import { closeBrackets } from "npm:@codemirror/autocomplete"; +import wasm from "./wasm.ts"; -import wasm from "./wasm.ts" - -import { sharedText } from "./share.ts"; +import { Share } from "./share.ts"; import { examples } from "./examples.ts"; import { bus } from "./bus.ts"; - -function tokenize(text: string) { +function tokenize(text: string): wasm.Tok[] { try { return wasm.lex(text); } catch (e) { - console.log(e) - return [] + console.log(e); + return []; } } -function compile(text: string): wasm.CompileResult { +function compile( + text: string, +): { log: wasm.CompileLog[]; ansi_log: string; machine: string | undefined } { try { return wasm.compile(text); } catch (e) { console.log(e); - // @ts-expect-error wasm defines extra cleanup - return {log: [], log_formatted: "", graph: ""}; + return { log: [], ansi_log: "", machine: "" }; } } const eventBusConnection = StateField.define({ create(state) { const text = state.doc.toString(); - bus.emit("editor/change", {text, doc: state.doc}); + 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}); + 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, ansi_log, machine } = compile(text); - bus.emit("compiled", {log, ansi_log, machine}) + bus.emit("compiled", { log, ansi_log, machine }); const marks = []; const docLen = doc.length; @@ -88,7 +92,9 @@ function buildAnalysis(text: string, doc: Text) { 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)); + if (end > start) { + marks.push(Decoration.mark({ class: cls }).range(start, end)); + } } } @@ -96,8 +102,7 @@ function buildAnalysis(text: string, doc: Text) { return { tokens, log, ansi_log, deco }; } -const tokenClass = (t: string) => -({ +const tokenClass = (t: string) => ({ comment: "tok-comment", keyword: "tok-keyword", error: "tok-error", @@ -113,7 +118,6 @@ const tokenClass = (t: string) => rbracket: "rb-", }[t] || "tok-ident"); - function severityClass(sev: string) { const s = (sev || "error").toLowerCase(); if (s === "warning") return "cm-diag-warning"; @@ -129,10 +133,16 @@ function sevRank(sev: string) { // ===================== Hover tooltip (uses cached diags) ===================== const diagHover = hoverTooltip((view, pos) => { const { log } = view.state.field(eventBusConnection); - const hits = log.filter((d) => d.start !== undefined && d.end !== undefined && pos >= d.start && pos <= d.end); + const hits = log.filter((d) => + d.start !== undefined && d.end !== undefined && 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]); + const top = hits.reduce( + (a, b) => (sevRank(b.level) > sevRank(a.level) ? b : a), + hits[0], + ); return { pos, @@ -144,8 +154,9 @@ const diagHover = hoverTooltip((view, pos) => { const title = document.createElement("div"); title.className = `tipTitle ${top.level}`; - title.textContent = - hits.length === 1 ? top.level.toUpperCase() : `${top.level.toUpperCase()} (${hits.length})`; + title.textContent = hits.length === 1 + ? top.level.toUpperCase() + : `${top.level.toUpperCase()} (${hits.length})`; const body = document.createElement("div"); body.className = "tipBody"; @@ -162,24 +173,20 @@ const diagHover = hoverTooltip((view, pos) => { }; }); -function save(text: string){ +function save(text: string) { globalThis.localStorage.save = text; } -function getSaved(): string | undefined{ +function getSaved(): string | undefined { return globalThis.localStorage.save; } -export function setText(text: string){ - editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: text } }); -} - -export function getText(): string{ - return editor.state.doc.toString() +function defaultText(): string { + return Share.sharedText() ?? getSaved() ?? examples[0].machine; } const state = EditorState.create({ - doc: "", + doc: defaultText(), extensions: [ lineNumbers(), highlightActiveLineGutter(), @@ -202,4 +209,17 @@ const editor = new EditorView({ parent: document.getElementById("editor")!, }); -bus.on("begin", _ => setText(sharedText() ?? getSaved() ?? examples[0].machine)) \ No newline at end of file +bus.on( + "begin", + (_) => bus.emit("controls/editor/set_text", { text: defaultText() }), +); + +bus.on("controls/editor/set_text", ({ text }) => { + editor.dispatch({ + changes: { from: 0, to: editor.state.doc.length, insert: text }, + }); +}); + +bus.on("example/selected", ({ example }) => { + bus.emit("controls/editor/set_text", { text: example.machine }); +}); diff --git a/web/root/src/examples.ts b/web/root/src/examples.ts index eb9234b..6745845 100644 --- a/web/root/src/examples.ts +++ b/web/root/src/examples.ts @@ -1,4 +1,4 @@ -import { setText } from "./editor.ts"; +import { bus } from "./bus.ts"; export type Category = | "Tutorial" @@ -241,6 +241,6 @@ function buildExamplesDropdown( } const selectEl = document.getElementById("exampleSelect") as HTMLSelectElement; -buildExamplesDropdown(selectEl, examples, (picked) => { - setText(picked.machine); +buildExamplesDropdown(selectEl, examples, (example) => { + bus.emit("example/selected", {example}); }); diff --git a/web/root/src/share.ts b/web/root/src/share.ts index d9eaba4..715b3ce 100644 --- a/web/root/src/share.ts +++ b/web/root/src/share.ts @@ -1,41 +1,49 @@ -import { getText } from "./editor.ts"; +import { bus } from "./bus.ts"; -const btn = document.getElementById("shareBtn")!; -const toast = document.getElementById("shareToast")!; +export class Share { + private static readonly btn: HTMLButtonElement = document.getElementById( + "shareBtn", + )! as HTMLButtonElement; + private static readonly toast: HTMLElement = document.getElementById( + "shareToast", + )!; -function generateShareLink() { - return `${globalThis.window.location.href}?share=${encodeURIComponent(btoa(getText()))}`; -} + private static docText: string; + private static shareText: string; -async function copy(text: string) { - await navigator.clipboard.writeText(text); -} + static { + bus.on("editor/change", ({ text }) => Share.docText = text); -btn.addEventListener("click", async () => { - await copy(generateShareLink()); + Share.btn.onclick = async (_) => { + const link = `${globalThis.window.location.href}?share=${ + encodeURIComponent(btoa(Share.docText)) + }`; + await navigator.clipboard.writeText(link); - toast.classList.remove("show"); - void toast.offsetWidth; - toast.classList.add("show"); -}); + Share.toast.classList.remove("show"); + void Share.toast.offsetWidth; + Share.toast.classList.add("show"); + }; - -export function sharedText(): string|null { - try{ - const url = new URL(globalThis.window.location.href); - let text: string | null = url.searchParams.get("share"); - if (text !== null) { - text = atob(text); - url.searchParams.delete("share"); - globalThis.window.history.replaceState( - {}, - document.title, - url.pathname + url.search + url.hash - ); + try { + const url = new URL(globalThis.window.location.href); + let text: string | null = url.searchParams.get("share"); + if (text !== null) { + text = atob(text); + url.searchParams.delete("share"); + globalThis.window.history.replaceState( + {}, + document.title, + url.pathname + url.search + url.hash, + ); + Share.shareText = text; + } + } catch (e) { + console.log(e); } - return text; - }catch(e){ - console.log(e) } - return null; -} \ No newline at end of file + + public static sharedText(): string | null { + return Share.shareText; + } +} diff --git a/web/root/src/simulation.ts b/web/root/src/simulation.ts index 083885f..7e78a02 100644 --- a/web/root/src/simulation.ts +++ b/web/root/src/simulation.ts @@ -1,13 +1,13 @@ import { bus } from "./bus.ts"; -import { +import type { Fa, Machine, - parse_machine_from_json, Pda, State, Symbol, Tm, } from "./automata.ts"; +import {parse_machine_from_json} from "./automata.ts"; export type SimStepResult = "pending" | "accept" | "reject"; export type Sim = FaSim | PdaSim | TmSim; @@ -26,7 +26,7 @@ let automaton: Machine = { bus.on("compiled", ({ machine }) => { if (machine) { try { - bus.emit("controls/clear_simulation", undefined); + bus.emit("controls/sim/clear", undefined); automaton = parse_machine_from_json(machine); bus.emit("automata/update", { automaton }); } catch (e) { @@ -34,11 +34,11 @@ bus.on("compiled", ({ machine }) => { } } }); -bus.on("controls/clear_simulation", (_) => { +bus.on("controls/sim/clear", (_) => { simulation = null; bus.emit("automata/sim/update", { simulation: null }); }); -bus.on("controls/step_simulation", (_) => { +bus.on("controls/sim/step", (_) => { if (simulation) { bus.emit("automata/sim/before_step", { simulation }); bus.emit("automata/sim/after_step", { @@ -51,10 +51,10 @@ 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.emit("controls/sim/reload", undefined) } }); -bus.on("controls/reload_simulation", (_) => { +bus.on("controls/sim/reload", (_) => { const input = machineInput.value; switch (automaton.type) { case "fa": diff --git a/web/root/src/visualizer.ts b/web/root/src/visualizer.ts index 8c34b15..0c4bae0 100644 --- a/web/root/src/visualizer.ts +++ b/web/root/src/visualizer.ts @@ -7,11 +7,11 @@ import type { Sim } from "./simulation.ts"; import type { Machine } from "./automata.ts"; -bus.on("controls/physics", ({enabled}) => { +bus.on("controls/vis/physics", ({enabled}) => { network.setOptions({ physics: { enabled } }); network.setOptions({edges: {smooth: enabled}}); }); -bus.on("controls/reset_network", _ => { +bus.on("controls/vis/reset_network", _ => { try { nodes.forEach((n) => { n.physics = true; @@ -285,17 +285,16 @@ function createGraph(): vis.Network { nodes: { shape: "custom", size: 18, - // // @ts-expect-error bad library - // chosen: { - // node: chosen_node, - // }, // @ts-expect-error bad library + chosen: { + node: chosen_node, + }, ctxRenderer: renderNode, }, edges: { chosen: { - // // @ts-expect-error bad library - // edge: chosen_edge, + // @ts-expect-error bad library + edge: chosen_edge, }, arrowStrikethrough: false, arrows: "to", @@ -304,9 +303,8 @@ function createGraph(): vis.Network { ); vis.DataSet; - network.on("doubleClick", (params: any) => { + network.on("doubleClick", (params: {nodes: string[]}) => { for (const node_id of params.nodes) { - // @ts-expect-error bad library const node: vis.Node = nodes.get(node_id)!; node.physics = !node.physics; nodes.update(node); @@ -325,7 +323,7 @@ function renderNode({ state: { selected, hover }, style, label, -}: {ctx: CanvasRenderingContext2D, id: string, x: number, y: number, state: {selected: boolean, hover: boolean}, style: any, label: string}) { +}: {ctx: CanvasRenderingContext2D, id: string, x: number, y: number, state: {selected: boolean, hover: boolean}, style: vis.NodeOptions, label: string}) { return { drawNode() { const t = getGraphTheme(); From 90d96f07383d59630fbd571c9451a500c96104be Mon Sep 17 00:00:00 2001 From: Parker TenBroeck <51721964+ParkerTenBroeck@users.noreply.github.com> Date: Sun, 11 Jan 2026 15:58:23 -0500 Subject: [PATCH 2/2] improving error messages --- automata/src/automatan/fa.rs | 555 +++++++++++++++++++++------------- automata/src/automatan/mod.rs | 12 +- automata/src/automatan/pda.rs | 43 ++- automata/src/automatan/tm.rs | 35 ++- automata/src/lib.rs | 34 +++ automata/src/loader/ast.rs | 32 +- automata/src/loader/log.rs | 264 +++++++++------- automata/src/loader/mod.rs | 108 ++++--- automata/src/loader/parser.rs | 9 +- web_lib/src/lib.rs | 23 +- 10 files changed, 680 insertions(+), 435 deletions(-) diff --git a/automata/src/automatan/fa.rs b/automata/src/automatan/fa.rs index e5fb674..66f4989 100644 --- a/automata/src/automatan/fa.rs +++ b/automata/src/automatan/fa.rs @@ -2,261 +2,377 @@ use std::collections::HashSet; use super::*; -use crate::loader::{ - Context, DELTA_LOWER, GAMMA_UPPER, SIGMA_UPPER, Spanned, - ast::{self, Symbol as Sym}, +use crate::{ + delta_lower, dual_struct_serde, epsilon, loader::{ + Context, INITIAL_STATE, Spanned, + ast::{self, Symbol as Sym, TopLevel}, + log::LogSink, + }, sigma_upper }; -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -pub struct TransitionFrom<'a> { - pub state: State<'a>, - pub letter: Option>, +dual_struct_serde! { + #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] + pub struct TransitionFrom<'a> { + #[serde(borrow)] + pub state: State<'a>, + pub letter: Option>, + } } -#[derive(Debug, PartialEq, Eq, Clone, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -pub struct TransitionTo<'a> { - pub state: State<'a>, +dual_struct_serde! { + #[derive(Debug, PartialEq, Eq, Clone, Hash)] + pub struct TransitionTo<'a> { + #[serde(borrow)] + pub state: State<'a>, - pub transition: Span, - pub function: Span, + pub transition: Span, + pub function: Span, + } } -#[derive(Clone, Debug)] -#[allow(unused)] -#[cfg_attr(feature = "serde", serde_with::serde_as)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -pub struct Fa<'a> { - pub initial_state: State<'a>, - pub states: HashMap, StateInfo>, - pub alphabet: HashMap, LetterInfo>, - pub final_states: HashMap, StateInfo>, - - #[cfg(feature = "serde")] - #[serde_as(as = "serde_with::Seq<(_, _)>")] - pub transitions: HashMap, HashSet>>, - #[cfg(not(feature = "serde"))] - pub transitions: HashMap, HashSet>>, +dual_struct_serde! { {#[serde_with::serde_as]} + #[derive(Clone, Debug)] + pub struct Fa<'a> { + #[serde(borrow)] + pub initial_state: State<'a>, + + #[serde(borrow)] + pub states: HashMap, StateInfo>, + + #[serde(borrow)] + pub alphabet: HashMap, LetterInfo>, + + #[serde(borrow)] + pub final_states: HashMap, StateInfo>, + + #[serde(borrow)] + #[serde_as(as = "serde_with::Seq<(_, _)>")] + pub transitions: HashMap, HashSet>>, + } } impl<'a> Fa<'a> { - pub fn parse( + pub fn compile( items: impl Iterator>>, ctx: &mut Context<'a>, options: Options, ) -> Option> { + FaCompiler::new(ctx, options).compile(items) + } +} - let mut initial_state = None; +pub struct FaCompiler<'a, 'b> { + ctx: &'b mut Context<'a>, + options: Options, - let mut states = HashMap::new(); - let mut alphabet = HashMap::new(); - let mut final_states = HashMap::new(); + initial_state: Option<(State<'a>, Span)>, - let mut transitions: HashMap, HashSet>> = - HashMap::new(); + states: HashMap, StateInfo>, + states_def: Option, + alphabet: HashMap, LetterInfo>, + alphabet_def: Option, + + final_states: HashMap, StateInfo>, + final_states_def: Option, + + transitions: HashMap, HashSet>>, +} + +impl<'a, 'b> FaCompiler<'a, 'b> { + pub fn new(ctx: &'b mut Context<'a>, options: Options) -> Self { + Self { + ctx, + options, + + initial_state: Default::default(), + states: Default::default(), + states_def: Default::default(), + alphabet: Default::default(), + alphabet_def: Default::default(), + final_states: Default::default(), + final_states_def: Default::default(), + transitions: Default::default(), + } + } + + pub fn compile( + mut self, + items: impl Iterator>>, + ) -> Option> { for Spanned(element, span) in items { - use Spanned as S; - use ast::TopLevel as TL; - match element { - TL::Item(S("Q", _), list) => { - if !states.is_empty() { - ctx.emit_error("states already set", span); - } - let Some(list) = list.expect_set(ctx) else { - continue; - }; - for item in list { - let Some(ident) = item.expect_ident(ctx) else { - continue; - }; - if states - .insert(State(ident), StateInfo { definition: item.1 }) - .is_some() - { - ctx.emit_error("state redefined", item.1); - } - } - - if list.is_empty() { - ctx.emit_error("states cannot be empty", span); - } - } - TL::Item(S("E" | SIGMA_UPPER | "sigma", _), list) => { - if !alphabet.is_empty() { - ctx.emit_error("alphabet already set", span); - } - let Some(list) = list.expect_set(ctx) else { - continue; - }; - for item in list { - let Some(ident) = item.expect_ident(ctx) else { - continue; - }; - - if ident.chars().count() != 1 { - ctx.emit_error("letter cannot be longer than one char", item.1); - } - - if alphabet - .insert(Letter(ident), LetterInfo { definition: item.1 }) - .is_some() - { - ctx.emit_error("letter redefined", item.1); - } - } - if list.is_empty() { - ctx.emit_error("alphabet cannot be empty", span); - } - } - TL::Item(S("F", _), list) => { - if !final_states.is_empty() { - ctx.emit_error("final states already set", span); - } - let Some(list) = list.expect_set(ctx) else { - continue; - }; - for item in list { - let Some(ident) = item.expect_ident(ctx) else { - continue; - }; - if states.contains_key(&State(ident)) { - if final_states - .insert(State(ident), StateInfo { definition: item.1 }) - .is_some() - { - ctx.emit_error("final state redefined", item.1); - } - } else { - ctx.emit_error("final state not defined in set of states", item.1); - } - } - } - TL::Item(S("I" | "q0", _), S(src, src_d)) => match src { - ast::Item::Symbol(Sym::Ident(ident)) => { - if initial_state.is_some() { - ctx.emit_error("initial state already set", span); - } - if states.contains_key(&State(ident)) { - initial_state = Some(State(ident)) - } else { - ctx.emit_error("initial state symbol not defined as a state", src_d); - } - } - _ => ctx.emit_error("expected ident", src_d), - }, - TL::Item(S(name, dest_s), _) => { - ctx.emit_error(format!("unknown item {name:?}, expected 'Q' | 'E' | '{SIGMA_UPPER}' | 'sigma' | 'F' | 'T' | '{GAMMA_UPPER}' | 'gamma' | 'I' | 'q0' | 'S' | 'z0'"), dest_s); - } - - TL::TransitionFunc(S((S("d" | DELTA_LOWER | "delta", _), tuple), _), list) => { - let list = list.set_weak(); - let Some((state, letter)) = tuple.as_ref().expect_fa_transition_function(ctx) - else { - continue; - }; - if !states.contains_key(&State(state.0)) { - ctx.emit_error("transition state not defined as state", state.1); - continue; - }; - - let letter: Option> = match letter.0 { - Sym::Epsilon => { - if !options.epsilon_moves { - ctx.emit_error("epsilon moves not permitted", letter.1); - } - None - } - Sym::Ident(val) => { - if !alphabet.contains_key(&Letter(val)) { - ctx.emit_error( - "transition letter not defined in alphabet", - letter.1, - ); - } - Some(Letter(val)) - } - }; - - for item in list { - let Some(next_state) = item.expect_ident(ctx) else { - continue; - }; - let next_state = Spanned(next_state, item.1); - - if !states.contains_key(&State(next_state.0)) { - ctx.emit_error("transition state not defined as state", next_state.1); - continue; - }; - - let entry: &mut _ = transitions - .entry(TransitionFrom { - letter, - state: State(state.0), - }) - .or_default(); - if !entry.is_empty() && !options.non_deterministic { - ctx.emit_error("transition already defined for this starting point (non determinism not permitted)", item.1); - } - if !entry.insert(TransitionTo { - state: State(next_state.0), - - function: tuple.1, - transition: item.1, - }) { - ctx.emit_warning("duplicate transition", item.1); - } - } - } - TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { - ctx.emit_error( - format!( - "unknown function {name:?}, expected 'd' | 'delta' | '{DELTA_LOWER}'" - ), - dest_s, - ); - } - - TL::ProductionRule(_, _) => { - ctx.emit_error("unexpected production rule", span); - } - TL::Table() => ctx.emit_error("unexpected table", span), - } + self.compile_top_level(element, span); } - if alphabet.is_empty() { - ctx.emit_error_locless("alphabet never defined"); + if self.alphabet_def.is_none() { + self.ctx + .emit_error_locless("alphabet never defined") + .emit_help_logless("add: E = {...}") + .emit_info_logless(concat!("E can be ", sigma_upper!(str))); } - if states.is_empty() { - ctx.emit_error_locless("states never defined"); + if self.states_def.is_none() { + self.ctx + .emit_error_locless("states never defined") + .emit_help_logless("add: Q = {...}"); } - let initial_state = match initial_state { - Some(some) => some, + if self.final_states_def.is_none() { + self.ctx + .emit_error_locless("final states never defined") + .emit_help_logless("add: F = {...}"); + } + + let initial_state = match self.initial_state { + Some(some) => some.0, None => { - if states.contains_key(&State("q0")) { - ctx.emit_warning_locless("initial state not defined, defaulting to 'q0'"); + if self.states.contains_key(&State("q0")) { + self.ctx + .emit_warning_locless("initial state not defined, defaulting to 'q0'") + .emit_help_logless(format!("add: {INITIAL_STATE} = q0")); } else { - ctx.emit_error_locless("initial state not defined"); + self.ctx + .emit_error_locless("initial state not defined") + .emit_help_logless(format!("add: {INITIAL_STATE} = ...")); } State("q0") } }; - if ctx.contains_errors() { + if self.transitions.is_empty(){ + self.ctx.emit_warning_locless("no transitions defined") + .emit_help_logless("consider defining one: d(state, letter|epsilon) = state | {state, state, ...}") + .emit_info_logless(concat!("d can be ", delta_lower!(str))) + .emit_info_logless(concat!("epsilon can be ", epsilon!(str))); + } + + if self.ctx.contains_errors() { return None; } Some(Fa { initial_state, - states, - alphabet, - final_states, - transitions, + states: self.states, + alphabet: self.alphabet, + final_states: self.final_states, + transitions: self.transitions, }) } + + fn compile_top_level(&mut self, element: TopLevel<'a>, span: Span) { + use Spanned as S; + use ast::TopLevel as TL; + match element { + TL::Item(S("Q", _), list) => self.compile_states(list, span), + TL::Item(S(sigma_upper!(pat), _), list) => self.compile_alphabet(list, span), + TL::Item(S("F", _), list) => self.compile_final_states(list, span), + TL::Item(S(INITIAL_STATE, _), item) => self.compile_initial_state(item, span), + TL::Item(S(name, dest_s), _) => { + self.ctx.emit_error(format!("unknown item {name:?}, expected states, alphabet, final states, initial state"), dest_s); + } + + TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { + self.compile_transition_function(args, list) + } + TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { + self.ctx.emit_error( + format!( + "unknown function {name:?}, expected transition function ( {} )", + delta_lower!(str) + ), + dest_s, + ); + } + + TL::ProductionRule(_, _) => { + self.ctx.emit_error("unexpected production rule", span); + } + TL::Table() => _ = self.ctx.emit_error("unexpected table", span), + } + } + + fn compile_states(&mut self, list: Spanned>, top_level: Span) { + if let Some(previous) = self.states_def { + self.ctx + .emit_error("states already set", top_level) + .emit_info("previously defined here", previous); + } + let Some(list) = list.expect_set(self.ctx) else { + return; + }; + for item in list { + let Some(ident) = item.expect_ident(self.ctx) else { + continue; + }; + if let Some(previous) = self + .states + .insert(State(ident), StateInfo { definition: item.1 }) + { + self.ctx + .emit_error("state redefined", item.1) + .emit_info("previously defined here", previous.definition); + } + } + + if list.is_empty() { + self.ctx.emit_error("states cannot be empty", top_level); + } + self.states_def = Some(top_level); + } + + fn compile_alphabet(&mut self, list: Spanned>, top_level: Span) { + if let Some(previous) = self.alphabet_def { + self.ctx + .emit_error("alphabet already set", top_level) + .emit_info("previously defined here", previous); + } + let Some(list) = list.expect_set(self.ctx) else { + return; + }; + for item in list { + let Some(ident) = item.expect_ident(self.ctx) else { + continue; + }; + + if ident.chars().count() != 1 { + self.ctx + .emit_error("letter cannot be longer than one char", item.1); + } + + if let Some(previous) = self + .alphabet + .insert(Letter(ident), LetterInfo { definition: item.1 }) + { + self.ctx + .emit_error("letter redefined", item.1) + .emit_help("previously defined here", previous.definition); + } + } + if list.is_empty() { + self.ctx.emit_error("alphabet cannot be empty", top_level); + } + self.alphabet_def = Some(top_level); + } + + fn compile_final_states(&mut self, list: Spanned>, top_level: Span) { + if let Some(previous) = self.final_states_def { + self.ctx + .emit_error("final states already set", top_level) + .emit_help("previously defined here", previous); + } + let Some(list) = list.expect_set(self.ctx) else { + return; + }; + for item in list { + let Some(ident) = item.expect_ident(self.ctx) else { + continue; + }; + if self.states.contains_key(&State(ident)) { + if self + .final_states + .insert(State(ident), StateInfo { definition: item.1 }) + .is_some() + { + self.ctx.emit_error("final state redefined", item.1); + } + } else { + self.ctx + .emit_error("final state not defined in set of states", item.1); + } + } + self.final_states_def = Some(top_level); + } + + fn compile_initial_state( + &mut self, + Spanned(src, src_d): Spanned>, + top_level: Span, + ) { + match src { + ast::Item::Symbol(Sym::Ident(ident)) => { + if let Some((_, previous)) = self.initial_state { + self.ctx + .emit_error("initial state already set", top_level) + .emit_help("previously defined here", previous); + } + if self.states.contains_key(&State(ident)) { + self.initial_state = Some((State(ident), top_level)) + } else { + self.ctx + .emit_error("initial state symbol not defined as a state", src_d); + } + } + _ => _ = self.ctx.emit_error("expected ident", src_d), + } + } + fn compile_transition_function( + &mut self, + args: Spanned>, + list: Spanned>, + ) { + let list = list.set_weak(); + let Some((state, letter)) = args.as_ref().expect_fa_transition_function(self.ctx) else { + return; + }; + if !self.states.contains_key(&State(state.0)) { + self.ctx + .emit_error("transition state not defined as state", state.1); + return; + }; + + let letter: Option> = match letter.0 { + Sym::Epsilon(_) => { + if !self.options.epsilon_moves { + self.ctx.emit_error("epsilon moves not permitted", letter.1); + } + None + } + Sym::Ident(val) => { + if !self.alphabet.contains_key(&Letter(val)) { + self.ctx + .emit_error("transition letter not defined in alphabet", letter.1); + } + Some(Letter(val)) + } + }; + + for item in list { + let Some(next_state) = item.expect_ident(self.ctx) else { + continue; + }; + let next_state = Spanned(next_state, item.1); + + if !self.states.contains_key(&State(next_state.0)) { + self.ctx + .emit_error("transition state not defined as state", next_state.1); + continue; + }; + + let entry: &mut _ = self + .transitions + .entry(TransitionFrom { + letter, + state: State(state.0), + }) + .or_default(); + if let Some(entry) = entry.iter().next() + && !self.options.non_deterministic + { + self.ctx.emit_error("transition already defined for this starting point (non determinism not permitted)", item.1) + .emit_info("previously defined here", entry.transition); + } + if let Some(previous) = entry.replace(TransitionTo { + state: State(next_state.0), + + function: args.1, + transition: item.1, + }) { + self.ctx + .emit_warning("duplicate transition", item.1) + .emit_info("previously defined here", previous.transition); + } + } + } } impl<'a> Spanned<&ast::Tuple<'a>> { @@ -271,7 +387,12 @@ impl<'a> Spanned<&ast::Tuple<'a>> { ] => { return Some((Spanned(state, *state_span), Spanned(*letter, *letter_span))); } - _ => ctx.emit_error("expected FA transition function (ident, ident|~)", self.1), + _ => { + _ = ctx.emit_error( + "expected FA transition function (state, letter|epsilon)", + self.1, + ) + } } None } diff --git a/automata/src/automatan/mod.rs b/automata/src/automatan/mod.rs index ba329eb..bf457c6 100644 --- a/automata/src/automatan/mod.rs +++ b/automata/src/automatan/mod.rs @@ -14,31 +14,31 @@ pub struct Options { } #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] pub struct State<'a>(pub &'a str); #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] pub struct Symbol<'a>(pub &'a str); #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] pub struct Letter<'a>(pub &'a str); #[derive(Clone, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct StateInfo { pub definition: Span, } #[derive(Clone, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SymbolInfo { pub definition: Span, } #[derive(Clone, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct LetterInfo { pub definition: Span, } diff --git a/automata/src/automatan/pda.rs b/automata/src/automatan/pda.rs index f6b9255..5532a14 100644 --- a/automata/src/automatan/pda.rs +++ b/automata/src/automatan/pda.rs @@ -2,10 +2,9 @@ use std::collections::HashSet; use super::*; -use crate::loader::{ - Context, DELTA_LOWER, GAMMA_UPPER, SIGMA_UPPER, Spanned, - ast::{self, Symbol as Sym}, -}; +use crate::{delta_lower, gamma_upper, loader::{ + Context, INITIAL_STACK, INITIAL_STATE, Spanned, ast::{self, Symbol as Sym}, log::LogSink +}, sigma_upper}; #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] @@ -90,7 +89,7 @@ impl<'a> Pda<'a> { ctx.emit_error("states cannot be empty", span); } } - TL::Item(S("E" | SIGMA_UPPER | "sigma", _), list) => { + TL::Item(S(sigma_upper!(pat), _), list) => { if !alphabet.is_empty() { ctx.emit_error("alphabet already set", span); } @@ -142,7 +141,7 @@ impl<'a> Pda<'a> { } final_states = Some(map); } - TL::Item(S("T" | GAMMA_UPPER | "gamma", _), list) => { + TL::Item(S(gamma_upper!(pat), _), list) => { if !symbols.is_empty() { ctx.emit_error("stack symbols already set", span); } @@ -166,7 +165,7 @@ impl<'a> Pda<'a> { ctx.emit_error("stack symbols cannot be empty", span); } } - TL::Item(S("I" | "q0", _), S(src, src_d)) => match src { + TL::Item(S(INITIAL_STATE, _), S(src, src_d)) => match src { ast::Item::Symbol(Sym::Ident(ident)) => { if initial_state.is_some() { ctx.emit_error("initial state already set", span); @@ -177,9 +176,9 @@ impl<'a> Pda<'a> { ctx.emit_error("initial state symbol not defined as a state", src_d); } } - _ => ctx.emit_error("expected ident", src_d), + _ => _ = ctx.emit_error("expected ident", src_d), }, - TL::Item(S("S" | "z0", _), S(src, src_d)) => match src { + TL::Item(S(INITIAL_STACK, _), S(src, src_d)) => match src { ast::Item::Symbol(Sym::Ident(ident)) => { if initial_stack.is_some() { ctx.emit_error("initial stack already set", span); @@ -193,13 +192,13 @@ impl<'a> Pda<'a> { ); } } - _ => ctx.emit_error("expected ident", src_d), + _ => _ = ctx.emit_error("expected ident", src_d), }, TL::Item(S(name, dest_s), _) => { - ctx.emit_error(format!("unknown item {name:?}, expected 'Q' | 'E' | '{SIGMA_UPPER}' | 'sigma' | 'F' | 'T' | '{GAMMA_UPPER}' | 'gamma' | 'I' | 'q0' | 'S' | 'z0'"), dest_s); + ctx.emit_error(format!("unknown item {name:?}, expected states, alphabet, symbols, final states, initial state, initial stack"), dest_s); } - TL::TransitionFunc(S((S("d" | DELTA_LOWER | "delta", _), tuple), _), list) => { + TL::TransitionFunc(S((S(delta_lower!(pat), _), tuple), _), list) => { let list = list.set_weak(); let Some((state, letter, stack_symbol)) = tuple.as_ref().expect_pda_transition_function(ctx) @@ -219,7 +218,7 @@ impl<'a> Pda<'a> { }; let letter: Option> = match letter.0 { - Sym::Epsilon => { + Sym::Epsilon(_) => { if !options.epsilon_moves { ctx.emit_error("epsilon moves not permitted", letter.1); } @@ -253,7 +252,7 @@ impl<'a> Pda<'a> { .iter() .rev() .filter_map(|symbol| { - if matches!(symbol.0, ast::Item::Symbol(Sym::Epsilon)) { + if matches!(symbol.0, ast::Item::Symbol(Sym::Epsilon(_))) { return None; } let ident = symbol.expect_ident(ctx)?; @@ -290,7 +289,7 @@ impl<'a> Pda<'a> { TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { ctx.emit_error( format!( - "unknown function {name:?}, expected 'd' | 'delta' | '{DELTA_LOWER}'" + "unknown function {name:?}, expected transition function ( {} )", delta_lower!(str) ), dest_s, ); @@ -299,7 +298,7 @@ impl<'a> Pda<'a> { TL::ProductionRule(_, _) => { ctx.emit_error("unexpected production rule", span); } - TL::Table() => ctx.emit_error("unexpected table", span), + TL::Table() => _ = ctx.emit_error("unexpected table", span), } } @@ -318,14 +317,14 @@ impl<'a> Pda<'a> { let initial_stack = match initial_stack { Some(some) => some, None => { - if symbols.contains_key(&Symbol("z0")) { + if symbols.contains_key(&Symbol("Z0")) { ctx.emit_warning_locless( - "initial stack symbol not defined, defaulting to 'z0'", + "initial stack symbol not defined, defaulting to 'Z0'", ); } else { ctx.emit_error_locless("initial stack symbol not defined"); } - Symbol("z0") + Symbol("Z0") } }; @@ -374,8 +373,8 @@ impl<'a, 'b> Spanned<&'b ast::Tuple<'a>> { Spanned(symbol, *symbol_span), )); } - _ => ctx.emit_error( - "expected PDA transition function (ident, ident|~, ident)", + _ => _ = ctx.emit_error( + "expected PDA transition function (state, letter|epsilon, symbol)", self.1, ), } @@ -392,7 +391,7 @@ impl<'a, 'b> Spanned<&'b ast::Tuple<'a>> { ] => { return Some((Spanned(state, *state_span), list.list_weak())); } - _ => ctx.emit_error("expected PDA transition (ident, item|[item])", self.1), + _ => _ = ctx.emit_error("expected PDA transition (state, symbol|[symbol])", self.1), } None } diff --git a/automata/src/automatan/tm.rs b/automata/src/automatan/tm.rs index 90e2135..179ab45 100644 --- a/automata/src/automatan/tm.rs +++ b/automata/src/automatan/tm.rs @@ -2,10 +2,9 @@ use std::collections::HashSet; use super::*; -use crate::loader::{ - Context, DELTA_LOWER, GAMMA_UPPER, SIGMA_UPPER, Spanned, - ast::{self, Symbol as Sym}, -}; +use crate::{delta_lower, gamma_upper, loader::{ + BLANK_SYMBOL, Context, Spanned, ast::{self, Symbol as Sym}, log::LogSink +}}; #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] @@ -118,7 +117,7 @@ impl<'a> Tm<'a> { } } } - TL::Item(S("T" | GAMMA_UPPER | "gamma", _), list) => { + TL::Item(S(gamma_upper!(pat), _), list) => { if !symbols.is_empty() { ctx.emit_error("tape symbols already set", span); } @@ -142,7 +141,7 @@ impl<'a> Tm<'a> { ctx.emit_error("tape symbols cannot be empty", span); } } - TL::Item(S("I" | "q0", _), S(src, src_d)) => match src { + TL::Item(S("q0", _), S(src, src_d)) => match src { ast::Item::Symbol(Sym::Ident(ident)) => { if initial_state.is_some() { ctx.emit_error("initial state already set", span); @@ -153,9 +152,9 @@ impl<'a> Tm<'a> { ctx.emit_error("initial state symbol not defined as a state", src_d); } } - _ => ctx.emit_error("expected ident", src_d), + _ => _ = ctx.emit_error("expected ident", src_d), }, - TL::Item(S("S" | "z0", _), S(src, src_d)) => match src { + TL::Item(S(BLANK_SYMBOL, _), S(src, src_d)) => match src { ast::Item::Symbol(Sym::Ident(ident)) => { if initial_tape.is_some() { ctx.emit_error("initial tape symbol already set", span); @@ -169,13 +168,13 @@ impl<'a> Tm<'a> { ); } } - _ => ctx.emit_error("expected ident", src_d), + _ => _ = ctx.emit_error("expected ident", src_d), }, TL::Item(S(name, dest_s), _) => { - ctx.emit_error(format!("unknown item {name:?}, expected 'Q' | 'E' | '{SIGMA_UPPER}' | 'sigma' | 'F' | 'T' | '{GAMMA_UPPER}' | 'gamma' | 'I' | 'q0' | 'S' | 'z0'"), dest_s); + ctx.emit_error(format!("unknown item {name:?}, expected states, symbols, final states, initial state, blank symbol"), dest_s); } - TL::TransitionFunc(S((S("d" | DELTA_LOWER | "delta", _), tuple), _), list) => { + TL::TransitionFunc(S((S(delta_lower!(pat), _), tuple), _), list) => { let list = list.set_weak(); let Some((from_state, from_tape)) = tuple.as_ref().expect_tm_transition_function(ctx) @@ -229,9 +228,9 @@ impl<'a> Tm<'a> { } } TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { - ctx.emit_error( + ctx.emit_error( format!( - "unknown function {name:?}, expected 'd' | 'delta' | '{DELTA_LOWER}'" + "unknown function {name:?}, expected transition function ( {} )", delta_lower!(str) ), dest_s, ); @@ -240,7 +239,7 @@ impl<'a> Tm<'a> { TL::ProductionRule(_, _) => { ctx.emit_error("unexpected production rule", span); } - TL::Table() => ctx.emit_error("unexpected table", span), + TL::Table() => _ = ctx.emit_error("unexpected table", span), } } @@ -303,7 +302,7 @@ impl<'a> Spanned<&ast::Tuple<'a>> { ] => { return Some((Spanned(state, *state_span), Spanned(*tape, *tape_span))); } - _ => ctx.emit_error("expected TM transition function (ident, ident)", self.1), + _ => _ = ctx.emit_error("expected TM transition function (state, symbol)", self.1), } None } @@ -321,7 +320,7 @@ impl<'a> Spanned<&ast::Tuple<'a>> { let direction = match direction { ast::Symbol::Ident("left" | "L" | "<") => Direction::Left, ast::Symbol::Ident("right" | "R" | ">") => Direction::Right, - ast::Symbol::Epsilon | ast::Symbol::Ident("~") => Direction::None, + ast::Symbol::Epsilon(_) | ast::Symbol::Ident("~") => Direction::None, ast::Symbol::Ident(ident) => { ctx.emit_error( format!("invalid direction specified '{ident}'"), @@ -336,8 +335,8 @@ impl<'a> Spanned<&ast::Tuple<'a>> { Spanned(direction, *direction_span), )); } - _ => ctx.emit_error( - "expected TM transition function (ident, ident, ident)", + _ => _ = ctx.emit_error( + "expected TM transition function (state, symbol, direction)", self.1, ), } diff --git a/automata/src/lib.rs b/automata/src/lib.rs index af73e12..ecad2bb 100644 --- a/automata/src/lib.rs +++ b/automata/src/lib.rs @@ -1,2 +1,36 @@ pub mod automatan; pub mod loader; + + +#[macro_export] +macro_rules! dual_struct_serde { + ($({$(#[$serde_specific:meta])*})? + $(#[$struct_meta:meta])* + $vis:vis struct $Name:ident $(<$($gen:tt),*>)? + { + $( + $(#[$field_meta:meta])* + $fvis:vis $fname:ident : $fty:ty + ),* $(,)? + } + ) => { + #[cfg(feature = "serde")] + $(#[$struct_meta])* + $( $(#[$serde_specific])* )? + #[derive(serde::Serialize, serde::Deserialize)] + $vis struct $Name $(<$($gen)*>)? { + $( + $(#[$field_meta])* + $fvis $fname: $fty + ),* + } + + #[cfg(not(feature = "serde"))] + $(#[$struct_meta])* + $vis struct $Name $(<$($gen)*>)? { + $( + $fvis $fname: $fty + ),* + } + }; +} \ No newline at end of file diff --git a/automata/src/loader/ast.rs b/automata/src/loader/ast.rs index 5f6afcb..4db3aca 100644 --- a/automata/src/loader/ast.rs +++ b/automata/src/loader/ast.rs @@ -16,7 +16,7 @@ pub struct Tuple<'a>(pub Vec>>); #[derive(Clone, Copy, Debug)] pub enum Symbol<'a> { - Epsilon, + Epsilon(&'a str), Ident(&'a str), } @@ -62,14 +62,14 @@ pub enum TopLevel<'a> { Table(), } -use crate::loader::Context; +use crate::loader::{Context, log::LogSink}; impl<'a> Spanned> { pub fn expect_symbol(&self, ctx: &mut Context<'a>) -> Option> { match &self.0 { Item::Symbol(sym) => return Some(*sym), - Item::Tuple(_) => ctx.emit_error("expected ident found tuple", self.1), - Item::List(_) => ctx.emit_error("expected ident found list", self.1), + Item::Tuple(_) => _ = ctx.emit_error("expected ident found tuple", self.1), + Item::List(_) => _ = ctx.emit_error("expected ident found list", self.1), } None } @@ -77,18 +77,18 @@ impl<'a> Spanned> { pub fn expect_ident(&self, ctx: &mut Context<'a>) -> Option<&'a str> { match &self.0 { Item::Symbol(Symbol::Ident(ident)) => return Some(ident), - Item::Symbol(Symbol::Epsilon) => ctx.emit_error("expected ident found epsilon", self.1), - Item::Tuple(_) => ctx.emit_error("expected ident found tuple", self.1), - Item::List(_) => ctx.emit_error("expected ident found list", self.1), + Item::Symbol(Symbol::Epsilon(_)) => _ = ctx.emit_error("expected ident found epsilon", self.1), + Item::Tuple(_) => _ = ctx.emit_error("expected ident found tuple", self.1), + Item::List(_) => _ = ctx.emit_error("expected ident found list", self.1), } None } pub fn expect_set(&self, ctx: &mut Context<'a>) -> Option<&[Spanned>]> { match &self.0 { - Item::Symbol(Symbol::Ident(_)) => ctx.emit_error("expected set found ident", self.1), - Item::Symbol(Symbol::Epsilon) => ctx.emit_error("expected set found epsilon", self.1), - Item::Tuple(_) => ctx.emit_error("expected set found tuple", self.1), + Item::Symbol(Symbol::Ident(_)) => _ = ctx.emit_error("expected set found ident", self.1), + Item::Symbol(Symbol::Epsilon(_)) => _ = ctx.emit_error("expected set found epsilon", self.1), + Item::Tuple(_) => _ = ctx.emit_error("expected set found tuple", self.1), Item::List(list) => return Some(&list.0), } None @@ -96,9 +96,9 @@ impl<'a> Spanned> { pub fn expect_list(&self, ctx: &mut Context<'a>) -> Option<&[Spanned>]> { match &self.0 { - Item::Symbol(Symbol::Ident(_)) => ctx.emit_error("expected list found ident", self.1), - Item::Symbol(Symbol::Epsilon) => ctx.emit_error("expected list found epsilon", self.1), - Item::Tuple(_) => ctx.emit_error("expected list found tuple", self.1), + Item::Symbol(Symbol::Ident(_)) => _ = ctx.emit_error("expected list found ident", self.1), + Item::Symbol(Symbol::Epsilon(_)) => _ = ctx.emit_error("expected list found epsilon", self.1), + Item::Tuple(_) => _ = ctx.emit_error("expected list found tuple", self.1), Item::List(list) => return Some(&list.0), } None @@ -120,10 +120,10 @@ impl<'a> Spanned> { pub fn expect_tuple(&self, ctx: &mut Context<'a>) -> Option>> { match &self.0 { - Item::Symbol(Symbol::Ident(_)) => ctx.emit_error("expected tuple found ident", self.1), - Item::Symbol(Symbol::Epsilon) => ctx.emit_error("expected tuple found epsilon", self.1), + Item::Symbol(Symbol::Ident(_)) => _ = ctx.emit_error("expected tuple found ident", self.1), + Item::Symbol(Symbol::Epsilon(_)) => _ = ctx.emit_error("expected tuple found epsilon", self.1), Item::Tuple(tuple) => return Some(Spanned(tuple, self.1)), - Item::List(_) => ctx.emit_error("expected tuple found list", self.1), + Item::List(_) => _ = ctx.emit_error("expected tuple found list", self.1), } None } diff --git a/automata/src/loader/log.rs b/automata/src/loader/log.rs index 7d8e813..084047c 100644 --- a/automata/src/loader/log.rs +++ b/automata/src/loader/log.rs @@ -2,11 +2,97 @@ use std::fmt::Display; use crate::loader::Span; + +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct Logs { logs: Vec, has_error: bool, } +pub trait LogSink { + fn emit(&mut self, entry: LogEntry) -> &mut LogEntry; + + fn emit_error_locless(&mut self, msg: impl Into) -> &mut LogEntry { + self.emit(LogEntry { + message: msg.into(), + span: None, + level: LogLevel::Error, + child: None, + }) + } + + fn emit_error(&mut self, msg: impl Into, span: Span) -> &mut LogEntry { + self.emit(LogEntry { + message: msg.into(), + span: Some(span), + level: LogLevel::Error, + child: None, + }) + } + + fn emit_warning(&mut self, msg: impl Into, span: Span) -> &mut LogEntry { + self.emit(LogEntry { + message: msg.into(), + span: Some(span), + level: LogLevel::Warning, + child: None, + }) + } + + fn emit_warning_locless(&mut self, msg: impl Into) -> &mut LogEntry { + self.emit(LogEntry { + message: msg.into(), + span: None, + level: LogLevel::Warning, + child: None, + }) + } + + fn emit_info(&mut self, msg: impl Into, span: Span) -> &mut LogEntry { + self.emit(LogEntry { + message: msg.into(), + span: Some(span), + level: LogLevel::Info, + child: None, + }) + } + + fn emit_info_logless(&mut self, msg: impl Into) -> &mut LogEntry { + self.emit(LogEntry { + message: msg.into(), + span: None, + level: LogLevel::Info, + child: None, + }) + } + + fn emit_help(&mut self, msg: impl Into, span: Span) -> &mut LogEntry { + self.emit(LogEntry { + message: msg.into(), + span: Some(span), + level: LogLevel::Help, + child: None, + }) + } + + fn emit_help_logless(&mut self, msg: impl Into) -> &mut LogEntry { + self.emit(LogEntry { + message: msg.into(), + span: None, + level: LogLevel::Help, + child: None, + }) + } +} + +impl LogSink for Logs { + fn emit(&mut self, entry: LogEntry) -> &mut LogEntry { + self.has_error |= matches!(entry.level, LogLevel::Error); + self.logs.push(entry); + self.logs.last_mut().unwrap() + } +} + impl Logs { pub fn new() -> Self { Self { @@ -19,51 +105,6 @@ impl Logs { self.has_error } - pub fn emit(&mut self, entry: LogEntry) { - self.has_error |= matches!(entry.level, LogLevel::Error); - self.logs.push(entry); - } - - pub fn emit_error_locless(&mut self, msg: impl Into) { - self.emit(LogEntry { - message: msg.into(), - span: None, - level: LogLevel::Error, - }); - } - - pub fn emit_error(&mut self, msg: impl Into, span: Span) { - self.emit(LogEntry { - message: msg.into(), - span: Some(span), - level: LogLevel::Error, - }); - } - - pub fn emit_warning(&mut self, msg: impl Into, span: Span) { - self.emit(LogEntry { - message: msg.into(), - span: Some(span), - level: LogLevel::Warning, - }); - } - - pub fn emit_warning_locless(&mut self, msg: impl Into) { - self.emit(LogEntry { - message: msg.into(), - span: None, - level: LogLevel::Warning, - }); - } - - pub fn emit_info(&mut self, msg: impl Into, span: Span) { - self.emit(LogEntry { - message: msg.into(), - span: Some(span), - level: LogLevel::Info, - }); - } - pub fn displayable_with<'a>( &'a self, src: &'a str, @@ -86,16 +127,27 @@ impl Default for Logs { } } +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub enum LogLevel { Info, Warning, Error, + Help, } +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct LogEntry { pub message: String, pub span: Option, pub level: LogLevel, + pub child: Option>, +} + +impl LogSink for LogEntry { + fn emit(&mut self, entry: LogEntry) -> &mut LogEntry { + self.child = Some(Box::new(entry)); + self.child.as_mut().unwrap() + } } pub struct LogEntryDisplay<'a> { @@ -109,76 +161,86 @@ impl<'a> Display for LogEntryDisplay<'a> { pub const BOLD: &str = "\x1b[1m"; // pub const UNDERLINE: &str = "\x1b[4m"; pub const RED: &str = "\x1b[31m"; - // pub const GREEN: &str = "\x1b[32m"; + pub const GREEN: &str = "\x1b[32m"; pub const YELLOW: &str = "\x1b[33m"; // pub const BLUE: &str = "\x1b[34m"; pub const CYAN: &str = "\x1b[36m"; - match self.entry.level { - LogLevel::Info => write!(f, "{BOLD}{CYAN}info{RESET}{BOLD}: ")?, - LogLevel::Warning => write!(f, "{BOLD}{YELLOW}warning{RESET}{BOLD}: ")?, - LogLevel::Error => write!(f, "{BOLD}{RED}error{RESET}{BOLD}: ")?, - } - writeln!(f, "{}{RESET}", self.entry.message)?; + let mut next_entry = Some(self.entry); - if let Some(span) = self.entry.span { - let line_start = self.src.get(..=span.0).unwrap_or("").lines().count(); - let line_end = self.src.get(..span.1).unwrap_or("").lines().count(); + while let Some(entry) = next_entry { + match entry.level { + LogLevel::Help => write!(f, "{BOLD}{GREEN}help{RESET}{BOLD}: ")?, + LogLevel::Info => write!(f, "{BOLD}{CYAN}info{RESET}{BOLD}: ")?, + LogLevel::Warning => write!(f, "{BOLD}{YELLOW}warning{RESET}{BOLD}: ")?, + LogLevel::Error => write!(f, "{BOLD}{RED}error{RESET}{BOLD}: ")?, + } + writeln!(f, "{}{RESET}", entry.message)?; - let padding = if line_end == 0 {1} else {line_end.ilog10() as usize}; + if let Some(span) = entry.span { + let line_start = self.src.get(..=span.0).unwrap_or("").lines().count(); + let line_end = self.src.get(..span.1).unwrap_or("").lines().count(); - let start = self - .src - .get(..span.0) - .and_then(|s| s.rfind('\n')) - .map(|v| v + 1) - .unwrap_or(0); + let padding = if line_end == 0 { + 1 + } else { + line_end.ilog10() as usize + }; - let end = if self.src.get(..span.1).unwrap_or("").ends_with("\n") { - span.1 - } else { - self.src - .get(span.1..) - .and_then(|s| s.find('\n')) - .map(|v| v + span.1) - .unwrap_or(self.src.len()) - }; + let start = self + .src + .get(..span.0) + .and_then(|s| s.rfind('\n')) + .map(|v| v + 1) + .unwrap_or(0); - let mut index = start; - for (i, line) in self - .src - .get(start..end) - .unwrap_or("") - .split_inclusive("\n") - .enumerate() - { - write!(f, "{BOLD}{CYAN}{:>padding$}: {RESET}", i + line_start)?; - for char in line.chars() { - if char == '\t' { - write!(f, " ")? - } else { - write!(f, "{char}")? + let end = if self.src.get(..span.1).unwrap_or("").ends_with("\n") { + span.1 + } else { + self.src + .get(span.1..) + .and_then(|s| s.find('\n')) + .map(|v| v + span.1) + .unwrap_or(self.src.len()) + }; + + let mut index = start; + for (i, line) in self + .src + .get(start..end) + .unwrap_or("") + .split_inclusive("\n") + .enumerate() + { + write!(f, "{BOLD}{CYAN}{:>padding$}: {RESET}", i + line_start)?; + for char in line.chars() { + if char == '\t' { + write!(f, " ")? + } else { + write!(f, "{char}")? + } } - } - if !line.ends_with("\n") { - writeln!(f)?; - } - write!(f, "{BOLD}{CYAN}")?; - for _ in 0..padding + 3 { - write!(f, " ")?; - } - for char in line.chars() { - if (span.0..span.1).contains(&index) { - write!(f, "~")?; - } else { + if !line.ends_with("\n") { + writeln!(f)?; + } + write!(f, "{BOLD}{CYAN}")?; + for _ in 0..padding + 3 { write!(f, " ")?; } - index += char.len_utf8(); + for char in line.chars() { + if (span.0..span.1).contains(&index) { + write!(f, "~")?; + } else { + write!(f, " ")?; + } + index += char.len_utf8(); + } + write!(f, "{RESET}")?; + index += '\n'.len_utf8(); + writeln!(f)?; } - write!(f, "{RESET}")?; - index += '\n'.len_utf8(); - writeln!(f)?; } + next_entry = entry.child.as_deref() } Ok(()) diff --git a/automata/src/loader/mod.rs b/automata/src/loader/mod.rs index 96687cd..5379a3d 100644 --- a/automata/src/loader/mod.rs +++ b/automata/src/loader/mod.rs @@ -1,23 +1,67 @@ -use crate::{automatan::*, loader::ast::TopLevel}; +use crate::{ + automatan::*, + loader::{ + ast::TopLevel, + log::{LogEntry, LogSink}, + }, +}; pub mod ast; pub mod lexer; pub mod log; pub mod parser; -pub const EPSILON_LOWER: &str = "Ɛ"; -pub const EPSILON_LOWER_MATH: &str = "𝛆"; -pub const DELTA_LOWER: &str = "δ"; -pub const SIGMA_UPPER: &str = "Σ"; -pub const GAMMA_UPPER: &str = "Γ"; -pub const GAMMA_LOWER: &str = "γ"; +#[macro_export] +macro_rules! maker { + (pat: $($pat:pat),*) => { + $($pat)|* + }; + (arr: $($expr:expr),*) => { + [$($expr),*] + }; + (str: $first:literal, $($remainder:literal),+) => { + concat!($crate::maker!(str: $first), " | ", $crate::maker!(str: $($remainder),*)) + }; + (str: $first:literal) => { + concat!("'",$first,"'") + }; +} + +pub const INITIAL_STATE: &str = "q0"; +pub const INITIAL_STACK: &str = "z0"; +pub const BLANK_SYMBOL: &str = "B"; + +#[macro_export] +macro_rules! epsilon { + ($ident: ident) => { + $crate::maker!($ident: "epsilon","~", "Ɛ", "ε", "ϵ", "𝛆", "𝛜", "𝜀", "𝜖", "𝜺", "𝝐", "𝝴", "𝞊", "𝞮", "𝟄") + }; +} + +#[macro_export] +macro_rules! delta_lower { + ($ident: ident) => { + $crate::maker!($ident: "delta","D","d","ẟ","δ", "𝛅", "𝛿", "𝜹", "𝝳", "𝞭") + }; +} + +#[macro_export] +macro_rules! sigma_upper { + ($ident: ident) => { + $crate::maker!($ident: "E","S", "sigma","Σ","𝚺", "𝛴", "𝜮", "𝝨", "𝞢") + }; +} + +#[macro_export] +macro_rules! gamma_upper { + ($ident: ident) => { + $crate::maker!($ident: "T","G","gamma","Γ","Ⲅ", "𝚪", "𝛤", "𝜞", "𝝘", "𝞒") + }; +} #[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -pub struct Span( - #[cfg_attr(feature = "serde", serde(rename = "start"))] pub usize, - #[cfg_attr(feature = "serde", serde(rename = "end"))] pub usize, -); +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Span(pub usize, pub usize); impl Span { pub fn join(&self, end: Span) -> Span { Span(self.0, end.1) @@ -41,6 +85,12 @@ pub struct Context<'a> { src: &'a str, } +impl<'a> LogSink for Context<'a> { + fn emit(&mut self, entry: log::LogEntry) -> &mut LogEntry { + self.logs.emit(entry) + } +} + impl<'a> Context<'a> { pub fn new(src: &'a str) -> Self { Self { @@ -61,30 +111,6 @@ impl<'a> Context<'a> { Span(self.src.len(), self.src.len()) } - pub fn emit(&mut self, entry: log::LogEntry) { - self.logs.emit(entry); - } - - pub fn emit_error_locless(&mut self, msg: impl Into) { - self.logs.emit_error_locless(msg); - } - - pub fn emit_error(&mut self, msg: impl Into, span: Span) { - self.logs.emit_error(msg, span); - } - - pub fn emit_warning(&mut self, msg: impl Into, span: Span) { - self.logs.emit_warning(msg, span); - } - - pub fn emit_warning_locless(&mut self, msg: impl Into) { - self.logs.emit_warning_locless(msg); - } - - pub fn emit_info(&mut self, msg: impl Into, span: Span) { - self.logs.emit_info(msg, span); - } - pub fn contains_errors(&self) -> bool { self.logs.contains_errors() } @@ -127,11 +153,13 @@ pub fn parse_universal<'a>(ctx: &mut Context<'a>) -> Option> { (item.expect_ident(ctx)?, span) } Some(S(_, span)) => { - ctx.emit_error("expected type= as first item", span); + ctx.emit_error("expected type= as first item", span) + .emit_help_logless("add: type = ..."); return None; } None => { - ctx.emit_error("expected type= as first item", ctx.eof()); + ctx.emit_error("expected type= as first item", ctx.eof()) + .emit_help_logless("add: type = ..."); return None; } }; @@ -164,8 +192,8 @@ pub fn parse_universal<'a>(ctx: &mut Context<'a>) -> Option> { }; Some(match parse_type(items.next(), ctx)? { - Type::Dfa => Machine::Fa(fa::Fa::parse(items, ctx, D)?), - Type::Nfa => Machine::Fa(fa::Fa::parse(items, ctx, N)?), + Type::Dfa => Machine::Fa(fa::Fa::compile(items, ctx, D)?), + Type::Nfa => Machine::Fa(fa::Fa::compile(items, ctx, N)?), Type::Dpda => Machine::Pda(pda::Pda::parse(items, ctx, D)?), Type::Npda => Machine::Pda(pda::Pda::parse(items, ctx, N)?), Type::Tm => Machine::Tm(tm::Tm::parse(items, ctx, D)?), diff --git a/automata/src/loader/parser.rs b/automata/src/loader/parser.rs index 631470a..d03a5c9 100644 --- a/automata/src/loader/parser.rs +++ b/automata/src/loader/parser.rs @@ -1,3 +1,5 @@ +use crate::epsilon; +use crate::loader::log::LogSink; use crate::loader::{Context, Span}; use super::lexer::Token as T; @@ -49,7 +51,7 @@ impl<'a, 'b> Parser<'a, 'b> { return self.peek; } Some(S(Ok(ok), r)) => return Some(S(ok, r)), - Some(S(Err(err), span)) => self.ctx.emit_error(format!("lexer: {err:?}"), span), + Some(S(Err(err), span)) => _ = self.ctx.emit_error(format!("lexer: {err:?}"), span), None => return None, } } @@ -90,9 +92,8 @@ impl<'a, 'b> Parser<'a, 'b> { fn parse_as_symbol(&mut self, tok: S>) -> S> { match tok { - S(T::Tilde, r) => S(Symbol::Epsilon, r), - S(T::Ident("epsilon"), r) => S(Symbol::Epsilon, r), - S(T::Ident(super::EPSILON_LOWER), r) => S(Symbol::Epsilon, r), + S(T::Tilde, r) => S(Symbol::Epsilon("~"), r), + S(T::Ident(repr@ epsilon!(pat)), r) => S(Symbol::Epsilon(repr), r), S(T::Ident(ident), r) => S(Symbol::Ident(ident), r), S(got, span) => { self.ctx.emit_error( diff --git a/web_lib/src/lib.rs b/web_lib/src/lib.rs index e9ec072..75c4099 100644 --- a/web_lib/src/lib.rs +++ b/web_lib/src/lib.rs @@ -1,6 +1,10 @@ use std::collections::HashMap; -use automata::loader::{self, Context, Span, Spanned, lexer::Lexer}; +use automata::{ + delta_lower, epsilon, gamma_upper, + loader::{self, Context, Span, Spanned, lexer::Lexer}, + sigma_upper, +}; use serde::Serialize; use wasm_bindgen::prelude::wasm_bindgen; @@ -88,16 +92,11 @@ pub fn lex(input: &str) -> Vec { { Kind::Keyword } + + // ugly hack to keep single ascii letters non keyworded for user + Token::Ident(ident) if ident.is_ascii() && ident.len()==1 => Kind::Ident, Token::Ident( - loader::EPSILON_LOWER - | "epsilon" - | loader::DELTA_LOWER - | "delta" - | loader::GAMMA_UPPER - | "gamma" - | loader::GAMMA_LOWER - | loader::SIGMA_UPPER - | "sigma", + epsilon!(pat) | delta_lower!(pat) | sigma_upper!(pat) | gamma_upper!(pat), ) => Kind::Keyword, Token::Ident(_) => Kind::Ident, Token::LineEnd => Kind::Punc, @@ -127,6 +126,7 @@ pub fn lex(input: &str) -> Vec { #[wasm_bindgen] #[derive(Clone, Copy)] pub enum LogLevel { + Help = "help", Info = "info", Warning = "warning", Error = "error", @@ -165,7 +165,7 @@ pub fn compile(input: &str) -> CompileResult { use std::fmt::Write; let ansi_log = ctx.logs_display().fold(String::new(), |mut s, e| { - write!(&mut s, "{e}").unwrap(); + writeln!(&mut s, "{e}").unwrap(); s }); @@ -174,6 +174,7 @@ pub fn compile(input: &str) -> CompileResult { .into_entries() .map(|e| CompileLog { level: match e.level { + loader::log::LogLevel::Help => LogLevel::Help, loader::log::LogLevel::Info => LogLevel::Info, loader::log::LogLevel::Warning => LogLevel::Warning, loader::log::LogLevel::Error => LogLevel::Error,