├── .gitignore ├── LICENSE ├── README.markdown ├── examples ├── .gitignore ├── double.tb ├── hello.tb └── squares.tb ├── pom.xml ├── src ├── main │ └── java │ │ └── com │ │ └── grahamedgecombe │ │ └── tinybasic │ │ ├── TinyBasicCompiler.java │ │ ├── ast │ │ ├── BinaryExpression.java │ │ ├── BinaryOperator.java │ │ ├── BranchStatement.java │ │ ├── BranchType.java │ │ ├── EndStatement.java │ │ ├── Expression.java │ │ ├── IfStatement.java │ │ ├── ImmediateExpression.java │ │ ├── ImmediateString.java │ │ ├── InputStatement.java │ │ ├── LetStatement.java │ │ ├── Line.java │ │ ├── PrintStatement.java │ │ ├── Program.java │ │ ├── RelationalOperator.java │ │ ├── ReturnStatement.java │ │ ├── Statement.java │ │ ├── StringExpression.java │ │ ├── UnaryExpression.java │ │ ├── UnaryOperator.java │ │ └── VariableExpression.java │ │ ├── codegen │ │ ├── CodeGenerator.java │ │ └── x86_64 │ │ │ └── X86_64CodeGenerator.java │ │ ├── parser │ │ └── Parser.java │ │ ├── stackir │ │ ├── Instruction.java │ │ ├── InstructionSequence.java │ │ └── Opcode.java │ │ └── tokenizer │ │ ├── Token.java │ │ └── Tokenizer.java └── test │ └── java │ └── com │ └── grahamedgecombe │ └── tinybasic │ ├── ast │ └── StackIRTest.java │ ├── parser │ └── ParserTest.java │ └── tokenizer │ └── TokenizerTest.java └── tinybasic /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | *~ 3 | *.iml 4 | *.o 5 | *.asm 6 | /target 7 | !.git* 8 | !.mailmap 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Graham Edgecombe 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | Tiny BASIC Compiler 2 | =================== 3 | 4 | Introduction 5 | ------------ 6 | 7 | A simple [Tiny BASIC][tinybasic] compiler that targets x86-64 Linux machines. 8 | 9 | Usage 10 | ----- 11 | 12 | The compiler can be built with Java 8 and [Apache Maven][maven]. The following 13 | command will build it and run the unit tests: 14 | 15 | mvn compile test 16 | 17 | You also need [NASM][nasm] and [GCC][gcc], which are used for assembling the 18 | output of the compiler and linking it with the standard C library. 19 | 20 | Example programs can be found in the `examples` folder. They can be compiled and 21 | executed like so: 22 | 23 | $ cat examples/squares.tb 24 | 10 LET X = 1 25 | 20 PRINT X * X 26 | 30 LET X = X + 1 27 | 40 IF X > 10 THEN END 28 | 50 GOTO 20 29 | $ ./tinybasic examples/squares.tb 30 | $ ./examples/squares 31 | 1 32 | 4 33 | 9 34 | 16 35 | 25 36 | 36 37 | 49 38 | 64 39 | 81 40 | 100 41 | $ 42 | 43 | License 44 | ------- 45 | 46 | This project is available under the terms of the [ISC license][isc], which is 47 | similar to the 2-clause BSD license. See the `LICENSE` file for the copyright 48 | information and licensing terms. 49 | 50 | [maven]: https://maven.apache.org/ 51 | [tinybasic]: https://en.wikipedia.org/wiki/Tiny_BASIC 52 | [isc]: https://www.isc.org/software/license/ 53 | [gcc]: http://gcc.gnu.org/ 54 | [nasm]: http://www.nasm.us/ 55 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | /squares 2 | /double 3 | /hello 4 | -------------------------------------------------------------------------------- /examples/double.tb: -------------------------------------------------------------------------------- 1 | 10 INPUT X 2 | 20 GOSUB 50 3 | 30 PRINT X 4 | 40 END 5 | 50 LET X = X * 2 6 | 60 RETURN 7 | -------------------------------------------------------------------------------- /examples/hello.tb: -------------------------------------------------------------------------------- 1 | 10 PRINT "Hello, world!" 2 | -------------------------------------------------------------------------------- /examples/squares.tb: -------------------------------------------------------------------------------- 1 | 10 LET X = 1 2 | 20 PRINT X * X 3 | 30 LET X = X + 1 4 | 40 IF X > 10 THEN END 5 | 50 GOTO 20 6 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.grahamedgecombe 6 | tiny-basic-compiler 7 | 1.0-SNAPSHOT 8 | jar 9 | 10 | Tiny BASIC Compiler 11 | 12 | 13 | UTF-8 14 | 15 | 16 | 17 | 18 | 19 | org.apache.maven.plugins 20 | maven-compiler-plugin 21 | 3.1 22 | 23 | 1.8 24 | 1.8 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | junit 33 | junit 34 | 4.11 35 | test 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/TinyBasicCompiler.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic; 2 | 3 | import com.grahamedgecombe.tinybasic.codegen.CodeGenerator; 4 | import com.grahamedgecombe.tinybasic.codegen.x86_64.X86_64CodeGenerator; 5 | import com.grahamedgecombe.tinybasic.parser.Parser; 6 | import com.grahamedgecombe.tinybasic.tokenizer.Tokenizer; 7 | 8 | import java.io.IOException; 9 | import java.nio.charset.StandardCharsets; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | 14 | public final class TinyBasicCompiler { 15 | 16 | public static void main(String[] args) throws IOException { 17 | Path inputPath = Paths.get(args[0]); 18 | Path outputPath = Paths.get(args[1]); 19 | try (Tokenizer tokenizer = new Tokenizer(Files.newBufferedReader(inputPath, StandardCharsets.UTF_8))) { 20 | try (Parser parser = new Parser(tokenizer)) { 21 | try (CodeGenerator generator = new X86_64CodeGenerator(Files.newBufferedWriter(outputPath, StandardCharsets.UTF_8))) { 22 | generator.generate(parser.parse().compile()); 23 | } 24 | } 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/BinaryExpression.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | import java.util.Objects; 8 | 9 | public final class BinaryExpression extends Expression { 10 | 11 | private final BinaryOperator operator; 12 | private final Expression leftExpression, rightExpression; 13 | 14 | public BinaryExpression(BinaryOperator operator, Expression leftExpression, Expression rightExpression) { 15 | this.operator = operator; 16 | this.leftExpression = leftExpression; 17 | this.rightExpression = rightExpression; 18 | } 19 | 20 | public BinaryOperator getOperator() { 21 | return operator; 22 | } 23 | 24 | public Expression getLeftExpression() { 25 | return leftExpression; 26 | } 27 | 28 | public Expression getRightExpression() { 29 | return rightExpression; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) return true; 35 | if (o == null || getClass() != o.getClass()) return false; 36 | 37 | BinaryExpression that = (BinaryExpression) o; 38 | 39 | if (!leftExpression.equals(that.leftExpression)) return false; 40 | if (operator != that.operator) return false; 41 | if (!rightExpression.equals(that.rightExpression)) return false; 42 | 43 | return true; 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | return Objects.hash(operator, leftExpression, rightExpression); 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return "(" + leftExpression + " " + operator + " " + rightExpression + ")"; 54 | } 55 | 56 | @Override 57 | public void compile(InstructionSequence seq) { 58 | leftExpression.compile(seq); 59 | rightExpression.compile(seq); 60 | switch (operator) { 61 | case PLUS: 62 | seq.append(new Instruction(Opcode.ADD)); 63 | break; 64 | case MINUS: 65 | seq.append(new Instruction(Opcode.SUB)); 66 | break; 67 | case MULT: 68 | seq.append(new Instruction(Opcode.MUL)); 69 | break; 70 | case DIV: 71 | seq.append(new Instruction(Opcode.DIV)); 72 | break; 73 | default: 74 | throw new AssertionError(); 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/BinaryOperator.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | public enum BinaryOperator { 4 | PLUS('+'), MINUS('-'), MULT('*'), DIV('/'); 5 | 6 | private final char character; 7 | 8 | private BinaryOperator(char character) { 9 | this.character = character; 10 | } 11 | 12 | public String toString() { 13 | return String.valueOf(character); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/BranchStatement.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | import java.util.Objects; 8 | 9 | public final class BranchStatement extends Statement { 10 | 11 | private final BranchType type; 12 | private final int target; 13 | 14 | public BranchStatement(BranchType type, int target) { 15 | this.type = type; 16 | this.target = target; 17 | } 18 | 19 | public BranchType getType() { 20 | return type; 21 | } 22 | 23 | public int getTarget() { 24 | return target; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | 32 | BranchStatement that = (BranchStatement) o; 33 | 34 | if (target != that.target) return false; 35 | if (type != that.type) return false; 36 | 37 | return true; 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(type, target); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return type + " " + target; 48 | } 49 | 50 | @Override 51 | public void compile(InstructionSequence seq) { 52 | Opcode opcode; 53 | switch (type) { 54 | case GOTO: 55 | opcode = Opcode.JMP; 56 | break; 57 | case GOSUB: 58 | opcode = Opcode.CALL; 59 | break; 60 | default: 61 | throw new AssertionError(); 62 | } 63 | seq.append(new Instruction(opcode, seq.createLineLabel(target))); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/BranchType.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | public enum BranchType { 4 | GOTO, GOSUB 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/EndStatement.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | public final class EndStatement extends Statement { 8 | 9 | @Override 10 | public boolean equals(Object o) { 11 | if (this == o) return true; 12 | if (o == null || getClass() != o.getClass()) return false; 13 | 14 | return true; 15 | } 16 | 17 | @Override 18 | public int hashCode() { 19 | return 0; 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return "END"; 25 | } 26 | 27 | @Override 28 | public void compile(InstructionSequence seq) { 29 | seq.append(new Instruction(Opcode.HLT)); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/Expression.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | public abstract class Expression extends StringExpression { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/IfStatement.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | import java.util.Objects; 8 | 9 | public final class IfStatement extends Statement { 10 | 11 | private final RelationalOperator operator; 12 | private final Expression leftExpression, rightExpression; 13 | private final Statement statement; 14 | 15 | public IfStatement(RelationalOperator operator, Expression leftExpression, Expression rightExpression, Statement statement) { 16 | this.operator = operator; 17 | this.leftExpression = leftExpression; 18 | this.rightExpression = rightExpression; 19 | this.statement = statement; 20 | } 21 | 22 | public RelationalOperator getOperator() { 23 | return operator; 24 | } 25 | 26 | public Expression getLeftExpression() { 27 | return leftExpression; 28 | } 29 | 30 | public Expression getRightExpression() { 31 | return rightExpression; 32 | } 33 | 34 | public Statement getStatement() { 35 | return statement; 36 | } 37 | 38 | @Override 39 | public boolean equals(Object o) { 40 | if (this == o) return true; 41 | if (o == null || getClass() != o.getClass()) return false; 42 | 43 | IfStatement that = (IfStatement) o; 44 | 45 | if (!leftExpression.equals(that.leftExpression)) return false; 46 | if (operator != that.operator) return false; 47 | if (!rightExpression.equals(that.rightExpression)) return false; 48 | if (!statement.equals(that.statement)) return false; 49 | 50 | return true; 51 | } 52 | 53 | @Override 54 | public int hashCode() { 55 | return Objects.hash(operator, leftExpression, rightExpression, statement); 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return "IF " + leftExpression + " " + operator + " " + rightExpression + " THEN " + statement; 61 | } 62 | 63 | @Override 64 | public void compile(InstructionSequence seq) { 65 | leftExpression.compile(seq); 66 | rightExpression.compile(seq); 67 | 68 | String thenLabel = seq.createGeneratedLabel(); 69 | String endLabel = seq.createGeneratedLabel(); 70 | 71 | Opcode opcode; 72 | switch (operator) { 73 | case EQ: 74 | opcode = Opcode.JMPEQ; 75 | break; 76 | case NE: 77 | opcode = Opcode.JMPNE; 78 | break; 79 | case LTE: 80 | opcode = Opcode.JMPLTE; 81 | break; 82 | case LT: 83 | opcode = Opcode.JMPLT; 84 | break; 85 | case GT: 86 | opcode = Opcode.JMPGT; 87 | break; 88 | case GTE: 89 | opcode = Opcode.JMPGTE; 90 | break; 91 | default: 92 | throw new AssertionError(); 93 | } 94 | 95 | seq.append( 96 | new Instruction(opcode, thenLabel), 97 | new Instruction(Opcode.JMP, endLabel), 98 | new Instruction(Opcode.LABEL, thenLabel) 99 | ); 100 | 101 | statement.compile(seq); 102 | 103 | seq.append(new Instruction(Opcode.LABEL, endLabel)); 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/ImmediateExpression.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | public final class ImmediateExpression extends Expression { 8 | 9 | private final int value; 10 | 11 | public ImmediateExpression(int value) { 12 | this.value = value; 13 | } 14 | 15 | public int getValue() { 16 | return value; 17 | } 18 | 19 | @Override 20 | public boolean equals(Object o) { 21 | if (this == o) return true; 22 | if (o == null || getClass() != o.getClass()) return false; 23 | 24 | ImmediateExpression that = (ImmediateExpression) o; 25 | 26 | if (value != that.value) return false; 27 | 28 | return true; 29 | } 30 | 31 | @Override 32 | public int hashCode() { 33 | return value; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return Integer.toString(value); 39 | } 40 | 41 | @Override 42 | public void compile(InstructionSequence seq) { 43 | seq.append(new Instruction(Opcode.PUSHI, value)); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/ImmediateString.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | public final class ImmediateString extends StringExpression { 8 | 9 | private final String value; 10 | 11 | public ImmediateString(String value) { 12 | this.value = value; 13 | } 14 | 15 | public String getValue() { 16 | return value; 17 | } 18 | 19 | @Override 20 | public boolean equals(Object o) { 21 | if (this == o) return true; 22 | if (o == null || getClass() != o.getClass()) return false; 23 | 24 | ImmediateString that = (ImmediateString) o; 25 | 26 | if (!value.equals(that.value)) return false; 27 | 28 | return true; 29 | } 30 | 31 | @Override 32 | public int hashCode() { 33 | return value.hashCode(); 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return "\"" + value + "\""; 39 | } 40 | 41 | @Override 42 | public void compile(InstructionSequence seq) { 43 | seq.append(new Instruction(Opcode.PUSHS, value)); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/InputStatement.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public final class InputStatement extends Statement { 13 | 14 | private final List names; 15 | 16 | public InputStatement(String... names) { 17 | this.names = Collections.unmodifiableList(Arrays.asList(names)); 18 | } 19 | 20 | public InputStatement(List names) { 21 | this.names = Collections.unmodifiableList(new ArrayList<>(names)); 22 | } 23 | 24 | public List getNames() { 25 | return names; 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) return true; 31 | if (o == null || getClass() != o.getClass()) return false; 32 | 33 | InputStatement that = (InputStatement) o; 34 | 35 | if (!names.equals(that.names)) return false; 36 | 37 | return true; 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return names.hashCode(); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | StringBuilder buf = new StringBuilder("INPUT "); 48 | for (int i = 0; i < names.size(); i++) { 49 | buf.append(names.get(i)); 50 | if (i != (names.size() - 1)) 51 | buf.append(", "); 52 | } 53 | return buf.toString(); 54 | } 55 | 56 | @Override 57 | public void compile(InstructionSequence seq) { 58 | for (String name : names) { 59 | seq.append( 60 | new Instruction(Opcode.IN), 61 | new Instruction(Opcode.STORE, name) 62 | ); 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/LetStatement.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | import java.util.Objects; 8 | 9 | public final class LetStatement extends Statement { 10 | 11 | private final String name; 12 | private final Expression value; 13 | 14 | public LetStatement(String name, Expression value) { 15 | this.name = name; 16 | this.value = value; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public Expression getValue() { 24 | return value; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | 32 | LetStatement that = (LetStatement) o; 33 | 34 | if (!name.equals(that.name)) return false; 35 | if (!value.equals(that.value)) return false; 36 | 37 | return true; 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(name, value); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "LET " + name + " = " + value; 48 | } 49 | 50 | @Override 51 | public void compile(InstructionSequence seq) { 52 | value.compile(seq); 53 | seq.append(new Instruction(Opcode.STORE, name)); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/Line.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | import java.util.Objects; 8 | 9 | public final class Line { 10 | 11 | private final int number; 12 | private final Statement statement; 13 | 14 | public Line(int number, Statement statement) { 15 | this.number = number; 16 | this.statement = statement; 17 | } 18 | 19 | public int getNumber() { 20 | return number; 21 | } 22 | 23 | public Statement getStatement() { 24 | return statement; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | 32 | Line line = (Line) o; 33 | 34 | if (number != line.number) return false; 35 | if (!statement.equals(line.statement)) return false; 36 | 37 | return true; 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(number, statement); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return number + " " + statement; 48 | } 49 | 50 | public void compile(InstructionSequence seq) { 51 | seq.append(new Instruction(Opcode.LABEL, seq.createLineLabel(number))); 52 | statement.compile(seq); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/PrintStatement.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | public final class PrintStatement extends Statement { 13 | 14 | private final List values; 15 | 16 | public PrintStatement(StringExpression... values) { 17 | this.values = Collections.unmodifiableList(Arrays.asList(values)); 18 | } 19 | 20 | public PrintStatement(List values) { 21 | this.values = Collections.unmodifiableList(new ArrayList<>(values)); 22 | } 23 | 24 | public List getValues() { 25 | return values; 26 | } 27 | 28 | @Override 29 | public boolean equals(Object o) { 30 | if (this == o) return true; 31 | if (o == null || getClass() != o.getClass()) return false; 32 | 33 | PrintStatement that = (PrintStatement) o; 34 | 35 | if (!values.equals(that.values)) return false; 36 | 37 | return true; 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return values.hashCode(); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | StringBuilder buf = new StringBuilder("PRINT "); 48 | for (int i = 0; i < values.size(); i++) { 49 | buf.append(values.get(i)); 50 | if (i != (values.size() - 1)) 51 | buf.append(", "); 52 | } 53 | return buf.toString(); 54 | } 55 | 56 | @Override 57 | public void compile(InstructionSequence seq) { 58 | for (StringExpression value : values) { 59 | value.compile(seq); 60 | 61 | /* this is rather hacky, but the only place types are important, so it doesn't seem worth improving it */ 62 | seq.append(new Instruction(value instanceof ImmediateString ? Opcode.OUTS : Opcode.OUTI)); 63 | } 64 | 65 | seq.append(new Instruction(Opcode.PUSHS, "\n"), new Instruction(Opcode.OUTS)); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/Program.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | public final class Program { 11 | 12 | private final List lines; 13 | 14 | public Program(Line... lines) { 15 | this.lines = Collections.unmodifiableList(Arrays.asList(lines)); 16 | } 17 | 18 | public Program(List lines) { 19 | this.lines = Collections.unmodifiableList(new ArrayList<>(lines)); 20 | } 21 | 22 | public List getLines() { 23 | return lines; 24 | } 25 | 26 | @Override 27 | public boolean equals(Object o) { 28 | if (this == o) return true; 29 | if (o == null || getClass() != o.getClass()) return false; 30 | 31 | Program program = (Program) o; 32 | 33 | if (!lines.equals(program.lines)) return false; 34 | 35 | return true; 36 | } 37 | 38 | @Override 39 | public int hashCode() { 40 | return lines.hashCode(); 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | StringBuilder buf = new StringBuilder(); 46 | for (Line line : lines) { 47 | buf.append(line).append("\n"); 48 | } 49 | return buf.toString(); 50 | } 51 | 52 | public InstructionSequence compile() { 53 | InstructionSequence seq = new InstructionSequence(); 54 | for (Line line : lines) { 55 | line.compile(seq); 56 | } 57 | return seq; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/RelationalOperator.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | public enum RelationalOperator { 4 | EQ("="), NE("<>"), GT(">"), GTE(">="), LT("<"), LTE("<="); 5 | 6 | private final String string; 7 | 8 | private RelationalOperator(String string) { 9 | this.string = string; 10 | } 11 | 12 | public String toString() { 13 | return string; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/ReturnStatement.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | public final class ReturnStatement extends Statement { 8 | 9 | @Override 10 | public boolean equals(Object o) { 11 | if (this == o) return true; 12 | if (o == null || getClass() != o.getClass()) return false; 13 | 14 | return true; 15 | } 16 | 17 | @Override 18 | public int hashCode() { 19 | return 0; 20 | } 21 | 22 | @Override 23 | public String toString() { 24 | return "RETURN"; 25 | } 26 | 27 | @Override 28 | public void compile(InstructionSequence seq) { 29 | seq.append(new Instruction(Opcode.RET)); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/Statement.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 4 | 5 | public abstract class Statement { 6 | 7 | public abstract void compile(InstructionSequence seq); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/StringExpression.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 4 | 5 | public abstract class StringExpression { 6 | 7 | public abstract void compile(InstructionSequence seq); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/UnaryExpression.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | import java.util.Objects; 8 | 9 | public final class UnaryExpression extends Expression { 10 | 11 | private final UnaryOperator operator; 12 | private final Expression expression; 13 | 14 | public UnaryExpression(UnaryOperator operator, Expression expression) { 15 | this.operator = operator; 16 | this.expression = expression; 17 | } 18 | 19 | public UnaryOperator getOperator() { 20 | return operator; 21 | } 22 | 23 | public Expression getExpression() { 24 | return expression; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object o) { 29 | if (this == o) return true; 30 | if (o == null || getClass() != o.getClass()) return false; 31 | 32 | UnaryExpression that = (UnaryExpression) o; 33 | 34 | if (!expression.equals(that.expression)) return false; 35 | if (operator != that.operator) return false; 36 | 37 | return true; 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(operator, expression); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "(" + operator + expression + ")"; 48 | } 49 | 50 | @Override 51 | public void compile(InstructionSequence seq) { 52 | switch (operator) { 53 | case PLUS: 54 | expression.compile(seq); 55 | break; 56 | case MINUS: 57 | seq.append(new Instruction(Opcode.PUSHI, 0)); 58 | expression.compile(seq); 59 | seq.append(new Instruction(Opcode.SUB)); 60 | break; 61 | default: 62 | throw new AssertionError(); 63 | } 64 | 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/UnaryOperator.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | public enum UnaryOperator { 4 | PLUS('+'), MINUS('-'); 5 | 6 | private final char character; 7 | 8 | private UnaryOperator(char character) { 9 | this.character = character; 10 | } 11 | 12 | public String toString() { 13 | return String.valueOf(character); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/ast/VariableExpression.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | 7 | public final class VariableExpression extends Expression { 8 | 9 | private final String name; 10 | 11 | public VariableExpression(String name) { 12 | this.name = name; 13 | } 14 | 15 | public String getName() { 16 | return name; 17 | } 18 | 19 | @Override 20 | public boolean equals(Object o) { 21 | if (this == o) return true; 22 | if (o == null || getClass() != o.getClass()) return false; 23 | 24 | VariableExpression that = (VariableExpression) o; 25 | 26 | if (!name.equals(that.name)) return false; 27 | 28 | return true; 29 | } 30 | 31 | @Override 32 | public int hashCode() { 33 | return name.hashCode(); 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return name; 39 | } 40 | 41 | @Override 42 | public void compile(InstructionSequence seq) { 43 | seq.append(new Instruction(Opcode.LOAD, name)); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/codegen/CodeGenerator.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.codegen; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 4 | 5 | import java.io.Closeable; 6 | import java.io.IOException; 7 | 8 | public abstract class CodeGenerator implements Closeable { 9 | 10 | public abstract void generate(InstructionSequence seq) throws IOException; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/codegen/x86_64/X86_64CodeGenerator.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.codegen.x86_64; 2 | 3 | import com.grahamedgecombe.tinybasic.codegen.CodeGenerator; 4 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 5 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 6 | 7 | import java.io.IOException; 8 | import java.io.Writer; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | public final class X86_64CodeGenerator extends CodeGenerator { 13 | 14 | private final Writer writer; 15 | 16 | public X86_64CodeGenerator(Writer writer) { 17 | this.writer = writer; 18 | } 19 | 20 | @Override 21 | public void generate(InstructionSequence seq) throws IOException { 22 | writer.append("[extern exit]\n"); 23 | writer.append("[extern printf]\n"); 24 | writer.append("[extern scanf]\n"); 25 | writer.append("[section .code]\n"); 26 | writer.append("[global main]\n"); 27 | writer.append("main:\n"); 28 | writer.append(" push rbp\n"); 29 | writer.append(" mov rbp, rsp\n"); 30 | writer.append(" sub rsp, " + (8 * 27) + "\n"); 31 | 32 | Map strings = new HashMap<>(); 33 | for (Instruction instruction : seq.getInstructions()) { 34 | switch (instruction.getOpcode()) { 35 | case LABEL: 36 | writer.append(instruction.getStringOperand().get() + ":\n"); 37 | break; 38 | 39 | case PUSHI: 40 | writer.append(" push 0x" + Integer.toHexString(instruction.getIntegerOperand().get()) + "\n"); 41 | break; 42 | 43 | case PUSHS: 44 | String label = seq.createGeneratedLabel(); 45 | strings.put(label, instruction.getStringOperand().get()); 46 | writer.append(" push " + label + "\n"); 47 | break; 48 | 49 | case LOAD: 50 | writer.append(" mov rax, [rbp - " + varIndex(instruction) + "]\n"); 51 | writer.append(" push rax\n"); 52 | break; 53 | 54 | case STORE: 55 | writer.append(" pop rax\n"); 56 | writer.append(" mov [rbp - " + varIndex(instruction) + "], rax\n"); 57 | break; 58 | 59 | case ADD: 60 | writer.append(" pop rax\n"); 61 | writer.append(" pop rbx\n"); 62 | writer.append(" add rax, rbx\n"); 63 | writer.append(" push rax\n"); 64 | break; 65 | 66 | case SUB: 67 | writer.append(" pop rax\n"); 68 | writer.append(" pop rbx\n"); 69 | writer.append(" sub rax, rbx\n"); 70 | writer.append(" push rax\n"); 71 | break; 72 | 73 | case MUL: 74 | writer.append(" pop rax\n"); 75 | writer.append(" pop rbx\n"); 76 | writer.append(" imul rax, rbx\n"); 77 | writer.append(" push rax\n"); 78 | break; 79 | 80 | case DIV: 81 | writer.append(" xor rdx, rdx\n"); 82 | writer.append(" pop rax\n"); 83 | writer.append(" pop rbx\n"); 84 | writer.append(" idiv rbx\n"); 85 | writer.append(" push rax\n"); 86 | break; 87 | 88 | case CALL: 89 | writer.append(" call " + instruction.getStringOperand().get() + "\n"); 90 | break; 91 | 92 | case RET: 93 | writer.append(" ret\n"); 94 | break; 95 | 96 | case JMP: 97 | writer.append(" jmp " + instruction.getStringOperand().get() + "\n"); 98 | break; 99 | 100 | case JMPGT: 101 | writer.append(" pop rbx\n"); 102 | writer.append(" pop rax\n"); 103 | writer.append(" cmp rax, rbx\n"); 104 | writer.append(" jg " + instruction.getStringOperand().get() + "\n"); 105 | break; 106 | 107 | case JMPGTE: 108 | writer.append(" pop rbx\n"); 109 | writer.append(" pop rax\n"); 110 | writer.append(" cmp rax, rbx\n"); 111 | writer.append(" jge " + instruction.getStringOperand().get() + "\n"); 112 | break; 113 | 114 | case JMPLT: 115 | writer.append(" pop rbx\n"); 116 | writer.append(" pop rax\n"); 117 | writer.append(" cmp rax, rbx\n"); 118 | writer.append(" jl " + instruction.getStringOperand().get() + "\n"); 119 | break; 120 | 121 | case JMPLTE: 122 | writer.append(" pop rbx\n"); 123 | writer.append(" pop rax\n"); 124 | writer.append(" cmp rax, rbx\n"); 125 | writer.append(" jle " + instruction.getStringOperand().get() + "\n"); 126 | break; 127 | 128 | case JMPEQ: 129 | writer.append(" pop rbx\n"); 130 | writer.append(" pop rax\n"); 131 | writer.append(" cmp rax, rbx\n"); 132 | writer.append(" je " + instruction.getStringOperand().get() + "\n"); 133 | break; 134 | 135 | case JMPNE: 136 | writer.append(" pop rbx\n"); 137 | writer.append(" pop rax\n"); 138 | writer.append(" cmp rax, rbx\n"); 139 | writer.append(" jne " + instruction.getStringOperand().get() + "\n"); 140 | break; 141 | 142 | case HLT: 143 | writer.append(" mov rdi, 0\n"); 144 | writer.append(" call exit\n"); 145 | break; 146 | 147 | case IN: 148 | strings.put("num_fmt", "%d"); 149 | writer.append(" lea rsi, [rbp - " + (8 * 26) + "]\n"); 150 | writer.append(" mov rdi, num_fmt\n"); 151 | writer.append(" mov al, 0\n"); 152 | writer.append(" call scanf\n"); 153 | writer.append(" xor rax, rax\n"); 154 | writer.append(" mov eax, [rbp - " + (8 * 26) + "]\n"); 155 | writer.append(" push rax\n"); 156 | break; 157 | 158 | case OUTS: 159 | strings.put("str_fmt", "%s"); 160 | writer.append(" pop rsi\n"); 161 | writer.append(" mov rdi, str_fmt\n"); 162 | writer.append(" mov al, 0\n"); 163 | writer.append(" call printf\n"); 164 | break; 165 | 166 | case OUTI: 167 | strings.put("num_fmt", "%d"); 168 | writer.append(" pop rsi\n"); 169 | writer.append(" mov rdi, num_fmt\n"); 170 | writer.append(" mov al, 0\n"); 171 | writer.append(" call printf\n"); 172 | break; 173 | } 174 | } 175 | 176 | writer.append(" mov rax, 0\n"); 177 | writer.append(" mov rsp, rbp\n"); 178 | writer.append(" pop rbp\n"); 179 | writer.append(" ret\n"); 180 | 181 | writer.append("[section .rodata]\n"); 182 | for (Map.Entry string : strings.entrySet()) { 183 | writer.append(string.getKey() + ":\n"); 184 | writer.append(" db \"" + escape(string.getValue()) + "\", 0\n"); 185 | } 186 | } 187 | 188 | private String escape(String value) { 189 | value = value.replace("\n", "\", 10, \""); 190 | return value; 191 | } 192 | 193 | private int varIndex(Instruction instruction) { 194 | return (instruction.getStringOperand().get().charAt(0) - 'A') * 8; 195 | } 196 | 197 | @Override 198 | public void close() throws IOException { 199 | writer.close(); 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/parser/Parser.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.parser; 2 | 3 | import com.grahamedgecombe.tinybasic.ast.*; 4 | import com.grahamedgecombe.tinybasic.tokenizer.Token; 5 | import com.grahamedgecombe.tinybasic.tokenizer.Token.Type; 6 | import com.grahamedgecombe.tinybasic.tokenizer.Tokenizer; 7 | 8 | import java.io.Closeable; 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | public final class Parser implements Closeable { 14 | 15 | private final Tokenizer tokenizer; 16 | private Token token; 17 | 18 | public Parser(Tokenizer tokenizer) throws IOException { 19 | this.tokenizer = tokenizer; 20 | this.token = tokenizer.nextToken(); 21 | } 22 | 23 | private void consume() throws IOException { 24 | token = tokenizer.nextToken(); 25 | } 26 | 27 | private boolean accept(Type type) throws IOException { 28 | if (token.getType() == type) { 29 | consume(); 30 | return true; 31 | } 32 | 33 | return false; 34 | } 35 | 36 | private void expect(Type type) throws IOException { 37 | if (!accept(type)) 38 | throw new IOException("Unexpected " + token.getType() + ", expecting " + type); 39 | } 40 | 41 | public Program parse() throws IOException { 42 | List lines = new ArrayList<>(); 43 | while (!accept(Type.EOF)) { 44 | lines.add(nextLine()); 45 | } 46 | return new Program(lines); 47 | } 48 | 49 | Line nextLine() throws IOException { 50 | if (token.getType() != Type.NUMBER) 51 | throw new IOException("Unexpected " + token.getType() + ", expecting NUMBER"); 52 | 53 | int lineNumber = Integer.parseInt(token.getValue().get()); 54 | consume(); 55 | 56 | Statement stmt = nextStatement(); 57 | 58 | if (token.getType() != Type.LF && token.getType() != Type.EOF) 59 | throw new IOException("Unexpected " + token.getType() + ", expecting LF or EOF"); 60 | 61 | consume(); 62 | return new Line(lineNumber, stmt); 63 | } 64 | 65 | Statement nextStatement() throws IOException { 66 | if (token.getType() != Type.KEYWORD) 67 | throw new IOException("Unexpected " + token.getType() + ", expecting KEYWORD"); 68 | 69 | String keyword = token.getValue().get(); 70 | switch (keyword) { 71 | case "PRINT": 72 | consume(); 73 | 74 | List values = new ArrayList<>(); 75 | do { 76 | if (token.getType() == Type.STRING) { 77 | values.add(new ImmediateString(token.getValue().get())); 78 | consume(); 79 | } else { 80 | values.add(nextExpression()); 81 | } 82 | } while (accept(Type.COMMA)); 83 | 84 | return new PrintStatement(values); 85 | 86 | case "IF": 87 | consume(); 88 | 89 | Expression left = nextExpression(); 90 | 91 | RelationalOperator operator; 92 | switch (token.getType()) { 93 | case EQ: 94 | operator = RelationalOperator.EQ; 95 | break; 96 | case NE: 97 | operator = RelationalOperator.NE; 98 | break; 99 | case LT: 100 | operator = RelationalOperator.LT; 101 | break; 102 | case LTE: 103 | operator = RelationalOperator.LTE; 104 | break; 105 | case GT: 106 | operator = RelationalOperator.GT; 107 | break; 108 | case GTE: 109 | operator = RelationalOperator.GTE; 110 | break; 111 | default: 112 | throw new IOException("Unexpected " + token.getType() + ", expecting EQ, NE, LT, LTE, GT or GTE"); 113 | } 114 | consume(); 115 | 116 | Expression right = nextExpression(); 117 | 118 | if (token.getType() != Type.KEYWORD) 119 | throw new IOException("Unexpected " + token.getType() + ", expecting KEYWORD"); 120 | 121 | String thenKeyword = token.getValue().get(); 122 | if (!thenKeyword.equals("THEN")) 123 | throw new IOException("Unexpected keyword " + keyword + ", expecting THEN"); 124 | 125 | consume(); 126 | 127 | Statement statement = nextStatement(); 128 | return new IfStatement(operator, left, right, statement); 129 | 130 | case "GOTO": 131 | case "GOSUB": 132 | consume(); 133 | 134 | if (token.getType() != Type.NUMBER) 135 | throw new IOException("Unexpected " + token.getType() + ", expecting NUMBER"); 136 | 137 | int target = Integer.parseInt(token.getValue().get()); 138 | consume(); 139 | 140 | BranchType type = keyword.equals("GOTO") ? BranchType.GOTO : BranchType.GOSUB; 141 | return new BranchStatement(type, target); 142 | 143 | case "INPUT": 144 | consume(); 145 | 146 | List names = new ArrayList<>(); 147 | do { 148 | if (token.getType() != Type.VAR) 149 | throw new IOException("Unexpected " + token.getType() + ", expecting VAR"); 150 | 151 | names.add(token.getValue().get()); 152 | consume(); 153 | } while (accept(Type.COMMA)); 154 | 155 | return new InputStatement(names); 156 | 157 | case "LET": 158 | consume(); 159 | 160 | if (token.getType() != Type.VAR) 161 | throw new IOException("Unexpected " + token.getType() + ", expecting VAR"); 162 | 163 | String name = token.getValue().get(); 164 | consume(); 165 | 166 | expect(Type.EQ); 167 | 168 | return new LetStatement(name, nextExpression()); 169 | 170 | case "RETURN": 171 | consume(); 172 | return new ReturnStatement(); 173 | 174 | case "END": 175 | consume(); 176 | return new EndStatement(); 177 | 178 | default: 179 | throw new IOException("Unknown keyword: " + keyword); 180 | } 181 | } 182 | 183 | Expression nextExpression() throws IOException { 184 | Expression left; 185 | 186 | if (token.getType() == Type.PLUS || token.getType() == Type.MINUS) { 187 | UnaryOperator operator = token.getType() == Type.PLUS ? UnaryOperator.PLUS : UnaryOperator.MINUS; 188 | consume(); 189 | 190 | left = new UnaryExpression(operator, nextTerm()); 191 | } else { 192 | left = nextTerm(); 193 | } 194 | 195 | while (token.getType() == Type.PLUS || token.getType() == Type.MINUS) { 196 | BinaryOperator operator = token.getType() == Type.PLUS ? BinaryOperator.PLUS : BinaryOperator.MINUS; 197 | consume(); 198 | 199 | Expression right = nextTerm(); 200 | left = new BinaryExpression(operator, left, right); 201 | } 202 | 203 | return left; 204 | } 205 | 206 | private Expression nextTerm() throws IOException { 207 | Expression left = nextFactor(); 208 | 209 | while (token.getType() == Type.MULT || token.getType() == Type.DIV) { 210 | BinaryOperator operator = token.getType() == Type.MULT ? BinaryOperator.MULT : BinaryOperator.DIV; 211 | consume(); 212 | 213 | left = new BinaryExpression(operator, left, nextFactor()); 214 | } 215 | 216 | return left; 217 | } 218 | 219 | private Expression nextFactor() throws IOException { 220 | switch (token.getType()) { 221 | case VAR: 222 | Expression expr = new VariableExpression(token.getValue().get()); 223 | consume(); 224 | return expr; 225 | 226 | case NUMBER: 227 | expr = new ImmediateExpression(Integer.parseInt(token.getValue().get())); 228 | consume(); 229 | return expr; 230 | 231 | case LPAREN: 232 | consume(); 233 | expr = nextExpression(); 234 | expect(Type.RPAREN); 235 | return expr; 236 | 237 | default: 238 | throw new IOException("Unexpected " + token.getType() + ", expecting VAR, NUMBER or LPAREN"); 239 | } 240 | } 241 | 242 | @Override 243 | public void close() throws IOException { 244 | tokenizer.close(); 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/stackir/Instruction.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.stackir; 2 | 3 | import java.util.Objects; 4 | import java.util.Optional; 5 | 6 | public final class Instruction { 7 | 8 | private final Opcode opcode; 9 | private final Optional stringOperand; 10 | private final Optional integerOperand; 11 | 12 | public Instruction(Opcode opcode) { 13 | this.opcode = opcode; 14 | this.stringOperand = Optional.empty(); 15 | this.integerOperand = Optional.empty(); 16 | } 17 | 18 | public Instruction(Opcode opcode, String operand) { 19 | this.opcode = opcode; 20 | this.stringOperand = Optional.of(operand); 21 | this.integerOperand = Optional.empty(); 22 | } 23 | 24 | public Instruction(Opcode opcode, int operand) { 25 | this.opcode = opcode; 26 | this.stringOperand = Optional.empty(); 27 | this.integerOperand = Optional.of(operand); 28 | } 29 | 30 | public Opcode getOpcode() { 31 | return opcode; 32 | } 33 | 34 | public Optional getStringOperand() { 35 | return stringOperand; 36 | } 37 | 38 | public Optional getIntegerOperand() { 39 | return integerOperand; 40 | } 41 | 42 | @Override 43 | public boolean equals(Object o) { 44 | if (this == o) return true; 45 | if (o == null || getClass() != o.getClass()) return false; 46 | 47 | Instruction that = (Instruction) o; 48 | 49 | if (!integerOperand.equals(that.integerOperand)) return false; 50 | if (opcode != that.opcode) return false; 51 | if (!stringOperand.equals(that.stringOperand)) return false; 52 | 53 | return true; 54 | } 55 | 56 | @Override 57 | public int hashCode() { 58 | return Objects.hash(opcode, stringOperand, integerOperand); 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | if (stringOperand.isPresent()) 64 | return opcode + " " + stringOperand.get(); 65 | else if (integerOperand.isPresent()) 66 | return opcode + " " + integerOperand.get(); 67 | else 68 | return opcode.toString(); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/stackir/InstructionSequence.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.stackir; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | public final class InstructionSequence { 9 | 10 | private final List instructions = new ArrayList<>(); 11 | private int labelCounter = 0; 12 | 13 | public String createLineLabel(int line) { 14 | return "line_" + line; 15 | } 16 | 17 | public String createGeneratedLabel() { 18 | return "generated_" + (labelCounter++); 19 | } 20 | 21 | public void append(Instruction... instructions) { 22 | this.instructions.addAll(Arrays.asList(instructions)); 23 | } 24 | 25 | public List getInstructions() { 26 | return Collections.unmodifiableList(instructions); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/stackir/Opcode.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.stackir; 2 | 3 | public enum Opcode { 4 | PUSHS, 5 | PUSHI, 6 | LOAD, 7 | STORE, 8 | ADD, 9 | MUL, 10 | SUB, 11 | DIV, 12 | CALL, 13 | RET, 14 | JMP, 15 | JMPGT, 16 | JMPGTE, 17 | JMPLT, 18 | JMPLTE, 19 | JMPNE, 20 | JMPEQ, 21 | HLT, 22 | IN, 23 | OUTS, 24 | OUTI, 25 | LABEL 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/tokenizer/Token.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.tokenizer; 2 | 3 | import java.util.Objects; 4 | import java.util.Optional; 5 | 6 | public final class Token { 7 | 8 | public enum Type { 9 | EOF, 10 | LF, 11 | VAR, 12 | KEYWORD, 13 | NUMBER, 14 | STRING, 15 | PLUS, 16 | MINUS, 17 | MULT, 18 | DIV, 19 | LPAREN, 20 | RPAREN, 21 | EQ, 22 | NE, 23 | GT, 24 | GTE, 25 | LT, 26 | LTE, 27 | COMMA 28 | } 29 | 30 | private final Type type; 31 | private final Optional value; 32 | 33 | public Token(Type type) { 34 | this.type = type; 35 | this.value = Optional.empty(); 36 | } 37 | 38 | public Token(Type type, String value) { 39 | this.type = type; 40 | this.value = Optional.of(value); 41 | } 42 | 43 | public Type getType() { 44 | return type; 45 | } 46 | 47 | public Optional getValue() { 48 | return value; 49 | } 50 | 51 | @Override 52 | public boolean equals(Object o) { 53 | if (this == o) return true; 54 | if (o == null || getClass() != o.getClass()) return false; 55 | 56 | Token token = (Token) o; 57 | 58 | if (type != token.type) return false; 59 | if (!value.equals(token.value)) return false; 60 | 61 | return true; 62 | } 63 | 64 | @Override 65 | public int hashCode() { 66 | return Objects.hash(type, value); 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | return Token.class.getSimpleName() + " [type=" + type + ", value=" + value + "]"; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/grahamedgecombe/tinybasic/tokenizer/Tokenizer.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.tokenizer; 2 | 3 | import com.grahamedgecombe.tinybasic.tokenizer.Token.Type; 4 | 5 | import java.io.Closeable; 6 | import java.io.IOException; 7 | import java.io.Reader; 8 | import java.io.StringReader; 9 | 10 | public final class Tokenizer implements Closeable { 11 | 12 | private static boolean isWhitespace(int ch) { 13 | return Character.isWhitespace(ch); 14 | } 15 | 16 | private static boolean isDigit(int ch) { 17 | return ch >= '0' && ch <= '9'; 18 | } 19 | 20 | private static boolean isAlpha(int ch) { 21 | return ch >= 'A' && ch <= 'Z'; 22 | } 23 | 24 | private final Reader reader; 25 | 26 | public Tokenizer(String str) { 27 | this.reader = new StringReader(str); 28 | } 29 | 30 | public Tokenizer(Reader reader) { 31 | this.reader = reader; 32 | } 33 | 34 | private int peek() throws IOException { 35 | reader.mark(1); 36 | try { 37 | return reader.read(); 38 | } finally { 39 | reader.reset(); 40 | } 41 | } 42 | 43 | public Token nextToken() throws IOException { 44 | for (;;) { 45 | int ch = reader.read(); 46 | if (ch == -1) 47 | return new Token(Type.EOF); 48 | else if (ch == '\n') 49 | return new Token(Type.LF); 50 | else if (ch == '+') 51 | return new Token(Type.PLUS); 52 | else if (ch == '-') 53 | return new Token(Type.MINUS); 54 | else if (ch == '*') 55 | return new Token(Type.MULT); 56 | else if (ch == '/') 57 | return new Token(Type.DIV); 58 | else if (ch == '(') 59 | return new Token(Type.LPAREN); 60 | else if (ch == ')') 61 | return new Token(Type.RPAREN); 62 | else if (ch == ',') 63 | return new Token(Type.COMMA); 64 | else if (ch == '"') 65 | return nextStringToken(); 66 | else if (ch == '=') 67 | return new Token(Type.EQ); 68 | else if (ch == '>' || ch == '<') 69 | return nextRelationalOperatorToken(ch); 70 | else if (isAlpha(ch) && !isAlpha(peek())) 71 | return new Token(Type.VAR, new String(new char[] { (char) ch })); 72 | else if (isAlpha(ch)) 73 | return nextKeywordToken(ch); 74 | else if (isDigit(ch)) 75 | return nextNumberToken(ch); 76 | else if (!isWhitespace(ch)) 77 | throw new IOException("Unexpected character: " + ch); 78 | } 79 | } 80 | 81 | private Token nextRelationalOperatorToken(int first) throws IOException { 82 | int second = peek(); 83 | 84 | if (first == '>') { 85 | if (second == '<') { 86 | reader.skip(1); 87 | return new Token(Type.NE); 88 | } else if (second == '=') { 89 | reader.skip(1); 90 | return new Token(Type.GTE); 91 | } else { 92 | return new Token(Type.GT); 93 | } 94 | } else { 95 | assert first == '<'; 96 | 97 | if (second == '>') { 98 | reader.skip(1); 99 | return new Token(Type.NE); 100 | } else if (second == '=') { 101 | reader.skip(1); 102 | return new Token(Type.LTE); 103 | } else { 104 | return new Token(Type.LT); 105 | } 106 | } 107 | } 108 | 109 | private Token nextStringToken() throws IOException { 110 | StringBuilder buf = new StringBuilder(); 111 | for (;;) { 112 | int ch = reader.read(); 113 | if (ch == -1) 114 | throw new IOException("Unexpected EOF within string"); 115 | else if (ch == '"') 116 | break; 117 | 118 | buf.append((char) ch); 119 | } 120 | return new Token(Type.STRING, buf.toString()); 121 | } 122 | 123 | private Token nextKeywordToken(int first) throws IOException { 124 | StringBuilder buf = new StringBuilder(); 125 | buf.append((char) first); 126 | for (;;) { 127 | int ch = peek(); 128 | if (!isAlpha(ch)) 129 | break; 130 | 131 | reader.skip(1); 132 | buf.append((char) ch); 133 | } 134 | return new Token(Type.KEYWORD, buf.toString()); 135 | } 136 | 137 | private Token nextNumberToken(int first) throws IOException { 138 | StringBuilder buf = new StringBuilder(); 139 | buf.append((char) first); 140 | for (;;) { 141 | int ch = peek(); 142 | if (!isDigit(ch)) 143 | break; 144 | 145 | reader.skip(1); 146 | buf.append((char) ch); 147 | } 148 | return new Token(Type.NUMBER, buf.toString()); 149 | } 150 | 151 | @Override 152 | public void close() throws IOException { 153 | reader.close(); 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/com/grahamedgecombe/tinybasic/ast/StackIRTest.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.ast; 2 | 3 | import com.grahamedgecombe.tinybasic.stackir.Instruction; 4 | import com.grahamedgecombe.tinybasic.stackir.InstructionSequence; 5 | import com.grahamedgecombe.tinybasic.stackir.Opcode; 6 | import org.junit.Test; 7 | 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | public final class StackIRTest { 14 | 15 | @Test 16 | public void testSimpleArithmetic() { 17 | Expression expr = new BinaryExpression( 18 | BinaryOperator.PLUS, 19 | new ImmediateExpression(3), 20 | new ImmediateExpression(4)); 21 | 22 | InstructionSequence seq = new InstructionSequence(); 23 | expr.compile(seq); 24 | 25 | List expected = Arrays.asList( 26 | new Instruction(Opcode.PUSHI, 3), 27 | new Instruction(Opcode.PUSHI, 4), 28 | new Instruction(Opcode.ADD) 29 | ); 30 | 31 | assertEquals(expected, seq.getInstructions()); 32 | } 33 | 34 | @Test 35 | public void testNestedArithmetic() { 36 | Expression expr = new BinaryExpression( 37 | BinaryOperator.MULT, 38 | new BinaryExpression( 39 | BinaryOperator.PLUS, 40 | new ImmediateExpression(3), 41 | new ImmediateExpression(4) 42 | ), 43 | new ImmediateExpression(5) 44 | ); 45 | 46 | InstructionSequence seq = new InstructionSequence(); 47 | expr.compile(seq); 48 | 49 | List expected = Arrays.asList( 50 | new Instruction(Opcode.PUSHI, 3), 51 | new Instruction(Opcode.PUSHI, 4), 52 | new Instruction(Opcode.ADD), 53 | new Instruction(Opcode.PUSHI, 5), 54 | new Instruction(Opcode.MUL) 55 | ); 56 | 57 | assertEquals(expected, seq.getInstructions()); 58 | } 59 | 60 | @Test 61 | public void testNegation() { 62 | Expression expr = new UnaryExpression(UnaryOperator.MINUS, new ImmediateExpression(42)); 63 | 64 | InstructionSequence seq = new InstructionSequence(); 65 | expr.compile(seq); 66 | 67 | List expected = Arrays.asList( 68 | new Instruction(Opcode.PUSHI, 0), 69 | new Instruction(Opcode.PUSHI, 42), 70 | new Instruction(Opcode.SUB) 71 | ); 72 | 73 | assertEquals(expected, seq.getInstructions()); 74 | } 75 | 76 | @Test 77 | public void testVar() { 78 | Expression expr = new VariableExpression("X"); 79 | 80 | InstructionSequence seq = new InstructionSequence(); 81 | expr.compile(seq); 82 | 83 | List expected = Arrays.asList(new Instruction(Opcode.LOAD, "X")); 84 | 85 | assertEquals(expected, seq.getInstructions()); 86 | } 87 | 88 | @Test 89 | public void testEndStatement() { 90 | Statement stmt = new EndStatement(); 91 | 92 | InstructionSequence seq = new InstructionSequence(); 93 | stmt.compile(seq); 94 | 95 | List expected = Arrays.asList(new Instruction(Opcode.HLT)); 96 | 97 | assertEquals(expected, seq.getInstructions()); 98 | } 99 | 100 | @Test 101 | public void testReturnStatement() { 102 | Statement stmt = new ReturnStatement(); 103 | 104 | InstructionSequence seq = new InstructionSequence(); 105 | stmt.compile(seq); 106 | 107 | List expected = Arrays.asList(new Instruction(Opcode.RET)); 108 | 109 | assertEquals(expected, seq.getInstructions()); 110 | } 111 | 112 | @Test 113 | public void testLetStatement() { 114 | Statement stmt = new LetStatement("X", new ImmediateExpression(42)); 115 | 116 | InstructionSequence seq = new InstructionSequence(); 117 | stmt.compile(seq); 118 | 119 | List expected = Arrays.asList( 120 | new Instruction(Opcode.PUSHI, 42), 121 | new Instruction(Opcode.STORE, "X") 122 | ); 123 | 124 | assertEquals(expected, seq.getInstructions()); 125 | } 126 | 127 | @Test 128 | public void testBranchStatement() { 129 | Statement stmt = new BranchStatement(BranchType.GOTO, 10); 130 | 131 | InstructionSequence seq = new InstructionSequence(); 132 | stmt.compile(seq); 133 | 134 | List expected = Arrays.asList(new Instruction(Opcode.JMP, "line_10")); 135 | 136 | assertEquals(expected, seq.getInstructions()); 137 | } 138 | 139 | @Test 140 | public void testIfStatement() { 141 | Statement stmt = new IfStatement( 142 | RelationalOperator.EQ, 143 | new ImmediateExpression(13), 144 | new ImmediateExpression(37), 145 | new EndStatement() 146 | ); 147 | 148 | InstructionSequence seq = new InstructionSequence(); 149 | stmt.compile(seq); 150 | 151 | List expected = Arrays.asList( 152 | new Instruction(Opcode.PUSHI, 13), 153 | new Instruction(Opcode.PUSHI, 37), 154 | new Instruction(Opcode.JMPEQ, "generated_0"), 155 | new Instruction(Opcode.JMP, "generated_1"), 156 | new Instruction(Opcode.LABEL, "generated_0"), 157 | new Instruction(Opcode.HLT), 158 | new Instruction(Opcode.LABEL, "generated_1") 159 | ); 160 | 161 | assertEquals(expected, seq.getInstructions()); 162 | } 163 | 164 | @Test 165 | public void testInputStatement() { 166 | Statement stmt = new InputStatement("A", "B", "C"); 167 | 168 | InstructionSequence seq = new InstructionSequence(); 169 | stmt.compile(seq); 170 | 171 | List expected = Arrays.asList( 172 | new Instruction(Opcode.IN), 173 | new Instruction(Opcode.STORE, "A"), 174 | new Instruction(Opcode.IN), 175 | new Instruction(Opcode.STORE, "B"), 176 | new Instruction(Opcode.IN), 177 | new Instruction(Opcode.STORE, "C") 178 | ); 179 | 180 | assertEquals(expected, seq.getInstructions()); 181 | } 182 | 183 | @Test 184 | public void testPrintStatement() { 185 | Statement stmt = new PrintStatement(new ImmediateExpression(123), new ImmediateString("abc")); 186 | 187 | InstructionSequence seq = new InstructionSequence(); 188 | stmt.compile(seq); 189 | 190 | List expected = Arrays.asList( 191 | new Instruction(Opcode.PUSHI, 123), 192 | new Instruction(Opcode.OUTI), 193 | new Instruction(Opcode.PUSHS, "abc"), 194 | new Instruction(Opcode.OUTS), 195 | new Instruction(Opcode.PUSHS, "\n"), 196 | new Instruction(Opcode.OUTS) 197 | ); 198 | 199 | assertEquals(expected, seq.getInstructions()); 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/test/java/com/grahamedgecombe/tinybasic/parser/ParserTest.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.parser; 2 | 3 | import com.grahamedgecombe.tinybasic.ast.*; 4 | import com.grahamedgecombe.tinybasic.tokenizer.Tokenizer; 5 | import org.junit.Test; 6 | 7 | import java.io.IOException; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | public final class ParserTest { 12 | 13 | @Test 14 | public void testUnaryOperator() throws IOException { 15 | try (Tokenizer tokenizer = new Tokenizer("-7")) { 16 | try (Parser parser = new Parser(tokenizer)) { 17 | Expression expr = parser.nextExpression(); 18 | assertEquals(new UnaryExpression(UnaryOperator.MINUS, new ImmediateExpression(7)), expr); 19 | } 20 | } 21 | } 22 | 23 | @Test 24 | public void testBinaryOperator() throws IOException { 25 | try (Tokenizer tokenizer = new Tokenizer("3 + 4")) { 26 | try (Parser parser = new Parser(tokenizer)) { 27 | Expression expr = parser.nextExpression(); 28 | assertEquals(new BinaryExpression( 29 | BinaryOperator.PLUS, 30 | new ImmediateExpression(3), 31 | new ImmediateExpression(4) 32 | ), expr); 33 | } 34 | } 35 | } 36 | 37 | @Test 38 | public void testAssociativity() throws IOException { 39 | try (Tokenizer tokenizer = new Tokenizer("3 + 4 + 5")) { 40 | try (Parser parser = new Parser(tokenizer)) { 41 | Expression expr = parser.nextExpression(); 42 | assertEquals(new BinaryExpression( 43 | BinaryOperator.PLUS, 44 | new BinaryExpression( 45 | BinaryOperator.PLUS, 46 | new ImmediateExpression(3), 47 | new ImmediateExpression(4) 48 | ), 49 | new ImmediateExpression(5) 50 | ), expr); 51 | } 52 | } 53 | } 54 | 55 | @Test 56 | public void testPrecedence() throws IOException { 57 | try (Tokenizer tokenizer = new Tokenizer("3 + 4 * 5")) { 58 | try (Parser parser = new Parser(tokenizer)) { 59 | Expression expr = parser.nextExpression(); 60 | assertEquals(new BinaryExpression( 61 | BinaryOperator.PLUS, 62 | new ImmediateExpression(3), 63 | new BinaryExpression( 64 | BinaryOperator.MULT, 65 | new ImmediateExpression(4), 66 | new ImmediateExpression(5) 67 | ) 68 | ), expr); 69 | } 70 | } 71 | } 72 | 73 | @Test 74 | public void testParentheses() throws IOException { 75 | try (Tokenizer tokenizer = new Tokenizer("(3 + 4) * 5")) { 76 | try (Parser parser = new Parser(tokenizer)) { 77 | Expression expr = parser.nextExpression(); 78 | assertEquals(new BinaryExpression( 79 | BinaryOperator.MULT, 80 | new BinaryExpression( 81 | BinaryOperator.PLUS, 82 | new ImmediateExpression(3), 83 | new ImmediateExpression(4) 84 | ), 85 | new ImmediateExpression(5) 86 | ), expr); 87 | } 88 | } 89 | } 90 | 91 | @Test 92 | public void testEndStatement() throws IOException { 93 | try (Tokenizer tokenizer = new Tokenizer("END")) { 94 | try (Parser parser = new Parser(tokenizer)) { 95 | Statement stmt = parser.nextStatement(); 96 | assertEquals(new EndStatement(), stmt); 97 | } 98 | } 99 | } 100 | 101 | @Test 102 | public void testReturnStatement() throws IOException { 103 | try (Tokenizer tokenizer = new Tokenizer("RETURN")) { 104 | try (Parser parser = new Parser(tokenizer)) { 105 | Statement stmt = parser.nextStatement(); 106 | assertEquals(new ReturnStatement(), stmt); 107 | } 108 | } 109 | } 110 | 111 | @Test 112 | public void testLetStatement() throws IOException { 113 | try (Tokenizer tokenizer = new Tokenizer("LET X = 3")) { 114 | try (Parser parser = new Parser(tokenizer)) { 115 | Statement stmt = parser.nextStatement(); 116 | assertEquals(new LetStatement("X", new ImmediateExpression(3)), stmt); 117 | } 118 | } 119 | } 120 | 121 | @Test 122 | public void testInputStatement() throws IOException { 123 | try (Tokenizer tokenizer = new Tokenizer("INPUT A, B, C")) { 124 | try (Parser parser = new Parser(tokenizer)) { 125 | Statement stmt = parser.nextStatement(); 126 | assertEquals(new InputStatement("A", "B", "C"), stmt); 127 | } 128 | } 129 | } 130 | 131 | @Test 132 | public void testBranchStatement() throws IOException { 133 | try (Tokenizer tokenizer = new Tokenizer("GOTO 400")) { 134 | try (Parser parser = new Parser(tokenizer)) { 135 | Statement stmt = parser.nextStatement(); 136 | assertEquals(new BranchStatement(BranchType.GOTO, 400), stmt); 137 | } 138 | } 139 | } 140 | 141 | @Test 142 | public void testIfStatement() throws IOException { 143 | try (Tokenizer tokenizer = new Tokenizer("IF X = Y THEN END")) { 144 | try (Parser parser = new Parser(tokenizer)) { 145 | Statement stmt = parser.nextStatement(); 146 | assertEquals(new IfStatement( 147 | RelationalOperator.EQ, 148 | new VariableExpression("X"), 149 | new VariableExpression("Y"), 150 | new EndStatement() 151 | ), stmt); 152 | } 153 | } 154 | } 155 | 156 | @Test 157 | public void testPrintStatement() throws IOException { 158 | try (Tokenizer tokenizer = new Tokenizer("PRINT \"The meaning of life is \", 42, \".\"")) { 159 | try (Parser parser = new Parser(tokenizer)) { 160 | Statement stmt = parser.nextStatement(); 161 | assertEquals(new PrintStatement( 162 | new ImmediateString("The meaning of life is "), 163 | new ImmediateExpression(42), 164 | new ImmediateString(".") 165 | ), stmt); 166 | } 167 | } 168 | } 169 | 170 | @Test 171 | public void testLine() throws IOException { 172 | try (Tokenizer tokenizer = new Tokenizer("10 PRINT \"Hello, world!\"")) { 173 | try (Parser parser = new Parser(tokenizer)) { 174 | Line line = parser.nextLine(); 175 | assertEquals(new Line(10, new PrintStatement(new ImmediateString("Hello, world!"))), line); 176 | } 177 | } 178 | } 179 | 180 | @Test 181 | public void testProgram() throws IOException { 182 | try (Tokenizer tokenizer = new Tokenizer("10 LET X = X + 1\n20 PRINT X\n30 GOTO 10")) { 183 | try (Parser parser = new Parser(tokenizer)) { 184 | Program program = parser.parse(); 185 | Program expected = new Program( 186 | new Line(10, new LetStatement("X", new BinaryExpression( 187 | BinaryOperator.PLUS, 188 | new VariableExpression("X"), 189 | new ImmediateExpression(1) 190 | ))), 191 | new Line(20, new PrintStatement(new VariableExpression("X"))), 192 | new Line(30, new BranchStatement(BranchType.GOTO, 10))); 193 | assertEquals(expected, program); 194 | } 195 | } 196 | } 197 | 198 | } 199 | -------------------------------------------------------------------------------- /src/test/java/com/grahamedgecombe/tinybasic/tokenizer/TokenizerTest.java: -------------------------------------------------------------------------------- 1 | package com.grahamedgecombe.tinybasic.tokenizer; 2 | 3 | import com.grahamedgecombe.tinybasic.tokenizer.Token.Type; 4 | import org.junit.Test; 5 | 6 | import java.io.IOException; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | 10 | public final class TokenizerTest { 11 | 12 | @Test 13 | public void testEof() throws IOException { 14 | try (Tokenizer tokenizer = new Tokenizer("")) { 15 | assertEquals(new Token(Type.EOF), tokenizer.nextToken()); 16 | } 17 | } 18 | 19 | @Test 20 | public void testNewLine() throws IOException { 21 | try (Tokenizer tokenizer = new Tokenizer("\n")) { 22 | assertEquals(new Token(Type.LF), tokenizer.nextToken()); 23 | } 24 | } 25 | 26 | @Test 27 | public void testString() throws IOException { 28 | try (Tokenizer tokenizer = new Tokenizer("\"hello\"")) { 29 | assertEquals(new Token(Type.STRING, "hello"), tokenizer.nextToken()); 30 | } 31 | } 32 | 33 | @Test 34 | public void testNumber() throws IOException { 35 | try (Tokenizer tokenizer = new Tokenizer("123")) { 36 | assertEquals(new Token(Type.NUMBER, "123"), tokenizer.nextToken()); 37 | } 38 | } 39 | 40 | @Test 41 | public void testRelationalOperators() throws IOException { 42 | try (Tokenizer tokenizer = new Tokenizer("= <= < <> >< > >=")) { 43 | assertEquals(new Token(Type.EQ), tokenizer.nextToken()); 44 | assertEquals(new Token(Type.LTE), tokenizer.nextToken()); 45 | assertEquals(new Token(Type.LT), tokenizer.nextToken()); 46 | assertEquals(new Token(Type.NE), tokenizer.nextToken()); 47 | assertEquals(new Token(Type.NE), tokenizer.nextToken()); 48 | assertEquals(new Token(Type.GT), tokenizer.nextToken()); 49 | assertEquals(new Token(Type.GTE), tokenizer.nextToken()); 50 | } 51 | } 52 | 53 | @Test 54 | public void testVariable() throws IOException { 55 | try (Tokenizer tokenizer = new Tokenizer("G")) { 56 | assertEquals(new Token(Type.VAR, "G"), tokenizer.nextToken()); 57 | } 58 | } 59 | 60 | @Test 61 | public void testKeyword() throws IOException { 62 | try (Tokenizer tokenizer = new Tokenizer("GOTO")) { 63 | assertEquals(new Token(Type.KEYWORD, "GOTO"), tokenizer.nextToken()); 64 | } 65 | } 66 | 67 | @Test 68 | public void testArithmeticOperators() throws IOException { 69 | try (Tokenizer tokenizer = new Tokenizer("+ - * /")) { 70 | assertEquals(new Token(Type.PLUS), tokenizer.nextToken()); 71 | assertEquals(new Token(Type.MINUS), tokenizer.nextToken()); 72 | assertEquals(new Token(Type.MULT), tokenizer.nextToken()); 73 | assertEquals(new Token(Type.DIV), tokenizer.nextToken()); 74 | } 75 | } 76 | 77 | @Test 78 | public void testMisc() throws IOException { 79 | try (Tokenizer tokenizer = new Tokenizer("( ) ,")) { 80 | assertEquals(new Token(Type.LPAREN), tokenizer.nextToken()); 81 | assertEquals(new Token(Type.RPAREN), tokenizer.nextToken()); 82 | assertEquals(new Token(Type.COMMA), tokenizer.nextToken()); 83 | } 84 | } 85 | 86 | @Test 87 | public void testWithoutWhitespace() throws IOException { 88 | try (Tokenizer tokenizer = new Tokenizer("PRINT13+37")) { 89 | assertEquals(new Token(Type.KEYWORD, "PRINT"), tokenizer.nextToken()); 90 | assertEquals(new Token(Type.NUMBER, "13"), tokenizer.nextToken()); 91 | assertEquals(new Token(Type.PLUS), tokenizer.nextToken()); 92 | assertEquals(new Token(Type.NUMBER, "37"), tokenizer.nextToken()); 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /tinybasic: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | java -cp target/classes com.grahamedgecombe.tinybasic.TinyBasicCompiler $1 ${1/%.*/.asm} 3 | nasm -felf64 -o ${1/%.*/.o} ${1/%.*/.asm} 4 | gcc -lc -o ${1/%.*/} ${1/%.*/.o} 5 | --------------------------------------------------------------------------------