diff --git a/lisp/paydays.lisp b/lisp/paydays.lisp new file mode 100644 index 0000000..91e740f --- /dev/null +++ b/lisp/paydays.lisp @@ -0,0 +1,11 @@ +(let ((*first-payday-day* 6) + (*leap-year* 0) + (*days-in-year* 365) + (*two-weeks* 14)) + + (+ 1 + (/ (- (+ *days-in-year* + *leap-year*) + *first-payday-day*) + *two-weeks*)) +) diff --git a/src/function/builtin/special/LET.java b/src/function/builtin/special/LET.java index 26c6195..302b1bd 100644 --- a/src/function/builtin/special/LET.java +++ b/src/function/builtin/special/LET.java @@ -6,94 +6,82 @@ import sexpression.*; import table.*; public class LET extends LispFunction { - + + private ArgumentValidator argumentValidator; private ExecutionContext executionContext; - + private SymbolTable localScope; + public LET() { + this.argumentValidator = new ArgumentValidator("LET"); + this.argumentValidator.setMinimumNumberOfArguments(1); + this.argumentValidator.setFirstArgumentExpectedType(Cons.class); this.executionContext = ExecutionContext.getInstance(); } - public SExpression call(Cons argList) { - // make sure we have received at least one argument - if (argList.nullp()) { - throw new RuntimeException("too few arguments given to LET"); - } + public SExpression call(Cons argumentList) { + argumentValidator.validate(argumentList); + Cons variableDefinitions = (Cons) argumentList.getCar(); + Cons body = (Cons) argumentList.getCdr(); - // create a new symbol table on top of the current environment to add - // all the local variables to - SymbolTable environment = new SymbolTable(executionContext.getScope()); - - SExpression car = argList.getCar(); - Cons cdr = (Cons) argList.getCdr(); - - addVariablesToTable(environment, car); - executionContext.setScope(environment); - - SExpression retval = Nil.getInstance(); - - // evaluate all S-expression in the body - while (cdr.consp()) { - retval = EVAL.eval(cdr.getCar()); - cdr = (Cons) cdr.getCdr(); - } - - // restore the environment to its original value - executionContext.setScope(environment.getParent()); - - return retval; + return evaluateInScope(variableDefinitions, body); } - // Add a list of variables and their values to the specified symbol table. - // - // Parameters: environment - the symbol table to add the variables and - // their values to (must not be null) - // vars - a list of variable/value pairs (must be either a - // proper list of pairs or NIL) - // Throws: RuntimeException - Indicates that 'vars' is not a proper list or - // that it contains an member that is not a - // variable/value pair. - // Precondition: 'environment' and 'vars' must not be null. - // Postcondition: All of the variables in 'vars' have been placed into - // 'environment' with their values. - private void addVariablesToTable(SymbolTable environment, SExpression vars) { - // makes sure the list of variable/value pairs is a list - if (!vars.listp()) { - throw new RuntimeException("LET: " + vars + " is not a properly formatted" + " variable/value pair list"); + private SExpression evaluateInScope(Cons variableDefinitions, Cons body) { + createLocalScope(variableDefinitions); + SExpression lastEvaluation = evaluateBody(body); + restorePreviousScope(); + + return lastEvaluation; + } + + private SymbolTable createLocalScope(Cons variableDefinitions) { + localScope = new SymbolTable(executionContext.getScope()); + addVariablesToScope(localScope, variableDefinitions); + executionContext.setScope(localScope); + + return localScope; + } + + private void addVariablesToScope(SymbolTable scope, Cons variableDefinitions) { + validateAllPairsAreLists(variableDefinitions); + + for (; variableDefinitions.consp(); variableDefinitions = (Cons) variableDefinitions.getCdr()) { + Cons symbolValuePair = (Cons) variableDefinitions.getCar(); + + validatePair(symbolValuePair); + + Cons restOfPair = (Cons) symbolValuePair.getCdr(); + SExpression symbol = symbolValuePair.getCar(); + SExpression value = restOfPair.getCar(); + + scope.put(symbol.toString(), EVAL.eval(value)); } + } - // add all variables in 'vars' to 'environment' - while (vars.consp()) { - Cons varList = (Cons) vars; - SExpression varListCar = varList.getCar(); + private void validateAllPairsAreLists(Cons variableDefinitions) { + ArgumentValidator variableDefinitionListValidator = new ArgumentValidator("LET|argumentList|"); + variableDefinitionListValidator.setEveryArgumentExpectedType(Cons.class); + variableDefinitionListValidator.validate(variableDefinitions); + } - // make sure this variable/value pair is a list - if (!varListCar.consp()) { - throw new RuntimeException("LET: " + varListCar + " is not a properly formatted" - + " variable/value pair"); - } + private void validatePair(Cons symbolValuePair) { + ArgumentValidator pairValidator = new ArgumentValidator("LET|pair|"); + pairValidator.setExactNumberOfArguments(2); + pairValidator.setFirstArgumentExpectedType(Symbol.class); + pairValidator.validate((Cons) symbolValuePair); + } - Cons varSpec = (Cons) varListCar; - SExpression symbol = varSpec.getCar(); - SExpression varSpecCdr = varSpec.getCdr(); + private SExpression evaluateBody(Cons body) { + SExpression lastEvaluation = Nil.getInstance(); - // make sure this variable pair has a value associated with it - if (!varSpecCdr.consp()) { - throw new RuntimeException("LET: illegal variable " + "specification " + varSpec); - } + for (; body.consp(); body = (Cons) body.getCdr()) + lastEvaluation = EVAL.eval(body.getCar()); - Cons varValue = (Cons) varSpecCdr; - SExpression value = varValue.getCar(); + return lastEvaluation; + } - // make sure there are no more members of this variable/value pair - // and that 'symbol' is actually a symbol - if ((!varValue.getCdr().nullp()) || (!symbol.symbolp())) { - throw new RuntimeException("LET: illegal variable " + "specification " + varSpec); - } - - environment.put(symbol.toString(), value); - - vars = varList.getCdr(); - } + private void restorePreviousScope() { + executionContext.setScope(localScope.getParent()); } public boolean evaluateArguments() { diff --git a/test/function/builtin/special/LETTester.java b/test/function/builtin/special/LETTester.java new file mode 100644 index 0000000..bfc3814 --- /dev/null +++ b/test/function/builtin/special/LETTester.java @@ -0,0 +1,141 @@ +package function.builtin.special; + +import static testutil.TestUtilities.*; + +import org.junit.*; + +import function.ArgumentValidator.*; +import function.builtin.EVAL.UndefinedSymbolException; +import sexpression.*; +import table.ExecutionContext; + +public class LETTester { + + private ExecutionContext executionContext; + + public LETTester() { + this.executionContext = ExecutionContext.getInstance(); + } + + @Before + public void setUp() { + executionContext.clearContext(); + } + + @Test + public void simpleLet() { + String input = "(let ((x 1)) x)"; + + assertSExpressionsMatch(new LispNumber("1"), evaluateString(input)); + } + + @Test + public void emptyLet_ReturnsNil() { + String input = "(let ())"; + + assertSExpressionsMatch(Nil.getInstance(), evaluateString(input)); + } + + @Test + public void letWithSetf_DoesNotAlterGlobalVariable() { + String before = "(setf x 22)"; + String input = "(let ((x 1)) x)"; + String after = "x"; + + assertSExpressionsMatch(new LispNumber("22"), evaluateString(before)); + assertSExpressionsMatch(new LispNumber("1"), evaluateString(input)); + assertSExpressionsMatch(new LispNumber("22"), evaluateString(after)); + } + + @Test + public void letWithNestedSetf_DoesNotAlterGlobalVariable() { + String before = "(setf x 22)"; + String input = "(let ((x 33)) (setf x 44) x)"; + String after = "x"; + + assertSExpressionsMatch(new LispNumber("22"), evaluateString(before)); + assertSExpressionsMatch(new LispNumber("44"), evaluateString(input)); + assertSExpressionsMatch(new LispNumber("22"), evaluateString(after)); + } + + @Test + public void nestedLet() { + String input = "(let ((x 1)) (let ((y (+ 1 x))) y))"; + + assertSExpressionsMatch(new LispNumber("2"), evaluateString(input)); + } + + @Test + public void alterGlobalVariableFromLet() { + String before = "(setf x 1)"; + String input = "(let ((y 1)) (setf x 2))"; + String after = "x"; + + assertSExpressionsMatch(new LispNumber("1"), evaluateString(before)); + assertSExpressionsMatch(new LispNumber("2"), evaluateString(input)); + assertSExpressionsMatch(new LispNumber("2"), evaluateString(after)); + } + + @Test + public void accessGlobalVariableFromLet() { + String before = "(setf x 1)"; + String input = "(let () x)"; + + assertSExpressionsMatch(new LispNumber("1"), evaluateString(before)); + assertSExpressionsMatch(new LispNumber("1"), evaluateString(input)); + } + + @Test(expected = UndefinedSymbolException.class) + public void letDoesNotSetGlobalVariable() { + String input = "(let ((x 1)) nil)"; + + evaluateString(input); + evaluateString("x"); + } + + @Test(expected = BadArgumentTypeException.class) + public void testLetWithNonList() { + evaluateString("(let a)"); + } + + @Test(expected = BadArgumentTypeException.class) + public void testLetWithNoPairs() { + evaluateString("(let (a))"); + } + + @Test(expected = TooFewArgumentsException.class) + public void testLetWithTooFewItemsInPair() { + evaluateString("(let ((a)))"); + } + + @Test(expected = TooManyArgumentsException.class) + public void testLetWithTooManyItemsInPair() { + evaluateString("(let ((a b c)))"); + } + + @Test(expected = BadArgumentTypeException.class) + public void testLetWithNonSymbolInPair() { + evaluateString("(let ((1 b)))"); + } + + @Test(expected = TooFewArgumentsException.class) + public void testLetWithTooFewArguments() { + evaluateString("(let)"); + } + + @Test(expected = DottedArgumentListException.class) + public void testLetWithDottedArgumentList() { + evaluateString("(apply 'let (cons 'a 'b))"); + } + + @Test(expected = DottedArgumentListException.class) + public void testLetWithDottedPairList() { + evaluateString("(apply 'let (cons (cons 'a 'b) nil))"); + } + + @Test(expected = DottedArgumentListException.class) + public void testLetWithDottedPair() { + evaluateString("(apply 'let (cons (cons (cons 'a 'b) nil) nil))"); + } + +}