diff --git a/cobalt/lang/Cobalt.java b/cobalt/lang/Cobalt.java index 99ab722..e080567 100644 --- a/cobalt/lang/Cobalt.java +++ b/cobalt/lang/Cobalt.java @@ -22,6 +22,7 @@ public class Cobalt { } else if (args.length == 1) { runFile(args[0]); } else { + System.out.println("Cobalt v0.1.0"); runPrompt(); } } diff --git a/cobalt/lang/Environment.java b/cobalt/lang/Environment.java new file mode 100644 index 0000000..c89834e --- /dev/null +++ b/cobalt/lang/Environment.java @@ -0,0 +1,54 @@ +package cobalt.lang; + +import java.util.HashMap; +import java.util.Map; + +class Environment { + final Environment enclosing; + private final Map values = new HashMap<>(); + + + Environment() { + enclosing = null; + } + + Environment(Environment enclosing) { + this.enclosing = enclosing; + } + + + Object get(Token name) { + if (values.containsKey(name.lexeme)) { + return values.get(name.lexeme); + } + + if (enclosing != null) return enclosing.get(name); + + throw new RuntimeError(name, + "Undefined reference to variable: '" + name.lexeme + "'."); + } + + void define(Token name, Object value) { + if (!values.containsKey(name.lexeme)) { + values.put(name.lexeme, value); + } else { + throw new RuntimeError(name, + "Undefined reference to variable: '" + name.lexeme + "'."); + } + } + + void assign(Token name, Object value) { + if (values.containsKey(name.lexeme)) { + values.put(name.lexeme, value); + return; + } + + if (enclosing != null) { + enclosing.assign(name, value); + return; + } + + throw new RuntimeError(name, + "Undefined variable '" + name.lexeme + "'."); + } +} \ No newline at end of file diff --git a/cobalt/lang/Expr.java b/cobalt/lang/Expr.java index c21b6f4..8303cb5 100644 --- a/cobalt/lang/Expr.java +++ b/cobalt/lang/Expr.java @@ -8,6 +8,8 @@ abstract class Expr { R visitGroupingExpr(Grouping expr); R visitLiteralExpr(Literal expr); R visitUnaryExpr(Unary expr); + R visitVarExpr(Variable expr); + R visitAssignExpr(Assign expr); } @@ -68,5 +70,33 @@ abstract class Expr { final Expr right; } + static class Variable extends Expr { + Variable(Token name, boolean nullable) { + this.name = name; + this.nullable = nullable; + } + + R accept(Visitor visitor) { + return visitor.visitVarExpr(this); + } + + final Token name; + final boolean nullable; + } + + static class Assign extends Expr { + Assign(Token name, Expr value) { + this.name = name; + this.value = value; + } + + R accept(Visitor visitor) { + return visitor.visitAssignExpr(this); + } + + final Token name; + final Expr value; + } + abstract R accept(Visitor visitor); } diff --git a/cobalt/lang/Interpreter.java b/cobalt/lang/Interpreter.java index db8e67d..646fe94 100644 --- a/cobalt/lang/Interpreter.java +++ b/cobalt/lang/Interpreter.java @@ -1,14 +1,11 @@ -// -// Continue at Section 7.2.3 -// Page 100 -// - package cobalt.lang; import java.util.List; class Interpreter implements Expr.Visitor, Stmt.Visitor { + private Environment environment = new Environment(); + void interpret(List statements) { try { for (Stmt statement : statements) { @@ -38,6 +35,19 @@ class Interpreter implements Expr.Visitor, Stmt.Visitor { return null; } + @Override + public Object visitVarExpr(Expr.Variable expr) { + // TODO: Fix analogous variable issues + return environment.get(expr.name); + // Object var_value = environment.get(expr.name); + // if (var_value != null) { + // return environment.get(expr.name); + // } else { + // throw new RuntimeError(expr.name, + // "Reference to undefined variable: '" + expr.name + "'."); + // } + } + private boolean isTruthy(Object object) { if (object == null) return false; @@ -59,6 +69,26 @@ class Interpreter implements Expr.Visitor, Stmt.Visitor { stmt.accept(this); } + private void executeBlock(List statements, + Environment environment) { + Environment previous = this.environment; + try { + this.environment = environment; + + for (Stmt statement : statements) { + execute(statement); + } + } finally { + this.environment = previous; + } + } + + @Override + public Void visitBlockStmt(Stmt.Block stmt) { + executeBlock(stmt.statements, new Environment(environment)); + return null; + } + @Override public Void visitExpressionStmt(Stmt.Expression stmt) { evaluate(stmt.expression); @@ -72,6 +102,28 @@ class Interpreter implements Expr.Visitor, Stmt.Visitor { return null; } + @Override + public Void visitVarStmt(Stmt.Var stmt) { + Object value = null; + if (stmt.initializer != null) { + value = evaluate(stmt.initializer); + } + + if (!stmt.nullable && value == null) { + throw new RuntimeError(stmt.name, "Assignment of 'nil' to non-nullable type"); + } else { + environment.define(stmt.name, value); + return null; + } + } + + @Override + public Object visitAssignExpr(Expr.Assign expr) { + Object value = evaluate(expr.value); + environment.assign(expr.name, value); + return value; + } + @Override public Object visitBinaryExpr(Expr.Binary expr) { Object left = evaluate(expr.left); @@ -100,8 +152,6 @@ class Interpreter implements Expr.Visitor, Stmt.Visitor { checkNumberOperand(expr.operator, right); return (double) left - (double) right; case PLUS: - System.out.println("Left: " + left.getClass()); - System.out.println("Right: " + right.getClass()); if (left instanceof Double && right instanceof Double) { return (double) left + (double) right; } else if (left instanceof String && right instanceof String) { @@ -110,8 +160,12 @@ class Interpreter implements Expr.Visitor, Stmt.Visitor { return (String) left + (Double) right; } else if (left instanceof Double && right instanceof String) { return (Double) left + (String) right; + } else if ((left == null) && right instanceof String) { + return "nil" + (String) right; + } else if (left instanceof String && (right == null)) { + return (String) left + "nil"; } - throw new RuntimeError(expr.operator, "Operands must be of type number or string"); + throw new RuntimeError(expr.operator, "Invalid operands: '" + left + "' and '" + right + "'"); case SLASH: checkNumberOperands(expr.operator, left, right); checkNumberOperand(expr.operator, right); diff --git a/cobalt/lang/Parser.java b/cobalt/lang/Parser.java index d7819cf..b7945fa 100644 --- a/cobalt/lang/Parser.java +++ b/cobalt/lang/Parser.java @@ -1,6 +1,9 @@ package cobalt.lang; import java.util.List; + +import cobalt.lang.Expr.Variable; + import java.util.ArrayList; import static cobalt.lang.TokenType.*; @@ -18,7 +21,7 @@ class Parser { List parse() { List statements = new ArrayList<>(); while (!isAtEnd()) { - statements.add(statement()); + statements.add(declaration()); } return statements; @@ -26,12 +29,24 @@ class Parser { private Expr expression() { - return equality(); + return assignment(); + } + + + private Stmt declaration() { + try { + if (match(VAR)) return varDeclaration(); + return statement(); + } catch (ParseError error) { + synchronize(); + return null; + } } private Stmt statement() { if (match(PRINT)) return printStatement(); + if (match(LEFT_BRACE)) return new Stmt.Block(block()); return expressionStatement(); } @@ -39,17 +54,72 @@ class Parser { private Stmt printStatement() { Expr value = expression(); - consume(SEMICOLON, "Expect ';' after value."); + consume(SEMICOLON, "Expected ';' after value."); return new Stmt.Print(value); } + + private Stmt varDeclaration() { + Token name = consume(IDENTIFIER, "Expected variable name."); + + Expr initializer = null; + boolean nullable = false; + + if (match(NULLABLE)) { + nullable = true; + } + + if (match(EQUAL)) { + initializer = expression(); + } + + consume(SEMICOLON, "Expected ';' after variable declaration."); + + return new Stmt.Var(name, nullable, initializer); + } + + private Stmt expressionStatement() { Expr expr = expression(); - consume(SEMICOLON, "Expect ';' after value."); + consume(SEMICOLON, "Expected ';' after value."); return new Stmt.Expression(expr); } + private List block() { + List statements = new ArrayList<>(); + + while (!check(RIGHT_BRACE) && !isAtEnd()) { + statements.add(declaration()); + } + + consume(RIGHT_BRACE, "Expected '}' after block."); + return statements; + } + + + private Expr assignment() { + Expr expr = equality(); + + if (match(EQUAL)) { + Token equals = previous(); + Expr value = assignment(); + + if (expr instanceof Expr.Variable) { + Variable var = (Expr.Variable) expr; + Token name = var.name; + if (!var.nullable && value == null) { + error(equals, "Invalid assignment of 'nil' on non-nullable type"); + } else { + return new Expr.Assign(name, value); + } + } + error(equals, "Invalid assignment on non-variable type"); + } + + return expr; + } + // Parser definition for handling equalities // // equality -> comparison ( ( "!=" | "==" ) comparison )* ; @@ -139,6 +209,14 @@ class Parser { return new Expr.Literal(previous().literal); } + if (match(IDENTIFIER)) { + if (match(NULLABLE)) { + return new Expr.Variable(previous(), true); + } else { + return new Expr.Variable(previous(), false); + } + } + if (match(LEFT_PAREN)) { Expr expr = expression(); consume(RIGHT_PAREN, "Expect ')' after expression."); diff --git a/cobalt/lang/Scanner.java b/cobalt/lang/Scanner.java index 91267c8..e286380 100644 --- a/cobalt/lang/Scanner.java +++ b/cobalt/lang/Scanner.java @@ -89,14 +89,15 @@ class Scanner { case '>': addToken(match('=') ? GREATER_EQUAL : GREATER); break; - case '/': - if (match('/')) { - // A comment goes until the end of the line - while (peek() != '\n' && !isAtEnd()) advance(); - } else { - addToken(SLASH); - } + case '/': addToken(SLASH); break; + case '?': addToken(NULLABLE); break; + + // Comments + case '#': + // A comment goes until the end of the line + while (peek() != '\n' && !isAtEnd()) advance(); break; + // Whitespace and new lines case ' ': diff --git a/cobalt/lang/Stmt.java b/cobalt/lang/Stmt.java index 51f785b..4aaff4f 100644 --- a/cobalt/lang/Stmt.java +++ b/cobalt/lang/Stmt.java @@ -4,22 +4,24 @@ import java.util.List; abstract class Stmt { interface Visitor { - //R visitBlockStmt(Block stmt); + R visitBlockStmt(Block stmt); R visitExpressionStmt(Expression stmt); R visitPrintStmt(Print stmt); - //R visitVarStmt(Var stmt); + R visitVarStmt(Var stmt); } - // static class Block extends Stmt { - // Block(List statements) { - // this.statements = statements; - // } - // R accept(Visitor visitor) { - // return visitor.visitBlockStmt(this); - // } + static class Block extends Stmt { + Block(List statements) { + this.statements = statements; + } + + R accept(Visitor visitor) { + return visitor.visitBlockStmt(this); + } + + final List statements; + } - // final List statements; - // } static class Expression extends Stmt { Expression(Expr expression) { this.expression = expression; @@ -42,19 +44,22 @@ abstract class Stmt { final Expr expression; } - // static class Var extends Stmt { - // Var(Token name, Expr initializer) { - // this.name = name; - // this.initializer = initializer; - // } + + static class Var extends Stmt { + Var(Token name, boolean nullable, Expr initializer) { + this.name = name; + this.nullable = nullable; + this.initializer = initializer; + } - // R accept(Visitor visitor) { - // return visitor.visitVarStmt(this); - // } + R accept(Visitor visitor) { + return visitor.visitVarStmt(this); + } - // final Token name; - // final Expr initializer; - // } + final Token name; + final boolean nullable; + final Expr initializer; + } abstract R accept(Visitor visitor); } \ No newline at end of file diff --git a/cobalt/lang/TokenType.java b/cobalt/lang/TokenType.java index 2cd6a6f..d183a8b 100644 --- a/cobalt/lang/TokenType.java +++ b/cobalt/lang/TokenType.java @@ -7,7 +7,7 @@ enum TokenType { // One or two character tokens. BANG, BANG_EQUAL, EQUAL, EQUAL_EQUAL, - GREATER, GREATER_EQUAL, LESS, LESS_EQUAL, + GREATER, GREATER_EQUAL, LESS, LESS_EQUAL, NULLABLE, // Literals IDENTIFIER, STRING, NUMBER, diff --git a/cobalt/tool/GenerateAst.java b/cobalt/tool/GenerateAst.java index 4cb9e2e..49e68d4 100644 --- a/cobalt/tool/GenerateAst.java +++ b/cobalt/tool/GenerateAst.java @@ -18,6 +18,7 @@ public class GenerateAst { "Binary : Expr left, Token operator, Expr right", "Grouping : Expr expression", "Literal : Object value", + "Variable : Token Name", "Unary : Token operator, Expr right", "Variable : Token name" )); diff --git a/examples/test.cobalt b/examples/test.cobalt index 892fd86..0bd7c22 100644 --- a/examples/test.cobalt +++ b/examples/test.cobalt @@ -1,5 +1,25 @@ +# Statement tests print "These are statement tests for Cobalt!"; print 10 + 12; print 13 + (14 - 7) * 3; print "Hello, " + "World!"; -print "String " + "Multiplication " * 3; \ No newline at end of file +print "String " + "Multiplication " * 3; + +# Variable declaration +var a = 10; +print ("a is " + a); + +# Nullable variable declaration +var b? = nil; +print ("b is " + b); + +# Examples of structured block statements with scoping +{ + print "we are now in a nested scope!"; + + # Don't really like this behaviour, as I think it should throw a runtime error + # Instead it pulls a's state of 10 in the upper environment block + # Still trying to resolve this... + var a = a + 5; + print ("a is " + a); +} \ No newline at end of file