From c0f92795ac74c03815030765ee6645094146a407 Mon Sep 17 00:00:00 2001 From: Uko Kokņevičs Date: Fri, 2 Aug 2024 20:07:38 +0300 Subject: Initial commit --- src/main/java/lv/enes/orang/Builtins.java | 95 +++++++ src/main/java/lv/enes/orang/Codepoint.java | 24 ++ src/main/java/lv/enes/orang/Lexer.java | 168 ++++++++++++ src/main/java/lv/enes/orang/Main.java | 77 ++++++ src/main/java/lv/enes/orang/NonEmptyList.java | 23 ++ src/main/java/lv/enes/orang/OrangException.java | 11 + .../java/lv/enes/orang/OrangRuntimeException.java | 11 + src/main/java/lv/enes/orang/Parser.java | 291 +++++++++++++++++++++ src/main/java/lv/enes/orang/ParserException.java | 7 + src/main/java/lv/enes/orang/PeekableStream.java | 38 +++ src/main/java/lv/enes/orang/Scope.java | 54 ++++ src/main/java/lv/enes/orang/State.java | 12 + src/main/java/lv/enes/orang/Token.java | 15 ++ src/main/java/lv/enes/orang/TokenType.java | 91 +++++++ .../java/lv/enes/orang/ast/ArrayExpression.java | 21 ++ .../java/lv/enes/orang/ast/BinaryExpression.java | 53 ++++ .../java/lv/enes/orang/ast/BooleanLiteral.java | 12 + .../java/lv/enes/orang/ast/CallExpression.java | 12 + src/main/java/lv/enes/orang/ast/DefSpec.java | 6 + src/main/java/lv/enes/orang/ast/Definition.java | 15 ++ src/main/java/lv/enes/orang/ast/DoExpression.java | 19 ++ src/main/java/lv/enes/orang/ast/Expression.java | 14 + src/main/java/lv/enes/orang/ast/FnExpression.java | 14 + .../java/lv/enes/orang/ast/IfElseExpression.java | 22 ++ src/main/java/lv/enes/orang/ast/IntLiteral.java | 12 + .../java/lv/enes/orang/ast/LetInExpression.java | 27 ++ src/main/java/lv/enes/orang/ast/Program.java | 17 ++ src/main/java/lv/enes/orang/ast/Statement.java | 8 + src/main/java/lv/enes/orang/ast/StringLiteral.java | 13 + .../java/lv/enes/orang/ast/UnaryExpression.java | 23 ++ .../java/lv/enes/orang/ast/VariableExpression.java | 12 + src/main/java/lv/enes/orang/value/Array.java | 65 +++++ .../java/lv/enes/orang/value/BuiltinFunction.java | 31 +++ src/main/java/lv/enes/orang/value/Function.java | 28 ++ .../java/lv/enes/orang/value/OrangBoolean.java | 55 ++++ .../java/lv/enes/orang/value/OrangInteger.java | 70 +++++ src/main/java/lv/enes/orang/value/OrangString.java | 49 ++++ .../enes/orang/value/PartialBuiltinFunction.java | 32 +++ .../java/lv/enes/orang/value/PartialFunction.java | 37 +++ src/main/java/lv/enes/orang/value/Undefined.java | 21 ++ src/main/java/lv/enes/orang/value/Value.java | 77 ++++++ src/main/java/module-info.java | 9 + 42 files changed, 1691 insertions(+) create mode 100644 src/main/java/lv/enes/orang/Builtins.java create mode 100644 src/main/java/lv/enes/orang/Codepoint.java create mode 100644 src/main/java/lv/enes/orang/Lexer.java create mode 100644 src/main/java/lv/enes/orang/Main.java create mode 100644 src/main/java/lv/enes/orang/NonEmptyList.java create mode 100644 src/main/java/lv/enes/orang/OrangException.java create mode 100644 src/main/java/lv/enes/orang/OrangRuntimeException.java create mode 100644 src/main/java/lv/enes/orang/Parser.java create mode 100644 src/main/java/lv/enes/orang/ParserException.java create mode 100644 src/main/java/lv/enes/orang/PeekableStream.java create mode 100644 src/main/java/lv/enes/orang/Scope.java create mode 100644 src/main/java/lv/enes/orang/State.java create mode 100644 src/main/java/lv/enes/orang/Token.java create mode 100644 src/main/java/lv/enes/orang/TokenType.java create mode 100644 src/main/java/lv/enes/orang/ast/ArrayExpression.java create mode 100644 src/main/java/lv/enes/orang/ast/BinaryExpression.java create mode 100644 src/main/java/lv/enes/orang/ast/BooleanLiteral.java create mode 100644 src/main/java/lv/enes/orang/ast/CallExpression.java create mode 100644 src/main/java/lv/enes/orang/ast/DefSpec.java create mode 100644 src/main/java/lv/enes/orang/ast/Definition.java create mode 100644 src/main/java/lv/enes/orang/ast/DoExpression.java create mode 100644 src/main/java/lv/enes/orang/ast/Expression.java create mode 100644 src/main/java/lv/enes/orang/ast/FnExpression.java create mode 100644 src/main/java/lv/enes/orang/ast/IfElseExpression.java create mode 100644 src/main/java/lv/enes/orang/ast/IntLiteral.java create mode 100644 src/main/java/lv/enes/orang/ast/LetInExpression.java create mode 100644 src/main/java/lv/enes/orang/ast/Program.java create mode 100644 src/main/java/lv/enes/orang/ast/Statement.java create mode 100644 src/main/java/lv/enes/orang/ast/StringLiteral.java create mode 100644 src/main/java/lv/enes/orang/ast/UnaryExpression.java create mode 100644 src/main/java/lv/enes/orang/ast/VariableExpression.java create mode 100644 src/main/java/lv/enes/orang/value/Array.java create mode 100644 src/main/java/lv/enes/orang/value/BuiltinFunction.java create mode 100644 src/main/java/lv/enes/orang/value/Function.java create mode 100644 src/main/java/lv/enes/orang/value/OrangBoolean.java create mode 100644 src/main/java/lv/enes/orang/value/OrangInteger.java create mode 100644 src/main/java/lv/enes/orang/value/OrangString.java create mode 100644 src/main/java/lv/enes/orang/value/PartialBuiltinFunction.java create mode 100644 src/main/java/lv/enes/orang/value/PartialFunction.java create mode 100644 src/main/java/lv/enes/orang/value/Undefined.java create mode 100644 src/main/java/lv/enes/orang/value/Value.java create mode 100644 src/main/java/module-info.java (limited to 'src/main/java') diff --git a/src/main/java/lv/enes/orang/Builtins.java b/src/main/java/lv/enes/orang/Builtins.java new file mode 100644 index 0000000..0a76387 --- /dev/null +++ b/src/main/java/lv/enes/orang/Builtins.java @@ -0,0 +1,95 @@ +package lv.enes.orang; + +import lombok.extern.slf4j.Slf4j; +import lv.enes.orang.value.*; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import static lv.enes.orang.State.STDIN; +import static lv.enes.orang.State.STDOUT; + +@Slf4j +public final class Builtins { + public static final Map BUILTINS = Map.ofEntries( + Map.entry("__builtin_isRepl", new BuiltinFunction(1, Builtins::isRepl)), + Map.entry("__builtin_len", new BuiltinFunction(1, Builtins::len)), + Map.entry("__builtin_parseInt", new BuiltinFunction(1, Builtins::parseInt)), + Map.entry("__builtin_print", new BuiltinFunction(1, Builtins::print)), + Map.entry("__builtin_randInt", new BuiltinFunction(2, Builtins::randInt)), + Map.entry("__builtin_readLn", new BuiltinFunction(1, Builtins::readLn)) + ); + + private Builtins() {} + + private static Value isRepl(List args) { + return OrangBoolean.TRUE; + } + + private static Value len(List args) throws OrangRuntimeException { + assert args.size() == 1; + var arg = args.getFirst(); + + if (arg instanceof OrangString(var value)) { + return new OrangInteger(value.length()); + } else if (arg instanceof Array(var items)) { + return new OrangInteger(items.size()); + } else { + throw new OrangRuntimeException(STR."len \{arg.typeName()} is not implemented"); + } + } + + private static Value parseInt(List args) throws OrangRuntimeException { + assert args.size() == 1; + + var arg = args.getFirst(); + if (arg instanceof OrangInteger) { + log.warn("Attempted to parseInt an Integer!"); + return arg; + } else if (arg instanceof OrangString(var value)) { + try { + return new OrangInteger(Integer.parseInt(value.trim())); + } catch (NumberFormatException ex) { + throw new OrangRuntimeException(ex); + } + } else { + throw new OrangRuntimeException(STR."parseInt \{arg.typeName()} is not implemented"); + } + } + + private static Value print(List args) { + assert args.size() == 1; + STDOUT.print(args.getFirst().display()); + STDOUT.flush(); + return args.getFirst(); + } + + private static Value randInt(List args) throws OrangRuntimeException { + assert args.size() == 2; + + var minv = args.getFirst(); + var maxv = args.get(1); + if (minv instanceof OrangInteger(var min) && maxv instanceof OrangInteger(var max)) { + return new OrangInteger(new Random().nextInt(min, max)); + } else { + throw new OrangRuntimeException(STR."randInt \{minv.typeName()} \{maxv.typeName()} is not implemented"); + } + } + + private static Value readLn(List args) throws OrangRuntimeException { + assert args.size() == 1; + + var arg = args.getFirst(); + if (!(arg instanceof Array) || !((Array) arg).items().isEmpty()) { + log.warn("You should call readLn with an empty tuple like `readLn ()`"); + } + + try { + return new OrangString(STDIN.readLine()); + } catch (IOException e) { + throw new OrangRuntimeException(e); + } + } +} diff --git a/src/main/java/lv/enes/orang/Codepoint.java b/src/main/java/lv/enes/orang/Codepoint.java new file mode 100644 index 0000000..0dfaa2b --- /dev/null +++ b/src/main/java/lv/enes/orang/Codepoint.java @@ -0,0 +1,24 @@ +package lv.enes.orang; + +public record Codepoint(int cp) { + @Override + public String toString() { + return Character.toString(cp); + } + + public boolean isIdentInitial() { + return Character.isLetter(cp) || cp == '_'; + } + + public boolean isIdentFinal() { + return isIdentInitial() || Character.isDigit(cp); + } + + public boolean isNumeral() { + return Character.isDigit(cp); + } + + public boolean isWhitespace() { + return Character.isWhitespace(cp); + } +} diff --git a/src/main/java/lv/enes/orang/Lexer.java b/src/main/java/lv/enes/orang/Lexer.java new file mode 100644 index 0000000..0ee2503 --- /dev/null +++ b/src/main/java/lv/enes/orang/Lexer.java @@ -0,0 +1,168 @@ +package lv.enes.orang; + +import java.io.*; +import java.util.Iterator; +import java.util.function.BiFunction; +import java.util.function.Predicate; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public class Lexer implements Iterator { + private final PeekableStream input; + + public Lexer(InputStream input) { + this(new InputStreamReader(input)); + } + + public Lexer(Reader input) { + var cpStream = new BufferedReader(input) + .lines() + .flatMapToInt(str -> IntStream.concat(str.codePoints(), IntStream.of('\n'))) + .mapToObj(Codepoint::new); + var theEof = Stream.of(new Codepoint(-1)); + this.input = new PeekableStream<>(Stream.concat(cpStream, theEof).iterator()); + } + + public Lexer(String input) { + this(new StringReader(input)); + } + + private boolean hasNext = true; + + @Override + public Token next() { + var tok = nextToken(); + if (tok.type() == TokenType.EOF) { + hasNext = false; + } + return tok; + } + + @Override + public boolean hasNext() { + return hasNext; + } + + private Token nextToken() { + skipWhitespace(); + return switch (input.peek().cp()) { + case -1 -> new Token(TokenType.EOF, ""); + + case '*' -> new Token(TokenType.ASTERISK, input.next()); + case '!' -> new Token(TokenType.BANG, input.next()); + case '[' -> new Token(TokenType.BRACKET_LEFT, input.next()); + case ']' -> new Token(TokenType.BRACKET_RIGHT, input.next()); + case ',' -> new Token(TokenType.COMMA, input.next()); + case '=' -> new Token(TokenType.EQUAL, input.next()); + case '>' -> { + var first = input.next(); + if (input.peek().cp() == '=') { + yield new Token(TokenType.GREATER_EQUAL, first, input.next()); + } else { + yield new Token(TokenType.GREATER, first); + } + } + case '<' -> { + var first = input.next(); + if (input.peek().cp() == '=') { + yield new Token(TokenType.LESS_EQUAL, first, input.next()); + } else { + yield new Token(TokenType.LESS, first); + } + } + case '-' -> { + var first = input.next(); + if (input.peek().cp() == '>') { + yield new Token(TokenType.MINUS_GREATER, first, input.next()); + } else { + yield new Token(TokenType.MINUS, first); + } + } + case '(' -> new Token(TokenType.PAREN_LEFT, input.next()); + case ')' -> new Token(TokenType.PAREN_RIGHT, input.next()); + case '+' -> new Token(TokenType.PLUS, input.next()); + case '?' -> { + var first = input.next(); + if (input.peek().cp() == '=') { + yield new Token(TokenType.QUESTION_EQUAL, first, input.next()); + } else { + yield new Token(TokenType.ILLEGAL, first, input.next()); + } + } + case ';' -> new Token(TokenType.SEMICOLON, input.next()); + case '/' -> { + var first = input.next(); + if (input.peek().cp() == '=') { + yield new Token(TokenType.SLASH_EQUAL, first, input.next()); + } else { + yield new Token(TokenType.SLASH, first); + } + } + + case '"' -> new Token(TokenType.STRING, readString()); + + default -> { + if (input.peek().isIdentInitial()) { + var ident = readIdentifier(); + var type = switch (ident) { + case "and" -> TokenType.AND; + case "def" -> TokenType.DEF; + case "do" -> TokenType.DO; + case "else" -> TokenType.ELSE; + case "end" -> TokenType.END; + case "false" -> TokenType.FALSE; + case "fn" -> TokenType.FN; + case "if" -> TokenType.IF; + case "in" -> TokenType.IN; + case "let" -> TokenType.LET; + case "then" -> TokenType.THEN; + case "true" -> TokenType.TRUE; + default -> TokenType.IDENTIFIER; + }; + yield new Token(type, ident); + } else if (input.peek().isNumeral()) { + yield new Token(TokenType.INTEGER, readInteger()); + } else { + yield new Token(TokenType.ILLEGAL, input.next()); + } + } + }; + } + + private T foldWhile(Predicate pred, T initial, BiFunction combine) { + var res = initial; + var ch = input.peek(); + while (pred.test(ch)) { + res = combine.apply(res, input.next()); + ch = input.peek(); + } + return res; + } + + private String readWhile(Predicate pred) { + return foldWhile(pred, new StringBuilder(), StringBuilder::append).toString(); + } + + private void skipWhile(Predicate pred) { + foldWhile(pred, Object.class, (x, _) -> x); + } + + private String readIdentifier() { + return readWhile(Codepoint::isIdentFinal); + } + + private String readInteger() { + return readWhile(Codepoint::isNumeral); + } + + private String readString() { + input.next(); + var literal = readWhile(cp -> cp.cp() != '"'); + input.next(); + return literal; + } + + private void skipWhitespace() { + skipWhile(Codepoint::isWhitespace); + } +} diff --git a/src/main/java/lv/enes/orang/Main.java b/src/main/java/lv/enes/orang/Main.java new file mode 100644 index 0000000..9a607ed --- /dev/null +++ b/src/main/java/lv/enes/orang/Main.java @@ -0,0 +1,77 @@ +package lv.enes.orang; + +import java.io.FileReader; +import java.io.IOException; + +import static lv.enes.orang.State.STDIN; +import static lv.enes.orang.State.STDOUT; + +public class Main { + public static final String PROMPT = ">> "; + + public static void main() throws IOException { + repl(); + } + + private static void repl() throws IOException { + var scope = new Scope(); + + try (var stream = Main.class.getResourceAsStream("prelude.orang")) { + var prog = Parser.parseProgram(stream); + scope = prog.runStatement(scope); + } catch (OrangException ex) { + STDOUT.println(STR."While evaluating prelude: \{ex}"); + throw new RuntimeException(ex); + } + + boolean running = true; + while (running) { + STDOUT.print(PROMPT); + STDOUT.flush(); + + var line = STDIN.readLine(); + if (line == null) { + return; + } + + if (line.isEmpty()) { + continue; + } + + if (line.charAt(0) == ':') { + if (line.length() == 1) { + continue; + } + + switch (line.charAt(1)) { + case 'l': + var filename = line.substring(2).trim(); + try (var reader = new FileReader((filename))) { + var prog = Parser.parseProgram(reader); + scope = prog.runStatement(scope); + } catch (IOException | OrangException ex) { + STDOUT.println(ex); + } + break; + case 'q': + running = false; + break; + default: + STDOUT.println("Unrecognised REPL command"); + } + continue; + } + + try { + var prog = Parser.parseProgram(line); + scope = prog.runStatement(scope); + if (scope.getLastResult() != null) { + STDOUT.print("-> "); + STDOUT.println(scope.getLastResult().stringify()); + } + } catch (OrangException ex) { + STDOUT.println(ex); + } + } + } +} diff --git a/src/main/java/lv/enes/orang/NonEmptyList.java b/src/main/java/lv/enes/orang/NonEmptyList.java new file mode 100644 index 0000000..b532a18 --- /dev/null +++ b/src/main/java/lv/enes/orang/NonEmptyList.java @@ -0,0 +1,23 @@ +package lv.enes.orang; + +import java.util.AbstractList; +import java.util.List; + +public class NonEmptyList extends AbstractList { + private final List delegate; + + public NonEmptyList(List delegate) { + assert !delegate.isEmpty(); + this.delegate = List.copyOf(delegate); + } + + @Override + public E get(int index) { + return delegate.get(index); + } + + @Override + public int size() { + return delegate.size(); + } +} diff --git a/src/main/java/lv/enes/orang/OrangException.java b/src/main/java/lv/enes/orang/OrangException.java new file mode 100644 index 0000000..9308643 --- /dev/null +++ b/src/main/java/lv/enes/orang/OrangException.java @@ -0,0 +1,11 @@ +package lv.enes.orang; + +public abstract class OrangException extends Exception { + protected OrangException(String message) { + super(message); + } + + protected OrangException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/lv/enes/orang/OrangRuntimeException.java b/src/main/java/lv/enes/orang/OrangRuntimeException.java new file mode 100644 index 0000000..9648c30 --- /dev/null +++ b/src/main/java/lv/enes/orang/OrangRuntimeException.java @@ -0,0 +1,11 @@ +package lv.enes.orang; + +public class OrangRuntimeException extends OrangException { + public OrangRuntimeException(String message) { + super(message); + } + + public OrangRuntimeException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/lv/enes/orang/Parser.java b/src/main/java/lv/enes/orang/Parser.java new file mode 100644 index 0000000..fd4b9bb --- /dev/null +++ b/src/main/java/lv/enes/orang/Parser.java @@ -0,0 +1,291 @@ +package lv.enes.orang; + +import lv.enes.orang.ast.*; +import lv.enes.orang.ast.IfElseExpression; +import lv.enes.orang.ast.Statement; + +import java.io.InputStream; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Predicate; + +public class Parser { + public static Program parseProgram(InputStream in) throws ParserException { + var parser = new Parser(in); + return parser.parseProgram(); + } + + public static Program parseProgram(Reader in) throws ParserException { + var parser = new Parser(in); + return parser.parseProgram(); + } + + public static Program parseProgram(String in) throws ParserException { + var parser = new Parser(in); + return parser.parseProgram(); + } + + private final PeekableStream input; + + public Parser(InputStream in) { + this(new Lexer(in)); + } + + public Parser(Reader in) { + this(new Lexer(in)); + } + + public Parser(String in) { + this(new Lexer(in)); + } + + public Parser(Iterator input) { + this.input = new PeekableStream<>(input); + } + + public Program parseProgram() throws ParserException { + var statements = new ArrayList(); + while (!maybeConsumeToken(TokenType.EOF)) { + statements.add(parseStatement()); + maybeConsumeToken(TokenType.SEMICOLON); + } + return new Program(Collections.unmodifiableList(statements)); + } + + private Token consume(Predicate pred, String msg) throws ParserException { + var tok = input.next(); + if (!pred.test(tok)) { + throw new ParserException(STR."\{msg}, got \{tok}"); + } + return tok; + } + + private Token consumeToken(TokenType type) throws ParserException { + return consume(tok -> tok.type() == type, STR."Expected \{type}"); + } + + private boolean maybeConsumeToken(TokenType type) { + if (input.peek().type() == type) { + input.next(); + return true; + } + return false; + } + + private ArrayExpression parseArray() throws ParserException { + consumeToken(TokenType.BRACKET_LEFT); + if (maybeConsumeToken(TokenType.BRACKET_RIGHT)) { + return new ArrayExpression(List.of()); + } + + var items = new ArrayList(); + do { + items.add(parseExpression()); + } while (maybeConsumeToken(TokenType.COMMA)); + consumeToken(TokenType.BRACKET_RIGHT); + + return new ArrayExpression(Collections.unmodifiableList(items)); + } + + private List parseArgSpecs() { + var argSpecs = new ArrayList(); + while (true) { + if (input.peek().type() == TokenType.IDENTIFIER) { + argSpecs.add(input.next().literal()); + } else { + break; + } + } + return Collections.unmodifiableList(argSpecs); + } + + private Expression parseBinaryExpression() throws ParserException { + var lhs = parseCallExpression(); + if (!input.peek().type().isBinaryOp()) { + return lhs; + } + + return parseBinaryExpressionRhs(lhs, input.next().type().toBinaryOp()); + } + + private Expression parseBinaryExpressionRhs(Expression lhs, BinaryExpression.Operator op) throws ParserException { + var rhs = parseCallExpression(); + if (!input.peek().type().isBinaryOp()) { + return new BinaryExpression(op, lhs, rhs); + } + + var op2 = input.next().type().toBinaryOp(); + if (op2.bindsStrongerThan(op)) { + return new BinaryExpression(op, lhs, parseBinaryExpressionRhs(rhs, op2)); + } else { + return parseBinaryExpressionRhs(new BinaryExpression(op, lhs, rhs), op2); + } + } + + private BooleanLiteral parseBoolean() throws ParserException { + var t = consume(tok -> tok.type() == TokenType.FALSE || tok.type() == TokenType.TRUE, "Expected TRUE or FALSE"); + return new BooleanLiteral(t.type() == TokenType.TRUE); + } + + private Expression parseCallExpression() throws ParserException { + var callee = parseSimpleExpression(); + while (couldStartSimpleExpression(input.peek().type())) { + var arg = parseSimpleExpression(); + callee = new CallExpression(callee, arg); + } + return callee; + } + + private Definition parseDefinition() throws ParserException { + consumeToken(TokenType.DEF); + var defSpec = parseDefSpec(); + consumeToken(TokenType.EQUAL); + var value = parseExpression(); + if (defSpec.args().isEmpty()) { + return new Definition(defSpec.name(), value); + } else { + return new Definition(defSpec.name(), new FnExpression(new NonEmptyList<>(defSpec.args()), value)); + } + } + + private DefSpec parseDefSpec() throws ParserException { + var name = consumeToken(TokenType.IDENTIFIER).literal(); + var argSpecs = parseArgSpecs(); + return new DefSpec(name, argSpecs); + } + + private DoExpression parseDoExpression() throws ParserException { + consumeToken(TokenType.DO); + var exprs = new ArrayList(); + do { + exprs.add(parseExpression()); + } while (maybeConsumeToken(TokenType.SEMICOLON)); + consumeToken(TokenType.END); + return new DoExpression(Collections.unmodifiableList(exprs)); + } + + private Expression parseExpression() throws ParserException { + if (input.peek().type().isUnaryOp()) { + return parseUnaryExpression(); + } + return parseBinaryExpression(); + } + + private FnExpression parseFnExpression() throws ParserException { + consumeToken(TokenType.FN); + var argSpecs = parseArgSpecs(); + if (argSpecs.isEmpty()) { + throw new ParserException("Function definition with no arguments"); + } + var body = maybeConsumeToken(TokenType.MINUS_GREATER) ? parseExpression() : parseDoExpression(); + return new FnExpression(new NonEmptyList<>(argSpecs), body); + } + + private IfElseExpression parseIfElseExpression() throws ParserException { + consumeToken(TokenType.IF); + var cond = parseExpression(); + consumeToken(TokenType.THEN); + var trueBranch = parseExpression(); + consumeToken(TokenType.ELSE); + var falseBranch = parseExpression(); + return new IfElseExpression(cond, trueBranch, falseBranch); + } + + private IntLiteral parseInteger() throws ParserException { + var tok = consumeToken(TokenType.INTEGER); + return new IntLiteral(Integer.parseInt(tok.literal())); + } + + private LetInExpression parseLetInExpression() throws ParserException { + consumeToken(TokenType.LET); + var bindings = new ArrayList(); + do { + var defSpec = parseDefSpec(); + consumeToken(TokenType.EQUAL); + var value = parseExpression(); + if (defSpec.args().isEmpty()) { + bindings.add(new LetInExpression.Binding(defSpec.name(), value)); + } else { + var fn = new FnExpression(new NonEmptyList<>(defSpec.args()), value); + bindings.add(new LetInExpression.Binding(defSpec.name(), fn)); + } + } while (maybeConsumeToken(TokenType.AND)); + consumeToken(TokenType.IN); + var body = parseExpression(); + return new LetInExpression(Collections.unmodifiableList(bindings), body); + } + + private Expression parseSimpleExpression() throws ParserException { + return switch (input.peek().type()) { + case PAREN_LEFT -> { + consumeToken(TokenType.PAREN_LEFT); + if (maybeConsumeToken(TokenType.PAREN_RIGHT)) { + yield new ArrayExpression(List.of()); + } + var expr = parseExpression(); + consumeToken(TokenType.PAREN_RIGHT); + yield expr; + } + case TRUE, FALSE -> parseBoolean(); + case INTEGER -> parseInteger(); + case IDENTIFIER -> new VariableExpression(input.next().literal()); + case STRING -> parseString(); + case BRACKET_LEFT -> parseArray(); + case IF -> parseIfElseExpression(); + case LET -> parseLetInExpression(); + case FN -> parseFnExpression(); + case DO -> parseDoExpression(); + default -> throw new ParserException(STR."Unexpected token \{input.peek()}"); + }; + } + + private boolean couldStartSimpleExpression(TokenType type) { + return switch (type) { + case PAREN_LEFT, TRUE, FALSE, INTEGER, IDENTIFIER, STRING, BRACKET_LEFT, IF, LET, FN, DO -> true; + default -> false; + }; + } + + private Statement parseStatement() throws ParserException { + if (input.peek().type() == TokenType.DEF) { + return parseDefinition(); + } else { + return parseExpression(); + } + } + + private Expression parseString() throws ParserException { + var sb = new StringBuilder(); + var cps = input.next().literal().codePoints().iterator(); + while (cps.hasNext()) { + var cp = cps.next(); + if (cp == '\\') { + var escapeChar = cps.next(); + //noinspection UnnecessaryUnboxing + sb.append(switch (escapeChar.intValue()) { + case '\'' -> '\''; + case '"' -> '"'; + case 'r' -> '\r'; + case 'n' -> '\n'; + case 't' -> '\t'; + default -> throw new ParserException(STR."Unknown string escape '\\\{escapeChar}'"); + }); + } else { + sb.appendCodePoint(cp); + } + } + return new StringLiteral(sb.toString()); + } + + private Expression parseUnaryExpression() throws ParserException { + if (input.peek().type().isUnaryOp()) { + var op = input.next().type().toUnaryOp(); + return new UnaryExpression(op, parseUnaryExpression()); + } else { + return parseSimpleExpression(); + } + } +} diff --git a/src/main/java/lv/enes/orang/ParserException.java b/src/main/java/lv/enes/orang/ParserException.java new file mode 100644 index 0000000..9000a93 --- /dev/null +++ b/src/main/java/lv/enes/orang/ParserException.java @@ -0,0 +1,7 @@ +package lv.enes.orang; + +public class ParserException extends OrangException { + public ParserException(String message) { + super(message); + } +} diff --git a/src/main/java/lv/enes/orang/PeekableStream.java b/src/main/java/lv/enes/orang/PeekableStream.java new file mode 100644 index 0000000..b77bab1 --- /dev/null +++ b/src/main/java/lv/enes/orang/PeekableStream.java @@ -0,0 +1,38 @@ +package lv.enes.orang; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; + +public class PeekableStream implements Iterator { + private final Iterator input; + private final Deque buffer = new ArrayDeque<>(); + + public PeekableStream(Iterator input) { + this.input = input; + } + + @Override + public boolean hasNext() { + return !buffer.isEmpty() || input.hasNext(); + } + + @Override + public T next() { + if (!buffer.isEmpty()) { + return buffer.pop(); + } else { + return input.next(); + } + } + + public T peek() { + var value = next(); + putBack(value); + return value; + } + + public void putBack(T value) { + buffer.push(value); + } +} diff --git a/src/main/java/lv/enes/orang/Scope.java b/src/main/java/lv/enes/orang/Scope.java new file mode 100644 index 0000000..3625c69 --- /dev/null +++ b/src/main/java/lv/enes/orang/Scope.java @@ -0,0 +1,54 @@ +package lv.enes.orang; + +import jakarta.annotation.Nullable; +import lv.enes.orang.value.Value; + +import java.util.HashMap; +import java.util.Map; + +public class Scope { + private final Map definitions; + + private final Value lastResult; + + public Scope() { + this(new HashMap<>(Builtins.BUILTINS), null); + } + + private Scope(Map definitions, Value lastResult) { + this.definitions = definitions; + this.lastResult = lastResult; + } + + public Value getDefinition(String name) throws OrangRuntimeException { + if (definitions.containsKey(name)) { + return definitions.get(name); + } + throw new OrangRuntimeException(STR."Value named \{name} is not defined!"); + } + + @Nullable + public Value getLastResult() { + return lastResult; + } + + public Scope withDefinition(String key, Value value) { + var newDefs = new HashMap<>(definitions); + newDefs.put(key, value); + return new Scope(newDefs, null); + } + + public Scope withDefinitions(Map definitions) { + var newDefs = new HashMap<>(this.definitions); + newDefs.putAll(definitions); + return new Scope(newDefs, null); + } + + public void setDefinition(String key, Value value) { + definitions.put(key, value); + } + + public Scope withLastResult(Value value) { + return new Scope(definitions, value); + } +} diff --git a/src/main/java/lv/enes/orang/State.java b/src/main/java/lv/enes/orang/State.java new file mode 100644 index 0000000..46b115b --- /dev/null +++ b/src/main/java/lv/enes/orang/State.java @@ -0,0 +1,12 @@ +package lv.enes.orang; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintWriter; + +public final class State { + public static final BufferedReader STDIN = new BufferedReader(new InputStreamReader(System.in)); + public static final PrintWriter STDOUT = new PrintWriter(System.out); + + private State() {} +} diff --git a/src/main/java/lv/enes/orang/Token.java b/src/main/java/lv/enes/orang/Token.java new file mode 100644 index 0000000..4456b8f --- /dev/null +++ b/src/main/java/lv/enes/orang/Token.java @@ -0,0 +1,15 @@ +package lv.enes.orang; + +public record Token(TokenType type, String literal) { + public Token(TokenType type, Codepoint... cps) { + this(type, codepointsToString(cps)); + } + + private static String codepointsToString(Codepoint... cps) { + var sb = new StringBuilder(cps.length); + for (var cp : cps) { + sb.append(cp); + } + return sb.toString(); + } +} diff --git a/src/main/java/lv/enes/orang/TokenType.java b/src/main/java/lv/enes/orang/TokenType.java new file mode 100644 index 0000000..960435e --- /dev/null +++ b/src/main/java/lv/enes/orang/TokenType.java @@ -0,0 +1,91 @@ +package lv.enes.orang; + +import lv.enes.orang.ast.BinaryExpression; +import lv.enes.orang.ast.UnaryExpression; + +public enum TokenType { + ILLEGAL, + EOF, + + // Literals + IDENTIFIER, + INTEGER, + STRING, + + // Keywords + AND, + DEF, + DO, + ELSE, + END, + FALSE, + FN, + IF, + IN, + LET, + THEN, + TRUE, + + // Special chars + ASTERISK, + BANG, + BRACKET_LEFT, + BRACKET_RIGHT, + COMMA, + EQUAL, + GREATER, + GREATER_EQUAL, + LESS, + LESS_EQUAL, + MINUS, + MINUS_GREATER, + PAREN_LEFT, + PAREN_RIGHT, + PLUS, + QUESTION_EQUAL, + SEMICOLON, + SLASH, + SLASH_EQUAL, + + ; + + public boolean isBinaryOp() { + return switch (this) { + case ASTERISK, SLASH, PLUS, MINUS, QUESTION_EQUAL, SLASH_EQUAL, GREATER, GREATER_EQUAL, LESS, LESS_EQUAL + -> true; + default -> false; + }; + } + + public BinaryExpression.Operator toBinaryOp() { + return switch (this) { + case ASTERISK -> BinaryExpression.Operator.MULTIPLY; + case SLASH -> BinaryExpression.Operator.DIVIDE; + case PLUS -> BinaryExpression.Operator.ADD; + case MINUS -> BinaryExpression.Operator.SUBTRACT; + case QUESTION_EQUAL -> BinaryExpression.Operator.EQUALS; + case SLASH_EQUAL -> BinaryExpression.Operator.NOT_EQUALS; + case GREATER -> BinaryExpression.Operator.GT; + case GREATER_EQUAL -> BinaryExpression.Operator.GTE; + case LESS -> BinaryExpression.Operator.LT; + case LESS_EQUAL -> BinaryExpression.Operator.LTE; + default -> throw new IllegalStateException("Token " + this + " is not a binary operator"); + }; + } + + public boolean isUnaryOp() { + return switch (this) { + case PLUS, MINUS, BANG -> true; + default -> false; + }; + } + + public UnaryExpression.Operator toUnaryOp() { + return switch (this) { + case PLUS -> UnaryExpression.Operator.PLUS; + case MINUS -> UnaryExpression.Operator.NEGATE; + case BANG -> UnaryExpression.Operator.NOT; + default -> throw new IllegalStateException("Token " + this + " is not a unary operator"); + }; + } +} diff --git a/src/main/java/lv/enes/orang/ast/ArrayExpression.java b/src/main/java/lv/enes/orang/ast/ArrayExpression.java new file mode 100644 index 0000000..d2437cf --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/ArrayExpression.java @@ -0,0 +1,21 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.Array; +import lv.enes.orang.value.Value; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public record ArrayExpression(List items) implements Expression { + @Override + public Value evaluateExpression(Scope scope) throws OrangRuntimeException { + var values = new ArrayList(); + for (var item : items) { + values.add(item.evaluateExpression(scope)); + } + return new Array(Collections.unmodifiableList(values)); + } +} diff --git a/src/main/java/lv/enes/orang/ast/BinaryExpression.java b/src/main/java/lv/enes/orang/ast/BinaryExpression.java new file mode 100644 index 0000000..c0b988c --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/BinaryExpression.java @@ -0,0 +1,53 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.Value; + +public record BinaryExpression(Operator operator, Expression lhs, Expression rhs) implements Expression { + public enum Operator { + EQUALS, + NOT_EQUALS, + GT, + GTE, + LT, + LTE, + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE, + ; + + public boolean bindsStrongerThan(Operator other) { + return switch (this) { + case EQUALS, NOT_EQUALS, GT, GTE, LT, LTE -> false; + case ADD, SUBTRACT -> switch (other) { + case EQUALS, NOT_EQUALS, GT, GTE, LTE -> true; + default -> false; + }; + case MULTIPLY, DIVIDE -> switch (other) { + case EQUALS, NOT_EQUALS, GT, GTE, LT, LTE, ADD, SUBTRACT -> true; + default -> false; + }; + }; + } + } + + @Override + public Value evaluateExpression(Scope scope) throws OrangRuntimeException { + var lhs = lhs().evaluateExpression(scope); + var rhs = rhs().evaluateExpression(scope); + return switch (operator) { + case EQUALS -> lhs.orangEquals(rhs); + case NOT_EQUALS -> lhs.notEquals(rhs); + case GT -> lhs.greaterThan(rhs); + case GTE -> lhs.greaterThanOrEqual(rhs); + case LT -> lhs.lessThan(rhs); + case LTE -> lhs.lessThanOrEqual(rhs); + case ADD -> lhs.add(rhs); + case SUBTRACT -> lhs.subtract(rhs); + case MULTIPLY -> lhs.multiply(rhs); + case DIVIDE -> lhs.divide(rhs); + }; + } +} diff --git a/src/main/java/lv/enes/orang/ast/BooleanLiteral.java b/src/main/java/lv/enes/orang/ast/BooleanLiteral.java new file mode 100644 index 0000000..9110f67 --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/BooleanLiteral.java @@ -0,0 +1,12 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.Scope; +import lv.enes.orang.value.OrangBoolean; +import lv.enes.orang.value.Value; + +public record BooleanLiteral(boolean value) implements Expression { + @Override + public Value evaluateExpression(Scope scope) { + return OrangBoolean.of(value); + } +} diff --git a/src/main/java/lv/enes/orang/ast/CallExpression.java b/src/main/java/lv/enes/orang/ast/CallExpression.java new file mode 100644 index 0000000..8f05496 --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/CallExpression.java @@ -0,0 +1,12 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.Value; + +public record CallExpression(Expression callee, Expression arg) implements Expression { + @Override + public Value evaluateExpression(Scope scope) throws OrangRuntimeException { + return callee.evaluateExpression(scope).call(arg.evaluateExpression(scope)); + } +} diff --git a/src/main/java/lv/enes/orang/ast/DefSpec.java b/src/main/java/lv/enes/orang/ast/DefSpec.java new file mode 100644 index 0000000..2233d3f --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/DefSpec.java @@ -0,0 +1,6 @@ +package lv.enes.orang.ast; + +import java.util.List; + +public record DefSpec(String name, List args) { +} diff --git a/src/main/java/lv/enes/orang/ast/Definition.java b/src/main/java/lv/enes/orang/ast/Definition.java new file mode 100644 index 0000000..715daae --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/Definition.java @@ -0,0 +1,15 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.Undefined; + +public record Definition(String name, Expression body) implements Statement { + @Override + public Scope runStatement(Scope scope) throws OrangRuntimeException { + scope.setDefinition(name, Undefined.INSTANCE); + var value = body.evaluateExpression(scope); + scope.setDefinition(name, value); + return scope; + } +} diff --git a/src/main/java/lv/enes/orang/ast/DoExpression.java b/src/main/java/lv/enes/orang/ast/DoExpression.java new file mode 100644 index 0000000..9d69a3e --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/DoExpression.java @@ -0,0 +1,19 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.Value; + +import java.util.List; + +public record DoExpression(List body) implements Expression { + // assert body.!isEmpty() + + @Override + public Value evaluateExpression(Scope scope) throws OrangRuntimeException { + for (var i = 0; i < body.size() - 1; i++) { + body.get(i).evaluateExpression(scope); + } + return body.getLast().evaluateExpression(scope); + } +} diff --git a/src/main/java/lv/enes/orang/ast/Expression.java b/src/main/java/lv/enes/orang/ast/Expression.java new file mode 100644 index 0000000..a0fc3af --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/Expression.java @@ -0,0 +1,14 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.Value; + +public interface Expression extends Statement { + Value evaluateExpression(Scope scope) throws OrangRuntimeException; + + @Override + default Scope runStatement(Scope scope) throws OrangRuntimeException { + return scope.withLastResult(evaluateExpression(scope)); + } +} diff --git a/src/main/java/lv/enes/orang/ast/FnExpression.java b/src/main/java/lv/enes/orang/ast/FnExpression.java new file mode 100644 index 0000000..3c3522f --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/FnExpression.java @@ -0,0 +1,14 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.NonEmptyList; +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.Function; +import lv.enes.orang.value.Value; + +public record FnExpression(NonEmptyList args, Expression body) implements Expression { + @Override + public Value evaluateExpression(Scope scope) throws OrangRuntimeException { + return new Function(scope, args, body); + } +} diff --git a/src/main/java/lv/enes/orang/ast/IfElseExpression.java b/src/main/java/lv/enes/orang/ast/IfElseExpression.java new file mode 100644 index 0000000..9b52cc5 --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/IfElseExpression.java @@ -0,0 +1,22 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.OrangBoolean; +import lv.enes.orang.value.Value; + +public record IfElseExpression(Expression condition, Expression trueBranch, Expression falseBranch) implements Expression { + @Override + public Value evaluateExpression(Scope scope) throws OrangRuntimeException { + var condValue = condition.evaluateExpression(scope); + if (condValue instanceof OrangBoolean value) { + if (value.value()) { + return trueBranch.evaluateExpression(scope); + } else { + return falseBranch.evaluateExpression(scope); + } + } else { + throw new OrangRuntimeException(STR."Condition in an if should be a Boolean not a \{condValue.typeName()}"); + } + } +} diff --git a/src/main/java/lv/enes/orang/ast/IntLiteral.java b/src/main/java/lv/enes/orang/ast/IntLiteral.java new file mode 100644 index 0000000..339ba3b --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/IntLiteral.java @@ -0,0 +1,12 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.Scope; +import lv.enes.orang.value.OrangInteger; +import lv.enes.orang.value.Value; + +public record IntLiteral(int value) implements Expression { + @Override + public Value evaluateExpression(Scope scope) { + return new OrangInteger(value); + } +} diff --git a/src/main/java/lv/enes/orang/ast/LetInExpression.java b/src/main/java/lv/enes/orang/ast/LetInExpression.java new file mode 100644 index 0000000..2379902 --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/LetInExpression.java @@ -0,0 +1,27 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.Undefined; +import lv.enes.orang.value.Value; + +import java.util.HashMap; +import java.util.List; + +public record LetInExpression(List bindings, Expression body) implements Expression { + @Override + public Value evaluateExpression(Scope scope) throws OrangRuntimeException { + var newDefs = new HashMap(); + for (var binding : bindings) { + newDefs.put(binding.name, Undefined.INSTANCE); + } + var newState = scope.withDefinitions(newDefs); + for (var binding : bindings) { + var value = binding.value.evaluateExpression(newState); + newState.setDefinition(binding.name, value); + } + return body.evaluateExpression(newState); + } + + public record Binding(String name, Expression value) {} +} diff --git a/src/main/java/lv/enes/orang/ast/Program.java b/src/main/java/lv/enes/orang/ast/Program.java new file mode 100644 index 0000000..ae5d31e --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/Program.java @@ -0,0 +1,17 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; + +import java.util.List; + +public record Program(List statements) implements Statement { + @Override + public Scope runStatement(Scope inScope) throws OrangRuntimeException { + var state = inScope; + for (var statement : statements) { + state = statement.runStatement(state); + } + return state; + } +} diff --git a/src/main/java/lv/enes/orang/ast/Statement.java b/src/main/java/lv/enes/orang/ast/Statement.java new file mode 100644 index 0000000..62b76bd --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/Statement.java @@ -0,0 +1,8 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; + +public interface Statement { + Scope runStatement(Scope scope) throws OrangRuntimeException, OrangRuntimeException; +} diff --git a/src/main/java/lv/enes/orang/ast/StringLiteral.java b/src/main/java/lv/enes/orang/ast/StringLiteral.java new file mode 100644 index 0000000..367dff5 --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/StringLiteral.java @@ -0,0 +1,13 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.OrangString; +import lv.enes.orang.value.Value; + +public record StringLiteral(String value) implements Expression { + @Override + public Value evaluateExpression(Scope scope) throws OrangRuntimeException { + return new OrangString(value); + } +} diff --git a/src/main/java/lv/enes/orang/ast/UnaryExpression.java b/src/main/java/lv/enes/orang/ast/UnaryExpression.java new file mode 100644 index 0000000..a0f565b --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/UnaryExpression.java @@ -0,0 +1,23 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.Value; + +public record UnaryExpression(Operator operator, Expression child) implements Expression { + public enum Operator { + PLUS, + NEGATE, + NOT, + } + + @Override + public Value evaluateExpression(Scope scope) throws OrangRuntimeException { + var child = child().evaluateExpression(scope); + return switch (operator) { + case PLUS -> child.plus(); + case NEGATE -> child.negate(); + case NOT -> child.not(); + }; + } +} diff --git a/src/main/java/lv/enes/orang/ast/VariableExpression.java b/src/main/java/lv/enes/orang/ast/VariableExpression.java new file mode 100644 index 0000000..1f5334d --- /dev/null +++ b/src/main/java/lv/enes/orang/ast/VariableExpression.java @@ -0,0 +1,12 @@ +package lv.enes.orang.ast; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.value.Value; + +public record VariableExpression(String name) implements Expression { + @Override + public Value evaluateExpression(Scope scope) throws OrangRuntimeException { + return scope.getDefinition(name); + } +} diff --git a/src/main/java/lv/enes/orang/value/Array.java b/src/main/java/lv/enes/orang/value/Array.java new file mode 100644 index 0000000..c3b645d --- /dev/null +++ b/src/main/java/lv/enes/orang/value/Array.java @@ -0,0 +1,65 @@ +package lv.enes.orang.value; + +import lv.enes.orang.OrangRuntimeException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public record Array(List items) implements Value { + @Override + public String typeName() { + return "Array"; + } + + @Override + public String stringify() { + if (items.isEmpty()) { + return "[]"; + } + var sb = new StringBuilder("["); + sb.append(items.getFirst().stringify()); + for (int i = 1; i < items.size(); i++) { + sb.append(", ").append(items.get(i).stringify()); + } + sb.append("]"); + return sb.toString(); + } + + @Override + public String display() { + if (items.isEmpty()) { + return ""; + } + var sb = new StringBuilder(); + sb.append(items.getFirst().display()); + for (var i = 1; i < items.size(); i++) { + sb.append(" ").append(items.get(i).display()); + } + return sb.toString(); + } + + @Override + public Value add(Value rhs) throws OrangRuntimeException { + if (rhs instanceof Array(var rhsi)) { + var newItems = new ArrayList<>(this.items); + newItems.addAll(rhsi); + return new Array(Collections.unmodifiableList(newItems)); + } else { + throw new OrangRuntimeException(STR."add not implemented for Array and \{rhs.typeName()}"); + } + } + + @Override + public Value multiply(Value rhs) throws OrangRuntimeException { + if (rhs instanceof OrangInteger(var repeat)) { + var newItems = new ArrayList(items.size() * repeat); + for (var i = 0; i < repeat; i++) { + newItems.addAll(items); + } + return new Array(Collections.unmodifiableList(newItems)); + } else { + throw new OrangRuntimeException(STR."multiply not implemented for Array and \{rhs.typeName()}"); + } + } +} diff --git a/src/main/java/lv/enes/orang/value/BuiltinFunction.java b/src/main/java/lv/enes/orang/value/BuiltinFunction.java new file mode 100644 index 0000000..92ccc2e --- /dev/null +++ b/src/main/java/lv/enes/orang/value/BuiltinFunction.java @@ -0,0 +1,31 @@ +package lv.enes.orang.value; + +import lv.enes.orang.OrangRuntimeException; + +import java.util.List; + +public record BuiltinFunction(int argCount, Impl impl) implements Value { + @FunctionalInterface + public interface Impl { + Value apply(List args) throws OrangRuntimeException; + } + + @Override + public String typeName() { + return "BuiltinFunction"; + } + + @Override + public String stringify() { + return "#builtinFunction"; + } + + @Override + public Value call(Value param) throws OrangRuntimeException { + if (argCount == 1) { + return impl.apply(List.of(param)); + } else { + return new PartialBuiltinFunction(argCount, List.of(param), impl); + } + } +} diff --git a/src/main/java/lv/enes/orang/value/Function.java b/src/main/java/lv/enes/orang/value/Function.java new file mode 100644 index 0000000..1f288c6 --- /dev/null +++ b/src/main/java/lv/enes/orang/value/Function.java @@ -0,0 +1,28 @@ +package lv.enes.orang.value; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.ast.Expression; + +import java.util.List; + +public record Function(Scope scope, List args, Expression body) implements Value { + @Override + public String typeName() { + return "Function"; + } + + @Override + public String stringify() { + return "#function"; + } + + @Override + public Value call(Value param) throws OrangRuntimeException { + if (args.size() == 1) { + return body.evaluateExpression(scope.withDefinition(args.getFirst(), param)); + } else { + return new PartialFunction(scope, args, List.of(param), body); + } + } +} diff --git a/src/main/java/lv/enes/orang/value/OrangBoolean.java b/src/main/java/lv/enes/orang/value/OrangBoolean.java new file mode 100644 index 0000000..8a4776c --- /dev/null +++ b/src/main/java/lv/enes/orang/value/OrangBoolean.java @@ -0,0 +1,55 @@ +package lv.enes.orang.value; + +import lombok.EqualsAndHashCode; +import lv.enes.orang.OrangRuntimeException; + +@EqualsAndHashCode +public final class OrangBoolean implements Value { + public final static OrangBoolean TRUE = new OrangBoolean(true); + public final static OrangBoolean FALSE = new OrangBoolean(false); + + private final boolean value; + + private OrangBoolean(boolean value) { + this.value = value; + } + + public static OrangBoolean of(boolean value) { + if (value) { + return TRUE; + } + return FALSE; + } + + public boolean value() { + return value; + } + + @Override + public String typeName() { + return "Boolean"; + } + + @Override + public String stringify() { + if (value) { + return "true"; + } else { + return "false"; + } + } + + @Override + public OrangBoolean not() { + return new OrangBoolean(!value); + } + + @Override + public OrangBoolean or(Value rhs) throws OrangRuntimeException { + if (rhs instanceof OrangBoolean rhsb) { + return new OrangBoolean(value || rhsb.value); + } else { + throw new OrangRuntimeException(STR."or is not implemented for Boolean and \{rhs.typeName()}"); + } + } +} diff --git a/src/main/java/lv/enes/orang/value/OrangInteger.java b/src/main/java/lv/enes/orang/value/OrangInteger.java new file mode 100644 index 0000000..9b8d505 --- /dev/null +++ b/src/main/java/lv/enes/orang/value/OrangInteger.java @@ -0,0 +1,70 @@ +package lv.enes.orang.value; + +import lv.enes.orang.OrangRuntimeException; + +public record OrangInteger(int value) implements Value { + @Override + public String typeName() { + return "Integer"; + } + + @Override + public String stringify() { + return Integer.toString(value); + } + + @Override + public OrangInteger negate() { + return new OrangInteger(-value); + } + + @Override + public OrangInteger plus() { + return this; + } + + @Override + public OrangInteger add(Value rhs) throws OrangRuntimeException { + if (rhs instanceof OrangInteger(int rhsi)) { + return new OrangInteger(value + rhsi); + } else { + throw new OrangRuntimeException(STR."add is not implemented for Integer and \{rhs.typeName()}"); + } + } + + @Override + public OrangInteger divide(Value rhs) throws OrangRuntimeException { + if (rhs instanceof OrangInteger(int rhsi)) { + return new OrangInteger(value / rhsi); + } else { + throw new OrangRuntimeException(STR."divide is not implemented for Integer and \{rhs.typeName()}"); + } + } + + @Override + public OrangInteger multiply(Value rhs) throws OrangRuntimeException { + if (rhs instanceof OrangInteger(int rhsi)) { + return new OrangInteger(value * rhsi); + } else { + throw new OrangRuntimeException(STR."multiply is not implemented for Integer and \{rhs.typeName()}"); + } + } + + @Override + public OrangInteger subtract(Value rhs) throws OrangRuntimeException { + if (rhs instanceof OrangInteger(int rhsi)) { + return new OrangInteger(value - rhsi); + } else { + throw new OrangRuntimeException(STR."subtract is not implemented for Integer and \{rhs.typeName()}"); + } + } + + @Override + public OrangBoolean greaterThan(Value rhs) throws OrangRuntimeException { + if (rhs instanceof OrangInteger(int rhsi)) { + return OrangBoolean.of(value > rhsi); + } else { + throw new OrangRuntimeException(STR."greaterThan is not implemented for Integer and \{rhs.typeName()}"); + } + } +} diff --git a/src/main/java/lv/enes/orang/value/OrangString.java b/src/main/java/lv/enes/orang/value/OrangString.java new file mode 100644 index 0000000..c03f7ac --- /dev/null +++ b/src/main/java/lv/enes/orang/value/OrangString.java @@ -0,0 +1,49 @@ +package lv.enes.orang.value; + +import lv.enes.orang.OrangRuntimeException; + +public record OrangString(String value) implements Value { + @Override + public String typeName() { + return "String"; + } + + @Override + public String stringify() { + var sb = new StringBuilder("\""); + var cps = value.codePoints().iterator(); + while (cps.hasNext()) { + var cp = cps.next(); + if (cp == '"') { + sb.append("\\\""); + } else { + sb.appendCodePoint(cp); + } + } + sb.append('"'); + return sb.toString(); + } + + @Override + public String display() { + return value; + } + + @Override + public OrangString add(Value rhs) throws OrangRuntimeException { + if (rhs instanceof OrangString(String rhss)) { + return new OrangString(value + rhss); + } else { + throw new OrangRuntimeException(STR."add is not implemented for Integer and \{rhs.typeName()}"); + } + } + + @Override + public Value multiply(Value rhs) throws OrangRuntimeException { + if (rhs instanceof OrangInteger(var repeat)) { + return new OrangString(value.repeat(Math.max(0, repeat))); + } else { + throw new OrangRuntimeException(STR."multiply not implemented for Array and \{rhs.typeName()}"); + } + } +} diff --git a/src/main/java/lv/enes/orang/value/PartialBuiltinFunction.java b/src/main/java/lv/enes/orang/value/PartialBuiltinFunction.java new file mode 100644 index 0000000..37278a4 --- /dev/null +++ b/src/main/java/lv/enes/orang/value/PartialBuiltinFunction.java @@ -0,0 +1,32 @@ +package lv.enes.orang.value; + +import lv.enes.orang.OrangRuntimeException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public record PartialBuiltinFunction(int argCount, List params, BuiltinFunction.Impl impl) implements Value { + @Override + public String typeName() { + return "BuiltinFunction"; + } + + @Override + public String stringify() { + return "#builtinFunction"; + } + + @Override + public Value call(Value param) throws OrangRuntimeException { + List newParams = new ArrayList<>(params); + newParams.add(param); + newParams = Collections.unmodifiableList(newParams); + + if (newParams.size() == argCount) { + return impl.apply(newParams); + } else { + return new PartialBuiltinFunction(argCount, newParams, impl); + } + } +} diff --git a/src/main/java/lv/enes/orang/value/PartialFunction.java b/src/main/java/lv/enes/orang/value/PartialFunction.java new file mode 100644 index 0000000..59e1466 --- /dev/null +++ b/src/main/java/lv/enes/orang/value/PartialFunction.java @@ -0,0 +1,37 @@ +package lv.enes.orang.value; + +import lv.enes.orang.OrangRuntimeException; +import lv.enes.orang.Scope; +import lv.enes.orang.ast.Expression; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public record PartialFunction(Scope scope, List args, List params, Expression body) implements Value { + @Override + public String typeName() { + return "Function"; + } + + @Override + public String stringify() { + return "#function"; + } + + @Override + public Value call(Value param) throws OrangRuntimeException { + var newParams = new ArrayList<>(params); + newParams.add(param); + if (newParams.size() == args.size()) { + var newDefs = new HashMap(); + for (var i = 0; i < args.size(); i++) { + newDefs.put(args.get(i), newParams.get(i)); + } + return body.evaluateExpression(scope.withDefinitions(newDefs)); + } + + return new PartialFunction(scope, args, Collections.unmodifiableList(newParams), body); + } +} diff --git a/src/main/java/lv/enes/orang/value/Undefined.java b/src/main/java/lv/enes/orang/value/Undefined.java new file mode 100644 index 0000000..a341ee8 --- /dev/null +++ b/src/main/java/lv/enes/orang/value/Undefined.java @@ -0,0 +1,21 @@ +package lv.enes.orang.value; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode +public final class Undefined implements Value { + public static final Undefined INSTANCE = new Undefined(); + + private Undefined() { + } + + @Override + public String typeName() { + return "Undefined"; + } + + @Override + public String stringify() { + return "#undefined"; + } +} diff --git a/src/main/java/lv/enes/orang/value/Value.java b/src/main/java/lv/enes/orang/value/Value.java new file mode 100644 index 0000000..fa8275c --- /dev/null +++ b/src/main/java/lv/enes/orang/value/Value.java @@ -0,0 +1,77 @@ +package lv.enes.orang.value; + +import lv.enes.orang.OrangRuntimeException; + +public sealed interface Value + permits Array, BuiltinFunction, Function, OrangBoolean, OrangInteger, OrangString, PartialBuiltinFunction, + PartialFunction, Undefined { + String typeName(); + String stringify(); + + default String display() { + return stringify(); + } + + + default Value negate() throws OrangRuntimeException { + throw new OrangRuntimeException(STR."negate is not implemented for \{typeName()}"); + } + + default Value not() throws OrangRuntimeException { + throw new OrangRuntimeException(STR."not is not implemented for \{typeName()}"); + } + + default Value plus() throws OrangRuntimeException { + throw new OrangRuntimeException(STR."plus is not implemented for \{typeName()}"); + } + + + default Value add(Value rhs) throws OrangRuntimeException { + throw new OrangRuntimeException(STR."add is not implemented for \{typeName()} and \{rhs.typeName()}"); + } + + default Value call(Value rhs) throws OrangRuntimeException { + throw new OrangRuntimeException(STR."call is not implemented for \{typeName()} and \{rhs.typeName()}"); + } + + default Value divide(Value rhs) throws OrangRuntimeException { + throw new OrangRuntimeException(STR."divide is not implemented for \{typeName()} and \{rhs.typeName()}"); + } + + default Value multiply(Value rhs) throws OrangRuntimeException { + throw new OrangRuntimeException(STR."multiply is not implemented for \{typeName()} and \{rhs.typeName()}"); + } + + default Value or(Value rhs) throws OrangRuntimeException { + throw new OrangRuntimeException(STR."or is not implemented for \{typeName()} and \{rhs.typeName()}"); + } + + default Value subtract(Value rhs) throws OrangRuntimeException { + throw new OrangRuntimeException(STR."subtract is not implemented for \{typeName()} and \{rhs.typeName()}"); + } + + + default OrangBoolean greaterThan(Value rhs) throws OrangRuntimeException { + throw new OrangRuntimeException(STR."greater than is not implemented for \{typeName()} and \{rhs.typeName()}"); + } + + default OrangBoolean greaterThanOrEqual(Value rhs) throws OrangRuntimeException { + return greaterThan(rhs).or(orangEquals(rhs)); + } + + default OrangBoolean lessThan(Value rhs) throws OrangRuntimeException { + return greaterThanOrEqual(rhs).not(); + } + + default OrangBoolean lessThanOrEqual(Value rhs) throws OrangRuntimeException { + return greaterThan(rhs).not(); + } + + default OrangBoolean orangEquals(Value rhs) { + return OrangBoolean.of(this.equals(rhs)); + } + + default OrangBoolean notEquals(Value rhs) { + return orangEquals(rhs).not(); + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..a4bdb66 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module lv.enes.orang { + exports lv.enes.orang; + + requires static jakarta.annotation; + requires static lombok; + + requires org.slf4j; + requires org.slf4j.simple; +} \ No newline at end of file -- cgit v1.2.3