mindustry logic execution, map- and schematic- parsing and rendering
Diffstat (limited to 'lemu/src/parser.rs')
| -rw-r--r-- | lemu/src/parser.rs | 659 |
1 files changed, 659 insertions, 0 deletions
diff --git a/lemu/src/parser.rs b/lemu/src/parser.rs new file mode 100644 index 0000000..f81c71b --- /dev/null +++ b/lemu/src/parser.rs @@ -0,0 +1,659 @@ +use std::io::Write as Wr; + +use logos::Span; + +use super::{ + executor::{ExecutorBuilderInternal, Instruction, UPInstr}, + instructions::{ + draw::{ + Clear, Flush, Line, RectBordered, RectFilled, SetColorConst, SetColorDyn, SetStroke, + Triangle, + }, + io::{Print, Read, Write}, + AlwaysJump, ConditionOp, DynJump, End, Instr, Jump, MathOp1, MathOp2, Op1, Op2, Set, Stop, + }, + lexer::{Lexer, Token}, + memory::LAddress, +}; + +/// Errors returned when parsing fails. +#[derive(thiserror::Error, Debug)] +pub enum Error<'s> { + /// Occurs from eg `set x`. (needs a value to set to) + #[error("unexpected end of stream")] + UnexpectedEof, + /// Occurs from eg `op add\n...` (needs a variable) + #[error("expected variable, got {0:?}")] + ExpectedVar(Token<'s>, Span), + /// Occurs from eg `draw 4` (needs a ident of the type of drawing) + #[error("expected identifier, got {0:?}")] + ExpectedIdent(Token<'s>, Span), + /// Occurs from eg `jump house` (assuming house isnt a label). + #[error("expected jump target, got {0:?}")] + ExpectedJump(Token<'s>, Span), + /// Occurs from eg `op add "three" "four"` + #[error("expected number, got {0:?}")] + ExpectedNum(Token<'s>, Span), + /// Occurs from eg `op 4` (4 is not add/mul/...) + #[error("expected operator, got {0:?}")] + ExpectedOp(Token<'s>, Span), + /// Occurs from eg `write cell1 5.5` (5.5 is not int) + #[error("expected integer, got {0:?}")] + ExpectedInt(Token<'s>, Span), + /// Occurs from eg `4.0 add 5.0` + #[error("expected instruction, got {0:?}")] + ExpectedInstr(Token<'s>, Span), + /// Occurs from eg + /// ```text + /// lable: + /// jump label always + /// ``` + /// (typo: lable not label) + #[error("unable to find label {0:?}")] + LabelNotFound(&'s str, Span), + /// Occurs from eg `jump 4910294029 always` + #[error("unable to jump to instruction {0:?}")] + InvalidJump(Instruction, Span), + /// Occurs from eg `read bank9223372036854775807 5` (only `126` banks can exist) + #[error("cannot get cell>{0:?}")] + MemoryTooFar(usize, Span), + /// Occurs from `read register1` + #[error("unknown memory type {0:?}, expected (cell)|(bank)")] + InvalidMemoryType(&'s str, Span), + /// Occurs from `drawflush bank1` + #[error("unknown display type {0:?}, expected 'display'")] + InvalidDisplayType(&'s str, Span), + /// Occurs from `draw house` (or `draw image`, a valid but unsupported instruction here) + #[error("unknown image operation {0:?}")] + UnsupportedImageOp(&'s str, Span), + #[error("couldnt get display #{0:?}.")] + /// Occurs from eg `display 50`. + /// + /// call `display` 50 more times to have more display options: + /// ```rust,ignore + /// executor + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display() + /// .display(); + /// ``` + NoDisplay(usize, Span), +} + +macro_rules! tokstr { + ($tok:expr) => { + match $tok { + Token::Ident(i) => Some(i), + Token::GetLink => Some("getlink"), + Token::Read => Some("read"), + Token::Write => Some("write"), + Token::Set => Some("set"), + Token::Op => Some("op"), + Token::End => Some("end"), + Token::Draw => Some("draw"), + Token::DrawFlush => Some("drawflush"), + Token::Print => Some("print"), + Token::PackColor => Some("packcolor"), + Token::Jump => Some("jump"), + Token::Stop => Some("stop"), + Token::Counter => Some("@counter"), + Token::Equal => Some("equal"), + Token::NotEqual => Some("notEqual"), + Token::LessThan => Some("lessThan"), + Token::LessThanEq => Some("lessThanEq"), + Token::GreaterThan => Some("greaterThan"), + Token::GreaterThanEq => Some("greaterThanEq"), + Token::StrictEqual => Some("strictEqual"), + Token::Always => Some("always"), + Token::Add => Some("add"), + Token::Sub => Some("sub"), + Token::Mul => Some("mul"), + Token::Div => Some("div"), + Token::IDiv => Some("idiv"), + Token::Mod => Some("mod"), + Token::Pow => Some("pow"), + Token::And => Some("land"), + Token::Not => Some("not"), + Token::ShiftLeft => Some("shl"), + Token::ShiftRight => Some("shr"), + Token::BitOr => Some("or"), + Token::BitAnd => Some("and"), + Token::ExclusiveOr => Some("xor"), + Token::Max => Some("max"), + Token::Min => Some("min"), + Token::Angle => Some("angle"), + Token::AngleDiff => Some("angleDiff"), + Token::Len => Some("len"), + Token::Noise => Some("noise"), + Token::Abs => Some("abs"), + Token::Log => Some("log"), + Token::Log10 => Some("log10"), + Token::Floor => Some("floor"), + Token::Ceil => Some("ceil"), + Token::Sqrt => Some("sqrt"), + Token::Rand => Some("rand"), + Token::Sin => Some("sin"), + Token::Cos => Some("cos"), + Token::Tan => Some("tan"), + Token::ASin => Some("asin"), + Token::ACos => Some("acos"), + Token::ATan => Some("atan"), + _ => None, + } + }; +} + +impl Error<'_> { + /// Produces a [`Diagnostic`] from this error. + #[cfg(feature = "diagnose")] + pub fn diagnose<'s>( + &self, + source: &'s str, + fname: Option<&'s str>, + ) -> yumy::Diagnostic<yumy::Source<'s>> { + use yumy::{ + owo_colors::{OwoColorize, Style}, + Diagnostic, Label, Source, SourceSpan, + }; + + let error = "error".red(); + let note = "note".yellow(); + let help = "help".bright_green(); + let e_sty = Style::new().bright_red(); + macro_rules! err { + ($span:expr, $msg:literal $(, $args:expr)* $(,)?) => { + Label::styled(SourceSpan::new($span.start as u32, $span.end as u32), format!($msg $(, $args)*), e_sty) + }; + } + macro_rules! dig { + ($ms:literal $(, $args:expr)* $(,)?) => { + Diagnostic::new(format!($ms $(, $args)*)).with_source(Source::new(source, fname)) + }; + } + let mut d; + match self { + Error::UnexpectedEof => { + d = dig!("{error}: wasnt able to finish read").with_label(err!( + source.len() - 1..source.len() - 1, + "there was supposed to be another token here" + )); + } + Error::ExpectedVar(_, s) => { + d = dig!("{error}: expected a variable") + .with_label(err!(s, "this was supposed to be a variable")); + } + Error::ExpectedIdent(_, s) => { + d = dig!("{error}: expected a identifier") + .with_label(err!(s, "this was supposed to be a identifier")); + } + Error::ExpectedJump(t, s) => { + d = dig!("{error}: expected jump target") + .with_label(err!(s, "this was supposed to be a jump target")) + .with_footnote( + format!("{note}: a jump target is a label(ident), or a line number in integer form (not a float)"), + ); + if let Token::Num(n) = t { + d.add_footnote(format!("{help}: remove the fractional part: {n:.0}")); + } + } + Error::ExpectedNum(_, s) => { + d = dig!("{error}: expected number") + .with_label(err!(s, "this was supposed to be a number")); + } + Error::ExpectedOp(t, s) => { + d = dig!("{error}: expected operator") + .with_label(err!(s, "this was supposed to be a operator")); + if let Some(i) = tokstr!(*t) && let Some((mat,score)) = rust_fuzzy_search::fuzzy_search_best_n(i, crate::instructions::OPS, 1).first() && *score > 0.5 { + d.add_footnote(format!("{help}: maybe you meant {mat}")); + } + } + Error::ExpectedInt(t, s) => { + d = dig!("{error}: expected integer") + .with_label(err!(s, "this was supposed to be a integer")); + if let Token::Num(n) = t { + d.add_footnote(format!("{help}: remove the fractional part: {n:.0}")); + } + } + Error::ExpectedInstr(_, s) => { + d = dig!("{error}: expected instruction") + .with_label(err!(s, "this was supposed to be a instruction")); + // it occurs to me that this wont ever be a string, as idents are turned into `Code` + // if let Some(i) = tokstr!(t.clone()) && let Some((mat,score)) = rust_fuzzy_search::fuzzy_search_best_n(i, crate::instructions::INSTRS, 1).get(0) && *score > 0.5 { + // d.add_footnote(format!("{help}: maybe you meant {mat}")); + // } + } + Error::LabelNotFound(_, s) => { + d = dig!("{error}: label not found") + .with_label(err!(s, "this was supposed to be a (existing) label")); + } + Error::InvalidJump(Instruction(target), s) => { + d = dig!("{error}: invalid jump") + .with_label(err!(s, "line#{target} is not in the program")) + .with_footnote(format!( + "{help}: there are 0..{} available lines", + source.lines().count() + )); + } + Error::MemoryTooFar(b, s) => { + d = dig!("{error}: invalid memory cell/bank") + .with_label(err!(s, "cant get cell/bank#{b}")) + .with_footnote(format!("{note}: only 126 cells/banks are allowed")); + } + Error::InvalidMemoryType(t, s) => { + d = dig!("{error}: invalid memory type") + .with_label(err!(s, "cant get {t}")) + .with_footnote(format!("{note}: only banks/cells are allowed")); + } + Error::InvalidDisplayType(disp, s) => { + d = dig!("{error}: invalid display type") + .with_label(err!(s, "cant get {disp}")) + .with_footnote(format!("{help}: change this to 'display'")); + } + Error::UnsupportedImageOp(op, s) => { + d = dig!("{error}: invalid image op").with_label(err!( + s, + "must be one of {{clear, color, col, stroke, line, rect, lineRect, triangle}}" + )); + if let Some((mat,score)) = rust_fuzzy_search::fuzzy_search_best_n(op, crate::instructions::draw::INSTRS, 1).first() && *score > 0.5 { + d.add_footnote(format!("{help}: you may have meant {mat}")); + } + } + Error::NoDisplay(disp, s) => { + d = dig!("{error}: no display allocated").with_label(err!(s, "display#{disp} has not been created")).with_footnote(format!("{note}: it is impossible for me to dynamically allocate displays, as 'display1' could be large or small")); + } + }; + d + } +} + +#[derive(Debug)] +enum UJump<'v> { + Sometimes { + a: LAddress<'v>, + b: LAddress<'v>, + op: ConditionOp, + }, + Always, +} + +pub fn parse<'source, W: Wr>( + mut tokens: Lexer<'source>, + executor: &mut ExecutorBuilderInternal<'source, W>, +) -> Result<(), Error<'source>> { + let mut mem = Vec::new(); // maps &str to usize + // maps "start" to 0 + let mut labels = Vec::new(); + let mut unfinished_jumps = Vec::new(); + macro_rules! tok { + () => { + tokens.next().ok_or(Error::UnexpectedEof) + }; + } + macro_rules! err { + ($e:ident($($stuff:expr)+)) => { + Error::$e($($stuff,)+ tokens.span()) + } + } + macro_rules! yeet { + ($e:ident($($stuff:expr)+)) => { + return Err(Error::$e($($stuff,)+ tokens.span())) + }; + } + #[rustfmt::skip] + macro_rules! nextline { + () => { + while let Some(tok) = tokens.next() && tok != Token::Newline { } + }; + } + macro_rules! take_int { + ($tok:expr) => { + match $tok { + Token::Num(n) if n.fract() == 0.0 && n >= 0.0 => Ok(n as usize), + t => Err(err!(ExpectedInt(t))), + } + }; + } + macro_rules! take_memory { + () => {{ + let container = take_ident!(tok!()?)?; + let cell_n = take_int!(tok!()?)?; + if cell_n > 126 || cell_n == 0 { + yeet!(MemoryTooFar(cell_n)); + } + match container { + "bank" => executor.bank(cell_n), + "cell" => executor.cell(cell_n), + _ => yeet!(InvalidMemoryType(container)), + } + }}; + } + macro_rules! addr { + ($n:expr) => {{ + let n = $n; + match mem + .iter() + .enumerate() + .find(|(_, &v)| v == n) + .map(|(i, _)| i) + { + // SAFETY: we tell it the size is mem.len(); i comes from mem, this is fine + Some(i) => unsafe { LAddress::addr(i) }, + None => { + mem.push(n); + // SAFETY: see above + unsafe { LAddress::addr(mem.len() - 1) } + } + } + }}; + } + macro_rules! take_ident { + ($tok:expr) => {{ + let tok = $tok; + tokstr!(tok).ok_or(err!(ExpectedIdent(tok))) + }}; + } + macro_rules! take_var { + ($tok:expr) => {{ + let tok = $tok; + if let Some(i) = tokstr!(tok) { + Ok(addr!(i)) + } else { + match tok { + Token::Num(n) => Ok(LAddress::cnst(n)), + Token::String(s) => Ok(LAddress::cnst(s)), + t => Err(err!(ExpectedVar(t))), + } + } + }}; + } + macro_rules! take_numvar { + ($tok:expr) => {{ + let tok = $tok; + if let Some(i) = tokstr!(tok) { + Ok(addr!(i)) + } else { + match tok { + Token::Num(n) => Ok(LAddress::cnst(n)), + t => Err(err!(ExpectedNum(t))), + } + } + }}; + } + while let Some(token) = tokens.next() { + match token { + // # omg + Token::Comment(_) => { + executor.noop(); + } + // label: + Token::Ident(v) if v.ends_with(':') => { + labels.push((&v[..v.len() - 1], executor.next())); + } + // print "5" + Token::Print => { + let val = take_var!(tok!()?)?; + executor.add(Print { val }); + } + // set x 4 + Token::Set => { + let from = tok!()?; + if from == Token::Counter { + let to = take_numvar!(tok!()?)?; + executor.add(DynJump { to, proglen: 0 }); + } else { + let from = addr!(take_ident!(from)?); + let to = take_var!(tok!()?)?; + executor.add(Set { from, to }); + } + } + // stop + Token::Stop => { + executor.add(Stop {}); + } + // jump start equal a b + Token::Jump => { + let tok = tok!()?; + // label jump + if let Some(i) = tokstr!(tok) { + let op = tok!()?; + if op == Token::Always { + executor.jmp(); + unfinished_jumps.push((UJump::Always, i, executor.last())); + } else { + let op = op.try_into().map_err(|()| err!(ExpectedOp(op)))?; + let a = take_var!(tok!()?)?; + let b = take_var!(tok!()?)?; + executor.jmp(); + unfinished_jumps.push((UJump::Sometimes { a, b, op }, i, executor.last())); + } + } else if let Ok(n) = take_int!(tok) { + let to = Instruction(n); + let op = tok!()?; + if op == Token::Always { + executor.add(AlwaysJump { to }); + } else { + let op = op.try_into().map_err(|()| err!(ExpectedOp(op)))?; + let a = take_var!(tok!()?)?; + let b = take_var!(tok!()?)?; + executor.add(Jump::new(op, to, a, b)); + } + } else { + yeet!(ExpectedJump(tok)); + }; + } + // op add c 1 2 + Token::Op => { + let op = tok!()?; + if let Ok(op) = MathOp1::try_from(op) { + // assigning to a var is useless but legal + let out = take_numvar!(tok!()?)?; + let x = take_numvar!(tok!()?)?; + executor.add(Op1::new(op, x, out)); + } else if let Ok(op) = MathOp2::try_from(op) { + let out = take_numvar!(tok!()?)?; + let a = take_numvar!(tok!()?)?; + let b = take_numvar!(tok!()?)?; + executor.add(Op2::new(op, a, b, out)); + } else { + yeet!(ExpectedOp(op)); + } + } + // write 5.0 bank1 4 (aka bank1[4] = 5.0) + Token::Write => { + let set = take_numvar!(tok!()?)?; + let container = take_memory!(); + let index = container.limit(take_int!(tok!()?)?); + + executor.add(Write { + index, + set, + container, + }); + } + // read result cell1 4 (aka result = cell1[4]) + Token::Read => { + let output = take_var!(tok!()?)?; + let container = take_memory!(); + let index = container.limit(take_int!(tok!()?)?); + executor.add(Read { + index, + output, + container, + }); + } + Token::Draw => { + let dty = tok!()?; + let Token::Ident(instr) = dty else { + yeet!(ExpectedIdent(dty)); + }; + #[rustfmt::skip] + macro_rules! four { ($a:expr) => { ($a, $a, $a, $a) }; } + #[rustfmt::skip] + macro_rules! six { ($a:expr) => { ($a, $a, $a, $a, $a, $a) }; } + match instr { + "clear" => { + let (r, g, b, a) = four! { take_numvar!(tok!()?)? }; + executor.draw(Clear { r, g, b, a }); + } + "color" => { + let (r, g, b, a) = four! { take_numvar!(tok!()?)? }; + executor.draw(SetColorDyn { r, g, b, a }); + } + "col" => { + let col = take_int!(tok!()?)?; + let r = (col & 0xff00_0000 >> 24) as u8; + let g = (col & 0x00ff_0000 >> 16) as u8; + let b = (col & 0x0000_ff00 >> 8) as u8; + let a = (col & 0x0000_00ff) as u8; + executor.draw(SetColorConst { r, g, b, a }); + } + "stroke" => { + let size = take_numvar!(tok!()?)?; + executor.draw(SetStroke { size }); + } + "line" => { + let (x, y, x2, y2) = four! { take_numvar!(tok!()?)? }; + executor.draw(Line { + point_a: (x, y), + point_b: (x2, y2), + }); + } + "rect" => { + let (x, y, width, height) = four! { take_numvar!(tok!()?)? }; + executor.draw(RectFilled { + position: (x, y), + width, + height, + }); + } + "lineRect" => { + let (x, y, width, height) = four! { take_numvar!(tok!()?)? }; + executor.draw(RectBordered { + position: (x, y), + width, + height, + }); + } + "triangle" => { + let (x, y, x2, y2, x3, y3) = six! { take_numvar!(tok!()?)? }; + executor.draw(Triangle { + points: ((x, y), (x2, y2), (x3, y3)), + }); + } + // poly is TODO, image is WONTFIX + i => yeet!(UnsupportedImageOp(i)), + } + } + Token::DrawFlush => { + let screen = take_ident!(tok!()?)?; + if screen != "display" { + yeet!(InvalidDisplayType(screen)); + } + let display = executor + .display(take_int!(tok!()?)?) + .map_err(|n| err!(NoDisplay(n)))?; + executor.add(Flush { display }); + } + // end + Token::End => { + executor.add(End {}); + } + // starting newline, simply skip. continue, so as not to to trigger the nextline!() + Token::Newline => continue, + // unknown instruction + Token::Ident(i) => { + let mut c = String::from(i); + while let Some(tok) = tokens.next() && tok != Token::Newline { + use std::fmt::Write; + write!(c, " {tok}").expect("didnt know writing to a string could fail"); + } + executor.code(c); + // we take the newline here + continue; + } + t => yeet!(ExpectedInstr(t)), + } + nextline!(); + } + + for (j, l, Instruction(i)) in unfinished_jumps { + let to = labels + .iter() + .find(|(v, _)| v == &l) + .ok_or_else(|| err!(LabelNotFound(l)))? + .1; + executor.program[i] = UPInstr::Instr(match j { + UJump::Always => Instr::from(AlwaysJump { to }), + UJump::Sometimes { a, b, op } => Instr::from(Jump::new(op, to, a, b)), + }); + } + + // check jump validity + for i in &executor.program { + if let UPInstr::Instr(Instr::Jump(Jump { to, .. }) | Instr::AlwaysJump(AlwaysJump { to })) = + i + { + if !executor.valid(*to) { + yeet!(InvalidJump(*to)); + } + } + } + + // set dynjumps + let len = executor.program.len(); + for i in &mut executor.program { + if let UPInstr::Instr(Instr::DynJump(DynJump { proglen, .. })) = i { + *proglen = len; + } + } + + executor.mem(mem.len()); + + Ok(()) +} |