[muscript] First iteration, seems to work

This commit is contained in:
Johannes Frohnmeyer 2022-06-03 19:54:31 +02:00
parent 03a121357c
commit a4e9a3425e
Signed by: Johannes
GPG Key ID: E76429612C2929F4
43 changed files with 1660 additions and 7 deletions

55
muscript/README.md Normal file
View File

@ -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

13
muscript/build.gradle Normal file
View File

@ -0,0 +1,13 @@
dependencies {
}
publishing {
publications {
mavenJava(MavenPublication) {
groupId = 'io.gitlab.jfronny'
artifactId = 'muscript'
from components.java
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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 == '$';
}
}

View File

@ -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;
}
}
}

View File

@ -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
}

View File

@ -0,0 +1,5 @@
package io.gitlab.jfronny.muscript.compiler;
public enum Type {
String, Number, Boolean, Object
}

View File

@ -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)));
}
}

View File

@ -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();
};
}
}

View File

@ -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);
}
}

View File

@ -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);
}
};
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();
};
}
}

View File

@ -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();
};
}
}

View File

@ -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);
}
}

View File

@ -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");
}
}

View File

@ -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 + " ");
}
}
}

View File

@ -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");
}
}

View File

@ -0,0 +1,4 @@
package io.gitlab.jfronny.muscript.optic;
public interface OBool extends OAny<Boolean> {
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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>";
}
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,8 @@
package io.gitlab.jfronny.muscript.optic;
public final class ONull implements OAny<Object> {
@Override
public Object getValue() {
return null;
}
}

View File

@ -0,0 +1,4 @@
package io.gitlab.jfronny.muscript.optic;
public interface ONumber extends OAny<Double> {
}

View File

@ -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);
}
}

View File

@ -0,0 +1,4 @@
package io.gitlab.jfronny.muscript.optic;
public interface OString extends OAny<String> {
}

View File

@ -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"));
}
}

View File

@ -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)"));
}
}

View File

@ -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"));
}
}

View File

@ -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'"));
}
}

View File

@ -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())
));
}
}

View File

@ -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"));
}
}

View File

@ -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"));
}
}

View File

@ -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'"));
}
}

View File

@ -2,4 +2,5 @@ rootProject.name = 'Commons'
include 'commons-gson'
include 'commons-slf4j'
include 'muscript'

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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.*;