[muscript] First iteration, seems to work
This commit is contained in:
parent
03a121357c
commit
a4e9a3425e
|
@ -0,0 +1,55 @@
|
|||
# μScript
|
||||
MuScript was created to allow respackopts pack authors to specify conditions for loading resources
|
||||
in a more human-friendly manner than the previous json tree-based system.
|
||||
It is intended to be vaguely like java in its syntax, though it deviates in some aspects.
|
||||
The language is parsed into an AST representation which is executed in-place, no compilation is performed.
|
||||
MuScript supports outputting data using various types, not just strings or booleans
|
||||
|
||||
## Value types
|
||||
This DSL supports numbers (double), booleans, strings, objects, lists and functions.
|
||||
The topmost operator of a condition must always return a boolean for it to be valid in the context of respackopts.
|
||||
The values of input data are according to the pack config.
|
||||
String literals may be written with quotation marks as follows: `"some text"` or `'some text'`
|
||||
Numbers may be written as follows: `103` or `10.15`
|
||||
Booleans may be written either as `true` or `false`
|
||||
Objects, lists and functions cannot be created manually but may be provided to the script as parameters or from functions.
|
||||
Additionally, function parameters will automatically be packed in a list
|
||||
|
||||
Please ensure you use proper whitespace, as this might behave unexpectedly otherwise.
|
||||
|
||||
## Operators
|
||||
Numbers support the following operators (x and y are numbers):
|
||||
- Addition: `x + y`
|
||||
- Subtraction: `x - y`
|
||||
- Multiplication: `x * y`
|
||||
- Division: `x / y`
|
||||
- Modulo: `x % y`
|
||||
- Power: `x ^ y`
|
||||
- Inversion: `-x`
|
||||
- Greater than: `x > y`
|
||||
- Greater than or equal: `x >= y`
|
||||
- Less than: `x < y`
|
||||
- Less than or equal: `x <= y`
|
||||
- Equality: `x == y`
|
||||
- Inequality: `x != y`
|
||||
|
||||
Strings support the following operators (x and y are strings, a is any value):
|
||||
- Equality: `x == y`
|
||||
- Inequality: `x != y`
|
||||
- Concatenation: `x || a` or `a || x`
|
||||
|
||||
Booleans support the following operators (x and y are booleans, a and b are values of the same type):
|
||||
- Conditional: `x ? a : b`
|
||||
- Inversion: `!x`
|
||||
- And: `x & y`
|
||||
- Or: `x | y`
|
||||
- Equality (=XNOR): `x == y`
|
||||
- Inequality (=XOR): `x != y`
|
||||
|
||||
Objects support the following operators (x is an object with an entry called `entry`):
|
||||
- Value access via `.`: `x.entry`
|
||||
- Value access via square brackets: `x["entry"]` (also supports other types)
|
||||
|
||||
Parentheses (`()`) may be used to indicate order of operation
|
||||
|
||||
Namespacing is done using double colons like in c++ (`namespace::value`), though it should not be used
|
|
@ -0,0 +1,13 @@
|
|||
dependencies {
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
groupId = 'io.gitlab.jfronny'
|
||||
artifactId = 'muscript'
|
||||
|
||||
from components.java
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package io.gitlab.jfronny.muscript.compiler;
|
||||
|
||||
/**
|
||||
* Class for storing errors produced while parsing.
|
||||
*/
|
||||
public record Error(int line, int character, char ch, String message) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("[line %d, character %d] at '%s': %s", line, character, ch, message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,185 @@
|
|||
package io.gitlab.jfronny.muscript.compiler;
|
||||
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
import io.gitlab.jfronny.commons.StringFormatter;
|
||||
|
||||
public abstract class Expr<T> {
|
||||
public abstract Type getResultType();
|
||||
public abstract T get(OAny<?> branch, OAny<?> dataRoot);
|
||||
|
||||
public T get(OAny<?> branch) {
|
||||
return get(branch, branch);
|
||||
}
|
||||
|
||||
public BoolExpr asBoolExpr() {
|
||||
if (this instanceof BoolExpr e) return e;
|
||||
throw new IllegalArgumentException("Expected boolean but is " + getResultType());
|
||||
}
|
||||
|
||||
public StringExpr asStringExpr() {
|
||||
if (this instanceof StringExpr e) return e;
|
||||
return new StringExpr() {
|
||||
@Override
|
||||
public String get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return StringFormatter.toString(Expr.this.get(branch, dataRoot));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public NumberExpr asNumberExpr() {
|
||||
if (this instanceof NumberExpr e) return e;
|
||||
throw new IllegalArgumentException("Expected number but is " + getResultType());
|
||||
}
|
||||
|
||||
public abstract ObjectExpr asObjectExpr();
|
||||
|
||||
public boolean isNull() {
|
||||
return this instanceof NullExpr;
|
||||
}
|
||||
|
||||
public static abstract class BoolExpr extends Expr<Boolean> {
|
||||
@Override
|
||||
public Type getResultType() {
|
||||
return Type.Boolean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectExpr asObjectExpr() {
|
||||
return new ObjectExpr() {
|
||||
@Override
|
||||
public OAny<?> get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return OFinal.of(BoolExpr.this.get(branch, dataRoot));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class StringExpr extends Expr<String> {
|
||||
@Override
|
||||
public Type getResultType() {
|
||||
return Type.String;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectExpr asObjectExpr() {
|
||||
return new ObjectExpr() {
|
||||
@Override
|
||||
public OAny<?> get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return OFinal.of(StringExpr.this.get(branch, dataRoot));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class NumberExpr extends Expr<Double> {
|
||||
@Override
|
||||
public Type getResultType() {
|
||||
return Type.Number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectExpr asObjectExpr() {
|
||||
return new ObjectExpr() {
|
||||
@Override
|
||||
public OAny<?> get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return OFinal.of(NumberExpr.this.get(branch, dataRoot));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class ObjectExpr extends Expr<OAny<?>> {
|
||||
@Override
|
||||
public Type getResultType() {
|
||||
return Type.Object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoolExpr asBoolExpr() {
|
||||
return new BoolExpr() {
|
||||
@Override
|
||||
public Boolean get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return ObjectExpr.this.get(branch, dataRoot).asBool().getValue();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringExpr asStringExpr() {
|
||||
return new StringExpr() {
|
||||
@Override
|
||||
public String get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return ObjectExpr.this.get(branch, dataRoot).asString().getValue();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public NumberExpr asNumberExpr() {
|
||||
return new NumberExpr() {
|
||||
@Override
|
||||
public Double get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return ObjectExpr.this.get(branch, dataRoot).asNumber().getValue();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectExpr asObjectExpr() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class NullExpr extends Expr<Object> {
|
||||
@Override
|
||||
public Type getResultType() {
|
||||
return Type.Object;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectExpr asObjectExpr() {
|
||||
return new ObjectExpr() {
|
||||
@Override
|
||||
public OAny<?> get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return new ONull();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static BoolExpr literal(boolean bool) {
|
||||
return new BoolExpr() {
|
||||
@Override
|
||||
public Boolean get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return bool;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static StringExpr literal(String string) {
|
||||
return new StringExpr() {
|
||||
@Override
|
||||
public String get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static NumberExpr literal(double number) {
|
||||
return new NumberExpr() {
|
||||
@Override
|
||||
public Double get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return number;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static Expr<?> literalNull() {
|
||||
return new NullExpr();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
package io.gitlab.jfronny.muscript.compiler;
|
||||
|
||||
// Heavily inspired by starscript
|
||||
public class Lexer {
|
||||
/** The type of the token */
|
||||
public Token token;
|
||||
/** The string representation of the token */
|
||||
public String lexeme;
|
||||
|
||||
public int line = 1, character = -1;
|
||||
public char ch;
|
||||
|
||||
private final String source;
|
||||
private int start, current;
|
||||
|
||||
public Lexer(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
/** Scans for the next token storing it in {@link Lexer#token} and {@link Lexer#lexeme}. Produces {@link Token#EOF} if the end of source code has been reached and {@link Token#Error} if there has been an error */
|
||||
public void next() {
|
||||
start = current;
|
||||
|
||||
if (isAtEnd()) {
|
||||
createToken(Token.EOF);
|
||||
return;
|
||||
}
|
||||
|
||||
// Scan expression
|
||||
skipWhitespace();
|
||||
if (isAtEnd()) {
|
||||
createToken(Token.EOF);
|
||||
return;
|
||||
}
|
||||
|
||||
char c = advance();
|
||||
|
||||
if (isDigit(c) || (c == '-' && isDigit(peek()))) number();
|
||||
else if (isAlpha(c)) identifier();
|
||||
else {
|
||||
switch (c) {
|
||||
case '\'', '"' -> string();
|
||||
|
||||
case '=' -> {if (match('=')) createToken(Token.EqualEqual); else unexpected();}
|
||||
case '!' -> createToken(match('=') ? Token.BangEqual : Token.Bang);
|
||||
|
||||
case '+' -> createToken(Token.Plus);
|
||||
case '-' -> createToken(Token.Minus);
|
||||
case '*' -> createToken(Token.Star);
|
||||
case '/' -> createToken(Token.Slash);
|
||||
case '%' -> createToken(Token.Percentage);
|
||||
case '>' -> createToken(match('=') ? Token.GreaterEqual : Token.Greater);
|
||||
case '<' -> createToken(match('=') ? Token.LessEqual : Token.Less);
|
||||
|
||||
case '&' -> createToken(Token.And);
|
||||
case '|' -> createToken(match('|') ? Token.Concat : Token.Or);
|
||||
case '^' -> createToken(Token.UpArrow);
|
||||
|
||||
case '.' -> createToken(Token.Dot);
|
||||
case ',' -> createToken(Token.Comma);
|
||||
case '?' -> createToken(Token.QuestionMark);
|
||||
case ':' -> createToken(Token.Colon);
|
||||
case '(' -> createToken(Token.LeftParen);
|
||||
case ')' -> createToken(Token.RightParen);
|
||||
case '[' -> createToken(Token.LeftBracket);
|
||||
case ']' -> createToken(Token.RightBracket);
|
||||
|
||||
default -> unexpected();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void string() {
|
||||
while (!isAtEnd() && peek() != '"' && peek() != '\'') {
|
||||
if (peek() == '\n') line++;
|
||||
advance();
|
||||
}
|
||||
|
||||
if (isAtEnd()) {
|
||||
createToken(Token.Error, "Unterminated expression.");
|
||||
}
|
||||
else {
|
||||
advance();
|
||||
createToken(Token.String, source.substring(start + 1, current - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private void number() {
|
||||
while (isDigit(peek())) advance();
|
||||
|
||||
if (peek() == '.' && isDigit(peekNext())) {
|
||||
advance();
|
||||
|
||||
while (isDigit(peek())) advance();
|
||||
}
|
||||
|
||||
createToken(Token.Number);
|
||||
}
|
||||
|
||||
private void identifier() {
|
||||
while (!isAtEnd()) {
|
||||
char c = peek();
|
||||
if (isAlpha(c) || isDigit(c)) {
|
||||
advance();
|
||||
} else if (c == ':' && peekNext() == ':') {
|
||||
advance();
|
||||
advance();
|
||||
} else break;
|
||||
}
|
||||
|
||||
createToken(Token.Identifier);
|
||||
|
||||
switch (lexeme) {
|
||||
case "null" -> token = Token.Null;
|
||||
case "true" -> token = Token.True;
|
||||
case "false" -> token = Token.False;
|
||||
}
|
||||
}
|
||||
|
||||
private void skipWhitespace() {
|
||||
while (true) {
|
||||
if (isAtEnd()) return;
|
||||
char c = peek();
|
||||
|
||||
switch (c) {
|
||||
case ' ', '\r', '\t' -> advance();
|
||||
case '\n' -> {
|
||||
line++;
|
||||
advance();
|
||||
}
|
||||
default -> {
|
||||
start = current;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
private void unexpected() {
|
||||
createToken(Token.Error, "Unexpected character.");
|
||||
}
|
||||
|
||||
private void createToken(Token token, String lexeme) {
|
||||
this.token = token;
|
||||
this.lexeme = lexeme;
|
||||
}
|
||||
|
||||
private void createToken(Token token) {
|
||||
createToken(token, source.substring(start, current));
|
||||
}
|
||||
|
||||
private boolean match(char expected) {
|
||||
if (isAtEnd()) return false;
|
||||
if (source.charAt(current) != expected) return false;
|
||||
|
||||
advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
private char advance() {
|
||||
character++;
|
||||
return ch = source.charAt(current++);
|
||||
}
|
||||
|
||||
private char peek() {
|
||||
if (isAtEnd()) return '\0';
|
||||
return source.charAt(current);
|
||||
}
|
||||
|
||||
private char peekNext() {
|
||||
if (current + 1 >= source.length()) return '\0';
|
||||
return source.charAt(current + 1);
|
||||
}
|
||||
|
||||
private boolean isAtEnd() {
|
||||
return current >= source.length();
|
||||
}
|
||||
|
||||
private boolean isDigit(char c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
private boolean isAlpha(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,285 @@
|
|||
package io.gitlab.jfronny.muscript.compiler;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.expr.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class Parser {
|
||||
private final Lexer lexer;
|
||||
|
||||
private final TokenData previous = new TokenData();
|
||||
private final TokenData current = new TokenData();
|
||||
|
||||
public static Expr<?> parse(String source) {
|
||||
return new Parser(new Lexer(source)).parse();
|
||||
}
|
||||
|
||||
public Parser(Lexer lexer) {
|
||||
this.lexer = lexer;
|
||||
}
|
||||
|
||||
public Expr<?> parse() {
|
||||
advance();
|
||||
return expression();
|
||||
}
|
||||
|
||||
// Expressions
|
||||
|
||||
private Expr<?> expression() {
|
||||
try {
|
||||
return conditional();
|
||||
} catch (RuntimeException e) {
|
||||
if (e instanceof ParseException) throw e;
|
||||
else throw error(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Expr<?> conditional() {
|
||||
Expr<?> expr = and();
|
||||
|
||||
if (match(Token.QuestionMark)) {
|
||||
Expr<?> trueExpr = expression();
|
||||
consume(Token.Colon, "Expected ':' after first part of condition.");
|
||||
Expr<?> falseExpr = expression();
|
||||
expr = new Conditional(expr.asBoolExpr(), trueExpr, falseExpr);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Expr<?> and() {
|
||||
Expr<?> expr = or();
|
||||
|
||||
while (match(Token.And)) {
|
||||
Expr<?> right = or();
|
||||
expr = new LogicBiExpr(expr.asBoolExpr(), right.asBoolExpr(), Token.And);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Expr<?> or() {
|
||||
Expr<?> expr = equality();
|
||||
|
||||
while (match(Token.Or)) {
|
||||
Expr<?> right = equality();
|
||||
expr = new LogicBiExpr(expr.asBoolExpr(), right.asBoolExpr(), Token.Or);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Expr<?> equality() {
|
||||
Expr<?> expr = concat();
|
||||
|
||||
while (match(Token.EqualEqual, Token.BangEqual)) {
|
||||
Token op = previous.token;
|
||||
Expr<?> right = concat();
|
||||
Expr.BoolExpr e = new Equal(expr, right);
|
||||
if (op == Token.BangEqual) e = new Not(e);
|
||||
expr = e;
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Expr<?> concat() {
|
||||
Expr<?> expr = comparison();
|
||||
|
||||
while (match(Token.Concat)) {
|
||||
Token op = previous.token;
|
||||
Expr<?> right = comparison();
|
||||
expr = new Concatenate(expr.asStringExpr(), right.asStringExpr());
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Expr<?> comparison() {
|
||||
Expr<?> expr = term();
|
||||
|
||||
while (match(Token.Greater, Token.GreaterEqual, Token.Less, Token.LessEqual)) {
|
||||
Token op = previous.token;
|
||||
Expr<?> right = term();
|
||||
expr = new Compare(expr.asNumberExpr(), right.asNumberExpr(), op);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Expr<?> term() {
|
||||
Expr<?> expr = factor();
|
||||
|
||||
while (match(Token.Plus, Token.Minus)) {
|
||||
Token op = previous.token;
|
||||
Expr<?> right = factor();
|
||||
expr = new MathBiExpr(expr.asNumberExpr(), right.asNumberExpr(), op);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Expr<?> factor() {
|
||||
Expr<?> expr = exp();
|
||||
|
||||
while (match(Token.Star, Token.Slash, Token.Percentage, Token.UpArrow)) {
|
||||
Token op = previous.token;
|
||||
Expr<?> right = exp();
|
||||
expr = new MathBiExpr(expr.asNumberExpr(), right.asNumberExpr(), op);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Expr<?> exp() {
|
||||
Expr<?> expr = unary();
|
||||
|
||||
while (match(Token.UpArrow)) {
|
||||
Token op = previous.token;
|
||||
Expr<?> right = unary();
|
||||
expr = new MathBiExpr(expr.asNumberExpr(), right.asNumberExpr(), op);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Expr<?> unary() {
|
||||
if (match(Token.Bang, Token.Minus)) {
|
||||
Token op = previous.token;
|
||||
Expr<?> right = unary();
|
||||
return op == Token.Bang ? new Not(right.asBoolExpr()) : new Invert(right.asNumberExpr());
|
||||
}
|
||||
|
||||
return call();
|
||||
}
|
||||
|
||||
private Expr<?> call() {
|
||||
Expr<?> expr = primary();
|
||||
|
||||
while (true) {
|
||||
if (match(Token.LeftParen)) {
|
||||
expr = finishCall(expr);
|
||||
}
|
||||
else if (match(Token.Dot)) {
|
||||
TokenData name = consume(Token.Identifier, "Expected field name after '.'.");
|
||||
expr = new Get(expr.asObjectExpr(), Expr.literal(name.lexeme));
|
||||
}
|
||||
else if (match(Token.LeftBracket)) {
|
||||
expr = new Get(expr.asObjectExpr(), expression());
|
||||
consume(Token.RightBracket, "Expected closing bracket");
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private Expr<?> finishCall(Expr<?> callee) {
|
||||
List<Expr.ObjectExpr> args = new ArrayList<>(2);
|
||||
|
||||
if (!check(Token.RightParen)) {
|
||||
do {
|
||||
args.add(expression().asObjectExpr());
|
||||
} while (match(Token.Comma));
|
||||
}
|
||||
|
||||
consume(Token.RightParen, "Expected ')' after function arguments.");
|
||||
return new Call(callee.asObjectExpr(), args);
|
||||
}
|
||||
|
||||
private Expr<?> primary() {
|
||||
if (match(Token.Null)) return Expr.literalNull();
|
||||
if (match(Token.String)) return Expr.literal(previous.lexeme);
|
||||
if (match(Token.True, Token.False)) return Expr.literal(previous.lexeme.equals("true"));
|
||||
if (match(Token.Number)) return Expr.literal(Double.parseDouble(previous.lexeme));
|
||||
if (match(Token.Identifier)) return new Variable(previous.lexeme);
|
||||
|
||||
if (match(Token.LeftParen)) {
|
||||
Expr<?> expr = expression();
|
||||
consume(Token.RightParen, "Expected ')' after expression.");
|
||||
return new Group(expr);
|
||||
}
|
||||
|
||||
throw error("Expected expression.");
|
||||
}
|
||||
|
||||
// Helpers
|
||||
private ParseException error(String message) {
|
||||
return new ParseException(new Error(current.line, current.character, current.ch, message));
|
||||
}
|
||||
|
||||
private TokenData consume(Token token, String message) {
|
||||
if (check(token)) return advance();
|
||||
throw error(message);
|
||||
}
|
||||
|
||||
private boolean match(Token... tokens) {
|
||||
for (Token token : tokens) {
|
||||
if (check(token)) {
|
||||
advance();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean check(Token token) {
|
||||
if (isAtEnd()) return false;
|
||||
return current.token == token;
|
||||
}
|
||||
|
||||
private TokenData advance() {
|
||||
previous.set(current);
|
||||
|
||||
lexer.next();
|
||||
current.set(lexer.token, lexer.lexeme, lexer.line, lexer.character, lexer.ch);
|
||||
|
||||
if (current.token == Token.Error) {
|
||||
throw error(current.lexeme);
|
||||
}
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
private boolean isAtEnd() {
|
||||
return current.token == Token.EOF;
|
||||
}
|
||||
|
||||
// Token data
|
||||
private static class TokenData {
|
||||
public Token token;
|
||||
public String lexeme;
|
||||
public int line, character;
|
||||
public char ch;
|
||||
|
||||
public void set(Token token, String lexeme, int line, int character, char ch) {
|
||||
this.token = token;
|
||||
this.lexeme = lexeme;
|
||||
this.line = line;
|
||||
this.character = character;
|
||||
this.ch = ch;
|
||||
}
|
||||
|
||||
public void set(TokenData data) {
|
||||
set(data.token, data.lexeme, data.line, data.character, data.ch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s '%s'", token, lexeme);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Exception
|
||||
public static class ParseException extends RuntimeException {
|
||||
public final Error error;
|
||||
|
||||
public ParseException(Error error) {
|
||||
super(error.toString());
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package io.gitlab.jfronny.muscript.compiler;
|
||||
|
||||
public enum Token {
|
||||
String, Identifier, Number,
|
||||
|
||||
Null,
|
||||
True, False,
|
||||
And, Or,
|
||||
|
||||
EqualEqual, BangEqual,
|
||||
|
||||
Concat,
|
||||
|
||||
Greater, GreaterEqual,
|
||||
Less, LessEqual,
|
||||
|
||||
Plus, Minus,
|
||||
Star, Slash, Percentage, UpArrow,
|
||||
Bang,
|
||||
|
||||
Dot, Comma,
|
||||
QuestionMark, Colon,
|
||||
LeftParen, RightParen,
|
||||
LeftBracket, RightBracket,
|
||||
|
||||
Error, EOF
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package io.gitlab.jfronny.muscript.compiler;
|
||||
|
||||
public enum Type {
|
||||
String, Number, Boolean, Object
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
|
||||
public class Call extends Expr.ObjectExpr {
|
||||
private final ObjectExpr left;
|
||||
private final List<ObjectExpr> args;
|
||||
|
||||
public Call(ObjectExpr left, List<ObjectExpr> args) {
|
||||
this.left = left;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAny<?> get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return left.get(branch, dataRoot).asCallable().getValue().apply(OFinal.of(args.stream().map(e -> e.get(dataRoot, dataRoot)).toArray(OAny[]::new)));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
public class Compare extends Expr.BoolExpr {
|
||||
private final NumberExpr left;
|
||||
private final NumberExpr right;
|
||||
private final Token comparator;
|
||||
|
||||
public Compare(NumberExpr left, NumberExpr right, Token comparator) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.comparator = comparator;
|
||||
if (comparator != Token.Greater && comparator != Token.GreaterEqual && comparator != Token.Less && comparator != Token.LessEqual)
|
||||
throw new IllegalArgumentException("Compare only supports > >= < <= operators");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
double left = this.left.get(branch, dataRoot);
|
||||
double right = this.right.get(branch, dataRoot);
|
||||
return switch (comparator) {
|
||||
case Greater -> left > right;
|
||||
case GreaterEqual -> left >= right;
|
||||
case Less -> left < right;
|
||||
case LessEqual -> left <= right;
|
||||
default -> throw new IllegalArgumentException();
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
public class Concatenate extends Expr.StringExpr {
|
||||
private final StringExpr left;
|
||||
private final StringExpr right;
|
||||
|
||||
public Concatenate(StringExpr left, StringExpr right) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return left.get(branch, dataRoot) + right.get(branch, dataRoot);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
public class Conditional extends Expr {
|
||||
public final BoolExpr condition;
|
||||
public final Expr<?> trueExpr;
|
||||
public final Expr<?> falseExpr;
|
||||
|
||||
public Conditional(BoolExpr condition, Expr<?> trueExpr, Expr<?> falseExpr) {
|
||||
this.condition = condition;
|
||||
this.trueExpr = trueExpr;
|
||||
this.falseExpr = falseExpr;
|
||||
if (trueExpr.getResultType() != falseExpr.getResultType())
|
||||
throw new IllegalArgumentException("Values used in conditional operator must be of the same type");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getResultType() {
|
||||
return trueExpr.getResultType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(OAny branch, OAny dataRoot) {
|
||||
return condition.get(branch, dataRoot) ? trueExpr.get(branch, dataRoot) : falseExpr.get(branch, dataRoot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoolExpr asBoolExpr() {
|
||||
BoolExpr trueExpr = this.trueExpr.asBoolExpr();
|
||||
BoolExpr falseExpr = this.falseExpr.asBoolExpr();
|
||||
return new BoolExpr() {
|
||||
@Override
|
||||
public Boolean get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return condition.get(branch, dataRoot) ? trueExpr.get(branch, dataRoot) : falseExpr.get(branch, dataRoot);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringExpr asStringExpr() {
|
||||
StringExpr trueExpr = this.trueExpr.asStringExpr();
|
||||
StringExpr falseExpr = this.falseExpr.asStringExpr();
|
||||
return new StringExpr() {
|
||||
@Override
|
||||
public String get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return condition.get(branch, dataRoot) ? trueExpr.get(branch, dataRoot) : falseExpr.get(branch, dataRoot);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public NumberExpr asNumberExpr() {
|
||||
NumberExpr trueExpr = this.trueExpr.asNumberExpr();
|
||||
NumberExpr falseExpr = this.falseExpr.asNumberExpr();
|
||||
return new NumberExpr() {
|
||||
@Override
|
||||
public Double get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return condition.get(branch, dataRoot) ? trueExpr.get(branch, dataRoot) : falseExpr.get(branch, dataRoot);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectExpr asObjectExpr() {
|
||||
ObjectExpr trueExpr = this.trueExpr.asObjectExpr();
|
||||
ObjectExpr falseExpr = this.falseExpr.asObjectExpr();
|
||||
return new ObjectExpr() {
|
||||
@Override
|
||||
public OAny<?> get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return condition.get(branch, dataRoot) ? trueExpr.get(branch, dataRoot) : falseExpr.get(branch, dataRoot);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class Equal extends Expr.BoolExpr {
|
||||
private final Expr<?> left;
|
||||
private final Expr<?> right;
|
||||
|
||||
public Equal(Expr<?> left, Expr<?> right) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return Objects.equals(unwrap(left.get(branch, dataRoot)), unwrap(right.get(branch, dataRoot)));
|
||||
}
|
||||
|
||||
private Object unwrap(Object o) {
|
||||
if (o instanceof OAny<?> a) return unwrap(a.getValue());
|
||||
return o;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
public class Get extends Expr.ObjectExpr {
|
||||
private final ObjectExpr left;
|
||||
private final Expr<?> name;
|
||||
|
||||
public Get(ObjectExpr left, Expr<?> name) {
|
||||
this.left = left;
|
||||
this.name = name;
|
||||
if (name.getResultType() != Type.String && name.getResultType() != Type.Number && name.getResultType() != Type.Object) {
|
||||
throw new IllegalArgumentException("Name must be either a string or a number");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAny<?> get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
OAny<?> left = this.left.get(branch, dataRoot);
|
||||
if (left instanceof OObject o) {
|
||||
return o.get(name.asStringExpr().get(dataRoot, dataRoot));
|
||||
} else if (left instanceof OList l) {
|
||||
return l.get(name.asNumberExpr().get(dataRoot, dataRoot).intValue());
|
||||
}
|
||||
throw new IllegalArgumentException("Name is not of a valid type");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
public class Group extends Expr {
|
||||
public final Expr expr;
|
||||
|
||||
public Group(Expr expr) {
|
||||
this.expr = expr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type getResultType() {
|
||||
return expr.getResultType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object get(OAny branch, OAny dataRoot) {
|
||||
return expr.get(branch, dataRoot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BoolExpr asBoolExpr() {
|
||||
return expr.asBoolExpr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringExpr asStringExpr() {
|
||||
return expr.asStringExpr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NumberExpr asNumberExpr() {
|
||||
return expr.asNumberExpr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObjectExpr asObjectExpr() {
|
||||
return expr.asObjectExpr();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
public class Invert extends Expr.NumberExpr {
|
||||
private final Expr.NumberExpr inner;
|
||||
|
||||
public Invert(Expr.NumberExpr inner) {
|
||||
this.inner = inner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return -inner.get(branch, dataRoot);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
public class LogicBiExpr extends Expr.BoolExpr {
|
||||
private final Expr.BoolExpr left;
|
||||
private final Expr.BoolExpr right;
|
||||
private final Token comparator;
|
||||
|
||||
public LogicBiExpr(Expr.BoolExpr left, Expr.BoolExpr right, Token fnc) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.comparator = fnc;
|
||||
if (fnc != Token.And && fnc != Token.Or && fnc != Token.UpArrow)
|
||||
throw new IllegalArgumentException("Logic only supports & | ^ operators");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
boolean left = this.left.get(branch, dataRoot);
|
||||
boolean right = this.right.get(branch, dataRoot);
|
||||
return switch (comparator) {
|
||||
case And -> left & right;
|
||||
case Or -> left | right;
|
||||
case UpArrow -> left ^ right;
|
||||
default -> throw new IllegalArgumentException();
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
public class MathBiExpr extends Expr.NumberExpr {
|
||||
private final NumberExpr left;
|
||||
private final NumberExpr right;
|
||||
private final Token comparator;
|
||||
|
||||
public MathBiExpr(NumberExpr left, NumberExpr right, Token fnc) {
|
||||
this.left = left;
|
||||
this.right = right;
|
||||
this.comparator = fnc;
|
||||
if (fnc != Token.Plus && fnc != Token.Minus && fnc != Token.Star && fnc != Token.Slash && fnc != Token.UpArrow && fnc != Token.Percentage)
|
||||
throw new IllegalArgumentException("Math only supports + - * / operators");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
double left = this.left.get(branch, dataRoot);
|
||||
double right = this.right.get(branch, dataRoot);
|
||||
return switch (comparator) {
|
||||
case Plus -> left + right;
|
||||
case Minus -> left - right;
|
||||
case Star -> left * right;
|
||||
case Slash -> left / right;
|
||||
case UpArrow -> Math.pow(left, right);
|
||||
case Percentage -> left % right;
|
||||
default -> throw new IllegalArgumentException();
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
public class Not extends Expr.BoolExpr {
|
||||
private final BoolExpr inner;
|
||||
|
||||
public Not(BoolExpr inner) {
|
||||
this.inner = inner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
return !inner.get(branch, dataRoot);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package io.gitlab.jfronny.muscript.compiler.expr;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
public class Variable extends Expr.ObjectExpr {
|
||||
private final String name;
|
||||
|
||||
public Variable(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAny<?> get(OAny<?> branch, OAny<?> dataRoot) {
|
||||
if (branch.asObject().has(name)) return branch.asObject().get(name);
|
||||
else if (name.contains("::")) {
|
||||
OAny<?> res = branch;
|
||||
for (String s : name.split("::")) {
|
||||
if (!res.asObject().has(s))
|
||||
throw new IllegalArgumentException("This object doesn't contain that name");
|
||||
res = res.asObject().get(s);
|
||||
}
|
||||
return res;
|
||||
} else throw new IllegalArgumentException("This object doesn't contain that name");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package io.gitlab.jfronny.muscript.debug;
|
||||
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
import java.lang.reflect.*;
|
||||
import java.util.*;
|
||||
|
||||
public class ObjectGraphPrinter {
|
||||
public static String printGraph(Object o) throws IllegalAccessException {
|
||||
if (o == null) return "null";
|
||||
StringBuilder builder = new StringBuilder();
|
||||
IndentingWriter writer = new IndentingWriter(builder, "");
|
||||
writer.writeLine("[" + o.getClass().getSimpleName() + "]");
|
||||
printGraph(writer.level(), o, o.getClass());
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static void printGraph(IndentingWriter writer, Object o, Class<?> klazz) throws IllegalAccessException {
|
||||
for (Field field : klazz.getDeclaredFields()) {
|
||||
if (Modifier.isStatic(field.getModifiers())) continue;
|
||||
field.setAccessible(true);
|
||||
Object fo = field.get(o);
|
||||
if (fo == null) {
|
||||
writer.writeLine(field.getName() + " [" + field.getType().getSimpleName() + "]: null");
|
||||
continue;
|
||||
}
|
||||
Class<?> kz = fo.getClass();
|
||||
String name = field.getName() + " [" + kz.getSimpleName() + "]";
|
||||
if (kz.isEnum()) {
|
||||
writer.writeLine(name + " = " + fo);
|
||||
} else if (kz.isAssignableFrom(String.class)) {
|
||||
writer.writeLine(name + " = \"" + fo + "\"");
|
||||
} else if (kz.isAssignableFrom(Double.class)) {
|
||||
writer.writeLine(name + " = " + fo);
|
||||
} else if (kz.isAssignableFrom(OAny.class)) {
|
||||
writer.writeLine(name + " = " + fo);
|
||||
} else if (kz.isAssignableFrom(Collection.class)) {
|
||||
writer.writeLine(name + ":");
|
||||
for (Object element : (Collection<?>) fo) {
|
||||
printGraph(writer.level(), element, element.getClass());
|
||||
}
|
||||
} else {
|
||||
writer.writeLine(name + ":");
|
||||
printGraph(writer.level(), fo, fo.getClass());
|
||||
}
|
||||
}
|
||||
klazz = klazz.getSuperclass();
|
||||
if (klazz != null) printGraph(writer, o, klazz);
|
||||
}
|
||||
|
||||
private record IndentingWriter(StringBuilder out, String indentation) {
|
||||
public void writeLine(String text) {
|
||||
out.append(indentation).append(text.replace("\n", "\\n")).append('\n');
|
||||
}
|
||||
|
||||
public IndentingWriter level() {
|
||||
return new IndentingWriter(out, indentation + " ");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package io.gitlab.jfronny.muscript.optic;
|
||||
|
||||
import io.gitlab.jfronny.commons.*;
|
||||
|
||||
public interface OAny<T> {
|
||||
T getValue();
|
||||
|
||||
default OBool asBool() {
|
||||
if (this instanceof OBool bool) return bool;
|
||||
else throw new IllegalArgumentException("This value is not a bool");
|
||||
}
|
||||
|
||||
default ONumber asNumber() {
|
||||
if (this instanceof ONumber number) return number;
|
||||
else throw new IllegalArgumentException("This value is not a number");
|
||||
}
|
||||
|
||||
default OString asString() {
|
||||
if (this instanceof OString string) return string;
|
||||
else return OFinal.of(StringFormatter.toString(getValue()));
|
||||
}
|
||||
|
||||
default OObject asObject() {
|
||||
if (this instanceof OObject object) return object;
|
||||
else throw new IllegalArgumentException("This value is not an object");
|
||||
}
|
||||
|
||||
default OList asList() {
|
||||
if (this instanceof OList list) return list;
|
||||
else throw new IllegalArgumentException("This value is not a list");
|
||||
}
|
||||
|
||||
default OCallable asCallable() {
|
||||
if (this instanceof OCallable callable) return callable;
|
||||
else throw new IllegalArgumentException("This value is not a callable");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package io.gitlab.jfronny.muscript.optic;
|
||||
|
||||
public interface OBool extends OAny<Boolean> {
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package io.gitlab.jfronny.muscript.optic;
|
||||
|
||||
import java.util.function.*;
|
||||
|
||||
public interface OCallable extends OAny<Function<OList, OAny<?>>> {
|
||||
default OAny<?> call(OList args) {
|
||||
return getValue().apply(args);
|
||||
}
|
||||
|
||||
default OAny<?> call(OAny<?>... args) {
|
||||
return call(OFinal.of(args));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package io.gitlab.jfronny.muscript.optic;
|
||||
|
||||
import io.gitlab.jfronny.commons.StringFormatter;
|
||||
|
||||
public abstract class OContainer<T> implements OAny<T> {
|
||||
private T value;
|
||||
|
||||
@Override
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public T setValue(T value) {
|
||||
if (value != null)
|
||||
this.value = value;
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return StringFormatter.toString(value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package io.gitlab.jfronny.muscript.optic;
|
||||
|
||||
import io.gitlab.jfronny.commons.StringFormatter;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.*;
|
||||
|
||||
public record OFinal<T>(T value) implements OAny<T> {
|
||||
public static OBool of(boolean b) {
|
||||
return new FBool(b);
|
||||
}
|
||||
|
||||
public static ONumber of(double b) {
|
||||
return new FNumber(b);
|
||||
}
|
||||
|
||||
public static OString of(String b) {
|
||||
return new FString(b);
|
||||
}
|
||||
|
||||
public static OObject of(Map<String, OAny<?>> b) {
|
||||
return new FObject(Map.copyOf(b));
|
||||
}
|
||||
|
||||
public static OList of(OAny<?>... b) {
|
||||
return new FList(List.of(b));
|
||||
}
|
||||
|
||||
public static OCallable of(Function<OList, OAny<?>> b) {
|
||||
return new FCallable(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public T getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return StringFormatter.toString(value);
|
||||
}
|
||||
|
||||
private record FBool(boolean value) implements OBool {
|
||||
@Override
|
||||
public Boolean getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return StringFormatter.toString(value);
|
||||
}
|
||||
}
|
||||
|
||||
private record FNumber(double value) implements ONumber {
|
||||
@Override
|
||||
public Double getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return StringFormatter.toString(value);
|
||||
}
|
||||
}
|
||||
|
||||
private record FString(String value) implements OString {
|
||||
@Override
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return StringFormatter.toString(value);
|
||||
}
|
||||
}
|
||||
|
||||
private record FObject(Map<String, OAny<?>> value) implements OObject {
|
||||
@Override
|
||||
public Map<String, OAny<?>> getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return StringFormatter.toString(value);
|
||||
}
|
||||
}
|
||||
|
||||
private record FList(List<OAny<?>> value) implements OList {
|
||||
@Override
|
||||
public List<OAny<?>> getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return StringFormatter.toString(value);
|
||||
}
|
||||
}
|
||||
|
||||
private record FCallable(Function<OList, OAny<?>> value) implements OCallable {
|
||||
@Override
|
||||
public Function<OList, OAny<?>> getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "<Callable>";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package io.gitlab.jfronny.muscript.optic;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public interface OList extends OAny<List<OAny<?>>> {
|
||||
default OAny<?> get(int i) {
|
||||
return getValue().get(i);
|
||||
}
|
||||
default int size() {
|
||||
return getValue().size();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package io.gitlab.jfronny.muscript.optic;
|
||||
|
||||
public final class ONull implements OAny<Object> {
|
||||
@Override
|
||||
public Object getValue() {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package io.gitlab.jfronny.muscript.optic;
|
||||
|
||||
public interface ONumber extends OAny<Double> {
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package io.gitlab.jfronny.muscript.optic;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public interface OObject extends OAny<Map<String, OAny<?>>> {
|
||||
default OAny<?> get(String key) {
|
||||
return getValue().get(key);
|
||||
}
|
||||
|
||||
default boolean has(String key) {
|
||||
return getValue().containsKey(key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package io.gitlab.jfronny.muscript.optic;
|
||||
|
||||
public interface OString extends OAny<String> {
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package io.gitlab.jfronny.muscript.test;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import static io.gitlab.jfronny.muscript.test.MuTestUtil.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class BooleanTest {
|
||||
@Test
|
||||
void simpleLogic() {
|
||||
assertTrue(bool("false | true"));
|
||||
assertTrue(bool("false != true"));
|
||||
assertTrue(bool("!false != false"));
|
||||
assertFalse(bool("!false & false"));
|
||||
assertTrue(bool("!false & true"));
|
||||
assertTrue(bool("true == true"));
|
||||
assertTrue(bool("false != true"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void conditional() {
|
||||
assertEquals(3, number("true ? 3 : 4"));
|
||||
assertEquals(4, number("false ? 3 : 4"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package io.gitlab.jfronny.muscript.test;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import static io.gitlab.jfronny.muscript.test.MuTestUtil.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class CallableTest {
|
||||
@Test
|
||||
void basicFunctionTest() {
|
||||
assertEquals(3, number("object.subfunc(0, 1, 2)"));
|
||||
assertEquals(18, number("object.subfunc(0, object.subfunc(1, 2, 3), 4)"));
|
||||
assertTrue(bool("repeatArgs().repeatArgs().boolean"));
|
||||
assertEquals(32, number("function(object.subfunc(0, 1), 5)"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.gitlab.jfronny.muscript.test;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import static io.gitlab.jfronny.muscript.test.MuTestUtil.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class CombinationTest {
|
||||
@Test
|
||||
void simpleTest() {
|
||||
assertTrue(bool("1 + 5 < 12 & (true != false)"));
|
||||
assertEquals("59", string("15 / 3 || 2 + 7"));
|
||||
assertEquals("7yes5", string("2 + 5 || (true ? 'yes' : 'no') || 15 / 3"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.gitlab.jfronny.muscript.test;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import static io.gitlab.jfronny.muscript.test.MuTestUtil.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ListTest {
|
||||
@Test
|
||||
void listAccess() {
|
||||
assertTrue(bool("list[0]"));
|
||||
assertTrue(bool("list[1] < 3"));
|
||||
assertTrue(bool("list[2] || list[0] == '3true'"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package io.gitlab.jfronny.muscript.test;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import io.gitlab.jfronny.muscript.debug.*;
|
||||
import io.gitlab.jfronny.muscript.optic.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import static io.gitlab.jfronny.muscript.optic.OFinal.*;
|
||||
|
||||
public class MuTestUtil {
|
||||
public static double number(String source) {
|
||||
return Parser.parse(source).asNumberExpr().get(makeArgs());
|
||||
}
|
||||
|
||||
public static boolean bool(String source) {
|
||||
Expr<?> tree = Parser.parse(source);
|
||||
try {
|
||||
return tree.asBoolExpr().get(makeArgs());
|
||||
} catch (RuntimeException e) {
|
||||
try {
|
||||
System.out.println("Caught error with tree:\n" + ObjectGraphPrinter.printGraph(tree));
|
||||
} catch (IllegalAccessException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static String string(String source) {
|
||||
return Parser.parse(source).asStringExpr().get(makeArgs());
|
||||
}
|
||||
|
||||
public static OAny<?> makeArgs() {
|
||||
return of(Map.of(
|
||||
"boolean", of(true),
|
||||
"number", of(15),
|
||||
"string", of("Value"),
|
||||
"object", of(Map.of(
|
||||
"subvalue", of(1024),
|
||||
"subfunc", of(v -> of(v.get(1).asNumber().getValue() * v.size())),
|
||||
"1", of("One")
|
||||
)),
|
||||
"object2", of(Map.of(
|
||||
"valuename", of("subvalue"),
|
||||
"sub", of(Map.of(
|
||||
"val", of(10)
|
||||
))
|
||||
)),
|
||||
"list", of(of(true), of(2), of("3")),
|
||||
"function", of(v -> of(Math.pow(v.get(0).asNumber().getValue(), v.get(1).asNumber().getValue()))),
|
||||
"repeatArgs", of(v -> makeArgs())
|
||||
));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package io.gitlab.jfronny.muscript.test;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import static io.gitlab.jfronny.muscript.test.MuTestUtil.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class NumberTest {
|
||||
@Test
|
||||
void simpleMath() {
|
||||
assertEquals(12, number("7 + 5"));
|
||||
assertEquals(12, number("14 - 2"));
|
||||
assertEquals(12, number("4* 3"));
|
||||
assertEquals(12, number("24 /2"));
|
||||
assertEquals(12, number("92 % 20"));
|
||||
assertEquals(12, number("2^2*3"));
|
||||
assertEquals(-12, number("-12"));
|
||||
assertEquals(12, number("-1 * -12 + 2 + (-2)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void compare() {
|
||||
assertTrue(bool("12 < 10 * 2"));
|
||||
assertTrue(bool("12 > 14 / 2"));
|
||||
assertTrue(bool("12 == 10 + 2"));
|
||||
assertTrue(bool("12 >= 10 + 2"));
|
||||
assertTrue(bool("10 <= 10 + 2"));
|
||||
assertTrue(bool("12 != 10 * 2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void orderOfOperations() {
|
||||
assertEquals(12, number("2 + 5 * 2"));
|
||||
assertEquals(12, number("3*2^2"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package io.gitlab.jfronny.muscript.test;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import static io.gitlab.jfronny.muscript.test.MuTestUtil.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class ObjectTest {
|
||||
@Test
|
||||
void valueAccess() {
|
||||
assertTrue(bool("boolean"));
|
||||
assertTrue(bool("object.subvalue > 1000"));
|
||||
assertTrue(bool("object.subfunc(2, 4, 8) == 12"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void arrayAccess() {
|
||||
assertTrue(bool("object[1] == 'One'"));
|
||||
assertTrue(bool("object['subvalue'] == 1024"));
|
||||
assertTrue(bool("object[object2.valuename] == 1024"));
|
||||
assertTrue(bool("object2['sub'].val == 10"));
|
||||
assertTrue(bool("object2.sub['val'] == 10"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package io.gitlab.jfronny.muscript.test;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.*;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import static io.gitlab.jfronny.muscript.test.MuTestUtil.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class StringTest {
|
||||
@Test
|
||||
void operators() {
|
||||
assertEquals("Hello, world!", string("'Hello, ' || 'world!'"));
|
||||
assertEquals("Yes 15 hello", string("'Yes ' || 16 - 1 || ' hello'"));
|
||||
assertEquals("Value", string("string"));
|
||||
assertTrue(bool("string == 'Value'"));
|
||||
assertFalse(bool("string == 'Something else'"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidCode() {
|
||||
assertThrows(Parser.ParseException.class, () -> Parser.parse("15 + true"));
|
||||
assertThrows(Parser.ParseException.class, () -> Parser.parse("15 + true"));
|
||||
assertThrows(Parser.ParseException.class, () -> Parser.parse("string = 'Value'"));
|
||||
}
|
||||
}
|
|
@ -2,4 +2,5 @@ rootProject.name = 'Commons'
|
|||
|
||||
include 'commons-gson'
|
||||
include 'commons-slf4j'
|
||||
include 'muscript'
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package io.gitlab.jfronny.commons;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class StringFormatter {
|
||||
public static String toString(Object o) {
|
||||
if (o == null) return "null";
|
||||
else if (o instanceof Double d) {
|
||||
if (d % 1.0 != 0)
|
||||
return String.format(Locale.US, "%s", d);
|
||||
else
|
||||
return String.format(Locale.US, "%.0f", d);
|
||||
} else return o.toString();
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package io.gitlab.jfronny.commons.log;
|
||||
|
||||
import io.gitlab.jfronny.commons.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
|
@ -85,13 +86,13 @@ public interface Logger {
|
|||
}
|
||||
|
||||
default String format(String format, Object arg) {
|
||||
return format.replaceFirst("\\{}", Objects.toString(arg));
|
||||
return format.replaceFirst("\\{}", StringFormatter.toString(arg));
|
||||
}
|
||||
|
||||
default String format(String format, Object... args) {
|
||||
if (args == null || format == null) return format;
|
||||
for (Object arg : args) {
|
||||
format = format.replaceFirst("\\{}", Objects.toString(arg));
|
||||
format = format.replaceFirst("\\{}", StringFormatter.toString(arg));
|
||||
}
|
||||
return format;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
package io.gitlab.jfronny.commons.test;
|
||||
|
||||
import io.gitlab.jfronny.commons.log.JavaUtilLogger;
|
||||
import io.gitlab.jfronny.commons.log.Logger;
|
||||
import io.gitlab.jfronny.commons.log.NopLogger;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import io.gitlab.jfronny.commons.log.*;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
|
|
Loading…
Reference in New Issue