diff --git a/StackBasedLanguage/README.md b/StackBasedLanguage/README.md new file mode 100644 index 0000000..aeaa21e --- /dev/null +++ b/StackBasedLanguage/README.md @@ -0,0 +1,92 @@ +# GRSBPL - Generic Random Stack Based Programming Language + +uses some form of reverse polish notation + +``` +1 5 * 5 + +> 10 +``` + +There is a stack and variables. Operations are done on the stack, and you can store results in variables (a bit like in +the JVM). The stack contains integer values. Floating point numbers are not supported. + +When the program finishes (run to the end of the program), the last value on the stack is returned. If the stack is +clear, 0 is always returned. If there is an error during execution, -1 is returned along with an error message to +stderr. + +## Operators and keywords: + +* any number `n` -> push the numeric value of n +* any character `'c'` -> push c as its escaped ascii value +* `+` -> add two values on the stack, pops both and pushes the result +* `-` -> subtract two values on the stack, pops both and pushes the result +* `*` -> multiply two values on the stack, pops both and pushes the result +* `/` -> divide, pops both and pushes the result +* `%` -> mod, pops both and pushes the result +* `not` -> invert stack value (!=0 -> 0, 0 -> 1) +* `swap` -> swaps the 2 top stack values +* `out` -> pop and output it to the console as ascii +* `nount` -> pop and output as a number to the console +* `in` -> push input char as ascii to the stack +* `# comment #` text between # is ignored +* `# comment\n` text after # is ignored +* `&word` -> pop and store it in a variable +* `@word` -> load variable and push it, does not consume the variable +* `:indent` -> define a label +* `goto ident` -> goto a label if the value on the stack is !=0, peek + +Identifier: \w + +Character escape sequences: +\n, \r, \\, \0, \', \b, \f + +## Examples: + +FizzBuzz + +```grsbpl +1 &i # init loop counter +:start # set start label +@i 100 - not goto exit # if i is 100, exit +@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 + +:exit 0 +``` + +## Some Tips + +* Increment a variable: + `@i 1 + &i` +* Pop a value from the stack and discard it: + `&dev_null` (just use any unused variable) +* Goto if equal + `@i 100 - not goto finished` +* Goto not equal + `@i 100 - goto finished` +* Exit the program + `... goto exit ... :exit 0` +* Exit with exit code depending on the branch + ```grsbpl + ... + 69 swap goto exit # push 69 to the 2nd stack position + ... + 5 swap goto exit # push 5 to the 2nd stack position + ... + :exit &del # pop the top stack value to expose the pushed value + ``` \ No newline at end of file diff --git a/StackBasedLanguage/pom.xml b/StackBasedLanguage/pom.xml new file mode 100644 index 0000000..5fb4e18 --- /dev/null +++ b/StackBasedLanguage/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + com.github.nilstrieb + GRSBPL + 1.0-SNAPSHOT + + + org.junit.jupiter + junit-jupiter-api + 5.6.1 + test + + + org.junit.jupiter + junit-jupiter-engine + 5.6.1 + test + + + + + 14 + 14 + + + \ No newline at end of file diff --git a/StackBasedLanguage/src/main/java/IntStack.java b/StackBasedLanguage/src/main/java/IntStack.java new file mode 100644 index 0000000..c6a232c --- /dev/null +++ b/StackBasedLanguage/src/main/java/IntStack.java @@ -0,0 +1,63 @@ +import java.util.OptionalInt; +import java.util.function.BiFunction; + +public class IntStack { + private int[] values; + private int pointer; + + private static final int INITIAL_CAPACITY = 100; + + 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 int peek() { + return values[pointer]; + } + + public OptionalInt tryPop() { + if (pointer == -1) { + return OptionalInt.empty(); + } else { + return OptionalInt.of(pop()); + } + } + + public void performOn2(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/StackBasedLanguage/src/main/java/Interpreter.java b/StackBasedLanguage/src/main/java/Interpreter.java new file mode 100644 index 0000000..54bd7c9 --- /dev/null +++ b/StackBasedLanguage/src/main/java/Interpreter.java @@ -0,0 +1,295 @@ +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public class Interpreter { + + private final Map KEYWORDS = Map.of( + "out", this::out, + "nout", this::nout, + "in", this::in, + "goto", this::condGoto, + "not", this::not, + "swap", this::swap + ); + + private IntStack stack; + private HashMap variables; + private HashMap labels; + private char[] program; + private int i; + private int lineNumber; + + public static void main(String[] args) { + if (args.length < 2) { + System.err.println("usage: "); + System.exit(1); + } + try { + String s = Files.readString(Path.of(args[1])); + Interpreter interpreter = new Interpreter(); + int exit = interpreter.run(s.toCharArray()); + System.exit(exit); + } catch (IOException e) { + System.err.println("File not found"); + } + } + + public int run(char[] chars) { + program = chars; + stack = new IntStack(); + variables = new HashMap<>(); + labels = new HashMap<>(); + i = 0; + lineNumber = 1; + + firstPass(); + i = 0; + lineNumber = 1; + + while (hasNext()) { + try { + runStatement(); + } catch (Exception e) { + System.err.println("Exception occurred on line: " + lineNumber); + e.printStackTrace(); + return -1; + } + } + + return rest(); + } + + private int rest() { + OptionalInt i = stack.tryPop(); + + if (i.isEmpty()) { + return 0; + } else { + return i.getAsInt(); + } + } + + private void firstPass() { + while (hasNext()) { + if (advance() == ':') { + label(); + } + } + } + + private void runStatement() { + switch (advance()) { + case '+' -> add(); + case '-' -> subtract(); + case '*' -> multiply(); + case '/' -> divide(); + case '%' -> modulo(); + case '\'' -> character(); + case '\n' -> lineNumber++; + case ' ', '\t', '\r' -> { + } + case '#' -> comment(); + case '&' -> store(); + case '@' -> load(); + case ':' -> ignoreLabel(); + default -> { + if (Character.isDigit(current())) { + number(); + } else { + keyword(); + } + } + } + } + + private void add() { + stack.performOn2(Integer::sum); + } + + private void subtract() { + stack.performOn2((i1, i2) -> i1 - i2); + } + + private void multiply() { + stack.performOn2((i1, i2) -> i1 * i2); + } + + private void divide() { + stack.performOn2((i1, i2) -> i1 / i2); + } + + private void modulo() { + stack.performOn2((i1, i2) -> i1 % i2); + } + + + private void character() { + int 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 -> { + System.err.println("Invalid escape sequence: \\" + escaped + " on line " + lineNumber); + System.exit(1); + throw new IllegalStateException("system exit failed"); + } + }; + } + stack.push(value); + consume(); + } + + private void comment() { + while (true) { + char next = advance(); + if (next == '\n') { + lineNumber++; + break; + } else if (next == '#') { + break; + } + } + } + + private void store() { + whitespace(); + String name = ident(); + variables.put(name, stack.pop()); + } + + private void load() { + whitespace(); + String name = ident(); + stack.push(variables.get(name)); + } + + private void label() { + whitespace(); + String name = ident(); + labels.put(name, i); + } + + // consume but don't use + private void ignoreLabel() { + whitespace(); + ident(); + } + + private void number() { + String number = String.valueOf(current()); + while (Character.isDigit(peek())) { + number += advance(); + } + stack.push(Integer.parseInt(number)); + } + + private String ident() { + String word = String.valueOf(current()); + + while (Character.isAlphabetic(peek()) || Character.isDigit(peek()) || peek() == '_') { + word += advance(); + } + + return word; + } + + private void keyword() { + String word = ident(); + + Runnable r = KEYWORDS.get(word); + + if (r == null) { + throw new RuntimeException("Invalid keyword: " + word); + } + r.run(); + } + + private void whitespace() { + while (Character.isWhitespace(advance())) { + if (current() == '\n') { + lineNumber++; + } + } + } + + // keywords + private void out() { + System.out.print((char) stack.pop()); + } + + private void nout() { + System.out.print(stack.pop()); + } + + private void in() { + try { + stack.push(System.in.read()); + } catch (IOException e) { + System.err.println("Error reading input on line number " + lineNumber); + System.exit(1); + } + } + + private void condGoto() { + whitespace(); + String label = ident(); + consume(); + if (stack.peek() != 0) { + Integer index = labels.get(label); + if (index == null) { + System.err.println("Label :" + label + " not found on line number: " + lineNumber); + System.exit(1); + } + i = index; + } + } + + private void not() { + int value = stack.pop(); + if (value == 0) { + stack.push(1); + } else { + stack.push(0); + } + } + + private void swap() { + stack.swap(); + } + + // parsing + private char current() { + return program[i - 1]; + } + + private char advance() { + if (i == program.length) { + return '\0'; + } + return program[i++]; + } + + private char peek() { + if (i == program.length) { + return '\0'; + } + return program[i]; + } + + private void consume() { + i++; + } + + private boolean hasNext() { + return i < program.length; + } +} \ No newline at end of file diff --git a/StackBasedLanguage/src/test/java/IntStackTest.java b/StackBasedLanguage/src/test/java/IntStackTest.java new file mode 100644 index 0000000..099a754 --- /dev/null +++ b/StackBasedLanguage/src/test/java/IntStackTest.java @@ -0,0 +1,41 @@ +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +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.performOn2((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/StackBasedLanguage/src/test/java/InterpreterTest.java b/StackBasedLanguage/src/test/java/InterpreterTest.java new file mode 100644 index 0000000..25c2b92 --- /dev/null +++ b/StackBasedLanguage/src/test/java/InterpreterTest.java @@ -0,0 +1,178 @@ +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.FileNotFoundException; +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 static org.junit.jupiter.api.Assertions.assertEquals; + +class InterpreterTest { + + static Interpreter interpreter; + + @BeforeEach + void setup() { + interpreter = new Interpreter(); + } + + @Test + void arithmeticOperations() { + String program = "1 1 * 2 +"; + int result = 3; + assertEquals(result, interpreter.run(program.toCharArray())); + } + + @Test + void bigNumbers() { + String program = "1000 1234 +"; + int result = 2234; + + assertEquals(result, interpreter.run(program.toCharArray())); + } + + @Test + void comment() { + String program = "1 # sdkfjsaf se9 83 252h43ui\n 2 # test 5 # +"; + int result = 3; + + assertEquals(result, interpreter.run(program.toCharArray())); + } + + @Test + void variables() { + String program = "1 &one 2 &two 3 &three 8 @two +"; + int result = 10; + assertEquals(result, interpreter.run(program.toCharArray())); + } + + @Test + void labels() { + String program = "1 :first 2 0"; + int result = 0; + assertEquals(result, interpreter.run(program.toCharArray())); + } + + @Test + void gotoBack() { + String program = "10 &i \n" + + ":start \n" + + "@i nout '\n' out \n" + + "@i 1 - &i \n" + + "@i goto start \n" + + " 0"; + int result = 0; + assertEquals(result, interpreter.run(program.toCharArray())); + } + + @Test + void gotoSkip() { + String program = "1 :first 0 goto first 1 goto skip 3754 78349758 :skip"; + int result = 1; + assertEquals(result, interpreter.run(program.toCharArray())); + } + + @Test + void fizzBuzz() throws IOException, URISyntaxException { + String program = Files.readString(Path.of(getClass().getResource("fizzbuzz.grsbpl").toURI())); + int result = 0; + assertEquals(result, interpreter.run(program.toCharArray())); + } + + @Test + void stackManipulationTest() { + String program = "1 2 swap"; + int result = 1; + assertEquals(result, interpreter.run(program.toCharArray())); + + String program2 = "0 not"; + int result2 = 1; + assertEquals(result2, interpreter.run(program2.toCharArray())); + + String program3 = "1 not"; + int result3 = 0; + assertEquals(result3, interpreter.run(program3.toCharArray())); + } + + @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"; + int result = 0; + + OutStream o = null; + try { + o = new OutStream(); + System.setOut(o); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + + assertEquals(result, interpreter.run(program.toCharArray())); + assertEquals("hello world!\n", o.getOut()); + } + + static class OutStream extends PrintStream { + private final StringBuilder builder = new StringBuilder(); + + public OutStream() throws FileNotFoundException { + super(new OutputStream() { + @Override + public void write(int b) { + } + }); + } + + @Override + public void print(boolean b) { + builder.append(b); + } + + @Override + public void print(char c) { + builder.append(c); + } + + @Override + public void print(int i) { + builder.append(i); + } + + @Override + public void print(long l) { + builder.append(l); + } + + @Override + public void print(float f) { + builder.append(f); + } + + @Override + public void print(double d) { + builder.append(d); + } + + @Override + public void print(char[] s) { + builder.append(s); + } + + @Override + public void print(String s) { + builder.append(s); + } + + @Override + public void print(Object obj) { + super.print(obj); + } + + public String getOut() { + return builder.toString(); + } + } +} \ No newline at end of file diff --git a/StackBasedLanguage/src/test/resources/fizzbuzz.grsbpl b/StackBasedLanguage/src/test/resources/fizzbuzz.grsbpl new file mode 100644 index 0000000..d33c1a5 --- /dev/null +++ b/StackBasedLanguage/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