VeLaInterpreter.java
/**
* VStar: a statistical analysis tool for variable star data.
* Copyright (C) 2010 AAVSO (http://www.aavso.org/)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.aavso.tools.vstar.vela;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.aavso.tools.vstar.scripting.VStarScriptingAPI;
import org.aavso.tools.vstar.ui.VStar;
import org.aavso.tools.vstar.ui.resources.ResourceAccessor;
import org.aavso.tools.vstar.util.Pair;
import org.aavso.tools.vstar.util.date.AbstractDateUtil;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.ConsoleErrorListener;
import org.antlr.v4.runtime.ParserRuleContext;
/**
* VeLa: VStar expression Language interpreter
*/
public class VeLaInterpreter {
private boolean verbose;
private List<File> sourceDirectories;
private Stack<Operand> stack;
private Stack<VeLaEnvironment<Operand>> environments;
// AST and result caches.
private static Map<String, AST> exprToAST = new HashMap<String, AST>();
// Regular expression pattern cache.
private static Map<String, Pattern> regexPatterns = new HashMap<String, Pattern>();
private static List<FunctionExecutor> javaClassFunctionExecutors = null;
private VeLaErrorListener errorListener;
/**
* Construct a VeLa interpreter with an initial scope and intrinsic functions.
*
* @param verbose Verbose mode?
* @param addVStarAPI Add the VStar API?
* @param sourceDirectories A list of source directories containing VeLa source
* files (ending in ".vl" or ".vela") to be loaded.
*/
public VeLaInterpreter(boolean verbose, boolean addVStarAPI, List<File> sourceDirectories) {
this.verbose = verbose;
this.sourceDirectories = sourceDirectories;
errorListener = new VeLaErrorListener();
stack = new Stack<Operand>();
environments = new Stack<VeLaEnvironment<Operand>>();
environments.push(new VeLaScope());
// Collect functions from reflection over Java classes
Set<Class<?>> permittedTypes = new HashSet<Class<?>>();
permittedTypes.add(int.class);
permittedTypes.add(double.class);
permittedTypes.add(boolean.class);
permittedTypes.add(String.class);
permittedTypes.add(CharSequence.class);
permittedTypes.add(void.class);
permittedTypes.add(Type.DBL_ARR.getClass());
permittedTypes.add(Type.DBL_CLASS_ARR.getClass());
if (addVStarAPI) {
// TODO: really needed here?
permittedTypes.add(VStarScriptingAPI.class);
}
if (javaClassFunctionExecutors == null) {
javaClassFunctionExecutors = new ArrayList<FunctionExecutor>();
addFunctionExecutorsFromClass(Math.class, null, permittedTypes, Collections.emptySet());
addFunctionExecutorsFromClass(String.class, null, permittedTypes,
new HashSet<String>(Arrays.asList("JOIN", "FORMAT")));
if (addVStarAPI) {
addFunctionExecutorsFromClass(VStarScriptingAPI.class, VStarScriptingAPI.getInstance(), permittedTypes,
Collections.emptySet());
}
}
// intrinsic bindings and functions
initBindings();
initFunctionExecutors();
// standard library code
loadStdLib();
// allows user to override intrinsic/standard library code
loadUserCode();
}
/**
* Construct a VeLa interpreter with verbose mode as specified, adding the VeLa
* API, and no source directories.
*
* @param verbose Verbose mode?
*/
public VeLaInterpreter(boolean verbose) {
this(verbose, true, Collections.emptyList());
}
/**
* Construct a VeLa interpreter with verbose mode set to false, adding the VeLa
* API, and no source directories.
*/
public VeLaInterpreter() {
this(false, true, Collections.emptyList());
}
/**
* @param verbose the verbose to set
*/
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
/**
* Push an environment onto the stack.
*
* @param environment The environment to be pushed.
*/
public void pushEnvironment(VeLaEnvironment<Operand> environment) {
this.environments.push(environment);
}
/**
* Pop the top-most environment from the stack and return it.
*
* @return The top-most environment.
*/
public VeLaEnvironment<Operand> popEnvironment() {
return this.environments.pop();
}
/**
* Peek the top-most environment on the stack and return it.
*
* @return The top-most environment.
*/
public VeLaEnvironment<Operand> peekEnvironment() {
return this.environments.peek();
}
/**
* @return The stack of environments
*/
public Stack<VeLaEnvironment<Operand>> getEnvironments() {
return environments;
}
/**
* Return all scopes (activation records) on the stack as a list in order from
* oldest to newest.
*/
public List<VeLaScope> getScopes() {
List<VeLaScope> scopes = new ArrayList<VeLaScope>();
for (VeLaEnvironment<Operand> env : environments) {
if (env instanceof VeLaScope) {
scopes.add((VeLaScope) env);
}
}
return scopes;
}
/**
* @return the operand stack
*/
public Stack<Operand> getStack() {
return stack;
}
/**
* Pop and return an operand from the stack if not empty.
*
* @param msgOnError The exception message to use if the stack is empty.
* @return The operand on top of the stack.
* @throws VeLaEvalError Thrown when the stack is empty.
*/
public Operand pop(String msgOnError) throws VeLaEvalError {
if (!stack.isEmpty()) {
return stack.pop();
} else {
throw new VeLaEvalError(msgOnError);
}
}
/**
* VeLa program interpreter entry point.
*
* @param file A path to a file containing a VeLa program string to be
* interpreted.
* @return An optional result, depending upon whether a value was left on the
* stack.
* @throws VeLaParseError If a parse error occurs.
* @throws VeLaEvalError If an evaluation error occurs.
*/
public Optional<Operand> program(File path) throws VeLaParseError, VeLaEvalError {
StringBuffer code = new StringBuffer();
try {
try (Stream<String> stream = Files.lines(Paths.get(path.getAbsolutePath()))) {
stream.forEachOrdered(line -> {
code.append(line);
code.append("\n");
});
}
} catch (IOException e) {
throw new VeLaEvalError("Error when attempting to read VeLa file " + path.getAbsolutePath());
}
return program(code.toString());
}
/**
* VeLa program interpreter entry point.
*
* @param prog The VeLa program string to be interpreted.
* @return An optional result, depending upon whether a value was left on the
* stack.
* @throws VeLaParseError If a parse error occurs.
* @throws VeLaEvalError If an evaluation error occurs.
*/
public Optional<Operand> program(String prog) throws VeLaParseError, VeLaEvalError {
return veLaToResultASTPair(prog).first;
}
/**
* VeLa program interpreter entry point.
*
* @param prog The VeLa program string to be interpreted.
* @return A pair consisting of an optional result, depending upon whether a
* value was left on the stack, and the AST that gave rise to the
* result.
* @throws VeLaParseError If a parse error occurs.
* @throws VeLaEvalError If an evaluation error occurs.
*/
public Pair<Optional<Operand>, AST> veLaToResultASTPair(String prog) throws VeLaParseError, VeLaEvalError {
VeLaParser.SequenceContext tree = getParser(prog).sequence();
return commonInterpreter(prog, tree);
}
/**
* Expression interpreter entry point.
*
* @param expr The expression string to be interpreted.
* @return An operand.
* @throws VeLaParseError If a parse error occurs.
* @throws VeLaEvalError If an evaluation error occurs.
*/
// public double expression(String expr) throws VeLaEvalError {
//
// // TODO: why?
//
// VeLaParser.AdditiveExpressionContext tree = getParser(expr).additiveExpression();
//
// Optional<Operand> result = commonInterpreter(expr, tree).first;
//
// if (result.isPresent()) {
// if (result.get().getType() == Type.REAL) {
// return (double) result.get().doubleVal();
// } else if (result.get().getType() == Type.INTEGER) {
// return result.get().intVal();
// } else {
// throw new VeLaEvalError("Numeric value expected as result");
// }
// } else {
// throw new VeLaEvalError("Numeric value expected as result");
// }
// }
/**
* Expression interpreter entry point.
*
* @param expr The expression string to be interpreted.
* @return An operand.
* @throws VeLaParseError If a parse error occurs.
* @throws VeLaEvalError If an evaluation error occurs.
*/
public Operand expressionToOperand(String expr) throws VeLaParseError, VeLaEvalError {
// TODO: why?
VeLaParser.ExpressionContext tree = getParser(expr).expression();
Optional<Operand> result = commonInterpreter(expr, tree).first;
if (result.isPresent()) {
return result.get();
} else {
throw new VeLaEvalError("Result expected");
}
}
/**
* VeLa boolean expression interpreter entry point.
*
* @param expr The VeLa expression string to be interpreted.
* @return A Boolean value result.
* @throws VeLaParseError If a parse error occurs.
* @throws VeLaEvalError If an evaluation error occurs.
* Only used in test code: remove?
*/
@Deprecated
public boolean booleanExpression(String expr) throws VeLaParseError, VeLaEvalError {
VeLaParser.BooleanExpressionContext tree = getParser(expr).booleanExpression();
Optional<Operand> result = commonInterpreter(expr, tree).first;
if (result.isPresent()) {
return result.get().booleanVal();
} else {
throw new VeLaEvalError("Numeric value expected as result");
}
}
// Helpers
/**
* Given an expression string, return a VeLa parser object.
*
* @param expr The expression string.
* @return The parser object.
*/
private VeLaParser getParser(String expr) {
CharStream stream = new ANTLRInputStream(expr);
VeLaLexer lexer = new VeLaLexer(stream);
lexer.removeErrorListener(ConsoleErrorListener.INSTANCE);
lexer.addErrorListener(errorListener);
CommonTokenStream tokens = new CommonTokenStream(lexer);
VeLaParser parser = new VeLaParser(tokens);
parser.addErrorListener(errorListener);
return parser;
}
/**
* Common VeLa evaluation entry point. This will be most effective when prog is
* an often used expression.
*
* @param prog The VeLa program string to be interpreted.
* @param tree The result of parsing the VeLa expression.
* @return An optional result depending upon whether a value is left on the
* stack and the AST that was constructed and evaluated.
* @throws VeLaEvalError If an evaluation error occurs.
*/
public Pair<Optional<Operand>, AST> commonInterpreter(String prog, ParserRuleContext tree) throws VeLaEvalError {
Optional<Operand> result = Optional.empty();
AST ast = commonParseTreeWalker(prog, tree);
if (ast != null) {
// Evaluate the abstract syntax tree and cache the result.
eval(ast);
if (!stack.isEmpty()) {
result = Optional.of(stack.pop());
} else {
result = Optional.empty();
}
}
return new Pair<Optional<Operand>, AST>(result, ast);
}
/**
* Common parse tree walker and AST generator.
*
* @param prog The VeLa program to be interpreted.
* @param tree The parse tree resulting from parsing the VeLa expression.
* @return The abstract syntax tree created by walking the parse tree.
* @throws VeLaParseError If a parse error occurs.
*/
protected AST commonParseTreeWalker(String prog, ParserRuleContext tree) throws VeLaParseError {
AST ast = null;
// Remove whitespace and change to uppercase to ensure a canonical
// expression string for caching purposes.
prog = prog.replace(" ", "").replace("\t", "").toUpperCase();
// We cache abstract syntax trees by top-level program string
// to improve performance.
// boolean astCached = false;
if (exprToAST.containsKey(prog)) {
ast = exprToAST.get(prog);
// astCached = true;
} else {
ExpressionVisitor visitor = new ExpressionVisitor(this);
ast = visitor.visit(tree);
if (ast != null) {
// This relates a VeLa program or expression to an AST.
exprToAST.put(prog, ast);
}
}
// if (verbose && ast != null) {
// if (astCached) {
// System.out.println(String.format("%s [AST cached]", ast));
// } else {
// System.out.println(ast);
// }
// }
return ast;
}
/**
* <p>
* Given an AST representing a VeLa program, interpret this via a depth first
* traversal, leaving the result of evaluation on the stack.
* </p>
* <p>
* The name "eval" is used in deference to John McCarthy's Lisp and its eval
* function, the equivalent of Maxwell's equations in Computer Science.
* </p>
* <p>
* I've also just noticed that VeLa is an anagram of eval! :)
* </p>
*
* @param ast An abstract syntax tree.
* @throws VeLaEvalError If an evaluation error occurs.
*/
public void eval(AST ast) throws VeLaEvalError {
if (ast.isLiteral()) {
stack.push(ast.getOperand());
} else {
Operation op = ast.getOp();
if (op.arity() == 2) {
// Binary
eval(ast.left());
eval(ast.right());
applyBinaryOperation(op);
} else if (op.arity() == 1) {
// Unary
eval(ast.head());
Operand operand = stack.pop();
switch (op) {
case NEG:
switch (operand.getType()) {
case INTEGER:
stack.push(new Operand(Type.INTEGER, -operand.intVal()));
break;
case REAL:
stack.push(new Operand(Type.REAL, -operand.doubleVal()));
break;
case LIST:
List<Operand> negResult = new ArrayList<Operand>();
for (int i = 0; i < operand.listVal().size(); i++) {
Operand scalar = operand.listVal().get(i);
switch (scalar.getType()) {
case INTEGER:
negResult.add(new Operand(Type.INTEGER, -scalar.intVal()));
break;
case REAL:
negResult.add(new Operand(Type.REAL, -scalar.doubleVal()));
break;
default:
binaryOpError(op, Type.INTEGER, Type.REAL);
break;
}
}
stack.push(new Operand(Type.LIST, negResult));
break;
default:
binaryOpError(op, Type.INTEGER, Type.REAL, Type.LIST);
break;
}
break;
case NOT:
switch (operand.getType()) {
case BOOLEAN:
stack.push(new Operand(Type.BOOLEAN, !operand.booleanVal()));
break;
case INTEGER:
stack.push(new Operand(Type.INTEGER, ~operand.intVal()));
break;
case LIST:
List<Operand> notResult = new ArrayList<Operand>();
for (int i = 0; i < operand.listVal().size(); i++) {
Operand scalar = operand.listVal().get(i);
switch (scalar.getType()) {
case BOOLEAN:
notResult.add(new Operand(Type.BOOLEAN, !scalar.booleanVal()));
break;
case INTEGER:
notResult.add(new Operand(Type.INTEGER, ~scalar.intVal()));
break;
default:
binaryOpError(op, Type.INTEGER, Type.BOOLEAN);
break;
}
}
stack.push(new Operand(Type.LIST, notResult));
break;
default:
binaryOpError(op, Type.INTEGER, Type.BOOLEAN, Type.LIST);
;
break;
}
}
} else if (ast.getOp() == Operation.SYMBOL) {
// Look up variable or function in the environment stack,
// pushing it onto the operand stack if it exists, looking for
// and evaluating a function if not, throwing an exception
// otherwise.
String name = ast.getToken().toUpperCase();
// Bound symbol?
Optional<Operand> result = lookupBinding(name);
if (result.isPresent()) {
stack.push(result.get());
} else {
// Function?
Optional<List<FunctionExecutor>> funList = lookupFunctions(name);
if (funList.isPresent()) {
// The first function in the list is chosen in the
// absence of parameter type information.
stack.push(new Operand(Type.FUNCTION, funList.get().get(0)));
} else {
throw new VeLaEvalError("Unknown binding \"" + ast.getToken() + "\"");
}
}
} else if (ast.getOp() == Operation.LIST) {
// Evaluate list elements.
List<Operand> elements = new ArrayList<Operand>();
if (ast.hasChildren()) {
for (int i = ast.getChildren().size() - 1; i >= 0; i--) {
eval(ast.getChildren().get(i));
}
// Create and push list of operands.
for (int i = 1; i <= ast.getChildren().size(); i++) {
elements.add(stack.pop());
}
}
stack.push(new Operand(Type.LIST, elements));
} else if (ast.getOp().isSpecialForm()) {
specialForm(ast);
}
}
}
/**
* Handle special forms.
*
* @param ast The special form's AST.
*/
private void specialForm(AST ast) {
switch (ast.getOp()) {
case SEQUENCE:
// Evaluate each child AST in turn. No children means an empty
// program or one consisting only of whitespace or comments.
if (ast.hasChildren()) {
for (AST child : ast.getChildren()) {
eval(child);
}
}
break;
case BIND:
case IS:
eval(ast.right());
String varName = ast.left().getToken();
String msg = "No value to bind to \"" + varName + "\"";
bind(varName, pop(msg), ast.getOp() == Operation.IS);
break;
case FUNDEF:
// Does this function have a name or is it anonymous?
Optional<String> name = Optional.empty();
if (ast.head().getOp() == Operation.SYMBOL) {
name = Optional.of(ast.head().getToken());
}
// Extract components from AST in order to create a function
// executor.
Optional<String> helpString = Optional.empty();
List<String> parameterNames = new ArrayList<String>();
List<Type> parameterTypes = new ArrayList<Type>();
Optional<Type> returnType = Optional.empty();
Optional<AST> functionBody = Optional.empty();
for (int i = name.isPresent() ? 1 : 0; i < ast.getChildren().size(); i++) {
AST child = ast.getChildren().get(i);
switch (child.getOp()) {
case HELP_COMMENT:
String help = child.getToken();
help = help.replace("<<", "").replace(">>", "").trim();
helpString = Optional.of(help);
break;
case PAIR:
if (child.hasChildren()) {
parameterNames.add(child.left().getToken());
parameterTypes.add(Type.name2Vela(child.right().getToken()));
}
break;
case SYMBOL:
returnType = Optional.of(Type.name2Vela(child.getToken()));
break;
case SEQUENCE:
functionBody = Optional.of(child);
break;
default:
break;
}
}
// Add the named function to the top-most scope's function namespace
// or the push the anonymous function to the operand stack.
UserDefinedFunctionExecutor function = new UserDefinedFunctionExecutor(this, name, parameterNames,
parameterTypes, returnType, functionBody, helpString);
if (name.isPresent()) {
addFunctionExecutor(function);
} else {
stack.push(new Operand(Type.FUNCTION, function));
}
break;
case FUNCALL:
List<Operand> params = new ArrayList<Operand>();
FunctionExecutor anon = null;
int childLimit = 1;
for (int i = ast.getChildren().size() - 1; i >= childLimit; i--) {
eval(ast.getChildren().get(i));
}
// Prepare actual parameter list.
for (int i = childLimit; i <= ast.getChildren().size() - 1; i++) {
Operand value = stack.pop();
params.add(value);
}
if (ast.head().getOp() == Operation.SYMBOL) {
applyFunction(ast.head().getToken(), params);
} else {
eval(ast.head());
anon = stack.pop().functionVal();
if (!applyFunction(anon, params)) {
throw new VeLaEvalError("Invalid parameters for function \"" + anon + "\"");
}
}
break;
case WHEN:
// Evaluate each antecedent in turn, pushing the value
// of the first consequent whose antecedent is true and stop
// antecedent evaluation.
for (AST pair : ast.getChildren()) {
eval(pair.left());
if (stack.pop().booleanVal()) {
eval(pair.right());
break;
}
}
break;
case IF:
// Evaluate the antecedent, pushing the value of the first
// consequent if the condition is true, and the value of the
// second consequent otherwise.
eval(ast.head());
if (stack.pop().booleanVal()) {
eval(ast.getChildren().get(1));
} else if (ast.getChildren().size() == 3) {
eval(ast.getChildren().get(2));
}
break;
case WHILE:
// Evaluate the condition, executing the body while it is true.
while (true) {
eval(ast.left());
if (!stack.isEmpty() && stack.peek().getType() == Type.BOOLEAN && stack.pop().booleanVal()) {
eval(ast.right());
} else {
break;
}
}
break;
default:
break;
}
}
/**
* Apply a binary operation to the values on the stack, consuming them and
* leaving a result on the stack.
*
* @param op The operation to be applied.
*/
private void applyBinaryOperation(Operation op) {
Operand operand2 = stack.pop();
Operand operand1 = stack.pop();
if (operand1.getType() == Type.LIST || operand2.getType() == Type.LIST) {
applyBinaryListOperation(op, operand1, operand2);
} else {
// TODO Refactor to N methods or define functions for each in
// Operation/Operand or use lambda for n+m, n-m ...
// =>
// https://stackoverflow.com/questions/13604703/how-do-i-define-a-method-which-takes-a-lambda-as-a-parameter-in-java-8
// type unification is not relevant to all operations, e.g. IN;
// Unify the operand types if possible.
Pair<Operand, Operand> operands = unifyTypes(operand1, operand2);
operand1 = operands.first;
operand2 = operands.second;
// Arbitrarily use the type of the first operand.
Type type = operands.first.getType();
switch (op) {
case ADD:
switch (type) {
case INTEGER:
stack.push(new Operand(Type.INTEGER, operand1.intVal() + operand2.intVal()));
break;
case REAL:
stack.push(new Operand(Type.REAL, operand1.doubleVal() + operand2.doubleVal()));
break;
case STRING:
stack.push(new Operand(Type.STRING, operand1.stringVal() + operand2.stringVal()));
break;
default:
binaryOpError(op, Type.INTEGER, Type.REAL, Type.STRING);
break;
}
break;
case SUB:
switch (type) {
case INTEGER:
stack.push(new Operand(Type.INTEGER, operand1.intVal() - operand2.intVal()));
break;
case REAL:
stack.push(new Operand(Type.REAL, operand1.doubleVal() - operand2.doubleVal()));
break;
default:
binaryOpError(op, Type.INTEGER, Type.REAL);
break;
}
break;
case MUL:
switch (type) {
case INTEGER:
stack.push(new Operand(Type.INTEGER, operand1.intVal() * operand2.intVal()));
break;
case REAL:
stack.push(new Operand(Type.REAL, operand1.doubleVal() * operand2.doubleVal()));
break;
default:
binaryOpError(op, Type.INTEGER, Type.REAL);
break;
}
break;
case DIV:
switch (type) {
case INTEGER:
if (operand2.intVal() != 0) {
stack.push(new Operand(Type.INTEGER, operand1.intVal() / operand2.intVal()));
} else {
throw new VeLaEvalError(
String.format("%s/%s: division by zero error", operand1.intVal(), operand2.intVal()));
}
break;
case REAL:
Double result = operand1.doubleVal() / operand2.doubleVal();
if (!result.isInfinite()) {
stack.push(new Operand(Type.REAL, result));
} else {
throw new VeLaEvalError(String.format("%s/%s: division by zero error", operand1.doubleVal(),
operand2.doubleVal()));
}
break;
default:
binaryOpError(op, Type.INTEGER, Type.REAL);
break;
}
break;
case POW:
switch (type) {
case INTEGER:
long base = operand1.intVal();
long result = base;
long exponent = operand2.intVal();
if (exponent == 0) {
result = 1;
} else {
// multiply operand1 by itself n-1 times
for (int i = 1; i <= exponent - 1; i++) {
result *= base;
}
}
stack.push(new Operand(Type.INTEGER, result));
break;
case REAL:
stack.push(new Operand(Type.REAL, Math.pow(operand1.doubleVal(), operand2.doubleVal())));
break;
default:
binaryOpError(op, Type.INTEGER, Type.REAL);
break;
}
break;
case AND:
switch (type) {
case BOOLEAN:
stack.push(new Operand(Type.BOOLEAN, operand1.booleanVal() & operand2.booleanVal()));
break;
case INTEGER:
stack.push(new Operand(Type.INTEGER, operand1.intVal() & operand2.intVal()));
break;
default:
binaryOpError(op, Type.INTEGER, Type.BOOLEAN);
break;
}
break;
case XOR:
switch (type) {
case BOOLEAN:
stack.push(new Operand(Type.BOOLEAN, operand1.booleanVal() ^ operand2.booleanVal()));
break;
case INTEGER:
stack.push(new Operand(Type.INTEGER, operand1.intVal() ^ operand2.intVal()));
break;
default:
binaryOpError(op, Type.INTEGER, Type.BOOLEAN);
break;
}
break;
case OR:
switch (type) {
case BOOLEAN:
stack.push(new Operand(Type.BOOLEAN, operand1.booleanVal() | operand2.booleanVal()));
break;
case INTEGER:
stack.push(new Operand(Type.INTEGER, operand1.intVal() | operand2.intVal()));
break;
default:
binaryOpError(op, Type.INTEGER, Type.BOOLEAN);
break;
}
break;
case EQUAL:
switch (type) {
case BOOLEAN:
stack.push(new Operand(Type.BOOLEAN, operand1.booleanVal() == operand2.booleanVal()));
break;
case INTEGER:
stack.push(new Operand(Type.BOOLEAN, operand1.intVal() == operand2.intVal()));
break;
case REAL:
stack.push(new Operand(Type.BOOLEAN, operand1.doubleVal() == operand2.doubleVal()));
break;
case STRING:
stack.push(new Operand(Type.BOOLEAN, operand1.stringVal().equals(operand2.stringVal())));
break;
default:
binaryOpError(op, Type.BOOLEAN, Type.INTEGER, Type.REAL, Type.STRING);
break;
}
break;
case NOT_EQUAL:
switch (type) {
case BOOLEAN:
stack.push(new Operand(Type.BOOLEAN, operand1.booleanVal() != operand2.booleanVal()));
break;
case INTEGER:
stack.push(new Operand(Type.BOOLEAN, operand1.intVal() != operand2.intVal()));
break;
case REAL:
stack.push(new Operand(Type.BOOLEAN, operand1.doubleVal() != operand2.doubleVal()));
break;
case STRING:
stack.push(new Operand(Type.BOOLEAN, !operand1.stringVal().equals(operand2.stringVal())));
break;
default:
binaryOpError(op, Type.BOOLEAN, Type.INTEGER, Type.REAL, Type.STRING);
break;
}
break;
case GREATER_THAN:
switch (type) {
case INTEGER:
stack.push(new Operand(Type.BOOLEAN, operand1.intVal() > operand2.intVal()));
break;
case REAL:
stack.push(new Operand(Type.BOOLEAN, operand1.doubleVal() > operand2.doubleVal()));
break;
case STRING:
stack.push(new Operand(Type.BOOLEAN, operand1.stringVal().compareTo(operand2.stringVal()) > 0));
break;
default:
binaryOpError(op, Type.INTEGER, Type.REAL, Type.STRING);
break;
}
break;
case LESS_THAN:
switch (type) {
case INTEGER:
stack.push(new Operand(Type.BOOLEAN, operand1.intVal() < operand2.intVal()));
break;
case REAL:
stack.push(new Operand(Type.BOOLEAN, operand1.doubleVal() < operand2.doubleVal()));
break;
case STRING:
stack.push(new Operand(Type.BOOLEAN, operand1.stringVal().compareTo(operand2.stringVal()) < 0));
break;
default:
binaryOpError(op, Type.INTEGER, Type.REAL, Type.STRING);
break;
}
break;
case GREATER_THAN_OR_EQUAL:
switch (type) {
case INTEGER:
stack.push(new Operand(Type.BOOLEAN, operand1.intVal() >= operand2.intVal()));
break;
case REAL:
stack.push(new Operand(Type.BOOLEAN, operand1.doubleVal() >= operand2.doubleVal()));
break;
case STRING:
stack.push(new Operand(Type.BOOLEAN, operand1.stringVal().compareTo(operand2.stringVal()) >= 0));
break;
default:
binaryOpError(op, Type.INTEGER, Type.REAL, Type.STRING);
break;
}
break;
case LESS_THAN_OR_EQUAL:
switch (type) {
case INTEGER:
stack.push(new Operand(Type.BOOLEAN, operand1.intVal() <= operand2.intVal()));
break;
case REAL:
stack.push(new Operand(Type.BOOLEAN, operand1.doubleVal() <= operand2.doubleVal()));
break;
case STRING:
stack.push(new Operand(Type.BOOLEAN, operand1.stringVal().compareTo(operand2.stringVal()) <= 0));
break;
default:
binaryOpError(op, Type.INTEGER, Type.REAL, Type.STRING);
break;
}
break;
case APPROXIMATELY_EQUAL:
if (type == Type.STRING) {
Pattern pattern;
String regex = operand2.stringVal();
if (!regexPatterns.containsKey(regex)) {
pattern = Pattern.compile(regex);
regexPatterns.put(regex, pattern);
}
pattern = regexPatterns.get(regex);
stack.push(new Operand(Type.BOOLEAN, pattern.matcher(operand1.stringVal()).matches()));
} else {
binaryOpError(op, Type.STRING);
break;
}
break;
case IN:
if (type == Type.STRING) {
// Is one string contained within another?
stack.push(new Operand(Type.BOOLEAN, operand2.stringVal().contains(operand1.stringVal())));
} else {
binaryOpError(op, Type.STRING);
}
break;
case SHL:
switch (type) {
case INTEGER:
stack.push(new Operand(Type.INTEGER, operand1.intVal() << operand2.intVal()));
break;
default:
binaryOpError(op, Type.INTEGER);
break;
}
break;
case SHR:
switch (type) {
case INTEGER:
stack.push(new Operand(Type.INTEGER, operand1.intVal() >> operand2.intVal()));
break;
default:
binaryOpError(op, Type.INTEGER);
break;
}
break;
default:
break;
}
}
}
/**
* Apply a binary operation to the operands, one of which is a list, leaving a
* result on the stack.
*
* @param op The operation to be applied.
* @param operand1 The first operand.
* @param operand2 The second operand.
*/
private void applyBinaryListOperation(Operation op, Operand operand1, Operand operand2) {
switch (op) {
case IN:
Pair<Operand, Operand> operands = unifyTypes(operand1, operand2);
operand1 = operands.first;
operand2 = operands.second;
if (operand2.getType() == Type.LIST) {
// Is a value contained within a list?
stack.push(new Operand(Type.BOOLEAN, operand2.listVal().contains(operand1)));
} else if (operand2.getType() == Type.STRING) {
// Is one string contained within another?
stack.push(new Operand(Type.BOOLEAN, operand2.stringVal().contains(operand1.stringVal())));
} else {
String msg = String.format("The second operand must be of type list or string for 'IN' operation", op);
throw new VeLaEvalError(msg);
}
break;
default:
List<Operand> result = new ArrayList<Operand>();
if (operand1.getType() == Type.LIST && operand2.getType() == Type.LIST) {
if (operand1.listVal().size() == operand2.listVal().size()) {
for (int i = 0; i < operand1.listVal().size(); i++) {
stack.push(operand1.listVal().get(i));
stack.push(operand2.listVal().get(i));
applyBinaryOperation(op);
result.add(stack.pop());
}
stack.push(new Operand(Type.LIST, result));
} else {
String msg = String.format("Lists must be of equal length " + "for '%s' operation", op);
throw new VeLaEvalError(msg);
}
} else if (operand1.getType() != Type.LIST) {
for (int i = 0; i < operand2.listVal().size(); i++) {
stack.push(operand1);
stack.push(operand2.listVal().get(i));
applyBinaryOperation(op);
result.add(stack.pop());
}
stack.push(new Operand(Type.LIST, result));
} else {
for (int i = 0; i < operand1.listVal().size(); i++) {
stack.push(operand1.listVal().get(i));
stack.push(operand2);
applyBinaryOperation(op);
result.add(stack.pop());
}
stack.push(new Operand(Type.LIST, result));
}
break;
}
}
/**
* Throw a VeLa evaluation error for the given operation and types.
*
* @param op The operator
* @param types The expected types
*/
private void binaryOpError(Operation op, Type... types) {
String typeStr = "";
for (int i = 0; i < types.length; i++) {
typeStr += types[i].name();
if (i < types.length - 1) {
typeStr += " or ";
}
}
String msg = String.format("'%s' expects values of type %s", op.token(), typeStr);
throw new VeLaEvalError(msg);
}
/**
* Unify operand types by converting both operands to strings if only one is a
* string or both operands to double if only one is an integer. We change
* nothing if either type is composite.
*
* @param a The first operand.
* @param b The second operand.
* @return The unified operands.
*/
private Pair<Operand, Operand> unifyTypes(Operand a, Operand b) {
Operand converted_a = a;
Operand converted_b = b;
if (!a.getType().isComposite() && !b.getType().isComposite()) {
if (a.getType() != Type.STRING && b.getType() == Type.STRING) {
converted_a = a.convertToString();
} else if (a.getType() == Type.STRING && b.getType() != Type.STRING) {
converted_b = b.convertToString();
} else if (a.getType() == Type.INTEGER && b.getType() == Type.REAL) {
converted_a = new Operand(Type.REAL, (double) a.intVal());
} else if (a.getType() == Type.REAL && b.getType() == Type.INTEGER) {
converted_b = new Operand(Type.REAL, (double) b.intVal());
}
}
return new Pair<Operand, Operand>(converted_a, converted_b);
}
// ** Variable related methods **
/**
* Given a variable name, search for it in the stack of environments, binding a
* value if found and the type of the binding is compatible with the type of the
* new value. The search proceeds from the top to the bottom of the stack,
* maintaining the natural stack ordering. If the name is not found, a new
* binding is created in the top-most scope.
*
* @param name The name to which to bind the value.
* @param value The value to be bound.
* @param isConstant Is this a constant binding?
*/
public void bind(String name, Operand value, boolean isConstant) {
boolean bound = false;
for (int i = environments.size() - 1; i >= 0; i--) {
VeLaEnvironment<Operand> env = environments.get(i);
if (env.isMutable()) {
Optional<Operand> possibleBinding = env.lookup(name);
if (possibleBinding.isPresent()) {
Operand existingBinding = possibleBinding.get();
Operand convertedVal = value.convert(existingBinding.getType());
if (convertedVal.getType() == existingBinding.getType()) {
// bind value to existing variable...
environments.get(i).bind(name, convertedVal, isConstant);
bound = true;
} else {
throw new VeLaEvalError(
String.format("The type of the value (%s) is not compatible with the bound type of %s.",
value, name));
}
break;
}
}
}
if (!bound) {
// If not bound to existing variable, create new binding in
// environment on top of stack.
VeLaEnvironment<Operand> env = environments.peek();
if (env.isMutable()) {
// ...a new binding
environments.peek().bind(name, value, isConstant);
} else {
// The environment at stack-top should be mutable...
throw new VeLaEvalError(String.format("The environment in which %s is bound is immutable.", name));
}
}
}
/**
* Given a variable name, search for it in the stack of environments, return an
* optional Operand instance. The search proceeds from the top to the bottom of
* the stack, maintaining the natural stack ordering.
*
* @param name The name of the variable to look up.
* @return The optional operand.
*/
public Optional<Operand> lookupBinding(String name) {
Optional<Operand> result = Optional.empty();
// Note: could use recursion or a reversed stream iterator instead
for (int i = environments.size() - 1; i >= 0; i--) {
result = environments.get(i).lookup(name);
if (result.isPresent()) {
break;
}
}
return result;
}
/**
* Read and interpret the VeLa standard library code.<br/>
* A VeLa error should not bring VStar down.<br/>
*/
private void loadStdLib() {
try {
program(ResourceAccessor.getVeLaStdLibStr());
} catch (Throwable t) {
VStar.LOGGER.warning("Error when sourcing VeLa standard library code: " + t.getLocalizedMessage());
}
}
/**
* Read and interpret user-defined code.<br/>
* A VeLa error should not bring VStar down.<br/>
* Ignore all but VeLa files (e.g. could be README files) and directories.
*/
private void loadUserCode() {
for (File dir : sourceDirectories) {
try {
if (dir.isDirectory()) {
for (File file : dir.listFiles()) {
if (file.getName().endsWith(".vl") || file.getName().endsWith(".vela")) {
program(file);
}
}
} else {
}
} catch (Throwable t) {
VStar.LOGGER.warning("Error when sourcing VeLa code: " + t.getLocalizedMessage());
}
}
}
// ** Function related methods *
/**
* Given a function name, search for it in the stack of environments, return an
* optional list of function executors. The search proceeds from the top to the
* bottom of the stack, maintaining the natural stack ordering.
*
* @param name The name of the variable to look up.
* @return The optional function executor list.
*/
public Optional<List<FunctionExecutor>> lookupFunctions(String name) {
Optional<List<FunctionExecutor>> functions = Optional.empty();
for (int i = environments.size() - 1; i >= 0; i--) {
VeLaEnvironment<Operand> environment = environments.get(i);
if (environment instanceof VeLaScope) {
functions = ((VeLaScope) environment).lookupFunction(name);
if (functions.isPresent()) {
break;
}
}
}
return functions;
}
/**
* Apply the function to the supplied parameter list, leaving the result on the
* stack.
*
* @param funcName The name of the function.
* @param params The parameter list.
* @throws VeLaEvalError If a function evaluation error occurs.
*/
private void applyFunction(String funcName, List<Operand> params) throws VeLaEvalError {
String canonicalFuncName = funcName.toUpperCase();
// Iterate over all variations of each potentially overloaded function,
// asking whether each conforms.
Optional<List<FunctionExecutor>> functions = lookupFunctions(canonicalFuncName);
boolean match = false;
if (functions.isPresent()) {
// First look for the name in the function namespace and try to
// apply it.
for (FunctionExecutor function : functions.get()) {
match = applyFunction(function, params);
if (match) {
break;
}
}
if (!match) {
StringBuffer candidateFunStr = new StringBuffer();
for (FunctionExecutor candidateFun : functions.get()) {
candidateFunStr.append(" ");
candidateFunStr.append(candidateFun.toString());
candidateFunStr.append("\n");
}
throw new VeLaEvalError("Invalid parameters for function \"" + funcName + "\":\n" + candidateFunStr);
}
} else {
// Instead of being a named function, it may be a function that's
// been bound to a symbol, so try that next.
Optional<Operand> value = lookupBinding(canonicalFuncName);
if (value.isPresent()) {
if (value.get().getType() == Type.FUNCTION) {
applyFunction(value.get().functionVal(), params);
}
} else {
throw new VeLaEvalError("Unknown function \"" + funcName + "\"");
}
}
}
/**
* Apply the function to the supplied parameter list if it conforms to them,
* leaving the result on the stack.
*
* @param function The function executor to be applied to the supplied
* parameters.
* @param params The actual parameter list.
* @return Does the function conform to the actual parameters?
* @throws VeLaEvalError If a function evaluation error occurs.
*/
private boolean applyFunction(FunctionExecutor function, List<Operand> params) throws VeLaEvalError {
boolean conforms = function.conforms(params);
if (conforms) {
// Apply the function to the actual parameters.
Optional<Operand> result = function.apply(params);
String funcRepr = function.toString();
if (result.isPresent()) {
// The function returned a result.
// Does the function have a return type defined?
if (function.returnType.isPresent()) {
// Attempt to convert to return type if necessary.
Operand convertedResult = result.get().convert(function.getReturnType().get());
if (convertedResult.getType() == function.returnType.get()) {
// The returned result was of the expected type or was converted to it.
stack.push(convertedResult);
} else {
// The returned result was not of the expected type.
throw new VeLaEvalError(String.format(
"The expected return type of %s does not match " + "the actual return type of %s.",
funcRepr, result.get().getType()));
}
} else {
throw new VeLaEvalError(
String.format("%s has no return type but a value " + "of type %s was returned.", funcRepr,
result.get().getType()));
}
} else {
if (function.returnType.isPresent()) {
// No result was returned but one was expected.
throw new VeLaEvalError(String.format("No value was returned by %s.", funcRepr));
}
}
}
return conforms;
}
/**
* Add a function executor to the current scope.
*
* @param executor The function executor to be added.
*/
public void addFunctionExecutor(FunctionExecutor executor) {
// It's possible that the top-most environment is not a scope, so find
// the top-most scope and add the function executor to it.
for (int i = environments.size() - 1; i >= 0; i--) {
VeLaEnvironment<Operand> environment = environments.get(i);
if (environment instanceof VeLaScope) {
VeLaScope scope = (VeLaScope) environment;
scope.addFunctionExecutor(executor);
}
}
}
/**
* Add useful/important bindings
*/
private void initBindings() {
bind("Π", new Operand(Type.REAL, Math.PI), true);
bind("PI", new Operand(Type.REAL, Math.PI), true);
bind("E", new Operand(Type.REAL, Math.E), true);
}
/**
* Initialise function executors
*/
private void initFunctionExecutors() {
// Special functions
addEval();
addExit();
addHelp();
addZeroArityFunctions();
// I/O
addIOProcedures();
// String functions
addFormatFunction();
addChrFunction();
addOrdFunction();
// List functions
addListHeadFunction();
addListTailFunction();
addListNthFunction();
addListLengthFunction();
addListConcatFunction();
addIntegerSeqFunction();
addRealSeqFunction();
addListMapFunction();
addListFilterFunction();
addListFindFunction();
addListPairwiseFindFunction();
addListForFunction();
for (Type type : Type.values()) {
if (!type.equals(Type.OBJECT) && !type.equals(Type.NONE)) {
addListAppendFunction(type);
addListReduceFunction(type);
}
}
// Collect functions from reflection over Java classes
for (FunctionExecutor function : javaClassFunctionExecutors) {
addFunctionExecutor(function);
if (verbose && function != null) {
System.out.println(function.toString());
}
}
}
private void addEval() {
String help = "Compiles and evaluates a VeLa program given\n"
+ "in the supplied string, returning the empty list if\n"
+ "there is no result, or a single element list if\n" + "there is a result.";
addFunctionExecutor(new FunctionExecutor(Optional.of("EVAL"), Arrays.asList("code"), Arrays.asList(Type.STRING),
Optional.of(Type.LIST), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
// Compile and evaluate code.
program(operands.get(0).stringVal());
Optional<Operand> result = program(operands.get(0).stringVal());
// Return a list containing the result or the empty list.
Optional<Operand> resultList;
if (result.isPresent()) {
resultList = Optional.of(new Operand(Type.LIST, Arrays.asList(result.get())));
} else {
resultList = Optional.of(Operand.EMPTY_LIST);
}
return resultList;
}
});
}
private void addZeroArityFunctions() {
String todayHelp = "Yields the Julian Day corresponding to the current year, month and day.";
addFunctionExecutor(new FunctionExecutor(Optional.of("TODAY"), Optional.of(Type.REAL), Optional.of(todayHelp)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
Calendar cal = Calendar.getInstance();
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH) + 1; // 0..11 -> 1..12
int day = cal.get(Calendar.DAY_OF_MONTH);
double jd = AbstractDateUtil.getInstance().calendarToJD(year, month, day);
return Optional.of(new Operand(Type.REAL, jd));
}
});
String intrinsicsHelp = "Returns a list of intrinsic functions.";
addFunctionExecutor(
new FunctionExecutor(Optional.of("INTRINSICS"), Optional.of(Type.LIST), Optional.of(intrinsicsHelp)) {
@Override
public Optional<Operand> apply(List<Operand> operands) throws VeLaEvalError {
List<Operand> funcInfoList = new ArrayList<Operand>();
VeLaScope environment = (VeLaScope) environments.get(0);
Map<String, List<FunctionExecutor>> functionMap = new TreeMap<String, List<FunctionExecutor>>(
environment.getFunctions());
for (String name : functionMap.keySet()) {
for (FunctionExecutor function : functionMap.get(name)) {
if (!(function instanceof UserDefinedFunctionExecutor)) {
Operand funcInfo = new Operand(Type.FUNCTION, function);
funcInfoList.add(funcInfo);
}
}
}
return Optional.of(new Operand(Type.LIST, funcInfoList));
}
});
String millisecsHelp = "Returns the number of milliseconds between the current time and midnight, January 1, 1970 UTC";
addFunctionExecutor(new FunctionExecutor(Optional.of("MILLISECONDS"), Optional.of(Type.INTEGER),
Optional.of(millisecsHelp)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
long milliseconds = System.currentTimeMillis();
return Optional.of(new Operand(Type.INTEGER, milliseconds));
}
});
}
private void addExit() {
String help = "Exits the current program with the specified exit code.";
addFunctionExecutor(new FunctionExecutor(Optional.of("EXIT"), Arrays.asList("exitCode"),
Arrays.asList(Type.INTEGER), Optional.empty(), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
System.exit((int) operands.get(0).intVal());
return Optional.empty();
}
});
}
private void addHelp() {
String help = "Returns a help string given an arbitrary parameter.";
addFunctionExecutor(new FunctionExecutor(Optional.of("HELP"), FunctionExecutor.ANY_FORMAL_NAMES,
FunctionExecutor.ANY_FORMAL_TYPES, Optional.of(Type.STRING), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
String helpMessage = null;
if (operands.size() >= 1) {
StringBuffer buf = new StringBuffer();
for (Operand operand : operands) {
String humanStr = operand.toHumanReadableString();
if (operand.getType() != Type.FUNCTION) {
buf.append(operand.getType());
buf.append(" : ");
buf.append(humanStr);
buf.append("\n");
} else {
buf.append(humanStr);
buf.append("\n");
Optional<String> helpStrOpt = operand.functionVal().helpString;
if (helpStrOpt.isPresent()) {
buf.append(helpStrOpt.get());
buf.append("\n");
}
}
buf.append("\n");
}
helpMessage = buf.toString();
} else {
throw new VeLaEvalError("One or more expression expected.");
}
return Optional.of(new Operand(Type.STRING, helpMessage));
}
});
}
private void addIOProcedures() {
// Any number or type of parameters will do.
String printHelp = "Prints an arbitrary number of parameters (or none) to standard output.";
addFunctionExecutor(new FunctionExecutor(Optional.of("PRINT"), FunctionExecutor.ANY_FORMAL_NAMES,
FunctionExecutor.ANY_FORMAL_TYPES, Optional.empty(), Optional.of(printHelp)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
return commonPrintProcedure(operands, false);
}
});
// Note: shouldn't need this but on command-line, the LF character
// prints literally. Why?
String printlnHelp = "Prints an arbitrary number of parameters (or none) to standard output, followed by a newline sequence.";
addFunctionExecutor(new FunctionExecutor(Optional.of("PRINTLN"), FunctionExecutor.ANY_FORMAL_NAMES,
FunctionExecutor.ANY_FORMAL_TYPES, Optional.empty(), Optional.of(printlnHelp)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
return commonPrintProcedure(operands, true);
}
});
String nextchHelp = "Gets and returns the next character from standard input.";
addFunctionExecutor(
new FunctionExecutor(Optional.of("NEXTCHAR"), Optional.of(Type.STRING), Optional.of(nextchHelp)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
Operand ch = null;
try {
ch = new Operand(Type.STRING, Character.toString((char) System.in.read()));
} catch (IOException e) {
ch = new Operand(Type.STRING, "");
}
return Optional.of(ch);
}
});
}
private Optional<Operand> commonPrintProcedure(List<Operand> operands, boolean eoln) {
for (Operand operand : operands) {
System.out.print(operand.toHumanReadableString());
}
if (eoln) {
System.out.println();
}
return Optional.empty();
}
private void addFormatFunction() {
String help = "Given a format string and a list of expressions, yields a formatted string.";
addFunctionExecutor(new FunctionExecutor(Optional.of("FORMAT"), Arrays.asList("formatString", "valueList"),
Arrays.asList(Type.STRING, Type.LIST), Optional.of(Type.STRING), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
List<Object> args = new ArrayList<Object>();
for (Operand operand : operands.get(1).listVal()) {
switch (operand.getType()) {
case INTEGER:
args.add(operand.intVal());
break;
case REAL:
args.add(operand.doubleVal());
break;
case STRING:
args.add(operand.stringVal());
break;
case BOOLEAN:
args.add(operand.booleanVal());
break;
case LIST:
args.add(operand.listVal());
break;
case FUNCTION:
args.add(operand.functionVal());
break;
}
}
Operand result = new Operand(Type.STRING,
String.format(operands.get(0).stringVal(), args.toArray(new Object[0])));
return Optional.of(result);
}
});
}
private void addChrFunction() {
String help = "Returns a single character string given an ordinal value.";
addFunctionExecutor(new FunctionExecutor(Optional.of("CHR"), Arrays.asList("ordinalValue"),
Arrays.asList(Type.INTEGER), Optional.of(Type.STRING), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
long ordVal = operands.get(0).intVal();
String str = ordVal > -1 ? Character.toString((char) ordVal) : "";
return Optional.of(new Operand(Type.STRING, str));
}
});
}
private void addOrdFunction() {
String help = "Returns an ordinal value given a single character string.";
addFunctionExecutor(new FunctionExecutor(Optional.of("ORD"), Arrays.asList("character"),
Arrays.asList(Type.STRING), Optional.of(Type.INTEGER), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
char chrVal = operands.get(0).stringVal().charAt(0);
return Optional.of(new Operand(Type.INTEGER, (int) chrVal));
}
});
}
private void addListHeadFunction() {
String help = "Returns the head of a list or the empty list if the list is empty.";
// Return type will change with invocation.
addFunctionExecutor(new FunctionExecutor(Optional.of("HEAD"), Arrays.asList("aList"), Arrays.asList(Type.LIST),
Optional.of(Type.LIST), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
List<Operand> list = operands.get(0).listVal();
Operand result;
if (!list.isEmpty()) {
result = list.get(0);
} else {
result = Operand.EMPTY_LIST;
}
setReturnType(Optional.of(result.getType()));
return Optional.of(result);
}
});
}
private void addListTailFunction() {
String help = "Returns the tail of a list or the empty list if the list is empty.";
// Return type will always be a list.
addFunctionExecutor(new FunctionExecutor(Optional.of("TAIL"), Arrays.asList("aList"), Arrays.asList(Type.LIST),
Optional.of(Type.LIST), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
List<Operand> list = operands.get(0).listVal();
Operand result;
if (!list.isEmpty()) {
List<Operand> tail = new ArrayList<Operand>(list);
tail.remove(0);
result = new Operand(Type.LIST, tail);
} else {
result = Operand.EMPTY_LIST;
}
return Optional.of(result);
}
});
}
private void addListNthFunction() {
String help = "Returns the nth element of a list or the empty list if the list is empty.";
// Return type will change with invocation; need a union type!
addFunctionExecutor(new FunctionExecutor(Optional.of("NTH"), Arrays.asList("aList", "index"),
Arrays.asList(Type.LIST, Type.INTEGER), Optional.empty(), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
List<Operand> list = operands.get(0).listVal();
Operand result;
if (!list.isEmpty()) {
result = list.get((int) operands.get(1).intVal());
} else {
result = Operand.EMPTY_LIST;
}
setReturnType(Optional.of(result.getType()));
return Optional.of(result);
}
});
}
private void addListLengthFunction() {
String help = "Returns the length of a list.";
// Return type will always be integer.
addFunctionExecutor(new FunctionExecutor(Optional.of("LENGTH"), Arrays.asList("aList"),
Arrays.asList(Type.LIST), Optional.of(Type.INTEGER), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
return Optional.of(new Operand(Type.INTEGER, operands.get(0).listVal().size()));
}
});
}
private void addListConcatFunction() {
String help = "Returns the concatenation of two lists.";
// Return type will always be LIST here.
addFunctionExecutor(new FunctionExecutor(Optional.of("CONCAT"), Arrays.asList("aList", "anotherList"),
Arrays.asList(Type.LIST, Type.LIST), Optional.of(Type.LIST), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
List<Operand> list1 = operands.get(0).listVal();
List<Operand> list2 = operands.get(1).listVal();
List<Operand> newList = new ArrayList<Operand>();
newList.addAll(list1);
newList.addAll(list2);
return Optional.of(new Operand(Type.LIST, newList));
}
});
}
private void addListAppendFunction(Type secondParameterType) {
String help = "Returns the result of appending an expression to a list.";
// Return type will always be LIST here.
addFunctionExecutor(new FunctionExecutor(Optional.of("APPEND"), Arrays.asList("aList", "newElement"),
Arrays.asList(Type.LIST, secondParameterType), Optional.of(Type.LIST), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
List<Operand> newList = new ArrayList<Operand>();
newList.addAll(operands.get(0).listVal());
newList.add(operands.get(1));
return Optional.of(new Operand(Type.LIST, newList));
}
});
}
private void addIntegerSeqFunction() {
String help = "Returns a list which is the sequence of the (inclusive) range\n"
+ "specified by the first and second parameters,\n"
+ "combined with the step, specified by the third parameter.";
// Return type will always be LIST here..
addFunctionExecutor(new FunctionExecutor(Optional.of("SEQ"), Arrays.asList("first", "last", "step"),
Arrays.asList(Type.INTEGER, Type.INTEGER, Type.INTEGER), Optional.of(Type.LIST), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
Long first = operands.get(0).intVal();
Long last = operands.get(1).intVal();
Long step = operands.get(2).intVal();
List<Operand> resultList = new ArrayList<Operand>();
for (long i = first; i <= last; i += step) {
resultList.add(new Operand(Type.INTEGER, i));
}
return Optional.of(new Operand(Type.LIST, resultList));
}
});
}
private void addRealSeqFunction() {
String help = "Returns a list which is the sequence of the range\n"
+ "specified by the first and second parameters,\n"
+ "combined with the step, specified by the third parameter.";
// Return type will always be LIST here..
addFunctionExecutor(new FunctionExecutor(Optional.of("SEQ"), Arrays.asList("firstIndex", "lastIndex", "step"),
Arrays.asList(Type.REAL, Type.REAL, Type.REAL), Optional.of(Type.LIST), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
Double first = operands.get(0).doubleVal();
Double last = operands.get(1).doubleVal();
Double step = operands.get(2).doubleVal();
List<Operand> resultList = new ArrayList<Operand>();
for (double i = first; i <= last; i += step) {
resultList.add(new Operand(Type.REAL, i));
}
return Optional.of(new Operand(Type.LIST, resultList));
}
});
}
private void addListMapFunction() {
String help = "Applies a function to each element of a list and returns a corresponding list.";
// Return type will always be LIST here.
addFunctionExecutor(new FunctionExecutor(Optional.of("MAP"), Arrays.asList("unaryFunction", "aList"),
Arrays.asList(Type.FUNCTION, Type.LIST), Optional.of(Type.LIST), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
FunctionExecutor fun = operands.get(0).functionVal();
List<Operand> list = operands.get(1).listVal();
List<Operand> resultList = new ArrayList<Operand>();
for (Operand item : list) {
List<Operand> params = Arrays.asList(item);
applyFunction(fun, params);
if (!stack.isEmpty()) {
resultList.add(stack.pop());
} else {
throw new VeLaEvalError("Expected function result");
}
}
return Optional.of(new Operand(Type.LIST, resultList));
}
});
}
private void addListFilterFunction() {
String help = "Applies a function (predicate) to each element of a list and returns\n"
+ "the subset of those elements that satisfy the predicate.";
// Return type will always be LIST here.
addFunctionExecutor(new FunctionExecutor(Optional.of("FILTER"), Arrays.asList("predicate", "aList"),
Arrays.asList(Type.FUNCTION, Type.LIST), Optional.of(Type.LIST), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
FunctionExecutor fun = operands.get(0).functionVal();
List<Operand> list = operands.get(1).listVal();
List<Operand> resultList = new ArrayList<Operand>();
for (Operand item : list) {
List<Operand> params = Arrays.asList(item);
applyFunction(fun, params);
if (!stack.isEmpty()) {
Operand retVal = stack.pop();
if (retVal.getType() == Type.BOOLEAN) {
if (retVal.booleanVal()) {
resultList.add(item);
}
} else {
throw new VeLaEvalError("Expected boolean value");
}
} else {
throw new VeLaEvalError("Expected boolean value");
}
}
return Optional.of(new Operand(Type.LIST, resultList));
}
});
}
private void addListFindFunction() {
// Return the index of the first element of a list matching a
// predicate, else -1.
String findHelp = "Return the index of the first element of a list matching a\n"
+ "predicate applied to a list element, else -1";
addFunctionExecutor(new FunctionExecutor(Optional.of("FIND"), Arrays.asList("unaryFunction", "aList"),
Arrays.asList(Type.FUNCTION, Type.LIST), Optional.of(Type.INTEGER), Optional.of(findHelp)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
FunctionExecutor fun = operands.get(0).functionVal();
List<Operand> list = operands.get(1).listVal();
int index = -1;
for (int i = 0; i < list.size(); i++) {
List<Operand> params = Arrays.asList(list.get(i));
applyFunction(fun, params);
if (!stack.isEmpty()) {
Operand retVal = stack.pop();
if (retVal.getType() == Type.BOOLEAN) {
if (retVal.booleanVal()) {
index = i;
break;
}
} else {
throw new VeLaEvalError("Expected boolean value");
}
} else {
throw new VeLaEvalError("Expected boolean value");
}
}
return Optional.of(new Operand(Type.INTEGER, index));
}
});
}
private void addListPairwiseFindFunction() {
// Return the index of the first element of a list matching a
// predicate applied to two list elements, else -1. Whereas FIND's predicate
// takes a single list element,
// PAIRWISEFIND takes two elements separated by a "step" value.
String pairwiseFindHelp = "Return the index of the first element of a list matching a\n"
+ "predicate applied to two list elements, else -1";
addFunctionExecutor(new FunctionExecutor(Optional.of("PAIRWISEFIND"),
Arrays.asList("unaryFunction", "aList", "step"), Arrays.asList(Type.FUNCTION, Type.LIST, Type.INTEGER),
Optional.of(Type.INTEGER), Optional.of(pairwiseFindHelp)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
FunctionExecutor fun = operands.get(0).functionVal();
List<Operand> list = operands.get(1).listVal();
long step = (int) operands.get(2).intVal();
int index = -1;
for (int i = 0; i < list.size() - 1; i += step) {
List<Operand> params = Arrays.asList(list.get(i), list.get(i + 1));
applyFunction(fun, params);
if (!stack.isEmpty()) {
Operand retVal = stack.pop();
if (retVal.getType() == Type.BOOLEAN) {
if (retVal.booleanVal()) {
index = i;
break;
}
} else {
throw new VeLaEvalError("Expected boolean value");
}
} else {
throw new VeLaEvalError("Expected boolean value");
}
}
return Optional.of(new Operand(Type.INTEGER, index));
}
});
}
private void addListReduceFunction(Type reductionType) {
String help = "Applies a function to each element of a list, returning\n"
+ "a single value. An initial value must be provided.";
// Return type will be same as function the parameter's type.
addFunctionExecutor(new FunctionExecutor(Optional.of("REDUCE"),
Arrays.asList("unaryFunction", "aList", "initialValue"),
Arrays.asList(Type.FUNCTION, Type.LIST, reductionType), Optional.of(reductionType), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
FunctionExecutor fun = operands.get(0).functionVal();
// Set return type on reduce dynamically each time.
setReturnType(fun.getReturnType());
List<Operand> list = operands.get(1).listVal();
Operand retVal = operands.get(2); // base value
for (Operand item : list) {
List<Operand> params = Arrays.asList(retVal, item);
applyFunction(fun, params);
if (!stack.isEmpty()) {
retVal = stack.pop();
}
}
return Optional.of(retVal);
}
});
}
private void addListForFunction() {
String help = "Invokes a function on each element of a list.";
// FOR should not return anything.
addFunctionExecutor(new FunctionExecutor(Optional.of("FOR"), Arrays.asList("unaryFunction", "aList"),
Arrays.asList(Type.FUNCTION, Type.LIST), Optional.empty(), Optional.of(help)) {
@Override
public Optional<Operand> apply(List<Operand> operands) {
FunctionExecutor fun = operands.get(0).functionVal();
List<Operand> list = operands.get(1).listVal();
for (Operand item : list) {
List<Operand> params = new ArrayList<Operand>();
params.add(item);
applyFunction(fun, params);
}
return Optional.empty();
}
});
}
/**
* Given a class, add non zero-arity VeLa type-compatible functions to the
* functions map.
*
* @param clazz The class from which to add function executors.
* @param instance The instance of this class on which to invoke the
* function.
* @param permittedTypes The set of Java types that are compatible with VeLa.
* @param exclusions Names of functions to exclude.
*/
public void addFunctionExecutorsFromClass(Class<?> clazz, Object instance, Set<Class<?>> permittedTypes,
Set<String> exclusions) {
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
String funcName = declaredMethod.getName().toUpperCase();
List<Class<?>> paramTypes = getJavaParameterTypes(declaredMethod, permittedTypes);
Class<?> returnType = declaredMethod.getReturnType();
// If the method is non-static, we need to include a parameter
// type for the object on which the method will be invoked.
if (!Modifier.isStatic(declaredMethod.getModifiers()) && instance == null) {
List<Class<?>> newParamTypes = new ArrayList<Class<?>>();
newParamTypes.add(clazz);
newParamTypes.addAll(paramTypes);
paramTypes = newParamTypes;
}
FunctionExecutor function = null;
if (!exclusions.contains(funcName) && permittedTypes.contains(returnType)) {
List<String> names = getJavaParameterNames(declaredMethod, permittedTypes);
List<Type> types = paramTypes.stream().map(t -> Type.java2Vela(t)).collect(Collectors.toList());
Optional<String> helpString = Optional.empty();
function = new JavaMethodExecutor(instance, declaredMethod, Optional.of(funcName), names, types,
Optional.of(Type.java2Vela(returnType)), helpString);
javaClassFunctionExecutors.add(function);
}
}
}
// TODO
// unify these two methods, e.g. via a Parameter class
private static List<String> getJavaParameterNames(Method method, Set<Class<?>> targetTypes) {
Parameter[] parameters = method.getParameters();
List<String> parameterNames = new ArrayList<String>();
for (Parameter parameter : parameters) {
String name = parameter.getName();
if (targetTypes.contains(parameter.getType())) {
parameterNames.add(name);
}
}
return parameterNames;
}
private static List<Class<?>> getJavaParameterTypes(Method method, Set<Class<?>> targetTypes) {
Parameter[] parameters = method.getParameters();
List<Class<?>> parameterTypes = new ArrayList<Class<?>>();
for (Parameter parameter : parameters) {
Class<?> type = parameter.getType();
if (targetTypes.contains(type)) {
parameterTypes.add(type);
}
}
return parameterTypes;
}
}