diff --git a/src/function/builtin/predicate/EQUAL.java b/src/function/builtin/predicate/EQUAL.java index c44784f..6ad5efe 100644 --- a/src/function/builtin/predicate/EQUAL.java +++ b/src/function/builtin/predicate/EQUAL.java @@ -6,6 +6,10 @@ import sexpression.*; @FunctionNames({ "EQUAL" }) public class EQUAL extends LispFunction { + public static boolean isEqual(SExpression firstArgument, SExpression secondArgument) { + return firstArgument.toString().equals(secondArgument.toString()); + } + private ArgumentValidator argumentValidator; public EQUAL() { @@ -27,8 +31,4 @@ public class EQUAL extends LispFunction { return isEqual(firstArgument, secondArgument) ? Symbol.T : Nil.getInstance(); } - private boolean isEqual(SExpression firstArgument, SExpression secondArgument) { - return firstArgument.toString().equals(secondArgument.toString()); - } - } diff --git a/src/function/builtin/special/CASE.java b/src/function/builtin/special/CASE.java new file mode 100644 index 0000000..e387140 --- /dev/null +++ b/src/function/builtin/special/CASE.java @@ -0,0 +1,80 @@ +package function.builtin.special; + +import static function.builtin.EVAL.eval; +import static function.builtin.predicate.EQUAL.isEqual; + +import function.*; +import sexpression.*; + +@FunctionNames({ "CASE" }) +public class CASE extends LispSpecialFunction { + + private ArgumentValidator argumentValidator; + + public CASE() { + this.argumentValidator = new ArgumentValidator("CASE"); + this.argumentValidator.setMinimumNumberOfArguments(1); + this.argumentValidator.setTrailingArgumentExpectedType(Cons.class); + this.argumentValidator.doNotAcceptNil(); + } + + public SExpression call(Cons argumentList) { + argumentValidator.validate(argumentList); + SExpression key = eval(argumentList.getFirst()); + + return callTailRecursive(key, (Cons) argumentList.getRest()); + } + + private SExpression callTailRecursive(SExpression key, Cons argumentList) { + if (argumentList.isNull()) + return Nil.getInstance(); + + Cons clause = (Cons) argumentList.getFirst(); + Cons remainingClauses = (Cons) argumentList.getRest(); + SExpression keyList = clause.getFirst(); + + if (isMatch(key, keyList)) + return evaluateResult(clause); + + return callTailRecursive(key, remainingClauses); + } + + private boolean isMatch(SExpression key, SExpression keyList) { + if (keyList.isNull()) + return false; + else if (keyList.isCons()) + return containsMatch(key, keyList); + + return isEqual(key, keyList) || isKeyListT(keyList); + } + + private boolean containsMatch(SExpression key, SExpression keyList) { + for (; keyList.isCons(); keyList = advanceCons(keyList)) + if (isEqual(key, getFirst(keyList))) + return true; + + return false; + } + + private boolean isKeyListT(SExpression keyList) { + return isEqual(Symbol.T, keyList); + } + + private SExpression advanceCons(SExpression knownCons) { + return ((Cons) knownCons).getRest(); + } + + private SExpression getFirst(SExpression knownCons) { + return ((Cons) knownCons).getFirst(); + } + + private SExpression evaluateResult(Cons clause) { + SExpression lastResultValue = Nil.getInstance(); + + for (SExpression result = clause.getRest(); result.isCons(); result = advanceCons(result)) + lastResultValue = eval(getFirst(result)); + + return lastResultValue; + } + +} diff --git a/src/table/FunctionTable.java b/src/table/FunctionTable.java index d880a35..43a57ed 100644 --- a/src/table/FunctionTable.java +++ b/src/table/FunctionTable.java @@ -39,6 +39,7 @@ public class FunctionTable { allBuiltIns.add(AND.class); allBuiltIns.add(APPLY.class); allBuiltIns.add(ATOM.class); + allBuiltIns.add(CASE.class); allBuiltIns.add(COND.class); allBuiltIns.add(CONS.class); allBuiltIns.add(DEFINE_MACRO.class); diff --git a/test/function/builtin/special/CASETester.java b/test/function/builtin/special/CASETester.java new file mode 100644 index 0000000..3e79ec3 --- /dev/null +++ b/test/function/builtin/special/CASETester.java @@ -0,0 +1,149 @@ +package function.builtin.special; + +import static testutil.TestUtilities.*; + +import org.junit.*; + +import function.ArgumentValidator.*; +import table.ExecutionContext; + +public class CASETester { + + private ExecutionContext executionContext; + + public CASETester() { + this.executionContext = ExecutionContext.getInstance(); + } + + @Before + public void setUp() { + executionContext.clearContext(); + } + + @After + public void tearDown() { + executionContext.clearContext(); + } + + @Test + public void caseWithOnlySwitchExpression() { + String input = "(case t)"; + + assertSExpressionsMatch(parseString("nil"), evaluateString(input)); + } + + @Test + public void caseWithEmptyConsequent() { + String input = "(case :a ((:a)))"; + + assertSExpressionsMatch(parseString("nil"), evaluateString(input)); + } + + @Test + public void caseWithOneClause_Match() { + String input = "(case :a ((:a) 'banana))"; + + assertSExpressionsMatch(parseString("banana"), evaluateString(input)); + } + + @Test + public void caseWithOneClause_NoMatch() { + String input = "(case :a ((:b) 'banana))"; + + assertSExpressionsMatch(parseString("nil"), evaluateString(input)); + } + + @Test + public void caseWithSeveralClauses_Match() { + String input = "(case :a ((:b) 'orange) ((:a) 'banana))"; + + assertSExpressionsMatch(parseString("banana"), evaluateString(input)); + } + + @Test + public void caseWithSeveralClauses_NoMatch() { + String input = "(case :a ((:b) 'orange) ((:c) 'banana))"; + + assertSExpressionsMatch(parseString("nil"), evaluateString(input)); + } + + @Test + public void caseWithSymbolicKeyList_Match() { + String input = "(case :a (:a 'orange))"; + + assertSExpressionsMatch(parseString("orange"), evaluateString(input)); + } + + @Test + public void caseWithSymbolicKeyList_NoMatch() { + String input = "(case :a (:b 'orange))"; + + assertSExpressionsMatch(parseString("nil"), evaluateString(input)); + } + + @Test + public void caseWithEmptyKeyList() { + String input = "(case nil (() 'orange))"; + + assertSExpressionsMatch(parseString("nil"), evaluateString(input)); + } + + @Test + public void caseWithNil() { + String input = "(case nil ((nil) 'orange))"; + + assertSExpressionsMatch(parseString("orange"), evaluateString(input)); + } + + @Test + public void caseWithDefaultClause() { + String input = "(case nil (() 'banana) (t 'orange))"; + + assertSExpressionsMatch(parseString("orange"), evaluateString(input)); + } + + @Test + public void caseWithOutOfOrderDefaultClause() { + String input = "(case :a (t 'orange) (:a 'banana))"; + + assertSExpressionsMatch(parseString("orange"), evaluateString(input)); + } + + @Test + public void caseWithKeyListContainingT() { + String input = "(case t ((t) 'banana))"; + + assertSExpressionsMatch(parseString("banana"), evaluateString(input)); + } + + @Test + public void caseWithMultipleMatches_ReturnsFirst() { + String input = "(case 2 ((0) 'banana) ((1) 'apple) ((2) 'avocado) ((2) 'greenbean))"; + + assertSExpressionsMatch(parseString("avocado"), evaluateString(input)); + } + + @Test + public void caseOnlyEvaluatesFirstMatchingClause() { + String input = "(case 2 ((0) (setf zero 0)) ((1) (setf one 1)) ((2) (setf two '2)) ((2) (setf two 'two)))"; + + evaluateString("(setf zero nil)"); + evaluateString("(setf one nil)"); + evaluateString(input); + + assertSExpressionsMatch(parseString("nil"), evaluateString("zero")); + assertSExpressionsMatch(parseString("nil"), evaluateString("one")); + assertSExpressionsMatch(parseString("2"), evaluateString("two")); + } + + @Test(expected = TooFewArgumentsException.class) + public void caseWithTooFewArguments() { + evaluateString("(case)"); + } + + @Test(expected = BadArgumentTypeException.class) + public void caseWithNonListClause() { + evaluateString("(case :a t)"); + } + +} diff --git a/test/function/builtin/special/CONDTester.java b/test/function/builtin/special/CONDTester.java index 6a16f86..5a63e06 100644 --- a/test/function/builtin/special/CONDTester.java +++ b/test/function/builtin/special/CONDTester.java @@ -9,80 +9,80 @@ import function.ArgumentValidator.*; public class CONDTester { @Test - public void testCondWithNoArguments() { + public void condWithNoArguments() { String input = "(cond)"; assertSExpressionsMatch(parseString("nil"), evaluateString(input)); } @Test - public void testCondWithTrue() { + public void condWithTrue() { String input = "(cond (T))"; assertSExpressionsMatch(parseString("T"), evaluateString(input)); } @Test - public void testCondWithNumber() { + public void condWithNumber() { String input = "(cond ((+ 1 2)))"; assertSExpressionsMatch(parseString("3"), evaluateString(input)); } @Test - public void testCondWithSingleExpression() { + public void condWithSingleExpression() { String input = "(cond (T \"true\"))"; assertSExpressionsMatch(parseString("\"true\""), evaluateString(input)); } @Test - public void testCondWithMultipleExpressions() { + public void condWithMultipleExpressions() { String input = "(cond ((= 1 2) 2) ((= 1 2) 2) ((= 1 1) 3))"; assertSExpressionsMatch(parseString("3"), evaluateString(input)); } @Test - public void testCondWithMultipleConditionsMatching_ReturnFirstOne() { + public void condWithMultipleConditionsMatching_ReturnFirstOne() { String input = "(cond ((= 1 1) 2) ((= 1 1) 3))"; assertSExpressionsMatch(parseString("2"), evaluateString(input)); } @Test - public void testCondWithMultipleConditionsMatching_OnlyEvaluatesFirstOne() { + public void condWithMultipleConditionsMatching_OnlyEvaluatesFirstOne() { String input = "(cond ((= 1 1) 2) ((= 1 1) x))"; assertSExpressionsMatch(parseString("2"), evaluateString(input)); } @Test - public void testCondWithMultipleResultValues_OnlyReturnsLast() { + public void condWithMultipleResultValues_OnlyReturnsLast() { String input = "(cond ((= 1 1) 2 3 4))"; assertSExpressionsMatch(parseString("4"), evaluateString(input)); } @Test - public void testCondWithNoConditionMatching_ReturnsNil() { + public void condWithNoConditionMatching_ReturnsNil() { String input = "(cond ((= 1 2) T) ((= 1 3) T))"; assertSExpressionsMatch(parseString("nil"), evaluateString(input)); } @Test(expected = BadArgumentTypeException.class) - public void testCondWithNilArgument_ThrowsException() { + public void condWithNilArgument_ThrowsException() { evaluateString("(cond ())"); } @Test(expected = BadArgumentTypeException.class) - public void testCondWithNonListArgument_ThrowsException() { + public void condWithNonListArgument_ThrowsException() { evaluateString("(cond o)"); } @Test(expected = DottedArgumentListException.class) - public void testCondWithDottedArgumentList_ThrowsException() { + public void condWithDottedArgumentList_ThrowsException() { evaluateString("(apply 'cond (cons '(nil T) 'b))"); } diff --git a/test/function/builtin/special/DEFINE_MACROTester.java b/test/function/builtin/special/DEFINE_MACROTester.java index ef369cc..45e5ceb 100644 --- a/test/function/builtin/special/DEFINE_MACROTester.java +++ b/test/function/builtin/special/DEFINE_MACROTester.java @@ -42,7 +42,7 @@ public class DEFINE_MACROTester { } @Test - public void testDefineMacro() { + public void defineMacro() { String input = "(define-macro f () t)"; assertSExpressionsMatch(parseString("f"), evaluateString(input)); @@ -50,7 +50,7 @@ public class DEFINE_MACROTester { } @Test - public void testDefineMacroWithEmptyBody() { + public void defineMacroWithEmptyBody() { String input = "(define-macro f ())"; assertSExpressionsMatch(parseString("f"), evaluateString(input)); @@ -58,26 +58,26 @@ public class DEFINE_MACROTester { } @Test - public void testDefineMacroDoesNotEvaluatesArguments() { + public void defineMacroDoesNotEvaluatesArguments() { evaluateString("(define-macro f (x) (car x))"); assertSExpressionsMatch(parseString("quote"), evaluateString("(f '(1 2 3))")); } @Test - public void testDefineMacroAdd() { + public void defineMacroAdd() { evaluateString("(define-macro f (x) (+ (eval x) 23))"); assertSExpressionsMatch(parseString("27"), evaluateString("(f (+ 2 2))")); } @Test - public void testDefineMacroSetVariable() { + public void defineMacroSetVariable() { evaluateString("(define-macro f (x) (set x 23))"); evaluateString("(f y)"); assertSExpressionsMatch(parseString("23"), evaluateString("y")); } @Test - public void testDefineMacroVariableCapture() { + public void defineMacroVariableCapture() { evaluateString("(setf x 0)"); evaluateString("(define-macro f (x) (set x 23))"); evaluateString("(f x)"); @@ -85,7 +85,7 @@ public class DEFINE_MACROTester { } @Test - public void testDefineMacroAvoidVariableCaptureConvention() { + public void defineMacroAvoidVariableCaptureConvention() { evaluateString("(setf x 0)"); evaluateString("(define-macro f (-x-) (set -x- 23))"); evaluateString("(f x)"); @@ -111,22 +111,22 @@ public class DEFINE_MACROTester { } @Test(expected = DottedArgumentListException.class) - public void testDefineMacroWithDottedLambdaList() { + public void defineMacroWithDottedLambdaList() { evaluateString("(funcall 'define-macro 'x (cons 'a 'b) ())"); } @Test(expected = BadArgumentTypeException.class) - public void testDefineMacroWithNonSymbolName() { + public void defineMacroWithNonSymbolName() { evaluateString("(define-macro 1 () ())"); } @Test(expected = BadArgumentTypeException.class) - public void testDefineMacroWithBadLambdaList() { + public void defineMacroWithBadLambdaList() { evaluateString("(define-macro x a ())"); } @Test(expected = TooFewArgumentsException.class) - public void testDefineMacroWithTooFewArguments() { + public void defineMacroWithTooFewArguments() { evaluateString("(define-macro x)"); } diff --git a/test/function/builtin/special/LETTester.java b/test/function/builtin/special/LETTester.java index 8e350c5..3445959 100644 --- a/test/function/builtin/special/LETTester.java +++ b/test/function/builtin/special/LETTester.java @@ -110,47 +110,47 @@ public class LETTester { } @Test(expected = BadArgumentTypeException.class) - public void testLetWithNonList() { + public void letWithNonList() { evaluateString("(let a)"); } @Test(expected = BadArgumentTypeException.class) - public void testLetWithNoPairs() { + public void letWithNoPairs() { evaluateString("(let (a))"); } @Test(expected = TooFewArgumentsException.class) - public void testLetWithTooFewItemsInPair() { + public void letWithTooFewItemsInPair() { evaluateString("(let ((a)))"); } @Test(expected = TooManyArgumentsException.class) - public void testLetWithTooManyItemsInPair() { + public void letWithTooManyItemsInPair() { evaluateString("(let ((a b c)))"); } @Test(expected = BadArgumentTypeException.class) - public void testLetWithNonSymbolInPair() { + public void letWithNonSymbolInPair() { evaluateString("(let ((1 b)))"); } @Test(expected = TooFewArgumentsException.class) - public void testLetWithTooFewArguments() { + public void letWithTooFewArguments() { evaluateString("(let)"); } @Test(expected = DottedArgumentListException.class) - public void testLetWithDottedArgumentList() { + public void letWithDottedArgumentList() { evaluateString("(apply 'let (cons 'a 'b))"); } @Test(expected = DottedArgumentListException.class) - public void testLetWithDottedPairList() { + public void letWithDottedPairList() { evaluateString("(apply 'let (cons (cons 'a 'b) nil))"); } @Test(expected = DottedArgumentListException.class) - public void testLetWithDottedPair() { + public void letWithDottedPair() { evaluateString("(apply 'let (cons (cons (cons 'a 'b) nil) nil))"); } diff --git a/test/function/builtin/special/QUOTETester.java b/test/function/builtin/special/QUOTETester.java index 4cf959d..01d51d6 100644 --- a/test/function/builtin/special/QUOTETester.java +++ b/test/function/builtin/special/QUOTETester.java @@ -9,26 +9,26 @@ import function.ArgumentValidator.*; public class QUOTETester { @Test - public void testQuoteSymbol() { + public void quoteSymbol() { String input = "'a"; assertSExpressionsMatch(parseString("a"), evaluateString(input)); } @Test - public void testQuoteList() { + public void quoteList() { String input = "'(l i s t)"; assertSExpressionsMatch(parseString("(l i s t)"), evaluateString(input)); } @Test(expected = TooFewArgumentsException.class) - public void testQuoteWithTooFewArguments() { + public void quoteWithTooFewArguments() { evaluateString("(quote)"); } @Test(expected = TooManyArgumentsException.class) - public void testQuoteWithTooManyArguments() { + public void quoteWithTooManyArguments() { evaluateString("(quote a b)"); } diff --git a/test/function/builtin/special/SETFTester.java b/test/function/builtin/special/SETFTester.java index 27d9b5a..ca0231a 100644 --- a/test/function/builtin/special/SETFTester.java +++ b/test/function/builtin/special/SETFTester.java @@ -29,11 +29,17 @@ public class SETFTester { } @Test - public void testSetf() { + public void setf() { evaluateString("(setf a 23)"); assertSExpressionsMatch(new LispNumber("23"), evaluateString("a")); } + @Test + public void setq() { + evaluateString("(setq a 23)"); + assertSExpressionsMatch(new LispNumber("23"), evaluateString("a")); + } + @Test public void lookupDefinedSymbol() { evaluateString("(setf a 23)"); @@ -56,6 +62,17 @@ public class SETFTester { assertSExpressionsMatch(new LispNumber("94"), evaluateString("a")); } + @Test + public void setfLocalVariable() { + SymbolTable global = executionContext.getScope(); + SymbolTable local = new SymbolTable(global); + local.put("A", new LispNumber("99")); + executionContext.setScope(local); + + evaluateString("(setf a 94)"); + assertSExpressionsMatch(new LispNumber("94"), evaluateString("a")); + } + @Test(expected = UndefinedSymbolException.class) public void setfLocalVariableDefined_DoesNotSetGlobal() { SymbolTable global = executionContext.getScope(); @@ -80,17 +97,17 @@ public class SETFTester { } @Test(expected = BadArgumentTypeException.class) - public void testSetfWithNonSymbol() { + public void setfWithNonSymbol() { evaluateString("(setf 1 2)"); } @Test(expected = TooFewArgumentsException.class) - public void testSetfWithTooFewArguments() { + public void setfWithTooFewArguments() { evaluateString("(setf x)"); } @Test(expected = TooManyArgumentsException.class) - public void testSetfWithTooManyArguments() { + public void setfWithTooManyArguments() { evaluateString("(setf a b c)"); }