diff --git a/.gitignore b/.gitignore index 83c5293..4796ce3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ bin/ -src/pp/iloc +src/pp/iloc/sample/ diff --git a/src/pp/iloc/Assembler.java b/src/pp/iloc/Assembler.java new file mode 100644 index 0000000..caec876 --- /dev/null +++ b/src/pp/iloc/Assembler.java @@ -0,0 +1,398 @@ +package pp.iloc; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.Lexer; +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.TokenStream; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.ParseTreeProperty; +import org.antlr.v4.runtime.tree.ParseTreeWalker; +import org.antlr.v4.runtime.tree.TerminalNode; + +import pp.iloc.model.Instr; +import pp.iloc.model.Label; +import pp.iloc.model.Num; +import pp.iloc.model.Op; +import pp.iloc.model.OpCode; +import pp.iloc.model.OpList; +import pp.iloc.model.Operand; +import pp.iloc.model.Operand.Type; +import pp.iloc.model.Program; +import pp.iloc.model.Reg; +import pp.iloc.model.Str; +import pp.iloc.parse.ErrorListener; +import pp.iloc.parse.FormatException; +import pp.iloc.parse.ILOCBaseListener; +import pp.iloc.parse.ILOCLexer; +import pp.iloc.parse.ILOCParser; +import pp.iloc.parse.ILOCParser.CommentContext; +import pp.iloc.parse.ILOCParser.DeclContext; +import pp.iloc.parse.ILOCParser.InstrContext; +import pp.iloc.parse.ILOCParser.InstrListContext; +import pp.iloc.parse.ILOCParser.OpCodeContext; +import pp.iloc.parse.ILOCParser.OperandContext; +import pp.iloc.parse.ILOCParser.ProgramContext; +import pp.iloc.parse.ILOCParser.RealOpContext; +import pp.iloc.parse.ILOCParser.SingleInstrContext; +import pp.iloc.parse.ILOCParser.SourcesContext; +import pp.iloc.parse.ILOCParser.TargetsContext; + +/** Assembler for the ILOC language. */ +public class Assembler { + private final ILOCWalker walker; + + /** Constructor for the singleton instance. */ + private Assembler() { + this.walker = new ILOCWalker(); + } + + /** Parses a given ILOC program given as a string, + * and returns the parsed program. + * @throws FormatException if there was an error parsing the program + */ + public Program assemble(String program) throws FormatException { + return assemble(CharStreams.fromString(program)); + } + + /** Parses a given ILOC program given as a file, + * and returns the parsed program. + * @throws FormatException if there was an error parsing the program + */ + public Program assemble(File file) throws FormatException, IOException { + return assemble(CharStreams.fromPath(file.toPath())); + } + + /** Parses a given ILOC program given as a character stream, + * and returns the parsed program. + * @throws FormatException if there was an error parsing the program + */ + public Program assemble(CharStream chars) throws FormatException { + ErrorListener listener = new ErrorListener(); + Lexer lexer = new ILOCLexer(chars); + lexer.removeErrorListeners(); + lexer.addErrorListener(listener); + TokenStream tokens = new CommonTokenStream(lexer); + ILOCParser parser = new ILOCParser(tokens); + parser.removeErrorListeners(); + parser.addErrorListener(listener); + ParseTree tree = parser.program(); + if (listener.hasErrors()) { + throw new FormatException(listener.getErrors()); + } + return assemble(tree); + } + + /** Parses a given ILOC program given as a parse tree, + * and returns the parsed program. + * @throws FormatException if there was an error parsing the program + */ + public Program assemble(ParseTree tree) throws FormatException { + Program result = this.walker.walk(tree); + result.check(); + return result; + } + + /** Returns the singleton assembler instance. */ + public static final Assembler instance() { + return INSTANCE; + } + + private static final Assembler INSTANCE = new Assembler(); + + private static class ILOCWalker extends ILOCBaseListener { + /** The program to be built. */ + private Program program; + /** Instructions associated with @{code instr} parse nodes. */ + private ParseTreeProperty instrs; + /** Operations associated with @{code op} parse nodes. */ + private ParseTreeProperty ops; + /** Operands associated with @{code operand} parse nodes. */ + private ParseTreeProperty operands; + /** Operand lists associated with @{code sources} parse nodes. */ + private ParseTreeProperty> sources; + /** Operand lists associated with targets parse nodes. */ + private ParseTreeProperty> targets; + /** Mapping of labels to place of declaration. */ + private Map labelMap; + /** Mapping of symbolic constants to place of initialisation. */ + private Map symbolMap; + /** The error listener of this walker. */ + private ErrorListener errors; + + public Program walk(ParseTree tree) throws FormatException { + // initialise the data structures + this.program = new Program(); + this.instrs = new ParseTreeProperty<>(); + this.ops = new ParseTreeProperty<>(); + this.operands = new ParseTreeProperty<>(); + this.sources = new ParseTreeProperty<>(); + this.targets = new ParseTreeProperty<>(); + this.labelMap = new HashMap<>(); + this.symbolMap = new HashMap<>(); + this.errors = new ErrorListener(); + new ParseTreeWalker().walk(this, tree); + if (this.errors.hasErrors()) { + throw new FormatException(this.errors.getErrors()); + } + return this.program; + } + + @Override + public void exitDecl(DeclContext ctx) { + Num symbol = new Num(ctx.ID().getText()); + if (addSymbol(ctx.getStart(), symbol)) { + this.program.setSymb(symbol, + Integer.parseInt(ctx.NUM().getText())); + } + } + + @Override + public void exitInstrList(InstrListContext ctx) { + OpList result = new OpList(); + if (ctx.label() != null) { + Label label = new Label(ctx.label().getText()); + if (addLabel(ctx.getStart(), label)) { + result.setLabel(label); + } + } + for (ParseTree op : ctx.op()) { + Op instr = getOp(op); + // op may be null if there was a format error + if (instr != null) { + result.addOp(instr); + } + } + addInstr(ctx, result); + } + + @Override + public void exitSingleInstr(SingleInstrContext ctx) { + Instr result = getOp(ctx.op()); + // op may be null if there was a format error + if (result != null) { + if (ctx.label() != null) { + Label label = new Label(ctx.label().getText()); + if (addLabel(ctx.getStart(), label)) { + result.setLabel(label); + } + } + addInstr(ctx, result); + } + } + + @Override + public void exitComment(CommentContext ctx) { + Op result = new Op(OpCode.comment); + String comment = ctx.COMMENT().getText(); + comment = comment.substring(2).trim(); + result.setComment(comment); + addOp(ctx, result); + } + + @Override + public void exitRealOp(RealOpContext ctx) { + OpCodeContext opCodeTree = ctx.opCode(); + OpCode code = OpCode.parse(opCodeTree.getText()); + if (code == null) { + this.errors.visitError( + opCodeTree.getStart(), + "Unrecognized opcode '%s'", opCodeTree.getText()); + return; + } + // collect operands + List opnds = new ArrayList<>(); + try { + List sources = getSources(ctx.sources()); + opnds.addAll(getOpnds(code, "source", code.getSourceSig(), + sources)); + List targets = getTargets(ctx.targets()); + opnds.addAll(getOpnds(code, "target", code.getTargetSig(), + targets)); + } catch (FormatException e) { + this.errors.visitError(opCodeTree.getStart(), e.getMessage()); + return; + } + // check for correct arrow symbol + if (!code.getTargetSig().isEmpty()) { + String expected = code.getClaz().getArrow(); + TerminalNode arrowToken = ctx.DARROW() == null ? ctx.ARROW() : ctx.DARROW(); + String actual = arrowToken.getText(); + if (!expected.equals(actual)) { + this.errors.visitError(arrowToken.getSymbol(), + "Expected '%s' but found '%s'", expected, actual); + } + } + Op result = new Op(code, opnds); + if (ctx.COMMENT() != null) { + String comment = ctx.COMMENT().getText(); + comment = comment.substring(2).trim(); + result.setComment(comment); + } + addOp(ctx, result); + } + + private List getOpnds(OpCode code, String role, + List sig, List orig) throws FormatException { + List result = new ArrayList<>(); + if (orig == null) { + orig = Collections.emptyList(); + } + if (orig.size() != sig.size()) { + throw new FormatException( + "Opcode '%s' expects %d %s parameters, but has %d", + code.name(), sig.size(), role, orig.size()); + } + for (int i = 0; i < orig.size(); i++) { + Operand opnd = orig.get(i); + Type actualType = opnd.getType(); + Type expectedType = sig.get(i); + if (expectedType == Type.REG && actualType == Type.LABEL) { + opnd = new Reg(((Label) opnd).getValue()); + } else if (expectedType != actualType) { + throw new FormatException( + "Opcode '%s' %s operand %d should be %s but is %s", + code.name(), role, i, expectedType.name(), + actualType.name()); + } + result.add(opnd); + } + return result; + } + + @Override + public void exitProgram(ProgramContext ctx) { + for (InstrContext instr : ctx.instr()) { + Instr i = getInstr(instr); + if (i != null) { + this.program.addInstr(i); + } + } + } + + @Override + public void exitSources(SourcesContext ctx) { + List result = new ArrayList<>(); + for (ParseTree o : ctx.operand()) { + result.add(getOperand(o)); + } + addSources(ctx, result); + } + + @Override + public void exitTargets(TargetsContext ctx) { + List result = new ArrayList<>(); + for (ParseTree o : ctx.operand()) { + result.add(getOperand(o)); + } + addTargets(ctx, result); + } + + @Override + public void exitOperand(OperandContext ctx) { + Operand result; + if (ctx.STR() != null) { + String str = ctx.STR().getText(); + result = new Str(str.substring(1, str.length() - 1).replaceAll( + "\\\"", "\"")); + } else if (ctx.NUM() != null) { + result = new Num(Integer.parseInt(ctx.NUM().getText())); + } else if (ctx.SYMB() != null) { + result = new Num(ctx.SYMB().getText().substring(1)); + } else if (ctx.LAB() != null) { + result = new Num( + new Label(ctx.LAB().getText().substring(1))); + } else { + result = new Label(ctx.ID().getText()); + } + addOperand(ctx, result); + } + + /** Sets the instruction attribute of a given node. */ + private void addInstr(ParseTree node, Instr instr) { + this.instrs.put(node, instr); + } + + /** Returns the instruction attribute of a given node. */ + private Instr getInstr(ParseTree node) { + return this.instrs.get(node); + } + + /** Sets the operation attribute of a given node. */ + private void addOp(ParseTree node, Op op) { + this.ops.put(node, op); + } + + /** Returns the operation attribute of a given node. */ + private Op getOp(ParseTree node) { + return this.ops.get(node); + } + + /** Sets the operand attribute of a given node. */ + private void addOperand(ParseTree node, Operand operand) { + this.operands.put(node, operand); + } + + /** Returns the operand attribute of a given node. */ + private Operand getOperand(ParseTree node) { + return this.operands.get(node); + } + + /** Sets the source operand list attribute of a given node. */ + private void addSources(ParseTree node, List sources) { + this.sources.put(node, sources); + } + + /** Returns the source operand list attribute of a given node. */ + private List getSources(ParseTree node) { + return this.sources.get(node); + } + + /** Sets the target operand list attribute of a given node. */ + private void addTargets(ParseTree node, List targets) { + this.targets.put(node, targets); + } + + /** Returns the target operand list attribute of a given node. */ + private List getTargets(ParseTree node) { + return this.targets.get(node); + } + + /** Registers label definition. Signals an error if the label + * was already defined. + * @return {@code true} if the label was new + */ + private boolean addLabel(Token token, Label label) { + Token oldToken = this.labelMap.put(label, token); + if (oldToken != null) { + this.errors.visitError(token, + "Label '%s' already defined at line %d", label, + oldToken.getLine()); + } + return oldToken == null; + } + + /** Registers a symbol initialisation. Signals an error if the + * symbol was already initialised. + * @return {@code true} if the symbol was new + */ + private boolean addSymbol(Token token, Num symbol) { + Token oldToken = this.symbolMap.put(symbol, token); + if (oldToken != null) { + this.errors.visitError(token, + "Symbolic constant '%s' already defined at line %d", + symbol, oldToken.getLine()); + } + return oldToken == null; + } + } +} diff --git a/src/pp/iloc/Simulator.java b/src/pp/iloc/Simulator.java new file mode 100644 index 0000000..74468a1 --- /dev/null +++ b/src/pp/iloc/Simulator.java @@ -0,0 +1,478 @@ +package pp.iloc; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.Scanner; + +import pp.iloc.eval.Machine; +import pp.iloc.model.Label; +import pp.iloc.model.Num; +import pp.iloc.model.Op; +import pp.iloc.model.OpClaz; +import pp.iloc.model.OpCode; +import pp.iloc.model.Program; +import pp.iloc.parse.FormatException; + +/** + * ILOC program simulator + * @author Arend Rensink + */ +public class Simulator { + /** Representation of true. */ + public static final int TRUE = -1; + /** Representation of false. */ + public static final int FALSE = 0; + /** Flag controlling debug mode. */ + public static boolean DEBUG = false; + + static public void main(String[] args) { + if (args.length == 0) { + System.err.println("Usage: filename.iloc"); + return; + } + try { + Program prog = Assembler.instance().assemble(new File(args[0])); + new Simulator(prog).run(); + } catch (FormatException | IOException exc) { + exc.printStackTrace(); + } + } + + /** The simulated program. */ + private final Program prg; + /** The virtual machine on which the program is run. */ + private final Machine vm; + /** Flag signifying that the input is from stdin. */ + private boolean stdIn; + /** The reader used for the in-operations. */ + private Scanner in; + /** The print writer used for the out-operations. */ + private PrintStream out; + + /** Constructs a simulator for a given program and VM. */ + public Simulator(Program program, Machine vm) { + assert program != null; + assert vm != null; + this.prg = program; + this.vm = vm; + this.out = System.out; + this.in = new Scanner(System.in); + this.stdIn = true; + } + + /** Constructs a simulator for a given program and a fresh VM. */ + public Simulator(Program program) { + this(program, new Machine()); + } + + /** Returns the program wrapped in this simulator. */ + public Program getProgram() { + return this.prg; + } + + /** Returns the Virtual Machine wrapped in this simulator. */ + public Machine getVM() { + return this.vm; + } + + /** Changes the input stream for the {@link OpCode#in}-operations. */ + public void setIn(InputStream in) { + this.in = new Scanner(in); + this.stdIn = false; + } + + /** Changes the output stream for the {@link OpCode#out}-operations. */ + public void setOut(OutputStream out) { + this.out = new PrintStream(out); + } + + /** Runs the program. */ + public void run() { + while (this.vm.getPC() < this.prg.size() && this.vm.getInterrupt() == 0) { + step(); + } + } + + /** Executes one operation of the program. */ + public void step() { + Op o = this.prg.getOpAt(this.vm.getPC()); + OPContext c = new OPContext(o); + Machine vm = this.vm; + if (DEBUG) { + System.out.printf("Op %d: %s%n", vm.getPC(), o); + System.out.println(vm); + } + switch (o.getOpCode()) { + case nop: + // do nothing + break; + case add: + c.setReg(2, c.reg(0) + c.reg(1)); + break; + case sub: + c.setReg(2, c.reg(0) - c.reg(1)); + break; + case mult: + c.setReg(2, c.reg(0) * c.reg(1)); + break; + case div: + c.setReg(2, c.reg(0) / c.reg(1)); + break; + case addI: + c.setReg(2, c.reg(0) + c.num(1)); + break; + case subI: + c.setReg(2, c.reg(0) - c.num(1)); + break; + case rsubI: + c.setReg(2, c.num(1) - c.reg(0)); + break; + case multI: + c.setReg(2, c.reg(0) * c.num(1)); + break; + case divI: + c.setReg(2, c.reg(0) / c.num(1)); + break; + case rdivI: + c.setReg(2, c.num(1) / c.reg(0)); + break; + case lshift: + c.setReg(2, c.num(0) << c.reg(1)); + break; + case lshiftI: + c.setReg(2, c.num(0) << c.num(1)); + break; + case rshift: + c.setReg(2, c.num(0) >>> c.reg(1)); + break; + case rshiftI: + c.setReg(2, c.num(0) >>> c.num(1)); + break; + case and: + c.setReg(2, -1 * c.reg(0) * c.reg(1)); + break; + case andI: + c.setReg(2, -1 * c.reg(0) * c.num(1)); + break; + case or: + c.setReg(2, Math.max(-1, c.reg(0) + c.reg(1))); + break; + case orI: + c.setReg(2, Math.max(-1, c.reg(0) + c.num(1))); + break; + case xor: + c.setReg(2, Math.max(-1, c.reg(0) ^ c.reg(1))); + break; + case xorI: + c.setReg(2, Math.max(-1, c.reg(0) ^ c.num(1))); + break; + case load: + c.setReg(1, vm.load(c.reg(0))); + break; + case loadI: + c.setReg(1, c.num(0)); + break; + case loadAI: + c.setReg(2, vm.load(c.reg(0) + c.num(1))); + break; + case loadAO: + c.setReg(2, vm.load(c.reg(0) + c.reg(1))); + break; + case store: + vm.store(c.reg(0), c.reg(1)); + break; + case storeAI: + vm.store(c.reg(0), c.reg(1) + c.num(2)); + break; + case storeAO: + vm.store(c.reg(0), c.reg(1) + c.reg(2)); + break; + case cload: + c.setReg(1, vm.loadC(c.reg(0))); + break; + case cloadAI: + c.setReg(2, vm.loadC(c.reg(0) + c.num(1))); + break; + case cloadAO: + c.setReg(2, vm.loadC(c.reg(0) + c.reg(1))); + break; + case cstore: + vm.storeC(c.reg(0), c.reg(1)); + break; + case cstoreAI: + vm.storeC(c.reg(0), c.reg(1) + c.num(2)); + break; + case cstoreAO: + vm.storeC(c.reg(0), c.reg(1) + c.reg(2)); + break; + case i2i: + c.setReg(1, c.reg(0)); + break; + case i2c: + c.setReg(1, (byte) c.reg(0)); + break; + case c2i: + c.setReg(1, (byte) c.reg(0)); + break; + case c2c: + c.setReg(1, (byte) c.reg(0)); + break; + case cmp_LT: + c.setReg(2, c.reg(0) < c.reg(1)); + break; + case cmp_LE: + c.setReg(2, c.reg(0) <= c.reg(1)); + break; + case cmp_EQ: + c.setReg(2, c.reg(0) == c.reg(1)); + break; + case cmp_GE: + c.setReg(2, c.reg(0) >= c.reg(1)); + break; + case cmp_GT: + c.setReg(2, c.reg(0) > c.reg(1)); + break; + case cmp_NE: + c.setReg(2, c.reg(0) != c.reg(1)); + break; + case cbr: + if (c.reg(0) == 0) { + this.vm.setPC(c.label(2)); + } else { + this.vm.setPC(c.label(1)); + } + break; + case jumpI: + this.vm.setPC(c.label(0)); + break; + case jump: + this.vm.setPC(c.reg(0)); + break; + case tbl: + // do nothing + break; + case push: + push(c.reg(0)); + break; + case pop: + c.setReg(0, pop()); + break; + case cpush: + pushC(c.reg(0)); + break; + case cpop: + c.setReg(0, popC()); + break; + case in: + String message = o.str(0).getText(); + if (this.stdIn) { + this.out.print(message); + } + String in = this.in.nextLine(); + int val = Integer.MAX_VALUE; + do { + try { + val = Integer.parseInt(in); + } catch (NumberFormatException e) { + // try again + if (this.stdIn) { + this.out.printf("Input '%s' should be a number%n", in); + this.out.print(message); + in = this.in.nextLine(); + } else { + throw new IllegalArgumentException(String.format( + "Input '%s' should be a number%n", in)); + } + } + } while (val == Integer.MAX_VALUE); + c.setReg(1, val); + break; + case out: + this.out.print(o.str(0).getText()); + this.out.println(c.reg(1)); + break; + case cin: + message = o.str(0).getText(); + if (this.stdIn) { + this.out.print(message); + } + in = this.in.nextLine(); + pushString(in); + break; + case cout: + this.out.print(o.str(0).getText()); + this.out.println(popString()); + break; + case comment: + // do nothing + break; + case halt: + this.vm.setInterrupt(c.reg(0)); + System.err.printf( + "Program halted with code %d at line %d.%n", + c.num(0), o.getLine()); + break; + case haltI: + this.vm.setInterrupt(c.num(0)); + System.err.printf( + "Program halted with code %d at line %d.%n", + c.num(0), o.getLine()); + break; + default: + System.err.printf( + "Error at line %d: Operation %s not yet implemented%n", + o.getLine(), o.getOpCode()); + this.vm.incPC(); + return; + } + if (o.getClaz() != OpClaz.CONTROL) { + this.vm.incPC(); + } + } + + /** Pushes a 4-byte integer onto the stack. */ + private void push(int val) { + int sp = this.vm.getReg(Machine.SP) - 4; + + if (sp < this.vm.getReg(Machine.BRK)) + throw new RuntimeException("Stack overflow"); + + this.vm.setReg(Machine.SP, sp); + this.vm.store(val, sp); + } + + /** Pushes a character onto the stack. */ + private void pushC(int val) { + int sp = this.vm.getReg(Machine.SP) - this.vm.getCharSize(); + + if (sp < this.vm.getReg(Machine.BRK)) + throw new RuntimeException("Stack overflow"); + + this.vm.setReg(Machine.SP, sp); + this.vm.storeC(val, sp); + } + + /** Pushes a string onto the stack. + * The string is represented by an integer length followed + * by the chars of the string, with first char on top . + */ + private void pushString(String text) { + for (int i = text.length() - 1; i >= 0; i--) { + pushC(text.charAt(i)); + } + push(text.length()); + } + + /** Pops a 4-byte integer from the stack. */ + private int pop() { + int sp = this.vm.getReg(Machine.SP); + int result = this.vm.load(sp); + if (DEBUG) + this.vm.store(0, sp); + this.vm.setReg(Machine.SP, sp + 4); + return result; + } + + /** Pops a character from the stack. */ + private int popC() { + int sp = this.vm.getReg(Machine.SP); + int result = this.vm.loadC(sp); + if (DEBUG) + this.vm.storeC(0, sp); + this.vm.setReg(Machine.SP, sp + this.vm.getCharSize()); + return result; + } + + /** Pops a string from the stack. + * The string should be represented by an integer length followed + * by the chars of the string, with first char on top. + * This method coerces all values to (Java) chars, + * so if {@link Machine#setCharSize(int)} has been set to 4 then + * all more significant bytes are lost + */ + private String popString() { + StringBuilder result = new StringBuilder(); + int len = pop(); + for (int i = 0; i < len; i++) { + result.append((char) popC()); + } + return result.toString(); + } + + /** Operation context. + * This is a helper class for easy access to the underlying VM, + * given a particular operation. + */ + private class OPContext { + private final Op op; + + OPContext(Op o) { + this.op = o; + } + + /** Sets a register of the processor to a given boolean value. + * The boolean is converted to {@link #TRUE} of {@link #FALSE}. + * @param regIx operand index of the register to be set + * @param val the boolean value + */ + public void setReg(int regIx, boolean val) { + getVM().setReg(this.op.reg(regIx), val ? TRUE : FALSE); + } + + /** Sets a register of the VM to a given integer value. + * @param regIx operand index of the register to be set + * @param val the value + */ + public void setReg(int regIx, int val) { + getVM().setReg(this.op.reg(regIx), val); + } + + /** Returns the value of a register of the VM. + * @param regIx operand index of the register to be returned + */ + public int reg(int regIx) { + return getVM().getReg(this.op.reg(regIx)); + } + + /** Returns the value assigned to a numeric constant. + * @param numIx operand index of the num to be returned + */ + public int num(int numIx) { + Num num = this.op.num(numIx); + switch (num.getKind()) { + case LAB: + Label label = num.getLabel(); + int line = getProgram().getLine(label); + if (line < 0) { + System.err.println("Label '" + label + + "' does not occur in program"); + } + return line; + case LIT: + return num.getValue(); + case SYMB: + Integer result = getVM().getNum(num); + if (result == null) { + result = getProgram().getSymb(num); + if (result == null) { + System.err.println("Symbolic constant '" + + num.getName() + + "' not initialised in VM or program"); + } + } + return result; + default: + assert false; + return 0; + } + } + + /** Returns the instruction number associated with a given label. */ + public int label(int ix) { + return getProgram().getLine(this.op.label(ix)); + } + } +} diff --git a/src/pp/iloc/eval/Machine.java b/src/pp/iloc/eval/Machine.java new file mode 100644 index 0000000..f8177c8 --- /dev/null +++ b/src/pp/iloc/eval/Machine.java @@ -0,0 +1,327 @@ +package pp.iloc.eval; + +import java.util.HashMap; +import java.util.Map; + +import pp.iloc.model.Num; +import pp.iloc.model.Reg; + +/** + * Virtual machine for ILOC program evaluation. + * @author Arend Rensink + */ +public class Machine { + /** Size of an integer values (in bytes). */ + public static final int INT_SIZE = 4; + /** Default size of a char value (in bytes). + * The actual size used in a particular Machine instance + * can be set using {@link #setCharSize(int)} + */ + public static final int DEFAULT_CHAR_SIZE = 1; + /** Size of an integer values (in bits). */ + public static final int BYTE_SIZE = 8; + /** Name of the allocation pointer register. + * This is initialised to start at address 0. + */ + public static final String ARP = "r_arp"; + /** The allocation pointer register (see {@link #ARP}). */ + public static final Reg ARP_REG = new Reg(ARP); + /** Name of the stack pointer register. + * This is initialised to start at the top of memory. + */ + public static final String SP = "sp"; + /** The stack pointer register (see {@link #SP}). */ + public static final Reg SP_REG = new Reg(SP); + /** Name of the program break register. + * This is initialised to halfway across the memory. + */ + public static final String BRK = "brk"; + /** The program break register (see {@link #BRK}). */ + public static final Reg BRK_REG = new Reg(BRK); + + /** The actual size of a char value in this machine. + * Typically, either 1 (as per {@link #DEFAULT_CHAR_SIZE}) + * or 4 (the same as {@link #INT_SIZE}). + * This affects the implementation of {@link #loadC} and + * {@link #storeC}. + */ + private int charSize; + /** Mapping from register names to register numbers. */ + private final Map registers; + /** Mapping from symbolic constants to actual values. */ + private final Map symbMap; + /** Memory of the machine. */ + private final Memory memory; + /** Counter of reserved memory cells. */ + private int reserved; + /** Program counter. */ + private int pc; + /** Value of interrupt. */ + private int interrupt; + + /** Constructs a new, initially empty machine + * with default-sized memory. */ + public Machine() { + this.symbMap = new HashMap<>(); + this.memory = new Memory(); + this.registers = new HashMap<>(); + this.charSize = DEFAULT_CHAR_SIZE; + clear(); + } + + /** Reinitialises the machine memory to a certain size (in bytes). + * This also resets the stack pointer (to the top of the memory). + * @param size + */ + public void setSize(int size) { + this.memory.setSize(size); + setReg(SP_REG, size); + setReg(BRK_REG, size/2); + } + + /** Sets the size used to store a char value. + * @param charSize the number of bytes used to store a char + * value; between 1 and 4 (inclusive) + */ + public void setCharSize(int charSize) { + if (charSize < 1 || charSize > 4) { + throw new IllegalArgumentException("Illegal character size: " + + charSize); + } + this.charSize = charSize; + } + + /** Returns the size used to store a char value. + * This is equal to {@link #DEFAULT_CHAR_SIZE} unless + * changed by a call to {@link #setCharSize} */ + public int getCharSize() { + return this.charSize; + } + + /** Reserves a memory segment of given length. + * @param length (in bytes) of the segment to be reserved + * @return the base address of the allocated block + * @see #alloc(String, int) + */ + public int alloc(int length) { + int result = this.reserved; + for (int i = 0; i < length; i++) { + this.memory.set(result + i, (byte) 0); + } + this.reserved += length; + return result; + } + + /** Reserves a memory segment of a given length (in bytes), + * assigns the base address to a symbolic constant, + * and returns the base address. + * The reserved memory is guaranteed to be unshared and + * initialized to 0. + * @param cst the name for the start address of the allocated memory + * @param length length (in bytes) of the segment to be reserved + * @return the allocated start address + * @throws IllegalArgumentException if the symbolic name is known + */ + public int alloc(String cst, int length) { + if (this.symbMap.get(new Num(cst)) != null) { + throw new IllegalArgumentException("Duplicate symbolic name '" + + cst + "'"); + } + int result = alloc(length); + setNum(cst, result); + return result; + } + + /** Initializes a memory segment of a length sufficient to + * accommodate a given list of initial values, + * assigns the start address to a symbolic name, + * and returns the start address. + * The reserved memory is guaranteed to be unshared. + * @param cst the name for the start address of the allocated memory + * @param vals the initial values + * @return the allocated start address + * @throws IllegalArgumentException if the symbolic name is known + */ + public int init(String cst, int... vals) { + if (this.symbMap.get(new Num(cst)) != null) { + throw new IllegalArgumentException("Duplicate symbolic name '" + + cst + "'"); + } + int result = alloc(vals.length * INT_SIZE); + setNum(cst, result); + for (int i = 0; i < vals.length; i++) { + store(vals[i], result + INT_SIZE * i); + } + return result; + } + + /** Declares a register with a given name, and sets its value to 0. */ + public void declare(String reg) { + setReg(reg, 0); + } + + /** Sets the value of a register with a given name to a given number. */ + public void setReg(String reg, int val) { + this.registers.put(reg, val); + } + + /** Sets the value of a given register to a given number. */ + public void setReg(Reg reg, int val) { + this.registers.put(reg.getName(), val); + } + + /** Returns the current value in a register with a given name. + * @throws IllegalArgumentException if no such register exists */ + public int getReg(String name) { + Integer result = this.registers.get(name); + if (result == null) { + throw new IllegalArgumentException("Unknown register '" + name + + "'"); + } + return result; + } + + /** Returns the current value in a given register. */ + public int getReg(Reg reg) { + return getReg(reg.getName()); + } + + /** Sets the value of a given named symbolic constant. + * @param name symbolic name, without '@'-prefix + * @throws IllegalArgumentException if the name is already declared + */ + public void setNum(String name, int val) { + Num symbol = new Num(name); + Integer oldVal = this.symbMap.put(symbol, val); + if (oldVal != null) { + throw new IllegalArgumentException("Duplicate symbol '" + symbol + + "': values " + oldVal + " and " + val); + } + } + + /** Returns the value of a given symbolic numeric value. + * @return the corresponding value, or null if + * the symbol is not defined. + */ + public Integer getNum(Num symb) { + return this.symbMap.get(symb); + } + + /** Convenience method to returns the value of a given symbolic constant. + * @param name symbolic name without '@'-prefix + * @throws IllegalArgumentException if the name is not declared + * @see #getNum(Num) + */ + public Integer getNum(String name) { + return getNum(new Num(name)); + } + + /** Returns the integer value starting at a given memory location. + * The value is computed from the four successive bytes starting + * at that location (most significant first). + */ + public int load(int loc) { + return load(loc, INT_SIZE); + } + + /** Returns the char value at a given memory location. + * This consists of a number of successive bytes determined + * by #getCharSize(). + */ + public int loadC(int loc) { + return 0xFF & load(loc, getCharSize()); + } + + /** Returns the integer value starting at a given memory location, + * consisting of 1 through 4 consecutive bytes. + * The value is computed from the successive bytes starting + * at that location (most significant first). + */ + private int load(int loc, int size) { + int result = 0; + for (int i = 0; i < size; i++) { + result <<= BYTE_SIZE; + result += 0xFF & this.memory.get(loc + i); + } + return result; + } + + /** Stores an integer value in memory, starting at a given location. + * The value is stored at the four successive bytes starting + * at that location (most significant first). + */ + public void store(int val, int loc) { + store(val, loc, INT_SIZE); + } + + /** Stores a character in memory, + * at a given location. + * The number of bytes used is determined by {@link #getCharSize()}. + */ + public void storeC(int val, int loc) { + store((char) val, loc, getCharSize()); + } + + /** Stores an integer value in memory, starting at a given location. + * The value is stored at the 1-4 successive bytes starting + * at that location (most significant first). + */ + private void store(int val, int loc, int len) { + for (int i = len - 1; i >= 0; i--) { + this.memory.set(loc + i, (byte) val); + val >>= BYTE_SIZE; + } + } + + /** Returns the current program counter value. */ + public int getPC() { + return this.pc; + } + + /** Increases the current program counter value. */ + public void incPC() { + this.pc++; + } + + /** sets the program counter to a given line number. */ + public void setPC(int line) { + if (line < 0) { + throw new IllegalArgumentException("Trying to jump to line " + line); + } + this.pc = line; + } + + /** + * Sets the interrupt value of the machine. + * @param interruptCode the new interrupt value + */ + public void setInterrupt(int interruptCode) { + this.interrupt = interruptCode; + } + + /** + * Returns the current interrupt value. + * @return the interrupt value + */ + public int getInterrupt() { + return interrupt; + } + + /** Clears the registers, constants, memory, PC and interrupt. */ + public void clear() { + this.registers.clear(); + this.symbMap.clear(); + this.memory.clear(); + this.pc = 0; + this.interrupt = 0; + setReg(ARP_REG, 0); + setReg(SP_REG, this.memory.size()); + setReg(BRK_REG, this.memory.size()/2); + } + + @Override + public String toString() { + return String.format("Registers: %s%nConstants: %s%nMemory: %s%n", + this.registers, this.symbMap, this.memory); + } +} diff --git a/src/pp/iloc/eval/Memory.java b/src/pp/iloc/eval/Memory.java new file mode 100644 index 0000000..b92f4dd --- /dev/null +++ b/src/pp/iloc/eval/Memory.java @@ -0,0 +1,56 @@ +package pp.iloc.eval; + +import java.util.Arrays; + +/** Simulated memory. */ +public class Memory { + /** The default size of the memory, in number of bytes. */ + public final static int DEFAULT_SIZE = 10000; + /** The memory array. */ + private byte[] mem = new byte[DEFAULT_SIZE]; + + /** Reinitialises the memory to a certain size. */ + public void setSize(int size) { + this.mem = new byte[size]; + } + + /** Sets a location in memory to a given value. */ + public void set(int loc, byte value) { + this.mem[loc] = value; + } + + /** + * Returns the value at a given memory location. + * The value is 0 if the location was never accessed before. + */ + public byte get(int loc) { + return this.mem[loc]; + } + + /** Returns the size of the used memory. */ + public int size() { + return this.mem.length; + } + + /** Removes all values from the memory. */ + public void clear() { + Arrays.fill(this.mem, (byte) 0); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < size(); i++) { + if (get(i) == 0) { + continue; + } + if (result.length() > 0) { + result.append(", "); + } + result.append(i); + result.append(":"); + result.append(String.format("%02X", get(i) & 0xFF)); + } + return result.toString(); + } +} diff --git a/src/pp/iloc/model/Instr.java b/src/pp/iloc/model/Instr.java new file mode 100644 index 0000000..9c39d69 --- /dev/null +++ b/src/pp/iloc/model/Instr.java @@ -0,0 +1,122 @@ +package pp.iloc.model; + +import java.util.Iterator; + +/** + * ILOC instruction + * @author Arend Rensink + */ +public abstract class Instr implements Iterable { + /** The line number of this instruction. */ + private int line = -1; + /** The label of this instruction. */ + private Label label; + /** The program in which this instruction occurs. */ + private Program prog; + /** Returns the number of operations in this instruction. */ + public abstract int size(); + + /** Returns an iterator over the operations in this instruction. */ + @Override + public abstract Iterator iterator(); + + /** Indicates if the line number of this instruction has been set. */ + boolean hasLine() { + return getLine() >= 0; + } + + /** Returns the line number of this instruction. + * @return the line number; {@code -1} if the line number has not been set. + */ + public int getLine() { + return this.line; + } + + /** Sets the line number of this instruction. */ + void setLine(int line) { + assert this.line < 0 && line >= 0; + this.line = line; + } + + /** Indicates if this instruction has a (non-null) label. */ + public boolean hasLabel() { + return getLabel() != null; + } + + /** Returns the optional label of this instruction. */ + public Label getLabel() { + return this.label; + } + + /** Sets the optional label of this instruction. + */ + public void setLabel(Label label) { + if (label == null) { + throw new IllegalArgumentException("Label may not be null"); + } + if (!(this.label == null || this.label.equals(label))) { + throw new IllegalArgumentException("Conflicting labels '" + + this.label + "' and '" + label + "'"); + } + assert label != null && this.label == null || label.equals(this.label); + if (this.label == null) { + this.label = label; + if (this.prog != null) { + this.prog.registerLabel(this); + } + } + } + + /** Sets the program in which this instruction occurs. + */ + void setProgram(Program prog) { + assert this.prog == null & prog != null; + this.prog = prog; + } + + /** Returns the string representation of the optional label. */ + String toLabelString() { + if (hasLabel()) { + return getLabel() + LABEL_SEP; + } else { + return ""; + } + } + + /** Returns a string of the form + * {@code label? opCode sources (arrow targets)? comment?} + * where the widths of the label, sources and targets parts + * are predetermined. + * @param labelSize width of the {@code label} part + * @param sourceSize width of the {@code sources} part + * @param targetSize width of the {@code targets} part + */ + abstract public String prettyPrint(int labelSize, int sourceSize, + int targetSize); + + @Override + public int hashCode() { + return (this.label == null) ? 0 : this.label.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!(obj instanceof Instr)) { + return false; + } + Instr other = (Instr) obj; + if (this.label == null) { + if (other.label != null) { + return false; + } + } else if (!this.label.equals(other.label)) { + return false; + } + return true; + } + + /** Label separator. */ + private final static String LABEL_SEP = ": "; +} diff --git a/src/pp/iloc/model/Label.java b/src/pp/iloc/model/Label.java new file mode 100644 index 0000000..4002a95 --- /dev/null +++ b/src/pp/iloc/model/Label.java @@ -0,0 +1,66 @@ +package pp.iloc.model; + +/** Label operand. + * @author Arend Rensink + */ +public class Label extends Operand { + /** Constructs a label object with a given label text. */ + public Label(String value) { + super(Type.LABEL); + assert wellformed(value) : String.format( + "Label '%s' is not well-formed", value); + this.value = value; + } + + /** Returns the value of this label. */ + public String getValue() { + return this.value; + } + + private final String value; + + @Override + public String toString() { + return this.value; + } + + @Override + public int hashCode() { + return getValue().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Label)) { + return false; + } + Label other = (Label) obj; + if (!getValue().equals(other.getValue())) { + return false; + } + return true; + } + + /** Tests if a string value is a well-formed label. */ + private boolean wellformed(String value) { + if (value == null) { + return false; + } + if (value.isEmpty()) { + return false; + } + if (!Character.isLetter(value.charAt(0))) { + return false; + } + for (int i = 1; i < value.length(); i++) { + char c = value.charAt(i); + if (!(Character.isLetterOrDigit(c) || c == '-' || c == '_')) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/src/pp/iloc/model/Num.java b/src/pp/iloc/model/Num.java new file mode 100644 index 0000000..aecdd76 --- /dev/null +++ b/src/pp/iloc/model/Num.java @@ -0,0 +1,154 @@ +package pp.iloc.model; + +/** Numeric operand. + * A numeric operand can be a literal or a (symbolic) constant. + * @author Arend Rensink + */ +public class Num extends Operand { + /** Value of the numeric operand, if it is a literal. */ + private final int value; + /** Name of the numeric operand, if it is a symbolic constant. */ + private final String name; + /** Label wrapped in the numeric operand, if it is label-based. */ + private final Label label; + /** The kind of numeric operand. */ + private final NumKind kind; + + /** Constructs a literal numeric operand. */ + public Num(int value) { + super(Type.NUM); + this.kind = NumKind.LIT; + this.value = value; + this.name = null; + this.label = null; + } + + /** Constructs a symbolic numeric operand. + * @name name symbolic name, without '@'-prefix + */ + public Num(String name) { + super(Type.NUM); + this.kind = NumKind.SYMB; + assert wellformed(name); + this.name = name; + this.value = -1; + this.label = null; + } + + /** Constructs a label-based numeric operand. */ + public Num(Label label) { + super(Type.NUM); + this.kind = NumKind.LAB; + this.label = label; + this.value = -1; + this.name = null; + } + + /** Returns the kind of this numeric operand. */ + public NumKind getKind() { + return this.kind; + } + + /** Returns the label on which this operand is based, + * if it is label-based. */ + public Label getLabel() { + return this.label; + } + + /** Returns the value of this numeric operand, if it is a literal. */ + public int getValue() { + return this.value; + } + + /** Returns the name of this numeric operand, if it is a constant. */ + public String getName() { + return this.name; + } + + @Override + public String toString() { + switch (getKind()) { + case LAB: + return "#" + getLabel(); + case LIT: + return "" + getValue(); + case SYMB: + return '@' + getName(); + default: + assert false; + return null; + } + } + + @Override + public int hashCode() { + int prime = 31; + int result = prime * getKind().hashCode(); + switch (getKind()) { + case LAB: + result += getLabel().hashCode(); + break; + case LIT: + result += getValue(); + break; + case SYMB: + result += getName().hashCode(); + } + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Num)) { + return false; + } + Num other = (Num) obj; + if (getKind() != other.getKind()) { + return false; + } + switch (getKind()) { + case LAB: + return getLabel().equals(other.getLabel()); + case LIT: + return getValue() == other.getValue(); + case SYMB: + return getName().equals(other.getName()); + default: + assert false; + return false; + } + } + + /** Tests if a string value is a well-formed name. */ + private boolean wellformed(String value) { + if (value == null) { + return false; + } + if (value.isEmpty()) { + return false; + } + if (!Character.isLetter(value.charAt(0))) { + return false; + } + for (int i = 1; i < value.length(); i++) { + char c = value.charAt(i); + if (!(Character.isLetterOrDigit(c) || c == '-' || c == '_')) { + return false; + } + } + return true; + } + + /** Type class for numeric operands. */ + public static enum NumKind { + /** Literal constant. */ + LIT, + /** Symbolic name. */ + SYMB, + /** Label-based constant. */ + LAB; + } +} \ No newline at end of file diff --git a/src/pp/iloc/model/Op.java b/src/pp/iloc/model/Op.java new file mode 100644 index 0000000..90397e1 --- /dev/null +++ b/src/pp/iloc/model/Op.java @@ -0,0 +1,277 @@ +package pp.iloc.model; + +import static pp.iloc.model.OpClaz.COMMENT; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import pp.iloc.model.Operand.Type; + +/** + * ILOC operation + * @author Arend Rensink + */ +public class Op extends Instr { + /** Comment separator. */ + private final static String COMMENT_SEP = "// "; + /** Operand separator. */ + private final static String OP_SEP = ","; + + /** The operation code. */ + private final OpCode opCode; + /** The list of arguments of this operation. */ + private final List args; + /** The optional comment for this operation. */ + private String comment; + + /** Constructs an unlabelled operation with a given opcode and arguments. */ + public Op(OpCode opCode, Operand... args) { + this(null, opCode, args); + } + + /** Constructs an unlabelled operation with a given opcode and arguments. */ + public Op(OpCode opCode, List args) { + this(null, opCode, args); + } + + /** Constructs a labelled operation with a given opcode and arguments. */ + public Op(Label label, OpCode opCode, Operand... args) { + this(label, opCode, Arrays.asList(args)); + } + + /** Constructs a labelled operation with a given opcode and arguments. + * @throws IllegalArgumentException if one of the arguments + * is not of the expected type + */ + public Op(Label label, OpCode opCode, List args) + throws IllegalArgumentException { + if (label != null) { + super.setLabel(label); + } + this.opCode = opCode; + int argsCount = opCode.getSigSize(); + if (args.size() != argsCount) { + throw new IllegalArgumentException(String.format( + "Operation '%s' expects %d arguments but has %d", opCode, + argsCount, args.size())); + } + for (int i = 0; i < argsCount; i++) { + Operand arg = args.get(i); + Type expected = opCode.getSig().get(i); + if (arg.getType() != expected) { + throw new IllegalArgumentException( + String.format( + "Operation '%s' argument %d should be '%s' but is '%s'", + this.opCode, i, expected, arg.getType())); + } + } + this.args = new ArrayList<>(args); + } + + /** Returns the class of operation (normal or control flow). */ + public OpClaz getClaz() { + return this.opCode.getClaz(); + } + + /** Returns the opcode of this operation. */ + public OpCode getOpCode() { + return this.opCode; + } + + /** Returns the list of all (source + target) arguments. */ + public List getArgs() { + return this.args; + } + + /** Convenience method to retrieve a given argument as {@link Reg}. */ + public Reg reg(int i) { + return (Reg) this.args.get(i); + } + + /** Convenience method to retrieve a given argument as {@link Str}. */ + public Str str(int i) { + return (Str) this.args.get(i); + } + + /** Convenience method to retrieve a given argument as {@link Num}. */ + public Num num(int i) { + return (Num) this.args.get(i); + } + + /** Convenience method to retrieve a given operand as {@link Label}. */ + public Label label(int i) { + return (Label) this.args.get(i); + } + + /** Indicates if this operation has a comment. */ + public boolean hasComment() { + return getComment() != null; + } + + /** Returns the optional comment for this operation. */ + public String getComment() { + return this.comment; + } + + /** Sets a comment for this operation. */ + public void setComment(String comment) { + this.comment = comment; + } + + @Override + public int size() { + return 1; + } + + @Override + public Iterator iterator() { + return Collections.singleton(this).iterator(); + } + + @Override + public String prettyPrint(int labelSize, int sourceSize, int targetSize) { + StringBuilder result = new StringBuilder(); + if (labelSize > 0) { + result.append(String + .format("%-" + labelSize + "s", toLabelString())); + } + int arrowSize = 4; + if (getClaz() == COMMENT) { + result.append(toCommentString()); + } + if (getOpCode() == OpCode.out) { + int size = sourceSize + targetSize + arrowSize; + result.append(String.format("%-8s", getOpCode().name())); + result.append(String.format("%-" + size + "s ", toSourceString())); + result.append(toCommentString()); + } else { + result.append(String.format("%-8s", getOpCode().name())); + if (sourceSize > 0) { + result.append(String.format("%-" + sourceSize + "s", + toSourceString())); + } + result.append(String + .format("%-" + arrowSize + "s", toArrowString())); + if (targetSize > 0) { + result.append(String.format("%-" + targetSize + "s ", + toTargetString())); + } + result.append(toCommentString()); + } + result.append('\n'); + return result.toString(); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + result.append(toLabelString()); + if (getClaz() != COMMENT) { + result.append(getOpCode()); + if (getOpCode().getSourceCount() > 0) { + result.append(' '); + result.append(toSourceString()); + } + if (getOpCode().getTargetCount() > 0) { + result.append(' '); + result.append(getClaz().getArrow()); + result.append(' '); + result.append(toTargetString()); + } + result.append(' '); + } + result.append(toCommentString()); + return result.toString(); + } + + /** Returns the string representation of the arrow symbol. */ + String toArrowString() { + if (getOpCode().getTargetCount() > 0 && getClaz() != COMMENT) { + return ' ' + getClaz().getArrow() + ' '; + } else { + return ""; + } + } + + /** Returns the string representation of the optional comment. */ + String toCommentString() { + if (hasComment()) { + return COMMENT_SEP + getComment(); + } else { + return ""; + } + } + + /** Returns the string representation of the source operands. */ + String toSourceString() { + StringBuilder result = new StringBuilder(); + boolean first = true; + for (int i = 0; i < getOpCode().getSourceCount(); i++) { + Operand o = getArgs().get(i); + if (first) { + first = false; + } else { + result.append(OP_SEP); + } + result.append(o); + } + return result.toString(); + } + + /** Returns the string representation of the target operands. */ + String toTargetString() { + StringBuilder result = new StringBuilder(); + boolean first = true; + for (int i = getOpCode().getSourceCount(); i < getOpCode() + .getSigSize(); i++) { + Operand o = getArgs().get(i); + if (first) { + first = false; + } else { + result.append(OP_SEP); + } + result.append(o); + } + return result.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + super.hashCode(); + result = prime * result + + ((this.comment == null) ? 0 : this.comment.hashCode()); + result = prime * result + this.opCode.hashCode(); + result = prime * result + this.args.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!super.equals(obj)) { + return false; + } + Op other = (Op) obj; + if (!hasComment()) { + if (other.hasComment()) { + return false; + } + } else if (!getComment().equals(other.getComment())) { + return false; + } + if (this.opCode != other.opCode) { + return false; + } + if (!this.args.equals(other.args)) { + return false; + } + return true; + } +} diff --git a/src/pp/iloc/model/OpClaz.java b/src/pp/iloc/model/OpClaz.java new file mode 100644 index 0000000..a737946 --- /dev/null +++ b/src/pp/iloc/model/OpClaz.java @@ -0,0 +1,25 @@ +package pp.iloc.model; + +/** + * Class of operation: either normal or control flow. + * @author Arend Rensink + */ +public enum OpClaz { + /** A normal (non-control) operation. */ + NORMAL("=>"), + /** A control operation, i.e., one that changes the PC. */ + CONTROL("->"), + /** Special operation type holding a comment. */ + COMMENT(""); + + private final String arrow; + + private OpClaz(String arrow) { + this.arrow = arrow; + } + + /** Returns the arrow symbol used for this operation type. */ + public String getArrow() { + return this.arrow; + } +} \ No newline at end of file diff --git a/src/pp/iloc/model/OpCode.java b/src/pp/iloc/model/OpCode.java new file mode 100644 index 0000000..e03b3ba --- /dev/null +++ b/src/pp/iloc/model/OpCode.java @@ -0,0 +1,252 @@ +package pp.iloc.model; + +import static pp.iloc.model.OpClaz.COMMENT; +import static pp.iloc.model.OpClaz.CONTROL; +import static pp.iloc.model.OpClaz.NORMAL; +import static pp.iloc.model.Operand.Type.LABEL; +import static pp.iloc.model.Operand.Type.NUM; +import static pp.iloc.model.Operand.Type.REG; +import static pp.iloc.model.Operand.Type.STR; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Code defining the type of a (non-control flow) operation. + * @author Arend Rensink + */ +public enum OpCode { + // Placeholder + /** Placeholder (no operation). */ + nop(0), + + // Register arithmetic + /** Addition (reg0 + reg1 => reg2). */ + add(2, REG, REG, REG), + /** Subtraction (reg0 - reg1 => reg2). */ + sub(2, REG, REG, REG), + /** Multiplication (reg0 * reg1 => reg2). */ + mult(2, REG, REG, REG), + /** Division (reg0 / reg1 => reg2). */ + div(2, REG, REG, REG), + + // Immediate arithmetic + /** Addition of immediate value (reg0 + num1 => reg2). */ + addI(2, REG, NUM, REG), + /** Subtraction of immediate value (reg0 - num1 => reg2). */ + subI(2, REG, NUM, REG), + /** Subtraction from immediate value (num1 - reg0 => reg2). */ + rsubI(2, REG, NUM, REG), + /** Multiplication by immediate value (reg0 * num1 => reg2). */ + multI(2, REG, NUM, REG), + /** Division by immediate value (reg0 / num1 => reg2). */ + divI(2, REG, NUM, REG), + /** Division of immediate value (num1 / reg0 => reg2). */ + rdivI(2, REG, NUM, REG), + + // Shifts (register + immediate) + /** Left-shift (reg0 << reg1 => reg2). */ + lshift(2, REG, REG, REG), + /** Left-shift immediate value (reg0 << num1 => reg2). */ + lshiftI(2, REG, NUM, REG), + /** Right-shift (reg0 >> reg1 => reg2). */ + rshift(2, REG, REG, REG), + /** Right-shift immediate value (reg0 >> num1 => reg2). */ + rshiftI(2, REG, NUM, REG), + + // Other bitwise operations + /** Bitwise OR (reg0 | reg1 => reg2). */ + or(2, REG, REG, REG), + /** Bitwise OR with immediate value (reg0 | num1 => reg2). */ + orI(2, REG, NUM, REG), + /** Bitwise AND (reg0 & reg1 => reg2). */ + and(2, REG, REG, REG), + /** Bitwise AND with immediate value (reg0 & num1 => reg2). */ + andI(2, REG, NUM, REG), + /** Bitwise XOR (reg0 ^ reg1 => reg2). */ + xor(2, REG, REG, REG), + /** Bitwise XOR with immediate value (reg0 ^ num1 => reg2). */ + xorI(2, REG, NUM, REG), + + // Memory operations + /** Load immediate (num0 => reg1). */ + loadI(1, NUM, REG), + /** Load (mem(reg0) => reg1). */ + load(1, REG, REG), + /** Load address + immediate (mem(reg0 + num1) => reg2). */ + loadAI(2, REG, NUM, REG), + /** Load address + offset (mem(reg0 + reg1) => reg2). */ + loadAO(2, REG, REG, REG), + /** Character load (mem(reg0) => reg1). */ + cload(1, REG, REG), + /** Character load address + immediate (mem(reg0 + num1) => reg1). */ + cloadAI(2, REG, NUM, REG), + /** Character load address + offset (mem(reg0 + reg1) => reg2). */ + cloadAO(2, REG, REG, REG), + /** Store (reg0 => mem(reg1)). */ + store(1, REG, REG), + /** Store (reg0 => mem(reg1 + num2)). */ + storeAI(1, REG, REG, NUM), + /** Store (reg0 => mem(reg1 + reg2)). */ + storeAO(1, REG, REG, REG), + /** Character store (reg0 => mem(reg)). */ + cstore(1, REG, REG), + /** Character store (reg0 => mem(reg1 + num2)). */ + cstoreAI(1, REG, REG, NUM), + /** Character store (reg0 => mem(reg1 + reg2)). */ + cstoreAO(1, REG, REG, REG), + + // Copy operations + /** Integer-to-integer copy (reg0 => reg1). */ + i2i(1, REG, REG), + /** Character-to-character copy (reg0 => reg1). */ + c2c(1, REG, REG), + /** Character-to-integer conversion (reg0 => reg1). */ + c2i(1, REG, REG), + /** Integer-to-character conversion (reg0 => reg1). */ + i2c(1, REG, REG), + + // Comparison operations + /** Less-than comparison (reg0 < reg1 => reg2). */ + cmp_LT(2, REG, REG, REG), + /** Less-or-equal comparison (reg0 <= reg1 => reg2). */ + cmp_LE(2, REG, REG, REG), + /** Equals comparison (reg0 == reg1 => reg2). */ + cmp_EQ(2, REG, REG, REG), + /** Greater-or-equal comparison (reg0 >= reg1 => reg2). */ + cmp_GE(2, REG, REG, REG), + /** Greater-than comparison (reg0 > reg1 => reg2). */ + cmp_GT(2, REG, REG, REG), + /** Not-equals comparison (reg0 != reg1 => reg2). */ + cmp_NE(2, REG, REG, REG), + + // Jump operations + /** Conditional branch (reg0 != 0 ? #label0 : #label1 => pc). */ + cbr(CONTROL, 1, REG, LABEL, LABEL), + /** Immediate jump (#label0 => pc). */ + jumpI(CONTROL, 0, LABEL), + /** Register jump (reg0 => pc). */ + jump(CONTROL, 0, REG), + /** Pseudo-op to record labels of a register jump. */ + tbl(2, REG, LABEL), + + // Extra ops for stack manipulation + /** Push the (4-byte integer) value of a register onto the stack. + * Not official ILOC. */ + push(1, REG), + /** Pop the (4-byte integer) stack top into a register. + * Not official ILOC. */ + pop(0, REG), + /** Push the (1-byte character) value of a register onto the stack. + * Not official ILOC. */ + cpush(1, REG), + /** Pop the (1-byte character) stack top into a register. + * Not official ILOC. */ + cpop(0, REG), + // Extra ops for simulation and debugging + /** Value input (str0 => stdout and stdin => reg1). + * Not official ILOC. */ + in(1, STR, REG), + /** Value output (str0 + reg1 => stdout). + * Not official ILOC. */ + out(2, STR, REG), + /** String input (str0 => stdout and stdin => stack). + * The string is represented as length + chars (first char on top). + * Not official ILOC. */ + cin(1, STR), + /** Value output (str0 + stack => stdout). + * The string is represented as length + chars (first char on top). + * Not official ILOC. */ + cout(1, STR), + /** Stand-alone program comment; effect = nop. + * Not official ILOC. */ + comment(COMMENT, 0), + /** Halt the program with immediate status code. + * Not official ILOC. */ + halt(1, REG), + /** Halt the program with immediate status code. + * Not official ILOC. */ + haltI(1, NUM); + + + /** The class that this opcode falls into. */ + private final OpClaz claz; + + /** The source operand types. */ + private final List sourceSig; + + /** The target operand types. */ + private final List targetSig; + + /** The operand types. */ + private final List sig; + + private OpCode(int sourceCount, Operand.Type... sig) { + this(NORMAL, sourceCount, sig); + } + + private OpCode(OpClaz claz, int sourceCount, Operand.Type... sig) { + this.claz = claz; + this.sourceSig = new ArrayList<>(sourceCount); + for (int i = 0; i < sourceCount; i++) { + this.sourceSig.add(sig[i]); + } + this.targetSig = new ArrayList<>(sig.length - sourceCount); + for (int i = sourceCount; i < sig.length; i++) { + this.targetSig.add(sig[i]); + } + this.sig = new ArrayList<>(Arrays.asList(sig)); + } + + /** Returns the class of this opcode (normal or control flow). */ + public OpClaz getClaz() { + return this.claz; + } + + /** Returns the number of operands. */ + public int getSigSize() { + return getSourceCount() + getTargetCount(); + } + + /** Returns the list of expected operand types. */ + public List getSig() { + return this.sig; + } + + /** Returns the number of source operands. */ + public int getSourceCount() { + return getSourceSig().size(); + } + + /** Returns the list of expected source operand types. */ + public List getSourceSig() { + return this.sourceSig; + } + + /** Returns the number of target operands. */ + public int getTargetCount() { + return getTargetSig().size(); + } + + /** Returns the list of expected target operand types. */ + public List getTargetSig() { + return this.targetSig; + } + + /** Returns the {@link OpCode} for a given string, if any. */ + public static OpCode parse(String code) { + return codeMap.get(code); + } + + private static final Map codeMap = new HashMap<>(); + static { + for (OpCode op : values()) { + if (op.getClaz() != OpClaz.COMMENT) { + codeMap.put(op.name(), op); + } + } + } +} diff --git a/src/pp/iloc/model/OpList.java b/src/pp/iloc/model/OpList.java new file mode 100644 index 0000000..b6c7803 --- /dev/null +++ b/src/pp/iloc/model/OpList.java @@ -0,0 +1,101 @@ +package pp.iloc.model; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** List of operations forming a single instruction. */ +public class OpList extends Instr { + /** The internally stored list of operations. */ + private final List opList; + + /** Constructs an operation list with an optional label. */ + public OpList() { + this.opList = new ArrayList<>(); + } + + /** Adds an operation to this list. */ + public void addOp(Op op) { + assert !hasLine() : "Line numer set; do not add new operations"; + this.opList.add(op); + } + + /** Returns the list of operations. */ + public List getOps() { + return this.opList; + } + + @Override + public int size() { + return this.opList.size(); + } + + @Override + public Iterator iterator() { + return getOps().iterator(); + } + + @Override + public void setLine(int line) { + super.setLine(line); + // also sets the line numbers of the operations in the list + for (Op op : this) { + op.setLine(line); + line++; + } + } + + /** Returns a string consisting of this operation list in a nice layout. + * @param indent the number of columns by which the string should be indented. + * All lines except the first will be indented by this number of spaces. + */ + @Override + public String prettyPrint(int labelSize, int sourceSize, int targetSize) { + StringBuilder result = new StringBuilder(); + result.append(String.format("%-" + labelSize + "s", toLabelString())); + result.append(LIST_PREFIX); + for (Op op : getOps()) { + sourceSize = Math.max(sourceSize, op.toSourceString().length()); + targetSize = Math.max(targetSize, op.toTargetString().length()); + } + int innerLabelSize = 0; + for (Op op : getOps()) { + result.append(op + .prettyPrint(innerLabelSize, sourceSize, targetSize)); + innerLabelSize = labelSize + LIST_PREFIX.length(); + } + result.append(String.format("%-" + labelSize + "s" + LIST_POSTFIX, "")); + result.append('\n'); + return result.toString(); + } + + @Override + public String toString() { + return toLabelString() + this.opList.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + this.opList.hashCode(); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) { + return false; + } + OpList other = (OpList) obj; + if (!this.opList.equals(other.opList)) { + return false; + } + return true; + } + + private final static String LIST_PREFIX = "[ "; + private final static String LIST_POSTFIX = "]"; +} diff --git a/src/pp/iloc/model/Operand.java b/src/pp/iloc/model/Operand.java new file mode 100644 index 0000000..03f58da --- /dev/null +++ b/src/pp/iloc/model/Operand.java @@ -0,0 +1,27 @@ +package pp.iloc.model; + +/** Abstract supertype of all kinds of operands. */ +abstract public class Operand { + private final Type type; + + protected Operand(Type type) { + this.type = type; + } + + /** Returns the type of this operand. */ + public Type getType() { + return this.type; + } + + /** Enumeration of all available operand types. */ + public static enum Type { + /** Register-type operand; class {@link Reg}. */ + REG, + /** Numeric operand; class {@link Num} or {@link Symb}. */ + NUM, + /** Label operand; class {@link Label}. */ + LABEL, + /** Literal string operand; class {@link Str}. */ + STR; + } +} diff --git a/src/pp/iloc/model/Program.java b/src/pp/iloc/model/Program.java new file mode 100644 index 0000000..486ba29 --- /dev/null +++ b/src/pp/iloc/model/Program.java @@ -0,0 +1,270 @@ +package pp.iloc.model; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import pp.iloc.model.Num.NumKind; +import pp.iloc.model.Operand.Type; +import pp.iloc.parse.FormatException; + +/** ILOC program. + * @author Arend Rensink + */ +public class Program { + /** Indexed list of all instructions in the program. */ + private final List instrList; + /** + * Indexed list of all operations in the program. + * This is the flattened list of instructions. + */ + private final List opList; + /** Mapping from labels defined in the program to corresponding + * index locations. + */ + private final Map labelMap; + /** (Partial) mapping from symbolic constants used in the program + * to corresponding numeric values. */ + private final Map symbMap; + + /** Creates a program with an initially empty instruction list. */ + public Program() { + this.instrList = new ArrayList<>(); + this.opList = new ArrayList<>(); + this.labelMap = new LinkedHashMap<>(); + this.symbMap = new LinkedHashMap<>(); + } + + /** Adds an instruction to the instruction list of this program. + * @throws IllegalArgumentException if the instruction has a known label + */ + public void addInstr(Instr instr) { + instr.setProgram(this); + instr.setLine(this.opList.size()); + if (instr.hasLabel()) { + registerLabel(instr); + } + this.instrList.add(instr); + for (Op op : instr) { + this.opList.add(op); + } + } + + /** Registers the label of a given instruction. */ + void registerLabel(Instr instr) { + Label label = instr.getLabel(); + Integer loc = this.labelMap.get(label); + if (loc != null) { + throw new IllegalArgumentException(String.format( + "Label %s already occurred at location %d", label, loc)); + } + this.labelMap.put(label, instr.getLine()); + } + + /** Returns the current list of instructions of this program. */ + public List getInstr() { + return Collections.unmodifiableList(this.instrList); + } + + /** Returns the flattened list of operations in this program. */ + public List getOps() { + return Collections.unmodifiableList(this.opList); + } + + /** Returns the operation at a given line number. */ + public Op getOpAt(int line) { + return this.opList.get(line); + } + + /** Returns the size of the program, in number of operations. */ + public int size() { + return this.opList.size(); + } + + /** + * Returns the location at which a given label is defined, if any. + * @return the location of an instruction with the label, or {@code -1} + * if the label is undefined + */ + public int getLine(Label label) { + Integer result = this.labelMap.get(label); + return result == null ? -1 : result; + } + + /** Assigns a fixed numeric value to a symbolic constant. + * It is an error to assign to the same constant twice. + * @param name constant name, without preceding '@' + */ + public void setSymb(Num symb, int value) { + if (this.symbMap.containsKey(symb)) { + throw new IllegalArgumentException("Constant '" + symb + + "' already assigned"); + } + this.symbMap.put(symb, value); + } + + /** + * Returns the value with which a given symbol has been + * initialised, if any. + */ + public Integer getSymb(Num symb) { + return this.symbMap.get(symb); + } + + /** + * Returns the value with which a given named symbol has been + * initialised, if any. + * @param name name of the symbol, without '@'-prefix + */ + public Integer getSymb(String name) { + return getSymb(new Num(name)); + } + + /** + * Checks for internal consistency, in particular whether + * all used labels are defined. + */ + public void check() throws FormatException { + List messages = new ArrayList<>(); + for (Instr instr : getInstr()) { + for (Op op : instr) { + messages.addAll(checkOpnds(op.getLine(), op.getArgs())); + } + } + if (!messages.isEmpty()) { + throw new FormatException(messages); + } + } + + private List checkOpnds(int loc, List opnds) { + List result = new ArrayList<>(); + for (Operand opnd : opnds) { + if (opnd instanceof Label) { + if (getLine((Label) opnd) < 0) { + result.add(String.format("Line %d: Undefined label '%s'", + loc, opnd)); + } + } + } + return result; + } + + /** + * Returns a mapping from registers to line numbers + * in which they appear. + */ + public Map> getRegLines() { + Map> result = new LinkedHashMap<>(); + for (Op op : this.opList) { + for (Operand opnd : op.getArgs()) { + if (opnd.getType() == Type.REG) { + Set ops = result.get(((Reg) opnd).getName()); + if (ops == null) { + result.put(((Reg) opnd).getName(), + ops = new LinkedHashSet<>()); + } + ops.add(op.getLine()); + } + } + } + return result; + } + + /** + * Returns a mapping from (symbolic) variables to line numbers + * in which they appear. + */ + public Map> getSymbLines() { + Map> result = new LinkedHashMap<>(); + for (Op op : this.opList) { + for (Operand opnd : op.getArgs()) { + if (!(opnd instanceof Num)) { + continue; + } + if (((Num) opnd).getKind() != NumKind.SYMB) { + continue; + } + String name = ((Num) opnd).getName(); + Set lines = result.get(name); + if (lines == null) { + result.put(name, lines = new LinkedHashSet<>()); + } + lines.add(op.getLine()); + } + } + return result; + } + + /** Returns a line-by-line printout of this program. */ + @Override + public String toString() { + StringBuilder result = new StringBuilder(); + for (Map.Entry symbEntry : this.symbMap.entrySet()) { + result.append(String.format("%s <- %d%n", symbEntry.getKey() + .getName(), symbEntry.getValue())); + } + for (Instr instr : getInstr()) { + result.append(instr.toString()); + result.append('\n'); + } + return result.toString(); + } + + @Override + public int hashCode() { + return this.instrList.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Program)) { + return false; + } + Program other = (Program) obj; + if (!this.instrList.equals(other.instrList)) { + return false; + } + return true; + } + + /** Returns a string consisting of this program in a nice layout. + */ + public String prettyPrint() { + StringBuilder result = new StringBuilder(); + // first print the symbolic declaration map + int idSize = 0; + for (Num symb : this.symbMap.keySet()) { + idSize = Math.max(idSize, symb.getName().length()); + } + for (Map.Entry symbEntry : this.symbMap.entrySet()) { + result.append(String.format("%-" + idSize + "s <- %d%n", symbEntry + .getKey().getName(), symbEntry.getValue())); + } + if (idSize > 0) { + result.append('\n'); + } + // then print the instructions + int labelSize = 0; + int sourceSize = 0; + int targetSize = 0; + for (Instr i : getInstr()) { + labelSize = Math.max(labelSize, i.toLabelString().length()); + if (i instanceof Op && ((Op) i).getOpCode() != OpCode.out) { + Op op = (Op) i; + sourceSize = Math.max(sourceSize, op.toSourceString().length()); + targetSize = Math.max(targetSize, op.toTargetString().length()); + } + } + for (Instr i : getInstr()) { + result.append(i.prettyPrint(labelSize, sourceSize, targetSize)); + } + return result.toString(); + } +} diff --git a/src/pp/iloc/model/Reg.java b/src/pp/iloc/model/Reg.java new file mode 100644 index 0000000..9ade405 --- /dev/null +++ b/src/pp/iloc/model/Reg.java @@ -0,0 +1,45 @@ +package pp.iloc.model; + +/** Register operand + * @author Arend Rensink + */ +public class Reg extends Operand { + private final String name; + + /** Constructs an operand with a given name. */ + public Reg(String name) { + super(Type.REG); + assert name != null && name.length() > 1 : "Register names must be non-empty strings"; + this.name = name; + } + + /** Returns the name of this register. */ + public String getName() { + return this.name; + } + + @Override + public String toString() { + return this.name; + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Reg)) { + return false; + } + Reg other = (Reg) obj; + if (!getName().equals(other.getName())) { + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/pp/iloc/model/Str.java b/src/pp/iloc/model/Str.java new file mode 100644 index 0000000..72e99fe --- /dev/null +++ b/src/pp/iloc/model/Str.java @@ -0,0 +1,48 @@ +package pp.iloc.model; + +/** Literal string operand. + * This is not part of official ILOC + * @author Arend Rensink + */ +public class Str extends Operand { + private final String text; + + /** Constructs a string operand with a given (string) value. */ + public Str(String value) { + super(Type.STR); + this.text = value; + } + + /** Returns the text of this string. */ + public String getText() { + return this.text; + } + + @Override + public String toString() { + return DQUOTE + this.text.replaceAll(DQUOTE, BSLASH + DQUOTE) + DQUOTE; + } + + @Override + public int hashCode() { + return getText().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof Str)) { + return false; + } + Str other = (Str) obj; + if (!getText().equals(other.getText())) { + return false; + } + return true; + } + + private final static String BSLASH = "\\"; + private final static String DQUOTE = "\""; +} \ No newline at end of file diff --git a/src/pp/iloc/parse/.gitignore b/src/pp/iloc/parse/.gitignore new file mode 100644 index 0000000..832b829 --- /dev/null +++ b/src/pp/iloc/parse/.gitignore @@ -0,0 +1,6 @@ +/ILOC.tokens +/ILOCLexer.tokens +/ILOCBaseListener.java +/ILOCLexer.java +/ILOCListener.java +/ILOCParser.java diff --git a/src/pp/iloc/parse/ErrorListener.java b/src/pp/iloc/parse/ErrorListener.java new file mode 100644 index 0000000..ed3471a --- /dev/null +++ b/src/pp/iloc/parse/ErrorListener.java @@ -0,0 +1,42 @@ +package pp.iloc.parse; + +import java.util.ArrayList; +import java.util.List; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.Token; + +/** Antlr error listener to collect errors rather than send them to stderr. */ +public class ErrorListener extends BaseErrorListener { + /** Errors collected by the listener. */ + private final List errors = new ArrayList<>(); + + @Override + public void syntaxError(Recognizer recognizer, + Object offendingSymbol, int line, int charPositionInLine, + String msg, RecognitionException e) { + this.errors.add(String.format("Line %d:%d - %s", line, + charPositionInLine, msg)); + } + + /** Adds an error message during the tree visit stage. */ + public void visitError(Token token, String msg, Object... args) { + int line = token.getLine(); + int charPositionInLine = token.getCharPositionInLine(); + msg = String.format(msg, args); + msg = String.format("Line %d:%d - %s", line, charPositionInLine, msg); + this.errors.add(msg); + } + + /** Indicates if the listener has collected any errors. */ + public boolean hasErrors() { + return !this.errors.isEmpty(); + } + + /** Returns the (possibly empty) list of errors collected by the listener. */ + public List getErrors() { + return this.errors; + } +} \ No newline at end of file diff --git a/src/pp/iloc/parse/FormatException.java b/src/pp/iloc/parse/FormatException.java new file mode 100644 index 0000000..cb0ab33 --- /dev/null +++ b/src/pp/iloc/parse/FormatException.java @@ -0,0 +1,43 @@ +package pp.iloc.parse; + +import java.util.ArrayList; +import java.util.List; + +/** Exception class to collect errors found during + * scanning, parsing and assembly of an ILOC program. + * @author Arend Rensink + */ +public class FormatException extends Exception { + private static final long serialVersionUID = 3908719003113286390L; + + /** Constructs an exception without a message. */ + public FormatException() { + // empty + } + + /** Constructs an exception with a formatted message. + * @param message format string in {@link String#format(String, Object...)} syntax + * @param args arguments for the format string + */ + public FormatException(String message, Object... args) { + super(String.format(message, args)); + this.messages.add(getMessage()); + } + + /** Constructs an exception from a list of messages. */ + public FormatException(List messages) { + super(concat(messages)); + this.messages.addAll(messages); + } + + private final List messages = new ArrayList<>(); + + static private final String concat(List args) { + StringBuilder result = new StringBuilder(); + for (String arg : args) { + result.append(arg); + result.append('\n'); + } + return result.toString(); + } +} diff --git a/src/pp/iloc/parse/ILOC.g4 b/src/pp/iloc/parse/ILOC.g4 new file mode 100644 index 0000000..952cc5e --- /dev/null +++ b/src/pp/iloc/parse/ILOC.g4 @@ -0,0 +1,70 @@ +grammar ILOC; + +@header{package pp.iloc.parse;} + +fragment LETTER: [a-zA-Z]; +fragment DIGIT: [0-9]; + +/** Full ILOC program. */ +program: decl* instr (EOL+ instr)* EOL* EOF; + +decl: ID ASS NUM COMMENT? EOL+ + ; + +/** Instruction: single op or []-bracketed non-empty op sequence. */ +instr + : (label ':')? + op #singleInstr + | (label ':')? + LSQ + EOL* + op + (EOL+ op)* + EOL* + RSQ #instrList + ; + +/** Single operation. */ +op : COMMENT #comment + | opCode sources ((ARROW|DARROW) targets)? + SEMI? + COMMENT? #realOp + ; + +sources: (operand (COMMA operand)*)?; + +targets: operand (COMMA operand)*; + +/** Operation label. */ +label: ID; + +/** Opcode: not distinguished by the parser. */ +opCode: ID; + +/** Operand: ID (label or register), number or string. */ +operand : ID | NUM | SYMB | LAB | STR; + +MINUS: '-'; +COMMA: ','; +SEMI: ';'; +LSQ: '['; +RSQ: ']'; +DARROW: '=>'; +ARROW: '->'; +ASS: '<-'; + +/** Identifier. */ +ID: LETTER (LETTER|DIGIT|[\-_])*; +/** Symbolic name. */ +SYMB: '@' ID; +/** Label used as numeric parameter. */ +LAB: '#' ID; +/** Number. */ +NUM: MINUS? DIGIT+; +/** String with optional escaped double quotes. */ +STR : '"' (~["\n\r] | '\\"')* '"'; +/** Java-style comment: // to end of line */ +COMMENT: '//' ~[\r\n]*; +/** Whitespace. */ +WS : [ \t]+ -> skip; +EOL : [\r\n]+; \ No newline at end of file diff --git a/src/pp/iloc/test/AssemblerTest.java b/src/pp/iloc/test/AssemblerTest.java new file mode 100644 index 0000000..9424565 --- /dev/null +++ b/src/pp/iloc/test/AssemblerTest.java @@ -0,0 +1,71 @@ +package pp.iloc.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; + +import org.junit.Test; + +import pp.iloc.Assembler; +import pp.iloc.model.Program; +import pp.iloc.parse.FormatException; + +@SuppressWarnings("javadoc") +public class AssemblerTest { + @Test + public void testFig13() { + Program p = parse("fig1-3"); + assertEquals(ints(0, 5, 6, 7, 8, 9), p.getRegLines().get("r_a")); + assertEquals(ints(1, 5), p.getRegLines().get("r_2")); + assertEquals(ints(0, 9), p.getSymbLines().get("a")); + } + + @Test + public void testFig13Init() { + Program p = parse("fig1-3-init"); + assertEquals(ints(0, 5, 6, 7, 8, 9), p.getRegLines().get("r_a")); + assertEquals(ints(1, 5), p.getRegLines().get("r_2")); + assertEquals(ints(0, 9), p.getSymbLines().get("a")); + } + + @Test + public void testFig13Stack() { + Program p = parse("fig1-3-stack"); + assertEquals(ints(0, 2, 3, 4, 6, 7, 8, 10, 11, 13, 14, 15), p + .getRegLines().get("r_1")); + assertEquals(ints(1, 2, 5, 6, 9, 10, 12, 14), p.getRegLines().get("r_2")); + assertEquals(ints(0, 15), p.getSymbLines().get("a")); + } + + private HashSet ints(Integer... vals) { + return new HashSet<>(Arrays.asList(vals)); + } + + Program parse(String filename) { + File file = new File(filename + ".iloc"); + if (!file.exists()) { + file = new File(BASE_DIR + filename + ".iloc"); + } + try { + Program result = Assembler.instance().assemble(file); + String print = result.prettyPrint(); + if (SHOW) { + System.out.println("Program " + file + ":"); + System.out.print(print); + } + Program other = Assembler.instance().assemble(print); + assertEquals(result, other); + return result; + } catch (FormatException | IOException e) { + fail(e.getMessage()); + return null; + } + } + + private final static String BASE_DIR = "pp/iloc/sample/"; + private final static boolean SHOW = true; +} diff --git a/src/pp/iloc/test/SimulatorTest.java b/src/pp/iloc/test/SimulatorTest.java new file mode 100644 index 0000000..6fab38f --- /dev/null +++ b/src/pp/iloc/test/SimulatorTest.java @@ -0,0 +1,112 @@ +package pp.iloc.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; + +import org.junit.Test; + +import pp.iloc.Assembler; +import pp.iloc.Simulator; +import pp.iloc.eval.Machine; +import pp.iloc.model.Program; +import pp.iloc.parse.FormatException; + +@SuppressWarnings("javadoc") +public class SimulatorTest { + @Test(timeout = 1000) + public void testFig13() { + Program p = parse("fig1-3"); + Machine c = new Machine(); + int a = c.init("a", 2); + c.init("b", 3); + c.init("c", 4); + c.init("d", 5); + new Simulator(p, c).run(); + if (SHOW) { + System.out.println(c); + } + assertEquals(240, c.load(a)); + } + + @Test(timeout = 1000) + public void testFig13Stack() { + Program p = parse("fig1-3-stack"); + Machine c = new Machine(); + int a = c.init("a", 2); + c.init("b", 3); + c.init("c", 4); + c.init("d", 5); + new Simulator(p, c).run(); + if (SHOW) { + System.out.println(c); + } + assertEquals(240, c.load(a)); + } + + @Test(timeout = 1000) + public void testFig13Init() { + Program p = parse("fig1-3-init"); + Machine c = new Machine(); + c.store(2, p.getSymb("a")); + c.store(3, p.getSymb("b")); + c.store(4, p.getSymb("c")); + c.store(5, p.getSymb("d")); + new Simulator(p, c).run(); + if (SHOW) { + System.out.println(c); + System.out.println(p.prettyPrint()); + } + assertEquals(240, c.load(p.getSymb("a"))); + } + + @Test(timeout = 1000) + public void testString() { + Program p = parse("string"); + Simulator sim = new Simulator(p); + sim.setIn(new ByteArrayInputStream("abc".getBytes())); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + sim.setOut(out); + sim.run(); + if (SHOW) { + System.out.println(p.prettyPrint()); + } + assertEquals("Doubled: abcabc", out.toString().trim()); + } + + @Test + //(timeout = 1000) + public void testStringChar4() { + Program p = parse("string4"); + Simulator sim = new Simulator(p); + sim.getVM().setCharSize(4); + sim.setIn(new ByteArrayInputStream("abc".getBytes())); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + sim.setOut(out); + sim.run(); + if (SHOW) { + System.out.println(p.prettyPrint()); + } + assertEquals("Doubled: abcabc", out.toString().trim()); + } + + Program parse(String filename) { + File file = new File(filename + ".iloc"); + if (!file.exists()) { + file = new File(BASE_DIR + filename + ".iloc"); + } + try { + return Assembler.instance().assemble(file); + } catch (FormatException | IOException e) { + fail(e.getMessage()); + return null; + } + } + + private final static String BASE_DIR = "pp/iloc/sample/"; + private final static boolean SHOW = true; +}