com.github.rgcjonas.kuemmelgtr.core.Compiler.java Source code

Java tutorial

Introduction

Here is the source code for com.github.rgcjonas.kuemmelgtr.core.Compiler.java

Source

/**
 * KuemmelGtr - A graphing calculator
 * Copyright (C) 2013 Jonas Kmmerlin <rgcjonas@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.github.rgcjonas.kuemmelgtr.core;

import java.lang.reflect.Method;

import org.objectweb.asm.*;

/**
 * The Compiler class will compile a stream of RPNTokens into a java method, and create a class.
 */
public class Compiler {
    public static byte[] compileFunc(TokenSource<RPNToken> source, String variable) throws ParsingError {
        // initialize class
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        writer.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC, "com/github/rgcjonas/kuemmelgtr/jit/BogusName", null,
                CompiledClassBase.class.getName().replace('.', '/'),
                new String[] { Evaluator.Function.class.getName().replace('.', '/') });

        // create constructor
        MethodVisitor construct = writer.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        construct.visitCode();
        construct.visitVarInsn(Opcodes.ALOAD, 0);
        construct.visitMethodInsn(Opcodes.INVOKESPECIAL, CompiledClassBase.class.getName().replace('.', '/'),
                "<init>", "()V");
        construct.visitInsn(Opcodes.RETURN);
        construct.visitMaxs(0, 0);
        construct.visitEnd();

        MethodVisitor method = writer.visitMethod(Opcodes.ACC_PUBLIC, "evaluate", "(D)D", null,
                new String[] { "com/github/rgcjonas/kuemmelgtr/core/RuntimeError" });

        compileCommon(source, method, variable);

        writer.visitEnd();
        return writer.toByteArray();
    }

    public static byte[] compileBasic(TokenSource<RPNToken> source) throws ParsingError {
        // initialize class
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

        writer.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC, "com/github/rgcjonas/kuemmelgtr/jit/BogusName", null,
                CompiledClassBase.class.getName().replace('.', '/'),
                new String[] { Evaluator.Basic.class.getName().replace('.', '/') });

        // create constructor
        MethodVisitor construct = writer.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        construct.visitCode();
        construct.visitVarInsn(Opcodes.ALOAD, 0);
        construct.visitMethodInsn(Opcodes.INVOKESPECIAL, CompiledClassBase.class.getName().replace('.', '/'),
                "<init>", "()V");
        construct.visitInsn(Opcodes.RETURN);
        construct.visitMaxs(0, 0);
        construct.visitEnd();

        MethodVisitor method = writer.visitMethod(Opcodes.ACC_PUBLIC, "evaluate", "()D", null,
                new String[] { "com/github/rgcjonas/kuemmelgtr/core/ParsingError",
                        "com/github/rgcjonas/kuemmelgtr/core/RuntimeError" });

        compileCommon(source, method, null);

        writer.visitEnd();
        return writer.toByteArray();
    }

    private static void compileCommon(TokenSource<RPNToken> source, MethodVisitor method, String variable)
            throws ParsingError {
        method.visitCode();

        // we now can compile our token stream
        int currentStackSize = 0; // we keep track of the current stack size to throw errors if we encounter an invalid instruction
        RPNToken t;
        while ((t = source.nextToken()) != null) {
            if (t instanceof RPNToken.Operand) {
                // we push it onto the stack
                method.visitLdcInsn(((RPNToken.Operand) t).getValue());
                currentStackSize++;
            } else if (t instanceof RPNToken.Operator) {
                RPNToken.Operator op = (RPNToken.Operator) t;

                if (currentStackSize < op.getNumOperands())
                    throw new ParsingError(t.getSrcLine(), t.getSrcColumn(), "Missing operand(s)");

                switch (op.getName()) {
                case "_add":
                    method.visitInsn(Opcodes.DADD);
                    currentStackSize--;
                    break;
                case "_mult":
                    method.visitInsn(Opcodes.DMUL);
                    currentStackSize--;
                    break;
                case "_sub":
                    method.visitInsn(Opcodes.DSUB);
                    currentStackSize--;
                    break;
                case "_div":
                    method.visitInsn(Opcodes.DDIV);
                    currentStackSize--;
                    break;
                case "_pow":
                    method.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Math", "pow", "(DD)D");
                    currentStackSize--;
                    break;
                default:
                    //HACK: support every function in java/lang/Math. Will be way more performant than any lookup-and-evaluate
                    //TODO: implement more functions inline
                    if (currentStackSize < 1)
                        throw new ParsingError(t.getSrcLine(), t.getSrcColumn(), "Missing operand");
                    try {
                        Method meth = Math.class.getDeclaredMethod(op.getName(), double.class); // just check if it's available and hope it returns a double
                        if (meth.getReturnType() != double.class)
                            throw new NoSuchMethodException(); // we don't want to blow up at runtime, do we?

                        method.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Math", op.getName(), "(D)D");
                    } catch (NoSuchMethodException e) {
                        // we do not give up. The method may be available at runtime.
                        method.visitVarInsn(Opcodes.ALOAD, 0);
                        method.visitInsn(Opcodes.DUP_X2); // swap the this pointer and double
                        method.visitInsn(Opcodes.POP);
                        method.visitLdcInsn(op.getName());
                        method.visitLdcInsn((int) op.getSrcLine());
                        method.visitLdcInsn((int) op.getSrcColumn());
                        method.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                                CompiledClassBase.class.getName().replace('.', '/'), "resolveAndEvaluateFunction",
                                "(DLjava/lang/String;II)D");
                    }
                }
            } else if (t instanceof RPNToken.VariableRecall) {
                // maybe, maybe the variable is an argument we can load
                if (variable != null && ((RPNToken.VariableRecall) t).getName().equalsIgnoreCase(variable)) {
                    method.visitVarInsn(Opcodes.DLOAD, 1);
                } else {
                    // we let our parent class do the hard work
                    method.visitVarInsn(Opcodes.ALOAD, 0);
                    method.visitLdcInsn(((RPNToken.VariableRecall) t).getName());
                    method.visitLdcInsn(t.getSrcLine());
                    method.visitLdcInsn(t.getSrcColumn());
                    method.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
                            CompiledClassBase.class.getName().replace('.', '/'), "getVariable",
                            "(Ljava/lang/String;II)D");
                }
                currentStackSize++;
            } else if (t instanceof RPNToken.VariableAssignment) {
                // also defer to our parent class
                // we do not give up. The method may be available at runtime.
                method.visitVarInsn(Opcodes.ALOAD, 0);
                method.visitInsn(Opcodes.DUP_X2); // swap the this pointer and double
                method.visitInsn(Opcodes.POP);
                method.visitLdcInsn(((RPNToken.VariableAssignment) t).getVariableName());
                method.visitLdcInsn(t.getSrcLine());
                method.visitLdcInsn(t.getSrcColumn());
                method.visitMethodInsn(Opcodes.INVOKEVIRTUAL, CompiledClassBase.class.getName().replace('.', '/'),
                        "setVariable", "(DLjava/lang/String;II)D");
            } else {
                throw new ParsingError(t.getSrcLine(), t.getSrcColumn(), "Unknown instruction: " + t);
            }
        }

        if (currentStackSize != 1)
            throw new ParsingError(0, 0, "Expected stack to be one value, found " + currentStackSize);

        method.visitInsn(Opcodes.DRETURN);

        method.visitMaxs(0, 0);
        method.visitEnd();
    }

    /**
     * Base class for compiled expressions
     */
    public static abstract class CompiledClassBase {
        private FunctionResolver resolver = null;
        private VariableStore vars = null;

        public void initialize(FunctionResolver resolver, VariableStore vars) {
            this.resolver = resolver;
            this.vars = vars;
        }

        protected double resolveAndEvaluateFunction(double argument, String name, int srcLine, int srcCol)
                throws RuntimeError {
            Evaluator.Function func = resolver.resolveFunc(name);
            if (func != null) {
                return func.evaluate(argument);
            } else {
                throw new RuntimeError(srcLine, srcCol, "Unknown function: " + name);
            }
        }

        protected double getVariable(String name, int srcLine, int srcCol) throws RuntimeError {
            return vars.get(name, srcLine, srcCol);
        }

        protected double setVariable(double value, String name, int srcLine, int srcCol) throws RuntimeError {
            return vars.set(name, value, srcLine, srcCol);
        }
    }

    /**
     * instantiate a compiled and loaded class
     * @param vars 
     * @throws ParsingError 
     */
    public static Object instantiate(Class<?> klass, FunctionResolver resolver, VariableStore vars)
            throws ParsingError {
        try {
            CompiledClassBase k = (CompiledClassBase) klass.newInstance();
            k.initialize(resolver, vars);
            return k;
        } catch (Exception e) {
            e.printStackTrace();
            throw new ParsingError(-1, -1, "Internal error: couldn't instantiate class");
        }
    }
}