Commit Diff


commit - /dev/null
commit + c5de16c57b82f6abd2a6ebbe2cbed524347bac05
blob - /dev/null
blob + ea8c4bf7f35f6f77f75d92ad8ce8349f6e81ddba (mode 644)
--- /dev/null
+++ .gitignore
@@ -0,0 +1 @@
+/target
blob - /dev/null
blob + 1a84a0966df721c2c027591b4b9ac8d6913f5d73 (mode 644)
--- /dev/null
+++ Cargo.lock
@@ -0,0 +1,212 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dotvis"
+version = "0.1.0"
+dependencies = [
+ "pest",
+ "pest_derive",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "pest"
+version = "2.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8"
+dependencies = [
+ "memchr",
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha2",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
blob - /dev/null
blob + 38731a70a889e12000e18791b8a66394d668c4c6 (mode 644)
--- /dev/null
+++ Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "dotvis"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+pest = "2.7.10"
+pest_derive = "2.7.10"
blob - /dev/null
blob + fe0326b6a5a875e13318d38ff247c1946ae7f93e (mode 644)
--- /dev/null
+++ src/ast.rs
@@ -0,0 +1,77 @@
+use std::fmt::{self, Display, Formatter};
+use std::collections::BTreeMap;
+
+
+#[derive(Default, Debug)]
+pub struct Node {
+    pub end: bool,
+}
+
+#[derive(Debug)]
+pub struct Edge {
+    pub from: String,
+    pub to: String,
+    pub chars: Vec<char>,
+}
+
+#[derive(Debug)]
+pub struct Machine {
+    pub nodes: BTreeMap<String, Node>,
+    pub edges: Vec<Edge>,
+    pub begin: String,
+    pub current: String,
+    pub consumed: Vec<char>,
+}
+
+impl Machine {
+    pub fn feed(&mut self, ch: char) -> bool {
+	let next = self
+	    .edges
+	    .iter()
+	    .find(|e| e.from == self.current && e.chars.contains(&ch))
+	    .map(|e| e.to.clone());
+
+	match next {
+	    Some(next) => {
+		self.current = next;
+		self.consumed.push(ch);
+		true
+	    },
+	    None => false,
+	}
+    }
+}
+
+impl Display for Edge {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+	write!(f, "\"{}\" -> \"{}\" [label=\"{:?}\"];", self.from, self.to, self.chars)
+    }
+}
+
+impl Display for Machine {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+	writeln!(f, "digraph {{")?;
+
+	for e in &self.edges {
+	    writeln!(f, "\t{e}")?;
+	}
+
+	writeln!(f, "\t__begin -> {};", self.begin)?;
+
+	writeln!(f)?;
+	writeln!(f, "\t__begin [style=\"invis\"];")?;
+	writeln!(f, "\t\"{}\" [style=\"filled\", fillcolor=\"green\"];", self.current)?;
+
+	for (name, node) in &self.nodes {
+	    let shape = match node {
+		Node { end: true } => "doublecircle",
+		Node { end: false } => "circle",
+	    };
+	    writeln!(f, "\t\"{name}\" [shape=\"{shape}\"];")?;
+	}
+
+	writeln!(f, "\t__consumed [shape=\"box\", label=\"consumed: {:?}\"];", self.consumed)?;
+
+	writeln!(f, "}}")
+    }
+}
blob - /dev/null
blob + c4beed8fb117a4dc6b30bcfcf83720a57f991308 (mode 644)
--- /dev/null
+++ src/grammar.pest
@@ -0,0 +1,13 @@
+file = { SOI ~ nodes ~ edges ~ EOI }
+
+nodes = { "nodes" ~ "{" ~ node+ ~ "}"  }
+node = { IDENT ~ ":" ~ nodespecs ~ ";" }
+nodespecs = { nodespec ~ ("," ~ nodespec)* }
+nodespec = @{ "begin" | "end" }
+
+edges = { "edges" ~ "{" ~ edge+ ~ "}" }
+edge = { IDENT ~ "->" ~ IDENT ~ STRING ~ ";" }
+
+IDENT = @{ ('a'..'z' | 'A'..'Z' | "_") ~ ('a'..'z' | 'A'..'Z' | '0'..'9' | "_")* }
+STRING = @{ "\"" ~ (!"\"" ~ ANY)+ ~ "\"" }
+WHITESPACE = _{ " " | "\t" | "\n" | "\r" }
\ No newline at end of file
blob - /dev/null
blob + dc25e626edaf9acb9c66b01542810d85de4a6932 (mode 644)
--- /dev/null
+++ src/main.rs
@@ -0,0 +1,75 @@
+use std::{path::Path, process::{Command, Stdio}, io::Write};
+
+use ast::Machine;
+
+mod ast;
+mod parser;
+
+fn write(mach: &Machine, tmpdir: &Path, id: usize) {
+    let path = tmpdir.join(format!("{id}.png"));
+    let mut cmd = Command::new("dot")
+        .args(&["-Tpng", "-o"])
+        .arg(path)
+        .stdin(Stdio::piped())
+        .spawn()
+        .unwrap();
+
+    let mut stdin = cmd.stdin.take().unwrap();
+
+    writeln!(&mut stdin, "{mach}").unwrap();
+    drop(stdin);
+
+    cmd.wait().unwrap();
+}
+
+fn compile(out: &Path, tmpdir: &Path, num: usize) {
+    let size = imgsize(&tmpdir.join(format!("{}.png", num - 1)));
+    let mut cmd = Command::new("convert");
+    for i in 0..num {
+	let path = tmpdir.join(format!("{i}.png"));
+	cmd.args(&["-delay", "100"]).arg(path);
+    }
+    let st = cmd
+        .args(&["-extent", &size])
+	.arg(out)
+	.status()
+	.unwrap()
+	.success();
+
+    if !st {
+	panic!("not ok");
+    }
+}
+
+fn imgsize(path: &Path) -> String {
+    let data = Command::new("identify")
+        .args(&["-format", "%wx%h"])
+	.arg(path)
+	.output()
+	.unwrap()
+	.stdout;
+    String::from_utf8(data).unwrap()
+}
+
+fn main() {
+    let tmpdir = std::env::temp_dir();
+    let input = "011";
+    let s = std::fs::read_to_string("test.dv").expect("failed to read");
+    let mut mach = crate::parser::parse(&s).expect("failed to parse");
+
+    write(&mach, &tmpdir, 0);
+    let mut id = 1;
+
+    for ch in input.chars() {
+	if !mach.feed(ch) {
+	    break;
+	}
+
+	write(&mach, &tmpdir, id);
+	id = id + 1;
+    }
+
+    let out = Path::new("test.gif");
+    compile(out, &tmpdir, id);
+
+}
blob - /dev/null
blob + 71fd85b7c74a163cd6f80dacc8695839566c9c77 (mode 644)
--- /dev/null
+++ src/parser.rs
@@ -0,0 +1,82 @@
+use std::collections::BTreeMap;
+
+use pest::{error::Error, iterators::Pair, Parser};
+use pest_derive::Parser;
+
+use crate::ast::{Edge, Machine, Node};
+
+#[derive(Parser)]
+#[grammar = "grammar.pest"]
+struct DotVisParser;
+
+fn parse_value(pair: Pair<Rule>) -> Machine {
+    let mut iter = pair.into_inner();
+    let nodes_iter = iter.next().unwrap().into_inner();
+    let edges_iter = iter.next().unwrap().into_inner();
+
+    let mut nodes = BTreeMap::new();
+    let mut edges = Vec::new();
+    let mut begin = None;
+
+    for node in nodes_iter {
+	let mut iter = node.into_inner();
+	let name = iter.next().unwrap().as_str().to_string();
+	let specs = iter.next().unwrap();
+	let mut end = false;
+	for spec in specs.into_inner() {
+	    match spec.as_str() {
+		"begin" if begin.is_some() => panic!("begin already specified"),
+		"begin" => begin = Some(name.clone()),
+		"end" => end = true,
+		spec => panic!("invalid spec: {spec}"),
+	    }
+	}
+	nodes.entry(name)
+	    .and_modify(|n: &mut Node| n.end = end)
+	    .or_insert(Node { end });
+    }
+
+    for edge in edges_iter {
+	let mut iter = edge.into_inner();
+	let from = iter.next().unwrap().as_str().to_string();
+	let to = iter.next().unwrap().as_str().to_string();
+	let chars = iter
+	    .next()
+	    .unwrap()
+	    .as_str()
+	    .strip_prefix('"')
+	    .unwrap()
+	    .strip_suffix('"')
+	    .unwrap()
+	    .chars()
+	    .collect();
+	nodes
+	    .entry(from.clone())
+	    .or_insert(Node::default());
+	nodes
+	    .entry(to.clone())
+	    .or_insert(Node::default());
+
+	let e = Edge {
+	    from,
+	    to,
+	    chars,
+	};
+	edges.push(e);
+    }
+
+    let begin = begin.unwrap_or_else(|| edges.first().unwrap().from.clone());
+    Machine {
+	current: begin.clone(),
+	begin,
+	nodes,
+	edges,
+	consumed: Vec::new(),
+    }
+}
+
+pub fn parse(s: &str) -> Result<Machine, Error<Rule>> {
+    let x = DotVisParser::parse(Rule::file, s)?.next().unwrap();
+
+    Ok(parse_value(x))
+}