Continued refactoring the LispParser and writing unit tests

This commit is contained in:
Mike Cifelli 2016-12-14 14:41:43 -05:00
parent 930c8137df
commit ad2375e2bd
19 changed files with 172 additions and 136 deletions

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8"?><launchConfiguration type="org.eclipse.ant.AntBuilderLaunchConfigurationType">
<launchConfiguration type="org.eclipse.ant.AntBuilderLaunchConfigurationType">
<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_AFTER_CLEAN_TARGETS" value="jar,"/> <stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_AFTER_CLEAN_TARGETS" value="jar,"/>
<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_AUTO_TARGETS" value="jar,"/> <stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_AUTO_TARGETS" value="jar,"/>
<stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_CLEAN_TARGETS" value="clean,"/> <stringAttribute key="org.eclipse.ant.ui.ATTR_ANT_CLEAN_TARGETS" value="clean,"/>
@ -24,4 +23,4 @@
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/LispInterpreter/build.xml}"/> <stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/LispInterpreter/build.xml}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_RUN_BUILD_KINDS" value="full,incremental,auto,clean"/> <stringAttribute key="org.eclipse.ui.externaltools.ATTR_RUN_BUILD_KINDS" value="full,incremental,auto,clean"/>
<booleanAttribute key="org.eclipse.ui.externaltools.ATTR_TRIGGERS_CONFIGURED" value="true"/> <booleanAttribute key="org.eclipse.ui.externaltools.ATTR_TRIGGERS_CONFIGURED" value="true"/>
</launchConfiguration> </launchConfiguration>

View File

@ -3,9 +3,8 @@ package parser;
import java.io.InputStream; import java.io.InputStream;
import error.LispException; import error.LispException;
import parser.MalformedSExpressionException.*;
import scanner.LispScanner; import scanner.LispScanner;
import sexpression.*; import sexpression.SExpression;
import token.Eof; import token.Eof;
import token.Token; import token.Token;
@ -17,11 +16,13 @@ public class LispParser {
private LispScanner scanner; private LispScanner scanner;
private Token nextToken; private Token nextToken;
private LispException delayedException;
private boolean isNextTokenStored; private boolean isNextTokenStored;
public LispParser(InputStream inputStream, String fileName) { public LispParser(InputStream inputStream, String fileName) {
scanner = new LispScanner(inputStream, fileName); scanner = new LispScanner(inputStream, fileName);
nextToken = null; nextToken = null;
delayedException = null;
isNextTokenStored = false; isNextTokenStored = false;
} }
@ -37,59 +38,27 @@ public class LispParser {
nextToken = scanner.getNextToken(); nextToken = scanner.getNextToken();
isNextTokenStored = true; isNextTokenStored = true;
} catch (LispException e) { } catch (LispException e) {
// This method should give the illusion of not actually reading delayedException = e;
// a token, so we ignore any exception. It will be thrown when we
// return the next sExpression.
} }
} }
public SExpression getNextSExpression() { public SExpression getNextSExpression() {
throwDelayedExceptionIfNecessary();
if (!isNextTokenStored) if (!isNextTokenStored)
nextToken = scanner.getNextToken(); nextToken = scanner.getNextToken();
else
isNextTokenStored = false;
return sExpr(); isNextTokenStored = false;
return nextToken.sExpr(scanner::getNextToken);
} }
// sExpr ::= NUMBER | IDENTIFIER | STRING | QUOTE_MARK sExpr | private void throwDelayedExceptionIfNecessary() {
// LEFT_PAREN sExprTail if (delayedException != null) {
private SExpression sExpr() { LispException exceptionToThrow = delayedException;
switch (nextToken.getType()) { delayedException = null;
case NUMBER:
return new LispNumber(nextToken.getText());
case IDENTIFIER:
return new Symbol(nextToken.getText());
case STRING:
return new LispString(nextToken.getText());
case QUOTE_MARK:
nextToken = scanner.getNextToken();
SExpression arg = sExpr();
return new Cons(new Symbol("QUOTE"), new Cons(arg, Nil.getUniqueInstance())); throw exceptionToThrow;
case LEFT_PAREN:
return sExprTail();
case RIGHT_PAREN:
throw new StartsWithRightParenthesisException(nextToken);
case EOF:
throw new EofEncounteredException(nextToken);
default:
throw new UnrecognizedTokenException(nextToken);
}
}
// sExprTail ::= RIGHT_PAREN | sExpr sExprTail
private SExpression sExprTail() {
nextToken = scanner.getNextToken();
switch (nextToken.getType()) {
case RIGHT_PAREN:
return Nil.getUniqueInstance();
default:
SExpression car = sExpr();
SExpression cdr = sExprTail();
return new Cons(car, cdr);
} }
} }

View File

@ -1,4 +1,4 @@
package parser; package sexpression;
import java.text.MessageFormat; import java.text.MessageFormat;

View File

@ -1,7 +1,10 @@
package token; package token;
import file.FilePosition; import java.util.function.Supplier;
import file.FilePosition;
import sexpression.MalformedSExpressionException.EofEncounteredException;
import sexpression.SExpression;
public class Eof extends Token { public class Eof extends Token {
@ -10,8 +13,8 @@ public class Eof extends Token {
} }
@Override @Override
public Type getType() { public SExpression sExpr(Supplier<Token> getNextToken) {
return Type.EOF; throw new EofEncounteredException(this);
} }
} }

View File

@ -1,7 +1,10 @@
package token; package token;
import file.FilePosition; import java.util.function.Supplier;
import file.FilePosition;
import sexpression.SExpression;
import sexpression.Symbol;
public class Identifier extends Token { public class Identifier extends Token {
@ -10,8 +13,8 @@ public class Identifier extends Token {
} }
@Override @Override
public Type getType() { public SExpression sExpr(Supplier<Token> getNextToken) {
return Type.IDENTIFIER; return new Symbol(getText());
} }
} }

View File

@ -1,6 +1,9 @@
package token; package token;
import java.util.function.Supplier;
import file.FilePosition; import file.FilePosition;
import sexpression.SExpression;
public class LeftParenthesis extends Token { public class LeftParenthesis extends Token {
@ -9,8 +12,10 @@ public class LeftParenthesis extends Token {
} }
@Override @Override
public Type getType() { public SExpression sExpr(Supplier<Token> getNextToken) {
return Type.LEFT_PAREN; Token nextToken = getNextToken.get();
return nextToken.sExprTail(getNextToken);
} }
} }

View File

@ -1,7 +1,10 @@
package token; package token;
import file.FilePosition; import java.util.function.Supplier;
import file.FilePosition;
import sexpression.LispNumber;
import sexpression.SExpression;
public class Number extends Token { public class Number extends Token {
@ -10,8 +13,8 @@ public class Number extends Token {
} }
@Override @Override
public Type getType() { public SExpression sExpr(Supplier<Token> getNextToken) {
return Type.NUMBER; return new LispNumber(getText());
} }
} }

View File

@ -1,7 +1,9 @@
package token; package token;
import file.FilePosition; import java.util.function.Supplier;
import file.FilePosition;
import sexpression.*;
public class QuoteMark extends Token { public class QuoteMark extends Token {
@ -10,8 +12,11 @@ public class QuoteMark extends Token {
} }
@Override @Override
public Type getType() { public SExpression sExpr(Supplier<Token> getNextToken) {
return Type.QUOTE_MARK; Token nextToken = getNextToken.get();
SExpression argument = nextToken.sExpr(getNextToken);
return new Cons(new Symbol("QUOTE"), new Cons(argument, Nil.getUniqueInstance()));
} }
} }

View File

@ -1,7 +1,10 @@
package token; package token;
import file.FilePosition; import java.util.function.Supplier;
import file.FilePosition;
import sexpression.LispString;
import sexpression.SExpression;
public class QuotedString extends Token { public class QuotedString extends Token {
@ -10,8 +13,8 @@ public class QuotedString extends Token {
} }
@Override @Override
public Type getType() { public SExpression sExpr(Supplier<Token> getNextToken) {
return Type.STRING; return new LispString(getText());
} }
} }

View File

@ -1,7 +1,11 @@
package token; package token;
import file.FilePosition; import java.util.function.Supplier;
import file.FilePosition;
import sexpression.MalformedSExpressionException.StartsWithRightParenthesisException;
import sexpression.Nil;
import sexpression.SExpression;
public class RightParenthesis extends Token { public class RightParenthesis extends Token {
@ -10,8 +14,13 @@ public class RightParenthesis extends Token {
} }
@Override @Override
public Type getType() { public SExpression sExpr(Supplier<Token> getNextToken) {
return Type.RIGHT_PAREN; throw new StartsWithRightParenthesisException(this);
}
@Override
public SExpression sExprTail(Supplier<Token> getNextToken) {
return Nil.getUniqueInstance();
} }
} }

View File

@ -1,44 +1,52 @@
package token; package token;
import java.util.function.Supplier;
import file.FilePosition; import file.FilePosition;
import sexpression.Cons;
import sexpression.SExpression;
/** /**
* A token in Lisp. * A token in Lisp.
*/ */
public abstract class Token { public abstract class Token {
public enum Type {
LEFT_PAREN, RIGHT_PAREN, STRING, QUOTE_MARK, NUMBER, IDENTIFIER, EOF
}
private String text; private String text;
private String fileName; private FilePosition position;
private int line;
private int column;
public Token(String text, FilePosition position) { public Token(String text, FilePosition position) {
this.text = text; this.text = text;
this.fileName = position.getFileName(); this.position = position;
this.line = position.getLineNumber();
this.column = position.getColumnNumber();
} }
public abstract Type getType();
public String getText() { public String getText() {
return text; return text;
} }
public String getFileName() { public String getFileName() {
return fileName; return position.getFileName();
} }
public int getLine() { public int getLine() {
return line; return position.getLineNumber();
} }
public int getColumn() { public int getColumn() {
return column; return position.getColumnNumber();
}
// sExpr ::= NUMBER | IDENTIFIER | STRING | QUOTE_MARK sExpr |
// LEFT_PAREN sExprTail
public abstract SExpression sExpr(Supplier<Token> getNextToken);
// sExprTail ::= RIGHT_PAREN | sExpr sExprTail
public SExpression sExprTail(Supplier<Token> getNextToken) {
SExpression car = sExpr(getNextToken);
Token nextToken = getNextToken.get();
SExpression cdr = nextToken.sExprTail(getNextToken);
return new Cons(car, cdr);
} }
} }

View File

@ -3,7 +3,9 @@ package util;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
public class Characters { public final class Characters {
private Characters() {}
public static final int EOF = -1; public static final int EOF = -1;
@ -39,7 +41,7 @@ public class Characters {
public static boolean isLegalIdentifierCharacter(char c) { public static boolean isLegalIdentifierCharacter(char c) {
return (!Character.isWhitespace(c)) && (!illegalIdentifierCharacters.contains(c)); return (!Character.isWhitespace(c)) && (!illegalIdentifierCharacters.contains(c));
} }
public static boolean isLegalStringCharacter(char c) { public static boolean isLegalStringCharacter(char c) {
return true; return true;
} }

View File

@ -1,20 +1,31 @@
package parser; package parser;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static testutil.TestUtilities.createIOExceptionThrowingInputStream;
import static testutil.TestUtilities.createInputStreamFromString;
import java.io.InputStream; import java.io.InputStream;
import org.junit.Test; import org.junit.Test;
import error.LispException; import error.LispException;
import scanner.LispInputStream.UncheckedIOException;
import scanner.LispScanner.UnterminatedStringException;
import sexpression.MalformedSExpressionException.EofEncounteredException;
import sexpression.MalformedSExpressionException.StartsWithRightParenthesisException;
import sexpression.Nil; import sexpression.Nil;
import sexpression.SExpression; import sexpression.SExpression;
import testutils.TestUtilities; import token.TokenFactory.BadCharacterException;
public class LispParserTester { public class LispParserTester {
private LispParser createLispParser(String input) { private LispParser createLispParser(String input) {
InputStream stringInputStream = TestUtilities.createInputStreamFromString(input); InputStream stringInputStream = createInputStreamFromString(input);
return new LispParser(stringInputStream, "testFile");
}
private LispParser createIOExceptionThrowingLispParser() {
InputStream stringInputStream = createIOExceptionThrowingInputStream();
return new LispParser(stringInputStream, "testFile"); return new LispParser(stringInputStream, "testFile");
} }
@ -183,6 +194,17 @@ public class LispParserTester {
assertTrue(parser.isEof()); assertTrue(parser.isEof());
} }
@Test
public void givenMultipleComplexLists_CreatesCorrectSExpressions() {
String input = "(defun f (x) \n (print \n (list \"x is \" x) \n ) \n )"
+ "(defun f (x) \n (print \n (list \"x is \" x) \n ) \n )";
LispParser parser = createLispParser(input);
assertList(parser.getNextSExpression());
assertList(parser.getNextSExpression());
assertTrue(parser.isEof());
}
@Test @Test
public void givenMultipleExpressions_CreatesCorrectSExpressions() { public void givenMultipleExpressions_CreatesCorrectSExpressions() {
String input = "(setf x 2) x \"hi\" () 29"; String input = "(setf x 2) x \"hi\" () 29";
@ -208,7 +230,7 @@ public class LispParserTester {
assertTrue(parser.isEof()); assertTrue(parser.isEof());
} }
@Test(expected = LispException.class) @Test(expected = BadCharacterException.class)
public void givenBadToken_ThrowsException() { public void givenBadToken_ThrowsException() {
String input = "["; String input = "[";
LispParser parser = createLispParser(input); LispParser parser = createLispParser(input);
@ -216,7 +238,7 @@ public class LispParserTester {
parser.getNextSExpression(); parser.getNextSExpression();
} }
@Test(expected = LispException.class) @Test(expected = UnterminatedStringException.class)
public void givenUnterminatedString_ThrowsException() { public void givenUnterminatedString_ThrowsException() {
String input = "\"string"; String input = "\"string";
LispParser parser = createLispParser(input); LispParser parser = createLispParser(input);
@ -224,7 +246,7 @@ public class LispParserTester {
parser.getNextSExpression(); parser.getNextSExpression();
} }
@Test(expected = LispException.class) @Test(expected = EofEncounteredException.class)
public void givenUnterminatedList_ThrowsException() { public void givenUnterminatedList_ThrowsException() {
String input = "(bad list"; String input = "(bad list";
LispParser parser = createLispParser(input); LispParser parser = createLispParser(input);
@ -232,7 +254,7 @@ public class LispParserTester {
parser.getNextSExpression(); parser.getNextSExpression();
} }
@Test(expected = LispException.class) @Test(expected = StartsWithRightParenthesisException.class)
public void givenUnmatchedRightParenthesis_ThrowsException() { public void givenUnmatchedRightParenthesis_ThrowsException() {
String input = ")"; String input = ")";
LispParser parser = createLispParser(input); LispParser parser = createLispParser(input);
@ -240,7 +262,7 @@ public class LispParserTester {
parser.getNextSExpression(); parser.getNextSExpression();
} }
@Test(expected = LispException.class) @Test(expected = BadCharacterException.class)
public void givenBadCharacter_ThrowsExceptionAfterEofCalled() { public void givenBadCharacter_ThrowsExceptionAfterEofCalled() {
String input = "["; String input = "[";
LispParser parser = createLispParser(input); LispParser parser = createLispParser(input);
@ -254,7 +276,7 @@ public class LispParserTester {
parser.getNextSExpression(); parser.getNextSExpression();
} }
@Test(expected = LispException.class) @Test(expected = BadCharacterException.class)
public void givenBadCharacterAfterValidToken_ThrowsExceptionAtTheCorrectTime() { public void givenBadCharacterAfterValidToken_ThrowsExceptionAtTheCorrectTime() {
String input = "id["; String input = "id[";
LispParser parser = createLispParser(input); LispParser parser = createLispParser(input);
@ -279,9 +301,22 @@ public class LispParserTester {
try { try {
parser.getNextSExpression(); parser.getNextSExpression();
fail("Expected LispException"); fail("Expected LispException");
} catch (LispException e) { } catch (LispException e) {}
}
assertTrue(parser.isEof()); assertTrue(parser.isEof());
} }
@Test(expected = UncheckedIOException.class)
public void handlesIOExceptionCorrectly() {
LispParser parser = createIOExceptionThrowingLispParser();
try {
parser.isEof();
} catch (LispException e) {
fail("Exception thrown too early");
}
parser.getNextSExpression();
}
} }

View File

@ -1,8 +1,9 @@
package scanner; package scanner;
import static org.junit.Assert.*; import static org.junit.Assert.*;
import static testutil.TestUtilities.createIOExceptionThrowingInputStream;
import static testutil.TestUtilities.createInputStreamFromString;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import org.junit.Test; import org.junit.Test;
@ -10,7 +11,6 @@ import org.junit.Test;
import error.ErrorManager; import error.ErrorManager;
import scanner.LispInputStream.MaximumUnreadsExceededException; import scanner.LispInputStream.MaximumUnreadsExceededException;
import scanner.LispInputStream.UncheckedIOException; import scanner.LispInputStream.UncheckedIOException;
import testutils.TestUtilities;
public class LispCommentRemovingInputStreamTester { public class LispCommentRemovingInputStreamTester {
@ -19,7 +19,7 @@ public class LispCommentRemovingInputStreamTester {
} }
private LispInputStream createLispInputStream(String inputString) { private LispInputStream createLispInputStream(String inputString) {
InputStream stringInputStream = TestUtilities.createInputStreamFromString(inputString); InputStream stringInputStream = createInputStreamFromString(inputString);
return new LispCommentRemovingInputStream(stringInputStream); return new LispCommentRemovingInputStream(stringInputStream);
} }
@ -32,15 +32,6 @@ public class LispCommentRemovingInputStreamTester {
return charactersRead.toString(); return charactersRead.toString();
} }
private InputStream createIOExceptionThrowingInputStream() {
return new InputStream() {
public int read() throws IOException {
throw new IOException("test IOException");
}
};
}
@Test @Test
public void noBytesIn_noBytesOut() { public void noBytesIn_noBytesOut() {
String input = ""; String input = "";

View File

@ -1,12 +1,12 @@
package scanner; package scanner;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static testutil.TestUtilities.createInputStreamFromString;
import java.io.InputStream; import java.io.InputStream;
import org.junit.Test; import org.junit.Test;
import testutils.TestUtilities;
import token.Token; import token.Token;
public class LispScannerLineColumnTester { public class LispScannerLineColumnTester {
@ -30,7 +30,7 @@ public class LispScannerLineColumnTester {
} }
private void assertTokenLineAndColumnsMatch(String input, LineColumn[] expectedLineColumnList) { private void assertTokenLineAndColumnsMatch(String input, LineColumn[] expectedLineColumnList) {
InputStream stringInputStream = TestUtilities.createInputStreamFromString(input); InputStream stringInputStream = createInputStreamFromString(input);
LispScanner lispScanner = new LispScanner(stringInputStream, "testFile"); LispScanner lispScanner = new LispScanner(stringInputStream, "testFile");
for (LineColumn lineColumn : expectedLineColumnList) { for (LineColumn lineColumn : expectedLineColumnList) {

View File

@ -1,13 +1,12 @@
package scanner; package scanner;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static testutil.TestUtilities.createInputStreamFromString;
import java.io.InputStream; import java.io.InputStream;
import org.junit.Test; import org.junit.Test;
import testutils.TestUtilities;
public class LispScannerTextTester { public class LispScannerTextTester {
private void assertTokenTextMatches(String input, String[] expectedTextList) { private void assertTokenTextMatches(String input, String[] expectedTextList) {
@ -24,13 +23,13 @@ public class LispScannerTextTester {
} }
private LispScanner createLispScanner(String input) { private LispScanner createLispScanner(String input) {
InputStream stringInputStream = TestUtilities.createInputStreamFromString(input); InputStream stringInputStream = createInputStreamFromString(input);
return new LispScanner(stringInputStream, "testFile"); return new LispScanner(stringInputStream, "testFile");
} }
private void assertInputFileNameMatches(String input, String expectedInputFileName) { private void assertInputFileNameMatches(String input, String expectedInputFileName) {
InputStream stringInputStream = TestUtilities.createInputStreamFromString(input); InputStream stringInputStream = createInputStreamFromString(input);
LispScanner lispScanner = new LispScanner(stringInputStream, expectedInputFileName); LispScanner lispScanner = new LispScanner(stringInputStream, expectedInputFileName);
assertEquals(expectedInputFileName, lispScanner.getNextToken().getFileName()); assertEquals(expectedInputFileName, lispScanner.getNextToken().getFileName());

View File

@ -2,6 +2,7 @@ package scanner;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static testutil.TestUtilities.createInputStreamFromString;
import java.io.InputStream; import java.io.InputStream;
import java.util.ArrayList; import java.util.ArrayList;
@ -12,15 +13,8 @@ import org.junit.Test;
import error.ErrorManager; import error.ErrorManager;
import scanner.LispScanner.UnterminatedStringException; import scanner.LispScanner.UnterminatedStringException;
import testutils.TestUtilities; import token.*;
import token.Eof;
import token.Identifier;
import token.LeftParenthesis;
import token.Number; import token.Number;
import token.QuoteMark;
import token.QuotedString;
import token.RightParenthesis;
import token.Token;
import token.TokenFactory.BadCharacterException; import token.TokenFactory.BadCharacterException;
public class LispScannerTypeTester { public class LispScannerTypeTester {
@ -28,7 +22,7 @@ public class LispScannerTypeTester {
private List<Class<? extends Token>> expectedTypes; private List<Class<? extends Token>> expectedTypes;
private void assertTokenTypesMatch(String input) { private void assertTokenTypesMatch(String input) {
InputStream stringInputStream = TestUtilities.createInputStreamFromString(input); InputStream stringInputStream = createInputStreamFromString(input);
LispScanner lispScanner = new LispScanner(stringInputStream, "testFile"); LispScanner lispScanner = new LispScanner(stringInputStream, "testFile");
for (Class<? extends Token> type : expectedTypes) for (Class<? extends Token> type : expectedTypes)

View File

@ -0,0 +1,20 @@
package testutil;
import java.io.*;
public class TestUtilities {
public static InputStream createInputStreamFromString(String string) {
return new ByteArrayInputStream(string.getBytes());
}
public static InputStream createIOExceptionThrowingInputStream() {
return new InputStream() {
public int read() throws IOException {
throw new IOException("test IOException");
}
};
}
}

View File

@ -1,12 +0,0 @@
package testutils;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
public class TestUtilities {
public static InputStream createInputStreamFromString(String string) {
return new ByteArrayInputStream(string.getBytes());
}
}