From 43a52fb049b3dfa5fb0e5ff82bc9b7df80f22299 Mon Sep 17 00:00:00 2001 From: Victor Date: Tue, 22 Aug 2017 12:47:43 +0300 Subject: [PATCH] Add parser --- .../hotarufx/exceptions/ParseException.java | 14 ++ .../com/annimon/hotarufx/lib/NumberValue.java | 141 +++++++++++ .../com/annimon/hotarufx/lib/StringValue.java | 81 +++++++ .../java/com/annimon/hotarufx/lib/Types.java | 11 + .../java/com/annimon/hotarufx/lib/Value.java | 14 ++ .../annimon/hotarufx/parser/HotaruParser.java | 218 ++++++++++++++++++ .../annimon/hotarufx/parser/ParseError.java | 27 +++ .../annimon/hotarufx/parser/ParseErrors.java | 46 ++++ .../com/annimon/hotarufx/parser/Parser.java | 105 +++++++++ .../annimon/hotarufx/parser/ast/ASTNode.java | 19 ++ .../hotarufx/parser/ast/AccessNode.java | 33 +++ .../hotarufx/parser/ast/Accessible.java | 10 + .../hotarufx/parser/ast/AssignNode.java | 15 ++ .../hotarufx/parser/ast/BlockNode.java | 18 ++ .../hotarufx/parser/ast/FunctionNode.java | 24 ++ .../annimon/hotarufx/parser/ast/MapNode.java | 15 ++ .../com/annimon/hotarufx/parser/ast/Node.java | 6 + .../hotarufx/parser/ast/ResultVisitor.java | 14 ++ .../hotarufx/parser/ast/UnaryNode.java | 17 ++ .../hotarufx/parser/ast/ValueNode.java | 25 ++ .../hotarufx/parser/ast/VariableNode.java | 25 ++ .../hotarufx/parser/HotaruParserTest.java | 68 ++++++ 22 files changed, 946 insertions(+) create mode 100644 app/src/main/java/com/annimon/hotarufx/exceptions/ParseException.java create mode 100644 app/src/main/java/com/annimon/hotarufx/lib/NumberValue.java create mode 100644 app/src/main/java/com/annimon/hotarufx/lib/StringValue.java create mode 100644 app/src/main/java/com/annimon/hotarufx/lib/Types.java create mode 100644 app/src/main/java/com/annimon/hotarufx/lib/Value.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/HotaruParser.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ParseError.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ParseErrors.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/Parser.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/ASTNode.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/AccessNode.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/Accessible.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/AssignNode.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/BlockNode.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/FunctionNode.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/MapNode.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/Node.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/ResultVisitor.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/UnaryNode.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/ValueNode.java create mode 100644 app/src/main/java/com/annimon/hotarufx/parser/ast/VariableNode.java create mode 100644 app/src/test/java/com/annimon/hotarufx/parser/HotaruParserTest.java diff --git a/app/src/main/java/com/annimon/hotarufx/exceptions/ParseException.java b/app/src/main/java/com/annimon/hotarufx/exceptions/ParseException.java new file mode 100644 index 0000000..5b7e36a --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/exceptions/ParseException.java @@ -0,0 +1,14 @@ +package com.annimon.hotarufx.exceptions; + +import com.annimon.hotarufx.lexer.SourcePosition; + +public class ParseException extends RuntimeException { + + public ParseException() { + super(); + } + + public ParseException(String string, SourcePosition pos) { + super(string + " at " + pos.toString()); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/annimon/hotarufx/lib/NumberValue.java b/app/src/main/java/com/annimon/hotarufx/lib/NumberValue.java new file mode 100644 index 0000000..bfc40d9 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/lib/NumberValue.java @@ -0,0 +1,141 @@ +package com.annimon.hotarufx.lib; + +public class NumberValue implements Value { + + public static final NumberValue MINUS_ONE, ZERO, ONE; + + private static final int CACHE_MIN = -128, CACHE_MAX = 127; + private static final NumberValue[] NUMBER_CACHE; + static { + final int length = CACHE_MAX - CACHE_MIN + 1; + NUMBER_CACHE = new NumberValue[length]; + int value = CACHE_MIN; + for (int i = 0; i < length; i++) { + NUMBER_CACHE[i] = new NumberValue(value++); + } + + final int zeroIndex = -CACHE_MIN; + MINUS_ONE = NUMBER_CACHE[zeroIndex - 1]; + ZERO = NUMBER_CACHE[zeroIndex]; + ONE = NUMBER_CACHE[zeroIndex + 1]; + } + + public static NumberValue fromBoolean(boolean b) { + return b ? ONE : ZERO; + } + + public static NumberValue of(int value) { + if (CACHE_MIN <= value && value <= CACHE_MAX) { + return NUMBER_CACHE[-CACHE_MIN + value]; + } + return new NumberValue(value); + } + + public static NumberValue of(Number value) { + return new NumberValue(value); + } + + private final Number value; + + private NumberValue(Number value) { + this.value = value; + } + + @Override + public int type() { + return Types.NUMBER; + } + + @Override + public Number raw() { + return value; + } + + public boolean asBoolean() { + return value.intValue() != 0; + } + + public byte asByte() { + return value.byteValue(); + } + + public short asShort() { + return value.shortValue(); + } + + @Override + public int asInt() { + return value.intValue(); + } + + public long asLong() { + return value.longValue(); + } + + public float asFloat() { + return value.floatValue(); + } + + public double asDouble() { + return value.doubleValue(); + } + + @Override + public double asNumber() { + return value.doubleValue(); + } + + @Override + public String asString() { + return value.toString(); + } + + @Override + public int hashCode() { + int hash = 3; + hash = 71 * hash + value.hashCode(); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) + return false; + final Number other = ((NumberValue) obj).value; + if (value instanceof Double || other instanceof Double) { + return Double.compare(value.doubleValue(), other.doubleValue()) == 0; + } + if (value instanceof Float || other instanceof Float) { + return Float.compare(value.floatValue(), other.floatValue()) == 0; + } + if (value instanceof Long || other instanceof Long) { + return Long.compare(value.longValue(), other.longValue()) == 0; + } + return Integer.compare(value.intValue(), other.intValue()) == 0; + } + + @Override + public int compareTo(Value o) { + if (o.type() == Types.NUMBER) { + final Number other = ((NumberValue) o).value; + if (value instanceof Double || other instanceof Double) { + return Double.compare(value.doubleValue(), other.doubleValue()); + } + if (value instanceof Float || other instanceof Float) { + return Float.compare(value.floatValue(), other.floatValue()); + } + if (value instanceof Long || other instanceof Long) { + return Long.compare(value.longValue(), other.longValue()); + } + return Integer.compare(value.intValue(), other.intValue()); + } + return asString().compareTo(o.asString()); + } + + @Override + public String toString() { + return asString(); + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/lib/StringValue.java b/app/src/main/java/com/annimon/hotarufx/lib/StringValue.java new file mode 100644 index 0000000..08c9192 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/lib/StringValue.java @@ -0,0 +1,81 @@ +package com.annimon.hotarufx.lib; + +import java.util.Objects; + +public class StringValue implements Value { + + public static final StringValue EMPTY = new StringValue(""); + + private final String value; + + public StringValue(String value) { + this.value = value; + } + + public int length() { + return value.length(); + } + + @Override + public int type() { + return Types.STRING; + } + + @Override + public Object raw() { + return value; + } + + @Override + public int asInt() { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return 0; + } + } + + @Override + public double asNumber() { + try { + return Double.parseDouble(value); + } catch (NumberFormatException e) { + return 0; + } + } + + @Override + public String asString() { + return value; + } + + @Override + public int hashCode() { + int hash = 3; + hash = 97 * hash + Objects.hashCode(this.value); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) + return false; + final StringValue other = (StringValue) obj; + return Objects.equals(this.value, other.value); + } + + @Override + public int compareTo(Value o) { + if (o.type() == Types.STRING) { + return value.compareTo(((StringValue) o).value); + } + return asString().compareTo(o.asString()); + } + + @Override + public String toString() { + return asString(); + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/lib/Types.java b/app/src/main/java/com/annimon/hotarufx/lib/Types.java new file mode 100644 index 0000000..0e87a1c --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/lib/Types.java @@ -0,0 +1,11 @@ +package com.annimon.hotarufx.lib; + +public class Types { + + public static final int + OBJECT = 0, + NUMBER = 1, + STRING = 2, + MAP = 3, + FUNCTION = 4; +} diff --git a/app/src/main/java/com/annimon/hotarufx/lib/Value.java b/app/src/main/java/com/annimon/hotarufx/lib/Value.java new file mode 100644 index 0000000..474bb43 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/lib/Value.java @@ -0,0 +1,14 @@ +package com.annimon.hotarufx.lib; + +public interface Value extends Comparable { + + Object raw(); + + int asInt(); + + double asNumber(); + + String asString(); + + int type(); +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/HotaruParser.java b/app/src/main/java/com/annimon/hotarufx/parser/HotaruParser.java new file mode 100644 index 0000000..18452bb --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/HotaruParser.java @@ -0,0 +1,218 @@ +package com.annimon.hotarufx.parser; + +import com.annimon.hotarufx.exceptions.ParseException; +import com.annimon.hotarufx.lexer.HotaruTokenId; +import com.annimon.hotarufx.lexer.Token; +import com.annimon.hotarufx.parser.ast.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.val; + +public class HotaruParser extends Parser { + + public static Node parse(List tokens) { + val parser = new HotaruParser(tokens); + val program = parser.parse(); + if (parser.getParseErrors().hasErrors()) { + throw new ParseException(); + } + return program; + } + + public HotaruParser(List tokens) { + super(tokens); + } + + private Node block() { + val block = new BlockNode(); + block.start(getSourcePosition()); + consume(HotaruTokenId.LBRACE); + while (!match(HotaruTokenId.RBRACE)) { + block.add(statement()); + } + block.end(getSourcePosition()); + return block; + } + + private Node statementOrBlock() { + if (lookMatch(0, HotaruTokenId.LBRACE)) { + return block(); + } + return statement(); + } + + @Override + protected Node statement() { + return assignmentStatement(); + } + + private Node assignmentStatement() { + return expression(); + } + + private Node functionChain(Node qualifiedNameExpr) { + // f1()()() || f1().f2().f3() || f1().key + val expr = function(qualifiedNameExpr); + if (lookMatch(0, HotaruTokenId.LPAREN)) { + return functionChain(expr); + } + if (lookMatch(0, HotaruTokenId.DOT)) { + final List indices = variableSuffix(); + if (indices == null || indices.isEmpty()) return expr; + + if (lookMatch(0, HotaruTokenId.LPAREN)) { + // next function call + return functionChain(new AccessNode(expr, indices)); + } + // container access + return new AccessNode(expr, indices); + } + return expr; + } + + private FunctionNode function(Node qualifiedNameExpr) { + // function(arg1, arg2, ...) + consume(HotaruTokenId.LPAREN); + val function = new FunctionNode(qualifiedNameExpr); + while (!match(HotaruTokenId.RPAREN)) { + function.addArgument(expression()); + match(HotaruTokenId.COMMA); + } + return function; + } + + private Node map() { + // {key1 : value1, key2 : value2, ...} + consume(HotaruTokenId.LBRACE); + final Map elements = new HashMap<>(); + while (!match(HotaruTokenId.RBRACE)) { + val key = consume(HotaruTokenId.WORD).getText(); + consume(HotaruTokenId.COLON); + val value = expression(); + elements.put(key, value); + match(HotaruTokenId.COMMA); + } + return new MapNode(elements); + } + + + private Node expression() { + return assignment(); + } + + private Node assignment() { + val assignment = assignmentStrict(); + if (assignment != null) { + return assignment; + } + return unary(); + } + + private Node assignmentStrict() { + final int position = pos; + val startSourcePosition = getSourcePosition(); + val targetExpr = qualifiedName(); + if ((targetExpr == null) || !(targetExpr instanceof Accessible)) { + pos = position; + return null; + } + + if (!match(HotaruTokenId.EQ)) { + pos = position; + return null; + } + return new AssignNode((Accessible) targetExpr, expression()) + .start(startSourcePosition) + .end(getSourcePosition()); + } + + private Node unary() { + if (match(HotaruTokenId.MINUS)) { + return new UnaryNode(UnaryNode.Operator.NEGATE, primary()); + } + if (match(HotaruTokenId.PLUS)) { + return primary(); + } + return primary(); + } + + private Node primary() { + if (match(HotaruTokenId.LPAREN)) { + val result = expression(); + match(HotaruTokenId.RPAREN); + return result; + } + return variable(); + } + + private Node variable() { + // function(... + if (lookMatch(0, HotaruTokenId.WORD) && lookMatch(1, HotaruTokenId.LPAREN)) { + return functionChain(new ValueNode(consume(HotaruTokenId.WORD).getText())); + } + + final Node qualifiedNameExpr = qualifiedName(); + if (qualifiedNameExpr != null) { + // variable(args) || arr["key"](args) || obj.key(args) + if (lookMatch(0, HotaruTokenId.LPAREN)) { + return functionChain(qualifiedNameExpr); + } + return qualifiedNameExpr; + } + + if (lookMatch(0, HotaruTokenId.LBRACE)) { + return map(); + } + return value(); + } + + private Node qualifiedName() { + // var || var.key[index].key2 + final Token current = get(0); + if (!match(HotaruTokenId.WORD)) return null; + + final List indices = variableSuffix(); + if ((indices == null) || indices.isEmpty()) { + return new VariableNode(current.getText()); + } + return new AccessNode(current.getText(), indices); + } + + private List variableSuffix() { + final List indices = new ArrayList<>(); + while (lookMatch(0, HotaruTokenId.DOT)) { + if (match(HotaruTokenId.DOT)) { + val fieldName = consume(HotaruTokenId.WORD).getText(); + val key = new ValueNode(fieldName); + indices.add(key); + } + } + return indices; + } + + private Node value() { + val current = get(0); + if (match(HotaruTokenId.NUMBER)) { + return new ValueNode(createNumber(current.getText(), 10)); + } + if (match(HotaruTokenId.TEXT)) { + return new ValueNode(current.getText()); + } + throw new ParseException("Unknown expression: " + current, getSourcePosition()); + } + + private Number createNumber(String text, int radix) { + // Double + if (text.contains(".")) { + return Double.parseDouble(text); + } + // Integer + try { + return Integer.parseInt(text, radix); + } catch (NumberFormatException nfe) { + return Long.parseLong(text, radix); + } + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ParseError.java b/app/src/main/java/com/annimon/hotarufx/parser/ParseError.java new file mode 100644 index 0000000..68cdb8c --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ParseError.java @@ -0,0 +1,27 @@ +package com.annimon.hotarufx.parser; + +import com.annimon.hotarufx.lexer.SourcePosition; + +public class ParseError { + + private final Exception exception; + private final SourcePosition pos; + + public ParseError(Exception exception, SourcePosition pos) { + this.exception = exception; + this.pos = pos; + } + + public Exception getException() { + return exception; + } + + public SourcePosition getPosition() { + return pos; + } + + @Override + public String toString() { + return "ParseError " + exception.getMessage() + " at " + pos.toString(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ParseErrors.java b/app/src/main/java/com/annimon/hotarufx/parser/ParseErrors.java new file mode 100644 index 0000000..4c364ea --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ParseErrors.java @@ -0,0 +1,46 @@ +package com.annimon.hotarufx.parser; + +import com.annimon.hotarufx.lexer.SourcePosition; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; + +public class ParseErrors implements Iterable { + + private final List errors; + + public ParseErrors() { + errors = new ArrayList<>(); + } + + public void clear() { + errors.clear(); + } + + public void add(Exception ex, SourcePosition pos) { + errors.add(new ParseError(ex, pos)); + } + + public boolean hasErrors() { + return !errors.isEmpty(); + } + + @Override + public Iterator iterator() { + return errors.iterator(); + } + + public Stream errorsStream() { + return errors.stream(); + } + + @Override + public String toString() { + final StringBuilder result = new StringBuilder(); + for (ParseError error : errors) { + result.append(error).append(System.lineSeparator()); + } + return result.toString(); + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/Parser.java b/app/src/main/java/com/annimon/hotarufx/parser/Parser.java new file mode 100644 index 0000000..d345cb4 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/Parser.java @@ -0,0 +1,105 @@ +package com.annimon.hotarufx.parser; + +import com.annimon.hotarufx.exceptions.ParseException; +import com.annimon.hotarufx.lexer.HotaruTokenId; +import com.annimon.hotarufx.lexer.SourcePosition; +import com.annimon.hotarufx.lexer.Token; +import com.annimon.hotarufx.parser.ast.BlockNode; +import com.annimon.hotarufx.parser.ast.Node; +import java.util.List; +import lombok.val; + +public abstract class Parser { + + private static final Token EOF = new Token(HotaruTokenId.EOF, "", + 0, new SourcePosition(-1, -1, -1)); + + private final List tokens; + private final int size; + private final ParseErrors parseErrors; + private Node parsedNode; + + protected int pos; + + public Parser(List tokens) { + this.tokens = tokens; + this.size = tokens.size(); + parseErrors = new ParseErrors(); + pos = 0; + } + + public Node getParsedNode() { + return parsedNode; + } + + public ParseErrors getParseErrors() { + return parseErrors; + } + + public Node parse() { + parseErrors.clear(); + val result = new BlockNode(); + result.start(getSourcePosition()); + while (!match(HotaruTokenId.EOF)) { + try { + result.add(statement()); + } catch (Exception ex) { + parseErrors.add(ex, getSourcePosition()); + recover(); + } + } + result.end(getSourcePosition()); + parsedNode = result; + return result; + } + + protected abstract Node statement(); + + protected SourcePosition getSourcePosition() { + if (size == 0) return new SourcePosition(0, 0, 0); + if (pos >= size) return tokens.get(size - 1).getPosition(); + return tokens.get(pos).getPosition(); + } + + private void recover() { + int preRecoverPosition = pos; + for (int i = preRecoverPosition; i <= size; i++) { + pos = i; + try { + statement(); + // successfully parsed, + pos = i; // restore position + return; + } catch (Exception ex) { + // fail + } + } + } + + protected Token consume(HotaruTokenId type) { + val current = get(0); + if (type != current.getType()) { + throw new ParseException("Token " + current + " doesn't match " + type, current.getPosition()); + } + pos++; + return current; + } + + protected boolean match(HotaruTokenId type) { + val current = get(0); + if (type != current.getType()) return false; + pos++; + return true; + } + + protected boolean lookMatch(int pos, HotaruTokenId type) { + return get(pos).getType() == type; + } + + protected Token get(int relativePosition) { + val position = pos + relativePosition; + if (position >= size) return EOF; + return tokens.get(position); + } + +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/ASTNode.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/ASTNode.java new file mode 100644 index 0000000..dd4cfd4 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/ASTNode.java @@ -0,0 +1,19 @@ +package com.annimon.hotarufx.parser.ast; + +import com.annimon.hotarufx.lexer.SourcePosition; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Accessors(fluent = true) +public abstract class ASTNode implements Node { + + @Getter @Setter + private SourcePosition start; + @Getter @Setter + private SourcePosition end; + + public String getSourceRange() { + return start + " .. " + end; + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/AccessNode.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/AccessNode.java new file mode 100644 index 0000000..cfa27b9 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/AccessNode.java @@ -0,0 +1,33 @@ +package com.annimon.hotarufx.parser.ast; + +import com.annimon.hotarufx.lib.Value; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class AccessNode extends ASTNode implements Accessible { + + @Getter + public final Node root; + public final List indices; + + public AccessNode(String variable, List indices) { + this(new VariableNode(variable), indices); + } + + @Override + public Value get() { + return null; + } + + @Override + public Value set(Value value) { + return null; + } + + @Override + public R accept(ResultVisitor visitor, T input) { + return visitor.visit(this, input); + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/Accessible.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/Accessible.java new file mode 100644 index 0000000..74e3600 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/Accessible.java @@ -0,0 +1,10 @@ +package com.annimon.hotarufx.parser.ast; + +import com.annimon.hotarufx.lib.Value; + +public interface Accessible extends Node { + + Value get(); + + Value set(Value value); +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/AssignNode.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/AssignNode.java new file mode 100644 index 0000000..793b010 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/AssignNode.java @@ -0,0 +1,15 @@ +package com.annimon.hotarufx.parser.ast; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class AssignNode extends ASTNode { + + public final Accessible target; + public final Node value; + + @Override + public R accept(ResultVisitor visitor, T input) { + return visitor.visit(this, input); + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/BlockNode.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/BlockNode.java new file mode 100644 index 0000000..61563b1 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/BlockNode.java @@ -0,0 +1,18 @@ +package com.annimon.hotarufx.parser.ast; + +import java.util.ArrayList; +import java.util.List; + +public class BlockNode extends ASTNode { + + public final List statements = new ArrayList<>(); + + public void add(Node node) { + statements.add(node); + } + + @Override + public R accept(ResultVisitor visitor, T t) { + return visitor.visit(this, t); + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/FunctionNode.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/FunctionNode.java new file mode 100644 index 0000000..2c92ce0 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/FunctionNode.java @@ -0,0 +1,24 @@ +package com.annimon.hotarufx.parser.ast; + +import java.util.ArrayList; +import java.util.List; + +public class FunctionNode extends ASTNode { + + public final Node functionNode; + public final List arguments; + + public FunctionNode(Node functionNode) { + this.functionNode = functionNode; + arguments = new ArrayList<>(); + } + + public void addArgument(Node arg) { + arguments.add(arg); + } + + @Override + public R accept(ResultVisitor visitor, T input) { + return visitor.visit(this, input); + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/MapNode.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/MapNode.java new file mode 100644 index 0000000..3e45079 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/MapNode.java @@ -0,0 +1,15 @@ +package com.annimon.hotarufx.parser.ast; + +import java.util.Map; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class MapNode extends ASTNode { + + public final Map elements; + + @Override + public R accept(ResultVisitor visitor, T input) { + return visitor.visit(this, input); + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/Node.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/Node.java new file mode 100644 index 0000000..01c0a81 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/Node.java @@ -0,0 +1,6 @@ +package com.annimon.hotarufx.parser.ast; + +public interface Node { + + R accept(ResultVisitor visitor, T input); +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/ResultVisitor.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/ResultVisitor.java new file mode 100644 index 0000000..d5e24b5 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/ResultVisitor.java @@ -0,0 +1,14 @@ +package com.annimon.hotarufx.parser.ast; + +public interface ResultVisitor { + + R visit(AccessNode node, T t); + R visit(AssignNode node, T t); + R visit(BlockNode node, T t); + R visit(FunctionNode node, T t); + R visit(MapNode node, T t); + R visit(UnaryNode node, T t); + R visit(ValueNode node, T t); + R visit(VariableNode node, T t); + +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/UnaryNode.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/UnaryNode.java new file mode 100644 index 0000000..438ab8f --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/UnaryNode.java @@ -0,0 +1,17 @@ +package com.annimon.hotarufx.parser.ast; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class UnaryNode extends ASTNode { + + public enum Operator { NEGATE }; + + public final Operator operator; + public final Node node; + + @Override + public R accept(ResultVisitor visitor, T input) { + return visitor.visit(this, input); + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/ValueNode.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/ValueNode.java new file mode 100644 index 0000000..c080265 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/ValueNode.java @@ -0,0 +1,25 @@ +package com.annimon.hotarufx.parser.ast; + +import com.annimon.hotarufx.lib.NumberValue; +import com.annimon.hotarufx.lib.StringValue; +import com.annimon.hotarufx.lib.Value; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class ValueNode extends ASTNode { + + public final Value value; + + public ValueNode(Number value) { + this(NumberValue.of(value)); + } + + public ValueNode(String value) { + this(new StringValue(value)); + } + + @Override + public R accept(ResultVisitor visitor, T input) { + return visitor.visit(this, input); + } +} diff --git a/app/src/main/java/com/annimon/hotarufx/parser/ast/VariableNode.java b/app/src/main/java/com/annimon/hotarufx/parser/ast/VariableNode.java new file mode 100644 index 0000000..c79c4c8 --- /dev/null +++ b/app/src/main/java/com/annimon/hotarufx/parser/ast/VariableNode.java @@ -0,0 +1,25 @@ +package com.annimon.hotarufx.parser.ast; + +import com.annimon.hotarufx.lib.Value; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class VariableNode extends ASTNode implements Accessible { + + public final String name; + + @Override + public Value get() { + return null; + } + + @Override + public Value set(Value value) { + return value; + } + + @Override + public R accept(ResultVisitor visitor, T input) { + return visitor.visit(this, input); + } +} diff --git a/app/src/test/java/com/annimon/hotarufx/parser/HotaruParserTest.java b/app/src/test/java/com/annimon/hotarufx/parser/HotaruParserTest.java new file mode 100644 index 0000000..9170c00 --- /dev/null +++ b/app/src/test/java/com/annimon/hotarufx/parser/HotaruParserTest.java @@ -0,0 +1,68 @@ +package com.annimon.hotarufx.parser; + +import com.annimon.hotarufx.exceptions.ParseException; +import com.annimon.hotarufx.lexer.HotaruLexer; +import com.annimon.hotarufx.parser.ast.AssignNode; +import com.annimon.hotarufx.parser.ast.BlockNode; +import com.annimon.hotarufx.parser.ast.FunctionNode; +import com.annimon.hotarufx.parser.ast.MapNode; +import com.annimon.hotarufx.parser.ast.Node; +import com.annimon.hotarufx.parser.ast.ValueNode; +import com.annimon.hotarufx.parser.ast.VariableNode; +import org.junit.jupiter.api.Test; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class HotaruParserTest { + + static Node p(String input) { + return HotaruParser.parse(HotaruLexer.tokenize(input)); + } + + @Test + void testParseNodeAssignment() { + String input = "A = node({x: -1, text:'hello'})"; + Node node = p(input); + assertThat(node, instanceOf(BlockNode.class)); + + BlockNode block = (BlockNode) node; + assertThat(block.statements.size(), is(1)); + assertThat(block.start().getPosition(), is(1)); + assertThat(block.end().getPosition(), is(input.length())); + + Node firstNode = block.statements.get(0); + assertThat(firstNode, instanceOf(AssignNode.class)); + + AssignNode assignNode = (AssignNode) firstNode; + assertThat(assignNode.target, instanceOf(VariableNode.class)); + assertThat(assignNode.value, instanceOf(FunctionNode.class)); + + VariableNode target = (VariableNode) assignNode.target; + assertThat(target.name, is("A")); + + FunctionNode value = (FunctionNode) assignNode.value; + assertThat(value.functionNode, instanceOf(ValueNode.class)); + assertThat(((ValueNode)value.functionNode).value.asString(), is("node")); + assertThat(value.arguments.size(), is(1)); + + Node argument = value.arguments.get(0); + assertThat(argument, instanceOf(MapNode.class)); + + MapNode mapNode = (MapNode) argument; + assertThat(mapNode.elements.size(), is(2)); + assertThat(mapNode.elements, allOf( + hasKey("x"), hasKey("text") + )); + } + + @Test + void testParseErrors() { + assertThrows(ParseException.class, () -> p("A =")); + assertThrows(ParseException.class, () -> p("1 = A")); + assertThrows(ParseException.class, () -> p("{A = B}")); + } +} \ No newline at end of file