diff --git a/src/main/java/com/github/nilstrieb/grsbpl/GrsbplRunner.java b/src/main/java/com/github/nilstrieb/grsbpl/GrsbplRunner.java new file mode 100644 index 0000000..8cd4738 --- /dev/null +++ b/src/main/java/com/github/nilstrieb/grsbpl/GrsbplRunner.java @@ -0,0 +1,74 @@ +package com.github.nilstrieb.grsbpl; + +import com.github.nilstrieb.grsbpl.language.*; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; + +public class GrsbplRunner { + + private List program; + + public static void main(String[] args) { + if (args.length < 1) { + System.err.println("usage: "); + System.exit(1); + } + + try { + String s = Files.readString(Path.of(args[0])); + GrsbplRunner runner = new GrsbplRunner(); + int exit = runner.run(s); + System.exit(exit); + } catch (IOException e) { + System.err.println("File not found"); + } + } + + private int run(String program) { + this.program = program.lines().collect(Collectors.toUnmodifiableList()); + try { + List tokens = new Lexer().lex(program.toCharArray()); + Interpreter interpreter = new Interpreter(); + return interpreter.run(tokens); + } catch (LexException e) { + showError(e.getMessage(), e.getLineNumber(), e.getLineOffset(), e.getLineLength()); + } catch (RunException e) { + showError(e.getMessage(), e.getLineNumber(), e.getLineOffset(), e.getLineLength()); + } + return 1; + } + + private void showError(String message, int line, int offset, int length) { + if (length == -1) { + length = program.get(line - 1).length() - offset + 1; + } + + System.err.println("[GRSBPL Runtime Execution Error]"); + System.err.println(); + if (line - 1 > 0) { + System.err.println(" " + (line - 1) + " | " + program.get(line - 2)); + } + System.err.println(" " + line + " | " + program.get(line - 1)); + System.err.println(" " + s(len(line)) + " " + s(offset) + "^".repeat(length - 1)); + System.err.println(" " + s(len(line)) + " " + s(offset) + message); + System.err.println(); + if (program.size() > line + 1) { + System.err.println(" " + (line + 1) + " | " + program.get(line)); + } + if (program.size() > line + 2) { + System.err.println(" " + (line + 2) + " | " + program.get(line + 1)); + } + } + + private String s(int length) { + return " ".repeat(length); + } + + private int len(int i) { + return String.valueOf(i).length(); + } +} diff --git a/src/main/java/com/github/nilstrieb/grsbpl/language/IntStack.java b/src/main/java/com/github/nilstrieb/grsbpl/language/IntStack.java new file mode 100644 index 0000000..8bbecca --- /dev/null +++ b/src/main/java/com/github/nilstrieb/grsbpl/language/IntStack.java @@ -0,0 +1,72 @@ +package com.github.nilstrieb.grsbpl.language; + +import java.util.OptionalInt; +import java.util.function.BiFunction; + +/** + * A stack that exclusively holds integers + */ +public class IntStack { + private int[] values; + private int pointer; + + private static final int INITIAL_CAPACITY = 64; + + public IntStack() { + this(INITIAL_CAPACITY); + } + + public IntStack(int initialCapacity) { + values = new int[initialCapacity]; + pointer = -1; + } + + public void push(int value) { + checkResize(); + values[++pointer] = value; + } + + public int pop() { + if (pointer == -1) { + throw new IndexOutOfBoundsException("Cannot pop below zero"); + } + return values[pointer--]; + } + + public boolean isEmpty() { + return pointer < 0; + } + + public int peek() { + return values[pointer]; + } + + public OptionalInt tryPop() { + if (pointer == -1) { + return OptionalInt.empty(); + } else { + return OptionalInt.of(pop()); + } + } + + public void apply2(BiFunction function) { + int val2 = pop(); + int val1 = pop(); + push(function.apply(val1, val2)); + } + + private void checkResize() { + if (pointer == values.length - 1) { + int[] newValues = new int[values.length * 2]; + System.arraycopy(values, 0, newValues, 0, values.length); + this.values = newValues; + } + } + + public void swap() { + int val1 = pop(); + int val2 = pop(); + push(val1); + push(val2); + } +} diff --git a/src/main/java/com/github/nilstrieb/grsbpl/language/Interpreter.java b/src/main/java/com/github/nilstrieb/grsbpl/language/Interpreter.java new file mode 100644 index 0000000..db37d21 --- /dev/null +++ b/src/main/java/com/github/nilstrieb/grsbpl/language/Interpreter.java @@ -0,0 +1,352 @@ +package com.github.nilstrieb.grsbpl.language; + +import java.io.IOException; +import java.util.*; + +import static com.github.nilstrieb.grsbpl.language.TokenType.*; + +public class Interpreter { + + private static final int STACK_LIMIT = 1_000_000; + + private Stack frames; + private Map labels; + private Map functions; + private List program; + private int position; + + public int run(List chars) { + program = chars; + frames = new Stack<>(); + frames.push(new StackFrame()); + functions = new HashMap<>(); + labels = new HashMap<>(); + position = 0; + + firstPass(); + position = 0; + + while (hasNext()) { + executeNext(); + } + + return rest(); + } + + private IntStack stack() { + return frames.peek().getStack(); + } + + private Map variables() { + return frames.peek().getVariables(); + } + + private int rest() { + OptionalInt i = stack().tryPop(); + + if (i.isEmpty()) { + return 0; + } else { + return i.getAsInt(); + } + } + + private void firstPass() { + while (hasNext()) { + TokenType type = advance().getType(); + if (type == COLUMN) { + labels.put(expect(IDENTIFIER).getStringValue(), position); + } else if (type == FUNCTION) { + FunctionData fn = functionHeader(); + functions.put(fn.name, fn); + } + } + } + + private FunctionData functionHeader() { + String name = expect(IDENTIFIER).getStringValue(); + int paramCount = expect(CHARACTER).getIntValue(); + return new FunctionData(position, paramCount, name); + } + + private void executeNext() { + switch (peek().getType()) { + // values + case CHARACTER -> number(); + case CHAR -> character(); + case AMPERSAND -> store(); + case AT -> load(); + // binary operators + case PLUS -> add(); + case MINUS -> subtract(); + case STAR -> multiply(); + case SLASH -> divide(); + case PERCENT -> modulo(); + case BNOT -> bnot(); + case AND -> and(); + case OR -> or(); + case XOR -> xor(); + // other operators + case NOT -> not(); + case DUP -> dup(); + case SWAP -> swap(); + case POP -> pop(); + // io + case OUT -> out(); + case NOUT -> nout(); + case IN -> in(); + // control flow + case COLUMN -> ignoreLabel(); + case GOTO -> condGoto(); + case FUNCTION -> functionHeader(); + case IDENTIFIER -> callFunction(); + case RETURN -> returnFn(); + } + } + + + ///// values + + private void number() { + Token number = advance(); + stack().push(number.getIntValue()); + } + + private void character() { + Token character = advance(); + stack().push((char) character.getValue()); + } + + private void store() { + consume(); // & + Token variable = expect(IDENTIFIER); + String name = variable.getStringValue(); + variables().put(name, stack().pop()); + } + + private void load() { + consume(); // @ + Token variable = expect(IDENTIFIER); + String name = variable.getStringValue(); + stack().push(variables().get(name)); + } + + ///// binary operators + + private void add() { + consume(); + stack().apply2(Integer::sum); + } + + private void subtract() { + consume(); + stack().apply2((i1, i2) -> i1 - i2); + } + + private void multiply() { + consume(); + stack().apply2((i1, i2) -> i1 * i2); + } + + private void divide() { + consume(); + stack().apply2((i1, i2) -> i1 / i2); + } + + private void modulo() { + consume(); + stack().apply2((i1, i2) -> i1 % i2); + } + + private void bnot() { + consume(); + int value = ~stack().pop(); + stack().push(value); + } + + private void and() { + consume(); + stack().apply2((i1, i2) -> i1 & i2); + } + + private void or() { + consume(); + stack().apply2((i1, i2) -> i1 | i2); + } + + private void xor() { + consume(); + stack().apply2((i1, i2) -> i1 ^ i2); + } + + ///// other operators + + private void not() { + consume(); + int value = stack().pop(); + if (value == 0) { + stack().push(1); + } else { + stack().push(0); + } + } + + private void dup() { + consume(); + int value = stack().peek(); + stack().push(value); + } + + private void swap() { + consume(); + stack().swap(); + } + + private void pop() { + consume(); + if (stack().isEmpty()) { + throw runException("Cannot pop empty stack"); + } + stack().pop(); // checked pop + } + + ///// IO + + private void out() { + consume(); + System.out.print((char) stack().pop()); + } + + private void nout() { + consume(); + System.out.print(stack().pop()); + } + + private void in() { + consume(); + try { + stack().push(System.in.read()); + } catch (IOException e) { + throw runException("[VM] - Error reading input"); + } + } + + ///// control flow + + private void ignoreLabel() { + consume(); + expect(IDENTIFIER); + } + + private void condGoto() { + consume(); + String label = expect(IDENTIFIER).getStringValue(); + if (stack().peek() != 0) { + Integer index = labels.get(label); + if (index == null) { + throw runException("Label '" + label + "' not found"); + } + position = index; + } + } + + private void callFunction() { + String name = advance().getStringValue(); + FunctionData p = functions.get(name); + if (p != null) { + call(p); + } else { + throw runException("Function '" + name + "' not found"); + } + } + + private void call(FunctionData fn) { + if (frames.size() > STACK_LIMIT) { + throw runException("Stackoverflow, limit of " + STACK_LIMIT + " stack frames reached."); + } + + frames.peek().setPosition(position); + position = fn.index; + IntStack temp = new IntStack(); + for (int i = 0; i < fn.paramCount; i++) { + temp.push(stack().pop()); + } + frames.push(new StackFrame()); + for (int i = 0; i < fn.paramCount; i++) { + stack().push(temp.pop()); + } + } + + private void returnFn() { + consume(); + OptionalInt returnValue = frames.pop().getStack().tryPop(); + if (returnValue.isEmpty()) { + throw runException("Function has to return some value, but no value was found on the stack"); + } + if (frames.isEmpty()) { + throw runException("Tried to return outside of function, probably forgot to skip a function"); + } + stack().push(returnValue.getAsInt()); + position = frames.peek().getPosition(); + } + + ///// parsing helper methods + + private Token expect(TokenType type) { + if (peek().getType() == type) { + return advance(); + } else { + throw runException("Excepted token '" + type + "' but found '" + peek().getType() + "'"); + } + } + + private Token advance() { + if (position == program.size()) { + return new Token(EOF); + } + return program.get(position++); + } + + private Token peek() { + if (position == program.size()) { + return new Token(EOF); + } + return program.get(position); + } + + private void consume() { + position++; + } + + private boolean hasNext() { + return position < program.size() - 1; // last token is EOF + } + + private RunException runException(String message) { + Token last = program.get(position - 1); + int length; + if (peek().getLineNumber() == last.getLineNumber()) { + length = peek().getLineOffset() - last.getLineOffset(); + } else { + // length cannot be known, make it -1, the error message writer with access to the source code will figure it out + length = -1; + } + return new RunException(message, last.getLineNumber(), last.getLineOffset(), length); + } + + + /** + * The values for a function + */ + static class FunctionData { + private final String name; + public int index; + public int paramCount; + + public FunctionData(int i, int paramCount, String name) { + this.index = i; + this.paramCount = paramCount; + this.name = name; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/nilstrieb/grsbpl/language/LexException.java b/src/main/java/com/github/nilstrieb/grsbpl/language/LexException.java new file mode 100644 index 0000000..d562301 --- /dev/null +++ b/src/main/java/com/github/nilstrieb/grsbpl/language/LexException.java @@ -0,0 +1,27 @@ +package com.github.nilstrieb.grsbpl.language; + +public class LexException extends RuntimeException { + private final int lineNumber; + private final int lineOffset; + private final int lineLength; + + public LexException(String message, int lineNumber, int lineOffset, int lineLength) { + super(message); + this.lineNumber = lineNumber; + this.lineOffset = lineOffset; + this.lineLength = lineLength; + + } + + public int getLineNumber() { + return lineNumber; + } + + public int getLineOffset() { + return lineOffset; + } + + public int getLineLength() { + return lineLength; + } +} diff --git a/src/main/java/com/github/nilstrieb/grsbpl/language/Lexer.java b/src/main/java/com/github/nilstrieb/grsbpl/language/Lexer.java new file mode 100644 index 0000000..df6a698 --- /dev/null +++ b/src/main/java/com/github/nilstrieb/grsbpl/language/Lexer.java @@ -0,0 +1,193 @@ +package com.github.nilstrieb.grsbpl.language; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.github.nilstrieb.grsbpl.language.TokenType.*; + +/** + * The Lexer lexes the input and transforms it into tokens that the interpreter can then run + * Makes everything a lot easier + */ +public class Lexer { + private static final Map KEYWORDS = new HashMap<>(); + + private char[] program; + private List tokens; + private int position; + private int lineNumber; + private int lineOffset; + private int offsetLock; + + static { + KEYWORDS.put("out", OUT); + KEYWORDS.put("nout", NOUT); + KEYWORDS.put("in", IN); + KEYWORDS.put("goto", GOTO); + KEYWORDS.put("not", NOT); + KEYWORDS.put("swap", SWAP); + KEYWORDS.put("bnot", BNOT); + KEYWORDS.put("and", AND); + KEYWORDS.put("or", OR); + KEYWORDS.put("xor", XOR); + KEYWORDS.put("dup", DUP); + KEYWORDS.put("pop", POP); + KEYWORDS.put("function", FUNCTION); + KEYWORDS.put("return", RETURN); + } + + + public List lex(char[] chars) { + program = chars; + position = 0; + tokens = new ArrayList<>(); + lineOffset = 0; + lineNumber = 1; + + while (hasNext()) { + try { + next(); + } catch (LexException e) { + throw e; + } catch (Exception e) { + throw lexException("Unkown Syntax Error. " + e.getClass().getName() + ": " + e.getMessage()); + } + } + add(EOF); + return tokens; + } + + private void next() { + lockOffset(); + char next = advance(); + switch (next) { + case '+' -> add(PLUS); + case '-' -> add(MINUS); + case '*' -> add(STAR); + case '/' -> add(SLASH); + case '%' -> add(PERCENT); + case '&' -> add(AMPERSAND); + case '@' -> add(AT); + case ':' -> add(COLUMN); + case '\'' -> character(); + case ' ', '\t', '\r', '\n' -> { + } + case '#' -> comment(); + default -> { + if (Character.isDigit(next)) { + number(); + } else { + ident(); + } + } + } + } + + private void character() { + char value = advance(); + if (value == '\\') { + char escaped = advance(); + value = switch (escaped) { + case 'n' -> '\n'; + case 'r' -> '\r'; + case '\\' -> '\\'; + case '0' -> '\0'; + case '\'' -> '\''; + case 'b' -> '\b'; + case 'f' -> '\f'; + default -> throw new LexException("Invalid escape sequence: \\" + escaped, lineNumber, offsetLock, lineOffset - offsetLock); + }; + } + add(CHAR, value); + consume(); + } + + private void comment() { + while (true) { + char next = advance(); + if (next == '\n') { + break; + } else if (next == '#') { + break; + } + } + } + + private void ident() { + StringBuilder text = new StringBuilder(String.valueOf(last())); + while (isAlphaNumeric(peek())) { + text.append(advance()); + } + TokenType type = KEYWORDS.get(text.toString()); + if (type == null) { + add(IDENTIFIER, text.toString()); + } else { + add(type); + } + } + + private void number() { + StringBuilder number = new StringBuilder(String.valueOf(last())); + while (Character.isDigit(peek())) { + number.append(advance()); + } + try { + int value = Integer.parseInt(number.toString()); + add(CHARACTER, value); + } catch (NumberFormatException e) { + throw lexException("Value not an integer: " + number); + } + } + + private boolean isAlphaNumeric(char c) { + return Character.isAlphabetic(c) || Character.isDigit(c) || c == '_'; + } + + private boolean hasNext() { + return position < program.length; + } + + private void consume() { + advance(); + } + + private char last() { + return program[position - 1]; + } + + private char peek() { + if (hasNext()) { + return program[position]; + } else { + return '\0'; + } + } + + private char advance() { + lineOffset++; + char c = program[position++]; + if (c == '\n') { + lineNumber++; + lineOffset = 0; + } + return c; + } + + private void lockOffset() { + offsetLock = lineOffset; + } + + private LexException lexException(String message) { + throw new LexException(message, lineNumber, offsetLock, lineOffset - offsetLock); + } + + private void add(TokenType tokenType) { + tokens.add(new Token(tokenType, lineNumber, offsetLock)); + } + + private void add(TokenType tokenType, Object value) { + tokens.add(new Token(tokenType, value, lineNumber, offsetLock)); + } +} diff --git a/src/main/java/com/github/nilstrieb/grsbpl/language/RunException.java b/src/main/java/com/github/nilstrieb/grsbpl/language/RunException.java new file mode 100644 index 0000000..bca36e7 --- /dev/null +++ b/src/main/java/com/github/nilstrieb/grsbpl/language/RunException.java @@ -0,0 +1,26 @@ +package com.github.nilstrieb.grsbpl.language; + +public class RunException extends RuntimeException { + private final int lineNumber; + private final int lineOffset; + private final int lineLength; + + public RunException(String message, int lineNumber, int lineOffset, int lineLength) { + super(message); + this.lineNumber = lineNumber; + this.lineOffset = lineOffset; + this.lineLength = lineLength; + } + + public int getLineNumber() { + return lineNumber; + } + + public int getLineOffset() { + return lineOffset; + } + + public int getLineLength() { + return lineLength; + } +} diff --git a/src/main/java/com/github/nilstrieb/grsbpl/language/StackFrame.java b/src/main/java/com/github/nilstrieb/grsbpl/language/StackFrame.java new file mode 100644 index 0000000..b66d921 --- /dev/null +++ b/src/main/java/com/github/nilstrieb/grsbpl/language/StackFrame.java @@ -0,0 +1,31 @@ +package com.github.nilstrieb.grsbpl.language; + +import java.util.HashMap; +import java.util.Map; + +public class StackFrame { + private final IntStack stack; + private final Map variables; + private int position; + + public StackFrame() { + stack = new IntStack(); + variables = new HashMap<>(); + } + + public IntStack getStack() { + return stack; + } + + public Map getVariables() { + return variables; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } +} diff --git a/src/main/java/com/github/nilstrieb/grsbpl/language/Token.java b/src/main/java/com/github/nilstrieb/grsbpl/language/Token.java new file mode 100644 index 0000000..689657a --- /dev/null +++ b/src/main/java/com/github/nilstrieb/grsbpl/language/Token.java @@ -0,0 +1,73 @@ +package com.github.nilstrieb.grsbpl.language; + +import java.util.Objects; + +public class Token { + + private final TokenType type; + private final Object value; + private final int lineNumber; + private final int lineOffset; + + public Token(TokenType type, int lineNumber, int lineOffset) { + this(type, null, lineNumber, lineOffset); + } + + public Token(TokenType type, Object value, int lineNumber, int lineOffset) { + this.type = type; + this.value = value; + this.lineNumber = lineNumber; + this.lineOffset = lineOffset; + } + + public Token(TokenType type) { + this(type, 0, 0); + } + + public TokenType getType() { + return type; + } + + public Object getValue() { + return value; + } + + public String getStringValue() { + return (String) value; + } + + public int getIntValue() { + return (int) value; + } + + public int getLineNumber() { + return lineNumber; + } + + public int getLineOffset() { + return lineOffset; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Token token = (Token) o; + + if (lineNumber != token.lineNumber) return false; + if (lineOffset != token.lineOffset) return false; + if (type != token.type) return false; + return Objects.equals(value, token.value); + } + + @Override + public String toString() { + return "Token{" + + "type=" + type + + ", value=" + value + + ", lineNumber=" + lineNumber + + ", lineOffset=" + lineOffset + + '}'; + } +} diff --git a/src/main/java/com/github/nilstrieb/grsbpl/language/TokenType.java b/src/main/java/com/github/nilstrieb/grsbpl/language/TokenType.java new file mode 100644 index 0000000..a701b0f --- /dev/null +++ b/src/main/java/com/github/nilstrieb/grsbpl/language/TokenType.java @@ -0,0 +1,41 @@ +package com.github.nilstrieb.grsbpl.language; + +/** + * The different types of tokens + */ +public enum TokenType { + + // values + CHARACTER, CHAR, AMPERSAND("&"), AT("@"), + + // binary operators + PLUS("+"), MINUS("-"), STAR("*"), SLASH("/"), PERCENT("%"), + BNOT, AND, OR, XOR, + + // other operators + NOT, DUP, SWAP, POP, + + // io + OUT, NOUT, IN, + + // control flow + COLUMN(":"), GOTO, FUNCTION, IDENTIFIER, RETURN, + + // end + EOF; + + private final String display; + + TokenType() { + this.display = super.toString(); + } + + TokenType(String display) { + this.display = display; + } + + @Override + public String toString() { + return display; + } +} diff --git a/src/test/java/com/github/nilstrieb/grsbpl/IntStackTest.java b/src/test/java/com/github/nilstrieb/grsbpl/IntStackTest.java new file mode 100644 index 0000000..e686bb4 --- /dev/null +++ b/src/test/java/com/github/nilstrieb/grsbpl/IntStackTest.java @@ -0,0 +1,45 @@ +package com.github.nilstrieb.grsbpl; + +import com.github.nilstrieb.grsbpl.language.IntStack; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class IntStackTest { + + @Test + void pushPop() { + IntStack s = new IntStack(); + + s.push(100); + s.push(50); + + assertEquals(50, s.pop()); + assertEquals(100, s.pop()); + + assertThrows(IndexOutOfBoundsException.class, s::pop); + } + + @Test + void applyFunction() { + IntStack s = new IntStack(); + s.push(10); + s.push(2); + s.apply2((i1, i2) -> i1 / i2); + assertEquals(5, s.pop()); + assertThrows(IndexOutOfBoundsException.class, s::pop); + } + + @Test + void resize() { + IntStack s = new IntStack(); + for (int i = 0; i < 1000; i++) { + s.push(i); + } + + for (int i = 999; i >= 0; i--) { + assertEquals(i, s.pop()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/github/nilstrieb/grsbpl/InterpreterTest.java b/src/test/java/com/github/nilstrieb/grsbpl/InterpreterTest.java new file mode 100644 index 0000000..163ba7a --- /dev/null +++ b/src/test/java/com/github/nilstrieb/grsbpl/InterpreterTest.java @@ -0,0 +1,195 @@ +package com.github.nilstrieb.grsbpl; + +import com.github.nilstrieb.grsbpl.language.Interpreter; +import com.github.nilstrieb.grsbpl.language.Lexer; +import com.github.nilstrieb.grsbpl.language.Token; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class InterpreterTest { + + static Interpreter interpreter; + + static OutStream out; + + @BeforeEach + void setup() { + interpreter = new Interpreter(); + out = new OutStream(); + System.setOut(out); + } + + @Test + void arithmeticOperations() { + String program = "1 1 * 2 +"; + assertEquals(3, run(program)); + String program2 = "10 5 /"; + assertEquals(2, run(program2)); + } + + @Test + void bigNumbers() { + String program = "1000 1234 +"; + assertEquals(2234, run(program)); + } + + @Test + void comment() { + String program = "1 # sdkfjsaf se9 83 252h43ui\n 2 # test 5 # +"; + assertEquals(3, run(program)); + } + + @Test + void variables() { + String program = "1 &one 2 &two 3 &three 8 @two +"; + assertEquals(10, run(program)); + } + + @Test + void labels() { + String program = "1 :first 2 0"; + assertEquals(0, run(program)); + } + + @Test + void gotoBack() { + String program = "100000000 &i \n" + + ":start \n" + + "@i nout '\n' out \n" + + "@i 1 - &i \n" + + "@i goto start \n" + + " 0"; + int result = 0; + assertEquals(result, run(program)); + } + + @Test + void gotoSkip() { + String program = "1 :first 0 goto first 1 goto skip 3754 78349758 :skip"; + int result = 1; + assertEquals(result, run(program)); + } + + @Test + void fizzBuzz() throws IOException, URISyntaxException { + String program = Files.readString(Path.of(getClass().getClassLoader().getResource("fizzbuzz.grsbpl").toURI())); + int result = 0; + StringBuilder resultString = new StringBuilder(); + for (int i = 1; i < 100; i++) { + if (i % 15 == 0) resultString.append("FizzBuzz\n"); + else if (i % 5 == 0) resultString.append("Buzz\n"); + else if (i % 3 == 0) resultString.append("Fizz\n"); + else resultString.append(i).append("\n"); + } + assertEquals(result, run(program)); + assertEquals(resultString.toString(), out.getOut()); + } + + @Test + void stackManipulationTest() { + String program = "1 2 swap"; + assertEquals(1, run(program)); + + String program2 = "0 not"; + assertEquals(1, run(program2)); + + String program3 = "1 not"; + assertEquals(0, run(program3)); + + String program4 = "5 dup pop"; + assertEquals(5, run(program4)); + + String program5 = "1 2 pop"; + assertEquals(1, run(program5)); + } + + @Test + void bitwise() { + String p1 = "10 10 xor"; + assertEquals(0, run(p1)); + + String p2 = "1 bnot"; + assertEquals(~1, run(p2)); + + String p3 = 0xFF + " 1 and"; + assertEquals(1, run(p3)); + + String p4 = 0b001 + " " + 0b101 + " or"; + assertEquals(0b101, run(p4)); + } + + @Test + void functionTest() { + String program = "" + + "1 printNumber " + + "2 printNumber " + + "3 printNumber " + + "1 goto end" + + " " + + "function printNumber 1 nout 0 return" + + ":end 0"; + int result = 0; + assertEquals(result, run(program)); + assertEquals("123", out.getOut()); + } + + @Test + void factorial() throws URISyntaxException, IOException { + String program0 = 0 + Files.readString(Path.of(getClass().getClassLoader().getResource("factorial.grsbpl").toURI())); + String program1 = 1 + Files.readString(Path.of(getClass().getClassLoader().getResource("factorial.grsbpl").toURI())); + String program10 = 10 + Files.readString(Path.of(getClass().getClassLoader().getResource("factorial.grsbpl").toURI())); + + assertEquals(1, run(program0)); + assertEquals(1, run(program1)); + assertEquals(3628800, run(program10)); + } + + @Test + void outTest() { + String program = "'\n' '!' 'd' 'l' 'r' 'o' 'w' ' ' 'o' 'l' 'l' 'e' 'h' out out out out out out out out out out out out out 0"; + + assertEquals(0, run(program)); + assertEquals("hello world!\n", out.getOut()); + } + + static class OutStream extends PrintStream { + private final StringBuilder builder = new StringBuilder(); + + public OutStream() { + super(new OutputStream() { + @Override + public void write(int b) { + } + }); + } + + @Override + public void print(char c) { + builder.append(c); + } + + @Override + public void print(int i) { + builder.append(i); + } + + public String getOut() { + return builder.toString(); + } + } + + int run(String program) { + List tokens = new Lexer().lex(program.toCharArray()); + return interpreter.run(tokens); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/nilstrieb/grsbpl/LexerTest.java b/src/test/java/com/github/nilstrieb/grsbpl/LexerTest.java new file mode 100644 index 0000000..d1ba8c7 --- /dev/null +++ b/src/test/java/com/github/nilstrieb/grsbpl/LexerTest.java @@ -0,0 +1,122 @@ +package com.github.nilstrieb.grsbpl; + +import com.github.nilstrieb.grsbpl.language.Lexer; +import com.github.nilstrieb.grsbpl.language.Token; +import com.github.nilstrieb.grsbpl.language.TokenType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static com.github.nilstrieb.grsbpl.language.TokenType.*; +import static org.junit.jupiter.api.Assertions.*; + +class LexerTest { + + Lexer lexer; + + @BeforeEach + void setup() { + lexer = new Lexer(); + } + + @Test + void keywords() { + String program = "out in nout xor or and not bnot pop dup swap goto function return not"; + List expected = List.of(OUT, IN, NOUT, XOR, OR, AND, NOT, BNOT, POP, DUP, SWAP, GOTO, FUNCTION, RETURN, NOT, EOF); + List actual = getTypes(lex(program)); + + assertEquals(expected, actual); + } + + @Test + void symbols() { + String program = "+ & @ - % / * : +"; + List expected = List.of(PLUS, AMPERSAND, AT, MINUS, PERCENT, SLASH, STAR, COLUMN, PLUS, EOF); + List actual = getTypes(lex(program)); + assertEquals(expected, actual); + } + + @Test + void identifiers() { + String program = "out test xor hallo + stack"; + List expected = List.of(OUT, IDENTIFIER, XOR, IDENTIFIER, PLUS, IDENTIFIER, EOF); + List actual = lex(program); + assertEquals(expected, getTypes(actual)); + + Token test = new Token(IDENTIFIER, "test", 1, 4); + assertEquals(test, actual.get(1)); + + } + + @Test + void numbers() { + String program = "out 347 test 64006 in"; + List expected = List.of(OUT, CHARACTER, IDENTIFIER, CHARACTER, IN, EOF); + List actual = lex(program); + assertEquals(expected, getTypes(actual)); + + Token test = new Token(CHARACTER, 347, 1, 4); + assertEquals(test, actual.get(1)); + + } + + @Test + void chars() { + String program = "'h' '\\n' '\\r' '\\f' '\\\\' '\\b' '\\'' '\\0'"; + List expected = List.of('h', '\n', '\r', '\f', '\\', '\b', '\'', '\0'); + List actual = lex(program); + assertEquals(expected, actual.stream() + .map(Token::getValue) + .filter(Objects::nonNull) + .limit(8) + .collect(Collectors.toUnmodifiableList())); + } + + + @Test + void comments() { + String program = "goto # hallo # goto #test\n goto"; + List expected = List.of(GOTO, GOTO, GOTO, EOF); + List actual = getTypes(lex(program)); + + assertEquals(expected, actual); + } + + @Test + void lineNumber() { + String program = "goto \n \n goto \ngoto"; + List expected = List.of(GOTO, GOTO, GOTO, EOF); + List actual = lex(program); + assertEquals(expected, getTypes(actual)); + + Token test = new Token(GOTO, null, 4, 0); + assertEquals(test, actual.get(2)); + + } + + @Test + void identifierName() { + String program = "test ABC g9tgq fe_53f"; + List expected = List.of("test", "ABC", "g9tgq", "fe_53f"); + assertEquals(expected, getValues(lex(program))); + } + + List lex(String program) { + return lexer.lex(program.toCharArray()); + } + + List getValues(List tokens) { + return tokens.stream() + .map(Token::getValue) + .filter(Objects::nonNull) + .collect(Collectors.toUnmodifiableList()); + } + List getTypes(List tokens) { + return tokens.stream() + .map(Token::getType) + .collect(Collectors.toUnmodifiableList()); + } +} \ No newline at end of file diff --git a/src/test/resources/factorial.grsbpl b/src/test/resources/factorial.grsbpl new file mode 100644 index 0000000..102dde8 --- /dev/null +++ b/src/test/resources/factorial.grsbpl @@ -0,0 +1,9 @@ + factorial 1 goto exit + +function factorial 1 +dup not goto isZero +&del dup 1 - factorial * return +:isZero +1 return + +:exit pop \ No newline at end of file diff --git a/src/test/resources/fizzbuzz.grsbpl b/src/test/resources/fizzbuzz.grsbpl new file mode 100644 index 0000000..868265d --- /dev/null +++ b/src/test/resources/fizzbuzz.grsbpl @@ -0,0 +1,19 @@ +1 &i # init loop counter +:start # set start label +@i 100 - not goto finished # if i is 100, finish +@i 15 % not goto print_fizz_buzz # fizzbuzz +@i 5 % not goto print_buzz # buzz +@i 3 % not goto print_fizz # fizz +@i nout '\n' out # normal number +:end # go back here after printing +@i 1 + &i # increment i +1 goto start # go back to the start + +:print_fizz_buzz + 'F' out 'i' out 'z' out 'z' out 'B' out 'u' out 'z' out 'z' out '\n' out goto end +:print_fizz + 'F' out 'i' out 'z' out 'z' out '\n' out goto end +:print_buzz + 'B' out 'u' out 'z' out 'z' out '\n' out goto end + +:finished 0 \ No newline at end of file