diff --git a/src/main/java/dk/madsravn/interpreter/ast/BlockStatement.java b/src/main/java/dk/madsravn/interpreter/ast/BlockStatement.java new file mode 100644 index 0000000..aeb81fa --- /dev/null +++ b/src/main/java/dk/madsravn/interpreter/ast/BlockStatement.java @@ -0,0 +1,41 @@ +package dk.madsravn.interpreter.ast; + +import dk.madsravn.interpreter.tokens.Token; + +import java.util.List; + +public class BlockStatement implements IStatement { + private Token token; + private List statements; + + public BlockStatement(Token token, List statements) { + this.token = token; + this.statements = statements; + } + + public int getStatementsLength() { + return statements.size(); + } + + public List getStatements() { + return statements; + } + + @Override + public void statementNode() {} + + @Override + public String string() { + StringBuilder sb = new StringBuilder(); + for(IStatement statement : statements) { + sb.append(statement.string()); + } + + return sb.toString(); + } + + @Override + public String tokenLiteral() { + return token.getLiteral(); + } +} diff --git a/src/main/java/dk/madsravn/interpreter/ast/CallExpression.java b/src/main/java/dk/madsravn/interpreter/ast/CallExpression.java new file mode 100644 index 0000000..91bf457 --- /dev/null +++ b/src/main/java/dk/madsravn/interpreter/ast/CallExpression.java @@ -0,0 +1,43 @@ +package dk.madsravn.interpreter.ast; + +import dk.madsravn.interpreter.tokens.Token; + +import java.util.List; +import java.util.stream.Collectors; + +public class CallExpression implements IExpression { + private Token token; + private IExpression function; + private List arguments; + + public CallExpression(Token token, IExpression function, List arguments) { + this.token = token; + this.function = function; + this.arguments = arguments; + } + + public IExpression getFunction() { + return function; + } + + public List getArguments() { + return arguments; + } + @Override + public void expressionNode() {} + + public String string() { + StringBuilder sb = new StringBuilder(); + String argumentString = arguments.stream().map(p -> p.string()).collect(Collectors.joining(",")); + sb.append(function.string()); + sb.append("("); + sb.append(argumentString); + sb.append(")"); + + return sb.toString(); + } + + public String tokenLiteral() { + return token.getLiteral(); + } +} diff --git a/src/main/java/dk/madsravn/interpreter/ast/FunctionLiteral.java b/src/main/java/dk/madsravn/interpreter/ast/FunctionLiteral.java new file mode 100644 index 0000000..25fa0c4 --- /dev/null +++ b/src/main/java/dk/madsravn/interpreter/ast/FunctionLiteral.java @@ -0,0 +1,51 @@ +package dk.madsravn.interpreter.ast; + +import dk.madsravn.interpreter.tokens.Token; + +import java.util.List; +import java.util.stream.Collectors; + +public class FunctionLiteral implements IExpression{ + private Token token; + private List parameters; + private BlockStatement body; + + public FunctionLiteral(Token token, List parameters, BlockStatement body) { + this.token = token; + this.parameters = parameters; + this.body = body; + } + + public List getParameters() { + return parameters; + } + + public BlockStatement getBody() { + return body; + } + + public int getParametersLength() { + return parameters.size(); + } + @Override + public void expressionNode() {} + + @Override + public String string() { + StringBuilder sb = new StringBuilder(); + String paramString = parameters.stream().map(p -> p.string()).collect(Collectors.joining(",")); + + sb.append(token.getLiteral()); + sb.append("("); + sb.append(paramString); + sb.append(")"); + sb.append(body.string()); + + return sb.toString(); + } + + @Override + public String tokenLiteral() { + return token.getLiteral(); + } +} diff --git a/src/main/java/dk/madsravn/interpreter/ast/IfExpression.java b/src/main/java/dk/madsravn/interpreter/ast/IfExpression.java new file mode 100644 index 0000000..e4a2b25 --- /dev/null +++ b/src/main/java/dk/madsravn/interpreter/ast/IfExpression.java @@ -0,0 +1,52 @@ +package dk.madsravn.interpreter.ast; + +import dk.madsravn.interpreter.tokens.Token; + +public class IfExpression implements IExpression { + private Token token; + private IExpression condition; + private BlockStatement consequence; + private BlockStatement alternative; + + public IfExpression(Token token, IExpression condition, BlockStatement consequence, BlockStatement alternative) { + this.token = token; + this.condition = condition; + this.consequence = consequence; + this.alternative = alternative; + } + + public IExpression getCondition() { + return condition; + } + + public BlockStatement getConsequence() { + return consequence; + } + + public BlockStatement getAlternative() { + return alternative; + } + + @Override + public void expressionNode() {} + + @Override + public String string() { + StringBuilder sb = new StringBuilder(); + sb.append("if "); + sb.append(condition.string()); + sb.append(" "); + sb.append(consequence.string()); + if (alternative != null) { + sb.append("else "); + sb.append(alternative.string()); + } + + return sb.toString(); + } + + @Override + public String tokenLiteral() { + return token.getLiteral(); + } +} diff --git a/src/main/java/dk/madsravn/interpreter/parser/Parser.java b/src/main/java/dk/madsravn/interpreter/parser/Parser.java index 1ddb9aa..0cf4deb 100644 --- a/src/main/java/dk/madsravn/interpreter/parser/Parser.java +++ b/src/main/java/dk/madsravn/interpreter/parser/Parser.java @@ -5,6 +5,7 @@ import dk.madsravn.interpreter.tokens.Token; import dk.madsravn.interpreter.tokens.TokenType; +import java.sql.Array; import java.util.ArrayList; import java.util.List; @@ -82,6 +83,10 @@ private IExpression getPrefixExpression() { return parseBoolean(); case LPAREN: return parseGroupedExpression(); + case IF: + return parseIfExpression(); + case FUNCTION: + return parseFunctionLiteral(); default: noPrefixParseFunctionError(currentToken.getType()); // TODO: This is ugly @@ -89,6 +94,72 @@ private IExpression getPrefixExpression() { } } + private IExpression parseCallExpression(IExpression function) { + CallExpression callExpression = new CallExpression(currentToken, function, parseCallArguments()); + return callExpression; + } + + private List parseCallArguments() { + List callArguments = new ArrayList(); + if (peekTokenType(RPAREN)) { + nextToken(); + return callArguments; + } + nextToken(); + callArguments.add(parseExpression(LOWEST)); + + while(peekTokenType(COMMA)) { + nextToken(); + nextToken(); + callArguments.add(parseExpression(LOWEST)); + } + if (!expectPeekType(RPAREN)) { + return null; + } + + return callArguments; + } + + private IExpression parseFunctionLiteral() { + Token token = currentToken; + if(!expectPeekType(LPAREN)) { + return null; + } + List parameters = parseFunctionParameters(); + if(!expectPeekType(LBRACE)) { + return null; + } + BlockStatement body = parseBlockStatement(); + return new FunctionLiteral(token, parameters, body); + } + + private List parseFunctionParameters() { + List parameters = new ArrayList(); + + if(peekTokenType(RPAREN)) { + nextToken(); + return parameters; + } + + nextToken(); + + Identifier identifier = new Identifier(currentToken, currentToken.getLiteral()); + parameters.add(identifier); + + while(peekTokenType(COMMA)) { + nextToken(); + nextToken(); + identifier = new Identifier(currentToken, currentToken.getLiteral()); + parameters.add(identifier); + } + + if (!expectPeekType(RPAREN)) { + return null; + } + + return parameters; + } + private IExpression parseGroupedExpression() { nextToken(); IExpression expression = parseExpression(LOWEST); @@ -98,6 +169,56 @@ private IExpression parseGroupedExpression() { return expression; } + private IExpression parseIfExpression() { + Token token = currentToken; + if (!expectPeekType(LPAREN)) { + return null; + } + + nextToken(); + IExpression condition = parseExpression(LOWEST); + + if (!expectPeekType(RPAREN)) { + return null; + } + + if (!expectPeekType(LBRACE)) { + return null; + } + BlockStatement consequence = parseBlockStatement(); + BlockStatement alternative = parseAlternativeBlock(); + + IfExpression ifExpression = new IfExpression(token, condition, consequence, alternative); + return ifExpression; + } + + private BlockStatement parseAlternativeBlock() { + if (peekTokenType(ELSE)) { + nextToken(); + if (!expectPeekType(LBRACE)) { + return null; + } + return parseBlockStatement(); + } + return null; + } + + private BlockStatement parseBlockStatement() { + Token token = currentToken; + List statements = new ArrayList(); + + nextToken(); + + while (!currentTokenType(RBRACE) && !currentTokenType(TokenType.EOF)) { + IStatement statement = parseStatement(); + if (statement != null) { + statements.add(statement); + } + nextToken(); + } + return new BlockStatement(token, statements); + } + private IExpression parseExpression(PrecedenceEnum precedence) { IExpression left = getPrefixExpression(); if (left == null) { @@ -120,6 +241,8 @@ private IExpression parseExpression(PrecedenceEnum precedence) { private PrecedenceEnum getPrecedence(TokenType type) { switch (type) { + case LPAREN: + return CALL; case EQ, NOT_EQ: return EQUALS; case LT, GT: diff --git a/src/test/java/dk/madsravn/interpreter/parser/ParserTest.java b/src/test/java/dk/madsravn/interpreter/parser/ParserTest.java index 13c885e..05e4435 100644 --- a/src/test/java/dk/madsravn/interpreter/parser/ParserTest.java +++ b/src/test/java/dk/madsravn/interpreter/parser/ParserTest.java @@ -363,7 +363,193 @@ public void testOperatorPrecedenceParsing() { } } + public void testIdentifier(IExpression expression, String value) { + assertTrue(expression instanceof Identifier); + Identifier identifier = (Identifier) expression; + assertEquals(identifier.getValue(), value); + assertEquals(identifier.tokenLiteral(), value); + } + + @Test + public void testIfExpression() { + String input = "if (x < y) { x }"; + Lexer lexer = new Lexer(input); + Parser parser = new Parser(lexer); + Program program = parser.parseProgram(); + checkForParseErrors(parser, input); + + assertEquals(program.getStatementsLength(), 1); + assertTrue(program.getStatements().get(0) instanceof ExpressionStatement); + ExpressionStatement expressionStatement = (ExpressionStatement) program.getStatements().get(0); + + assertTrue(expressionStatement.getExpression() instanceof IfExpression); + IfExpression ifExpression = (IfExpression) expressionStatement.getExpression(); + + assertTrue(ifExpression.getCondition() instanceof InfixExpression); + InfixExpression infixExpression = (InfixExpression) ifExpression.getCondition(); + assertEquals(infixExpression.getOperator(), "<"); + + testIdentifier(infixExpression.getLeft(), "x"); + testIdentifier(infixExpression.getRight(), "y"); + + assertEquals(ifExpression.getConsequence().getStatementsLength(), 1); + assertTrue(ifExpression.getConsequence().getStatements().get(0) instanceof ExpressionStatement); + ExpressionStatement consequence = (ExpressionStatement) ifExpression.getConsequence().getStatements().get(0); + testIdentifier(consequence.getExpression(), "x"); + assertNull(ifExpression.getAlternative()); + + } + + public void testIfElseExpression() { + String input = "if (x < y) { x } else { y }"; + Lexer lexer = new Lexer(input); + Parser parser = new Parser(lexer); + Program program = parser.parseProgram(); + checkForParseErrors(parser, input); + + assertEquals(program.getStatementsLength(), 1); + assertTrue(program.getStatements().get(0) instanceof ExpressionStatement); + ExpressionStatement expressionStatement = (ExpressionStatement) program.getStatements().get(0); + + assertTrue(expressionStatement.getExpression() instanceof IfExpression); + IfExpression ifExpression = (IfExpression) expressionStatement.getExpression(); + + assertTrue(ifExpression.getCondition() instanceof InfixExpression); + InfixExpression infixExpression = (InfixExpression) ifExpression.getCondition(); + assertEquals(infixExpression.getOperator(), "<"); + + testIdentifier(infixExpression.getLeft(), "x"); + testIdentifier(infixExpression.getRight(), "y"); + + assertEquals(ifExpression.getConsequence().getStatementsLength(), 1); + assertTrue(ifExpression.getConsequence().getStatements().get(0) instanceof ExpressionStatement); + ExpressionStatement consequence = (ExpressionStatement) ifExpression.getConsequence().getStatements().get(0); + testIdentifier(consequence.getExpression(), "x"); + + assertEquals(ifExpression.getAlternative().getStatementsLength(), 1); + assertTrue(ifExpression.getAlternative().getStatements().get(0) instanceof ExpressionStatement); + ExpressionStatement alternative = (ExpressionStatement) ifExpression.getAlternative().getStatements().get(0); + testIdentifier(alternative.getExpression(), "y"); + } + + @Test + public void testFuntionLiteralParsing() { + String input = "fn(x, y) { x + y; }"; + Lexer lexer = new Lexer(input); + Parser parser = new Parser(lexer); + Program program = parser.parseProgram(); + checkForParseErrors(parser, input); + + assertEquals(program.getStatementsLength(), 1); + assertTrue(program.getStatements().get(0) instanceof ExpressionStatement); + + ExpressionStatement expressionStatement = (ExpressionStatement) program.getStatements().get(0); + assertTrue(expressionStatement.getExpression() instanceof FunctionLiteral); + + FunctionLiteral functionLiteral = (FunctionLiteral) expressionStatement.getExpression(); + assertEquals(functionLiteral.getParametersLength(), 2); + + testIdentifier(functionLiteral.getParameters().get(0), "x"); + testIdentifier(functionLiteral.getParameters().get(1), "y"); + + assertEquals(functionLiteral.getBody().getStatementsLength(), 1); + + assertTrue(functionLiteral.getBody().getStatements().get(0) instanceof ExpressionStatement); + ExpressionStatement bodyStatement = (ExpressionStatement) functionLiteral.getBody().getStatements().get(0); + + assertTrue(bodyStatement.getExpression() instanceof InfixExpression); + InfixExpression infixExpression = (InfixExpression) bodyStatement.getExpression(); + testIdentifier(infixExpression.getLeft(), "x"); + testIdentifier(infixExpression.getRight(), "y"); + assertEquals(infixExpression.getOperator(), "+"); + } + + private class FunctionParameterData { + public String input; + public List parameters; + public FunctionParameterData(String input, List parameters) { + this.input = input; + this.parameters = parameters; + } + } + + @Test + public void testFuntionParameterParsing() { + List inputs = Arrays.asList( + new FunctionParameterData("fn() {};", Arrays.asList()), + new FunctionParameterData("fn(x) {};", Arrays.asList("x")), + new FunctionParameterData("fn(x, y) {};", Arrays.asList("x", "y")), + new FunctionParameterData("fn(x, y, z) {};", Arrays.asList("x", "y", "z")) + ); + + for(FunctionParameterData input : inputs) { + Lexer lexer = new Lexer(input.input); + Parser parser = new Parser(lexer); + Program program = parser.parseProgram(); + checkForParseErrors(parser, input.input); + + assertEquals(program.getStatementsLength(), 1); + assertTrue(program.getStatements().get(0) instanceof ExpressionStatement); + ExpressionStatement expressionStatement = (ExpressionStatement) program.getStatements().get(0); + + assertTrue(expressionStatement.getExpression() instanceof FunctionLiteral); + FunctionLiteral functionLiteral = (FunctionLiteral) expressionStatement.getExpression(); + + assertEquals(functionLiteral.getParametersLength(), input.parameters.size()); + for(int i = 0; i < input.parameters.size(); i++) { + testIdentifier(functionLiteral.getParameters().get(i), input.parameters.get(i)); + } + } + } + + @Test + public void testCallExpressionParsing() { + String input = "add(1, 2 * 3, 4 + 5);"; + Lexer lexer = new Lexer(input); + Parser parser = new Parser(lexer); + Program program = parser.parseProgram(); + checkForParseErrors(parser, input); + + assertEquals(program.getStatementsLength(), 1); + assertTrue(program.getStatements().get(0) instanceof ExpressionStatement); + ExpressionStatement expressionStatement = (ExpressionStatement) program.getStatements().get(0); + assertTrue(expressionStatement.getExpression() instanceof CallExpression); + CallExpression callExpression = (CallExpression) expressionStatement.getExpression(); + + testIdentifier(callExpression.getFunction(), "add"); + assertEquals(callExpression.getArguments().size(), 3); + + testIntegerLiteral(callExpression.getArguments().get(0), 1); + testIntegerInfixExpression(callExpression.getArguments().get(1), 2, "*", 3); + testIntegerInfixExpression(callExpression.getArguments().get(2), 4, "+", 5); + + } + + public void testIntegerInfixExpression(IExpression expression, int left, String operator, int right) { + assertTrue(expression instanceof InfixExpression); + InfixExpression infixExpression = (InfixExpression) expression; + testIntegerLiteral(infixExpression.getLeft(), left); + testIntegerLiteral(infixExpression.getRight(), right); + assertEquals(infixExpression.getOperator(), operator); + } + + public void testIntegerLiteral(IExpression expression, int value) { + assertTrue(expression instanceof IntegerLiteral); + IntegerLiteral integerLiteral = (IntegerLiteral) expression; + assertEquals(integerLiteral.getValue(), value); + assertEquals(integerLiteral.tokenLiteral(), "" + value); + } + private void checkForParseErrors(Parser parser, String input) { - assertEquals(parser.getErrors().size(), 0, "Input [" + input + "] should not gives errors. There should not be any errors: " + parser.getErrors()); + assertEquals(parser.getErrors().size(), 0, "Input [" + input + "] should not gives errors. There should not be any errors: " + formatErrors(parser.getErrors())); + } + + private String formatErrors(List errors) { + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + for ( String error : errors) { + sb.append(error + "\n"); + } + return sb.toString(); } }