Compare commits

..

10 Commits

28 changed files with 1507 additions and 109 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.class

View File

@ -1,2 +1,6 @@
# CobaltScript
A small programming language designed for general purpose scripting and for learning interpreter design
# Cobalt
Cobalt is a small programming language designed for general purpose and systems level scripting. This project follows very closely to the curriculum from [Crafting Interpreters](http://craftinginterpreters.com/) by Robert Nystrom, and is intended to be a pet project for myself to learn programming language design and interpreter design concepts.
Currently, Cobalt follows the same grammer and design structure as [Lox](https://github.com/munificent/craftinginterpreters), the language featured in Nystrom's book. However, I eventually plan to mutate Cobalt from these design patterns and create a unique syntax and structure once I have reached a high enough proficiency.
This project is a **work in progress**, so some things might be a bit broken. YMMV :)

BIN
cobalt

Binary file not shown.

View File

@ -0,0 +1,54 @@
package cobalt.lang;
// Creates an unambiguous, if ugly, string representation of AST nodes.
class AstPrinter implements Expr.Visitor<String> {
String print(Expr expr) {
return expr.accept(this);
}
@Override
public String visitBinaryExpr(Expr.Binary expr) {
return parenthesize(expr.operator.lexeme, expr.left, expr.right);
}
@Override
public String visitGroupingExpr(Expr.Grouping expr) {
return parenthesize("group", expr.expression);
}
@Override
public String visitLiteralExpr(Expr.Literal expr) {
if (expr.value == null) return "nil";
return expr.value.toString();
}
@Override
public String visitUnaryExpr(Expr.Unary expr) {
return parenthesize(expr.operator.lexeme, expr.right);
}
private String parenthesize(String name, Expr... exprs) {
StringBuilder builder = new StringBuilder();
builder.append("(").append(name);
for (Expr expr : exprs) {
builder.append(" ");
builder.append(expr.accept(this));
}
builder.append(")");
return builder.toString();
}
public static void main(String[] args) {
Expr expression = new Expr.Binary(
new Expr.Unary(
new Token(TokenType.MINUS, "-", null, 1),
new Expr.Literal(123)),
new Token(TokenType.STAR, "*", null, 1),
new Expr.Grouping(
new Expr.Literal(45.67)));
System.out.println(new AstPrinter().print(expression));
}
}

91
cobalt/lang/Cobalt.java Normal file
View File

@ -0,0 +1,91 @@
package cobalt.lang;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
public class Cobalt {
private static final Interpreter interpreter = new Interpreter();
static boolean hadError = false;
static boolean hadRuntimeError = false;
public static void main(String[] args) throws IOException {
if (args.length > 1) {
System.out.println("Usage: cobalt [script]");
System.exit(64);
} else if (args.length == 1) {
runFile(args[0]);
} else {
System.out.println("Cobalt v0.1.0");
runPrompt();
}
}
private static void runFile(String path) throws IOException {
byte[] bytes = Files.readAllBytes(Paths.get(path));
run(new String(bytes, Charset.defaultCharset()));
if (hadError) System.exit(65);
if (hadRuntimeError) System.exit(70);
}
private static void runPrompt() throws IOException {
InputStreamReader input = new InputStreamReader(System.in);
BufferedReader reader = new BufferedReader(input);
for (;;) {
System.out.print("> ");
String line = reader.readLine();
if (line == null) break;
run(line);
hadError = false;
}
}
private static void run(String source) {
Scanner scanner = new Scanner(source);
List<Token> tokens = scanner.scanTokens();
Parser parser = new Parser(tokens);
List<Stmt> statements = parser.parse();
if (hadError) return;
//System.out.println(new AstPrinter().print(expression));
interpreter.interpret(statements);
}
static void error(int line, String message) {
report(line, "", message);
}
private static void report(int line, String where, String message) {
System.err.println(
"[line " + line + "] Error" + where + ": " + message
);
hadError = true;
}
static void error(Token token, String message) {
if (token.type == TokenType.EOF) {
report(token.line, " at end", message);
} else {
report(token.line, " at '" + token.lexeme + "'", message);
}
}
static void runtimeError(RuntimeError error) {
System.err.println("[line " + error.token.line + "] " + error.getMessage());
hadRuntimeError = true;
}
}

View File

@ -0,0 +1,54 @@
package cobalt.lang;
import java.util.HashMap;
import java.util.Map;
class Environment {
final Environment enclosing;
private final Map<String, Object> 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 + "'.");
}
}

102
cobalt/lang/Expr.java Normal file
View File

@ -0,0 +1,102 @@
package cobalt.lang;
import java.util.List;
abstract class Expr {
interface Visitor<R> {
R visitBinaryExpr(Binary expr);
R visitGroupingExpr(Grouping expr);
R visitLiteralExpr(Literal expr);
R visitUnaryExpr(Unary expr);
R visitVarExpr(Variable expr);
R visitAssignExpr(Assign expr);
}
static class Binary extends Expr {
Binary(Expr left, Token operator, Expr right) {
this.left = left;
this.operator = operator;
this.right = right;
}
<R> R accept(Visitor<R> visitor) {
return visitor.visitBinaryExpr(this);
}
final Expr left;
final Token operator;
final Expr right;
}
static class Grouping extends Expr {
Grouping(Expr expression) {
this.expression = expression;
}
<R> R accept(Visitor<R> visitor) {
return visitor.visitGroupingExpr(this);
}
final Expr expression;
}
static class Literal extends Expr {
Literal(Object value) {
this.value = value;
}
<R> R accept(Visitor<R> visitor) {
return visitor.visitLiteralExpr(this);
}
final Object value;
}
static class Unary extends Expr {
Unary(Token operator, Expr right) {
this.operator = operator;
this.right = right;
}
<R> R accept(Visitor<R> visitor) {
return visitor.visitUnaryExpr(this);
}
final Token operator;
final Expr right;
}
static class Variable extends Expr {
Variable(Token name, boolean nullable) {
this.name = name;
this.nullable = nullable;
}
<R> R accept(Visitor<R> 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> R accept(Visitor<R> visitor) {
return visitor.visitAssignExpr(this);
}
final Token name;
final Expr value;
}
abstract <R> R accept(Visitor<R> visitor);
}

View File

@ -0,0 +1,250 @@
package cobalt.lang;
import java.util.List;
class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
private Environment environment = new Environment();
void interpret(List<Stmt> statements) {
try {
for (Stmt statement : statements) {
execute(statement);
}
} catch(RuntimeError error) {
Cobalt.runtimeError(error);
}
}
@Override
public Object visitLiteralExpr(Expr.Literal expr) {
return expr.value;
}
@Override
public Object visitUnaryExpr(Expr.Unary expr) {
Object right = evaluate(expr.right);
switch (expr.operator.type) {
case BANG:
return !isTruthy(right);
case MINUS:
return -(double) right;
}
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;
if (object instanceof Boolean)
return (boolean) object;
return true;
}
@Override
public Object visitGroupingExpr(Expr.Grouping expr) {
return evaluate(expr.expression);
}
private Object evaluate(Expr expr) {
return expr.accept(this);
}
private void execute(Stmt stmt) {
stmt.accept(this);
}
private void executeBlock(List<Stmt> 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);
return null;
}
@Override
public Void visitPrintStmt(Stmt.Print stmt) {
Object value = evaluate(stmt.expression);
System.out.println(stringify(value));
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);
Object right = evaluate(expr.right);
switch (expr.operator.type) {
case GREATER:
checkNumberOperands(expr.operator, left, right);
return (double) left > (double) right;
case LESS:
checkNumberOperands(expr.operator, left, right);
return (double) left < (double) right;
case GREATER_EQUAL:
checkNumberOperands(expr.operator, left, right);
return (double) left >= (double) right;
case LESS_EQUAL:
checkNumberOperands(expr.operator, left, right);
return (double) left <= (double) right;
case BANG_EQUAL:
checkNumberOperands(expr.operator, left, right);
return !isEqual(left, right);
case EQUAL_EQUAL:
checkNumberOperands(expr.operator, left, right);
return isEqual(left, right);
case MINUS:
checkNumberOperand(expr.operator, right);
return (double) left - (double) right;
case PLUS:
if (left instanceof Double && right instanceof Double) {
return (double) left + (double) right;
} else if (left instanceof String && right instanceof String) {
return (String) left + (String) right;
} else if (left instanceof String && right instanceof Double) {
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, "Invalid operands: '" + left + "' and '" + right + "'");
case SLASH:
checkNumberOperands(expr.operator, left, right);
checkNumberOperand(expr.operator, right);
return (double) left / (double) right;
case STAR:
checkNumberOperands(expr.operator, left, right);
boolean usingStrings = false;
String val = "";
double limit = 0;
if (left instanceof String) {
usingStrings = true;
val = (String)left;
limit = (Double)right;
} else if (right instanceof String) {
usingStrings = true;
val = (String)right;
limit = (Double)left;
}
if (usingStrings) {
String output = "";
for (int i = 0; i < limit; i++) {
output += val;
}
return output;
} else {
return (double) left * (double) right;
}
}
// Unreachable
return null;
}
private boolean isEqual(Object a, Object b) {
if (a == null && b == null)
return true;
if (a == null)
return false;
return a.equals(b);
}
private String stringify(Object object) {
if (object == null) return "nil";
if (object instanceof Double) {
String text = object.toString();
if (text.endsWith(".0")) {
text = text.substring(0, text.length() - 2);
}
return text;
}
return object.toString();
}
private void checkNumberOperand(Token operator, Object operand) {
if (operand instanceof Double) {
if (operator.type == TokenType.SLASH && (double)operand == 0) {
throw new RuntimeError(operator, "Division by zero is not allowed");
}
return;
}
throw new RuntimeError(operator, "Operand must be a number");
}
private void checkNumberOperands(Token operator, Object left, Object right) {
if (left instanceof Double && right instanceof Double) {
return;
}
if (left instanceof String && right instanceof Double) {
return;
}
if (left instanceof Double && right instanceof String) {
return;
}
throw new RuntimeError(operator, "Operands must be numbers");
}
}

315
cobalt/lang/Parser.java Normal file
View File

@ -0,0 +1,315 @@
package cobalt.lang;
import java.util.List;
import cobalt.lang.Expr.Variable;
import java.util.ArrayList;
import static cobalt.lang.TokenType.*;
class Parser {
private static class ParseError extends RuntimeException {}
private final List<Token> tokens;
private int current = 0;
Parser(List<Token> tokens) {
this.tokens = tokens;
}
List<Stmt> parse() {
List<Stmt> statements = new ArrayList<>();
while (!isAtEnd()) {
statements.add(declaration());
}
return statements;
}
private Expr expression() {
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();
}
private Stmt printStatement() {
Expr value = expression();
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, "Expected ';' after value.");
return new Stmt.Expression(expr);
}
private List<Stmt> block() {
List<Stmt> 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 )* ;
//
private Expr equality() {
Expr expr = comparison();
while (match(BANG_EQUAL, EQUAL_EQUAL)) {
Token operator = previous();
Expr right = comparison();
expr = new Expr.Binary(expr, operator, right);
}
return expr;
}
// Parser definition for handling comparisons
//
// comparison -> term ( ( ">" | ">=" | "<" | "<=" ) term )* ;
//
private Expr comparison() {
Expr expr = term();
while (match(GREATER, GREATER_EQUAL, LESS, LESS_EQUAL)) {
Token operator = previous();
Expr right = term();
expr = new Expr.Binary(expr, operator, right);
}
return expr;
}
private Expr term() {
Expr expr = factor();
while (match(MINUS, PLUS)) {
Token operator = previous();
Expr right = factor();
expr = new Expr.Binary(expr, operator, right);
}
return expr;
}
private Expr factor() {
Expr expr = unary();
while (match(SLASH, STAR)) {
Token operator = previous();
Expr right = unary();
expr = new Expr.Binary(expr, operator, right);
}
return expr;
}
// Parser definition for handling unary statements
//
// unary -> ( "!" | "-" ) unary
// | primary ;
//
private Expr unary() {
if (match(BANG, MINUS)) {
Token operator = previous();
Expr right = unary();
return new Expr.Unary(operator, right);
}
return primary();
}
// Parser definition for handling primary statements
//
// primary -> NUMBER | STRING | "true" | "false" | "nil"
// | "(" expression ")" ;
//
private Expr primary() {
if (match(FALSE)) return new Expr.Literal(false);
if (match(TRUE)) return new Expr.Literal(true);
if (match(NIL)) return new Expr.Literal(null);
if (match(NUMBER, STRING)) {
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.");
return new Expr.Grouping(expr);
}
throw error(peek(), "Expected expression.");
}
// Checks to see if the current token has any of the given types. If so,
// it consumes the token and returns true. Otherwise, it returns false and leaves
// the current token alone.
private boolean match(TokenType... types) {
for (TokenType type : types) {
if (check(type)) {
advance();
return true;
}
}
return false;
}
// Consume function
private Token consume(TokenType type, String message) {
if (check(type)) return advance();
throw error(peek(), message);
}
// Returns true if the current token is of the given type, false if otherwise
private boolean check(TokenType type) {
if (isAtEnd()) return false;
return peek().type == type;
}
// Consume the current token and return it
private Token advance() {
if (!isAtEnd()) current++;
return previous();
}
// Check to see if we've run out of tokens to parse
private boolean isAtEnd() {
return peek().type == EOF;
}
// Return the current token we have yet to consume
private Token peek() {
return tokens.get(current);
}
// Return the previous token we just consumed
private Token previous() {
return tokens.get(current - 1);
}
// Throw a new parse error based on the incorrect token
private ParseError error(Token token, String message) {
Cobalt.error(token, message);
return new ParseError();
}
// Once finding a parse error, discard the rest of the tokens in
// a statement in order to prevent cascading errors.
private void synchronize() {
advance();
while (!isAtEnd()) {
if (previous().type == SEMICOLON) return;
switch (peek().type) {
case CLASS:
case FOR:
case FUNC:
case IF:
case PRINT:
case RETURN:
case VAR:
case WHILE:
return;
}
advance();
}
}
}

View File

@ -0,0 +1,10 @@
package cobalt.lang;
class RuntimeError extends RuntimeException {
final Token token;
RuntimeError(Token token, String message) {
super(message);
this.token = token;
}
}

318
cobalt/lang/Scanner.java Normal file
View File

@ -0,0 +1,318 @@
package cobalt.lang;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cobalt.lang.TokenType.*;
class Scanner {
private final String source;
private final List<Token> tokens = new ArrayList<>();
private int start = 0;
private int current = 0;
private int line = 0;
private static final Map<String, TokenType> keywords;
static {
keywords = new HashMap<>();
keywords.put("and", AND);
keywords.put("class", CLASS);
keywords.put("else", ELSE);
keywords.put("false", FALSE);
keywords.put("for", FOR);
keywords.put("func", FUNC);
keywords.put("if", IF);
keywords.put("nil", NIL);
keywords.put("or", OR);
keywords.put("print", PRINT);
keywords.put("return", RETURN);
keywords.put("super", SUPER);
keywords.put("this", THIS);
keywords.put("true", TRUE);
keywords.put("var", VAR);
keywords.put("while", WHILE);
}
// Constructor
// Set our source string to be the incoming character data from the
// input script
Scanner(String source) {
this.source = source;
}
// Scan the input file for all available tokens, return a Token list with all
// of our valid tokens
List<Token> scanTokens() {
while (!isAtEnd()) {
start = current;
scanToken();
}
tokens.add(new Token(EOF, "", null, line));
return tokens;
}
// Check to see if we have reached the end of the script
private boolean isAtEnd() {
return current >= source.length();
}
// Parse the current token from the scanner to see if its a valid
// lexeme. Report an error otherwise
private void scanToken() {
char c = advance();
switch (c) {
// Structural and Accessors
case '(': addToken(LEFT_PAREN); break;
case ')': addToken(RIGHT_PAREN); break;
case '{': addToken(LEFT_BRACE); break;
case '}': addToken(RIGHT_BRACE); break;
case ',': addToken(COMMA); break;
case '.': addToken(DOT); break;
case ';': addToken(SEMICOLON); break;
// Operators
case '*': addToken(STAR); break;
case '-': addToken(MINUS); break;
case '+': addToken(PLUS); break;
case '!':
addToken(match('=') ? BANG_EQUAL : BANG);
break;
case '=':
addToken(match('=') ? EQUAL_EQUAL : EQUAL);
break;
case '<':
addToken(match('=') ? LESS_EQUAL : LESS);
break;
case '>':
addToken(match('=') ? GREATER_EQUAL : GREATER);
break;
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 ' ':
case '\r':
case '\t':
//ignore whitespace characters
break;
case '\n':
line++;
break;
case '"':
string();
break;
default:
if (isDigit(c)) {
// Check to see if our incoming value is part of a number
// Switch on base prefix (hex, binary, base10...)
switch (peek()) {
case 'x':
advance(); // Advance to disregard the '0x' prefix
hex();
break;
case 'b':
advance(); // Advance to disregard the '0b' prefix
binary();
break;
case 'o':
advance(); // Advance to disregard the '0o' prefix
octal();
break;
default:
number();
break;
}
} else if (isAlpha(c)) {
// Check to see if our incoming value is part of
// a reserved word or identifier
identifier();
} else {
Cobalt.error(line, "Unexpected character.");
}
break;
}
}
// Determine if the char is a base 10 digit
private boolean isDigit(char c) {
return c >= '0' && c <= '9';
}
// Determine if the character is a valid base 16 digit
private boolean isHex(char c) {
return (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
}
// Determine if the character is a valid base 16 digit
private boolean isBinary(char c) {
return (c >= '0' && c <= '1');
}
// Determine if the character is a valid base 8 digit
private boolean isOctal(char c) {
return (c >= '0' && c <= '7');
}
// Parse and tokenize the value as a base 10 number
private void number() {
while (isDigit(peek())) advance();
// Look for a decimal place.
if (peek() == '.' && isDigit(peekNext())) {
// Consume the .
advance();
while (isDigit(peek())) advance();
}
addToken(NUMBER, Double.parseDouble(source.substring(start, current)));
}
// Parse and tokenize the value as a hexadecimal number, store in base 10
private void hex() {
while (isDigit(peek()) || isHex(peek())) advance();
addToken(NUMBER, Long.parseLong(source.substring(start + 2, current), 16));
}
// Parse and tokenize the value as a binary number, store in base 10
private void binary() {
while (isBinary(peek())) advance();
addToken(NUMBER, Long.parseLong(source.substring(start + 2, current), 2));
}
// Parse and tokenize the value as an octal number, store in base 10
private void octal() {
while (isOctal(peek())) advance();
addToken(NUMBER, Long.parseLong(source.substring(start + 2, current), 8));
}
// TODO: Lox spec supports multiline strings, we'll need to
// probably remove that support since I don't intend Cobalt's
// grammar to support that (maybe) :/
// TODO: Escape sequences are not supported atm, for the
// love of god please implement this functionality. Probably
// should make an enum for the valid escape sequences, parse them
// out like we do with operators, and inject the actual escape
// sequence in the object thats returned to the interpreter
// Process the input line if quotation marks are found
// and we have a string literal
private void string() {
while (peek() != '"' && !isAtEnd()) {
if (peek() == '\n') line++;
advance();
}
if (isAtEnd()) {
Cobalt.error(line, "Unterminated string.");
return;
}
// Get closing quotes
advance();
// Trim the
String value = source.substring(start + 1, current - 1);
addToken(STRING, value);
}
// Determine if the infoming token is alphanumeric, and
// add it to the Token list if it is valid
private void identifier() {
while (isAlphaNumeric(peek())) advance();
String text = source.substring(start, current);
TokenType type = keywords.get(text);
if (type == null) type = IDENTIFIER;
addToken(type);
}
// Checkout the next character in our input, but dont consume it
// This is mainly to process things like comments that take an entire line
private char peek() {
if (isAtEnd()) return '\0';
return source.charAt(current);
}
// Checkout the next+1 character in our input, but dont consume it
// This is mainly to process things like comments that take an entire line
private char peekNext() {
if (current + 1 >= source.length()) return '\0';
return source.charAt(current + 1);
}
// Check to see if the character passed is within
// [a-z][A-Z]
private boolean isAlpha(char c) {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
c == '_';
}
// Check to see if the character passed is within
// [a-z][A-Z][0-9]
private boolean isAlphaNumeric(char c) {
return isAlpha(c) || isDigit(c);
}
// Return a boolean based on if a char is found at the current cursor,
// then increment
private boolean match(char expected) {
if (isAtEnd()) return false;
if (source.charAt(current) != expected) return false;
current++;
return true;
}
// Advance the char pointer in the line scanner
private char advance() {
return source.charAt(current++);
}
// Add a token to the token List that does not have an object literal
// associated with it.
private void addToken(TokenType type) {
addToken(type, null);
}
// Add a token to the token List that has an object associated with it
private void addToken(TokenType type, Object literal) {
String text = source.substring(start, current);
tokens.add(new Token(type, text, literal, line));
}
}

65
cobalt/lang/Stmt.java Normal file
View File

@ -0,0 +1,65 @@
package cobalt.lang;
import java.util.List;
abstract class Stmt {
interface Visitor<R> {
R visitBlockStmt(Block stmt);
R visitExpressionStmt(Expression stmt);
R visitPrintStmt(Print stmt);
R visitVarStmt(Var stmt);
}
static class Block extends Stmt {
Block(List<Stmt> statements) {
this.statements = statements;
}
<R> R accept(Visitor<R> visitor) {
return visitor.visitBlockStmt(this);
}
final List<Stmt> statements;
}
static class Expression extends Stmt {
Expression(Expr expression) {
this.expression = expression;
}
<R> R accept(Visitor<R> visitor) {
return visitor.visitExpressionStmt(this);
}
final Expr expression;
}
static class Print extends Stmt {
Print(Expr expression) {
this.expression = expression;
}
<R> R accept(Visitor<R> visitor) {
return visitor.visitPrintStmt(this);
}
final Expr expression;
}
static class Var extends Stmt {
Var(Token name, boolean nullable, Expr initializer) {
this.name = name;
this.nullable = nullable;
this.initializer = initializer;
}
<R> R accept(Visitor<R> visitor) {
return visitor.visitVarStmt(this);
}
final Token name;
final boolean nullable;
final Expr initializer;
}
abstract <R> R accept(Visitor<R> visitor);
}

19
cobalt/lang/Token.java Normal file
View File

@ -0,0 +1,19 @@
package cobalt.lang;
class Token {
final TokenType type;
final String lexeme;
final Object literal;
final int line;
Token(TokenType type, String lexeme, Object literal, int line) {
this.type = type;
this.lexeme = lexeme;
this.literal = literal;
this.line = line;
}
public String toString() {
return type + " " + lexeme + " " + literal;
}
}

View File

@ -0,0 +1,20 @@
package cobalt.lang;
enum TokenType {
// Single character tokens.
LEFT_PAREN, RIGHT_PAREN, LEFT_BRACE, RIGHT_BRACE,
COMMA, DOT, MINUS, PLUS, SEMICOLON, SLASH, STAR,
// One or two character tokens.
BANG, BANG_EQUAL, EQUAL, EQUAL_EQUAL,
GREATER, GREATER_EQUAL, LESS, LESS_EQUAL, NULLABLE,
// Literals
IDENTIFIER, STRING, NUMBER,
// Keywords
AND, CLASS, ELSE, FALSE, FUNC, FOR, IF, NIL, OR,
PRINT, RETURN, SUPER, THIS, TRUE, VAR, WHILE,
EOF
}

View File

@ -0,0 +1,101 @@
package cobalt.tool;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;
public class GenerateAst {
public static void main(String[] args) throws IOException {
if (args.length != 1) {
System.err.println("Usage: generate_ast <output directory>");
System.exit(1);
}
String outputDir = args[0];
defineAst(outputDir, "Expr", Arrays.asList(
"Assign : Token name, Expr value",
"Binary : Expr left, Token operator, Expr right",
"Grouping : Expr expression",
"Literal : Object value",
"Variable : Token Name",
"Unary : Token operator, Expr right",
"Variable : Token name"
));
defineAst(outputDir, "Stmt", Arrays.asList(
"Block : List<Stmt> statements",
"Expression : Expr expression",
"Print : Expr expression",
"Var : Token name, Expr initializer"
));
}
private static void defineAst(String outputDir, String baseName, List<String> types) throws IOException {
String path = outputDir + "/" + baseName + ".java";
PrintWriter writer = new PrintWriter(path, "UTF-8");
writer.println("package com.craftinginterpreters.lox;");
writer.println("");
writer.println("import java.util.List;");
writer.println("");
writer.println("abstract class " + baseName + " {");
defineVisitor(writer, baseName, types);
// The AST classes.
for (String type : types) {
String className = type.split(":")[0].trim();
String fields = type.split(":")[1].trim();
defineType(writer, baseName, className, fields);
}
// The base accept() method.
writer.println("");
writer.println(" abstract <R> R accept(Visitor<R> visitor);");
writer.println("}");
writer.close();
}
private static void defineVisitor(PrintWriter writer, String baseName, List<String> types) {
writer.println(" interface Visitor<R> {");
for (String type : types) {
String typeName = type.split(":")[0].trim();
writer.println(" R visit" + typeName + baseName + "(" + typeName + " " + baseName.toLowerCase() + ");");
}
writer.println(" }");
}
private static void defineType(PrintWriter writer, String baseName, String className, String fieldList) {
writer.println(" static class " + className + " extends " + baseName + " {");
// Constructor.
writer.println(" " + className + "(" + fieldList + ") {");
// Store parameters in fields.
String[] fields = fieldList.split(", ");
for (String field : fields) {
String name = field.split(" ")[1];
writer.println(" this." + name + " = " + name + ";");
}
writer.println(" }");
// Visitor pattern.
writer.println();
writer.println(" <R> R accept(Visitor<R> visitor) {");
writer.println(" return visitor.visit" + className + baseName + "(this);");
writer.println(" }");
// Fields.
writer.println();
for (String field : fields) {
writer.println(" final " + field + ";");
}
writer.println(" }");
}
}

View File

@ -1,3 +0,0 @@
#!/bin/bash
g++ parser.cpp lexer.cpp main.cpp -o main

25
examples/test.cobalt Normal file
View File

@ -0,0 +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;
# 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);
}

View File

@ -1,14 +0,0 @@
#include <iostream>
#ifndef LEXER_H
#define LEXER_H
class Lexer
{
private:
std::string extension = ".prog";
public:
Lexer(std::string &file_name);
};
#endif

View File

@ -1,14 +0,0 @@
#include <iostream>
#ifndef PARSER_H
#define PARSER_H
class Parser
{
private:
public:
Parser(std::string input_file);
};
#endif

View File

75
lang_spec.txt Normal file
View File

@ -0,0 +1,75 @@
using cobalt.std;
using cobalt.math;
// Comments
/*
Block Comments
*/
public class MyProgram {
// *** Instance Variables ***
// (Option 1)
// Since mutable variables are frequently used, you should have to specify if the variable is intended
// to be immutable/constant
let x: int = 0; // Mutable type
let y: const int = 0; // Immutable type
// (Option 2)
// Assume all variables are immutable, and only allow them to be mutable if specified. Helpful in compiled, memory
// safe languages, but probably not for an interpreted language that sits in memory?
let mut x: int = 0; // Mutable type
let y: int = 0; // Immutable type
// *** Main and Declaring functions ***
// I think scripts should work similar to python, but not be as
// funky/verbose with function names
// If there is no main function defined within the script, the interpreter should
// process the file sequentially like Python,
// Otherwise, the main function is ran and operates like any normal program
// Best practice would have main return an integer, however it could be
// void or return any other type
// (Option 1, C/C++/Java style)
private int main1() {
// Some code
return 0;
}
// (Option 2, Swift/Rust style)
private func main() => int {
// Some code
return 0;
}
// (Option 3, Ada style)
private func main() returns int {
// Some code
return 0;
}
// *** Handling Multiple Main Methods ***
// I feel that classes should be able to have their own main methods, and it could be
// determined which one is the entry point by requiring the user to provide an entry point
//
// (Example)
// -Multiple classes in one file, each with their own main method
// -Specify the script along with the class when opening with Cobalt, and it will run that specific class
// -Attempt to run the class's main method. Error if it doesn't exist
// -If there are multiple mains and one isn't specific, just error
//
// Ex: cobalt script.cblt --main MyClass
//
// This allows for multiple "sub programs" within a single Cobalt script
}

BIN
main

Binary file not shown.

View File

@ -1,20 +0,0 @@
#OBJS specifies which files to compile as part of the project
OBJS = $(wildcard src/*.cpp)
#CC specifies which compiler we're using
CC = g++
#COMPILER_FLAGS specifies the additional compilation options we're using
# -w suppresses all warnings
COMPILER_FLAGS = -w
#LINKER_FLAGS specifies the libraries we're linking against
LINKER_FLAGS =
#OBJ_NAME specifies the name of our exectuable
OBJ_NAME = cobalt
#This is the target that compiles our executable
all : $(OBJS)
$(CC) $(OBJS) $(COMPILER_FLAGS) $(LINKER_FLAGS) -o $(OBJ_NAME)

View File

@ -1,3 +0,0 @@
hello world, this is a lexer test
hello world
Kanye's new name is Ye

View File

@ -1,30 +0,0 @@
#include <stdio.h>
#include <iostream>
#include <fstream>
#include "../include/lexer.h"
char c;
FILE* input_file;
Lexer::Lexer(std::string &file_name) {
input_file = fopen(file_name.c_str(),"r");
if(input_file == nullptr) {
std::cout << "[Error] Script " << input_file << " could not be read\n";
exit(1);
}
c = getc(input_file);
if (c == EOF) {
//do something related to eof here?
fclose(input_file);
}
}
char nextChar() {
if (c != EOF) {
c = getc(input_file);
} else {
c = EOF;
}
return c;
}

View File

@ -1,15 +0,0 @@
#include <stdio.h>
#include <iostream>
#include <vector>
#include "../include/lexer.h"
int main(int argc, char *argv[]) {
if (argc > 1) {
std::string file_name(argv[1]);
Lexer lex(file_name);
} else {
std::cout << "[Error] No input file provided\n";
}
return 0;
}

View File

@ -1,8 +0,0 @@
#include <stdio.h>
#include <iostream>
#include "../include/parser.h"
Parser::Parser(std::string input_file) {
}

View File