diff --git a/src/function/ArgumentValidator.java b/src/function/ArgumentValidator.java index 54ec05d..ba76318 100644 --- a/src/function/ArgumentValidator.java +++ b/src/function/ArgumentValidator.java @@ -8,20 +8,31 @@ import sexpression.*; public class ArgumentValidator { - private Class argumentType; + private Class firstArgumentType; + private Class trailingArgumentType; private String functionName; private Integer maximumNumberOfArguments; private Integer minimumNumberOfArguments; public ArgumentValidator(String functionName) { - this.argumentType = SExpression.class; + this.firstArgumentType = SExpression.class; + this.trailingArgumentType = SExpression.class; this.functionName = functionName; this.minimumNumberOfArguments = null; this.maximumNumberOfArguments = null; } - public void setArgumentType(Class argumentType) { - this.argumentType = argumentType; + public void setFirstArgumentExpectedType(Class argumentType) { + this.firstArgumentType = argumentType; + } + + public void setTrailingArgumentExpectedType(Class argumentType) { + this.trailingArgumentType = argumentType; + } + + public void setEveryArgumentExpectedType(Class argumentType) { + this.firstArgumentType = argumentType; + this.trailingArgumentType = argumentType; } public void setMaximumNumberOfArguments(int maximumNumberOfArguments) { @@ -38,12 +49,27 @@ public class ArgumentValidator { } public void validate(Cons argumentList) { + validateListNotDotted(argumentList); + if (containsTooFewArguments(argumentList)) throw new TooFewArgumentsException(functionName, argumentList); else if (containsTooManyArguments(argumentList)) throw new TooManyArgumentsException(functionName, argumentList); - else if (!isExpectedArgumentType(argumentList.getCar())) - throw new BadArgumentTypeException(functionName, argumentList.getCar()); + + validateArgumentTypes(argumentList); + } + + private void validateListNotDotted(Cons argumentList) { + Cons currentCons = argumentList; + SExpression nextCons = argumentList.getCdr(); + + while (!nextCons.nullp()) { + if (!nextCons.consp()) + throw new DottedArgumentListException(functionName, argumentList); + + currentCons = (Cons) nextCons; + nextCons = currentCons.getCdr(); + } } private boolean containsTooFewArguments(Cons argumentList) { @@ -54,8 +80,25 @@ public class ArgumentValidator { return (maximumNumberOfArguments != null) && (LENGTH.getLength(argumentList) > maximumNumberOfArguments); } - private boolean isExpectedArgumentType(SExpression firstArgument) { - return argumentType.isInstance(firstArgument); + private void validateArgumentTypes(Cons argumentList) { + if (!isExpectedFirstArgumentType(argumentList.getCar())) + throw new BadArgumentTypeException(functionName, argumentList.getCar(), firstArgumentType); + + validateRemainingArguments(argumentList); + } + + private boolean isExpectedFirstArgumentType(SExpression firstArgument) { + return firstArgumentType.isInstance(firstArgument); + } + + private void validateRemainingArguments(Cons cons) { + for (cons = (Cons) cons.getCdr(); !cons.nullp(); cons = (Cons) cons.getCdr()) + if (!isExpectedRemainingArgumentType(cons.getCar())) + throw new BadArgumentTypeException(functionName, cons.getCar(), trailingArgumentType); + } + + private boolean isExpectedRemainingArgumentType(SExpression remainingArgument) { + return trailingArgumentType.isInstance(remainingArgument); } public static class TooFewArgumentsException extends LispException { @@ -92,20 +135,44 @@ public class ArgumentValidator { } } + public static class DottedArgumentListException extends LispException { + + private static final long serialVersionUID = 1L; + private String functionName; + private Cons originalSExpression; + + public DottedArgumentListException(String functionName, Cons argumentList) { + this.functionName = functionName; + this.originalSExpression = new Cons(new Symbol(this.functionName), argumentList); + } + + @Override + public String getMessage() { + return MessageFormat.format("dotted argument list given to {0}: {1}", functionName, originalSExpression); + } + } + public static class BadArgumentTypeException extends LispException { private static final long serialVersionUID = 1L; private String functionName; private String argument; + private Class expected; - public BadArgumentTypeException(String functionName, SExpression argument) { + public BadArgumentTypeException(String functionName, SExpression argument, + Class expected) { this.functionName = functionName; this.argument = argument.toString(); + this.expected = expected; } @Override public String getMessage() { - return MessageFormat.format("{0}: {1} is not the expected type", functionName, argument); + DisplayName displayName = expected.getAnnotation(DisplayName.class); + String expectedType = (displayName == null) ? "unknown" : displayName.value(); + + return MessageFormat.format("{0}: {1} is not the expected type of ''{2}''", functionName, argument, + expectedType); } } diff --git a/src/function/UserDefinedFunction.java b/src/function/UserDefinedFunction.java index fd7e9ee..b276dde 100644 --- a/src/function/UserDefinedFunction.java +++ b/src/function/UserDefinedFunction.java @@ -8,32 +8,20 @@ import table.SymbolTable; public class UserDefinedFunction extends LispFunction { - private ArgumentValidator argumentValidator; private String name; private Cons body; private Cons lambdaExpression; private SymbolTable environment; private ArrayList formalParameters; + private ArgumentValidator argumentValidator; - /** - * Create a new user-defined function with the specified name, lambda list and body. - * - * @param name - * the name of this user-defined function - * @param lambdaList - * a list of the formal parameters of this user-defined function (MUST BE A PROPER - * LIST) - * @param body - * the body of this user-defined function (MUST BE A PROPER LIST) - */ public UserDefinedFunction(String name, Cons lambdaList, Cons body) { this.name = name; this.body = body; this.lambdaExpression = new Cons(new Symbol(name), new Cons(lambdaList, body)); this.environment = SETF.getEnvironment(); - this.formalParameters = new ArrayList(); - for (; lambdaList.consp(); lambdaList = (Cons) lambdaList.getCdr()) + for (this.formalParameters = new ArrayList<>(); lambdaList.consp(); lambdaList = (Cons) lambdaList.getCdr()) this.formalParameters.add(lambdaList.getCar().toString()); this.argumentValidator = new ArgumentValidator(this.name); diff --git a/src/function/builtin/APPLY.java b/src/function/builtin/APPLY.java index da9623b..33ee0dc 100644 --- a/src/function/builtin/APPLY.java +++ b/src/function/builtin/APPLY.java @@ -5,7 +5,6 @@ import sexpression.*; public class APPLY extends LispFunction { - private static final int NUMBER_OF_ARGUMENTS = 2; private ArgumentValidator argumentValidator; public static SExpression apply(Cons argList) { @@ -14,7 +13,8 @@ public class APPLY extends LispFunction { public APPLY() { this.argumentValidator = new ArgumentValidator("APPLY"); - this.argumentValidator.setExactNumberOfArguments(NUMBER_OF_ARGUMENTS); + this.argumentValidator.setExactNumberOfArguments(2); + this.argumentValidator.setTrailingArgumentExpectedType(Cons.class); } public SExpression call(Cons argList) { @@ -23,26 +23,9 @@ public class APPLY extends LispFunction { Cons cdr = (Cons) argList.getCdr(); SExpression functionName = argList.getCar(); SExpression argumentList = cdr.getCar(); + LispFunction function = EVAL.lookupFunctionOrLambda(functionName); - if (argumentList.listp()) { - LispFunction function = EVAL.lookupFunction(functionName.toString()); - - if (function == null) { - if (functionName.functionp()) { - function = ((LambdaExpression) functionName).getFunction(); - } else if (LAMBDA.isLambdaExpression(functionName)) { - Cons lexpr = (Cons) functionName; - - function = LAMBDA.createFunction(lexpr); - } else { - throw new RuntimeException("undefined function " + functionName); - } - } - - return function.call((Cons) argumentList); - } - - throw new RuntimeException("APPLY: " + argumentList + " is not a list"); + return function.call((Cons) argumentList); } } diff --git a/src/function/builtin/ATOM.java b/src/function/builtin/ATOM.java index 785f680..1cb8775 100644 --- a/src/function/builtin/ATOM.java +++ b/src/function/builtin/ATOM.java @@ -5,12 +5,11 @@ import sexpression.*; public class ATOM extends LispFunction { - private static final int NUMBER_OF_ARGUMENTS = 1; private ArgumentValidator argumentValidator; public ATOM() { this.argumentValidator = new ArgumentValidator("ATOM"); - this.argumentValidator.setExactNumberOfArguments(NUMBER_OF_ARGUMENTS); + this.argumentValidator.setExactNumberOfArguments(1); } public SExpression call(Cons argumentList) { diff --git a/src/function/builtin/EVAL.java b/src/function/builtin/EVAL.java index 4f01731..d965588 100644 --- a/src/function/builtin/EVAL.java +++ b/src/function/builtin/EVAL.java @@ -2,19 +2,14 @@ package function.builtin; import java.util.HashMap; -import function.*; +import function.LispFunction; import sexpression.*; -/** - * EVAL represents the EVAL function in Lisp. - */ public class EVAL extends LispFunction { - // A table to contain all the built-in and user-defined Lisp functions. private static HashMap functionTable = new HashMap(); static { - // place all of the built-in functions into the function table functionTable.put("*", new MULTIPLY()); functionTable.put("+", new PLUS()); functionTable.put("-", new MINUS()); @@ -50,33 +45,32 @@ public class EVAL extends LispFunction { functionTable.put("SYMBOL-FUNCTION", new SYMBOL_FUNCTION()); } - /** - * Retrieve the function table. - * - * @return the function table - */ public static HashMap getFunctionTable() { return functionTable; } - /** - * Look up a function by its name. - * - * @param functionName - * the name of the function to look up - * @return the function with the name functionName if it exists; null otherwise - */ public static LispFunction lookupFunction(String functionName) { return functionTable.get(functionName); } - /** - * Look up a symbol's value using its name. - * - * @param symbolName - * the name of the symbol to look up (must not be null) - * @return the value of symbolName if it has one; null otherwise - */ + public static LispFunction lookupFunctionOrLambda(SExpression functionExpression) { + LispFunction function = lookupFunction(functionExpression.toString()); + + if (function == null) + function = createLambdaFunction(functionExpression); + + return function; + } + + private static LispFunction createLambdaFunction(SExpression lambdaExpression) { + if (lambdaExpression.functionp()) + return ((LambdaExpression) lambdaExpression).getFunction(); + else if (LAMBDA.isLambdaExpression(lambdaExpression)) + return LAMBDA.createFunction((Cons) lambdaExpression); + else + throw new RuntimeException("undefined function " + lambdaExpression); + } + public static SExpression lookupSymbol(String symbolName) { if (symbolName.equals("NIL")) { return Nil.getUniqueInstance(); @@ -89,13 +83,6 @@ public class EVAL extends LispFunction { return SETF.lookup(symbolName); } - /** - * Determine if the given list is dotted. - * - * @param list - * the list to be tested (must not be null) - * @return true if list is dotted; false otherwise - */ public static boolean isDotted(Cons list) { if (list.nullp()) { return false; @@ -111,13 +98,6 @@ public class EVAL extends LispFunction { return true; } - /** - * Evaluate the given S-expression. - * - * @param sexpr - * the S-expression to evaluate - * @return the value of sexpr - */ public static SExpression eval(SExpression sexpr) { Cons expList = LIST.makeList(sexpr); EVAL evalFunction = new EVAL(); @@ -125,7 +105,6 @@ public class EVAL extends LispFunction { return evalFunction.call(expList); } - // The number of arguments that EVAL takes. private static final int NUM_ARGS = 1; public SExpression call(Cons argList) { @@ -164,29 +143,11 @@ public class EVAL extends LispFunction { return arg; // 'arg' is a NUMBER or a STRING } - // Evaluate the specified list. - // - // Parameters: list - the list to evaluate - // Returns: the value of 'list' - // Precondition: 'list' must not be null. private SExpression evaluateList(Cons list) { SExpression car = list.getCar(); SExpression cdr = list.getCdr(); - LispFunction function = lookupFunction(car.toString()); - - if (function == null) { - // check if the car of the list is a lambda expression - if (car.functionp()) { - function = ((LambdaExpression) car).getFunction(); - } else if (LAMBDA.isLambdaExpression(car)) { - Cons lexpr = (Cons) car; - - function = LAMBDA.createFunction(lexpr); - } else { - throw new RuntimeException("undefined function " + car); - } - } + LispFunction function = lookupFunctionOrLambda(car); // make sure the list of arguments for 'function' is a list if (cdr.listp()) { @@ -210,12 +171,6 @@ public class EVAL extends LispFunction { throw new RuntimeException("argument list given to " + car + " is dotted: " + list); } - // Evaluate a list of arguments for a function. - // - // Parameters: arguments - a list of arguments for a function - // Returns: a list consisting of the values of the S-expressions found in - // 'arguments' - // Precondition: 'arguments' must not be null. private Cons evaluateArgList(Cons arguments) { if (arguments.nullp()) { return Nil.getUniqueInstance(); diff --git a/src/function/builtin/PLUS.java b/src/function/builtin/PLUS.java index 82adc29..4d784bb 100644 --- a/src/function/builtin/PLUS.java +++ b/src/function/builtin/PLUS.java @@ -3,9 +3,6 @@ package function.builtin; import function.LispFunction; import sexpression.*; -/** - * PLUS represents the '+' function in Lisp. - */ public class PLUS extends LispFunction { public LispNumber call(Cons argList) { @@ -13,6 +10,9 @@ public class PLUS extends LispFunction { return new LispNumber(0); } + if (!argList.getCdr().listp()) + throw new RuntimeException("+: " + argList + " is dotted"); + SExpression argFirst = argList.getCar(); Cons argRest = (Cons) argList.getCdr(); diff --git a/src/sexpression/Atom.java b/src/sexpression/Atom.java index 2c9d9e5..8e969e7 100644 --- a/src/sexpression/Atom.java +++ b/src/sexpression/Atom.java @@ -1,5 +1,6 @@ package sexpression; +@DisplayName("atom") public abstract class Atom extends SExpression { private String text; diff --git a/src/sexpression/Cons.java b/src/sexpression/Cons.java index 00f5190..cd0dee1 100644 --- a/src/sexpression/Cons.java +++ b/src/sexpression/Cons.java @@ -1,5 +1,6 @@ package sexpression; +@DisplayName("list") public class Cons extends SExpression { private SExpression car; diff --git a/src/sexpression/DisplayName.java b/src/sexpression/DisplayName.java new file mode 100644 index 0000000..45e2079 --- /dev/null +++ b/src/sexpression/DisplayName.java @@ -0,0 +1,11 @@ +package sexpression; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DisplayName { + + String value(); + +} diff --git a/src/sexpression/LambdaExpression.java b/src/sexpression/LambdaExpression.java index 8055e46..63a20fb 100644 --- a/src/sexpression/LambdaExpression.java +++ b/src/sexpression/LambdaExpression.java @@ -2,6 +2,7 @@ package sexpression; import function.UserDefinedFunction; +@DisplayName("lambda-expression") public class LambdaExpression extends SExpression { private Cons lambdaExpression; diff --git a/src/sexpression/LispNumber.java b/src/sexpression/LispNumber.java index 3860056..7b122f4 100644 --- a/src/sexpression/LispNumber.java +++ b/src/sexpression/LispNumber.java @@ -4,6 +4,7 @@ import java.text.MessageFormat; import error.LispException; +@DisplayName("number") public class LispNumber extends Atom { private int value; diff --git a/src/sexpression/LispString.java b/src/sexpression/LispString.java index 3a148d1..db5524f 100644 --- a/src/sexpression/LispString.java +++ b/src/sexpression/LispString.java @@ -1,5 +1,6 @@ package sexpression; +@DisplayName("string") public class LispString extends Atom { public LispString(String text) { diff --git a/src/sexpression/Nil.java b/src/sexpression/Nil.java index ab2c529..49d9388 100644 --- a/src/sexpression/Nil.java +++ b/src/sexpression/Nil.java @@ -1,5 +1,6 @@ package sexpression; +@DisplayName("nil") public class Nil extends Cons { private static Nil uniqueInstance = new Nil(); diff --git a/src/sexpression/SExpression.java b/src/sexpression/SExpression.java index cb86b15..548a412 100644 --- a/src/sexpression/SExpression.java +++ b/src/sexpression/SExpression.java @@ -1,5 +1,6 @@ package sexpression; +@DisplayName("s-expression") public abstract class SExpression { public boolean nullp() { diff --git a/src/sexpression/Symbol.java b/src/sexpression/Symbol.java index 4b5e157..d4f618a 100644 --- a/src/sexpression/Symbol.java +++ b/src/sexpression/Symbol.java @@ -1,5 +1,6 @@ package sexpression; +@DisplayName("symbol") public class Symbol extends Atom { public static final Symbol T = new Symbol("T"); diff --git a/test/function/ArgumentValidatorTester.java b/test/function/ArgumentValidatorTester.java index 3193644..6beba37 100644 --- a/test/function/ArgumentValidatorTester.java +++ b/test/function/ArgumentValidatorTester.java @@ -103,14 +103,14 @@ public class ArgumentValidatorTester { @Test public void BadArgumentTypeException_HasCorrectSeverity() { - BadArgumentTypeException e = new BadArgumentTypeException("TEST", Nil.getUniqueInstance()); + BadArgumentTypeException e = new BadArgumentTypeException("TEST", Nil.getUniqueInstance(), SExpression.class); assertTrue(e.getSeverity() < ErrorManager.CRITICAL_LEVEL); } @Test public void BadArgumentTypeException_HasMessageText() { - BadArgumentTypeException e = new BadArgumentTypeException("TEST", Nil.getUniqueInstance()); + BadArgumentTypeException e = new BadArgumentTypeException("TEST", Nil.getUniqueInstance(), SExpression.class); assertNotNull(e.getMessage()); assertTrue(e.getMessage().length() > 0); @@ -118,14 +118,70 @@ public class ArgumentValidatorTester { @Test public void correctArgumentType_DoesNotThrowException() { - validator.setArgumentType(Nil.class); + validator.setEveryArgumentExpectedType(Nil.class); validator.validate(makeArgumentListOfSize(1)); } @Test(expected = BadArgumentTypeException.class) public void badArgumentType_ThrowsException() { - validator.setArgumentType(LispString.class); + validator.setEveryArgumentExpectedType(LispString.class); validator.validate(makeArgumentListOfSize(1)); } + @Test + public void correctFirstAndRestArgumentTypes_DoesNotThrowException() { + Cons argumentList = new Cons(Symbol.T, new Cons(Nil.getUniqueInstance(), Nil.getUniqueInstance())); + + validator.setFirstArgumentExpectedType(Symbol.class); + validator.setTrailingArgumentExpectedType(Cons.class); + validator.validate(argumentList); + } + + @Test(expected = BadArgumentTypeException.class) + public void badFirstArgumentType_ThrowsException() { + Cons argumentList = new Cons(Symbol.T, new Cons(Nil.getUniqueInstance(), Nil.getUniqueInstance())); + + validator.setFirstArgumentExpectedType(Cons.class); + validator.setTrailingArgumentExpectedType(Cons.class); + validator.validate(argumentList); + } + + @Test(expected = BadArgumentTypeException.class) + public void badTrailingArgumentType_ThrowsException() { + Cons argumentList = new Cons(Symbol.T, new Cons(Nil.getUniqueInstance(), Nil.getUniqueInstance())); + + validator.setFirstArgumentExpectedType(Symbol.class); + validator.setTrailingArgumentExpectedType(Symbol.class); + validator.validate(argumentList); + } + + @Test + public void expectedTypeWithNoDisplayName_DoesNotCauseNPE() { + Cons argumentList = new Cons(Symbol.T, new Cons(Nil.getUniqueInstance(), Nil.getUniqueInstance())); + SExpression withoutDisplayName = new SExpression() {}; + + validator.setEveryArgumentExpectedType(withoutDisplayName.getClass()); + + try { + validator.validate(argumentList); + } catch (BadArgumentTypeException e) { + e.getMessage(); + } + } + + @Test(expected = DottedArgumentListException.class) + public void givenDottedArgumentList_ThrowsException() { + Cons argumentList = new Cons(Symbol.T, Symbol.T); + + validator.validate(argumentList); + } + + @Test + public void dottedArgumentListException_HasMessageText() { + DottedArgumentListException e = new DottedArgumentListException("TEST", Nil.getUniqueInstance()); + + assertNotNull(e.getMessage()); + assertTrue(e.getMessage().length() > 0); + } + } diff --git a/test/function/builtin/APPLYTester.java b/test/function/builtin/APPLYTester.java index beac56d..9b1b62d 100644 --- a/test/function/builtin/APPLYTester.java +++ b/test/function/builtin/APPLYTester.java @@ -43,7 +43,7 @@ public class APPLYTester { evaluateString("(apply 'f '(1 2 3))"); } - @Test(expected = RuntimeException.class) + @Test(expected = BadArgumentTypeException.class) public void testApplyWithNonListSecondArgument() { evaluateString("(apply '+ '2)"); }