muScript: document custom objects
ci/woodpecker/push/woodpecker Pipeline was successful
Details
ci/woodpecker/push/woodpecker Pipeline was successful
Details
This commit is contained in:
parent
efb1512a60
commit
a2ce8d8f64
|
@ -33,8 +33,7 @@ public class Assign extends DynamicExpr {
|
|||
|
||||
@Override
|
||||
public void decompile(ExprWriter writer) throws IOException {
|
||||
if (!Lexer.isValidId(name)) throw new IllegalArgumentException("Not a valid variable name: " + name);
|
||||
writer.append(name).append(" = ");
|
||||
writer.appendLiteral(name).append(" = ");
|
||||
value.decompile(writer);
|
||||
}
|
||||
|
||||
|
|
|
@ -63,9 +63,7 @@ public class Closure extends DynamicExpr {
|
|||
public void decompile(ExprWriter writer) throws IOException {
|
||||
writer.append("{ ");
|
||||
for (int i = 0; i < boundArgs.size(); i++) {
|
||||
String arg = boundArgs.get(i);
|
||||
if (!Lexer.isValidId(arg)) throw new IllegalArgumentException("Not a valid argument name: " + arg);
|
||||
writer.append(arg);
|
||||
writer.appendLiteral(boundArgs.get(i));
|
||||
if (i == boundArgs.size() - 1 && variadic) writer.append("...");
|
||||
writer.append(' ');
|
||||
}
|
||||
|
|
|
@ -46,10 +46,9 @@ public class ObjectLiteral extends DynamicExpr {
|
|||
writer.increaseIndent().append("{\n");
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, DynamicExpr> entry : content.entrySet()) {
|
||||
if (!Lexer.isValidId(entry.getKey())) throw new IllegalStateException("Illegal key: " + entry.getKey());
|
||||
if (!first) writer.append(",\n");
|
||||
first = false;
|
||||
writer.append(entry.getKey()).append(" = ");
|
||||
writer.appendLiteral(entry.getKey()).append(" = ");
|
||||
entry.getValue().decompile(writer);
|
||||
}
|
||||
writer.decreaseIndent().append("\n}");
|
||||
|
|
|
@ -34,8 +34,7 @@ public class Variable extends DynamicExpr {
|
|||
|
||||
@Override
|
||||
public void decompile(ExprWriter writer) throws IOException {
|
||||
if (!Lexer.isValidId(name)) throw new IllegalArgumentException("Not a valid variable name: " + name);
|
||||
writer.append(name);
|
||||
writer.appendLiteral(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -30,6 +30,13 @@ public class ExprWriter implements Appendable, Closeable {
|
|||
return this;
|
||||
}
|
||||
|
||||
public ExprWriter appendLiteral(String s) throws IOException {
|
||||
if (!Lexer.isValidId(s)) {
|
||||
if (s.contains("`")) throw new IllegalArgumentException("Not a valid literal: " + s);
|
||||
else return append('`').append(s).append('`');
|
||||
} else return append(s);
|
||||
}
|
||||
|
||||
private String indent() {
|
||||
return " ".repeat(indent);
|
||||
}
|
||||
|
|
|
@ -44,11 +44,13 @@ public class Lexer {
|
|||
char c = advance();
|
||||
|
||||
if (isDigit(c)) number();
|
||||
else if (isIdentifier(c)) identifier();
|
||||
else if (isIdentifier(c)) identifier(false);
|
||||
else {
|
||||
switch (c) {
|
||||
case '\'', '"' -> string(c);
|
||||
|
||||
case '`' -> identifier(true);
|
||||
|
||||
case '=' -> {
|
||||
if (match('=')) createToken(Token.EqualEqual);
|
||||
else createToken(Token.Assign);
|
||||
|
@ -99,7 +101,7 @@ public class Lexer {
|
|||
}
|
||||
|
||||
if (isAtEnd()) {
|
||||
createToken(Token.Error, "Unterminated expression.");
|
||||
createToken(Token.Error, "Unterminated expression");
|
||||
} else {
|
||||
advance();
|
||||
createToken(Token.String, source.substring(start + 1, current - 1));
|
||||
|
@ -118,20 +120,32 @@ public class Lexer {
|
|||
createToken(Token.Number);
|
||||
}
|
||||
|
||||
private void identifier() {
|
||||
while (!isAtEnd()) {
|
||||
char c = peek();
|
||||
if (isIdentifier(c) || isDigit(c)) {
|
||||
private void identifier(boolean explicit) {
|
||||
if (explicit) {
|
||||
while (!isAtEnd() && peek() != '`') {
|
||||
advance();
|
||||
} else break;
|
||||
}
|
||||
}
|
||||
if (isAtEnd()) {
|
||||
createToken(Token.Error, "Unterminated expression");
|
||||
} else {
|
||||
advance();
|
||||
createToken(Token.Identifier, source.substring(start + 1, current - 1));
|
||||
}
|
||||
} else {
|
||||
while (!isAtEnd()) {
|
||||
char c = peek();
|
||||
if (isIdentifier(c) || isDigit(c)) {
|
||||
advance();
|
||||
} else break;
|
||||
}
|
||||
|
||||
createToken(Token.Identifier);
|
||||
createToken(Token.Identifier);
|
||||
|
||||
switch (lexeme) {
|
||||
case "null" -> token = Token.Null;
|
||||
case "true" -> token = Token.True;
|
||||
case "false" -> token = Token.False;
|
||||
switch (lexeme) {
|
||||
case "null" -> token = Token.Null;
|
||||
case "true" -> token = Token.True;
|
||||
case "false" -> token = Token.False;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ public class Parser {
|
|||
int start = previous.start;
|
||||
int end = previous.current - 1;
|
||||
Expr<?> trueExpr = expression();
|
||||
consume(Token.Colon, "Expected ':' after first part of condition.");
|
||||
consume(Token.Colon, "Expected ':' after first part of condition");
|
||||
Expr<?> falseExpr = expression();
|
||||
expr = new UnresolvedConditional(start, end, asBool(expr), trueExpr, falseExpr);
|
||||
}
|
||||
|
@ -242,7 +242,7 @@ public class Parser {
|
|||
expr = switch (previous.token) {
|
||||
case LeftParen -> finishCall(start, end, expr);
|
||||
case Dot -> {
|
||||
TokenData name = consume(Token.Identifier, "Expected field name after '.'.");
|
||||
TokenData name = consume(Token.Identifier, "Expected field name after '.'");
|
||||
yield new Get(start, end, asDynamic(expr), Expr.literal(name.start, name.current - 1, name.lexeme));
|
||||
}
|
||||
case DoubleColon -> {
|
||||
|
@ -251,7 +251,7 @@ public class Parser {
|
|||
callable = new Variable(previous.start, previous.current - 1, previous.lexeme);
|
||||
} else if (match(Token.LeftParen)) {
|
||||
callable = expression().asDynamicExpr();
|
||||
consume(Token.RightParen, "Expected ')' after expression.");
|
||||
consume(Token.RightParen, "Expected ')' after expression");
|
||||
} else throw error("Bind operator requires right side to be a literal identifier or to be wrapped in parentheses.");
|
||||
yield new Bind(start, end, callable, expr.asDynamicExpr());
|
||||
}
|
||||
|
@ -276,7 +276,7 @@ public class Parser {
|
|||
} while (match(Token.Comma));
|
||||
}
|
||||
|
||||
consume(Token.RightParen, "Expected ')' after function arguments.");
|
||||
consume(Token.RightParen, "Expected ')' after function arguments");
|
||||
return new Call(start, end, asDynamic(callee), args);
|
||||
}
|
||||
|
||||
|
@ -295,7 +295,7 @@ public class Parser {
|
|||
|
||||
if (match(Token.LeftParen)) {
|
||||
Expr<?> expr = expression();
|
||||
consume(Token.RightParen, "Expected ')' after expression.");
|
||||
consume(Token.RightParen, "Expected ')' after expression");
|
||||
return expr;
|
||||
}
|
||||
|
||||
|
@ -404,7 +404,7 @@ public class Parser {
|
|||
|
||||
private TokenData consume(Token token, String message) {
|
||||
if (check(token)) return advance();
|
||||
throw error(message);
|
||||
throw error(message + " but got " + current.token);
|
||||
}
|
||||
|
||||
private boolean match(Token... tokens) {
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package io.gitlab.jfronny.muscript.data.dynamic;
|
||||
|
||||
import io.gitlab.jfronny.muscript.compiler.ExprWriter;
|
||||
import io.gitlab.jfronny.muscript.compiler.Lexer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
@ -20,10 +19,9 @@ public non-sealed interface DObject extends Dynamic<Map<String, Dynamic<?>>> {
|
|||
writer.append('{');
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, Dynamic<?>> entry : getValue().entrySet()) {
|
||||
if (!Lexer.isValidId(entry.getKey())) throw new IllegalStateException("Illegal key: " + entry.getKey());
|
||||
if (!first) writer.append(", ");
|
||||
first = false;
|
||||
writer.append(entry.getKey()).append(" = ");
|
||||
writer.appendLiteral(entry.getKey()).append(" = ");
|
||||
entry.getValue().serialize(writer);
|
||||
}
|
||||
writer.append('}');
|
||||
|
|
|
@ -26,20 +26,22 @@ public record LocationalError(String message, Location start, @Nullable Location
|
|||
if (lineEnd == -1) lineEnd = source.length();
|
||||
|
||||
int lineIndex = lineStart > 0 ? (int) source.substring(0, lineStart).chars().filter(c -> c == '\n').count() : 0;
|
||||
int column = index - lineStart;
|
||||
int column = index - lineStart - 1;
|
||||
|
||||
return new Location(source.substring(lineStart + 1, lineEnd), column, lineIndex + 1);
|
||||
String line = lineStart == source.length() - 1 ? "" : source.substring(lineStart + 1, lineEnd);
|
||||
|
||||
return new Location(line, column, lineIndex + 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (column > line.length()) return "character " + (column + 1) + " of line " + row;
|
||||
else return "'" + line.charAt(column - 1) + "' (character " + (column + 1) + ")";
|
||||
if (column >= line.length()) return "character " + (column + 1) + " of line " + row;
|
||||
else return "'" + line.charAt(column) + "' (character " + (column + 1) + ")";
|
||||
}
|
||||
|
||||
public String prettyPrint() {
|
||||
String linePrefix = String.format("%1$6d", row) + " | ";
|
||||
return linePrefix + line + "\n" + " ".repeat(linePrefix.length() + column - 1) + "^-- ";
|
||||
return linePrefix + line + "\n" + " ".repeat(linePrefix.length() + column) + "^-- ";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,7 +89,7 @@ public record LocationalError(String message, Location start, @Nullable Location
|
|||
String linePrefix = String.format("%1$6d", start.row) + " | ";
|
||||
return "Error at " + start + ": " + message + "\n"
|
||||
+ linePrefix + start.line + "\n"
|
||||
+ " ".repeat(linePrefix.length() + start.column - 1) + "^"
|
||||
+ " ".repeat(linePrefix.length() + start.column) + "^"
|
||||
+ "-".repeat(end.column - start.column - 1)
|
||||
+ "^-- Here";
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.gitlab.jfronny.muscript.compiler.Parser;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.makeArgs;
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class CommentTest {
|
||||
|
@ -30,4 +31,14 @@ class CommentTest {
|
|||
n
|
||||
""").run(makeArgs()).asNumber().getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
void commentWithStuff() {
|
||||
assertDoesNotThrow(() -> Parser.parseScript("""
|
||||
ob = {
|
||||
k1 = 'Yes',
|
||||
`1` = "One"
|
||||
}
|
||||
""").run(makeArgs()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,16 @@ class LocationalErrorTest {
|
|||
@Test
|
||||
void invalidCode() {
|
||||
assertEquals("""
|
||||
Error at 't' (character 7): Expected Number but got Boolean
|
||||
Error at 't' (character 6): Expected Number but got Boolean
|
||||
1 | 15 + true
|
||||
^--^-- Here""",
|
||||
assertThrows(Parser.ParseException.class, () -> Parser.parse("15 + true")).error.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidCode2() {
|
||||
assertEquals("""
|
||||
Error at ''' (character 7): Expected Number but got String
|
||||
Error at ''' (character 6): Expected Number but got String
|
||||
1 | 15 + 'yes'
|
||||
^---^-- Here""",
|
||||
assertThrows(Parser.ParseException.class, () -> Parser.parse("15 + 'yes'")).error.toString());
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
package io.gitlab.jfronny.muscript.test;
|
||||
|
||||
import io.gitlab.jfronny.commons.StringFormatter;
|
||||
import io.gitlab.jfronny.muscript.StandardLib;
|
||||
import io.gitlab.jfronny.muscript.compiler.Parser;
|
||||
import io.gitlab.jfronny.muscript.error.LocationalException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.function.Executable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
import static io.gitlab.jfronny.muscript.test.util.MuTestUtil.makeArgs;
|
||||
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class ValidExampleTest {
|
||||
|
@ -20,6 +22,7 @@ class ValidExampleTest {
|
|||
source = new String(is.readAllBytes());
|
||||
}
|
||||
String[] split = source.lines().toArray(String[]::new);
|
||||
List<Executable> testCases = new LinkedList<>();
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
String s = split[i];
|
||||
if (s.equals("```mu")) {
|
||||
|
@ -37,18 +40,27 @@ class ValidExampleTest {
|
|||
i++;
|
||||
final String block = blockBuilder.substring(1);
|
||||
final String expectedResult = resultBuilder.substring(1);
|
||||
String result = null;
|
||||
try {
|
||||
result = Parser.parseScript(block).asExpr().asStringExpr().get(makeArgs());
|
||||
} catch (Throwable t) {
|
||||
assertEquals(expectedResult, StringFormatter.toString(t, e ->
|
||||
e instanceof LocationalException le
|
||||
? le.asPrintable(block).toString()
|
||||
: e.toString()
|
||||
));
|
||||
}
|
||||
if (result != null) assertEquals(expectedResult, result);
|
||||
testCases.add(new TestCase(block, expectedResult));
|
||||
}
|
||||
}
|
||||
|
||||
assertAll(testCases);
|
||||
}
|
||||
|
||||
record TestCase(String source, String expectedResult) implements Executable {
|
||||
@Override
|
||||
public void execute() {
|
||||
String result = null;
|
||||
try {
|
||||
result = Parser.parseScript(source).asExpr().asStringExpr().get(StandardLib.createScope());
|
||||
} catch (Throwable t) {
|
||||
assertEquals(expectedResult, StringFormatter.toString(t, e ->
|
||||
e instanceof LocationalException le
|
||||
? le.asPrintable(source).toString()
|
||||
: e.toString()
|
||||
));
|
||||
}
|
||||
if (result != null) assertEquals(expectedResult, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,10 +104,7 @@ Result:
|
|||
|
||||
## Objects
|
||||
|
||||
You cannot create your own objects, but objects may be passed to your script from functions or as parameters.
|
||||
You can read (but not write) their fields via `.` or `[]`.
|
||||
|
||||
In the following example, the objects `object` and `object2` is passed to the script.
|
||||
You can read (but not write) the fields of objects via `.` or `[]`.
|
||||
|
||||
The StdLib also contains two objects, namely `date` and `time` which allow reading the current date and time.
|
||||
They also allow creating date/time objects and comparing them.
|
||||
|
@ -117,6 +114,19 @@ They also allow creating date/time objects and comparing them.
|
|||
<br>
|
||||
|
||||
```mu
|
||||
object2 = {
|
||||
valuename = 'subvalue',
|
||||
sub = {
|
||||
val = 10
|
||||
},
|
||||
stringfunc = {text -> text}
|
||||
}
|
||||
|
||||
object = {
|
||||
subvalue = 1024,
|
||||
`1` = "One" // you can escape any identifier (not just object keys) with '`'
|
||||
}
|
||||
|
||||
listOf(
|
||||
object2.valuename, // This is how you would normally do this
|
||||
object2['valuename'], // You can also use []
|
||||
|
|
Loading…
Reference in New Issue