From ae2736d3931e23c445f769d714a151e14bfd5c4c Mon Sep 17 00:00:00 2001 From: Anuken Date: Tue, 9 Mar 2021 15:47:19 -0500 Subject: [PATCH] New logic parser --- .../misc/LogicStatementProcessor.java | 5 +- core/src/mindustry/logic/LAssembler.java | 110 +----------- core/src/mindustry/logic/LExecutor.java | 4 +- core/src/mindustry/logic/LParser.java | 160 ++++++++++++++++-- .../world/blocks/logic/LogicBlock.java | 5 +- 5 files changed, 158 insertions(+), 126 deletions(-) diff --git a/annotations/src/main/java/mindustry/annotations/misc/LogicStatementProcessor.java b/annotations/src/main/java/mindustry/annotations/misc/LogicStatementProcessor.java index 6e597bd002..a6b2643c61 100644 --- a/annotations/src/main/java/mindustry/annotations/misc/LogicStatementProcessor.java +++ b/annotations/src/main/java/mindustry/annotations/misc/LogicStatementProcessor.java @@ -26,7 +26,8 @@ public class LogicStatementProcessor extends BaseProcessor{ MethodSpec.Builder reader = MethodSpec.methodBuilder("read") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(tname("mindustry.logic.LStatement")) - .addParameter(String[].class, "tokens"); + .addParameter(String[].class, "tokens") + .addParameter(int.class, "length"); Seq types = types(RegisterStatement.class); @@ -76,7 +77,7 @@ public class LogicStatementProcessor extends BaseProcessor{ ""); //reading primitives, strings and enums is supported; nothing else is - reader.addStatement("if(tokens.length > $L) result.$L = $L(tokens[$L])", + reader.addStatement("if(length > $L) result.$L = $L(tokens[$L])", index + 1, field.name(), field.mirror().toString().equals("java.lang.String") ? diff --git a/core/src/mindustry/logic/LAssembler.java b/core/src/mindustry/logic/LAssembler.java index 75a7b77174..dfd64e9c44 100644 --- a/core/src/mindustry/logic/LAssembler.java +++ b/core/src/mindustry/logic/LAssembler.java @@ -4,9 +4,7 @@ import arc.func.*; import arc.struct.*; import arc.util.*; import mindustry.*; -import mindustry.gen.*; import mindustry.logic.LExecutor.*; -import mindustry.logic.LStatements.*; /** "Compiles" a sequence of statements into instructions. */ public class LAssembler{ @@ -37,10 +35,10 @@ public class LAssembler{ putConst("@tick", 0); } - public static LAssembler assemble(String data, int maxInstructions){ + public static LAssembler assemble(String data){ LAssembler asm = new LAssembler(); - Seq st = read(data, maxInstructions); + Seq st = read(data); asm.instructions = st.map(l -> l.build(asm)).filter(l -> l != null).toArray(LInstruction.class); return asm; @@ -57,107 +55,7 @@ public class LAssembler{ } public static Seq read(String data){ - return read(data, LExecutor.maxInstructions); - } - - public static Seq read(String data, int max){ - //empty data check - if(data == null || data.isEmpty()) return new Seq<>(); - - Seq statements = new Seq<>(); - String[] lines = data.split("\n"); - int index = 0; - for(String line : lines){ - if(line.isEmpty()) continue; - //remove trailing semicolons in case someone adds them in for no reason - if(line.endsWith(";")) line = line.substring(0, line.length() - 1); - - if(index++ > max) break; - - line = line.replace("\t", "").trim(); - - try{ - String[] arr; - if(line.startsWith("#")) continue; - - //yes, I am aware that this can be split with regex, but that's slow and even more incomprehensible - if(line.contains(" ")){ - Seq tokens = new Seq<>(); - boolean inString = false; - int lastIdx = 0; - - for(int i = 0; i < line.length() + 1; i++){ - char c = i == line.length() ? ' ' : line.charAt(i); - if(c == '#' && !inString){ - break; - }else if(c == '"'){ - inString = !inString; - }else if(c == ' ' && !inString){ - tokens.add(line.substring(lastIdx, Math.min(i, lastIdx + maxTokenLength))); - lastIdx = i + 1; - } - } - - arr = tokens.toArray(String.class); - }else{ - arr = new String[]{line}; - } - - //nothing found - if(arr.length == 0) continue; - - String type = arr[0]; - - //legacy stuff - if(type.equals("bop")){ - arr[0] = "op"; - - //field order for bop used to be op a, b, result, but now it's op result a b - String res = arr[4]; - arr[4] = arr[3]; - arr[3] = arr[2]; - arr[2] = res; - }else if(type.equals("uop")){ - arr[0] = "op"; - - if(arr[1].equals("negate")){ - arr = new String[]{ - "op", "mul", arr[3], arr[2], "-1" - }; - }else{ - //field order for uop used to be op a, result, but now it's op result a - String res = arr[3]; - arr[3] = arr[2]; - arr[2] = res; - } - } - - //fix up changed operaiton names - if(type.equals("op")){ - arr[1] = opNameChanges.get(arr[1], arr[1]); - } - - LStatement st = LogicIO.read(arr); - - if(st != null){ - statements.add(st); - }else{ - //attempt parsing using custom parser if a match is found - this is for mods - String first = arr[0]; - if(customParsers.containsKey(first)){ - statements.add(customParsers.get(first).get(arr)); - }else{ - //unparseable statement - statements.add(new InvalidStatement()); - } - } - }catch(Exception parseFailed){ - parseFailed.printStackTrace(); - //when parsing fails, add a dummy invalid statement - statements.add(new InvalidStatement()); - } - } - return statements; + return LParser.parse(data); } /** @return a variable ID by name. @@ -172,7 +70,7 @@ public class LAssembler{ symbol = symbol.trim(); //string case - if(symbol.startsWith("\"") && symbol.endsWith("\"")){ + if(!symbol.isEmpty() && symbol.charAt(0) == '\"' && symbol.charAt(symbol.length() - 1) == '\"'){ return putConst("___" + symbol, symbol.substring(1, symbol.length() - 1).replace("\\n", "\n")).id; } diff --git a/core/src/mindustry/logic/LExecutor.java b/core/src/mindustry/logic/LExecutor.java index 2e636cf4cd..3cfefee9ac 100644 --- a/core/src/mindustry/logic/LExecutor.java +++ b/core/src/mindustry/logic/LExecutor.java @@ -74,8 +74,8 @@ public class LExecutor{ } } - public void load(String data, int maxInstructions){ - load(LAssembler.assemble(data, maxInstructions)); + public void load(String data){ + load(LAssembler.assemble(data)); } /** Loads with a specified assembler. Resets all variables. */ diff --git a/core/src/mindustry/logic/LParser.java b/core/src/mindustry/logic/LParser.java index cae7b58c9d..29f3fbd53a 100644 --- a/core/src/mindustry/logic/LParser.java +++ b/core/src/mindustry/logic/LParser.java @@ -1,17 +1,33 @@ package mindustry.logic; import arc.struct.*; +import arc.util.*; +import mindustry.gen.*; +import mindustry.logic.LStatements.*; public class LParser{ + private static final String[] tokens = new String[16]; + private static final int maxJumps = 500; + private static final StringMap opNameChanges = StringMap.of( + "atan2", "angle", + "dst", "len" + ); + + private static final Seq jumps = new Seq<>(); + private static final ObjectIntMap jumpLocations = new ObjectIntMap<>(); + Seq statements = new Seq<>(); char[] chars; - int pos; + int pos, line, tok; LParser(String text){ this.chars = text.toCharArray(); } + /** Parses a sequence of statements from a string. */ public static Seq parse(String text){ + //don't waste time parsing null/empty text + if(text == null || text.isEmpty()) return new Seq<>(); return new LParser(text).parse(); } @@ -20,40 +36,158 @@ public class LParser{ while(pos < chars.length && chars[pos++] != '\n'); } - void label(){ - while(pos < chars.length){ + void error(String message){ + //TODO + throw new RuntimeException("Invalid code. " + message); + } + String string(){ + int from = pos; + + while(pos++ < chars.length){ + var c = chars[pos]; + if(c == '\n'){ + error("Missing closing quote \" before end of line."); + }else if(c == '"'){ + break; + } + } + + if(chars[pos] != '"') error("Missing closing quote \" before end of file."); + + return new String(chars, from, ++pos - from); + } + + String token(){ + int from = pos; + + while(pos < chars.length){ + char c = chars[pos]; + if(c == '\n' || c == ' ' || c == '#' || c == '\t' || c == ';') break; + pos ++; + } + + return new String(chars, from, pos - from); + } + + /** Apply changes after reading a list of tokens. */ + void checkRead(){ + if(tokens[0].equals("op")){ + //legacy name change + tokens[1] = opNameChanges.get(tokens[1], tokens[1]); } } + /** Reads the next statement until EOL/EOF. */ void statement(){ - //read jump - if(chars[pos] == '['){ - - } + boolean expectNext = false; + tok = 0; while(pos < chars.length){ - char c = chars[pos++]; + char c = chars[pos]; + if(tok >= tokens.length) error("Line too long; may only contain " + tokens.length + " tokens"); //reached end of line, bail out. - if(c == '\n') break; + if(c == '\n' || c == ';') break; + + if(expectNext && c != ' ' && c != '#' && c != '\t'){ + error("Expected space after string/token."); + } + + expectNext = false; if(c == '#'){ comment(); break; + }else if(c == '"'){ + tokens[tok ++] = string(); + expectNext = true; + }else if(c != ' ' && c != '\t'){ + tokens[tok ++] = token(); + expectNext = true; + }else{ + pos ++; + } + } + + //only process lines with at least 1 token + if(tok > 0){ + checkRead(); + + //store jump location, always ends with colon + if(tok == 1 && tokens[0].charAt(tokens[0].length() - 1) == ':'){ + if(jumpLocations.size >= maxJumps){ + error("Too many jump locations. Max jumps: " + maxJumps); + } + jumpLocations.put(tokens[0].substring(0, tokens[0].length() - 1), line); + }else{ + boolean wasJump; + String jumpLoc = null; + //clean up jump position before parsing + if(wasJump = (tokens[0].equals("jump") && tok > 1 && !Strings.canParseInt(tokens[1]))){ + jumpLoc = tokens[1]; + tokens[1] = "-1"; + } + + LStatement st; + + try{ + st = LogicIO.read(tokens, tok); + }catch(Exception e){ + //log invalid statements + Log.err(e); + st = new InvalidStatement(); + } + + //store jumps that use labels + if(st instanceof JumpStatement jump && wasJump){ + jumps.add(new JumpIndex(jump, jumpLoc)); + } + + if(st != null){ + statements.add(st); + }else{ + //attempt parsing using custom parser if a match is found; this is for mods + if(LAssembler.customParsers.containsKey(tokens[0])){ + statements.add(LAssembler.customParsers.get(tokens[0]).get(tokens)); + }else{ + //unparseable statement + statements.add(new InvalidStatement()); + } + } + line ++; } } } Seq parse(){ - while(pos < chars.length){ - switch(chars[pos++]){ - case '\n', ' ' -> {} //skip newlines and spaces - case '\r' -> pos ++; //skip the newline after the \r + jumps.clear(); + jumpLocations.clear(); + + while(pos < chars.length && line < LExecutor.maxInstructions){ + switch(chars[pos]){ + case '\n', ' ' -> pos ++; //skip newlines and spaces + case '\r' -> pos += 2; //skip the newline after the \r default -> statement(); } } + + //load destination indices + for(var i : jumps){ + i.jump.destIndex = jumpLocations.get(i.location, -1); + } + return statements; } + static class JumpIndex{ + JumpStatement jump; + String location; + + public JumpIndex(JumpStatement jump, String location){ + this.jump = jump; + this.location = location; + } + } + } diff --git a/core/src/mindustry/world/blocks/logic/LogicBlock.java b/core/src/mindustry/world/blocks/logic/LogicBlock.java index ac597fa1e7..d1cb1e0f10 100644 --- a/core/src/mindustry/world/blocks/logic/LogicBlock.java +++ b/core/src/mindustry/world/blocks/logic/LogicBlock.java @@ -1,6 +1,5 @@ package mindustry.world.blocks.logic; -import arc.*; import arc.func.*; import arc.math.geom.*; import arc.scene.ui.layout.*; @@ -302,7 +301,7 @@ public class LogicBlock extends Block{ try{ //create assembler to store extra variables - LAssembler asm = LAssembler.assemble(str, LExecutor.maxInstructions); + LAssembler asm = LAssembler.assemble(str); //store connections for(LogicLink link : links){ @@ -357,7 +356,7 @@ public class LogicBlock extends Block{ Log.err(e); //handle malformed code and replace it with nothing - executor.load("", LExecutor.maxInstructions); + executor.load(""); } } }