package pp.s1184725.boppi;

import java.util.*;
import java.util.logging.Logger;

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.*;
import org.apache.commons.lang3.StringUtils;

import pp.iloc.Assembler;
import pp.iloc.eval.Machine;
import pp.iloc.model.*;
import pp.s1184725.boppi.antlr.*;
import pp.s1184725.boppi.antlr.BoppiParser.*;
import pp.s1184725.boppi.type.*;
import pp.s1184725.boppi.symboltable.*;

/**
 * The generator stage of the Boppi toolchain.
 *
 * @author Frank Wibbelink
 */
public class BoppiGenerator extends BoppiBaseVisitor<Reg> {
	/**
	 * Whether local variables are always deallocated at the end of a function
	 * call to provide good memory management or kept if there are multiple
	 * references to the function for full closure support.
	 */
	public static final boolean PROPER_CLEANUP_INSTEAD_OF_PROPER_CLOSURES = false;

	private static final int ARBASESIZE = 16;
	private static final Num ZERO = new Num(0);
	private static final Num OFFSET_ARP = new Num(-4), OFFSET_RETURN_ADDR = new Num(-8),
			OFFSET_RETURN_VAL = new Num(-12), OFFSET_AL = new Num(-16), OFFSET_FUNCREF_ADDR = ZERO,
			OFFSET_FUNCREF_ARP = new Num(4), OFFSET_FUNCREF_ARSIZE = new Num(8), OFFSET_REF_COUNT = new Num(-8),
			OFFSET_REF_SIZE = new Num(-4), OFFSET_ARRAY_LENGTH = ZERO, OFFSET_ARRAY_OFFSET = new Num(4);
	/**
	 * Unknown type used in a read expression
	 */
	public static final Num ERROR_INPUT_UNKNOWN_TYPE = new Num(0x72656164);
	/**
	 * Unknown type used in a print expression
	 */
	public static final Num ERROR_OUTPUT_UNKNOWN_TYPE = new Num(0x77726974);
	/**
	 * Array out of bounds
	 */
	public static final Num ERROR_OUT_OF_BOUNDS = new Num(0x616f6f62);
	/**
	 * Negative array size
	 */
	public static final Num ERROR_ILLEGAL_ARRAY_SIZE = new Num(0x616e737a);

	private Program prog;
	private Annotations an;
	private final Reg tempArp = new Reg("ART"), arp = Machine.ARP_REG; //$NON-NLS-1$
	private Logger logger;
	private RegisterPool regPool;
	private int labels = 0;

	/**
	 * Maps operator tokens to ILOC opcodes.
	 */
	private static Map<Integer, OpCode> ops = new HashMap<Integer, OpCode>() {
		private static final long serialVersionUID = 8979722313842633807L;

		{
			put(BoppiLexer.AND, OpCode.and);
			put(BoppiLexer.DIVIDE, OpCode.div);
			put(BoppiLexer.EQ, OpCode.cmp_EQ);
			put(BoppiLexer.GT, OpCode.cmp_GT);
			put(BoppiLexer.GTE, OpCode.cmp_GE);
			put(BoppiLexer.LEQ, OpCode.cmp_LE);
			put(BoppiLexer.LT, OpCode.cmp_LT);
			put(BoppiLexer.MINUS, OpCode.sub);
			put(BoppiLexer.MULTIPLY, OpCode.mult);
			put(BoppiLexer.NEQ, OpCode.cmp_NE);
			put(BoppiLexer.PLUS, OpCode.add);
			put(BoppiLexer.OR, OpCode.or);
		}
	};

	/**
	 * Creates a new generator with the given variable annotations and attaches
	 * the given logger.
	 *
	 * @param ast
	 *            the program's AST
	 * @param annotations
	 *            the annotation object to use during generation
	 * @param logger
	 *            the logger to report errors to
	 * @return an ILOC program generated from the AST
	 */
	public static Program generateProgram(ParseTree ast, Annotations annotations, Logger logger) {
		BoppiGenerator generator = new BoppiGenerator(annotations, logger);
		generator.visit(ast);
		return generator.prog;
	}

	protected BoppiGenerator(Annotations annotations, Logger logger) {
		try {
			CharStream memlib = ToolChain.getCharStream(this.getClass(), "memlib.iloc"); //$NON-NLS-1$
			prog = Assembler.instance().assemble(memlib);

			CharStream stdlib = ToolChain.getCharStream(this.getClass(), "stdlib.iloc"); //$NON-NLS-1$
			Program prog2 = Assembler.instance().assemble(stdlib);

			try {
				for (Instr instr : prog2.getInstr())
					prog.addInstr(instr);
			} catch (Throwable t) {
				t.printStackTrace(System.err);
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}

		this.an = annotations;
		this.regPool = new RegisterPool(logger);
		this.logger = logger;
	}

	/**
	 * Constructs an operation from the parameters and adds it to the program
	 * under construction.
	 */
	private Op emit(String comment, Label label, OpCode opCode, Operand... args) {
		Op result = new Op(label, opCode, args);
		result.setComment(String.format("%s", comment)); //$NON-NLS-1$
		this.prog.addInstr(result);
		return result;
	}

	/**
	 * Constructs an operation from the parameters and adds it to the program
	 * under construction.
	 */
	private Op emit(String comment, OpCode opCode, Operand... args) {
		return emit(comment, (Label) null, opCode, args);
	}

	/**
	 * Increments the chain of ARs.
	 *
	 * @param temp
	 *            temporary register to use
	 * @param ar
	 *            register containing the top-level AR
	 */
	private void incARReferences(Reg temp, Reg ar) {
		String comment = "AR incRef"; //$NON-NLS-1$
		Label loop = makeLabel("aril"), done = makeLabel("arid"); //$NON-NLS-1$ //$NON-NLS-2$
		emit(comment, OpCode.i2i, ar, tempArp);
		emit(comment, OpCode.cmp_NE, tempArp, RegisterPool.ZERO, temp);
		emit(comment, OpCode.cbr, temp, loop, done);
		emit(comment, loop, OpCode.loadI, new Num(prog.size() + 5), temp);
		emit(comment, OpCode.push, temp);
		emit(comment, OpCode.subI, tempArp, new Num(ARBASESIZE), temp);
		emit(comment, OpCode.push, temp);
		emit(comment, OpCode.jumpI, new Label("memaddref")); //$NON-NLS-1$

		emit(comment, OpCode.loadAI, tempArp, OFFSET_AL, tempArp);
		emit(comment, OpCode.cmp_NE, tempArp, RegisterPool.ZERO, temp);
		emit(comment, OpCode.cbr, temp, loop, done);
		emit(comment, done, OpCode.nop);
	}

	/**
	 * Decrements the chain of ARs.
	 *
	 * @param temp
	 *            temporary register to use
	 * @param ar
	 *            register containing the top-level AR
	 */
	private void decARReferences(Reg temp, Reg ar) {
		String comment = "AR decRef"; //$NON-NLS-1$
		Label loop = makeLabel("ardl"), done = makeLabel("ardd"); //$NON-NLS-1$ //$NON-NLS-2$
		emit(comment, OpCode.i2i, ar, tempArp);
		emit(comment, OpCode.cmp_NE, tempArp, RegisterPool.ZERO, temp);
		emit(comment, OpCode.cbr, temp, loop, done);
		emit(comment, loop, OpCode.loadI, new Num(prog.size() + 5), temp);
		emit(comment, OpCode.push, temp);
		emit(comment, OpCode.subI, tempArp, new Num(ARBASESIZE), temp);
		emit(comment, OpCode.push, temp);
		emit(comment, OpCode.jumpI, new Label("memfree")); //$NON-NLS-1$

		emit(comment, OpCode.loadAI, tempArp, OFFSET_AL, tempArp);
		emit(comment, OpCode.cmp_NE, tempArp, RegisterPool.ZERO, temp);
		emit(comment, OpCode.cbr, temp, loop, done);
		emit(comment, done, OpCode.nop);
	}

	/**
	 * Allocates a chunk of memory and returns a pointer to the memory in the
	 * given register.
	 *
	 * @param temp
	 *            temporary register to use
	 * @param size
	 *            the size of the memory chunk
	 */
	private void malloc(Reg temp, int size) {
		emit("malloc", OpCode.loadI, new Num(prog.size() + 5), temp); //$NON-NLS-1$
		emit("malloc", OpCode.push, temp); //$NON-NLS-1$
		emit("malloc", OpCode.loadI, new Num(size), temp); //$NON-NLS-1$
		emit("malloc", OpCode.push, temp); //$NON-NLS-1$
		emit("malloc", OpCode.jumpI, new Label("memalloc")); //$NON-NLS-1$ //$NON-NLS-2$
		emit("malloc", OpCode.pop, temp); //$NON-NLS-1$
	}

	/**
	 * Allocates a chunk of memory and returns a pointer to the memory in the
	 * given register.
	 *
	 * @param temp
	 *            temporary register to use
	 * @param size
	 *            the register containing the size of the memory chunk
	 */
	private void malloc(Reg temp, Reg size) {
		emit("malloc", OpCode.loadI, new Num(prog.size() + 4), temp); //$NON-NLS-1$
		emit("malloc", OpCode.push, temp); //$NON-NLS-1$
		emit("malloc", OpCode.push, size); //$NON-NLS-1$
		emit("malloc", OpCode.jumpI, new Label("memalloc")); //$NON-NLS-1$ //$NON-NLS-2$
		emit("malloc", OpCode.pop, temp); //$NON-NLS-1$
	}

	/**
	 * Frees a chunk of memory pointed at by {@code ptr}.
	 *
	 * @param temp
	 *            temporary register to use
	 * @param ptr
	 *            the register containing the memory pointer
	 */
	private void free(Reg temp, Reg ptr) {
		emit("free", OpCode.loadI, new Num(prog.size() + 4), temp); //$NON-NLS-1$
		emit("free", OpCode.push, temp); //$NON-NLS-1$
		emit("free", OpCode.push, ptr); //$NON-NLS-1$
		emit("free", OpCode.jumpI, new Label("memfree")); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Increments the reference count of a memory chunk.
	 *
	 * @param temp
	 *            temporary register to use
	 * @param ptr
	 *            the register containing the memory pointer
	 */
	private void incRef(Reg temp, Reg ptr) {
		emit("memaddref", OpCode.loadI, new Num(prog.size() + 4), temp); //$NON-NLS-1$
		emit("memaddref", OpCode.push, temp); //$NON-NLS-1$
		emit("memaddref", OpCode.push, ptr); //$NON-NLS-1$
		emit("memaddref", OpCode.jumpI, new Label("memaddref")); //$NON-NLS-1$ //$NON-NLS-2$
	}

	/**
	 * Returns an error message for a given parse tree node.
	 *
	 * @param ctx
	 *            the parse tree node at which the error occurred
	 * @param message
	 *            the error message
	 * @param args
	 *            arguments for the message, see {@link String#format}
	 */
	private String getError(ParserRuleContext node, String message, Object... args) {
		int line = node.getStart().getLine();
		int column = node.getStart().getCharPositionInLine();
		return String.format(Messages.getString("BoppiGenerator.1"), line, column, String.format(message, args)); //$NON-NLS-1$
	}

	/**
	 * Constructs a new unique label with the given prefix to make it
	 * recognisable in ILOC.
	 *
	 * @param prefix
	 *            a short prefix for the label
	 * @return the label
	 */
	private Label makeLabel(String prefix) {
		return new Label(prefix + (labels++));
	}

	/**
	 * Increments the reference count to a type if it is a reference type. For
	 * function types this includes all access links. Assumes registers are
	 * reserved.
	 *
	 * @param type
	 *            the type of this reference
	 * @param addr
	 *            the address of the reference object
	 */
	private void incrementReference(Type type, Reg addr) {
		if (!(type instanceof ReferenceType))
			logger.severe(String.format(Messages.getString("BoppiGenerator.0"), type)); //$NON-NLS-1$

		regPool.withReg((temp, ar) -> {
			String comment = "add new reference"; //$NON-NLS-1$

			incRef(temp, addr);

			if (type instanceof FunctionType) {
				emit(comment, OpCode.loadAI, addr, OFFSET_FUNCREF_ARP, ar);

				incARReferences(temp, ar);
			}
		});
	}

	/**
	 * Decrements the reference count to a type if it is a reference type. For
	 * function types this includes all access links. Assumes registers are
	 * reserved.
	 *
	 * @param type
	 *            the type of this reference
	 * @param addr
	 *            the address of the reference object
	 */
	private void decrementReference(Type type, Reg addr) {
		if (!(type instanceof ReferenceType))
			logger.severe(String.format(Messages.getString("BoppiGenerator.2"), type)); //$NON-NLS-1$

		regPool.withReg((temp) -> {
			Label isNull = makeLabel("ynul"), notNull = makeLabel("nnul"); //$NON-NLS-1$ //$NON-NLS-2$
			String comment = "remove old reference"; //$NON-NLS-1$

			emit(comment, OpCode.cmp_EQ, addr, RegisterPool.ZERO, temp);
			emit(comment, OpCode.cbr, temp, isNull, notNull);

			emit(comment, notNull, OpCode.nop);

			free(temp, addr);

			if (type instanceof FunctionType) {
				emit(comment, OpCode.loadAI, addr, OFFSET_FUNCREF_ARP, addr);

				decARReferences(temp, addr);
			}

			emit(comment, isNull, OpCode.nop);
		});
	}

	/**
	 * Decrements all reference-type variables of a function. Must be called at
	 * the end of a function declaration. Assumes registers are reserved.
	 *
	 * @param scope
	 *            the function scope with all relevant variables
	 */
	private void dereferenceLocalVariables(FunctionScope<Type> scope) {
		regPool.withReg((addr, ft) -> {
			for (Variable<Type> var : scope.getVars()) {
				Type type = var.getType();

				if (type instanceof ReferenceType) {
					Label isNull = makeLabel("ynul"), notNull = makeLabel("nnul"); //$NON-NLS-1$ //$NON-NLS-2$
					String comment = "remove reference"; //$NON-NLS-1$
					emit(comment + " get var", OpCode.loadAI, arp, new Num(var.getOffset()), addr); //$NON-NLS-1$

					emit(comment, OpCode.cmp_EQ, addr, RegisterPool.ZERO, ft);
					emit(comment, OpCode.cbr, ft, isNull, notNull);

					emit(comment, notNull, OpCode.nop);

					free(ft, addr);

					if (type instanceof FunctionType) {
						emit(comment, OpCode.loadAI, addr, OFFSET_FUNCREF_ARP, addr);

						decARReferences(ft, addr);
					}

					emit(comment, isNull, OpCode.nop);
				}
			}

		});
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * This override only aggregates {@link ParserRuleContext}s.
	 */
	@Override
	public Reg visitChildren(RuleNode node) {
		Reg result = defaultResult();
		int n = node.getChildCount();
		for (int i = 0; i < n; i++) {
			if (!shouldVisitNextChild(node, result)) {
				break;
			}

			ParseTree c = node.getChild(i);
			Reg childResult = c.accept(this);
			if (c instanceof ParserRuleContext)
				result = aggregateResult(result, childResult);
		}

		return result;
	}

	/**
	 * {@inheritDoc}
	 * <p>
	 * This override logs any exceptions to the {@link Logger} attached to this
	 * generator.
	 */
	@Override
	public Reg visit(ParseTree tree) {
		try {
			return super.visit(tree);
		} catch (Exception e) {

			if (tree instanceof ParserRuleContext)
				logger.severe(getError((ParserRuleContext) tree, Messages.getString("BoppiGenerator.3"), //$NON-NLS-1$
						e.getClass().getName(), e.getMessage()));
			else
				logger.severe(
						String.format(Messages.getString("BoppiGenerator.4"), e.getClass().getName(), e.getMessage())); //$NON-NLS-1$
			return null;
		}
	}

	@Override
	public Reg visitAssign(AssignContext ctx) {
		Reg result = visit(ctx.assignStat() != null ? ctx.assignStat() : ctx.expr());

		regPool.blockReg(result, () -> {
			Type type = an.types.get(ctx);
			Reg addr = visit(ctx.variable());

			if (type instanceof ReferenceType) {
				regPool.blockReg(addr, () -> {
					regPool.withReg((temp) -> {
						emit("load reference", OpCode.load, addr, temp); //$NON-NLS-1$
						decrementReference(an.types.get(ctx), temp);
					});
				});
			}

			OpCode op = (SimpleType.CHAR.equals(type)) ? OpCode.cstore : OpCode.store;

			emit("to " + ctx.variable().getText(), op, result, addr); //$NON-NLS-1$

			if (type instanceof ReferenceType) {
				regPool.blockReg(addr, () -> {
					regPool.withReg((temp) -> {
						emit("load reference", OpCode.load, addr, temp); //$NON-NLS-1$
						incrementReference(an.types.get(ctx), temp);
					});
				});
			}
		});

		return result;
	}

	/**
	 * Call function:
	 * <ol>
	 * <li>generate load function reference</li>
	 * <li>generate allocate AR</li>
	 * <li>visit parameters and generate store in AR</li>
	 * <li>generate register save</li>
	 * <li>generate store AL, AR, return address</li>
	 * <li>generate increment AR references</li>
	 * <li>generate set AR</li>
	 * <li>generate call function</li>
	 * <li>generate decrement AR references</li>
	 * <li>generate register restore</li>
	 * <li>generate restore AR</li>
	 * </ol>
	 */
	@Override
	public Reg visitCall(CallContext ctx) {
		String base = "call " + ctx.variable().getText() + " - "; //$NON-NLS-1$ //$NON-NLS-2$
		Variable<Type> function = an.variables.get(ctx.variable());
		TupleType parameters = (TupleType) ((FunctionType) function.getType()).getParameter();

		Reg ar = regPool.withReg((tempAR) -> {
			regPool.withReg((rt) -> {
				Reg addr = visit(ctx.variable());
				emit(base + "load function reference", OpCode.load, addr, rt); //$NON-NLS-1$
				emit(base + "load AR size", OpCode.loadAI, rt, OFFSET_FUNCREF_ARSIZE, rt); //$NON-NLS-1$
				malloc(tempAR, rt);
			});

			emit(base + "shift AR", OpCode.addI, tempAR, new Num(ARBASESIZE), tempAR); //$NON-NLS-1$

			for (int i = 0; i < ctx.expr().size(); i++) {
				Reg exprReg = visit(ctx.expr(i));

				if (SimpleType.CHAR.equals(parameters.get(i)))
					emit(base + "store param " + i, OpCode.cstoreAI, exprReg, tempAR, new Num(parameters.getOffset(i))); //$NON-NLS-1$
				else
					emit(base + "store param " + i, OpCode.storeAI, exprReg, tempAR, new Num(parameters.getOffset(i))); //$NON-NLS-1$
			}
			;
		});

		Stack<Reg> inUse = new Stack<>();
		for (Reg reg : regPool.getInUse()) {
			inUse.push(reg);
			emit(base + "register save " + reg.getName(), OpCode.push, reg); //$NON-NLS-1$
		}

		regPool.blockReg(ar, () -> {
			regPool.withReg((r1, r2) -> {
				Reg addr = visit(ctx.variable());
				emit(base + "load function reference", OpCode.load, addr, r2); //$NON-NLS-1$
				emit(base + "link caller ARP", OpCode.storeAI, arp, ar, OFFSET_ARP); //$NON-NLS-1$
				emit(base + "load AL", OpCode.loadAI, r2, OFFSET_FUNCREF_ARP, r1); //$NON-NLS-1$
				emit(base + "link AL", OpCode.storeAI, r1, ar, OFFSET_AL); //$NON-NLS-1$

				emit("add ref for callee's AL", OpCode.loadAI, ar, OFFSET_AL, tempArp); //$NON-NLS-1$
				incARReferences(r1, tempArp);

				emit(base + "load return address", OpCode.loadI, new Num(prog.size() + 5), r1); //$NON-NLS-1$
				emit(base + "set return address", OpCode.storeAI, r1, ar, OFFSET_RETURN_ADDR); //$NON-NLS-1$
				emit(base + "move ARP", OpCode.i2i, ar, arp); //$NON-NLS-1$

				emit(base + "load target address", OpCode.loadAI, r2, OFFSET_FUNCREF_ADDR, r1); //$NON-NLS-1$
				emit(base + "execute", OpCode.jump, r1); //$NON-NLS-1$
			});
		});

		regPool.withReg((temp) -> {
			decARReferences(temp, arp);
		});

		while (!inUse.isEmpty()) {
			Reg reg = inUse.pop();
			emit(base + "register unsave " + reg.getName(), OpCode.pop, reg); //$NON-NLS-1$
		}

		return regPool.withReg((res) -> {
			emit(base + "load result", OpCode.loadAI, arp, OFFSET_RETURN_VAL, res); //$NON-NLS-1$
			emit(base + "reset ARP", OpCode.loadAI, arp, OFFSET_ARP, arp); //$NON-NLS-1$
		});
	}

	/**
	 * Declare named function:
	 * <ol>
	 * <li>generate jump over function body</li>
	 * <li>generate entry point</li>
	 * <li>visit function body</li>
	 * <li>generate return</li>
	 * <li>generate check whether no more AR references</li>
	 * <li>if so, generate dereference local variables</li>
	 * <li>generate allocate function reference</li>
	 * <li>generate store entry point and AR to function reference</li>
	 * <li>generate store function reference in variable</li>
	 * </ol>
	 */
	@Override
	public Reg visitDeclareFunction(DeclareFunctionContext ctx) {
		FunctionType funcType = (FunctionType) an.variables.get(ctx).getType();
		String base = "define " + ctx.IDENTIFIER().getText() + " - "; //$NON-NLS-1$ //$NON-NLS-2$

		Label skip = makeLabel("s"); //$NON-NLS-1$
		emit(base + "jump over body", OpCode.jumpI, skip); //$NON-NLS-1$
		int callAddress = emit(base + "entry point", OpCode.nop).getLine(); //$NON-NLS-1$

		Reg result = visit(ctx.body);

		if (funcType.getReturn() != SimpleType.VOID)
			emit(base + "move result", OpCode.storeAI, result, arp, OFFSET_RETURN_VAL); //$NON-NLS-1$

		if (PROPER_CLEANUP_INSTEAD_OF_PROPER_CLOSURES)
			dereferenceLocalVariables(an.function.get(ctx));
		else {
			Label cleanup = makeLabel("ycl"), noCleanup = makeLabel("ncl"); //$NON-NLS-1$ //$NON-NLS-2$
			regPool.withReg((ref, one) -> {
				emit("load ref count", OpCode.loadAI, arp, OFFSET_REF_COUNT, ref); //$NON-NLS-1$
				emit("one", OpCode.loadI, new Num(1), one); //$NON-NLS-1$
				emit("check more than one ref", OpCode.cmp_LE, ref, one, ref); //$NON-NLS-1$
				emit("remove vars if last reference", OpCode.cbr, ref, cleanup, noCleanup); //$NON-NLS-1$
			});
			emit("cleanup target", cleanup, OpCode.nop); //$NON-NLS-1$

			dereferenceLocalVariables(an.function.get(ctx));

			emit("no cleanup target", noCleanup, OpCode.nop); //$NON-NLS-1$
		}

		regPool.withReg((r1) -> {
			emit(base + "load return address", OpCode.loadAI, arp, OFFSET_RETURN_ADDR, r1); //$NON-NLS-1$
			emit(base + "go to return address", OpCode.jump, r1); //$NON-NLS-1$
		});

		emit(base + "skip target", skip, OpCode.nop); //$NON-NLS-1$

		regPool.withReg((r1, r2) -> {
			malloc(r2, funcType.getReferenceSize());
			emit(base + "load target address", OpCode.loadI, new Num(callAddress), r1); //$NON-NLS-1$
			emit(base + "set target address", OpCode.storeAI, r1, r2, OFFSET_FUNCREF_ADDR); //$NON-NLS-1$
			emit(base + "copy ARP", OpCode.storeAI, arp, r2, OFFSET_FUNCREF_ARP); //$NON-NLS-1$
			emit(base + "load AR size", OpCode.loadI, new Num(ARBASESIZE + an.function.get(ctx).getLocalDataSize()), //$NON-NLS-1$
					r1);
			emit(base + "set AR size", OpCode.storeAI, r1, r2, OFFSET_FUNCREF_ARSIZE); //$NON-NLS-1$

			emit(base + "set function reference", OpCode.storeAI, r2, arp, new Num(an.variables.get(ctx).getOffset())); //$NON-NLS-1$
			incARReferences(r2, arp);
		});

		return null;
	}

	@Override
	public Reg visitDefineArray(DefineArrayContext ctx) {
		Type elementType = ((ArrayType) an.types.get(ctx)).getType();
		Reg arrLength = visit(ctx.size);

		return regPool.blockReg(arrLength, () -> regPool.withReg((arrSize, addr) -> {
			Label validLength = makeLabel("aszt"), invalidLength = makeLabel("aszf"); //$NON-NLS-1$ //$NON-NLS-2$
			emit("check length non-negative", OpCode.cmp_GE, arrLength, RegisterPool.ZERO, arrSize); //$NON-NLS-1$
			emit("", OpCode.cbr, arrSize, validLength, invalidLength); //$NON-NLS-1$
			emit("invalid array size", invalidLength, OpCode.haltI, ERROR_ILLEGAL_ARRAY_SIZE); //$NON-NLS-1$
			emit("valid array size", validLength, OpCode.multI, arrLength, new Num(elementType.getSize()), arrSize); //$NON-NLS-1$

			emit("reserve array properties", OpCode.addI, arrSize, new Num(2 * Machine.INT_SIZE), arrSize); //$NON-NLS-1$

			malloc(addr, arrSize);

			emit("store length", OpCode.storeAI, arrLength, addr, OFFSET_ARRAY_LENGTH); //$NON-NLS-1$
			emit("store offset", OpCode.storeAI, visit(ctx.offset), addr, OFFSET_ARRAY_OFFSET); //$NON-NLS-1$
		}));

	}

	/**
	 * Loads the value on the address pointed to by the variable context.
	 * Because the array length is not stored by itself, it will be calculated
	 * here from the size property.
	 *
	 * @see BoppiBaseVisitor#visitGetVariable(GetVariableContext)
	 */
	@Override
	public Reg visitGetVariable(GetVariableContext ctx) {
		Type type = an.types.get(ctx);
		Reg result = visit(ctx.variable());
		OpCode op = SimpleType.CHAR.equals(an.types.get(ctx)) ? OpCode.cload : OpCode.load;

		emit("load address", op, result, result); //$NON-NLS-1$

		if (type instanceof ReferenceType)
			regPool.blockReg(result, () -> {
				incrementReference(type, result);
			});

		return result;
	}

	@Override
	public Reg visitIf(IfContext ctx) {
		Label toTrue = makeLabel("if_t"); //$NON-NLS-1$
		Label toFalse = makeLabel("if_f"); //$NON-NLS-1$
		Label toEnd = makeLabel("if_e"); //$NON-NLS-1$
		try {
			Reg cond = visit(ctx.cond);

			if (ctx.onFalse == null) {
				emit("", OpCode.cbr, cond, toTrue, toEnd); //$NON-NLS-1$

				emit("", toTrue, OpCode.nop); //$NON-NLS-1$
				visit(ctx.onTrue);

				emit("end target", toEnd, OpCode.nop); //$NON-NLS-1$
				return null;
			} else if (an.types.get(ctx) == SimpleType.VOID) {
				emit("", OpCode.cbr, cond, toTrue, toFalse); //$NON-NLS-1$

				emit("", toTrue, OpCode.nop); //$NON-NLS-1$
				visit(ctx.onTrue);
				emit("", OpCode.jumpI, toEnd); //$NON-NLS-1$

				emit("", toFalse, OpCode.nop); //$NON-NLS-1$
				visit(ctx.onFalse);

				emit("end target", toEnd, OpCode.nop); //$NON-NLS-1$
				return null;
			} else {
				return regPool.withReg((target) -> {
					emit("", OpCode.cbr, cond, toTrue, toFalse); //$NON-NLS-1$

					emit("", toTrue, OpCode.nop); //$NON-NLS-1$
					emit("result", OpCode.i2i, visit(ctx.onTrue), target); //$NON-NLS-1$
					emit("", OpCode.jumpI, toEnd); //$NON-NLS-1$

					emit("", toFalse, OpCode.nop); //$NON-NLS-1$
					emit("result", OpCode.i2i, visit(ctx.onFalse), target); //$NON-NLS-1$

					emit("end target", toEnd, OpCode.nop); //$NON-NLS-1$
				});
			}
		} catch (Throwable e) {
			throw e;
		}
	}

	@Override
	public Reg visitInfix1(Infix1Context ctx) {
		Reg lhs = visit(ctx.lhs);
		Reg rhs = regPool.blockReg(lhs, () -> visit(ctx.rhs));
		emit(ctx.getChild(1).getText(), ops.get(ctx.op.getType()), lhs, rhs, lhs);

		return lhs;
	}

	@Override
	public Reg visitInfix2(Infix2Context ctx) {
		Reg lhs = visit(ctx.lhs);
		Reg rhs = regPool.blockReg(lhs, () -> visit(ctx.rhs));
		emit(ctx.getChild(1).getText(), ops.get(ctx.op.getType()), lhs, rhs, lhs);

		return lhs;
	}

	@Override
	public Reg visitInfix3(Infix3Context ctx) {
		Reg lhs = visit(ctx.lhs);
		Reg rhs = regPool.blockReg(lhs, () -> visit(ctx.rhs));
		Type type = an.types.get(ctx.lhs);
		if (type instanceof SimpleType)
			emit(ctx.getChild(1).getText(), ops.get(ctx.op.getType()), lhs, rhs, lhs);
		else if (type instanceof ArrayType) {
			return regPool.blockReg(lhs, rhs, () -> {
				ArrayType arr = (ArrayType) type;
				Num elementSize = new Num(arr.getType().getSize());

				return regPool.withReg((i, temp1) -> {
					regPool.withReg((temp2) -> {
						Label equalSize = makeLabel("aeqs"), cond = makeLabel("aeqc"), loop = makeLabel("aeql"), //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
								end = makeLabel("aeqe"); //$NON-NLS-1$

						emit("load size lhs", OpCode.loadAI, lhs, OFFSET_REF_SIZE, temp1); //$NON-NLS-1$
						emit("load size rhs", OpCode.loadAI, rhs, OFFSET_REF_SIZE, temp2); //$NON-NLS-1$
						emit("compare array sizes", OpCode.cmp_EQ, temp1, temp2, temp1); //$NON-NLS-1$
						emit("loop if equal", OpCode.cbr, temp1, equalSize, end); //$NON-NLS-1$

						emit("iterate from 0", equalSize, OpCode.loadI, ZERO, i); //$NON-NLS-1$
						emit("equality true", OpCode.loadI, new Num(1), temp1); //$NON-NLS-1$

						emit("load arr size", cond, OpCode.loadAI, lhs, OFFSET_REF_SIZE, temp2); //$NON-NLS-1$
						emit("check i in bounds", OpCode.cmp_LT, i, temp2, temp2); //$NON-NLS-1$
						emit("loop", OpCode.cbr, temp2, loop, end); //$NON-NLS-1$

						emit("load left", loop, OpCode.loadAO, lhs, i, temp1); //$NON-NLS-1$
						emit("load right", OpCode.loadAO, rhs, i, temp2); //$NON-NLS-1$
						emit("increment i", OpCode.addI, i, elementSize, i); //$NON-NLS-1$

						emit("compare elements", OpCode.cmp_EQ, temp1, temp2, temp1); //$NON-NLS-1$
						emit("loop or break", OpCode.cbr, temp1, cond, end); //$NON-NLS-1$

						emit("end equality check", end, OpCode.nop); //$NON-NLS-1$

						if (ops.get(ctx.op.getType()).equals(OpCode.cmp_NE))
							emit("invert equality", OpCode.cmp_EQ, temp1, RegisterPool.ZERO, temp1); //$NON-NLS-1$
					});
				});
			});
		}

		return lhs;
	}

	@Override
	public Reg visitInfix4(Infix4Context ctx) {
		Reg lhs = visit(ctx.lhs);
		Reg rhs = regPool.blockReg(lhs, () -> visit(ctx.rhs));
		emit(ctx.getChild(1).getText(), ops.get(ctx.AND().getSymbol().getType()), lhs, rhs, lhs);

		return lhs;
	}

	@Override
	public Reg visitInfix5(Infix5Context ctx) {
		Reg lhs = visit(ctx.lhs);
		Reg rhs = regPool.blockReg(lhs, () -> visit(ctx.rhs));
		emit(ctx.getChild(1).getText(), ops.get(ctx.OR().getSymbol().getType()), lhs, rhs, lhs);

		return lhs;
	}

	@Override
	public Reg visitLiteralArray(LiteralArrayContext ctx) {
		int count = ctx.expr().size();
		int elementSize = ((ArrayType) an.types.get(ctx)).getType().getSize();

		return regPool.withReg((addr) -> {
			malloc(addr, count * elementSize + 2 * Machine.INT_SIZE);

			regPool.withReg((temp) -> {
				emit("load array length", OpCode.loadI, new Num(count), temp); //$NON-NLS-1$
				emit("store array length", OpCode.storeAI, temp, addr, OFFSET_ARRAY_LENGTH); //$NON-NLS-1$
			});

			emit("store array offset", OpCode.storeAI, RegisterPool.ZERO, addr, OFFSET_ARRAY_OFFSET); //$NON-NLS-1$

			for (int i = 0; i < count; i++) {
				Reg result = visit(ctx.expr(i));
				Num offset = new Num(i * elementSize + 2 * Machine.INT_SIZE);

				if (elementSize == 1)
					emit("store array element", OpCode.cstoreAI, result, addr, offset); //$NON-NLS-1$
				else
					emit("store array element", OpCode.storeAI, result, addr, offset); //$NON-NLS-1$
			}
		});
	}

	@Override
	public Reg visitLiteralBoolean(LiteralBooleanContext ctx) {
		return regPool.withReg((reg) -> {
			emit(ctx.getText(), OpCode.loadI, new Num(ctx.TRUE() != null ? 1 : 0), reg);
		});
	}

	@Override
	public Reg visitLiteralCharacter(LiteralCharacterContext ctx) {
		return regPool.withReg((reg) -> {
			emit(ctx.getText(), OpCode.loadI, new Num(ctx.CHAR().getText().codePointAt(1)), reg);
			emit(ctx.getText(), OpCode.i2c, reg, reg);
		});
	}

	@Override
	public Reg visitLiteralInteger(LiteralIntegerContext ctx) {
		return regPool.withReg((reg) -> {
			int val = 0;
			try {
				val = Integer.parseInt(ctx.LITERAL10().getText());
			} catch (NumberFormatException e) {
				logger.severe(getError(ctx, Messages.getString("BoppiGenerator.5"), ctx.LITERAL10().getText())); //$NON-NLS-1$
			}

			emit(ctx.getText(), OpCode.loadI, new Num(val), reg);
		});
	}

	@Override
	public Reg visitPrefix1(Prefix1Context ctx) {
		Reg reg = visit(ctx.expr());

		switch (ctx.op.getType()) {
		case BoppiLexer.MINUS:
			emit("unary -", OpCode.rsubI, reg, ZERO, reg); //$NON-NLS-1$
			break;

		case BoppiLexer.NOT:
			emit("not", OpCode.xorI, reg, new Num(1), reg); //$NON-NLS-1$
			break;
		}
		return reg;
	}

	@Override
	public Reg visitProgram(ProgramContext ctx) {
		emit("initialise zero register", OpCode.loadI, ZERO, RegisterPool.ZERO); //$NON-NLS-1$

		malloc(arp, an.function.get(ctx).getLocalDataSize() + ARBASESIZE);
		emit("construct main AR", OpCode.addI, arp, new Num(ARBASESIZE), arp); //$NON-NLS-1$
		visitChildren(ctx);

		dereferenceLocalVariables(an.function.get(ctx));

		emit("deconstruct main AR", OpCode.subI, arp, new Num(ARBASESIZE), arp); //$NON-NLS-1$
		regPool.withReg((temp) -> {
			free(temp, arp);
		});

		return null;
	}

	@Override
	public Reg visitRead(ReadContext ctx) {
		Reg res = null;
		for (VariableContext expr : ctx.variable()) {
			visit(expr);

			if (SimpleType.BOOL.equals(an.types.get(expr)) || SimpleType.INT.equals(an.types.get(expr)))
				res = regPool.withReg((temp) -> {
					emit("", OpCode.in, new Str(""), temp); //$NON-NLS-1$ //$NON-NLS-2$
					Reg addr = visit(expr);
					emit("save to var " + expr.getText(), OpCode.store, temp, addr); //$NON-NLS-1$
				});
			else if (SimpleType.CHAR.equals(an.types.get(expr)))
				res = regPool.withReg((temp) -> {
					emit("call stdcin", OpCode.loadI, new Num(prog.size() + 3), temp); //$NON-NLS-1$
					emit("call stdcin", OpCode.push, temp); //$NON-NLS-1$
					emit("call stdcin", OpCode.jumpI, new Label("stdcin")); //$NON-NLS-1$ //$NON-NLS-2$
					emit("get character", OpCode.cpop, temp); //$NON-NLS-1$
					Reg addr = visit(expr);
					emit("save to var " + expr.getText(), OpCode.cstore, temp, addr); //$NON-NLS-1$
				});
			else if (an.types.get(expr) instanceof ArrayType
					&& SimpleType.CHAR.equals(((ArrayType) an.types.get(expr)).getType()))
				res = regPool.withReg((temp) -> {
					emit("call stdsin", OpCode.loadI, new Num(prog.size() + 3), temp); //$NON-NLS-1$
					emit("call stdsin", OpCode.push, temp); //$NON-NLS-1$
					emit("call stdsin", OpCode.jumpI, new Label("stdsin")); //$NON-NLS-1$ //$NON-NLS-2$
					emit("get address", OpCode.pop, temp); //$NON-NLS-1$
					Reg addr = visit(expr);
					emit("save to var " + expr.getText(), OpCode.store, temp, addr); //$NON-NLS-1$
				});
			else
				emit("reading unknown type", OpCode.haltI, ERROR_INPUT_UNKNOWN_TYPE); //$NON-NLS-1$
		}

		return res;
	}

	@Override
	public Reg visitStats(StatsContext ctx) {

		for (int i = 0; i < ctx.stat().size() - 1; i++) {
			ParserRuleContext expr = ctx.stat(i);
			Reg result = visit(expr);

			if (result != null)
				regPool.blockReg(result, () -> {
					Type type = an.types.get(expr);

					if (type instanceof ReferenceType)
						decrementReference(type, result);
				});
		}
		ParserRuleContext last = ctx.stat(ctx.stat().size() - 1);

		return visit(last);
	}

	@Override
	public Reg visitVariableArray(VariableArrayContext ctx) {
		ArrayType type = (ArrayType) an.types.get(ctx.variable());
		Reg addr = visit(ctx.variable());

		emit("get array object", OpCode.load, addr, addr); //$NON-NLS-1$

		regPool.blockReg(addr, () -> {
			Reg offset = visit(ctx.expr());
			regPool.blockReg(offset, () -> {
				regPool.withReg((r1, r2) -> {
					Label outOfBounds = makeLabel("oob"), inBounds = makeLabel("nob"); //$NON-NLS-1$ //$NON-NLS-2$

					emit("load array offset", OpCode.loadAI, addr, OFFSET_ARRAY_OFFSET, r1); //$NON-NLS-1$
					emit("subtract array offset", OpCode.sub, offset, r1, offset); //$NON-NLS-1$
					emit("load array length", OpCode.loadAI, addr, OFFSET_ARRAY_LENGTH, r1); //$NON-NLS-1$
					emit("check array index", OpCode.cmp_LT, offset, r1, r1); //$NON-NLS-1$
					emit("check array index", OpCode.cmp_GE, offset, RegisterPool.ZERO, r2); //$NON-NLS-1$
					emit("check array index", OpCode.and, r1, r2, r2); //$NON-NLS-1$
					emit("check array index", OpCode.cbr, r2, inBounds, outOfBounds); //$NON-NLS-1$
					emit("array index out of bounds", outOfBounds, OpCode.haltI, ERROR_OUT_OF_BOUNDS); //$NON-NLS-1$

					emit("multiply index by size", inBounds, OpCode.multI, offset, new Num(type.getType().getSize()), //$NON-NLS-1$
							offset);
					emit("point to array body", OpCode.addI, offset, new Num(2 * Machine.INT_SIZE), offset); //$NON-NLS-1$
					emit("get array index address", OpCode.add, addr, offset, addr); //$NON-NLS-1$
				});
			});
		});

		return addr;
	}

	@Override
	public Reg visitVariableProperty(VariablePropertyContext ctx) {
		Type innerType = an.types.get(ctx.variable());
		Reg addr = visit(ctx.variable());

		emit("get object address", OpCode.load, addr, addr); //$NON-NLS-1$

		if (innerType instanceof ArrayType) {
			switch (ctx.IDENTIFIER().getText()) {
			case "length": //$NON-NLS-1$
				emit("point to length", OpCode.addI, addr, OFFSET_ARRAY_LENGTH, addr); //$NON-NLS-1$
				break;
			case "offset": //$NON-NLS-1$
				emit("point to offset", OpCode.addI, addr, OFFSET_ARRAY_OFFSET, addr); //$NON-NLS-1$
				break;
			}
		}
		return addr;
	}

	@Override
	public Reg visitVariableSimple(VariableSimpleContext ctx) {
		return regPool.withReg((result) -> {
			Variable<Type> var = an.variables.get(ctx);
			Reg ar = arp;

			int functionDepth = an.function.get(ctx).getFunctionDepth();

			if (var.getDepth() < functionDepth) {
				emit("travelling ALs", OpCode.i2i, ar, tempArp); //$NON-NLS-1$
				for (int i = var.getDepth(); i < functionDepth; i++)
					emit(StringUtils.repeat(' ', i) + "\\", OpCode.loadAI, tempArp, OFFSET_AL, tempArp); //$NON-NLS-1$

				ar = tempArp;
			}

			emit("add offset", OpCode.addI, ar, new Num(var.getOffset()), result); //$NON-NLS-1$
		});
	}

	@Override
	public Reg visitWhile(WhileContext ctx) {
		Label toLoop = makeLabel("while_t"); //$NON-NLS-1$
		Label toCond = makeLabel("while_f"); //$NON-NLS-1$
		Label toEnd = makeLabel("while_e"); //$NON-NLS-1$

		emit("to condition", OpCode.jumpI, toCond); //$NON-NLS-1$
		emit("loop target", toLoop, OpCode.nop); //$NON-NLS-1$
		visit(ctx.onTrue);

		emit("condition target", toCond, OpCode.nop); //$NON-NLS-1$
		emit("", OpCode.cbr, visit(ctx.cond), toLoop, toEnd); //$NON-NLS-1$
		emit("end target", toEnd, OpCode.nop); //$NON-NLS-1$

		return null;
	}

	@Override
	public Reg visitWrite(WriteContext ctx) {
		for (ExprContext expr : ctx.expr()) {
			Reg result = visit(expr);

			regPool.blockReg(result, () -> {
				Type type = an.types.get(expr);

				if (SimpleType.BOOL.equals(type)) {
					regPool.withReg((temp) -> {
						emit("call stdbout", OpCode.loadI, new Num(prog.size() + 4), temp); //$NON-NLS-1$
						emit("call stdbout", OpCode.push, temp); //$NON-NLS-1$
						emit("call stdbout", OpCode.push, result); //$NON-NLS-1$
						emit("call stdbout", OpCode.jumpI, new Label("stdbout")); //$NON-NLS-1$ //$NON-NLS-2$
					});
				} else if (SimpleType.INT.equals(type)) {
					emit("", OpCode.out, new Str(""), result); //$NON-NLS-1$ //$NON-NLS-2$
				} else if (SimpleType.CHAR.equals(type)) {
					regPool.withReg((temp) -> {
						emit("push character", OpCode.cpush, result); //$NON-NLS-1$
						emit("load 1", OpCode.loadI, new Num(1), temp); //$NON-NLS-1$
						emit("push 1", OpCode.push, temp); //$NON-NLS-1$
						emit("print character", OpCode.cout, new Str("")); //$NON-NLS-1$ //$NON-NLS-2$
					});
				} else if (type instanceof ArrayType && SimpleType.CHAR.equals(((ArrayType) type).getType())) {
					regPool.withReg((temp) -> {
						emit("call stdsout", OpCode.loadI, new Num(prog.size() + 4), temp); //$NON-NLS-1$
						emit("call stdsout", OpCode.push, temp); //$NON-NLS-1$
						emit("call stdsout", OpCode.push, result); //$NON-NLS-1$
						emit("call stdsout", OpCode.jumpI, new Label("stdsout")); //$NON-NLS-1$ //$NON-NLS-2$
					});
				} else {
					emit("writing unknown type", OpCode.haltI, ERROR_OUTPUT_UNKNOWN_TYPE); //$NON-NLS-1$
				}

				if (!expr.equals(ctx.expr(ctx.expr().size() - 1)))
					if (type instanceof ReferenceType)
						decrementReference(type, result);
			});

			if (ctx.expr().size() == 1)
				return result;
		}

		return null;
	}
}
