mirror of
https://github.com/aNNiMON/HotaruFX.git
synced 2024-09-19 14:14:21 +03:00
Add parser
This commit is contained in:
parent
45749bf471
commit
43a52fb049
@ -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());
|
||||||
|
}
|
||||||
|
}
|
141
app/src/main/java/com/annimon/hotarufx/lib/NumberValue.java
Normal file
141
app/src/main/java/com/annimon/hotarufx/lib/NumberValue.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
81
app/src/main/java/com/annimon/hotarufx/lib/StringValue.java
Normal file
81
app/src/main/java/com/annimon/hotarufx/lib/StringValue.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
11
app/src/main/java/com/annimon/hotarufx/lib/Types.java
Normal file
11
app/src/main/java/com/annimon/hotarufx/lib/Types.java
Normal file
@ -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;
|
||||||
|
}
|
14
app/src/main/java/com/annimon/hotarufx/lib/Value.java
Normal file
14
app/src/main/java/com/annimon/hotarufx/lib/Value.java
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package com.annimon.hotarufx.lib;
|
||||||
|
|
||||||
|
public interface Value extends Comparable<Value> {
|
||||||
|
|
||||||
|
Object raw();
|
||||||
|
|
||||||
|
int asInt();
|
||||||
|
|
||||||
|
double asNumber();
|
||||||
|
|
||||||
|
String asString();
|
||||||
|
|
||||||
|
int type();
|
||||||
|
}
|
218
app/src/main/java/com/annimon/hotarufx/parser/HotaruParser.java
Normal file
218
app/src/main/java/com/annimon/hotarufx/parser/HotaruParser.java
Normal file
@ -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<Token> tokens) {
|
||||||
|
val parser = new HotaruParser(tokens);
|
||||||
|
val program = parser.parse();
|
||||||
|
if (parser.getParseErrors().hasErrors()) {
|
||||||
|
throw new ParseException();
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HotaruParser(List<Token> 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<Node> 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<String, Node> 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<Node> indices = variableSuffix();
|
||||||
|
if ((indices == null) || indices.isEmpty()) {
|
||||||
|
return new VariableNode(current.getText());
|
||||||
|
}
|
||||||
|
return new AccessNode(current.getText(), indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Node> variableSuffix() {
|
||||||
|
final List<Node> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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<ParseError> {
|
||||||
|
|
||||||
|
private final List<ParseError> 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<ParseError> iterator() {
|
||||||
|
return errors.iterator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<ParseError> 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();
|
||||||
|
}
|
||||||
|
}
|
105
app/src/main/java/com/annimon/hotarufx/parser/Parser.java
Normal file
105
app/src/main/java/com/annimon/hotarufx/parser/Parser.java
Normal file
@ -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<Token> tokens;
|
||||||
|
private final int size;
|
||||||
|
private final ParseErrors parseErrors;
|
||||||
|
private Node parsedNode;
|
||||||
|
|
||||||
|
protected int pos;
|
||||||
|
|
||||||
|
public Parser(List<Token> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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<Node> indices;
|
||||||
|
|
||||||
|
public AccessNode(String variable, List<Node> indices) {
|
||||||
|
this(new VariableNode(variable), indices);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Value get() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Value set(Value value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <R, T> R accept(ResultVisitor<R, T> visitor, T input) {
|
||||||
|
return visitor.visit(this, input);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -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, T> R accept(ResultVisitor<R, T> visitor, T input) {
|
||||||
|
return visitor.visit(this, input);
|
||||||
|
}
|
||||||
|
}
|
@ -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<Node> statements = new ArrayList<>();
|
||||||
|
|
||||||
|
public void add(Node node) {
|
||||||
|
statements.add(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <R, T> R accept(ResultVisitor<R, T> visitor, T t) {
|
||||||
|
return visitor.visit(this, t);
|
||||||
|
}
|
||||||
|
}
|
@ -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<Node> arguments;
|
||||||
|
|
||||||
|
public FunctionNode(Node functionNode) {
|
||||||
|
this.functionNode = functionNode;
|
||||||
|
arguments = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addArgument(Node arg) {
|
||||||
|
arguments.add(arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <R, T> R accept(ResultVisitor<R, T> visitor, T input) {
|
||||||
|
return visitor.visit(this, input);
|
||||||
|
}
|
||||||
|
}
|
@ -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<String, Node> elements;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <R, T> R accept(ResultVisitor<R, T> visitor, T input) {
|
||||||
|
return visitor.visit(this, input);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package com.annimon.hotarufx.parser.ast;
|
||||||
|
|
||||||
|
public interface Node {
|
||||||
|
|
||||||
|
<R, T> R accept(ResultVisitor<R, T> visitor, T input);
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
package com.annimon.hotarufx.parser.ast;
|
||||||
|
|
||||||
|
public interface ResultVisitor<R, T> {
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
}
|
@ -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, T> R accept(ResultVisitor<R, T> visitor, T input) {
|
||||||
|
return visitor.visit(this, input);
|
||||||
|
}
|
||||||
|
}
|
@ -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, T> R accept(ResultVisitor<R, T> visitor, T input) {
|
||||||
|
return visitor.visit(this, input);
|
||||||
|
}
|
||||||
|
}
|
@ -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, T> R accept(ResultVisitor<R, T> visitor, T input) {
|
||||||
|
return visitor.visit(this, input);
|
||||||
|
}
|
||||||
|
}
|
@ -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}"));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user