Compare commits
10 Commits
2d9d56be76
...
175309ac7d
| Author | SHA1 | Date | |
|---|---|---|---|
| 175309ac7d | |||
| a5ee63fffc | |||
| c8b7214e6f | |||
| 5cd5dda34d | |||
| da88ff4071 | |||
| bc84ec43ce | |||
| 535c557cb4 | |||
| 6d2f1ee907 | |||
| 449ce575a7 | |||
| 8c05ed5835 |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
*.class
|
||||||
|
|
||||||
|
|
@ -1,2 +1,6 @@
|
||||||
# CobaltScript
|
# Cobalt
|
||||||
A small programming language designed for general purpose scripting and for learning interpreter design
|
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 :)
|
||||||
|
|
|
||||||
54
cobalt/lang/AstPrinter.java
Normal file
54
cobalt/lang/AstPrinter.java
Normal 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
91
cobalt/lang/Cobalt.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
cobalt/lang/Environment.java
Normal file
54
cobalt/lang/Environment.java
Normal 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
102
cobalt/lang/Expr.java
Normal 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);
|
||||||
|
}
|
||||||
250
cobalt/lang/Interpreter.java
Normal file
250
cobalt/lang/Interpreter.java
Normal 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
315
cobalt/lang/Parser.java
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
10
cobalt/lang/RuntimeError.java
Normal file
10
cobalt/lang/RuntimeError.java
Normal 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
318
cobalt/lang/Scanner.java
Normal 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
65
cobalt/lang/Stmt.java
Normal 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
19
cobalt/lang/Token.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
cobalt/lang/TokenType.java
Normal file
20
cobalt/lang/TokenType.java
Normal 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
|
||||||
|
}
|
||||||
101
cobalt/tool/GenerateAst.java
Normal file
101
cobalt/tool/GenerateAst.java
Normal 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(" }");
|
||||||
|
}
|
||||||
|
}
|
||||||
25
examples/test.cobalt
Normal file
25
examples/test.cobalt
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#ifndef PARSER_H
|
|
||||||
#define PARSER_H
|
|
||||||
|
|
||||||
class Parser
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
|
|
||||||
public:
|
|
||||||
Parser(std::string input_file);
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
||||||
75
lang_spec.txt
Normal file
75
lang_spec.txt
Normal 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
|
||||||
|
}
|
||||||
20
makefile
20
makefile
|
|
@ -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)
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
hello world, this is a lexer test
|
|
||||||
hello world
|
|
||||||
Kanye's new name is Ye
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
15
src/main.cpp
15
src/main.cpp
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
#include <stdio.h>
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "../include/parser.h"
|
|
||||||
|
|
||||||
Parser::Parser(std::string input_file) {
|
|
||||||
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user