mirror of
https://github.com/ParkerTenBroeck/automata.git
synced 2026-06-07 05:28:45 -04:00
added web UI
This commit is contained in:
parent
48f0c6eed8
commit
f1b8c08e8f
15 changed files with 1114 additions and 78 deletions
|
|
@ -5,9 +5,10 @@
|
||||||
# Replace llvmPackages with llvmPackages_X, where X is the latest LLVM version (at the time of writing, 16)
|
# Replace llvmPackages with llvmPackages_X, where X is the latest LLVM version (at the time of writing, 16)
|
||||||
llvmPackages.bintools
|
llvmPackages.bintools
|
||||||
rustup
|
rustup
|
||||||
wasm-bindgen-cli_0_2_100
|
# wasm-bindgen-cli_0_2_100
|
||||||
wasm-pack
|
wasm-pack
|
||||||
binaryen
|
binaryen
|
||||||
|
simple-http-server
|
||||||
];
|
];
|
||||||
RUSTC_VERSION = "nightly";
|
RUSTC_VERSION = "nightly";
|
||||||
# https://github.com/rust-lang/rust-bindgen#environment-variables
|
# https://github.com/rust-lang/rust-bindgen#environment-variables
|
||||||
|
|
|
||||||
|
|
@ -353,10 +353,12 @@ impl TransitionTable {
|
||||||
Some(symbol)
|
Some(symbol)
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
transitions_map
|
if !transitions_map
|
||||||
.entry((state, char, stack_symbol))
|
.entry((state, char, stack_symbol))
|
||||||
.or_insert(Vec::new())
|
.or_insert(HashSet::new())
|
||||||
.push((next_state, stack))
|
.insert((next_state, stack)) {
|
||||||
|
logs.emit_warning("duplicate transition", item.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TL::Assignment(S(Dest::Function(S(name, _), _), dest_s), _) => {
|
TL::Assignment(S(Dest::Function(S(name, _), _), dest_s), _) => {
|
||||||
|
|
@ -403,7 +405,7 @@ impl TransitionTable {
|
||||||
let initial_state = match initial_state {
|
let initial_state = match initial_state {
|
||||||
Some(some) => some,
|
Some(some) => some,
|
||||||
None => {
|
None => {
|
||||||
if let Some(initial) = states.get("z0") {
|
if let Some(initial) = states.get("q0") {
|
||||||
logs.emit_warning_locless("initial state not defined, defaulting to 'q0'");
|
logs.emit_warning_locless("initial state not defined, defaulting to 'q0'");
|
||||||
*initial
|
*initial
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,18 @@ impl<'a> Logs<'a> {
|
||||||
entry,
|
entry,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn entries(&self) -> &[LogEntry]{
|
||||||
|
&self.logs
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_entries(self) -> impl Iterator<Item = LogEntry>{
|
||||||
|
self.logs.into_iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn src(&self) -> &str{
|
||||||
|
&self.src
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum LogLevel {
|
pub enum LogLevel {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ impl<'a> Parser<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expect_token(&mut self, expected: Token<'a>) -> (bool, Span) {
|
fn expect_token(&mut self, expected: Token<'a>) -> (bool, Span) {
|
||||||
if let Some(Spanned(token, span)) = self.next_token() {
|
if let Some(Spanned(token, span)) = self.peek_token() {
|
||||||
if token != expected {
|
if token != expected {
|
||||||
self.logs.emit_error(
|
self.logs.emit_error(
|
||||||
format!("unexpected token {:#}, expected {:}", token, expected),
|
format!("unexpected token {:#}, expected {:}", token, expected),
|
||||||
|
|
@ -52,6 +52,7 @@ impl<'a> Parser<'a> {
|
||||||
);
|
);
|
||||||
(false, span)
|
(false, span)
|
||||||
} else {
|
} else {
|
||||||
|
self.next_token();
|
||||||
(true, span)
|
(true, span)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -212,27 +213,26 @@ impl<'a> Parser<'a> {
|
||||||
pub fn parse_elements(mut self) -> (Vec<Spanned<TopLevel<'a>>>, Logs<'a>) {
|
pub fn parse_elements(mut self) -> (Vec<Spanned<TopLevel<'a>>>, Logs<'a>) {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
loop {
|
while let Some(next) = self.next_token() {
|
||||||
let Some(next) = self.next_token() else { break };
|
|
||||||
match (next, self.peek_token()) {
|
match (next, self.peek_token()) {
|
||||||
(Spanned(Token::Ident(ident), start), Some(Spanned(Token::LPar, _))) => {
|
(Spanned(Token::Ident(ident), start), Some(Spanned(Token::LPar, _))) => {
|
||||||
let tuple = self.parse_tupple();
|
let tuple = self.parse_tupple();
|
||||||
let span = start.join(tuple.1);
|
let span = start.join(tuple.1);
|
||||||
let dest = Spanned(Dest::Function(Spanned(ident, start), tuple), span);
|
let dest = Spanned(Dest::Function(Spanned(ident, start), tuple), span);
|
||||||
self.expect_token(Token::Eq);
|
if !self.expect_token(Token::Eq).0{continue;}
|
||||||
let item = self.parse_item();
|
let item = self.parse_item();
|
||||||
let span = start.join(item.1);
|
let span = start.join(item.1);
|
||||||
result.push(Spanned(TopLevel::Assignment(dest, item), span));
|
result.push(Spanned(TopLevel::Assignment(dest, item), span));
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
Spanned(Token::Ident(_), _),
|
Spanned(Token::Ident(_), start),
|
||||||
Some(Spanned(Token::LSmallArrow | Token::Ident(_), _)),
|
Some(Spanned(Token::LSmallArrow, end)),
|
||||||
) => {
|
) => {
|
||||||
todo!()
|
self.logs.emit_error("Production rules are not yet supported", start.join(end));
|
||||||
}
|
}
|
||||||
(Spanned(Token::Ident(ident), start), _) => {
|
(Spanned(Token::Ident(ident), start), _) => {
|
||||||
let dest = Spanned(Dest::Ident(ident), start);
|
let dest = Spanned(Dest::Ident(ident), start);
|
||||||
self.expect_token(Token::Eq);
|
if !self.expect_token(Token::Eq).0{continue;}
|
||||||
let item = self.parse_item();
|
let item = self.parse_item();
|
||||||
let span = start.join(item.1);
|
let span = start.join(item.1);
|
||||||
result.push(Spanned(TopLevel::Assignment(dest, item), span));
|
result.push(Spanned(TopLevel::Assignment(dest, item), span));
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ name = "automata-web"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
automata = {path=".."}
|
automata = {path=".."}
|
||||||
console_error_panic_hook = "0.1.7"
|
console_error_panic_hook = "0.1.7"
|
||||||
|
|
|
||||||
15
web/build.sh
15
web/build.sh
|
|
@ -1,15 +1,2 @@
|
||||||
|
|
||||||
|
wasm-pack build --release --target web --no-typescript --out-dir root/automata --no-pack
|
||||||
CRATE_NAME="automata-web"
|
|
||||||
TARGET_NAME="automata-web"
|
|
||||||
OUT_FILE_NAME="./root/automata.wasm"
|
|
||||||
mkdir root
|
|
||||||
TARGET="../target"
|
|
||||||
|
|
||||||
|
|
||||||
cargo build --package automata-web --target wasm32-unknown-unknown --release
|
|
||||||
TARGET_NAME="${CRATE_NAME}.wasm"
|
|
||||||
WASM_PATH="${TARGET}/wasm32-unknown-unknown/release/$TARGET_NAME"
|
|
||||||
|
|
||||||
wasm-bindgen ${WASM_PATH} --out-dir root --out-name ${OUT_FILE_NAME} --no-modules --no-typescript
|
|
||||||
wasm-opt ${OUT_FILE_NAME} -O2 --fast-math -g -o ${OUT_FILE_NAME}
|
|
||||||
372
web/root/editor.css
Normal file
372
web/root/editor.css
Normal file
|
|
@ -0,0 +1,372 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
background: #909090;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: var(--topH, 50vh) 8px 1fr;
|
||||||
|
/* top pane, splitter, editor */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Top pane: terminal area */
|
||||||
|
.topPane {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--termW, 50vw) 8px 1fr;
|
||||||
|
/* terminal, splitter, filler */
|
||||||
|
min-height: 80px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bottom pane: editor */
|
||||||
|
.bottomPane {
|
||||||
|
min-height: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make editor fill its pane */
|
||||||
|
#editor {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.terminal {
|
||||||
|
background: #0b0f14;
|
||||||
|
color: #c9d1d9;
|
||||||
|
padding: 1em;
|
||||||
|
margin: 0px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||||
|
font-size: 12.5px;
|
||||||
|
line-height: 1.35;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ANSI text styles */
|
||||||
|
.ansi-bold { font-weight: 700; }
|
||||||
|
.ansi-dim { opacity: 0.7; }
|
||||||
|
|
||||||
|
/* Foreground colors (standard + bright) */
|
||||||
|
.ansi-fg-30 { color: #0b0f14; } /* black */
|
||||||
|
.ansi-fg-31 { color: #ff7b72; } /* red */
|
||||||
|
.ansi-fg-32 { color: #7ee787; } /* green */
|
||||||
|
.ansi-fg-33 { color: #f2cc60; } /* yellow */
|
||||||
|
.ansi-fg-34 { color: #79c0ff; } /* blue */
|
||||||
|
.ansi-fg-35 { color: #d2a8ff; } /* magenta */
|
||||||
|
.ansi-fg-36 { color: #a5d6ff; } /* cyan */
|
||||||
|
.ansi-fg-37 { color: #c9d1d9; } /* white */
|
||||||
|
|
||||||
|
.ansi-fg-90 { color: #6e7681; } /* bright black / gray */
|
||||||
|
.ansi-fg-91 { color: #ffa198; }
|
||||||
|
.ansi-fg-92 { color: #a6f3a6; }
|
||||||
|
.ansi-fg-93 { color: #ffe082; }
|
||||||
|
.ansi-fg-94 { color: #a5d6ff; }
|
||||||
|
.ansi-fg-95 { color: #e3b8ff; }
|
||||||
|
.ansi-fg-96 { color: #c7f0ff; }
|
||||||
|
.ansi-fg-97 { color: #ffffff; }
|
||||||
|
|
||||||
|
/* Background colors (optional) */
|
||||||
|
.ansi-bg-40 { background: #0b0f14; }
|
||||||
|
.ansi-bg-41 { background: rgba(255, 123, 114, 0.22); }
|
||||||
|
.ansi-bg-42 { background: rgba(126, 231, 135, 0.18); }
|
||||||
|
.ansi-bg-43 { background: rgba(242, 204, 96, 0.18); }
|
||||||
|
.ansi-bg-44 { background: rgba(121, 192, 255, 0.18); }
|
||||||
|
.ansi-bg-45 { background: rgba(210, 168, 255, 0.18); }
|
||||||
|
.ansi-bg-46 { background: rgba(165, 214, 255, 0.18); }
|
||||||
|
.ansi-bg-47 { background: rgba(201, 209, 217, 0.10); }
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* App layout */
|
||||||
|
.app {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: var(--canvasH, 35vh) 8px 1fr;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Canvas ---------- */
|
||||||
|
.canvasPane {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
#canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Bottom area (terminal + editor) ---------- */
|
||||||
|
/* Bottom area (terminal + editor) */
|
||||||
|
.bottomPane {
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 8px var(--termW, 40vw);
|
||||||
|
overflow: hidden;
|
||||||
|
/* IMPORTANT */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Terminal side */
|
||||||
|
.terminalPane {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
/* terminal scrolls */
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor side */
|
||||||
|
.editorPane {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
/* let CodeMirror scroll */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CodeMirror mount point */
|
||||||
|
#editor {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* VERY IMPORTANT: force CodeMirror to respect container height */
|
||||||
|
.cm-editor {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CodeMirror’s internal scroller (this is where the scrollbar lives) */
|
||||||
|
.cm-scroller {
|
||||||
|
overflow-y: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Splitters ---------- */
|
||||||
|
.hSplit {
|
||||||
|
cursor: row-resize;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vSplit {
|
||||||
|
cursor: col-resize;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hSplit:hover,
|
||||||
|
.vSplit:hover {
|
||||||
|
background: rgba(121, 192, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.diag {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diag li {
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Syntax colors via CSS classes applied by decorations --- */
|
||||||
|
.tok-comment {
|
||||||
|
color: #1a7b24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-keyword {
|
||||||
|
color: #b99400;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-error {
|
||||||
|
color: #ff0505;
|
||||||
|
font-weight: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-ident {
|
||||||
|
color: #90d4e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-brace {
|
||||||
|
color: #d73a49;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-punc {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tok-string {
|
||||||
|
color: #03621e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rainbow bracket depth classes */
|
||||||
|
.rb-0 {
|
||||||
|
color: #a35;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rb-1 {
|
||||||
|
color: #ed0;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rb-2 {
|
||||||
|
color: #9d5;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rb-3 {
|
||||||
|
color: #2cb;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rb-4 {
|
||||||
|
color: #36b;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rb-5 {
|
||||||
|
color: #639;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Severity underline styles */
|
||||||
|
.cm-diag-error {
|
||||||
|
text-decoration: underline wavy #d73a49;
|
||||||
|
/* red */
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-diag-warning {
|
||||||
|
text-decoration: underline wavy #ffd33d;
|
||||||
|
/* yellow */
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-diag-info {
|
||||||
|
text-decoration: underline wavy #79c0ff;
|
||||||
|
/* cyan-ish */
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip title coloring by severity */
|
||||||
|
.tipTitle.error {
|
||||||
|
color: #d73a49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tipTitle.warning {
|
||||||
|
color: #ffd33d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tipTitle.info {
|
||||||
|
color: #79c0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: diagnostics panel coloring */
|
||||||
|
.diag li.error {
|
||||||
|
color: #d73a49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diag li.warning {
|
||||||
|
color: #b08800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diag li.info {
|
||||||
|
color: #0366d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip styling */
|
||||||
|
.cm-tooltip.cm-tooltip-hover {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: black;
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
max-width: 420px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tipTitle {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tipBody {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* Loading screen */
|
||||||
|
|
||||||
|
.centered {
|
||||||
|
margin-right: auto;
|
||||||
|
margin-left: auto;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: #f0f0f0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: Ubuntu-Light, Helvetica, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lds-dual-ring {
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lds-dual-ring:after {
|
||||||
|
content: " ";
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 0px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
border-color: #fff transparent #fff transparent;
|
||||||
|
animation: lds-dual-ring 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes lds-dual-ring {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
400
web/root/editor.js
Normal file
400
web/root/editor.js
Normal file
|
|
@ -0,0 +1,400 @@
|
||||||
|
import { EditorView, keymap, hoverTooltip, Decoration, ViewPlugin } from "https://esm.sh/@codemirror/view";
|
||||||
|
import { EditorState, StateField } from "https://esm.sh/@codemirror/state";
|
||||||
|
import { defaultKeymap, history, historyKeymap } from "https://esm.sh/@codemirror/commands";
|
||||||
|
import { lineNumbers, highlightActiveLineGutter } from "https://esm.sh/@codemirror/view";
|
||||||
|
import { bracketMatching, indentOnInput } from "https://esm.sh/@codemirror/language";
|
||||||
|
import { closeBrackets } from "https://esm.sh/@codemirror/autocomplete";
|
||||||
|
import { oneDark } from "https://esm.sh/@codemirror/theme-one-dark";
|
||||||
|
|
||||||
|
import wasm from "./wasm.js"
|
||||||
|
|
||||||
|
function tokenize(text) {
|
||||||
|
try{
|
||||||
|
return wasm.lex(text);
|
||||||
|
}catch(e){
|
||||||
|
console.log(e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function compile(text) {
|
||||||
|
try{
|
||||||
|
return wasm.compile(text);
|
||||||
|
}catch(e){
|
||||||
|
console.log(e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* ===================================================================== */
|
||||||
|
|
||||||
|
// Map token types -> CSS classes
|
||||||
|
const tokenClass = (t) =>
|
||||||
|
({
|
||||||
|
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");
|
||||||
|
|
||||||
|
// ===================== Diagnostics helpers =====================
|
||||||
|
function severityClass(sev) {
|
||||||
|
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) {
|
||||||
|
if (sev === "error") return 3;
|
||||||
|
if (sev === "warning") return 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
function sevLabel(sev) {
|
||||||
|
if (sev === "warning") return "warning";
|
||||||
|
if (sev === "info") return "info";
|
||||||
|
return "error";
|
||||||
|
}
|
||||||
|
function sevAnsiColorClass(sev) {
|
||||||
|
if (sev === "warning") return "ansi-yellow";
|
||||||
|
if (sev === "info") return "ansi-cyan";
|
||||||
|
return "ansi-red";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function buildAnalysis(text, doc) {
|
||||||
|
const tokens = tokenize(text);
|
||||||
|
const {log, log_formatted} = compile(text);
|
||||||
|
|
||||||
|
// Build ONE Decoration set: syntax + diagnostics
|
||||||
|
const marks = [];
|
||||||
|
const docLen = doc.length;
|
||||||
|
|
||||||
|
for (const tok of tokens) {
|
||||||
|
const start = Math.max(0, Math.min(docLen, tok.start));
|
||||||
|
const end = Math.max(start, Math.min(docLen, tok.end));
|
||||||
|
var tc = tokenClass(tok.kind);
|
||||||
|
if (tc === "rb-"){
|
||||||
|
tc += tok.scope_level.toString();
|
||||||
|
}
|
||||||
|
if (end > start) {
|
||||||
|
marks.push(Decoration.mark({ class: tc }).range(start, end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const d of log) {
|
||||||
|
if (d.start === undefined || d.end === undefined)continue;
|
||||||
|
const start = Math.max(0, Math.min(docLen, d.start));
|
||||||
|
const endRaw = d.end == null ? d.start : d.end;
|
||||||
|
const end = Math.max(start, Math.min(docLen, endRaw));
|
||||||
|
const cls = severityClass(d.level);
|
||||||
|
if (end > start) {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deco = Decoration.set(marks, true);
|
||||||
|
return { tokens, log, log_formatted, deco };
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===================== Hover tooltip (uses cached diags) =====================
|
||||||
|
const diagHover = hoverTooltip((view, pos) => {
|
||||||
|
const { log } = view.state.field(analysisField);
|
||||||
|
const hits = log.filter((d) => 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]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pos,
|
||||||
|
end: pos,
|
||||||
|
above: true,
|
||||||
|
create() {
|
||||||
|
const dom = document.createElement("div");
|
||||||
|
dom.className = "cm-tooltip cm-tooltip-hover";
|
||||||
|
|
||||||
|
const title = document.createElement("div");
|
||||||
|
title.className = `tipTitle ${top.level}`;
|
||||||
|
title.textContent =
|
||||||
|
hits.length === 1 ? top.level.toUpperCase() : `${top.level.toUpperCase()} (${hits.length})`;
|
||||||
|
|
||||||
|
const body = document.createElement("div");
|
||||||
|
body.className = "tipBody";
|
||||||
|
body.textContent = hits
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => sevRank(b.level) - sevRank(a.level))
|
||||||
|
.map((h) => `[${h.level.toUpperCase()}] ${h.message}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
dom.appendChild(title);
|
||||||
|
dom.appendChild(body);
|
||||||
|
return { dom };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function ansiToHtml(input) {
|
||||||
|
const ESC_RE = /\x1b\[([0-9;]*)m/g;
|
||||||
|
|
||||||
|
let out = "";
|
||||||
|
let lastIndex = 0;
|
||||||
|
|
||||||
|
// current style state
|
||||||
|
let fg = null; // e.g. 31, 92
|
||||||
|
let bg = null; // e.g. 41
|
||||||
|
let bold = false;
|
||||||
|
let dim = false;
|
||||||
|
|
||||||
|
function openSpanIfNeeded(text) {
|
||||||
|
if (text.length === 0) return "";
|
||||||
|
const classes = [];
|
||||||
|
if (bold) classes.push("ansi-bold");
|
||||||
|
if (dim) classes.push("ansi-dim");
|
||||||
|
if (fg != null) classes.push(`ansi-fg-${fg}`);
|
||||||
|
if (bg != null) classes.push(`ansi-bg-${bg}`);
|
||||||
|
if (classes.length === 0) return escapeHtml(text);
|
||||||
|
return `<span class="${classes.join(" ")}">${escapeHtml(text)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCodes(codes) {
|
||||||
|
if (codes.length === 0) codes = [0];
|
||||||
|
for (const c of codes) {
|
||||||
|
const code = Number(c);
|
||||||
|
if (Number.isNaN(code)) continue;
|
||||||
|
|
||||||
|
if (code === 0) {
|
||||||
|
fg = null; bg = null; bold = false; dim = false;
|
||||||
|
} else if (code === 1) {
|
||||||
|
bold = true;
|
||||||
|
} else if (code === 2) {
|
||||||
|
dim = true;
|
||||||
|
} else if (code === 22) {
|
||||||
|
bold = false; dim = false;
|
||||||
|
} else if (code === 39) {
|
||||||
|
fg = null;
|
||||||
|
} else if (code === 49) {
|
||||||
|
bg = null;
|
||||||
|
} else if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {
|
||||||
|
fg = code;
|
||||||
|
} else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {
|
||||||
|
bg = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let m;
|
||||||
|
while ((m = ESC_RE.exec(input)) !== null) {
|
||||||
|
const chunk = input.slice(lastIndex, m.index);
|
||||||
|
out += openSpanIfNeeded(chunk);
|
||||||
|
|
||||||
|
const codes = m[1] ? m[1].split(";") : [];
|
||||||
|
applyCodes(codes);
|
||||||
|
|
||||||
|
lastIndex = ESC_RE.lastIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
out += openSpanIfNeeded(input.slice(lastIndex));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTerminal(view) {
|
||||||
|
const term = document.getElementById("terminal");
|
||||||
|
if (!term) return;
|
||||||
|
|
||||||
|
const { log, log_formatted } = view.state.field(analysisField);
|
||||||
|
|
||||||
|
let s = "";
|
||||||
|
s += `\x1b[90m[compile]\x1b[0m ${log.length} diagnostics\n`;
|
||||||
|
|
||||||
|
term.innerHTML = ansiToHtml(s+log_formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
const terminalPlugin = ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
constructor(view) {
|
||||||
|
this.view = view;
|
||||||
|
formatTerminal(view);
|
||||||
|
}
|
||||||
|
update(update) {
|
||||||
|
if (update.docChanged) formatTerminal(update.view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ===================== Build editor =====================
|
||||||
|
const initialText = `machine=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 state = EditorState.create({
|
||||||
|
doc: initialText,
|
||||||
|
extensions: [
|
||||||
|
lineNumbers(),
|
||||||
|
highlightActiveLineGutter(),
|
||||||
|
history(),
|
||||||
|
indentOnInput(),
|
||||||
|
bracketMatching(),
|
||||||
|
closeBrackets(),
|
||||||
|
keymap.of([...defaultKeymap, ...historyKeymap]),
|
||||||
|
oneDark,
|
||||||
|
|
||||||
|
analysisField,
|
||||||
|
diagHover,
|
||||||
|
terminalPlugin,
|
||||||
|
|
||||||
|
EditorView.lineWrapping,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
window.editor = new EditorView({
|
||||||
|
state,
|
||||||
|
parent: document.getElementById("editor"),
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function setDefaultLayoutWeights() {
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
const vw = window.innerWidth;
|
||||||
|
|
||||||
|
// Canvas: 30% of screen height
|
||||||
|
const canvasH = Math.round(vh * 0.60);
|
||||||
|
|
||||||
|
// Terminal: 35% of width
|
||||||
|
const termW = Math.round(vw * 0.30);
|
||||||
|
|
||||||
|
const app = document.getElementById("app");
|
||||||
|
app.style.setProperty("--canvasH", `${canvasH}px`);
|
||||||
|
app.style.setProperty("--termW", `${termW}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDefaultLayoutWeights();
|
||||||
|
|
||||||
|
|
||||||
|
(function enableLayoutSplitters() {
|
||||||
|
const app = document.getElementById("app");
|
||||||
|
const hSplit = document.getElementById("hSplit");
|
||||||
|
const vSplit = document.getElementById("vSplit");
|
||||||
|
const canvas = document.getElementById("canvas");
|
||||||
|
const canvasPane = document.getElementById("canvasPane");
|
||||||
|
|
||||||
|
let draggingH = false;
|
||||||
|
let draggingV = false;
|
||||||
|
|
||||||
|
// --- Canvas height splitter ---
|
||||||
|
hSplit.addEventListener("mousedown", (e) => {
|
||||||
|
draggingH = true;
|
||||||
|
document.body.style.cursor = "row-resize";
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Terminal/editor width splitter ---
|
||||||
|
vSplit.addEventListener("mousedown", (e) => {
|
||||||
|
draggingV = true;
|
||||||
|
document.body.style.cursor = "col-resize";
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
function resizeCanvasToPane() {
|
||||||
|
// Keep canvas resolution in sync with CSS size (crisp rendering)
|
||||||
|
const rect = canvasPane.getBoundingClientRect();
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const w = Math.max(1, Math.floor(rect.width * dpr));
|
||||||
|
const h = Math.max(1, Math.floor(rect.height * dpr));
|
||||||
|
if (canvas.width !== w) canvas.width = w;
|
||||||
|
if (canvas.height !== h) canvas.height = h;
|
||||||
|
|
||||||
|
// Optional: demo draw
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
ctx.fillStyle = "#1f6feb";
|
||||||
|
ctx.fillRect(20 * dpr, 20 * dpr, 140 * dpr, 60 * dpr);
|
||||||
|
ctx.fillStyle = "#c9d1d9";
|
||||||
|
ctx.font = `${14 * dpr}px ui-monospace, monospace`;
|
||||||
|
ctx.fillText("Canvas area", 30 * dpr, 55 * dpr);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", (e) => {
|
||||||
|
const rect = app.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (draggingH) {
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
const minCanvas = 80;
|
||||||
|
const minBottom = 180;
|
||||||
|
const maxCanvas = rect.height - 8 - minBottom;
|
||||||
|
const canvasH = Math.max(minCanvas, Math.min(maxCanvas, y));
|
||||||
|
app.style.setProperty("--canvasH", `${canvasH}px`);
|
||||||
|
resizeCanvasToPane();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draggingV) {
|
||||||
|
const bottomPane = document.getElementById("bottomPane");
|
||||||
|
const r = bottomPane.getBoundingClientRect();
|
||||||
|
const x = e.clientX - r.left;
|
||||||
|
const minTerm = 220;
|
||||||
|
const maxTerm = r.width - 8 - 220;
|
||||||
|
const termW = Math.max(minTerm, Math.min(maxTerm, r.width-x));
|
||||||
|
app.style.setProperty("--termW", `${termW}px`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("mouseup", () => {
|
||||||
|
draggingH = false;
|
||||||
|
draggingV = false;
|
||||||
|
document.body.style.cursor = "";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep canvas crisp on window resize too
|
||||||
|
window.addEventListener("resize", resizeCanvasToPane);
|
||||||
|
resizeCanvasToPane();
|
||||||
|
})();
|
||||||
45
web/root/index.html
Normal file
45
web/root/index.html
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Automata</title>
|
||||||
|
<link href="editor.css" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="centered" id="center_text">
|
||||||
|
<p style="font-size:16px">
|
||||||
|
Loading…
|
||||||
|
</p>
|
||||||
|
<div class="lds-dual-ring"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="app" style="display:none" id="app">
|
||||||
|
<section class="canvasPane" id="canvasPane">
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="hSplit" id="hSplit" title="Drag to resize canvas height"></div>
|
||||||
|
|
||||||
|
<section class="bottomPane" id="bottomPane">
|
||||||
|
|
||||||
|
<main class="editorPane">
|
||||||
|
<div id="editor"></div>
|
||||||
|
</main>
|
||||||
|
<div class="vSplit" id="vSplit" title="Drag to resize terminal/editor width"></div>
|
||||||
|
|
||||||
|
<div class="pane terminalPane">
|
||||||
|
<pre id="terminal" class="terminal"></pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="module" src="index.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
3
web/root/index.js
Normal file
3
web/root/index.js
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import wasm from "./wasm.js"
|
||||||
|
import "./editor.js"
|
||||||
|
|
||||||
15
web/root/package.json
Normal file
15
web/root/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "automata-web",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"files": [
|
||||||
|
"automata_bg.wasm",
|
||||||
|
"automata.js",
|
||||||
|
"automata_bg.js"
|
||||||
|
],
|
||||||
|
"main": "automata.js",
|
||||||
|
"sideEffects": [
|
||||||
|
"./automata.js",
|
||||||
|
"./snippets/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
26
web/root/wasm.js
Normal file
26
web/root/wasm.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
console.debug("Loading wasm…");
|
||||||
|
import init, * as wasm from "./automata/automata_web.js";
|
||||||
|
try{
|
||||||
|
console.debug("Wasm loaded. Starting app…");
|
||||||
|
window.wasm = wasm;
|
||||||
|
await init();
|
||||||
|
console.debug("App started.");
|
||||||
|
document.getElementById("center_text").innerHTML = '';
|
||||||
|
document.getElementById("app").style.display = '';
|
||||||
|
wasm.init();
|
||||||
|
}catch(e){
|
||||||
|
console.error("Failed to start: " + error);
|
||||||
|
document.getElementById("the_canvas_id").remove();
|
||||||
|
document.getElementById("center_text").innerHTML = `
|
||||||
|
<p>
|
||||||
|
An error occurred during loading:
|
||||||
|
</p>
|
||||||
|
<p style="font-family:Courier New">
|
||||||
|
${error}
|
||||||
|
</p>
|
||||||
|
<p style="font-size:14px">
|
||||||
|
Make sure you use a modern browser with WebGL and WASM enabled.
|
||||||
|
</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default wasm
|
||||||
2
web/run.sh
Executable file
2
web/run.sh
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
cd root
|
||||||
|
simple-http-server
|
||||||
219
web/src/lib.rs
Normal file
219
web/src/lib.rs
Normal file
|
|
@ -0,0 +1,219 @@
|
||||||
|
use automata::{
|
||||||
|
automata::npda::{self, NPDA},
|
||||||
|
loader::{self, Span, Spanned, lexer::Lexer},
|
||||||
|
};
|
||||||
|
|
||||||
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn test() {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(start)]
|
||||||
|
pub fn main() {}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn init() {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn silly(machine: &str, input: &str) {
|
||||||
|
let table = match npda::TransitionTable::load_table(machine) {
|
||||||
|
Ok((ok, logs)) => {
|
||||||
|
for log in logs.displayable() {
|
||||||
|
println!("{log}")
|
||||||
|
}
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
Err(logs) => {
|
||||||
|
for log in logs.displayable() {
|
||||||
|
println!("{log}")
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("running on: '{input}'");
|
||||||
|
let mut simulator = npda::Simulator::begin(input, table);
|
||||||
|
loop {
|
||||||
|
match simulator.step() {
|
||||||
|
npda::SimulatorResult::Pending => {}
|
||||||
|
npda::SimulatorResult::Reject => {
|
||||||
|
println!("REJECTED");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
npda::SimulatorResult::Accept(npda) => {
|
||||||
|
println!("ACCEPT: {npda:?}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum Kind {
|
||||||
|
Ident = "ident",
|
||||||
|
Keyword = "keyword",
|
||||||
|
Error = "error",
|
||||||
|
Comment = "comment",
|
||||||
|
Punc = "punc",
|
||||||
|
|
||||||
|
LPar = "lpar",
|
||||||
|
LBrace = "lbrace",
|
||||||
|
LBracket = "lbracket",
|
||||||
|
|
||||||
|
RPar = "rpar",
|
||||||
|
RBrace = "rbrace",
|
||||||
|
RBracket = "rbracket",
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct Tok {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub scope_level: usize,
|
||||||
|
pub kind: Kind,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn lex(input: &str) -> Vec<Tok> {
|
||||||
|
let mut scope_level = 0;
|
||||||
|
let mut index_utf16 = 0;
|
||||||
|
let mut index_utf8 = 0;
|
||||||
|
Lexer::new(input)
|
||||||
|
.map(|Spanned(tok, Span(start_utf8, end_utf8))| {
|
||||||
|
let since_last = &input[index_utf8..start_utf8];
|
||||||
|
let since_start = &input[start_utf8..end_utf8];
|
||||||
|
|
||||||
|
index_utf8 = end_utf8;
|
||||||
|
let start = index_utf16 + since_last.chars().map(char::len_utf16).sum::<usize>();
|
||||||
|
let end = start + since_start.chars().map(char::len_utf16).sum::<usize>();
|
||||||
|
index_utf16 = end;
|
||||||
|
|
||||||
|
let Ok(tok) = tok else {
|
||||||
|
return Tok {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
kind: Kind::Error,
|
||||||
|
scope_level,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
use automata::loader::lexer::Token;
|
||||||
|
let kind = match tok {
|
||||||
|
Token::LPar => Kind::LPar,
|
||||||
|
Token::RPar => Kind::RPar,
|
||||||
|
Token::LBrace => Kind::LBrace,
|
||||||
|
Token::RBrace => Kind::RBrace,
|
||||||
|
Token::LBracket => Kind::LBracket,
|
||||||
|
Token::RBracket => Kind::RBracket,
|
||||||
|
Token::Tilde => Kind::Keyword,
|
||||||
|
Token::Eq => Kind::Punc,
|
||||||
|
Token::Comma => Kind::Punc,
|
||||||
|
Token::Or => Kind::Punc,
|
||||||
|
Token::Plus => Kind::Punc,
|
||||||
|
Token::Star => Kind::Punc,
|
||||||
|
Token::And => Kind::Punc,
|
||||||
|
Token::LSmallArrow => Kind::Punc,
|
||||||
|
Token::LBigArrow => Kind::Punc,
|
||||||
|
Token::Comment(_) => Kind::Comment,
|
||||||
|
Token::Ident(_)
|
||||||
|
if input[..start_utf8]
|
||||||
|
.split("\n")
|
||||||
|
.last()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.is_empty() =>
|
||||||
|
{
|
||||||
|
Kind::Keyword
|
||||||
|
}
|
||||||
|
Token::Ident(
|
||||||
|
loader::EPSILON_LOWER
|
||||||
|
| "epsilon"
|
||||||
|
| loader::DELTA_LOWER
|
||||||
|
| "delta"
|
||||||
|
| loader::GAMMA_UPPER
|
||||||
|
| "gamma"
|
||||||
|
| loader::GAMMA_LOWER
|
||||||
|
| loader::SIGMA_UPPER
|
||||||
|
| "sigma",
|
||||||
|
) => Kind::Keyword,
|
||||||
|
Token::Ident(_) => Kind::Ident,
|
||||||
|
};
|
||||||
|
|
||||||
|
let scope_level = match kind {
|
||||||
|
Kind::LPar | Kind::LBrace | Kind::LBracket => {
|
||||||
|
scope_level = scope_level.saturating_add(1);
|
||||||
|
scope_level.saturating_sub(1)
|
||||||
|
}
|
||||||
|
Kind::RPar | Kind::RBrace | Kind::RBracket => {
|
||||||
|
scope_level = scope_level.saturating_sub(1);
|
||||||
|
scope_level
|
||||||
|
}
|
||||||
|
_ => scope_level,
|
||||||
|
};
|
||||||
|
Tok {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
kind,
|
||||||
|
scope_level,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum LogLevel {
|
||||||
|
Info = "info",
|
||||||
|
Warning = "warning",
|
||||||
|
Error = "error",
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter_with_clone)]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CompileLog {
|
||||||
|
pub level: LogLevel,
|
||||||
|
pub message: String,
|
||||||
|
pub start: Option<usize>,
|
||||||
|
pub end: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter_with_clone)]
|
||||||
|
pub struct CompileResult{
|
||||||
|
pub log: Vec<CompileLog>,
|
||||||
|
pub log_formatted: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen]
|
||||||
|
pub fn compile(input: &str) -> CompileResult {
|
||||||
|
let log = match npda::TransitionTable::load_table(input) {
|
||||||
|
Ok((_, logs)) => logs,
|
||||||
|
Err(logs) => logs,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::fmt::Write;
|
||||||
|
let log_formatted = log.displayable().fold(String::new(), |mut s, e|{write!(&mut s, "{e}").unwrap(); s});
|
||||||
|
|
||||||
|
let log = log.into_entries()
|
||||||
|
.map(|e| CompileLog {
|
||||||
|
level: match e.level {
|
||||||
|
loader::log::LogLevel::Info => LogLevel::Info,
|
||||||
|
loader::log::LogLevel::Warning => LogLevel::Warning,
|
||||||
|
loader::log::LogLevel::Error => LogLevel::Error,
|
||||||
|
},
|
||||||
|
message: e.message,
|
||||||
|
start: e
|
||||||
|
.span
|
||||||
|
.map(|span| input[..span.0].chars().map(char::len_utf16).count()),
|
||||||
|
end: e
|
||||||
|
.span
|
||||||
|
.map(|span| input[..span.1].chars().map(char::len_utf16).count()),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
CompileResult { log, log_formatted }
|
||||||
|
}
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
use automata::automata::npda;
|
|
||||||
|
|
||||||
use web_sys::window;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
console_error_panic_hook::set_once();
|
|
||||||
|
|
||||||
let document = window()
|
|
||||||
.and_then(|win| win.document())
|
|
||||||
.expect("Could not access the document");
|
|
||||||
let body = document.body().expect("Could not access document.body");
|
|
||||||
let text_node = document.create_text_node("Hello, world from Vanilla Rust!");
|
|
||||||
body.append_child(text_node.as_ref())
|
|
||||||
.expect("Failed to append text");
|
|
||||||
}
|
|
||||||
|
|
||||||
// pub fn main() {
|
|
||||||
// let input = include_str!("../../example.npda");
|
|
||||||
|
|
||||||
// let table = match npda::TransitionTable::load_table(input) {
|
|
||||||
// Ok((ok, logs)) => {
|
|
||||||
// for log in logs.displayable() {
|
|
||||||
// println!("{log}")
|
|
||||||
// }
|
|
||||||
// ok
|
|
||||||
// }
|
|
||||||
// Err(logs) => {
|
|
||||||
// for log in logs.displayable() {
|
|
||||||
// println!("{log}")
|
|
||||||
// }
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// let input = "aababaab";
|
|
||||||
// println!("running on: '{input}'");
|
|
||||||
// let mut simulator = npda::Simulator::begin(input, table);
|
|
||||||
// loop {
|
|
||||||
// match simulator.step(){
|
|
||||||
// npda::SimulatorResult::Pending => {},
|
|
||||||
// npda::SimulatorResult::Reject => {
|
|
||||||
// println!("REJECTED");
|
|
||||||
// break;
|
|
||||||
// },
|
|
||||||
// npda::SimulatorResult::Accept(npda) => {
|
|
||||||
// println!("ACCEPT: {npda:?}");
|
|
||||||
// break;
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue