diff --git a/src/function/builtin/SET.java b/src/function/builtin/SET.java new file mode 100644 index 0000000..ccb8dab --- /dev/null +++ b/src/function/builtin/SET.java @@ -0,0 +1,49 @@ +package function.builtin; + +import function.*; +import sexpression.*; +import table.*; + +public class SET extends LispFunction { + + private ArgumentValidator argumentValidator; + private ExecutionContext executionContext; + + public SET() { + this.argumentValidator = new ArgumentValidator("SET"); + this.argumentValidator.setExactNumberOfArguments(2); + this.argumentValidator.setFirstArgumentExpectedType(Symbol.class); + this.executionContext = ExecutionContext.getInstance(); + } + + public SExpression call(Cons argumentList) { + argumentValidator.validate(argumentList); + + Cons rest = (Cons) argumentList.getRest(); + SExpression symbol = argumentList.getFirst(); + SExpression value = rest.getFirst(); + + SymbolTable table = findScopeOfSymbol(symbol); + table.put(symbol.toString(), value); + + return value; + } + + private SymbolTable findScopeOfSymbol(SExpression symbol) { + SymbolTable table = executionContext.getScope(); + + while (!isSymbolInTable(symbol, table) && !isGlobalTable(table)) + table = table.getParent(); + + return table; + } + + private boolean isSymbolInTable(SExpression symbol, SymbolTable table) { + return table.contains(symbol.toString()); + } + + private boolean isGlobalTable(SymbolTable table) { + return table.getParent() == null; + } + +} diff --git a/src/function/builtin/special/DEFINE_MACRO.java b/src/function/builtin/special/DEFINE_MACRO.java new file mode 100644 index 0000000..51d5ac6 --- /dev/null +++ b/src/function/builtin/special/DEFINE_MACRO.java @@ -0,0 +1,17 @@ +package function.builtin.special; + +import function.*; +import sexpression.*; + +public class DEFINE_MACRO extends Define { + + public DEFINE_MACRO() { + super("DEFINE-MACRO"); + } + + @Override + protected UserDefinedFunction createFunction(SExpression functionName, Cons lambdaList, Cons functionBody) { + return new UserDefinedSpecialFunction(functionName.toString(), lambdaList, functionBody); + } + +} diff --git a/src/function/builtin/special/DEFUN.java b/src/function/builtin/special/DEFUN.java index 5962c56..42407d8 100644 --- a/src/function/builtin/special/DEFUN.java +++ b/src/function/builtin/special/DEFUN.java @@ -1,70 +1,17 @@ package function.builtin.special; -import java.text.MessageFormat; - -import environment.RuntimeEnvironment; -import error.*; -import function.*; -import function.builtin.cons.LIST; +import function.UserDefinedFunction; import sexpression.*; -import table.FunctionTable; -public class DEFUN extends LispSpecialFunction { - - private ArgumentValidator argumentValidator; - private ArgumentValidator lambdaListIsListValidator; - private ArgumentValidator lambdaListValidator; - private ErrorManager errorManager; +public class DEFUN extends Define { public DEFUN() { - this.argumentValidator = new ArgumentValidator("DEFUN"); - this.argumentValidator.setMinimumNumberOfArguments(2); - this.argumentValidator.setFirstArgumentExpectedType(Symbol.class); - - this.lambdaListIsListValidator = new ArgumentValidator("DEFUN|lambda-list|"); - this.lambdaListIsListValidator.setEveryArgumentExpectedType(Cons.class); - - this.lambdaListValidator = new ArgumentValidator("DEFUN|parameter|"); - this.lambdaListValidator.setEveryArgumentExpectedType(Symbol.class); - - this.errorManager = RuntimeEnvironment.getInstance().getErrorManager(); + super("DEFUN"); } - public SExpression call(Cons argumentList) { - argumentValidator.validate(argumentList); - - Cons remainingArguments = (Cons) argumentList.getRest(); - SExpression functionName = argumentList.getFirst(); - SExpression secondArgument = remainingArguments.getFirst(); - lambdaListIsListValidator.validate(LIST.makeList(secondArgument)); - - Cons lambdaList = (Cons) secondArgument; - lambdaListValidator.validate(lambdaList); - - Cons functionBody = (Cons) remainingArguments.getRest(); - UserDefinedFunction function = new UserDefinedFunction(functionName.toString(), lambdaList, functionBody); - - if (FunctionTable.isAlreadyDefined(functionName.toString())) - errorManager.handle(new RedefiningFunctionWarning(functionName.toString())); - - FunctionTable.defineFunction(functionName.toString(), function); - - return functionName; - } - - public class RedefiningFunctionWarning extends LispWarning { - - private static final long serialVersionUID = 1L; - private String functionName; - - public RedefiningFunctionWarning(String functionName) { - this.functionName = functionName; - } - - @Override - public String getMessage() { - return MessageFormat.format("redefining function {0}", functionName); - } + @Override + protected UserDefinedFunction createFunction(SExpression functionName, Cons lambdaList, Cons functionBody) { + return new UserDefinedFunction(functionName.toString(), lambdaList, functionBody); } } diff --git a/src/function/builtin/special/Define.java b/src/function/builtin/special/Define.java new file mode 100644 index 0000000..db31c1b --- /dev/null +++ b/src/function/builtin/special/Define.java @@ -0,0 +1,72 @@ +package function.builtin.special; + +import java.text.MessageFormat; + +import environment.RuntimeEnvironment; +import error.*; +import function.*; +import function.builtin.cons.LIST; +import sexpression.*; +import table.FunctionTable; + +public abstract class Define extends LispSpecialFunction { + + private ArgumentValidator argumentValidator; + private ArgumentValidator lambdaListIsListValidator; + private ArgumentValidator lambdaListValidator; + private ErrorManager errorManager; + + public Define(String functionName) { + this.argumentValidator = new ArgumentValidator(functionName); + this.argumentValidator.setMinimumNumberOfArguments(2); + this.argumentValidator.setFirstArgumentExpectedType(Symbol.class); + + this.lambdaListIsListValidator = new ArgumentValidator(functionName + "|lambda-list|"); + this.lambdaListIsListValidator.setEveryArgumentExpectedType(Cons.class); + + this.lambdaListValidator = new ArgumentValidator(functionName + "|parameter|"); + this.lambdaListValidator.setEveryArgumentExpectedType(Symbol.class); + + this.errorManager = RuntimeEnvironment.getInstance().getErrorManager(); + } + + public SExpression call(Cons argumentList) { + argumentValidator.validate(argumentList); + + Cons remainingArguments = (Cons) argumentList.getRest(); + SExpression functionName = argumentList.getFirst(); + SExpression secondArgument = remainingArguments.getFirst(); + lambdaListIsListValidator.validate(LIST.makeList(secondArgument)); + + Cons lambdaList = (Cons) secondArgument; + lambdaListValidator.validate(lambdaList); + + Cons functionBody = (Cons) remainingArguments.getRest(); + UserDefinedFunction function = createFunction(functionName, lambdaList, functionBody); + + if (FunctionTable.isAlreadyDefined(functionName.toString())) + errorManager.handle(new RedefiningFunctionWarning(functionName.toString())); + + FunctionTable.defineFunction(functionName.toString(), function); + + return functionName; + } + + protected abstract UserDefinedFunction createFunction(SExpression functionName, Cons lambdaList, Cons functionBody); + + public class RedefiningFunctionWarning extends LispWarning { + + private static final long serialVersionUID = 1L; + private String functionName; + + public RedefiningFunctionWarning(String functionName) { + this.functionName = functionName; + } + + @Override + public String getMessage() { + return MessageFormat.format("redefining function {0}", functionName); + } + } + +} diff --git a/src/table/FunctionTable.java b/src/table/FunctionTable.java index a68ca7d..d70063e 100644 --- a/src/table/FunctionTable.java +++ b/src/table/FunctionTable.java @@ -52,6 +52,7 @@ public class FunctionTable { functionTable.put("CDR", new REST()); functionTable.put("COND", new COND()); functionTable.put("CONS", new CONS()); + functionTable.put("DEFINE-MACRO", new DEFINE_MACRO()); functionTable.put("DEFUN", new DEFUN()); functionTable.put("EQ", new EQ()); functionTable.put("EQUAL", new EQUAL()); @@ -72,7 +73,9 @@ public class FunctionTable { functionTable.put("PRINT", new PRINT()); functionTable.put("QUOTE", new QUOTE()); functionTable.put("REST", new REST()); + functionTable.put("SET", new SET()); functionTable.put("SETF", new SETF()); + functionTable.put("SETQ", new SETF()); functionTable.put("SYMBOL-FUNCTION", new SYMBOL_FUNCTION()); } diff --git a/test/function/builtin/SETTester.java b/test/function/builtin/SETTester.java new file mode 100644 index 0000000..5ab4d23 --- /dev/null +++ b/test/function/builtin/SETTester.java @@ -0,0 +1,97 @@ +package function.builtin; + +import static org.junit.Assert.assertNull; +import static testutil.TestUtilities.*; + +import org.junit.*; + +import function.ArgumentValidator.*; +import function.builtin.EVAL.UndefinedSymbolException; +import sexpression.LispNumber; +import table.*; + +public class SETTester { + + private ExecutionContext executionContext; + + public SETTester() { + this.executionContext = ExecutionContext.getInstance(); + } + + @Before + public void setUp() { + executionContext.clearContext(); + } + + @After + public void tearDown() { + executionContext.clearContext(); + } + + @Test + public void testSet() { + evaluateString("(set 'a 23)"); + assertSExpressionsMatch(new LispNumber("23"), evaluateString("a")); + } + + @Test + public void lookupDefinedSymbol() { + evaluateString("(set 'a 23)"); + assertSExpressionsMatch(new LispNumber("23"), executionContext.lookupSymbolValue("A")); + } + + @Test + public void lookupUndefinedSymbol() { + assertNull(executionContext.lookupSymbolValue("A")); + } + + @Test + public void setGlobalVariable() { + evaluateString("(set 'a 23)"); + SymbolTable global = executionContext.getScope(); + executionContext.setScope(new SymbolTable(global)); + + evaluateString("(set 'a 94)"); + executionContext.setScope(global); + assertSExpressionsMatch(new LispNumber("94"), evaluateString("a")); + } + + @Test(expected = UndefinedSymbolException.class) + public void setLocalVariableDefined_DoesNotSetGlobal() { + SymbolTable global = executionContext.getScope(); + SymbolTable local = new SymbolTable(global); + local.put("A", new LispNumber("99")); + executionContext.setScope(local); + + evaluateString("(set 'a 94)"); + executionContext.setScope(global); + evaluateString("a"); + } + + @Test + public void setLocalVariableUndefined_SetsGlobal() { + SymbolTable global = executionContext.getScope(); + SymbolTable local = new SymbolTable(global); + executionContext.setScope(local); + + evaluateString("(set 'a 94)"); + executionContext.setScope(global); + assertSExpressionsMatch(new LispNumber("94"), evaluateString("a")); + } + + @Test(expected = BadArgumentTypeException.class) + public void testSetWithNonSymbol() { + evaluateString("(set '1 2)"); + } + + @Test(expected = TooFewArgumentsException.class) + public void testSetWithTooFewArguments() { + evaluateString("(set 'x)"); + } + + @Test(expected = TooManyArgumentsException.class) + public void testSetWithTooManyArguments() { + evaluateString("(set 'a 'b 'c)"); + } + +} diff --git a/test/function/builtin/special/DEFINE_MACROTester.java b/test/function/builtin/special/DEFINE_MACROTester.java new file mode 100644 index 0000000..ca6ef23 --- /dev/null +++ b/test/function/builtin/special/DEFINE_MACROTester.java @@ -0,0 +1,131 @@ +package function.builtin.special; + +import static org.junit.Assert.assertTrue; +import static testutil.TestUtilities.*; + +import java.io.*; + +import org.junit.*; + +import environment.RuntimeEnvironment; +import error.ErrorManager; +import function.ArgumentValidator.*; +import table.FunctionTable; + +public class DEFINE_MACROTester { + + private ByteArrayOutputStream outputStream; + private RuntimeEnvironment environment; + + public DEFINE_MACROTester() { + this.environment = RuntimeEnvironment.getInstance(); + } + + private void assertSomethingPrinted() { + assertTrue(outputStream.toByteArray().length > 0); + } + + @Before + public void setUp() { + outputStream = new ByteArrayOutputStream(); + + environment.setOutput(new PrintStream(outputStream)); + environment.setErrorManager(new ErrorManager()); + environment.setWarningOutputDecorator(s -> s); + + FunctionTable.reset(); + } + + @After + public void tearDown() { + FunctionTable.reset(); + } + + @Test + public void testDefineMacro() { + String input = "(define-macro f () t)"; + + assertSExpressionsMatch(parseString("f"), evaluateString(input)); + assertSExpressionsMatch(parseString("t"), evaluateString("(f)")); + } + + @Test + public void testDefineMacroWithEmptyBody() { + String input = "(define-macro f ())"; + + assertSExpressionsMatch(parseString("f"), evaluateString(input)); + assertSExpressionsMatch(parseString("()"), evaluateString("(f)")); + } + + @Test + public void testDefineMacroDoesNotEvaluatesArguments() { + evaluateString("(define-macro f (x) (car x))"); + assertSExpressionsMatch(parseString("quote"), evaluateString("(f '(1 2 3))")); + } + @Test + public void testDefineMacroAdd() { + evaluateString("(define-macro f (x) (+ (eval x) 23))"); + assertSExpressionsMatch(parseString("27"), evaluateString("(f (+ 2 2))")); + } + + + @Test + public void testDefineMacroSetVariable() { + evaluateString("(define-macro f (x) (set x 23))"); + evaluateString("(f y)"); + assertSExpressionsMatch(parseString("23"), evaluateString("y")); + } + + @Test + public void testDefineMacroVariableCapture() { + evaluateString("(setf x 0)"); + evaluateString("(define-macro f (x) (set x 23))"); + evaluateString("(f x)"); + assertSExpressionsMatch(parseString("0"), evaluateString("x")); + } + @Test + public void testDefineMacroAvoidVariableCaptureConvention() { + evaluateString("(setf x 0)"); + evaluateString("(define-macro f (-x-) (set -x- 23))"); + evaluateString("(f x)"); + assertSExpressionsMatch(parseString("23"), evaluateString("x")); + } + @Test + public void redefineMacro_DisplaysWarning() { + String input = "(define-macro myFunction () nil)"; + evaluateString(input); + evaluateString(input); + + assertSomethingPrinted(); + } + + @Test + public void redefineMacro_ActuallyRedefinesMacro() { + evaluateString("(define-macro myMacro () nil)"); + evaluateString("(define-macro myMacro () T)"); + + assertSomethingPrinted(); + assertSExpressionsMatch(parseString("t"), evaluateString("(myMacro)")); + } + + @Test(expected = DottedArgumentListException.class) + public void testDefineMacroWithDottedLambdaList() { + evaluateString("(funcall 'define-macro 'x (cons 'a 'b) ())"); + } + + @Test(expected = BadArgumentTypeException.class) + public void testDefineMacroWithNonSymbolName() { + evaluateString("(define-macro 1 () ())"); + } + + @Test(expected = BadArgumentTypeException.class) + public void testDefineMacroWithBadLambdaList() { + evaluateString("(define-macro x a ())"); + } + + @Test(expected = TooFewArgumentsException.class) + public void testDefineMacroWithTooFewArguments() { + evaluateString("(define-macro x)"); + } + +} diff --git a/test/function/builtin/special/DEFUNTester.java b/test/function/builtin/special/DEFUNTester.java index 257ce08..0bd3424 100644 --- a/test/function/builtin/special/DEFUNTester.java +++ b/test/function/builtin/special/DEFUNTester.java @@ -57,6 +57,12 @@ public class DEFUNTester { assertSExpressionsMatch(parseString("()"), evaluateString("(f)")); } + @Test + public void testDefunEvaluatesArguments() { + evaluateString("(defun f (x) (car x))"); + assertSExpressionsMatch(parseString("1"), evaluateString("(f '(1 2 3))")); + } + @Test public void redefineFunction_DisplaysWarning() { String input = "(defun myFunction () nil)"; @@ -77,9 +83,7 @@ public class DEFUNTester { @Test(expected = DottedArgumentListException.class) public void testDefunWithDottedLambdaList() { - String input = "(funcall 'defun 'x (cons 'a 'b) ())"; - - evaluateString(input); + evaluateString("(funcall 'defun 'x (cons 'a 'b) ())"); } @Test(expected = BadArgumentTypeException.class)