mindustry logic execution, map- and schematic- parsing and rendering
Diffstat (limited to 'lemu/src/parser.rs')
-rw-r--r--lemu/src/parser.rs659
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(())
+}