great commit history

This commit is contained in:
nora 2021-05-28 14:40:37 +02:00 committed by GitHub
parent af55c80677
commit 0b96e0dd0d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1279 additions and 0 deletions

View 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();
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View 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));
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 +
'}';
}
}

View file

@ -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;
}
}

View 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());
}
}
}

View 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);
}
}

View 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());
}
}

View 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

View 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