/* * $Id$ * * Copyright (C) 2003-2015 JNode.org * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library 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 Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; If not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package org.jnode.shell.bjorne; import java.util.ArrayDeque; import java.util.Arrays; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import org.jnode.shell.ShellException; import org.jnode.shell.ShellFailureException; import org.jnode.shell.ShellSyntaxException; /** * This class parses and evaluates the bjorne shell's arithmetic expression sublanguage. * * @author crawley@jnode.org */ public class BjorneArithmeticEvaluator { private static final int NONE = 1; private static final int PERCENT = 2; private static final int MINUS = 3; private static final int PLUS = 4; private static final int STAR = 5; private static final int SLASH = 6; private static final int PLUSPLUS = 7; private static final int MINUSMINUS = 8; private static final int PLING = 9; private static final int TWIDDLE = 10; private static final int STARSTAR = 11; private static final int PREFIX = 16; private static final HashMap<Integer, Integer> precedence = new HashMap<Integer, Integer>(); private static final HashSet<Integer> unaryOps; static { precedence.put(PLUSPLUS, 1); precedence.put(MINUSMINUS, 1); precedence.put(PLUSPLUS + PREFIX, 2); precedence.put(MINUSMINUS + PREFIX, 2); precedence.put(PLUS + PREFIX, 3); precedence.put(MINUS + PREFIX, 3); precedence.put(PLING + PREFIX, 4); precedence.put(TWIDDLE + PREFIX, 4); precedence.put(STARSTAR, 5); precedence.put(STAR, 6); precedence.put(SLASH, 6); precedence.put(PERCENT, 6); precedence.put(PLUS, 7); precedence.put(MINUS, 7); unaryOps = new HashSet<Integer>(Arrays.asList(new Integer[]{ PLUS + PREFIX, PLUSPLUS, PLUSPLUS + PREFIX, MINUS + PREFIX, MINUSMINUS, MINUSMINUS + PREFIX})); }; private class Primary { private final String name; private final long value; public Primary(String name, long value) { super(); this.name = name; this.value = value; } public String getName() { return name; } public long getValue() throws ShellSyntaxException { return name != null ? evalName(name) : value; } @Override public String toString() { try { return Long.toString(getValue()); } catch (ShellException ex) { return "OOPS"; } } } private final BjorneContext context; private final Deque<Integer> opStack = new ArrayDeque<Integer>(); private final Deque<Primary> valStack = new ArrayDeque<Primary>(); public BjorneArithmeticEvaluator(BjorneContext context) { super(); this.context = context; } protected synchronized String evaluateExpression(CharSequence source) throws ShellException { opStack.clear(); valStack.clear(); CharIterator ci = new CharIterator(source); Primary res = evalExpression(ci); return Long.toString(res.getValue()); } private Primary evalExpression(CharIterator ci) throws ShellException { int mark = opStack.size(); int ch = skipWhiteSpace(ci); while ((ch = skipWhiteSpace(ci)) != -1 && ch != ')') { int prefixOp = parseExpressionOperator(ci); switch (prefixOp) { case NONE: break; case PLUS: case MINUS: case PLUSPLUS: case MINUSMINUS: prefixOp += PREFIX; break; default: throw new ShellSyntaxException("Unexpected infix operator"); } skipWhiteSpace(ci); pushOperand(evalPrimary(ci)); if (prefixOp != NONE) { pushOperator(prefixOp, mark); } skipWhiteSpace(ci); int op = parseExpressionOperator(ci); if (op == PLUSPLUS || op == MINUSMINUS) { pushOperator(op, mark); skipWhiteSpace(ci); op = parseExpressionOperator(ci); } ch = skipWhiteSpace(ci); if (op == NONE) { if (ch != -1 && ch != ')') { throw new ShellSyntaxException("Expected an infix operator in expression"); } break; } else if (op == PLUSPLUS || op == MINUSMINUS) { throw new ShellSyntaxException("Expected an infix operator in expression"); } else if (ch == ')') { throw new ShellSyntaxException("Expected a number or variable name in expression"); } pushOperator(op, mark); } if (valStack.size() == 0) { throw new ShellSyntaxException("No expression within \"$((...))\""); } while (opStack.size() > mark) { evalOperation(); } return valStack.removeFirst(); } private void pushOperator(int op, int mark) throws ShellException { while (opStack.size() > mark && precedence.get(opStack.getFirst()) <= precedence.get(op)) { evalOperation(); } opStack.addFirst(op); } private void pushOperand(Primary operand) { valStack.addFirst(operand); } private void evalOperation() throws ShellException { Integer op = opStack.removeFirst(); Primary operand1; Primary operand2; if (unaryOps.contains(op)) { operand1 = valStack.removeFirst(); operand2 = null; } else { operand2 = valStack.removeFirst(); operand1 = valStack.removeFirst(); } long value; Primary res; switch (op) { case PLUS + PREFIX: res = new Primary(null, operand1.getValue()); break; case MINUS + PREFIX: res = new Primary(null, -operand1.getValue()); break; case PLUSPLUS + PREFIX: case MINUSMINUS + PREFIX: if (operand1.name == null) { throw new ShellSyntaxException("Cannot apply ++ or -- to a number or a subexpression"); } value = evalName(operand1.name) + (op == PLUSPLUS + PREFIX ? 1 : -1); context.setVariable(operand1.name, Long.toString(value)); res = new Primary(null, value); break; case PLUSPLUS: case MINUSMINUS: if (operand1.name == null) { throw new ShellSyntaxException("Cannot apply ++ or -- to a number or a subexpression"); } value = evalName(operand1.name); context.setVariable(operand1.name, Long.toString(value + (op == PLUSPLUS ? 1 : -1))); res = new Primary(null, value); break; case PLUS: res = new Primary(null, operand1.getValue() + operand2.getValue()); break; case MINUS: res = new Primary(null, operand1.getValue() - operand2.getValue()); break; case STAR: res = new Primary(null, operand1.getValue() * operand2.getValue()); break; case STARSTAR: res = new Primary(null, Math.round(Math.pow(operand1.getValue(), operand2.getValue()))); break; case SLASH: value = operand2.getValue(); if (value == 0) { throw new ShellException("Divide by zero in expression"); } res = new Primary(null, operand1.getValue() / value); break; case PERCENT: value = operand2.getValue(); if (value == 0) { throw new ShellException("Remainder by zero in expression"); } res = new Primary(null, operand1.getValue() % value); break; default: throw new ShellFailureException("operator not supported"); } valStack.addFirst(res); } private Primary evalPrimary(CharIterator ci) throws ShellException { int ch = ci.peekCh(); if (Character.isLetter(ch) || ch == '_') { return new Primary(context.parseParameter(ci), 0L); } else if (Character.isDigit(ch)) { return new Primary(null, parseNumber(ci)); } else if (ch == '(') { ci.nextCh(); Primary res = evalExpression(ci); skipWhiteSpace(ci); if ((ch = ci.nextCh()) != ')') { throw new ShellSyntaxException("Unmatched \"(\" (left parenthesis) in arithmetic expression"); } return res; } else { throw new ShellSyntaxException("Expected a number or variable name"); } } private long evalName(String name) throws ShellSyntaxException { try { String value = context.variable(name); return value == null ? 0L : Long.parseLong(value); } catch (NumberFormatException ex) { throw new ShellSyntaxException( "expression syntax error: '" + context.variable(name) + "' is not an integer"); } } private int skipWhiteSpace(CharIterator ci) { int ch = ci.peekCh(); while (ch == ' ' || ch == '\t' || ch == '\n') { ci.nextCh(); ch = ci.peekCh(); } return ch; } private int parseExpressionOperator(CharIterator ci) throws ShellSyntaxException { int ch = ci.peekCh(); switch (ch) { case '+': ci.nextCh(); if (ci.peekCh() == '+') { ci.nextCh(); return PLUSPLUS; } else { return PLUS; } case '-': ci.nextCh(); if (ci.peekCh() == '-') { ci.nextCh(); return MINUSMINUS; } else { return MINUS; } case '/': ci.nextCh(); return SLASH; case '*': ci.nextCh(); if (ci.peekCh() == '*') { ci.nextCh(); return STARSTAR; } else { return STAR; } case '%': ci.nextCh(); return PERCENT; default: return NONE; } } private long parseNumber(CharIterator ci) { StringBuilder sb = new StringBuilder(); int ch = ci.peekCh(); while (ch != -1 && Character.isDigit((char) ch)) { ci.nextCh(); sb.append((char) ch); ch = ci.peekCh(); } return Long.parseLong(sb.toString()); } }