net.team2xh.crt.language.compiler.Compiler.java Source code

Java tutorial

Introduction

Here is the source code for net.team2xh.crt.language.compiler.Compiler.java

Source

/*
 * Copyright (C) 2015 Hamza Haiken <tenchi@team2xh.net>
 *
 * 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 net.team2xh.crt.language.compiler;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import net.team2xh.crt.language.parser.CRTBaseVisitor;
import net.team2xh.crt.language.parser.CRTLexer;
import net.team2xh.crt.language.parser.CRTParser;
import net.team2xh.crt.language.parser.CRTParser.*;
import net.team2xh.crt.raytracer.Background;
import net.team2xh.crt.raytracer.Camera;
import net.team2xh.crt.raytracer.Material;
import net.team2xh.crt.raytracer.lights.Light;
import net.team2xh.crt.raytracer.Pigment;
import net.team2xh.crt.raytracer.Scene;
import net.team2xh.crt.raytracer.Settings;
import net.team2xh.crt.raytracer.entities.Box;
import net.team2xh.crt.raytracer.entities.Entity;
import net.team2xh.crt.raytracer.entities.Plane;
import net.team2xh.crt.raytracer.entities.Sphere;
import net.team2xh.crt.raytracer.entities.csg.Difference;
import net.team2xh.crt.raytracer.entities.csg.Intersection;
import net.team2xh.crt.raytracer.entities.csg.Union;
import net.team2xh.crt.raytracer.lights.ParallelLight;
import net.team2xh.crt.raytracer.lights.PointLight;
import net.team2xh.crt.raytracer.math.Vector3;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;

/**
 *
 * @author Hamza Haiken <tenchi@team2xh.net>
 */
final public class Compiler extends CRTBaseVisitor {

    private final Script script;
    private final Scope scope;
    private final String code;

    private final static String RGB = "rgb";
    private final static String RGBA = "rgba";
    private final static String VEC3 = "vec3";
    private final static String SIN = "sin";
    private final static String COS = "cos";
    private final static String TAN = "tan";
    private final static String RAND = "rand";

    private Random rand;

    private Compiler(String code) {
        this.code = code;
        script = new Script();
        scope = new Scope();
        rand = new Random(1337);
    }

    private static Script compile(String code, Compiler compiler) {
        CRTLexer lexer = new CRTLexer(new ANTLRInputStream(code));
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        CRTParser parser = new CRTParser(tokens);
        ParseTree tree = parser.script();

        compiler.visit(tree);

        return compiler.script;
    }

    public static Script compile(String code) {
        return compile(code, new Variable(new Identifier("t"), 0));
    }

    public static Script compile(String code, Variable v) {
        Compiler compiler = new Compiler(code);
        compiler.scope.add(v);
        return compile(code, compiler);
    }

    @Override
    public Script visitScript(ScriptContext ctx) {
        boolean hasSettings = false;
        boolean hasScene = false;
        for (StatementContext s : ctx.statement()) {
            Object o = s.accept(this);
            Class c = o.getClass();

            if (hasScene)
                throw new CompilerException(ctx, code, "Scene block must at the end of the script");

            if (c == Variable.class) {
                scope.add((Variable) o);
            } else if (c == Settings.class) {
                hasSettings = true;
                script.setSettings((Settings) o);
            } else if (c == Scene.class) {
                hasScene = true;
                script.setScene((Scene) o);
            } else {
                throw new CompilerException(ctx, code,
                        "Top-level statements must be either an assignment or settings/scene block");
            }

        }

        if (!hasSettings)
            throw new CompilerException("Script must define settings block");
        if (!hasScene)
            throw new CompilerException("Script must define a scene");

        script.getSettings().setParent(script.getScene());

        //System.out.println(scope.getVariables());
        System.out.println("Scene compiled (" + script.getScene().toString() + ")");

        return script;
    }

    @Override
    public Settings visitSettings(SettingsContext ctx) {

        Settings settings = script.getSettings();
        Scene scene = script.getScene();

        for (AttributeContext a : ctx.attribute()) {
            ExpressionContext expr = a.expression();
            Object value = resolve(expr);
            switch (a.IDENTIFIER().getText()) {
            case "title":
                assertAttributeType(value, "title", expr, String.class);
                System.out.println("Compiling " + value + "...");
                settings.setTitle((String) value);
                break;
            case "author":
                assertAttributeType(value, "author", expr, String.class);
                settings.setAuthor((String) value);
                break;
            case "date":
                assertAttributeType(value, "date", expr, String.class);
                settings.setDate((String) value);
                break;
            case "notes":
                assertAttributeType(value, "notes", expr, String.class);
                settings.setAuthor((String) value);
                break;
            case "gamma":
                assertAttributeType(value, "gamma", expr, Double.class);
                settings.setGamma((Double) value);
                break;
            case "background":
                assertAttributeType(value, "background", expr, Background.class);
                scene.setBackground((Background) value);
                break;
            case "camera":
                assertAttributeType(value, "camera", expr, Camera.class);
                scene.setCamera((Camera) value);
                break;
            case "supersampling":
                assertAttributeType(value, "supersampling", expr, Integer.class);
                settings.setSupersampling((int) value);
                break;
            case "dofsamples":
                assertAttributeType(value, "dofsamples", expr, Integer.class);
                settings.setDOFSamples((int) value);
                break;
            case "recursiondepth":
                assertAttributeType(value, "recursiondepth", expr, Integer.class);
                settings.setRecursionDepth((int) value);
                break;
            case "lights":
                assertAttributeType(value, "lights", expr, LinkedList.class);
                LinkedList<Object> lights = (LinkedList) value;
                for (Object o : lights) {
                    if (!(o instanceof Light))
                        throw new CompilerException(ctx, code, "Attribute 'light' must be of type Light");
                    scene.addLight((Light) o);
                }
                break;
            }
        }

        return settings;
    }

    private void assertAttributeType(Object object, String name, ParserRuleContext ctx, Class expected) {
        if (object == null) {
            throw new CompilerException(ctx, code, "Attribute '" + name + "' is required.");
        }
        if (object.getClass() != expected)
            throw new CompilerException(ctx, code,
                    "Attribute '" + name + "' must be of type " + expected.getSimpleName());
    }

    @Override
    public Scene visitScene(SceneContext ctx) {
        Scene scene = script.getScene();
        for (ExpressionContext expr : ctx.expression()) {
            Object o = resolve(expr);
            Class c = o.getClass();
            if (c == Variable.class) {
                scope.add((Variable) o);
            } else if (o instanceof Entity) {
                scene.add((Entity) o);
            }
        }
        return scene;
    }

    @Override
    public Object visitObject(ObjectContext ctx) {
        String name = ctx.NAME().getText();

        Map<String, Object> attributes = new HashMap<>();

        for (AttributeContext a : ctx.attribute()) {
            ExpressionContext expr = a.expression();
            Object value = resolve(expr);
            String key = a.IDENTIFIER().getText();
            attributes.put(key, value);
        }

        Object o = null;
        switch (name) {
        case "Material": {
            Object color = attributes.get("color");
            assertAttributeType(color, "color", ctx, Pigment.class);
            o = new Material((Pigment) color);

            Object reflectivity = attributes.get("reflectivity");
            if (reflectivity != null) {
                assertAttributeType(reflectivity, "reflectivity", ctx, Double.class);
                // TODO: reflectivity setter
                o = new Material((Pigment) color, (double) reflectivity);
            }

            break;
        }
        case "Camera": {
            Object position = attributes.get("position");
            Object pointing = attributes.get("pointing");
            Object fov = attributes.get("fov");
            assertAttributeType(position, "position", ctx, Vector3.class);
            assertAttributeType(pointing, "pointing", ctx, Vector3.class);
            assertAttributeType(fov, "fov", ctx, Double.class);
            o = new Camera((Vector3) position, (Vector3) pointing, (double) fov);

            Object aperture = attributes.get("aperture");
            if (aperture != null) {
                assertAttributeType(aperture, "aperture", ctx, Double.class);
                ((Camera) o).setAperture((double) aperture);
            }
            Object apertureShape = attributes.get("apertureshape");
            if (apertureShape != null) {
                assertAttributeType(apertureShape, "apertureshape", ctx, String.class);
                ((Camera) o).setShape(Camera.ApertureShape.valueOf(((String) apertureShape).toUpperCase()));
            }
            Object focaldistance = attributes.get("focaldistance");
            if (focaldistance != null) {
                assertAttributeType(focaldistance, "focaldistance", ctx, Double.class);
                ((Camera) o).setFocalDistance((double) focaldistance);
            }
            break;
        }
        case "ParallelLight": {
            Object from = attributes.get("from");
            Object pointing = attributes.get("pointing");
            Object color = attributes.get("color");
            assertAttributeType(from, "from", ctx, Vector3.class);
            assertAttributeType(pointing, "pointing", ctx, Vector3.class);
            assertAttributeType(color, "color", ctx, Pigment.class);
            o = new ParallelLight((Vector3) from, (Vector3) pointing, (Pigment) color);

            Object ambient = attributes.get("ambient");
            if (ambient != null) {
                assertAttributeType(ambient, "ambient", ctx, Double.class);
                ((ParallelLight) o).setAmbient((double) ambient);
            }
            break;
        }
        case "PointLight": {
            Object origin = attributes.get("origin");
            Object color = attributes.get("color");
            assertAttributeType(origin, "origin", ctx, Vector3.class);
            assertAttributeType(color, "color", ctx, Pigment.class);
            o = new PointLight((Vector3) origin, (Pigment) color);

            Object ambient = attributes.get("ambient");
            if (ambient != null) {
                assertAttributeType(ambient, "ambient", ctx, Double.class);
                ((PointLight) o).setAmbient((double) ambient);
            }
            Object falloff = attributes.get("falloff");
            if (falloff != null) {
                assertAttributeType(falloff, "falloff", ctx, Double.class);
                ((PointLight) o).setFalloff((double) falloff);
            }
            break;
        }
        case "Sphere": {
            Object center = attributes.get("center");
            Object radius = attributes.get("radius");
            Object material = attributes.get("material");
            assertAttributeType(center, "center", ctx, Vector3.class);
            assertAttributeType(radius, "radius", ctx, Double.class);
            assertAttributeType(material, "material", ctx, Material.class);
            o = new Sphere((Vector3) center, (double) radius, (Material) material);
            break;
        }
        case "Box": {
            Object cornerA = attributes.get("cornerA");
            Object cornerB = attributes.get("cornerB");
            Object material = attributes.get("material");
            assertAttributeType(cornerA, "cornerA", ctx, Vector3.class);
            assertAttributeType(cornerB, "cornerB", ctx, Vector3.class);
            assertAttributeType(material, "material", ctx, Material.class);
            o = new Box((Vector3) cornerA, (Vector3) cornerB, (Material) material);
            break;
        }
        case "Plane": {
            Object normal = attributes.get("normal");
            Object position = attributes.get("position");
            Object material = attributes.get("material");
            assertAttributeType(normal, "normal", ctx, Vector3.class);
            assertAttributeType(position, "position", ctx, Vector3.class);
            assertAttributeType(material, "material", ctx, Material.class);
            o = new Plane((Vector3) normal, (Vector3) position, (Material) material);
            break;
        }
        case "Background": {
            Object color = attributes.get("color");
            Object image = attributes.get("image");
            Object horizon = attributes.get("horizon");
            Object sky = attributes.get("sky");
            Object angle = attributes.get("angle");

            if (image != null) {
                assertAttributeType(image, "image", ctx, String.class);
                if (angle != null) {
                    assertAttributeType(angle, "angle", ctx, Double.class);
                    o = new Background((String) image, (double) angle);
                } else {
                    o = new Background((String) image);
                }
            } else if (horizon != null && sky != null) {
                assertAttributeType(horizon, "horizon", ctx, Pigment.class);
                assertAttributeType(sky, "sky", ctx, Pigment.class);
                o = new Background((Pigment) horizon, (Pigment) sky);
            } else {
                assertAttributeType(color, "color", ctx, Pigment.class);
                o = new Background((Pigment) color);
            }
            break;
        }
        }
        return o;
    }

    @Override
    public Variable visitAssignment(AssignmentContext ctx) {
        Object left = ctx.expression(0).accept(this);
        Object right = ctx.expression(1).accept(this);

        if (left.getClass() != Identifier.class) {
            throw new CompilerException(ctx, code, "Left-hand side of assignment must be an identifier");
        }

        Identifier name = (Identifier) left;

        return new Variable(name, right);
    }

    @Override
    public Identifier visitIdentifierPrimary(IdentifierPrimaryContext ctx) {
        return new Identifier(ctx.getText());
    }

    @Override
    public Integer visitIntegerLiteral(IntegerLiteralContext ctx) {
        return Integer.parseInt(ctx.getText());
    }

    @Override
    public Double visitFloatLiteral(FloatLiteralContext ctx) {
        return Double.parseDouble(ctx.getText());

    }

    @Override
    public String visitStringLiteral(StringLiteralContext ctx) {
        String str = ctx.getText();
        // Unescape characters
        // http://stackoverflow.com/a/7847310
        str = StringEscapeUtils.unescapeJava(str);
        return str.substring(1, str.length() - 1);
    }

    @Override
    public Boolean visitBooleanLiteral(BooleanLiteralContext ctx) {
        return Boolean.parseBoolean(ctx.getText());
    }

    @Override
    public List<Object> visitList(ListContext ctx) {
        return visitExpressionList(ctx.expressionList());
    }

    @Override
    public List<Object> visitExpressionList(ExpressionListContext ctx) {
        List<ExpressionContext> exprs = new LinkedList<>();
        if (ctx != null)
            exprs = ctx.expression();
        List<Object> result = new LinkedList<>();
        for (int i = 0; i < exprs.size(); ++i) {
            result.add(resolve(exprs.get(i)));
        }
        return result;
    }

    @Override
    public Object visitCall(CallContext ctx) {
        Object left = ctx.expression().accept(this);
        List<Object> arguments = visitExpressionList(ctx.expressionList());

        if (left.getClass() != Identifier.class)
            throw new CompilerException(ctx, code, "'" + left + "' muts be an identifier");

        Identifier name = (Identifier) left;
        double[] args;
        switch (name.getName()) {
        case RGB:
            args = checkArguments(arguments, RGB, 3, ctx);
            return new Pigment(args[0], args[1], args[2]);
        case RGBA:
            args = checkArguments(arguments, RGBA, 4, ctx);
            return new Pigment(args[0], args[1], args[2], args[3]);
        case VEC3:
            args = checkArguments(arguments, VEC3, 3, ctx);
            return new Vector3(args[0], args[1], args[2]);
        case SIN:
            args = checkArguments(arguments, SIN, 1, ctx);
            return Math.sin(args[0]);
        case COS:
            args = checkArguments(arguments, COS, 1, ctx);
            return Math.cos(args[0]);
        case TAN:
            args = checkArguments(arguments, TAN, 1, ctx);
            return Math.tan(args[0]);
        case RAND:
            args = checkArguments(arguments, RAND, 2, ctx);
            Double r = rand.nextDouble() * Math.abs(args[0] - args[1]) + Math.min(args[0], args[1]);
            return r;
        default:
            // TODO: macro call
            return null;
        }
    }

    private double[] checkArguments(List<Object> arguments, String name, int n, ParserRuleContext ctx) {
        String ex = "'" + name + "' takes " + n + " float arguments";
        if (arguments.size() != n)
            throw new CompilerException(ctx, code, ex);

        double[] args = new double[n];
        for (int i = 0; i < arguments.size(); ++i) {
            if (arguments.get(i).getClass() != Double.class)
                throw new CompilerException(ctx, code, ex);
            args[i] = (Double) arguments.get(i);
        }

        return args;
    }

    @Override
    public Object visitListAccess(ListAccessContext ctx) {
        Object left = resolve(ctx.expression(0));
        Object right = resolve(ctx.expression(1));

        if (left.getClass() != LinkedList.class)
            throw new CompilerException(ctx, code, "'" + left + "' is not a list");

        if (right.getClass() != Integer.class)
            throw new CompilerException(ctx, code, "List index must be an integer");

        List<Object> list = (LinkedList) left;
        Integer index = (Integer) right;

        if (index >= list.size())
            throw new CompilerException(ctx, code, "List index out of range");

        return list.get(index);
    }

    @Override
    public Object visitUnarySign(UnarySignContext ctx) {
        Object operand = resolve(ctx.expression());
        String sign = ctx.getChild(0).getText();

        if (operand.getClass() == Double.class) {
            switch (sign) {
            case "+":
                return ((Double) operand);
            case "-":
                return -((Double) operand);
            }
        }

        if (operand.getClass() == Integer.class) {
            switch (sign) {
            case "+":
                return ((Integer) operand);
            case "-":
                return -((Integer) operand);
            }
        }

        throw new CompilerException(ctx, code,
                "Unsupported type for unary operator '" + sign + "': " + operand.getClass().getSimpleName());
    }

    @Override
    public Boolean visitUnaryNot(UnaryNotContext ctx) {
        Object operand = resolve(ctx.expression());

        if (operand.getClass() == Boolean.class) {
            return !((Boolean) operand);
        }

        throw new CompilerException(ctx, code,
                "Unsupported type for unary operator '!': " + operand.getClass().getSimpleName());
    }

    @Override
    public Object visitMultiplication(MultiplicationContext ctx) {
        Object left = resolve(ctx.expression(0));
        Object right = resolve(ctx.expression(1));
        String operator = ctx.getChild(1).getText();

        Class l = left.getClass();
        Class r = right.getClass();
        Class d = Double.class;
        Class i = Integer.class;
        Class s = String.class;

        // Integer multiplication
        if (l == i && r == i) {
            Integer x = (Integer) left, y = (Integer) right;
            switch (operator) {
            case "*":
                return x * y;
            case "/":
                return x / y;
            case "%":
                return x % y;
            }
        }

        // Double multiplication
        if ((l == i || l == d) && (r == i || r == d)) {
            Double x = (l == i) ? ((Integer) left).doubleValue() : (Double) left;
            Double y = (r == i) ? ((Integer) right).doubleValue() : (Double) right;
            switch (operator) {
            case "*":
                return x * y;
            case "/":
                return x / y;
            case "%":
                return x % y;
            }
        }

        // String repetition
        if (l == s && r == i || l == i && r == s) {
            if (operator.equals("*")) {
                if (l == s)
                    return StringUtils.repeat((String) left, (Integer) right);

                return StringUtils.repeat((String) right, (Integer) left);
            }
        }

        throw new CompilerException(ctx, code, "Unsupported types for binary operator '" + operator + "': "
                + l.getSimpleName() + ", " + r.getSimpleName());
    }

    @Override
    public Object visitAddition(AdditionContext ctx) {
        Object left = resolve(ctx.expression(0));
        Object right = resolve(ctx.expression(1));
        String operator = ctx.getChild(1).getText();

        Class l = left.getClass();
        Class r = right.getClass();
        Class d = Double.class;
        Class i = Integer.class;
        Class s = String.class;
        Class b = Boolean.class;

        // Integer addition
        if (l == i && r == i) {
            Integer x = (Integer) left, y = (Integer) right;
            switch (operator) {
            case "+":
                return x + y;
            case "-":
                return x - y;
            }
        }

        // Double addition
        if ((l == i || l == d) && (r == i || r == d)) {
            Double x = (l == i) ? ((Integer) left).doubleValue() : (Double) left;
            Double y = (r == i) ? ((Integer) right).doubleValue() : (Double) right;
            switch (operator) {
            case "+":
                return x + y;
            case "-":
                return x - y;
            }
        }

        // String concatenation
        if (l == s) {
            if (operator.equals("+")) {
                if (r == i)
                    return (String) left + (Integer) right;
                if (r == d)
                    return (String) left + (Double) right;
                if (r == b)
                    return (String) left + (Boolean) right;
                if (r == s)
                    return (String) left + (String) right;
            }
        }
        if (r == s) {
            if (operator.equals("+")) {
                if (l == i)
                    return (Integer) left + (String) right;
                if (l == d)
                    return (Double) left + (String) right;
                if (l == b)
                    return (Boolean) left + (String) right;
            }
        }

        // Entity CSG
        if (left instanceof Entity && right instanceof Entity) {
            Entity x = (Entity) left, y = (Entity) right;
            switch (operator) {
            case "+":
                return new Union(x, y);
            case "-":
                return new Difference(x, y);
            case "^":
                return new Intersection(x, y);
            }
        }

        throw new CompilerException(ctx, code, "Unsupported types for binary operator '" + operator + "': "
                + l.getSimpleName() + ", " + r.getSimpleName());
    }

    @Override
    public Boolean visitComparison(ComparisonContext ctx) {
        Object left = resolve(ctx.expression(0));
        Object right = resolve(ctx.expression(1));
        String operator = ctx.getChild(1).getText();

        Class l = left.getClass();
        Class r = right.getClass();
        Class d = Double.class;
        Class i = Integer.class;

        // Integer comparison
        if (l == i && r == i) {
            Integer x = (Integer) left, y = (Integer) right;
            switch (operator) {
            case "<=":
                return x <= y;
            case ">=":
                return x >= y;
            case "<":
                return x < y;
            case ">":
                return x > y;
            }
        }

        // Double comparison
        if ((l == i || l == d) && (r == i || r == d)) {
            Double x = (l == i) ? ((Integer) left).doubleValue() : (Double) left;
            Double y = (r == i) ? ((Integer) right).doubleValue() : (Double) right;
            switch (operator) {
            case "<=":
                return x <= y;
            case ">=":
                return x >= y;
            case "<":
                return x < y;
            case ">":
                return x > y;
            }
        }

        // Other
        if (operator.equals("==")) {
            return left.equals(right);
        }
        if (operator.equals("!=")) {
            return !(left.equals(right));
        }

        throw new CompilerException(ctx, code, "Unsupported types for binary operator '" + operator + "': "
                + l.getSimpleName() + ", " + r.getSimpleName());
    }

    @Override
    public Boolean visitBinaryAnd(BinaryAndContext ctx) {
        Object left = resolve(ctx.expression(0));
        Object right = resolve(ctx.expression(1));
        String operator = ctx.getChild(1).getText();

        Class l = left.getClass();
        Class r = right.getClass();
        Class b = Boolean.class;

        if (l == b && r == b) {
            Boolean x = (Boolean) left, y = (Boolean) right;
            return x && y;
        }

        throw new CompilerException(ctx, code, "Unsupported types for binary operator '" + operator + "': "
                + l.getSimpleName() + ", " + r.getSimpleName());
    }

    @Override
    public Boolean visitBinaryOr(BinaryOrContext ctx) {
        Object left = resolve(ctx.expression(0));
        Object right = resolve(ctx.expression(1));
        String operator = ctx.getChild(1).getText();

        Class l = left.getClass();
        Class r = right.getClass();
        Class b = Boolean.class;

        if (l == b && r == b) {
            Boolean x = (Boolean) left, y = (Boolean) right;
            return x || y;
        }

        throw new CompilerException(ctx, code, "Unsupported types for binary operator '" + operator + "': "
                + l.getSimpleName() + ", " + r.getSimpleName());
    }

    @Override
    public Object visitTernary(TernaryContext ctx) {
        Object condition = resolve(ctx.expression(0));
        Object left = resolve(ctx.expression(1));
        Object right = resolve(ctx.expression(2));

        Class c = condition.getClass();
        Class l = left.getClass();
        Class r = right.getClass();
        Class b = Boolean.class;

        if (c != b)
            throw new CompilerException(ctx, code, "Ternary condition must be a Boolean");

        if (l == r) {
            return (Boolean) condition ? left : right;
        }

        throw new CompilerException(ctx, code,
                "Ternary results must be of the same type: " + l.getSimpleName() + ", " + r.getSimpleName());
    }

    @Override
    public Object visitPrimary(PrimaryContext ctx) {
        // ( expr )
        if (ctx.getChildCount() == 3)
            return ctx.getChild(1).accept(this);
        // literal or identifier
        return ctx.getChild(0).accept(this);
    }

    /**
     * Returns the value of an expression,
     * and if it is an identifier, fetches its value from the scope.
     *
     * @param expr Expression to resolve
     * @return Value of variable or evaluated expression
     */
    private Object resolve(ExpressionContext expr) {
        Object result = expr.accept(this);
        if (result.getClass() == Identifier.class) {
            result = scope.get((Identifier) result).getValue();
        }
        return result;
    }

}