Init Froj

This commit is contained in:
Jesper Saastamoinen 2024-09-17 11:15:57 +02:00
commit aa5564be1d
25 changed files with 2397 additions and 0 deletions

33
.gitignore vendored Normal file
View file

@ -0,0 +1,33 @@
### IntelliJ IDEA ###
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
.class
/src/*.jar
/.idea

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 jsaasta
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

54
README.md Normal file
View file

@ -0,0 +1,54 @@
# Frensta Open Java
## What is Froj?
Froj (Frensta Open Java) is a lightweight interpreter written in Java that makes use of the JVM.
### Frensta?
Where im currently living, in the countryside in a small town called 'Fränsta' in Sweden.
### Why Froj?
I wrote this interpreter as a way to learn and get some deeper understanding how interpreters is made.
### TODO:
Implement some standard functions and method such as:
* Read user input
* Block style comments `/* */`
* Lists
* And more!
# Pre-requisites
Java Runtime (1.8 or newer).
# Hello World
## The simplest way to get started:
* [Download the .jar under releases](https://github.com/jsaasta/Froj/releases/tag/stable)
* Create file ``helloworld.froj`` with the contents below:
print "Hello World";
* Run ``java -jar froj.jar ./helloworld.froj``
## Build the .jar from source:
### Makefile
* cd to ``/src``
* run``make compile``
### javac
* cd to `/src`
* run `javac -d ./ ./com/saasta/froj/*.java`
* run `jar -cfm froj.jar ./META-INF/MANIFEST.MF ./com/saasta/froj/*.class`
* Optional: Remove the .class files after creating the .jar:
* Linux: `rm -f ./com/saasta/froj/*.class`
* Windows(CMD): `del ./com/saasta/froj/*.class`
* Windows(PowerShell): `Remove-Item ./com/saasta/froj/*.class`
# Thank you
Special thanks to [Robert Nystrom's book Crafting Interpreters](https://craftinginterpreters.com), as the book paved the way for Froj.

11
froj.iml Normal file
View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

1
hello_world.froj Normal file
View file

@ -0,0 +1 @@
print "Hello world!";

3
src/META-INF/MANIFEST.MF Normal file
View file

@ -0,0 +1,3 @@
Manifest-Version: 1.0
Main-Class: com.jsaasta.froj.Froj

24
src/Makefile Normal file
View file

@ -0,0 +1,24 @@
FILE_PATH = ./com/jsaasta/froj
FILE = ../hello_world.froj
FILE_NAME_JAR = froj.jar
all: run
compile:
rm -f $(FILE_PATH)/*.class
javac -d ./ $(FILE_PATH)/*.java
jar -cfm $(FILE_NAME_JAR) ./META-INF/MANIFEST.MF $(FILE_PATH)/*.class
rm -f $(FILE_PATH)/*.class
clean:
rm -f $(FILE_PATH)/*.class
rm -f ./$(FILE_NAME_JAR)
javac:
javac -d ./ $(FILE_PATH)/*.java
jar:
jar -cfm $(FILE_NAME_JAR) ./META-INF/MANIFEST.MF $(FILE_PATH)/*.class
run:
java -jar $(FILE_NAME_JAR) $(FILE)

View file

@ -0,0 +1,62 @@
package com.jsaasta.froj;
import java.util.HashMap;
import java.util.Map;
public class Environment {
final Environment enclosing;
private final Map<String, Object> values = new HashMap<>();
public Environment() {
enclosing = null;
}
public Environment(Environment enclosing) {
this.enclosing = enclosing;
}
public 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 variable '" + name.lexeme + "'.");
}
public void define(String name, Object value) {
values.put(name, value);
}
public Environment ancestor(int distance) {
Environment environment = this;
for (int i = 0; i < distance; i++) {
environment = environment.enclosing;
}
return environment;
}
public Object getAt(int distance, String name) {
return ancestor(distance).values.get(name);
}
public void assignAt(int distance, Token name, Object value) {
ancestor(distance).values.put(name.lexeme, value);
}
public 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 + "'.");
}
}

View file

@ -0,0 +1,213 @@
package com.jsaasta.froj;
import java.util.List;
abstract class Expr {
interface Visitor<R> {
R visitAssignExpr(Assign expr);
R visitBinaryExpr(Binary expr);
R visitCallExpr(Call expr);
R visitGetExpr(Get expr);
R visitGroupingExpr(Grouping expr);
R visitLiteralExpr(Literal expr);
R visitLogicalExpr(Logical expr);
R visitSetExpr(Set expr);
R visitSuperExpr(Super expr);
R visitThisExpr(This expr);
R visitUnaryExpr(Unary expr);
R visitVariableExpr(Variable expr);
}
static class Assign extends Expr {
Assign(Token name, Expr value) {
this.name = name;
this.value = value;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitAssignExpr(this);
}
final Token name;
final Expr value;
}
static class Binary extends Expr {
Binary(Expr left, Token operator, Expr right) {
this.left = left;
this.operator = operator;
this.right = right;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitBinaryExpr(this);
}
final Expr left;
final Token operator;
final Expr right;
}
static class Call extends Expr {
Call(Expr callee, Token paren, List<Expr> arguments) {
this.callee = callee;
this.paren = paren;
this.arguments = arguments;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitCallExpr(this);
}
final Expr callee;
final Token paren;
final List<Expr> arguments;
}
static class Get extends Expr {
Get(Expr object, Token name) {
this.object = object;
this.name = name;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitGetExpr(this);
}
final Expr object;
final Token name;
}
static class Grouping extends Expr {
Grouping(Expr expression) {
this.expression = expression;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitGroupingExpr(this);
}
final Expr expression;
}
static class Literal extends Expr {
Literal(Object value) {
this.value = value;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitLiteralExpr(this);
}
final Object value;
}
static class Logical extends Expr {
Logical(Expr left, Token operator, Expr right) {
this.left = left;
this.operator = operator;
this.right = right;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitLogicalExpr(this);
}
final Expr left;
final Token operator;
final Expr right;
}
static class Set extends Expr {
Set(Expr object, Token name, Expr value) {
this.object = object;
this.name = name;
this.value = value;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitSetExpr(this);
}
final Expr object;
final Token name;
final Expr value;
}
static class Super extends Expr {
Super(Token keyword, Token method) {
this.keyword = keyword;
this.method = method;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitSuperExpr(this);
}
final Token keyword;
final Token method;
}
static class This extends Expr {
This(Token keyword) {
this.keyword = keyword;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitThisExpr(this);
}
final Token keyword;
}
static class Unary extends Expr {
Unary(Token operator, Expr right) {
this.operator = operator;
this.right = right;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitUnaryExpr(this);
}
final Token operator;
final Expr right;
}
static class Variable extends Expr {
Variable(Token name) {
this.name = name;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitVariableExpr(this);
}
final Token name;
}
abstract <R> R accept(Visitor<R> visitor);
}

View file

@ -0,0 +1,101 @@
package com.jsaasta.froj;
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 Froj {
private static final Interpreter interpreter = new Interpreter();
static boolean hadError = false;
static boolean hadRuntimeError = false;
public static void main(String[] args) throws IOException {
Froj froj = new Froj();
if (args.length != 1) {
System.out.println("Usage: froj [script]");
System.exit(64);
}
froj.runFile(args[0]);
}
private void runFile(String path) throws IOException {
try {
byte[] bytes = Files.readAllBytes(Paths.get(path));
run(new String(bytes, Charset.defaultCharset()));
// Indicate an error in the exit code.
if (hadError) System.exit(65);
if (hadRuntimeError) System.exit(70);
} catch (IOException exception) {
System.err.println("File not found: " + path);
}
}
/*
* Old REPL implementation. Not tested functionality in a while.
*
private 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;
hadRuntimeError = false;
}
}
*/
private 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;
Resolver resolver = new Resolver(interpreter);
resolver.resolve(statements);
if (hadError) return;
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(error.getMessage() +
"\n[line " + error.token.line + "]");
hadRuntimeError = true;
}
}

View file

@ -0,0 +1,10 @@
package com.jsaasta.froj;
import java.util.List;
public interface FrojCallable {
int arity();
Object call(Interpreter interpreter, List<Object> arguments);
}

View file

@ -0,0 +1,52 @@
package com.jsaasta.froj;
import java.util.List;
import java.util.Map;
public class FrojClass implements FrojCallable {
final String name;
final FrojClass superclass;
private final Map<String, FrojFunction> methods;
public FrojClass(String name, FrojClass superclass, Map<String, FrojFunction> methods) {
this.name = name;
this.superclass = superclass;
this.methods = methods;
}
public FrojFunction findMethod(String name) {
if (methods.containsKey(name)) {
return methods.get(name);
}
if (superclass != null) {
return superclass.findMethod(name);
}
return null;
}
@Override
public String toString() {
return name;
}
@Override
public int arity() {
FrojFunction initializer = findMethod("init");
if (initializer == null) return 0;
return initializer.arity();
}
@Override
public Object call(Interpreter interpreter, List<Object> arguments) {
FrojInstance instance = new FrojInstance(this);
FrojFunction initializer = findMethod("init");
if (initializer != null) {
initializer.bind(instance).call(interpreter, arguments);
}
return instance;
}
}

View file

@ -0,0 +1,59 @@
package com.jsaasta.froj;
import java.util.List;
public class FrojFunction implements FrojCallable {
private final Stmt.Function declaration;
private final Environment closure;
private final boolean isInitializer;
public FrojFunction(Stmt.Function declaration, Environment closure, boolean isInitializer) {
this.declaration = declaration;
this.closure = closure;
this.isInitializer = isInitializer;
}
public FrojFunction bind(FrojInstance instance) {
Environment environment = new Environment(closure);
environment.define("this", instance);
return new FrojFunction(declaration, environment, false);
}
@Override
public int arity() {
return declaration.params.size();
}
@Override
public Object call(Interpreter interpreter, List<Object> arguments) {
// Create a new environment for every call to handle recursive
// function calls.
Environment environment = new Environment(closure);
for (int i = 0; i < declaration.params.size(); i++) {
environment.define(declaration.params.get(i).lexeme,
arguments.get(i));
}
try {
interpreter.executeBlock(declaration.body, environment);
} catch (Return returnValue) {
if (isInitializer) return closure.getAt(0, "this");
return returnValue.value;
}
//If were in an initializer and execute a return statement, instead of returning
//the value (will always be nul), we return 'this' instead.
if (isInitializer) return closure.getAt(0, "this");
return null;
}
/**
* For printing function names, e.g "print add;" prints "<fn add>"
*
* @return name of function
*/
@Override
public String toString() {
return "<fn " + declaration.name.lexeme + ">";
}
}

View file

@ -0,0 +1,36 @@
package com.jsaasta.froj;
import java.util.HashMap;
import java.util.Map;
public class FrojInstance {
private FrojClass klass;
private final Map<String, Object> fields = new HashMap<>();
public FrojInstance(FrojClass klass) {
this.klass = klass;
}
public Object get(Token name) {
if (fields.containsKey(name.lexeme)) {
return fields.get(name.lexeme);
}
FrojFunction method = klass.findMethod(name.lexeme);
if (method != null) return method.bind(this);
throw new RuntimeError(name,
"Undefined property '" + name.lexeme + "'.");
}
public void set(Token name, Object value) {
fields.put(name.lexeme, value);
}
@Override
public String toString() {
return klass.name + " instance";
}
}

View file

@ -0,0 +1,370 @@
package com.jsaasta.froj;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Interpreter implements Expr.Visitor<Object>, Stmt.Visitor<Void> {
final Environment globals = new Environment();
private final Map<Expr, Integer> locals = new HashMap<>();
private Environment environment = globals;
public Interpreter() {
globals.define("clock", new FrojCallable() {
@Override
public int arity() {
return 0;
}
@Override
public Object call(Interpreter interpreter, List<Object> arguments) {
return (double) System.currentTimeMillis() / 1000.0;
}
@Override
public String toString() {
return "<native fn>";
}
});
}
public void interpret(List<Stmt> statements) {
try {
for (Stmt statement : statements) {
execute(statement);
}
} catch (RuntimeError error) {
Froj.runtimeError(error);
}
}
public void resolve(Expr expr, int depth) {
locals.put(expr, depth);
}
@Override
public Void visitBlockStmt(Stmt.Block stmt) {
executeBlock(stmt.statements, new Environment(environment));
return null;
}
@Override
public Void visitClassStmt(Stmt.Class stmt) {
Object superclass = null;
if (stmt.superclass != null) {
superclass = evaluate(stmt.superclass);
if (!(superclass instanceof FrojClass)) {
throw new RuntimeError(stmt.superclass.name, "Superclass must be a class.");
}
}
environment.define(stmt.name.lexeme, null);
if (stmt.superclass != null) {
environment = new Environment(environment);
environment.define("super", superclass);
}
Map<String, FrojFunction> methods = new HashMap<>();
for (Stmt.Function method : stmt.methods) {
FrojFunction function = new FrojFunction(method, environment, false);
methods.put(method.name.lexeme, function);
}
FrojClass klass = new FrojClass(stmt.name.lexeme, (FrojClass) superclass, methods);
if (superclass != null) {
environment = environment.enclosing;
}
environment.assign(stmt.name, klass);
return null;
}
@Override
public Void visitExpressionStmt(Stmt.Expression stmt) {
evaluate(stmt.expression);
return null;
}
@Override
public Void visitFunctionStmt(Stmt.Function stmt) {
FrojFunction function = new FrojFunction(stmt, environment, false);
environment.define(stmt.name.lexeme, function);
return null;
}
@Override
public Void visitIfStmt(Stmt.If stmt) {
if (isTruthy(evaluate(stmt.condition))) {
execute(stmt.thenBranch);
} else if (stmt.elseBranch != null) {
execute(stmt.elseBranch);
}
return null;
}
@Override
public Void visitPrintStmt(Stmt.Print stmt) {
Object value = evaluate(stmt.expression);
System.out.println(stringify(value));
return null;
}
@Override
public Void visitReturnStmt(Stmt.Return stmt) {
Object value = null;
if (stmt.value != null) value = evaluate(stmt.value);
throw new Return(value);
}
@Override
public Object visitThisExpr(Expr.This expr) {
return lookUpVariable(expr.keyword, expr);
}
@Override
public Void visitVarStmt(Stmt.Var stmt) {
Object value = null;
if (stmt.initializer != null) {
value = evaluate(stmt.initializer);
}
environment.define(stmt.name.lexeme, value);
return null;
}
@Override
public Void visitWhileStmt(Stmt.While stmt) {
while (isTruthy(evaluate(stmt.condition))) {
execute(stmt.body);
}
return null;
}
@Override
public Object visitAssignExpr(Expr.Assign expr) {
Object value = evaluate(expr.value);
Integer distance = locals.get(expr);
if (distance != null) {
environment.assignAt(distance, expr.name, value);
} else {
globals.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 GREATER_EQUAL:
checkNumberOperands(expr.operator, left, right);
return (double) left >= (double) right;
case LESS:
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:
return !isEqual(left, right);
case EQUAL_EQUAL:
return isEqual(left, right);
case MINUS:
checkNumberOperands(expr.operator, left, right);
return (double) left - (double) right;
case SLASH:
checkNumberOperands(expr.operator, left, right);
return (double) left / (double) right;
case STAR:
checkNumberOperands(expr.operator, left, right);
return (double) left * (double) right;
case PLUS: {
if (left instanceof Double && right instanceof Double) {
return (double) left + (double) right;
}
if (left instanceof String && right instanceof String) {
return (String) left + (String) right;
}
throw new RuntimeError(expr.operator, "Operands must be two numbers or two strings.");
}
}
return null;
}
@Override
public Object visitCallExpr(Expr.Call expr) {
Object callee = evaluate(expr.callee);
List<Object> arguments = new ArrayList<>();
for (Expr argument : expr.arguments) {
arguments.add(evaluate(argument));
}
if (!(callee instanceof FrojCallable)) {
throw new RuntimeError(expr.paren, "Can only call functions and classes.");
}
FrojCallable function = (FrojCallable) callee;
//Check if the amount of arguments is correct
if (arguments.size() != function.arity()) {
throw new RuntimeError(expr.paren,
"Expected " + function.arity() +
" arguments but got " + arguments.size() + ".");
}
return function.call(this, arguments);
}
@Override
public Object visitGetExpr(Expr.Get expr) {
Object object = evaluate(expr.object);
if (object instanceof FrojInstance) {
return ((FrojInstance) object).get(expr.name);
}
throw new RuntimeError(expr.name, "Only instances have properties.");
}
@Override
public Object visitGroupingExpr(Expr.Grouping expr) {
return evaluate(expr.expression);
}
@Override
public Object visitLiteralExpr(Expr.Literal expr) {
return expr.value;
}
@Override
public Object visitLogicalExpr(Expr.Logical expr) {
Object left = evaluate(expr.left);
if (expr.operator.type == TokenType.OR) {
if (isTruthy(left)) return left;
} else {
if (!isTruthy(left)) return left;
}
return evaluate(expr.right);
}
@Override
public Object visitSetExpr(Expr.Set expr) {
Object object = evaluate(expr.object);
if (!(object instanceof FrojInstance)) {
throw new RuntimeError(expr.name, "Only instances have fields.");
}
Object value = evaluate(expr.value);
((FrojInstance) object).set(expr.name, value);
return value;
}
@Override
public Object visitSuperExpr(Expr.Super expr) {
int distance = locals.get(expr);
FrojClass superclass = (FrojClass) environment.getAt(distance, "super");
FrojInstance object = (FrojInstance) environment.getAt(distance - 1, "this");
FrojFunction method = superclass.findMethod(expr.method.lexeme);
if (method == null) {
throw new RuntimeError(expr.method,
"Undefined property '" + expr.method.lexeme + "'.");
}
return method.bind(object);
}
@Override
public Object visitUnaryExpr(Expr.Unary expr) {
Object right = evaluate(expr.right);
switch (expr.operator.type) {
case BANG:
return !isTruthy(right);
case MINUS:
checkNumberOperands(expr.operator, right);
return -(double) right;
}
return null;
}
@Override
public Object visitVariableExpr(Expr.Variable expr) {
return lookUpVariable(expr.name, expr);
}
private Object lookUpVariable(Token name, Expr expr) {
Integer distance = locals.get(expr);
if (distance != null) {
return environment.getAt(distance, name.lexeme);
} else {
return globals.get(name);
}
}
private void checkNumberOperands(Token operator, Object operand) {
if (operand instanceof Double) 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;
throw new RuntimeError(operator, "Operands must be numbers.");
}
private Object evaluate(Expr expr) {
return expr.accept(this);
}
private void execute(Stmt stmt) {
stmt.accept(this);
}
public 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;
}
}
private boolean isTruthy(Object object) {
if (object == null) return false;
if (object instanceof Boolean) return (boolean) object;
return true;
}
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 "null";
if (object instanceof Double) {
String text = object.toString();
if (text.endsWith(".0")) {
text = text.substring(0, text.length() - 2);
}
return text;
}
return object.toString();
}
}

View file

@ -0,0 +1,410 @@
package com.jsaasta.froj;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static com.jsaasta.froj.TokenType.*;
public class Parser {
private static class ParseError extends RuntimeException {
}
private final List<Token> tokens;
private int current = 0;
Parser(List<Token> tokens) {
this.tokens = tokens;
}
public List<Stmt> parse() {
List<Stmt> statements = new ArrayList<>();
while (!isAtEnd()) {
statements.add(declaration());
}
return statements;
}
private Stmt statement() {
if (match(FOR)) return forStatement();
if (match(IF)) return ifStatement();
if (match(PRINT)) return printStatement();
if (match(RETURN)) return returnStatement();
if (match(WHILE)) return whileStatement();
if (match(LEFT_BRACE)) return new Stmt.Block(block());
return expressionStatement();
}
private Stmt forStatement() {
consume(LEFT_PAREN, "Expect '(' after 'for'.");
Stmt initializer;
if (match(SEMICOLON)) {
initializer = null;
} else if (match(VAR)) {
initializer = varDeclaration();
} else {
initializer = expressionStatement();
}
Expr condition = null;
if (!check(SEMICOLON)) {
condition = expression();
}
consume(SEMICOLON, "Expect ';' after loop condition.");
Expr increment = null;
if (!check(RIGHT_PAREN)) {
increment = expression();
}
consume(RIGHT_PAREN, "Expect ')' after for clauses.");
Stmt body = statement();
if (increment != null) {
body = new Stmt.Block(Arrays.asList(body, new Stmt.Expression(increment)));
}
if (condition == null) {
condition = new Expr.Literal(true);
}
body = new Stmt.While(condition, body);
if (initializer != null) {
body = new Stmt.Block(Arrays.asList(initializer, body));
}
return body;
}
private Stmt ifStatement() {
consume(LEFT_PAREN, "Expect '(' after 'if'.");
Expr condition = expression();
consume(RIGHT_PAREN, "Expect ')' after if condition.");
Stmt thenBranch = statement();
Stmt elseBranch = null;
if (match(ELSE)) {
elseBranch = statement();
}
return new Stmt.If(condition, thenBranch, elseBranch);
}
private Stmt printStatement() {
Expr value = expression();
consume(SEMICOLON, "Expect ';' after value.");
return new Stmt.Print(value);
}
private Stmt returnStatement() {
Token keyword = previous();
Expr value = null;
if (!check(SEMICOLON)) {
value = expression();
}
consume(SEMICOLON, "Expect ';' after return value.");
return new Stmt.Return(keyword, value);
}
private Stmt varDeclaration() {
Token name = consume(IDENTIFIER, "Expect variable name.");
Expr initializer = null;
if (match(EQUAL)) {
initializer = expression();
}
consume(SEMICOLON, "Expect ';' after variable declaration.");
return new Stmt.Var(name, initializer);
}
private Stmt whileStatement() {
consume(LEFT_PAREN, "Expect '(' after 'while'.");
Expr condition = expression();
consume(RIGHT_PAREN, "Expect ')' after condition.");
Stmt body = statement();
return new Stmt.While(condition, body);
}
private Stmt expressionStatement() {
Expr expr = expression();
consume((SEMICOLON), "Expect ';' after expression.");
return new Stmt.Expression(expr);
}
private Stmt.Function function(String kind) {
Token name = consume(IDENTIFIER, "Expect " + kind + " name.");
consume(LEFT_PAREN, "Expect '(' after " + kind + " name.");
List<Token> parameters = new ArrayList<>();
if (!check(RIGHT_PAREN)) {
do {
if (parameters.size() >= 255) {
error(peek(), "Can't have more than 255 parameters.");
}
parameters.add(
consume(IDENTIFIER, "Expect parameter name."));
} while (match(COMMA));
}
consume(RIGHT_PAREN, "Expect ')' after parameters.");
consume(LEFT_BRACE, "Expect '{' before " + kind + " body.");
List<Stmt> body = block();
return new Stmt.Function(name, parameters, body);
}
private List<Stmt> block() {
List<Stmt> statements = new ArrayList<>();
while (!check(RIGHT_BRACE) && !isAtEnd()) {
statements.add(declaration());
}
consume(RIGHT_BRACE, "Expect '}' after block.");
return statements;
}
private Expr assignment() {
Expr expr = or();
if (match(EQUAL)) {
Token equals = previous();
Expr value = assignment();
if (expr instanceof Expr.Variable) {
Token name = ((Expr.Variable) expr).name;
return new Expr.Assign(name, value);
} else if (expr instanceof Expr.Get) {
Expr.Get get = (Expr.Get) expr;
return new Expr.Set(get.object, get.name, value);
}
/* We report an error if the left-hand side isnt a
/ valid assignment target, but we dont throw it
/ because the parser isnt in a confused state
/ where we need to go into panic mode and
/ synchronize.
*/
//noinspection ThrowableNotThrown
error(equals, "Invalid assignment target.");
}
return expr;
}
private Expr or() {
Expr expr = and();
while (match(OR)) {
Token operator = previous();
Expr right = and();
expr = new Expr.Logical(expr, operator, right);
}
return expr;
}
private Expr and() {
Expr expr = equality();
while (match(AND)) {
Token operator = previous();
Expr right = equality();
expr = new Expr.Logical(expr, operator, right);
}
return expr;
}
private Expr expression() {
return assignment();
}
private Stmt declaration() {
try {
if (match(CLASS)) return classDeclaration();
if (match(FUNCTION)) return function("function");
if (match(VAR)) return varDeclaration();
return statement();
} catch (ParseError error) {
synchronize();
return null;
}
}
private Stmt classDeclaration() {
Token name = consume(IDENTIFIER, "Expect class name.");
Expr.Variable superclass = null;
if (match(LESS)) {
consume(IDENTIFIER, "Expect superclass name.");
superclass = new Expr.Variable(previous());
}
consume(LEFT_BRACE, "Expect '{' before class body.");
List<Stmt.Function> methods = new ArrayList<>();
while (!check(RIGHT_BRACE) && !isAtEnd()) {
methods.add(function("method"));
}
consume(RIGHT_BRACE, "Expect '}' after class body.");
return new Stmt.Class(name, superclass, methods);
}
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;
}
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;
}
private Expr unary() {
if (match(BANG, MINUS)) {
Token operator = previous();
Expr right = unary();
return new Expr.Unary(operator, right);
}
return call();
}
private Expr call() {
Expr expr = primary();
while (true) {
if (match(LEFT_PAREN)) {
expr = finishCall(expr);
} else if (match(DOT)) {
Token name = consume(IDENTIFIER, "Expect property name after '.'.");
expr = new Expr.Get(expr, name);
} else {
break;
}
}
return expr;
}
private Expr finishCall(Expr callee) {
List<Expr> arguments = new ArrayList<>();
if (!check(RIGHT_PAREN)) {
do {
if (arguments.size() >= 255) {
error(peek(), "Can't have more than 255 arguments.");
}
arguments.add(expression());
} while (match(COMMA));
}
Token paren = consume(RIGHT_PAREN,
"Expect ')' after arguments.");
return new Expr.Call(callee, paren, arguments);
}
private Expr primary() {
if (match(FALSE)) return new Expr.Literal(false);
if (match(TRUE)) return new Expr.Literal(true);
if (match(NULL)) return new Expr.Literal(null);
if (match(NUMBER, STRING)) {
return new Expr.Literal(previous().literal);
}
if (match(SUPER)) {
Token keyword = previous();
consume(DOT, "Expect '.' after 'super'.");
Token method = consume(IDENTIFIER, "Expect superclass method name.");
return new Expr.Super(keyword, method);
}
if (match(THIS)) return new Expr.This(previous());
if (match(IDENTIFIER)) {
return new Expr.Variable(previous());
}
if (match(LEFT_PAREN)) {
Expr expr = expression();
consume(RIGHT_PAREN, "Expect ')' after expression.");
return new Expr.Grouping(expr);
}
throw error(peek(), "Expect expression");
}
private boolean match(TokenType... types) {
for (TokenType type : types) {
if (check(type)) {
advance();
return true;
}
}
return false;
}
private Token consume(TokenType type, String message) {
if (check(type)) return advance();
throw error(peek(), message);
}
private boolean check(TokenType type) {
if (isAtEnd()) return false;
return peek().type == type;
}
private Token advance() {
if (!isAtEnd()) current++;
return previous();
}
private boolean isAtEnd() {
return peek().type == EOF;
}
private Token peek() {
return tokens.get(current);
}
private Token previous() {
return tokens.get(current - 1);
}
private ParseError error(Token token, String message) {
Froj.error(token, message);
return new ParseError();
}
private void synchronize() {
advance();
while (!isAtEnd()) {
if (previous().type == SEMICOLON) return;
switch (peek().type) {
case CLASS:
case FUNCTION:
case VAR:
case FOR:
case IF:
case WHILE:
case PRINT:
case RETURN:
return;
}
advance();
}
}
}

View file

@ -0,0 +1,308 @@
package com.jsaasta.froj;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
public class Resolver implements Expr.Visitor<Void>, Stmt.Visitor<Void> {
private final Interpreter interpreter;
private final Stack<Map<String, Boolean>> scopes = new Stack<>();
private FunctionType currentFuntion = FunctionType.NONE;
private ClassType currentClass = ClassType.NONE;
private enum FunctionType {
NONE,
FUNCTION,
INITIALIZER,
METHOD,
}
private enum ClassType {
NONE,
CLASS,
SUBCLASS
}
public Resolver(Interpreter interpreter) {
this.interpreter = interpreter;
}
public void resolve(List<Stmt> statements) {
for (Stmt statement : statements) {
resolve(statement);
}
}
private void resolve(Stmt stmt) {
stmt.accept(this);
}
private void resolve(Expr expr) {
expr.accept(this);
}
private void resolveFunction(Stmt.Function function, FunctionType type) {
FunctionType enclosingFunction = currentFuntion;
currentFuntion = type;
beginScope();
for (Token param : function.params) {
declare(param);
define(param);
}
resolve(function.body);
endScope();
currentFuntion = enclosingFunction;
}
/**
* If variable is found, resolve it and pass in the number of scopes
* between current innermost scope and where the variable was found.
*/
private void resolveLocal(Expr expr, Token name) {
for (int i = scopes.size() - 1; i >= 0; i--) {
if (scopes.get(i).containsKey(name.lexeme)) {
interpreter.resolve(expr, scopes.size() - 1 - i);
return;
}
}
}
private void beginScope() {
scopes.push(new HashMap<String, Boolean>());
}
private void endScope() {
scopes.pop();
}
private void declare(Token name) {
if (scopes.isEmpty()) return;
Map<String, Boolean> scope = scopes.peek();
if (scope.containsKey(name.lexeme)) {
Froj.error(name, "Already a variable with this name in this scope");
}
scope.put(name.lexeme, false);
}
private void define(Token name) {
if (scopes.isEmpty()) return;
scopes.peek().put(name.lexeme, true);
}
@Override
public Void visitAssignExpr(Expr.Assign expr) {
resolve(expr.value);
resolveLocal(expr, expr.name);
return null;
}
@Override
public Void visitBinaryExpr(Expr.Binary expr) {
resolve(expr.left);
resolve(expr.right);
return null;
}
@Override
public Void visitCallExpr(Expr.Call expr) {
resolve(expr.callee);
for (Expr argument : expr.arguments) {
resolve(argument);
}
return null;
}
@Override
public Void visitGetExpr(Expr.Get expr) {
resolve(expr.object);
return null;
}
@Override
public Void visitGroupingExpr(Expr.Grouping expr) {
resolve(expr.expression);
return null;
}
@Override
public Void visitLiteralExpr(Expr.Literal expr) {
return null;
}
@Override
public Void visitLogicalExpr(Expr.Logical expr) {
resolve(expr.left);
resolve(expr.right);
return null;
}
@Override
public Void visitSetExpr(Expr.Set expr) {
resolve(expr.value);
resolve(expr.object);
return null;
}
@Override
public Void visitUnaryExpr(Expr.Unary expr) {
resolve(expr.right);
return null;
}
/**
* Check to see if the variable is being accessed inside its own initializer.
* If the variable exists in the current scope but its value is false , that means we have declared it but
* not yet defined it.
*/
@Override
public Void visitVariableExpr(Expr.Variable expr) {
if (!scopes.isEmpty() &&
scopes.peek().get(expr.name.lexeme) == Boolean.FALSE) {
Froj.error(expr.name,
"Can't read local variable in its own initializer.");
}
resolveLocal(expr, expr.name);
return null;
}
@Override
public Void visitBlockStmt(Stmt.Block stmt) {
beginScope();
resolve(stmt.statements);
endScope();
return null;
}
@Override
public Void visitClassStmt(Stmt.Class stmt) {
ClassType enclosingClass = currentClass;
currentClass = ClassType.CLASS;
declare(stmt.name);
define(stmt.name);
if (stmt.superclass != null && stmt.name.lexeme.equals(stmt.superclass.name.lexeme)) {
Froj.error(stmt.superclass.name, "A class can't inherit from itself.");
}
if (stmt.superclass != null) {
currentClass = ClassType.SUBCLASS;
resolve(stmt.superclass);
}
if (stmt.superclass != null) {
beginScope();
scopes.peek().put("super", true);
}
beginScope();
scopes.peek().put("this", true);
for (Stmt.Function method : stmt.methods) {
FunctionType declaration = FunctionType.METHOD;
if (method.name.lexeme.equals("init")) {
declaration = FunctionType.INITIALIZER;
}
resolveFunction(method, declaration);
}
endScope();
if (stmt.superclass != null) endScope();
currentClass = enclosingClass;
return null;
}
@Override
public Void visitExpressionStmt(Stmt.Expression stmt) {
resolve(stmt.expression);
return null;
}
@Override
public Void visitFunctionStmt(Stmt.Function stmt) {
declare(stmt.name);
define(stmt.name);
resolveFunction(stmt, FunctionType.FUNCTION);
return null;
}
@Override
public Void visitIfStmt(Stmt.If stmt) {
resolve(stmt.condition);
resolve(stmt.thenBranch);
if (stmt.elseBranch != null) resolve(stmt.elseBranch);
return null;
}
@Override
public Void visitPrintStmt(Stmt.Print stmt) {
resolve(stmt.expression);
return null;
}
@Override
public Void visitReturnStmt(Stmt.Return stmt) {
if (currentFuntion == FunctionType.NONE) {
Froj.error(stmt.keyword, "Can't return from top-level code");
}
if (stmt.value != null) {
if (currentFuntion == FunctionType.INITIALIZER) {
Froj.error(stmt.keyword, "Can't return a value from an initializer.");
}
resolve(stmt.value);
}
return null;
}
@Override
public Void visitSuperExpr(Expr.Super expr) {
if (currentClass == ClassType.NONE) {
Froj.error(expr.keyword, "Can't use 'super outside of a class.");
} else if (currentClass != ClassType.SUBCLASS) {
Froj.error(expr.keyword, "Can't use 'super' in a class with no superclass");
}
resolveLocal(expr, expr.keyword);
return null;
}
@Override
public Void visitThisExpr(Expr.This expr) {
if (currentClass == ClassType.NONE) {
Froj.error(expr.keyword, "Can't use 'this' outside of a class.");
return null;
}
resolveLocal(expr, expr.keyword);
return null;
}
@Override
public Void visitVarStmt(Stmt.Var stmt) {
declare(stmt.name);
if (stmt.initializer != null) {
resolve(stmt.initializer);
}
define(stmt.name);
return null;
}
@Override
public Void visitWhileStmt(Stmt.While stmt) {
resolve(stmt.condition);
resolve(stmt.body);
return null;
}
}

View file

@ -0,0 +1,14 @@
package com.jsaasta.froj;
/**
* Used as a control flow class for "return" keyword.
* Throw "silent" exceptions to traverse and unwind the callstack all back to Function.call();.
*/
public class Return extends RuntimeException {
final Object value;
public Return(Object value) {
super(null, null, false, false);
this.value = value;
}
}

View file

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

View file

@ -0,0 +1,218 @@
package com.jsaasta.froj;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.jsaasta.froj.TokenType.*;
public class Scanner {
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("function", FUNCTION);
keywords.put("if", IF);
keywords.put("null", NULL);
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);
}
private final String source;
private final List<Token> tokens = new ArrayList<>();
private int start = 0;
private int current = 0;
private int line = 1;
Scanner(String source) {
this.source = source;
}
public List<Token> scanTokens() {
while (!isAtEnd()) {
// We are at the beginning of the next lexeme.
start = current;
scanToken();
}
tokens.add(new Token(EOF, "", null, line));
return tokens;
}
private void scanToken() {
char c = advance();
switch (c) {
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(MINUS);
break;
case '+':
addToken(PLUS);
break;
case ';':
addToken(SEMICOLON);
break;
case '*':
addToken(STAR);
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 '/':
if (match('/')) {
// A comment goes until the end of the line.
while (peek() != '\n' && !isAtEnd()) advance();
} else {
addToken(SLASH);
}
break;
case ' ':
case '\r':
case '\t':
// Ignore whitespace.
break;
case '\n':
line++;
break;
case '"':
string();
break;
default:
if (isDigit(c)) {
number();
} else if (isAlpha(c)) {
identifier();
} else {
Froj.error(line, "Unexpected character.");
}
break;
}
}
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);
}
private void string() {
while (peek() != '"' && !isAtEnd()) {
if (peek() == '\n') line++;
advance();
}
if (isAtEnd()) {
Froj.error(line, "Unterminated string.");
return;
}
// The closing '"'
advance();
// Trim the surrounding quotes.
String value = source.substring(start + 1, current - 1);
addToken(STRING, value);
}
private void number() {
while (isDigit(peek())) advance();
// Look for a fractional part.
if (peek() == '.' && isDigit(peekNext())) {
// Consume the "."
advance();
while (isDigit(peek())) advance();
}
addToken(NUMBER,
Double.parseDouble(source.substring(start, current)));
}
private boolean match(char expected) {
if (isAtEnd()) return false;
if (source.charAt(current) != expected) return false;
current++;
return true;
}
private char peek() {
if (isAtEnd()) return '\0';
return source.charAt(current);
}
private char peekNext() {
if (current + 1 >= source.length()) return '\0';
return source.charAt(current + 1);
}
private boolean isAlpha(char c) {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
c == '_';
}
private boolean isAlphaNumeric(char c) {
return isAlpha(c) || isDigit(c);
}
private boolean isDigit(char c) {
return c >= '0' && c <= '9';
}
private boolean isAtEnd() {
return current >= source.length();
}
private char advance() {
return source.charAt(current++);
}
private void addToken(TokenType type) {
addToken(type, null);
}
private void addToken(TokenType type, Object literal) {
String text = source.substring(start, current);
tokens.add(new Token(type, text, literal, line));
}
}

View file

@ -0,0 +1,162 @@
package com.jsaasta.froj;
import java.util.List;
abstract class Stmt {
interface Visitor<R> {
R visitBlockStmt(Block stmt);
R visitClassStmt(Class stmt);
R visitExpressionStmt(Expression stmt);
R visitFunctionStmt(Function stmt);
R visitIfStmt(If stmt);
R visitPrintStmt(Print stmt);
R visitReturnStmt(Return stmt);
R visitVarStmt(Var stmt);
R visitWhileStmt(While stmt);
}
static class Block extends Stmt {
Block(List<Stmt> statements) {
this.statements = statements;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitBlockStmt(this);
}
final List<Stmt> statements;
}
static class Class extends Stmt {
Class(Token name, Expr.Variable superclass, List<Stmt.Function> methods) {
this.name = name;
this.superclass = superclass;
this.methods = methods;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitClassStmt(this);
}
final Token name;
final Expr.Variable superclass;
final List<Stmt.Function> methods;
}
static class Expression extends Stmt {
Expression(Expr expression) {
this.expression = expression;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitExpressionStmt(this);
}
final Expr expression;
}
static class Function extends Stmt {
Function(Token name, List<Token> params, List<Stmt> body) {
this.name = name;
this.params = params;
this.body = body;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitFunctionStmt(this);
}
final Token name;
final List<Token> params;
final List<Stmt> body;
}
static class If extends Stmt {
If(Expr condition, Stmt thenBranch, Stmt elseBranch) {
this.condition = condition;
this.thenBranch = thenBranch;
this.elseBranch = elseBranch;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitIfStmt(this);
}
final Expr condition;
final Stmt thenBranch;
final Stmt elseBranch;
}
static class Print extends Stmt {
Print(Expr expression) {
this.expression = expression;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitPrintStmt(this);
}
final Expr expression;
}
static class Return extends Stmt {
Return(Token keyword, Expr value) {
this.keyword = keyword;
this.value = value;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitReturnStmt(this);
}
final Token keyword;
final Expr value;
}
static class Var extends Stmt {
Var(Token name, Expr initializer) {
this.name = name;
this.initializer = initializer;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitVarStmt(this);
}
final Token name;
final Expr initializer;
}
static class While extends Stmt {
While(Expr condition, Stmt body) {
this.condition = condition;
this.body = body;
}
@Override
<R> R accept(Visitor<R> visitor) {
return visitor.visitWhileStmt(this);
}
final Expr condition;
final Stmt body;
}
abstract <R> R accept(Visitor<R> visitor);
}

View file

@ -0,0 +1,19 @@
package com.jsaasta.froj;
public class Token {
public final TokenType type;
public final String lexeme;
public final Object literal;
final int line;
public 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,21 @@
package com.jsaasta.froj;
public 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,
// Literals.
IDENTIFIER, STRING, NUMBER,
// Keywords.
AND, CLASS, ELSE, FALSE, FUNCTION, FOR, IF, NULL, OR,
PRINT, RETURN, SUPER, THIS, TRUE, VAR, WHILE,
EOF
}

View file

@ -0,0 +1,98 @@
package com.jsaasta.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(64);
}
String outputDir = args[0];
defineAst(outputDir, "Expr", Arrays.asList(
"Assign : Token name, Expr value",
"Binary : Expr left, Token operator, Expr right",
"Call : Expr callee, Token paren, List<Expr> arguments",
"Get : Expr object, Token name",
"Grouping : Expr expression",
"Literal : Object value",
"Logical : Expr left, Token operator, Expr right",
"Set : Expr object, Token name, Expr value",
"Super : Token keyword, Token method",
"This : Token keyword",
"Unary: Token operator, Expr right",
"Variable : Token name"));
defineAst(outputDir, "Stmt", Arrays.asList(
"Block : List<Stmt> statements",
"Class : Token name, Expr.Variable superclass, List<Stmt.Function> methods",
"Expression : Expr expression",
"Function : Token name, List<Token> params, List<Stmt> body",
"If : Expr condition, Stmt thenBranch, Stmt elseBranch",
"Print : Expr expression",
"Return : Token keyword, Expr value",
"Var : Token name, Expr initializer",
"While : Expr condition, Stmt body"));
}
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.jsaasta.froj;");
writer.println();
writer.println("import java.util.List;");
writer.println();
writer.println("abstract class " + baseName + " {");
defineVisitor(writer, baseName, types);
for (String type : types) {
String className = type.split(":")[0].trim();
String fields = type.split(":")[1].trim();
defineType(writer, baseName, className, fields);
}
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("@Override");
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(" }");
}
}

87
wiki.md Normal file
View file

@ -0,0 +1,87 @@
# Examples
## if-statement
if(a < 0){
// ...
} else if(a > 0) {
// ...
} else {
// ...
}
## for-loop
for(var i = 0; i < 10; i = i + 1){
print i;
}
## while-loop
var a = 0;
while(a < 10){
a = a + 1;
}
## Functions & Callbacks
function add(a, b) {
return a + b;
}
function foo(bar){
var a = 1;
var b = 2;
return bar(a, b);
}
print add(4,5); // prints 9
print foo(add); // prints 3
# Classes and inheritance
class FooParent {
welcome(){
print "Hello from Parent";
}
}
class Foo < FooParent {
init(helloString){
this.helloString = helloString;
}
welcome(){
super.welcome();
print this.helloString;
}
}
var hello = "Hello World!";
var foo = Foo(hello);
foo.welcome();
---
class Person {
init(name){
this.name = name;
}
whoAmI() {
return "This persons name is: " + this.name;
}
}
var john = Person("John");
john.age = 30; //Initialize new variables outside of class
print john.whoAmI();
print john.age;