diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Assign.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Assign.java index fd3f46d..ca9fcfb 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Assign.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Assign.java @@ -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); } diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Closure.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Closure.java index 8f2af5a..f631fc5 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Closure.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Closure.java @@ -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(' '); } diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/ObjectLiteral.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/ObjectLiteral.java index db2bbe2..1b323a7 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/ObjectLiteral.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/ObjectLiteral.java @@ -46,10 +46,9 @@ public class ObjectLiteral extends DynamicExpr { writer.increaseIndent().append("{\n"); boolean first = true; for (Map.Entry 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}"); diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Variable.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Variable.java index e4f538a..de7a73f 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Variable.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Variable.java @@ -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 diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/ExprWriter.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/ExprWriter.java index c2b00e2..131eae4 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/ExprWriter.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/ExprWriter.java @@ -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); } diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Lexer.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Lexer.java index 3329283..5126b49 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Lexer.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Lexer.java @@ -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; + } } } diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Parser.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Parser.java index adf49b8..ec3c333 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Parser.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Parser.java @@ -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) { diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/DObject.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/DObject.java index e94b6ef..211610b 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/DObject.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/DObject.java @@ -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>> { writer.append('{'); boolean first = true; for (Map.Entry> 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('}'); diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalError.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalError.java index 8a441c5..e83d643 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalError.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalError.java @@ -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"; } diff --git a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/CommentTest.java b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/CommentTest.java index 91934de..eca9de4 100644 --- a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/CommentTest.java +++ b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/CommentTest.java @@ -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())); + } } diff --git a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/LocationalErrorTest.java b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/LocationalErrorTest.java index 26424ab..21aa4d4 100644 --- a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/LocationalErrorTest.java +++ b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/LocationalErrorTest.java @@ -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()); diff --git a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java index 3945bfe..9593b13 100644 --- a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java +++ b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/ValidExampleTest.java @@ -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 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); + } } } diff --git a/muscript/src/test/resources/example.md b/muscript/src/test/resources/example.md index c091d7d..cccc5d8 100644 --- a/muscript/src/test/resources/example.md +++ b/muscript/src/test/resources/example.md @@ -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.
```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 []