muScript: document custom objects
ci/woodpecker/push/woodpecker Pipeline was successful Details

This commit is contained in:
Johannes Frohnmeyer 2023-03-12 16:16:27 +01:00
parent efb1512a60
commit a2ce8d8f64
Signed by: Johannes
GPG Key ID: E76429612C2929F4
13 changed files with 109 additions and 56 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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