mirror of
https://github.com/Noratrieb/GRSBPL.git
synced 2026-01-16 20:55:02 +01:00
great commit history
This commit is contained in:
parent
af55c80677
commit
0b96e0dd0d
14 changed files with 1279 additions and 0 deletions
74
src/main/java/com/github/nilstrieb/grsbpl/GrsbplRunner.java
Normal file
74
src/main/java/com/github/nilstrieb/grsbpl/GrsbplRunner.java
Normal file
|
|
@ -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<String> program;
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (args.length < 1) {
|
||||
System.err.println("usage: <filename>");
|
||||
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<Token> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Integer, Integer, Integer> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<StackFrame> frames;
|
||||
private Map<String, Integer> labels;
|
||||
private Map<String, FunctionData> functions;
|
||||
private List<Token> program;
|
||||
private int position;
|
||||
|
||||
public int run(List<Token> 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<String, Integer> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
193
src/main/java/com/github/nilstrieb/grsbpl/language/Lexer.java
Normal file
193
src/main/java/com/github/nilstrieb/grsbpl/language/Lexer.java
Normal file
|
|
@ -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<String, TokenType> KEYWORDS = new HashMap<>();
|
||||
|
||||
private char[] program;
|
||||
private List<Token> 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<Token> 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, Integer> variables;
|
||||
private int position;
|
||||
|
||||
public StackFrame() {
|
||||
stack = new IntStack();
|
||||
variables = new HashMap<>();
|
||||
}
|
||||
|
||||
public IntStack getStack() {
|
||||
return stack;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getVariables() {
|
||||
return variables;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
45
src/test/java/com/github/nilstrieb/grsbpl/IntStackTest.java
Normal file
45
src/test/java/com/github/nilstrieb/grsbpl/IntStackTest.java
Normal file
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
195
src/test/java/com/github/nilstrieb/grsbpl/InterpreterTest.java
Normal file
195
src/test/java/com/github/nilstrieb/grsbpl/InterpreterTest.java
Normal file
|
|
@ -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<Token> tokens = new Lexer().lex(program.toCharArray());
|
||||
return interpreter.run(tokens);
|
||||
}
|
||||
}
|
||||
122
src/test/java/com/github/nilstrieb/grsbpl/LexerTest.java
Normal file
122
src/test/java/com/github/nilstrieb/grsbpl/LexerTest.java
Normal file
|
|
@ -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<TokenType> expected = List.of(OUT, IN, NOUT, XOR, OR, AND, NOT, BNOT, POP, DUP, SWAP, GOTO, FUNCTION, RETURN, NOT, EOF);
|
||||
List<TokenType> actual = getTypes(lex(program));
|
||||
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void symbols() {
|
||||
String program = "+ & @ - % / * : +";
|
||||
List<TokenType> expected = List.of(PLUS, AMPERSAND, AT, MINUS, PERCENT, SLASH, STAR, COLUMN, PLUS, EOF);
|
||||
List<TokenType> actual = getTypes(lex(program));
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void identifiers() {
|
||||
String program = "out test xor hallo + stack";
|
||||
List<TokenType> expected = List.of(OUT, IDENTIFIER, XOR, IDENTIFIER, PLUS, IDENTIFIER, EOF);
|
||||
List<Token> 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<TokenType> expected = List.of(OUT, CHARACTER, IDENTIFIER, CHARACTER, IN, EOF);
|
||||
List<Token> 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<Character> expected = List.of('h', '\n', '\r', '\f', '\\', '\b', '\'', '\0');
|
||||
List<Token> 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<TokenType> expected = List.of(GOTO, GOTO, GOTO, EOF);
|
||||
List<TokenType> actual = getTypes(lex(program));
|
||||
|
||||
assertEquals(expected, actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lineNumber() {
|
||||
String program = "goto \n \n goto \ngoto";
|
||||
List<TokenType> expected = List.of(GOTO, GOTO, GOTO, EOF);
|
||||
List<Token> 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<String> expected = List.of("test", "ABC", "g9tgq", "fe_53f");
|
||||
assertEquals(expected, getValues(lex(program)));
|
||||
}
|
||||
|
||||
List<Token> lex(String program) {
|
||||
return lexer.lex(program.toCharArray());
|
||||
}
|
||||
|
||||
List<Object> getValues(List<Token> tokens) {
|
||||
return tokens.stream()
|
||||
.map(Token::getValue)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
}
|
||||
List<TokenType> getTypes(List<Token> tokens) {
|
||||
return tokens.stream()
|
||||
.map(Token::getType)
|
||||
.collect(Collectors.toUnmodifiableList());
|
||||
}
|
||||
}
|
||||
9
src/test/resources/factorial.grsbpl
Normal file
9
src/test/resources/factorial.grsbpl
Normal file
|
|
@ -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
|
||||
19
src/test/resources/fizzbuzz.grsbpl
Normal file
19
src/test/resources/fizzbuzz.grsbpl
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue