├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src ├── main └── java │ └── org │ └── meteordev │ └── starscript │ ├── Instruction.java │ ├── Script.java │ ├── Section.java │ ├── StandardLib.java │ ├── Starscript.java │ ├── compiler │ ├── Compiler.java │ ├── Expr.java │ ├── Lexer.java │ ├── Parser.java │ └── Token.java │ ├── utils │ ├── AbstractExprVisitor.java │ ├── CompletionCallback.java │ ├── Error.java │ ├── SFunction.java │ ├── SemanticToken.java │ ├── SemanticTokenProvider.java │ ├── SemanticTokenType.java │ ├── Stack.java │ ├── StarscriptError.java │ └── VariableReplacementTransformer.java │ └── value │ ├── Value.java │ ├── ValueMap.java │ └── ValueType.java └── test └── java └── org └── meteordev └── starscript ├── Benchmark.java └── Main.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | build -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Meteor Development 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Starscript 2 | Fast text formatting language for Java. 3 | 4 | - Lightweight with no dependencies 5 | - Faster than `String.format` (See [Benchmark](https://github.com/MeteorDevelopment/starscript/blob/master/src/test/java/org/meteordev/starscript/Benchmark.java)) 6 | - Standard operators + - * / % ^ 7 | - Ability to call functions defined in java 8 | - Variables can be different each time they are used 9 | - Conditional output (ternary operator) 10 | - Variables can be maps 11 | 12 | ## Examples 13 | - `Hello {name}!` 14 | - `Number: {someNumber * 100}` 15 | - `FPS: {round(fps)}` 16 | - `Today is a {good ? 'good' : 'bad'} day` 17 | - `Name: {player.name}` 18 | 19 | ## Usage 20 | Gradle: 21 | ```groovy 22 | repositories { 23 | maven { 24 | name = "meteor-maven" 25 | url = "https://maven.meteordev.org/releases" 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation "meteordevelopment:starscript:0.2.2" 31 | } 32 | ``` 33 | 34 | Java: 35 | ```java 36 | // Parse 37 | Parser.Result result = Parser.parse("Hello {name}!"); 38 | 39 | // Check for errors 40 | if (result.hasErrors()) { 41 | for (Error error : result.errors) System.out.println(error); 42 | return; 43 | } 44 | 45 | // Compile 46 | Script script = Compiler.compile(result); 47 | 48 | // Create starscript instance 49 | Starscript ss = new Starscript(); 50 | StandardLib.init(ss); // Adds a few default functions, not required 51 | 52 | ss.set("name", "MineGame159"); 53 | // ss.set("name", () -> Value.string("MineGame159")); 54 | 55 | // Run 56 | System.out.println(ss.run(script)); // Hello MineGame159! 57 | ``` 58 | 59 | ## Documentation 60 | Full syntax and features can be found on [wiki](https://github.com/MeteorDevelopment/starscript/wiki). 61 | Javadocs can be found [here](https://javadoc.jitpack.io/com/github/MeteorDevelopment/starscript). 62 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "java" 3 | id "maven-publish" 4 | } 5 | 6 | group "org.meteordev" 7 | version "0.2.2" 8 | 9 | sourceCompatibility = targetCompatibility = JavaVersion.VERSION_1_8 10 | 11 | java { 12 | sourceCompatibility = targetCompatibility = JavaVersion.VERSION_1_8 13 | 14 | withSourcesJar() 15 | withJavadocJar() 16 | } 17 | 18 | repositories { 19 | mavenCentral() 20 | } 21 | 22 | dependencies { 23 | testImplementation "org.openjdk.jmh:jmh-core:1.36" 24 | testImplementation "org.openjdk.jmh:jmh-generator-annprocess:1.36" 25 | 26 | testAnnotationProcessor "org.openjdk.jmh:jmh-generator-annprocess:1.36" 27 | } 28 | 29 | compileJava { 30 | options.encoding = "UTF-8" 31 | } 32 | 33 | compileTestJava { 34 | options.encoding = "UTF-8" 35 | } 36 | 37 | javadoc { 38 | options.addStringOption('Xdoclint:none', '-quiet') 39 | } 40 | 41 | publishing { 42 | publications { 43 | java(MavenPublication) { 44 | from components.java 45 | } 46 | } 47 | 48 | repositories { 49 | maven { 50 | name = "meteor-maven" 51 | url = "https://maven.meteordev.org/releases" 52 | 53 | credentials { 54 | username = System.getenv("MAVEN_METEOR_ALIAS") 55 | password = System.getenv("MAVEN_METEOR_TOKEN") 56 | } 57 | 58 | authentication { 59 | basic(BasicAuthentication) 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MeteorDevelopment/starscript/248e5b9e1fb98e6532788c6f83e75c22a31a1cf0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'starscript' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/Instruction.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript; 2 | 3 | /** Instructions used by {@link Starscript}. */ 4 | public enum Instruction { 5 | Constant, 6 | Null, 7 | True, 8 | False, 9 | 10 | Add, 11 | Subtract, 12 | Multiply, 13 | Divide, 14 | Modulo, 15 | Power, 16 | 17 | AddConstant, 18 | 19 | Pop, 20 | Not, 21 | Negate, 22 | 23 | Equals, 24 | NotEquals, 25 | Greater, 26 | GreaterEqual, 27 | Less, 28 | LessEqual, 29 | 30 | Variable, 31 | Get, 32 | Call, 33 | 34 | Jump, 35 | JumpIfTrue, 36 | JumpIfFalse, 37 | 38 | Section, 39 | 40 | Append, 41 | ConstantAppend, 42 | VariableAppend, 43 | GetAppend, 44 | CallAppend, 45 | 46 | VariableGet, 47 | VariableGetAppend, 48 | 49 | End; 50 | 51 | private static final Instruction[] values = values(); 52 | 53 | public static Instruction valueOf(int i) { 54 | return values[i]; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/Script.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript; 2 | 3 | import org.meteordev.starscript.value.Value; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /** Compiled representation of starscript code that can be run inside {@link Starscript}. */ 10 | public class Script { 11 | public byte[] code = new byte[8]; 12 | private int size; 13 | 14 | public final List constants = new ArrayList<>(); 15 | 16 | private void write(int b) { 17 | if (size >= code.length) { 18 | byte[] newCode = new byte[code.length * 2]; 19 | System.arraycopy(code, 0, newCode, 0, code.length); 20 | code = newCode; 21 | } 22 | 23 | code[size++] = (byte) b; 24 | } 25 | 26 | /** Writes instruction to this script. */ 27 | public void write(Instruction insn) { 28 | write(insn.ordinal()); 29 | } 30 | 31 | /** Writes instruction with an additional byte to this script. */ 32 | public void write(Instruction insn, int b) { 33 | write(insn.ordinal()); 34 | write(b); 35 | } 36 | 37 | /** Writes instruction with an additional constant value to this script. */ 38 | public void write(Instruction insn, Value constant) { 39 | write(insn.ordinal()); 40 | writeConstant(constant); 41 | } 42 | 43 | /** Writes constant value to this script. */ 44 | public void writeConstant(Value constant) { 45 | int constantI = -1; 46 | 47 | for (int i = 0; i < constants.size(); i++) { 48 | if (constants.get(i).equals(constant)) { 49 | constantI = i; 50 | break; 51 | } 52 | } 53 | 54 | if (constantI == -1) { 55 | constantI = constants.size(); 56 | constants.add(constant); 57 | } 58 | 59 | write(constantI); 60 | } 61 | 62 | /** Begins a jump instruction. */ 63 | public int writeJump(Instruction insn) { 64 | write(insn); 65 | write(0); 66 | write(0); 67 | 68 | return size - 2; 69 | } 70 | 71 | /** Ends a jump instruction. */ 72 | public void patchJump(int offset) { 73 | int jump = size - offset - 2; 74 | 75 | code[offset] = (byte) ((jump >> 8) & 0xFF); 76 | code[offset + 1] = (byte) (jump & 0xFF); 77 | } 78 | 79 | /** Returns the number of bytes inside {@link #code}. */ 80 | public int getSize() { 81 | return size; 82 | } 83 | 84 | // Decompilation 85 | 86 | /** Decompiles this script and writes it to the {@link Appendable} argument. */ 87 | public void decompile(Appendable out) { 88 | try { 89 | for (int i = 0; i < size; i++) { 90 | Instruction insn = Instruction.valueOf(code[i]); 91 | out.append(String.format("%3d %-18s", i, insn)); 92 | 93 | switch (insn) { 94 | case AddConstant: 95 | case Variable: 96 | case VariableAppend: 97 | case Get: 98 | case GetAppend: 99 | case Constant: 100 | case ConstantAppend: i++; out.append(String.format("%3d '%s'", code[i], constants.get(code[i] & 0xFF))); break; 101 | case Call: 102 | case CallAppend: i++; out.append(String.format("%3d %s", code[i], code[i] == 1 ? "argument" : "arguments")); break; 103 | case Jump: 104 | case JumpIfTrue: 105 | case JumpIfFalse: i += 2; out.append(String.format("%3d -> %d", i - 2, i + 1 + (((code[i - 1] << 8) & 0xFF) | (code[i] & 0xFF)))); break; 106 | case Section: i++; out.append(String.format("%3d", code[i])); break; 107 | case VariableGet: 108 | case VariableGetAppend: i += 2; out.append(String.format("%3d.%-3d '%s.%s'", code[i - 1], code[i], constants.get(code[i - 1] & 0xFF), constants.get(code[i] & 0xFF))); break; 109 | } 110 | 111 | out.append('\n'); 112 | } 113 | } catch (IOException e) { 114 | throw new RuntimeException(e); 115 | } 116 | } 117 | 118 | /** Decompiles this script and writes it to {@link System#out}. */ 119 | public void decompile() { 120 | decompile(System.out); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/Section.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript; 2 | 3 | public class Section { 4 | private static final ThreadLocal SB = ThreadLocal.withInitial(StringBuilder::new); 5 | 6 | public final int index; 7 | public final String text; 8 | 9 | public Section next; 10 | 11 | public Section(int index, String text) { 12 | this.index = index; 13 | this.text = text; 14 | } 15 | 16 | @Override 17 | public String toString() { 18 | StringBuilder sb = SB.get(); 19 | sb.setLength(0); 20 | 21 | Section s = this; 22 | while (s != null) { 23 | sb.append(s.text); 24 | s = s.next; 25 | } 26 | 27 | return sb.toString(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/StandardLib.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript; 2 | 3 | import org.meteordev.starscript.value.Value; 4 | 5 | import java.text.SimpleDateFormat; 6 | import java.util.Date; 7 | import java.util.Random; 8 | 9 | /** Standard library with some default functions and variables. */ 10 | public class StandardLib { 11 | private static final Random rand = new Random(); 12 | 13 | public static final SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm"); 14 | public static final SimpleDateFormat dateFormat = new SimpleDateFormat("dd. MM. yyyy"); 15 | 16 | /** Adds the functions and variables to the provided {@link Starscript} instance. */ 17 | public static void init(Starscript ss) { 18 | // Variables 19 | ss.set("PI", Math.PI); 20 | ss.set("time", () -> Value.string(timeFormat.format(new Date()))); 21 | ss.set("date", () -> Value.string(dateFormat.format(new Date()))); 22 | 23 | // Numbers 24 | ss.set("round", StandardLib::round); 25 | ss.set("roundToString", StandardLib::roundToString); 26 | ss.set("floor", StandardLib::floor); 27 | ss.set("ceil", StandardLib::ceil); 28 | ss.set("abs", StandardLib::abs); 29 | ss.set("random", StandardLib::random); 30 | 31 | // Strings 32 | ss.set("string", StandardLib::string); 33 | ss.set("toUpper", StandardLib::toUpper); 34 | ss.set("toLower", StandardLib::toLower); 35 | ss.set("contains", StandardLib::contains); 36 | ss.set("replace", StandardLib::replace); 37 | ss.set("pad", StandardLib::pad); 38 | } 39 | 40 | // Numbers 41 | 42 | public static Value round(Starscript ss, int argCount) { 43 | if (argCount == 1) { 44 | double a = ss.popNumber("Argument to round() needs to be a number."); 45 | return Value.number(Math.round(a)); 46 | } 47 | else if (argCount == 2) { 48 | double b = ss.popNumber("Second argument to round() needs to be a number."); 49 | double a = ss.popNumber("First argument to round() needs to be a number."); 50 | 51 | double x = Math.pow(10, (int) b); 52 | return Value.number(Math.round(a * x) / x); 53 | } 54 | else { 55 | ss.error("round() requires 1 or 2 arguments, got %d.", argCount); 56 | return null; 57 | } 58 | } 59 | 60 | public static Value roundToString(Starscript ss, int argCount) { 61 | if (argCount == 1) { 62 | double a = ss.popNumber("Argument to round() needs to be a number."); 63 | return Value.string(Double.toString(Math.round(a))); 64 | } 65 | else if (argCount == 2) { 66 | double b = ss.popNumber("Second argument to round() needs to be a number."); 67 | double a = ss.popNumber("First argument to round() needs to be a number."); 68 | 69 | double x = Math.pow(10, (int) b); 70 | return Value.string(Double.toString(Math.round(a * x) / x)); 71 | } 72 | else { 73 | ss.error("round() requires 1 or 2 arguments, got %d.", argCount); 74 | return null; 75 | } 76 | } 77 | 78 | public static Value floor(Starscript ss, int argCount) { 79 | if (argCount != 1) ss.error("floor() requires 1 argument, got %d.", argCount); 80 | double a = ss.popNumber("Argument to floor() needs to be a number."); 81 | return Value.number(Math.floor(a)); 82 | } 83 | 84 | public static Value ceil(Starscript ss, int argCount) { 85 | if (argCount != 1) ss.error("ceil() requires 1 argument, got %d.", argCount); 86 | double a = ss.popNumber("Argument to ceil() needs to be a number."); 87 | return Value.number(Math.ceil(a)); 88 | } 89 | 90 | public static Value abs(Starscript ss, int argCount) { 91 | if (argCount != 1) ss.error("abs() requires 1 argument, got %d.", argCount); 92 | double a = ss.popNumber("Argument to abs() needs to be a number."); 93 | return Value.number(Math.abs(a)); 94 | } 95 | 96 | public static Value random(Starscript ss, int argCount) { 97 | if (argCount == 0) return Value.number(rand.nextDouble()); 98 | else if (argCount == 2) { 99 | double max = ss.popNumber("Second argument to random() needs to be a number."); 100 | double min = ss.popNumber("First argument to random() needs to be a number."); 101 | 102 | return Value.number(min + (max - min) * rand.nextDouble()); 103 | } 104 | 105 | ss.error("random() requires 0 or 2 arguments, got %d.", argCount); 106 | return Value.null_(); 107 | } 108 | 109 | // Strings 110 | 111 | private static Value string(Starscript ss, int argCount) { 112 | if (argCount != 1) ss.error("string() requires 1 argument, got %d.", argCount); 113 | return Value.string(ss.pop().toString()); 114 | } 115 | 116 | public static Value toUpper(Starscript ss, int argCount) { 117 | if (argCount != 1) ss.error("toUpper() requires 1 argument, got %d.", argCount); 118 | String a = ss.popString("Argument to toUpper() needs to be a string."); 119 | return Value.string(a.toUpperCase()); 120 | } 121 | 122 | public static Value toLower(Starscript ss, int argCount) { 123 | if (argCount != 1) ss.error("toLower() requires 1 argument, got %d.", argCount); 124 | String a = ss.popString("Argument to toLower() needs to be a string."); 125 | return Value.string(a.toLowerCase()); 126 | } 127 | 128 | public static Value contains(Starscript ss, int argCount) { 129 | if (argCount != 2) ss.error("replace() requires 2 arguments, got %d.", argCount); 130 | 131 | String search = ss.popString("Second argument to contains() needs to be a string."); 132 | String string = ss.popString("First argument to contains() needs to be a string."); 133 | 134 | return Value.bool(string.contains(search)); 135 | } 136 | 137 | public static Value replace(Starscript ss, int argCount) { 138 | if (argCount != 3) ss.error("replace() requires 3 arguments, got %d.", argCount); 139 | 140 | String to = ss.popString("Third argument to replace() needs to be a string."); 141 | String from = ss.popString("Second argument to replace() needs to be a string."); 142 | String string = ss.popString("First argument to replace() needs to be a string."); 143 | 144 | return Value.string(string.replace(from, to)); 145 | } 146 | 147 | public static Value pad(Starscript ss, int argCount) { 148 | if (argCount != 2) ss.error("pad() requires 2 arguments, got %d.", argCount); 149 | 150 | int width = (int) ss.popNumber("Second argument to pad() needs to be a number."); 151 | String text = ss.pop().toString(); 152 | 153 | if (text.length() >= Math.abs(width)) return Value.string(text); 154 | 155 | char[] padded = new char[Math.max(text.length(), Math.abs(width))]; 156 | 157 | if (width >= 0) { 158 | int padLength = width - text.length(); 159 | for (int i = 0; i < padLength; i++) padded[i] = ' '; 160 | for (int i = 0; i < text.length(); i++) padded[padLength + i] = text.charAt(i); 161 | } 162 | else { 163 | for (int i = 0; i < text.length(); i++) padded[i] = text.charAt(i); 164 | for (int i = 0; i < Math.abs(width) - text.length(); i++) padded[text.length() + i] = ' '; 165 | } 166 | 167 | return Value.string(new String(padded)); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/Starscript.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript; 2 | 3 | import org.meteordev.starscript.compiler.Expr; 4 | import org.meteordev.starscript.compiler.Parser; 5 | import org.meteordev.starscript.utils.Error; 6 | import org.meteordev.starscript.utils.*; 7 | import org.meteordev.starscript.value.Value; 8 | import org.meteordev.starscript.value.ValueMap; 9 | 10 | import java.util.function.Supplier; 11 | 12 | /** A VM (virtual machine) that can run compiled starscript code, {@link Script}. */ 13 | public class Starscript { 14 | private final ValueMap globals; 15 | 16 | private final Stack stack = new Stack<>(); 17 | 18 | public Starscript() { 19 | globals = new ValueMap(); 20 | } 21 | 22 | /** Creates a new Starscript instance with shared globals ({@link #getGlobals()}) from the parent instance. */ 23 | public Starscript(Starscript parent) { 24 | globals = parent.globals; 25 | } 26 | 27 | /** Runs the script and fills the provided {@link StringBuilder}. Throws {@link StarscriptError} if a runtime error happens. */ 28 | public Section run(Script script, StringBuilder sb) { 29 | stack.clear(); 30 | 31 | sb.setLength(0); 32 | int ip = 0; 33 | 34 | Section firstSection = null; 35 | Section section = null; 36 | int index = 0; 37 | 38 | loop: 39 | while (true) { 40 | switch (Instruction.valueOf(script.code[ip++])) { 41 | case Constant: push(script.constants.get(script.code[ip++] & 0xFF)); break; 42 | case Null: push(Value.null_()); break; 43 | case True: push(Value.bool(true)); break; 44 | case False: push(Value.bool(false)); break; 45 | 46 | case Add: { Value b = pop(); Value a = pop(); if (a.isNumber() && b.isNumber()) push(Value.number(a.getNumber() + b.getNumber())); else if (a.isString()) push(Value.string(a.getString() + b.toString())); else error("Can only add 2 numbers or 1 string and other value."); break; } 47 | case Subtract: { Value b = pop(); Value a = pop(); if (a.isNumber() && b.isNumber()) push(Value.number(a.getNumber() - b.getNumber())); else error("Can only subtract 2 numbers."); break; } 48 | case Multiply: { Value b = pop(); Value a = pop(); if (a.isNumber() && b.isNumber()) push(Value.number(a.getNumber() * b.getNumber())); else error("Can only multiply 2 numbers."); break; } 49 | case Divide: { Value b = pop(); Value a = pop(); if (a.isNumber() && b.isNumber()) push(Value.number(a.getNumber() / b.getNumber())); else error("Can only divide 2 numbers."); break; } 50 | case Modulo: { Value b = pop(); Value a = pop(); if (a.isNumber() && b.isNumber()) push(Value.number(a.getNumber() % b.getNumber())); else error("Can only modulo 2 numbers."); break; } 51 | case Power: { Value b = pop(); Value a = pop(); if (a.isNumber() && b.isNumber()) push(Value.number(Math.pow(a.getNumber(), b.getNumber()))); else error("Can only power 2 numbers."); break; } 52 | 53 | case AddConstant: { Value b = script.constants.get(script.code[ip++] & 0xFF); Value a = pop(); if (a.isNumber() && b.isNumber()) push(Value.number(a.getNumber() + b.getNumber())); else if (a.isString()) push(Value.string(a.getString() + b.toString())); else error("Can only add 2 numbers or 1 string and other value."); break; } 54 | 55 | case Pop: pop(); break; 56 | case Not: push(Value.bool(!pop().isTruthy())); break; 57 | case Negate: { Value a = pop(); if (a.isNumber()) push(Value.number(-a.getNumber())); else error("This operation requires a number."); break; } 58 | 59 | case Equals: push(Value.bool(pop().equals(pop()))); break; 60 | case NotEquals: push(Value.bool(!pop().equals(pop()))); break; 61 | case Greater: { Value b = pop(); Value a = pop(); if (a.isNumber() && b.isNumber()) push(Value.bool(a.getNumber() > b.getNumber())); else error("This operation requires 2 number."); break; } 62 | case GreaterEqual: { Value b = pop(); Value a = pop(); if (a.isNumber() && b.isNumber()) push(Value.bool(a.getNumber() >= b.getNumber())); else error("This operation requires 2 number."); break; } 63 | case Less: { Value b = pop(); Value a = pop(); if (a.isNumber() && b.isNumber()) push(Value.bool(a.getNumber() < b.getNumber())); else error("This operation requires 2 number."); break; } 64 | case LessEqual: { Value b = pop(); Value a = pop(); if (a.isNumber() && b.isNumber()) push(Value.bool(a.getNumber() <= b.getNumber())); else error("This operation requires 2 number."); break; } 65 | 66 | case Variable: { String name = script.constants.get(script.code[ip++] & 0xFF).getString(); Supplier s = globals.getRaw(name); push(s != null ? s.get() : Value.null_()); break; } 67 | case Get: { String name = script.constants.get(script.code[ip++] & 0xFF).getString(); Value v = pop(); if (!v.isMap()) { push(Value.null_()); break; } Supplier s = v.getMap().getRaw(name); push(s != null ? s.get() : Value.null_()); break; } 68 | case Call: { int argCount = script.code[ip++]; Value a = peek(argCount); if (a.isFunction()) { Value r = a.getFunction().run(this, argCount); pop(); push(r); } else error("Tried to call a %s, can only call functions.", a.type); break; } 69 | 70 | case Jump: { int jump = ((script.code[ip++] & 0xFF) << 8) | (script.code[ip++] & 0xFF); ip += jump; break; } 71 | case JumpIfTrue: { int jump = ((script.code[ip++] & 0xFF) << 8) | (script.code[ip++] & 0xFF); if (peek().isTruthy()) ip += jump; break; } 72 | case JumpIfFalse: { int jump = ((script.code[ip++] & 0xFF) << 8) | (script.code[ip++] & 0xFF); if (!peek().isTruthy()) ip += jump; break; } 73 | 74 | case Section: if (firstSection == null) { firstSection = new Section(index, sb.toString()); section = firstSection; } else { section.next = new Section(index, sb.toString()); section = section.next; } sb.setLength(0); index = script.code[ip++]; break; 75 | 76 | case Append: sb.append(pop().toString()); break; 77 | case ConstantAppend: sb.append(script.constants.get(script.code[ip++] & 0xFF).toString()); break; 78 | case VariableAppend: { Supplier s = globals.getRaw(script.constants.get(script.code[ip++] & 0xFF).getString()); sb.append((s == null ? Value.null_() : s.get()).toString()); break; } 79 | case GetAppend: { String name = script.constants.get(script.code[ip++] & 0xFF).getString(); Value v = pop(); if (!v.isMap()) { sb.append(Value.null_()); break; } Supplier s = v.getMap().getRaw(name); sb.append((s != null ? s.get() : Value.null_()).toString()); break; } 80 | case CallAppend: { int argCount = script.code[ip++]; Value a = peek(argCount); if (a.isFunction()) { Value r = a.getFunction().run(this, argCount); pop(); sb.append(r.toString()); } else error("Tried to call a %s, can only call functions.", a.type); break; } 81 | 82 | case VariableGet: { 83 | Value v; 84 | { String name = script.constants.get(script.code[ip++] & 0xFF).getString(); Supplier s = globals.getRaw(name); v = s != null ? s.get() : Value.null_(); } // Variable 85 | { String name = script.constants.get(script.code[ip++] & 0xFF).getString(); if (!v.isMap()) { push(Value.null_()); break; } Supplier s = v.getMap().getRaw(name); push(s != null ? s.get() : Value.null_()); } // Get 86 | break; 87 | } 88 | case VariableGetAppend: { 89 | Value v; 90 | { String name = script.constants.get(script.code[ip++] & 0xFF).getString(); Supplier s = globals.getRaw(name); v = s != null ? s.get() : Value.null_(); } // Variable 91 | { String name = script.constants.get(script.code[ip++] & 0xFF).getString(); if (!v.isMap()) { push(Value.null_()); break; } Supplier s = v.getMap().getRaw(name); v = s != null ? s.get() : Value.null_(); } // Get 92 | { sb.append(v.toString()); } // Append 93 | break; 94 | } 95 | 96 | case End: break loop; 97 | default: throw new UnsupportedOperationException("Unknown instruction '" + Instruction.valueOf(script.code[ip]) + "'"); 98 | } 99 | } 100 | 101 | if (firstSection != null) { 102 | section.next = new Section(index, sb.toString()); 103 | return firstSection; 104 | } 105 | 106 | return new Section(index, sb.toString()); 107 | } 108 | 109 | /** Runs the script. Throws {@link StarscriptError} if a runtime error happens. */ 110 | public Section run(Script script) { 111 | return run(script, new StringBuilder()); 112 | } 113 | 114 | // Stack manipulation 115 | 116 | /** Pushes a new value on the stack. */ 117 | public void push(Value value) { 118 | stack.push(value); 119 | } 120 | 121 | /** Pops a new value from the stack. */ 122 | public Value pop() { 123 | return stack.pop(); 124 | } 125 | 126 | /** Returns a value from the stack without removing it. */ 127 | public Value peek() { 128 | return stack.peek(); 129 | } 130 | 131 | /** Returns a value from the stack with an offset without removing it. */ 132 | public Value peek(int offset) { 133 | return stack.peek(offset); 134 | } 135 | 136 | /** Pops a value from the stack and returns it as boolean. Calls {@link Starscript#error(String, Object...)} with the provided message if the value is not boolean. */ 137 | public boolean popBool(String errorMsg) { 138 | Value a = pop(); 139 | if (!a.isBool()) error(errorMsg); 140 | return a.getBool(); 141 | } 142 | 143 | /** Pops a value from the stack and returns it as double. Calls {@link Starscript#error(String, Object...)} with the provided message if the value is not double. */ 144 | public double popNumber(String errorMsg) { 145 | Value a = pop(); 146 | if (!a.isNumber()) error(errorMsg); 147 | return a.getNumber(); 148 | } 149 | 150 | /** Pops a value from the stack and returns it as String. Calls {@link Starscript#error(String, Object...)} with the provided message if the value is not String. */ 151 | public String popString(String errorMsg) { 152 | Value a = pop(); 153 | if (!a.isString()) error(errorMsg); 154 | return a.getString(); 155 | } 156 | 157 | /** Pops a value from the stack and returns it as Object. Calls {@link Starscript#error(String, Object...)} with the provided message if the value is not Object. */ 158 | public Object popObject(String errorMsg) { 159 | Value a = pop(); 160 | if (!a.isObject()) error(errorMsg); 161 | return a.getObject(); 162 | } 163 | 164 | // Helpers 165 | 166 | /** Throws a {@link StarscriptError}. */ 167 | public void error(String format, Object... args) { 168 | throw new StarscriptError(String.format(format, args)); 169 | } 170 | 171 | // Globals 172 | 173 | /** Sets a variable supplier for the provided name.

See {@link ValueMap#set(String, Supplier)} for dot notation. */ 174 | public ValueMap set(String name, Supplier supplier) { 175 | return globals.set(name, supplier); 176 | } 177 | 178 | /** Sets a variable supplier that always returns the same value for the provided name.

See {@link ValueMap#set(String, Supplier)} for dot notation. */ 179 | public ValueMap set(String name, Value value) { 180 | return globals.set(name, value); 181 | } 182 | 183 | /** Sets a boolean variable supplier that always returns the same value for the provided name.

See {@link ValueMap#set(String, Supplier)} for dot notation. */ 184 | public ValueMap set(String name, boolean bool) { 185 | return globals.set(name, bool); 186 | } 187 | 188 | /** Sets a number variable supplier that always returns the same value for the provided name.

See {@link ValueMap#set(String, Supplier)} for dot notation. */ 189 | public ValueMap set(String name, double number) { 190 | return globals.set(name, number); 191 | } 192 | 193 | /** Sets a string variable supplier that always returns the same value for the provided name.

See {@link ValueMap#set(String, Supplier)} for dot notation. */ 194 | public ValueMap set(String name, String string) { 195 | return globals.set(name, string); 196 | } 197 | 198 | /** Sets a function variable supplier that always returns the same value for the provided name.

See {@link ValueMap#set(String, Supplier)} for dot notation. */ 199 | public ValueMap set(String name, SFunction function) { 200 | return globals.set(name, function); 201 | } 202 | 203 | /** Sets a map variable supplier that always returns the same value for the provided name.

See {@link ValueMap#set(String, Supplier)} for dot notation. */ 204 | public ValueMap set(String name, ValueMap map) { 205 | return globals.set(name, map); 206 | } 207 | 208 | /** Sets an object variable supplier that always returns the same value for the provided name.

See {@link ValueMap#set(String, Supplier)} for dot notation. */ 209 | public ValueMap set(String name, Object object) { 210 | return globals.set(name, object); 211 | } 212 | 213 | /** Removes all values from the globals. */ 214 | public void clear() { 215 | globals.clear(); 216 | } 217 | 218 | /** Removes a single value with the specified name from the globals and returns the removed value.

See {@link ValueMap#remove(String)} for dot notation. */ 219 | public Supplier remove(String name) { 220 | return globals.remove(name); 221 | } 222 | 223 | /** Returns the underlying {@link ValueMap} for global variables. */ 224 | public ValueMap getGlobals() { 225 | return globals; 226 | } 227 | 228 | // Completions 229 | 230 | /** Calls the provided callback for every completion that is able to be resolved from global variables. */ 231 | public void getCompletions(String source, int position, CompletionCallback callback) { 232 | Parser.Result result = Parser.parse(source); 233 | 234 | for (Expr expr : result.exprs) { 235 | completionsExpr(source, position, expr, callback); 236 | } 237 | 238 | for (Error error : result.errors) { 239 | if (error.expr != null) completionsExpr(source, position, error.expr, callback); 240 | } 241 | } 242 | 243 | private void completionsExpr(String source, int position, Expr expr, CompletionCallback callback) { 244 | if (position < expr.start || (position > expr.end && position != source.length())) return; 245 | 246 | if (expr instanceof Expr.Variable) { 247 | Expr.Variable var = (Expr.Variable) expr; 248 | String start = source.substring(var.start, position); 249 | 250 | for (String key : globals.keys()) { 251 | if (!key.startsWith("_") && key.startsWith(start)) callback.onCompletion(key, globals.getRaw(key).get().isFunction()); 252 | } 253 | } 254 | else if (expr instanceof Expr.Get) { 255 | Expr.Get get = (Expr.Get) expr; 256 | 257 | if (position >= get.end - get.name.length()) { 258 | Value value = resolveExpr(get.getObject()); 259 | 260 | if (value != null && value.isMap()) { 261 | String start = source.substring(get.getObject().end + 1, position); 262 | 263 | for (String key : value.getMap().keys()) { 264 | if (!key.startsWith("_") && key.startsWith(start)) callback.onCompletion(key, value.getMap().getRaw(key).get().isFunction()); 265 | } 266 | } 267 | } 268 | else { 269 | for (Expr child : expr.children) completionsExpr(source, position, child, callback); 270 | } 271 | } 272 | else if (expr instanceof Expr.Block) { 273 | if (((Expr.Block) expr).getExpr() == null) { 274 | for (String key : globals.keys()) { 275 | if (!key.startsWith("_")) callback.onCompletion(key, globals.getRaw(key).get().isFunction()); 276 | } 277 | } 278 | else { 279 | for (Expr child : expr.children) completionsExpr(source, position, child, callback); 280 | } 281 | } 282 | else { 283 | for (Expr child : expr.children) completionsExpr(source, position, child, callback); 284 | } 285 | } 286 | 287 | private Value resolveExpr(Expr expr) { 288 | if (expr instanceof Expr.Variable) { 289 | Supplier supplier = globals.getRaw(((Expr.Variable) expr).name); 290 | return supplier != null ? supplier.get() : null; 291 | } 292 | else if (expr instanceof Expr.Get) { 293 | Value value = resolveExpr(((Expr.Get) expr).getObject()); 294 | if (value == null || !value.isMap()) return null; 295 | 296 | Supplier supplier = value.getMap().getRaw(((Expr.Get) expr).name); 297 | return supplier != null ? supplier.get() : null; 298 | } 299 | 300 | return null; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/compiler/Compiler.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.compiler; 2 | 3 | import org.meteordev.starscript.Instruction; 4 | import org.meteordev.starscript.Script; 5 | import org.meteordev.starscript.Starscript; 6 | import org.meteordev.starscript.value.Value; 7 | 8 | /** Compiler that produces compiled starscript code from {@link Parser.Result}. */ 9 | public class Compiler implements Expr.Visitor { 10 | private final Script script = new Script(); 11 | 12 | private int blockDepth; 13 | 14 | private boolean constantAppend; 15 | private boolean variableAppend; 16 | private boolean getAppend; 17 | private boolean callAppend; 18 | 19 | private Compiler() {} 20 | 21 | /** Produces compiled {@link Script} from {@link Parser.Result} that can be run inside {@link Starscript}. */ 22 | public static Script compile(Parser.Result result) { 23 | Compiler compiler = new Compiler(); 24 | 25 | for (Expr expr : result.exprs) compiler.compile(expr); 26 | compiler.script.write(Instruction.End); 27 | 28 | return compiler.script; 29 | } 30 | 31 | // Expressions 32 | 33 | @Override 34 | public void visitNull(Expr.Null expr) { 35 | script.write(Instruction.Null); 36 | } 37 | 38 | @Override 39 | public void visitString(Expr.String expr) { 40 | script.write((blockDepth == 0 || constantAppend) ? Instruction.ConstantAppend : Instruction.Constant, Value.string(expr.string)); 41 | } 42 | 43 | @Override 44 | public void visitNumber(Expr.Number expr) { 45 | script.write(Instruction.Constant, Value.number(expr.number)); 46 | } 47 | 48 | @Override 49 | public void visitBool(Expr.Bool expr) { 50 | script.write(expr.bool ? Instruction.True : Instruction.False); 51 | } 52 | 53 | @Override 54 | public void visitBlock(Expr.Block expr) { 55 | blockDepth++; 56 | 57 | if (expr.getExpr() instanceof Expr.String) constantAppend = true; 58 | else if (expr.getExpr() instanceof Expr.Variable) variableAppend = true; 59 | else if (expr.getExpr() instanceof Expr.Get) getAppend = true; 60 | else if (expr.getExpr() instanceof Expr.Call) callAppend = true; 61 | 62 | compile(expr.getExpr()); 63 | 64 | if (!constantAppend && !variableAppend && !getAppend && !callAppend) script.write(Instruction.Append); 65 | else { 66 | constantAppend = false; 67 | variableAppend = false; 68 | getAppend = false; 69 | callAppend = false; 70 | } 71 | 72 | blockDepth--; 73 | } 74 | 75 | @Override 76 | public void visitGroup(Expr.Group expr) { 77 | compile(expr.getExpr()); 78 | } 79 | 80 | @Override 81 | public void visitBinary(Expr.Binary expr) { 82 | compile(expr.getLeft()); 83 | 84 | if (expr.op == Token.Plus && (expr.getRight() instanceof Expr.String || expr.getRight() instanceof Expr.Number)) { 85 | script.write(Instruction.AddConstant, expr.getRight() instanceof Expr.String ? Value.string(((Expr.String) expr.getRight()).string) : Value.number(((Expr.Number) expr.getRight()).number)); 86 | return; 87 | } 88 | else compile(expr.getRight()); 89 | 90 | switch (expr.op) { 91 | case Plus: script.write(Instruction.Add); break; 92 | case Minus: script.write(Instruction.Subtract); break; 93 | case Star: script.write(Instruction.Multiply); break; 94 | case Slash: script.write(Instruction.Divide); break; 95 | case Percentage: script.write(Instruction.Modulo); break; 96 | case UpArrow: script.write(Instruction.Power); break; 97 | 98 | case EqualEqual: script.write(Instruction.Equals); break; 99 | case BangEqual: script.write(Instruction.NotEquals); break; 100 | case Greater: script.write(Instruction.Greater); break; 101 | case GreaterEqual: script.write(Instruction.GreaterEqual); break; 102 | case Less: script.write(Instruction.Less); break; 103 | case LessEqual: script.write(Instruction.LessEqual); break; 104 | } 105 | } 106 | 107 | @Override 108 | public void visitUnary(Expr.Unary expr) { 109 | compile(expr.getRight()); 110 | 111 | if (expr.op == Token.Bang) script.write(Instruction.Not); 112 | else if (expr.op == Token.Minus) script.write(Instruction.Negate); 113 | } 114 | 115 | @Override 116 | public void visitVariable(Expr.Variable expr) { 117 | script.write(variableAppend ? Instruction.VariableAppend : Instruction.Variable, Value.string(expr.name)); 118 | } 119 | 120 | @Override 121 | public void visitGet(Expr.Get expr) { 122 | boolean prevGetAppend = getAppend; 123 | getAppend = false; 124 | 125 | boolean variableGet = expr.getObject() instanceof Expr.Variable; 126 | if (!variableGet) compile(expr.getObject()); 127 | 128 | getAppend = prevGetAppend; 129 | 130 | if (variableGet) { 131 | script.write(getAppend ? Instruction.VariableGetAppend : Instruction.VariableGet, Value.string(((Expr.Variable) expr.getObject()).name)); 132 | script.writeConstant(Value.string(expr.name)); 133 | } 134 | else script.write(getAppend ? Instruction.GetAppend : Instruction.Get, Value.string(expr.name)); 135 | } 136 | 137 | @Override 138 | public void visitCall(Expr.Call expr) { 139 | boolean prevCallAppend = callAppend; 140 | compile(expr.getCallee()); 141 | 142 | callAppend = false; 143 | for (int i = 0; i < expr.getArgCount(); i++) compile(expr.getArg(i)); 144 | 145 | callAppend = prevCallAppend; 146 | script.write(callAppend ? Instruction.CallAppend : Instruction.Call, expr.getArgCount()); 147 | } 148 | 149 | @Override 150 | public void visitLogical(Expr.Logical expr) { 151 | compile(expr.getLeft()); 152 | int endJump = script.writeJump(expr.op == Token.And ? Instruction.JumpIfFalse : Instruction.JumpIfTrue); 153 | 154 | script.write(Instruction.Pop); 155 | compile(expr.getRight()); 156 | 157 | script.patchJump(endJump); 158 | } 159 | 160 | @Override 161 | public void visitConditional(Expr.Conditional expr) { 162 | compile(expr.getCondition()); 163 | int falseJump = script.writeJump(Instruction.JumpIfFalse); 164 | 165 | script.write(Instruction.Pop); 166 | compile(expr.getTrueExpr()); 167 | int endJump = script.writeJump(Instruction.Jump); 168 | 169 | script.patchJump(falseJump); 170 | script.write(Instruction.Pop); 171 | compile(expr.getFalseExpr()); 172 | 173 | script.patchJump(endJump); 174 | } 175 | 176 | @Override 177 | public void visitSection(Expr.Section expr) { 178 | script.write(Instruction.Section, expr.index); 179 | compile(expr.getExpr()); 180 | } 181 | 182 | // Helpers 183 | 184 | private void compile(Expr expr) { 185 | if (expr != null) expr.accept(this); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/compiler/Expr.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.compiler; 2 | 3 | import java.util.List; 4 | 5 | /** Expressions that form the AST (abstract syntax tree) of parsed starscript code. */ 6 | public abstract class Expr { 7 | private static final Expr[] EMPTY_CHILDREN = new Expr[0]; 8 | 9 | public interface Visitor { 10 | void visitNull(Null expr); 11 | void visitString(String expr); 12 | void visitNumber(Number expr); 13 | void visitBool(Bool expr); 14 | void visitBlock(Block expr); 15 | void visitGroup(Group expr); 16 | void visitBinary(Binary expr); 17 | void visitUnary(Unary expr); 18 | void visitVariable(Variable expr); 19 | void visitGet(Get expr); 20 | void visitCall(Call expr); 21 | void visitLogical(Logical expr); 22 | void visitConditional(Conditional expr); 23 | void visitSection(Section expr); 24 | } 25 | 26 | public final int start, end; 27 | 28 | public Expr parent; 29 | public final Expr[] children; 30 | 31 | public Expr(int start, int end, Expr[] children) { 32 | this.start = start; 33 | this.end = end; 34 | this.children = children; 35 | 36 | for (Expr child : this.children) { 37 | child.parent = this; 38 | } 39 | } 40 | 41 | public Expr(int start, int end) { 42 | this(start, end, EMPTY_CHILDREN); 43 | } 44 | 45 | public abstract void accept(Visitor visitor); 46 | 47 | public java.lang.String getSource(java.lang.String source) { 48 | return source.substring(start, end); 49 | } 50 | 51 | public void replaceChild(Expr toReplace, Expr replacement) { 52 | for (int i = 0; i < children.length; i++) { 53 | if (children[i] != toReplace) continue; 54 | 55 | children[i] = replacement; 56 | toReplace.parent = null; 57 | replacement.parent = this; 58 | 59 | break; 60 | } 61 | } 62 | 63 | public void replace(Expr replacement) { 64 | parent.replaceChild(this, replacement); 65 | } 66 | 67 | public static class Null extends Expr { 68 | public Null(int start, int end) { 69 | super(start, end); 70 | } 71 | 72 | @Override 73 | public void accept(Visitor visitor) { 74 | visitor.visitNull(this); 75 | } 76 | } 77 | 78 | public static class String extends Expr { 79 | public final java.lang.String string; 80 | 81 | public String(int start, int end, java.lang.String string) { 82 | super(start, end); 83 | 84 | this.string = string; 85 | } 86 | 87 | @Override 88 | public void accept(Visitor visitor) { 89 | visitor.visitString(this); 90 | } 91 | } 92 | 93 | public static class Number extends Expr { 94 | public final double number; 95 | 96 | public Number(int start, int end, double number) { 97 | super(start, end); 98 | 99 | this.number = number; 100 | } 101 | 102 | @Override 103 | public void accept(Visitor visitor) { 104 | visitor.visitNumber(this); 105 | } 106 | } 107 | 108 | public static class Bool extends Expr { 109 | public final boolean bool; 110 | 111 | public Bool(int start, int end, boolean bool) { 112 | super(start, end); 113 | 114 | this.bool = bool; 115 | } 116 | 117 | @Override 118 | public void accept(Visitor visitor) { 119 | visitor.visitBool(this); 120 | } 121 | } 122 | 123 | public static class Block extends Expr { 124 | public Block(int start, int end, Expr expr) { 125 | super(start, end, expr != null ? new Expr[] { expr } : EMPTY_CHILDREN); 126 | } 127 | 128 | @Override 129 | public void accept(Visitor visitor) { 130 | visitor.visitBlock(this); 131 | } 132 | 133 | public Expr getExpr() { 134 | return children[0]; 135 | } 136 | } 137 | 138 | public static class Group extends Expr { 139 | public Group(int start, int end, Expr expr) { 140 | super(start, end, new Expr[] { expr }); 141 | } 142 | 143 | @Override 144 | public void accept(Visitor visitor) { 145 | visitor.visitGroup(this); 146 | } 147 | 148 | public Expr getExpr() { 149 | return children[0]; 150 | } 151 | } 152 | 153 | public static class Binary extends Expr { 154 | public final Token op; 155 | 156 | public Binary(int start, int end, Expr left, Token op, Expr right) { 157 | super(start, end, new Expr[] { left, right }); 158 | 159 | this.op = op; 160 | } 161 | 162 | @Override 163 | public void accept(Visitor visitor) { 164 | visitor.visitBinary(this); 165 | } 166 | 167 | public Expr getLeft() { 168 | return children[0]; 169 | } 170 | 171 | public Expr getRight() { 172 | return children[1]; 173 | } 174 | } 175 | 176 | public static class Unary extends Expr { 177 | public final Token op; 178 | 179 | public Unary(int start, int end, Token op, Expr right) { 180 | super(start, end, new Expr[] { right }); 181 | 182 | this.op = op; 183 | } 184 | 185 | @Override 186 | public void accept(Visitor visitor) { 187 | visitor.visitUnary(this); 188 | } 189 | 190 | public Expr getRight() { 191 | return children[0]; 192 | } 193 | } 194 | 195 | public static class Variable extends Expr { 196 | public final java.lang.String name; 197 | 198 | public Variable(int start, int end, java.lang.String name) { 199 | super(start, end); 200 | 201 | this.name = name; 202 | } 203 | 204 | @Override 205 | public void accept(Visitor visitor) { 206 | visitor.visitVariable(this); 207 | } 208 | } 209 | 210 | public static class Get extends Expr { 211 | public final java.lang.String name; 212 | 213 | public Get(int start, int end, Expr object, java.lang.String name) { 214 | super(start, end, new Expr[] { object }); 215 | 216 | this.name = name; 217 | } 218 | 219 | @Override 220 | public void accept(Visitor visitor) { 221 | visitor.visitGet(this); 222 | } 223 | 224 | public Expr getObject() { 225 | return children[0]; 226 | } 227 | } 228 | 229 | public static class Call extends Expr { 230 | public Call(int start, int end, Expr callee, List args) { 231 | super(start, end, combine(callee, args)); 232 | } 233 | 234 | @Override 235 | public void accept(Visitor visitor) { 236 | visitor.visitCall(this); 237 | } 238 | 239 | public Expr getCallee() { 240 | return children[0]; 241 | } 242 | 243 | public int getArgCount() { 244 | return children.length - 1; 245 | } 246 | 247 | public Expr getArg(int i) { 248 | return children[i + 1]; 249 | } 250 | 251 | private static Expr[] combine(Expr callee, List args) { 252 | Expr[] exprs = new Expr[args.size() + 1]; 253 | 254 | exprs[0] = callee; 255 | for (int i = 0; i < args.size(); i++) exprs[i + 1] = args.get(i); 256 | 257 | return exprs; 258 | } 259 | } 260 | 261 | public static class Logical extends Expr { 262 | public final Token op; 263 | 264 | public Logical(int start, int end, Expr left, Token op, Expr right) { 265 | super(start, end, new Expr[] { left, right }); 266 | 267 | this.op = op; 268 | } 269 | 270 | @Override 271 | public void accept(Visitor visitor) { 272 | visitor.visitLogical(this); 273 | } 274 | 275 | public Expr getLeft() { 276 | return children[0]; 277 | } 278 | 279 | public Expr getRight() { 280 | return children[1]; 281 | } 282 | } 283 | 284 | public static class Conditional extends Expr { 285 | public Conditional(int start, int end, Expr condition, Expr trueExpr, Expr falseExpr) { 286 | super(start, end, new Expr[] { condition, trueExpr, falseExpr }); 287 | } 288 | 289 | @Override 290 | public void accept(Visitor visitor) { 291 | visitor.visitConditional(this); 292 | } 293 | 294 | public Expr getCondition() { 295 | return children[0]; 296 | } 297 | 298 | public Expr getTrueExpr() { 299 | return children[1]; 300 | } 301 | 302 | public Expr getFalseExpr() { 303 | return children[2]; 304 | } 305 | } 306 | 307 | public static class Section extends Expr { 308 | public final int index; 309 | 310 | public Section(int start, int end, int index, Expr expr) { 311 | super(start, end, new Expr[] { expr }); 312 | 313 | this.index = index; 314 | } 315 | 316 | @Override 317 | public void accept(Visitor visitor) { 318 | visitor.visitSection(this); 319 | } 320 | 321 | public Expr getExpr() { 322 | return children[0]; 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/compiler/Lexer.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.compiler; 2 | 3 | /** Takes starscript source code and produces a stream or tokens that are used for parsing. */ 4 | public class Lexer { 5 | /** The type of the token. */ 6 | public Token token; 7 | /** The string representation of the token. */ 8 | public String lexeme; 9 | 10 | public int line = 1, character = -1; 11 | public char ch; 12 | 13 | private final String source; 14 | private final StringBuilder stringBuilder = new StringBuilder(); 15 | public int start, current; 16 | private int expressionDepth; 17 | 18 | public Lexer(String source) { 19 | this.source = source; 20 | } 21 | 22 | /** Scans for 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. */ 23 | public void next() { 24 | start = current; 25 | 26 | if (isAtEnd()) { 27 | createToken(Token.EOF); 28 | return; 29 | } 30 | 31 | if (expressionDepth > 0) { 32 | // Scan expression 33 | skipWhitespace(); 34 | if (isAtEnd()) { 35 | createToken(Token.EOF); 36 | return; 37 | } 38 | 39 | char c = advance(); 40 | 41 | if (isDigit(c) || (c == '-' && isDigit(peek()))) number(); 42 | else if (isAlpha(c)) identifier(); 43 | else { 44 | switch (c) { 45 | case '\'': 46 | case '"': string(c); break; 47 | 48 | case '=': if (match('=')) createToken(Token.EqualEqual); else unexpected(); break; 49 | case '!': createToken(match('=') ? Token.BangEqual : Token.Bang); break; 50 | case '>': createToken(match('=') ? Token.GreaterEqual : Token.Greater); break; 51 | case '<': createToken(match('=') ? Token.LessEqual : Token.Less); break; 52 | 53 | case '+': createToken(Token.Plus); break; 54 | case '-': createToken(Token.Minus); break; 55 | case '*': createToken(Token.Star); break; 56 | case '/': createToken(Token.Slash); break; 57 | case '%': createToken(Token.Percentage); break; 58 | case '^': createToken(Token.UpArrow); break; 59 | 60 | case '.': createToken(Token.Dot); break; 61 | case ',': createToken(Token.Comma); break; 62 | case '?': createToken(Token.QuestionMark); break; 63 | case ':': createToken(Token.Colon); break; 64 | case '(': createToken(Token.LeftParen); break; 65 | case ')': createToken(Token.RightParen); break; 66 | case '{': expressionDepth++; createToken(Token.LeftBrace); break; 67 | case '}': expressionDepth--; createToken(Token.RightBrace); break; 68 | 69 | case '#': 70 | while (isDigit(peek())) advance(); 71 | createToken(Token.Section, source.substring(start + 1, current)); 72 | break; 73 | 74 | default: unexpected(); 75 | } 76 | } 77 | } 78 | else { 79 | // Scan string, start an expression or section 80 | char c = advance(); 81 | if (c == '\n') line++; 82 | 83 | if (canStartExpression(c, peek())) { 84 | expressionDepth++; 85 | createToken(Token.LeftBrace); 86 | } 87 | else if (canStartSection(c, peek())) { 88 | while (isDigit(peek())) advance(); 89 | createToken(Token.Section, source.substring(start + 1, current)); 90 | } 91 | else { 92 | while (!isAtEnd() && !canStartExpression(peek(), peekNext()) && !canStartSection(peek(), peekNext())) { 93 | if (peek() == '\n') line++; 94 | 95 | char advanced = advance(); 96 | 97 | if ((advanced == '{' && peek() == '{') || (advanced == '#' && peek() == '#')) { 98 | advance(); 99 | } 100 | } 101 | 102 | createToken(Token.String); 103 | } 104 | } 105 | } 106 | 107 | private void string(char delimiter) { 108 | stringBuilder.setLength(0); 109 | 110 | while (!isAtEnd()) { 111 | if (peek() == '\\') { 112 | advance(); 113 | if (isAtEnd()) { 114 | createToken(Token.Error, "Unterminated expression."); 115 | } 116 | } else if (peek() == delimiter) { 117 | break; 118 | } else if (peek() == '\n') { 119 | line++; 120 | } 121 | 122 | stringBuilder.append(advance()); 123 | } 124 | 125 | if (isAtEnd()) { 126 | createToken(Token.Error, "Unterminated expression."); 127 | } 128 | else { 129 | advance(); 130 | createToken(Token.String, stringBuilder.toString()); 131 | } 132 | } 133 | 134 | private void number() { 135 | while (isDigit(peek())) advance(); 136 | 137 | if (peek() == '.' && isDigit(peekNext())) { 138 | advance(); 139 | 140 | while (isDigit(peek())) advance(); 141 | } 142 | 143 | createToken(Token.Number); 144 | } 145 | 146 | private void identifier() { 147 | while (!isAtEnd() && isAlphaNumeric(peek())) advance(); 148 | 149 | createToken(Token.Identifier); 150 | 151 | switch (lexeme) { 152 | case "null": token = Token.Null; break; 153 | case "true": token = Token.True; break; 154 | case "false": token = Token.False; break; 155 | case "and": token = Token.And; break; 156 | case "or": token = Token.Or; break; 157 | } 158 | } 159 | 160 | private boolean canStartExpression(char c1, char c2) { 161 | return c1 == '{' && c2 != '{'; 162 | } 163 | 164 | private boolean canStartSection(char c1, char c2) { 165 | return c1 == '#' && isDigit(c2); 166 | } 167 | 168 | private void skipWhitespace() { 169 | while (true) { 170 | if (isAtEnd()) return; 171 | char c = peek(); 172 | 173 | switch (c) { 174 | case ' ': 175 | case '\r': 176 | case '\t': advance(); break; 177 | case '\n': line++; advance(); break; 178 | default: start = current; return; 179 | } 180 | } 181 | } 182 | 183 | // Helpers 184 | 185 | public boolean isInExpression() { 186 | return expressionDepth > 0; 187 | } 188 | 189 | private void unexpected() { 190 | createToken(Token.Error, "Unexpected character."); 191 | } 192 | 193 | private void createToken(Token token, String lexeme) { 194 | this.token = token; 195 | this.lexeme = lexeme; 196 | } 197 | 198 | private void createToken(Token token) { 199 | createToken(token, source.substring(start, current)); 200 | } 201 | 202 | private boolean match(char expected) { 203 | if (isAtEnd()) return false; 204 | if (source.charAt(current) != expected) return false; 205 | 206 | advance(); 207 | return true; 208 | } 209 | 210 | private char advance() { 211 | character++; 212 | return ch = source.charAt(current++); 213 | } 214 | 215 | private char peek() { 216 | if (isAtEnd()) return '\0'; 217 | return source.charAt(current); 218 | } 219 | 220 | private char peekNext() { 221 | if (current + 1 >= source.length()) return '\0'; 222 | return source.charAt(current + 1); 223 | } 224 | 225 | private boolean isAtEnd() { 226 | return current >= source.length(); 227 | } 228 | 229 | private boolean isDigit(char c) { 230 | return c >= '0' && c <= '9'; 231 | } 232 | 233 | private boolean isAlpha(char c) { 234 | return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; 235 | } 236 | 237 | private boolean isAlphaNumeric(char c) { 238 | return isAlpha(c) || isDigit(c); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/compiler/Parser.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.compiler; 2 | 3 | import org.meteordev.starscript.utils.Error; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** Parser that produces AST (abstract syntax tree) from starscript code and reports errors. */ 9 | public class Parser { 10 | private final Lexer lexer; 11 | 12 | private final TokenData previous = new TokenData(); 13 | private final TokenData current = new TokenData(); 14 | 15 | private int expressionDepth; 16 | 17 | private Parser(String source) { 18 | lexer = new Lexer(source); 19 | } 20 | 21 | private Result parse_() { 22 | Result result = new Result(); 23 | 24 | advance(); 25 | 26 | while (!isAtEnd()) { 27 | try { 28 | result.exprs.add(statement()); 29 | } catch (ParseException e) { 30 | result.errors.add(e.error); 31 | synchronize(); 32 | } 33 | } 34 | 35 | return result; 36 | } 37 | 38 | /** Parses starscript code and returns {@link Result}. */ 39 | public static Result parse(String source) { 40 | return new Parser(source).parse_(); 41 | } 42 | 43 | // Statements 44 | 45 | private Expr statement() { 46 | if (match(Token.Section)) { 47 | if (previous.lexeme.isEmpty()) error("Expected section index.", null); 48 | 49 | int start = previous.start; 50 | 51 | int index = Integer.parseInt(previous.lexeme); 52 | Expr expr = expression(); 53 | expr = new Expr.Section(start, previous.end, index, expr); 54 | 55 | if (index > 255) error("Section index cannot be larger than 255.", expr); 56 | return expr; 57 | } 58 | 59 | return expression(); 60 | } 61 | 62 | // Expressions 63 | 64 | private Expr expression() { 65 | return conditional(); 66 | } 67 | 68 | private Expr conditional() { 69 | int start = previous.start; 70 | Expr expr = and(); 71 | 72 | if (match(Token.QuestionMark)) { 73 | 74 | Expr trueExpr = statement(); 75 | consume(Token.Colon, "Expected ':' after first part of condition.", expr); 76 | Expr falseExpr = statement(); 77 | expr = new Expr.Conditional(start, previous.end, expr, trueExpr, falseExpr); 78 | } 79 | 80 | return expr; 81 | } 82 | 83 | private Expr and() { 84 | Expr expr = or(); 85 | 86 | while (match(Token.And)) { 87 | int start = previous.start; 88 | 89 | Expr right = or(); 90 | expr = new Expr.Logical(start, previous.end, expr, Token.And, right); 91 | } 92 | 93 | return expr; 94 | } 95 | 96 | private Expr or() { 97 | Expr expr = equality(); 98 | 99 | while (match(Token.Or)) { 100 | int start = previous.start; 101 | 102 | Expr right = equality(); 103 | expr = new Expr.Logical(start, previous.end, expr, Token.Or, right); 104 | } 105 | 106 | return expr; 107 | } 108 | 109 | private Expr equality() { 110 | int start = previous.start; 111 | Expr expr = comparison(); 112 | 113 | while (match(Token.EqualEqual, Token.BangEqual)) { 114 | Token op = previous.token; 115 | Expr right = comparison(); 116 | expr = new Expr.Binary(start, previous.end, expr, op, right); 117 | } 118 | 119 | return expr; 120 | } 121 | 122 | private Expr comparison() { 123 | int start = previous.start; 124 | Expr expr = term(); 125 | 126 | while (match(Token.Greater, Token.GreaterEqual, Token.Less, Token.LessEqual)) { 127 | Token op = previous.token; 128 | Expr right = term(); 129 | expr = new Expr.Binary(start, previous.end, expr, op, right); 130 | } 131 | 132 | return expr; 133 | } 134 | 135 | private Expr term() { 136 | int start = previous.start; 137 | Expr expr = factor(); 138 | 139 | while (match(Token.Plus, Token.Minus)) { 140 | Token op = previous.token; 141 | Expr right = factor(); 142 | expr = new Expr.Binary(start, previous.end, expr, op, right); 143 | } 144 | 145 | return expr; 146 | } 147 | 148 | private Expr factor() { 149 | int start = previous.start; 150 | Expr expr = unary(); 151 | 152 | while (match(Token.Star, Token.Slash, Token.Percentage, Token.UpArrow)) { 153 | Token op = previous.token; 154 | Expr right = unary(); 155 | expr = new Expr.Binary(start, previous.end, expr, op, right); 156 | } 157 | 158 | return expr; 159 | } 160 | 161 | private Expr unary() { 162 | if (match(Token.Bang, Token.Minus)) { 163 | int start = previous.start; 164 | 165 | Token op = previous.token; 166 | Expr right = unary(); 167 | return new Expr.Unary(start, previous.end, op, right); 168 | } 169 | 170 | return call(); 171 | } 172 | 173 | private Expr call() { 174 | Expr expr = primary(); 175 | int start = previous.start; 176 | 177 | while (true) { 178 | if (match(Token.LeftParen)) { 179 | expr = finishCall(expr); 180 | } 181 | else if (match(Token.Dot)) { 182 | if (!check(Token.Identifier)) { 183 | expr = new Expr.Get(start, current.end, expr, ""); 184 | } 185 | 186 | TokenData name = consume(Token.Identifier, "Expected field name after '.'.", expr); 187 | expr = new Expr.Get(start, previous.end, expr, name.lexeme); 188 | } 189 | else { 190 | break; 191 | } 192 | } 193 | 194 | return expr; 195 | } 196 | 197 | private Expr finishCall(Expr callee) { 198 | List args = new ArrayList<>(2); 199 | 200 | if (!check(Token.RightParen)) { 201 | do { 202 | args.add(expression()); 203 | } while (match(Token.Comma)); 204 | } 205 | 206 | Expr expr = new Expr.Call(callee.start, previous.end, callee, args); 207 | consume(Token.RightParen, "Expected ')' after function arguments.", expr); 208 | return expr; 209 | } 210 | 211 | private Expr primary() { 212 | if (match(Token.Null)) return new Expr.Null(previous.start, previous.end); 213 | if (match(Token.String)) return new Expr.String(previous.start, previous.end, previous.lexeme); 214 | if (match(Token.True, Token.False)) return new Expr.Bool(previous.start, previous.end, previous.lexeme.equals("true")); 215 | if (match(Token.Number)) return new Expr.Number(previous.start, previous.end, Double.parseDouble(previous.lexeme)); 216 | if (match(Token.Identifier)) return new Expr.Variable(previous.start, previous.end, previous.lexeme); 217 | 218 | if (match(Token.LeftParen)) { 219 | int start = previous.start; 220 | 221 | Expr expr = statement(); 222 | expr = new Expr.Group(start, previous.end, expr); 223 | 224 | consume(Token.RightParen, "Expected ')' after expression.", expr); 225 | return expr; 226 | } 227 | 228 | if (match(Token.LeftBrace)) { 229 | int start = previous.start; 230 | int prevExpressionDepth = expressionDepth; 231 | 232 | expressionDepth++; 233 | Expr expr; 234 | 235 | try { 236 | expr = statement(); 237 | } 238 | catch (ParseException e) { 239 | if (e.error.expr == null) e.error.expr = new Expr.Block(start, previous.end, null); 240 | throw e; 241 | } 242 | 243 | if (prevExpressionDepth == 0) { 244 | expr = new Expr.Block(start, previous.end, expr); 245 | } 246 | 247 | consume(Token.RightBrace, "Expected '}' after expression.", expr); 248 | expressionDepth--; 249 | return expr; 250 | } 251 | 252 | error("Expected expression.", null); 253 | return null; 254 | } 255 | 256 | // Helpers 257 | 258 | private void synchronize() { 259 | while (!isAtEnd()) { 260 | if (match(Token.LeftBrace)) expressionDepth++; 261 | else if (match(Token.RightBrace)) { 262 | expressionDepth--; 263 | if (expressionDepth == 0) return; 264 | } 265 | else advance(); 266 | } 267 | } 268 | 269 | private void error(String message, Expr expr) { 270 | throw new ParseException(new Error(current.line, current.character, current.ch, message, expr)); 271 | } 272 | 273 | private TokenData consume(Token token, String message, Expr expr) { 274 | if (check(token)) return advance(); 275 | error(message, expr); 276 | return null; 277 | } 278 | 279 | private boolean match(Token... tokens) { 280 | for (Token token : tokens) { 281 | if (check(token)) { 282 | advance(); 283 | return true; 284 | } 285 | } 286 | 287 | return false; 288 | } 289 | 290 | private boolean check(Token token) { 291 | if (isAtEnd()) return false; 292 | return current.token == token; 293 | } 294 | 295 | private TokenData advance() { 296 | previous.set(current); 297 | 298 | lexer.next(); 299 | current.set(lexer.token, lexer.lexeme, lexer.start, lexer.current, lexer.line, lexer.character, lexer.ch); 300 | 301 | return previous; 302 | } 303 | 304 | private boolean isAtEnd() { 305 | return current.token == Token.EOF; 306 | } 307 | 308 | // Token data 309 | 310 | private static class TokenData { 311 | public Token token; 312 | public String lexeme; 313 | public int start, end, line, character; 314 | public char ch; 315 | 316 | public void set(Token token, String lexeme, int start, int end, int line, int character, char ch) { 317 | this.token = token; 318 | this.lexeme = lexeme; 319 | this.start = start; 320 | this.end = end; 321 | this.line = line; 322 | this.character = character; 323 | this.ch = ch; 324 | } 325 | 326 | public void set(TokenData data) { 327 | set(data.token, data.lexeme, data.start, data.end, data.line, data.character, data.ch); 328 | } 329 | 330 | @Override 331 | public String toString() { 332 | return String.format("%s '%s'", token, lexeme); 333 | } 334 | } 335 | 336 | // Parse Exception 337 | 338 | private static class ParseException extends RuntimeException { 339 | public final Error error; 340 | 341 | public ParseException(Error error) { 342 | this.error = error; 343 | } 344 | } 345 | 346 | // Result 347 | 348 | /** A class that holds the parsed AST (abstract syntax tree) and any errors that could have been found. */ 349 | public static class Result { 350 | public final List exprs = new ArrayList<>(); 351 | public final List errors = new ArrayList<>(); 352 | 353 | /** Helper method that returns true if there was 1 or more errors. */ 354 | public boolean hasErrors() { 355 | return errors.size() > 0; 356 | } 357 | 358 | public void accept(Expr.Visitor visitor) { 359 | for (Expr expr : exprs) expr.accept(visitor); 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/compiler/Token.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.compiler; 2 | 3 | /** A type of a token produces by {@link Lexer}. */ 4 | public enum Token { 5 | String, Identifier, Number, 6 | 7 | Null, 8 | True, False, 9 | And, Or, 10 | 11 | EqualEqual, BangEqual, 12 | Greater, GreaterEqual, 13 | Less, LessEqual, 14 | 15 | Plus, Minus, 16 | Star, Slash, Percentage, UpArrow, 17 | Bang, 18 | 19 | Dot, Comma, 20 | QuestionMark, Colon, 21 | LeftParen, RightParen, 22 | LeftBrace, RightBrace, 23 | 24 | Section, 25 | 26 | Error, EOF 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/utils/AbstractExprVisitor.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.utils; 2 | 3 | import org.meteordev.starscript.compiler.Expr; 4 | 5 | public abstract class AbstractExprVisitor implements Expr.Visitor { 6 | @Override 7 | public void visitNull(Expr.Null expr) { 8 | for (Expr child : expr.children) child.accept(this); 9 | } 10 | 11 | @Override 12 | public void visitString(Expr.String expr) { 13 | for (Expr child : expr.children) child.accept(this); 14 | } 15 | 16 | @Override 17 | public void visitNumber(Expr.Number expr) { 18 | for (Expr child : expr.children) child.accept(this); 19 | } 20 | 21 | @Override 22 | public void visitBool(Expr.Bool expr) { 23 | for (Expr child : expr.children) child.accept(this); 24 | } 25 | 26 | @Override 27 | public void visitBlock(Expr.Block expr) { 28 | for (Expr child : expr.children) child.accept(this); 29 | } 30 | 31 | @Override 32 | public void visitGroup(Expr.Group expr) { 33 | for (Expr child : expr.children) child.accept(this); 34 | } 35 | 36 | @Override 37 | public void visitBinary(Expr.Binary expr) { 38 | for (Expr child : expr.children) child.accept(this); 39 | } 40 | 41 | @Override 42 | public void visitUnary(Expr.Unary expr) { 43 | for (Expr child : expr.children) child.accept(this); 44 | } 45 | 46 | @Override 47 | public void visitVariable(Expr.Variable expr) { 48 | for (Expr child : expr.children) child.accept(this); 49 | } 50 | 51 | @Override 52 | public void visitGet(Expr.Get expr) { 53 | for (Expr child : expr.children) child.accept(this); 54 | } 55 | 56 | @Override 57 | public void visitCall(Expr.Call expr) { 58 | for (Expr child : expr.children) child.accept(this); 59 | } 60 | 61 | @Override 62 | public void visitLogical(Expr.Logical expr) { 63 | for (Expr child : expr.children) child.accept(this); 64 | } 65 | 66 | @Override 67 | public void visitConditional(Expr.Conditional expr) { 68 | for (Expr child : expr.children) child.accept(this); 69 | } 70 | 71 | @Override 72 | public void visitSection(Expr.Section expr) { 73 | for (Expr child : expr.children) child.accept(this); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/utils/CompletionCallback.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.utils; 2 | 3 | import org.meteordev.starscript.Starscript; 4 | 5 | /** Used in {@link Starscript#getCompletions(String, int, CompletionCallback)}. */ 6 | public interface CompletionCallback { 7 | void onCompletion(String completion, boolean function); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/utils/Error.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.utils; 2 | 3 | import org.meteordev.starscript.compiler.Expr; 4 | 5 | /** Class for storing errors produced while parsing. */ 6 | public class Error { 7 | public final int line; 8 | public final int character; 9 | public final char ch; 10 | public final String message; 11 | public Expr expr; 12 | 13 | public Error(int line, int character, char ch, String message, Expr expr) { 14 | this.line = line; 15 | this.character = character; 16 | this.ch = ch; 17 | this.message = message; 18 | this.expr = expr; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | return String.format("[line %d, character %d] at '%s': %s", line, character, ch, message); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/utils/SFunction.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.utils; 2 | 3 | import org.meteordev.starscript.Starscript; 4 | import org.meteordev.starscript.value.Value; 5 | 6 | /** Interface used for {@link Value#function(SFunction)}. */ 7 | public interface SFunction { 8 | Value run(Starscript ss, int agrCount); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/utils/SemanticToken.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.utils; 2 | 3 | /** A single token containing it's {@link SemanticTokenType type} and position range where it is located in the source string. */ 4 | public class SemanticToken { 5 | public final SemanticTokenType type; 6 | public final int start, end; 7 | 8 | public SemanticToken(SemanticTokenType type, int start, int end) { 9 | this.type = type; 10 | this.start = start; 11 | this.end = end; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/utils/SemanticTokenProvider.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.utils; 2 | 3 | import org.meteordev.starscript.compiler.Expr; 4 | import org.meteordev.starscript.compiler.Lexer; 5 | import org.meteordev.starscript.compiler.Parser; 6 | import org.meteordev.starscript.compiler.Token; 7 | 8 | import java.util.Comparator; 9 | import java.util.Iterator; 10 | import java.util.List; 11 | 12 | /** Provides a list of {@link SemanticToken}s for a given Starscript source input. The main use case for semantic tokens is syntax highlighting. */ 13 | public class SemanticTokenProvider { 14 | /** See {@link SemanticTokenProvider}. The tokens are added to the list (which is automatically cleared) passed to this function. */ 15 | public static void get(String source, List tokens) { 16 | tokens.clear(); 17 | 18 | // Lexer 19 | Lexer lexer = new Lexer(source); 20 | 21 | lexer.next(); 22 | while (lexer.token != Token.EOF) { 23 | switch (lexer.token) { 24 | case Dot: 25 | tokens.add(new SemanticToken(SemanticTokenType.Dot, lexer.start, lexer.current)); 26 | break; 27 | 28 | case Comma: 29 | tokens.add(new SemanticToken(SemanticTokenType.Comma, lexer.start, lexer.current)); 30 | break; 31 | 32 | case EqualEqual: 33 | case BangEqual: 34 | case Greater: 35 | case GreaterEqual: 36 | case Less: 37 | case LessEqual: 38 | case Plus: 39 | case Minus: 40 | case Star: 41 | case Slash: 42 | case Percentage: 43 | case UpArrow: 44 | case Bang: 45 | case QuestionMark: 46 | case Colon: 47 | tokens.add(new SemanticToken(SemanticTokenType.Operator, lexer.start, lexer.current)); 48 | break; 49 | 50 | case String: 51 | if (lexer.isInExpression()) tokens.add(new SemanticToken(SemanticTokenType.String, lexer.start, lexer.current)); 52 | break; 53 | 54 | case Number: 55 | tokens.add(new SemanticToken(SemanticTokenType.Number, lexer.start, lexer.current)); 56 | break; 57 | 58 | case Null: 59 | case True: 60 | case False: 61 | case And: 62 | case Or: 63 | tokens.add(new SemanticToken(SemanticTokenType.Keyword, lexer.start, lexer.current)); 64 | break; 65 | 66 | case LeftParen: 67 | case RightParen: 68 | tokens.add(new SemanticToken(SemanticTokenType.Paren, lexer.start, lexer.current)); 69 | break; 70 | 71 | case LeftBrace: 72 | case RightBrace: 73 | tokens.add(new SemanticToken(SemanticTokenType.Brace, lexer.start, lexer.current)); 74 | break; 75 | 76 | case Section: 77 | tokens.add(new SemanticToken(SemanticTokenType.Section, lexer.start, lexer.current)); 78 | break; 79 | } 80 | 81 | lexer.next(); 82 | } 83 | 84 | // Parser 85 | Parser.Result result = Parser.parse(source); 86 | 87 | if (result.hasErrors()) { 88 | Error error = result.errors.get(0); 89 | 90 | // Remove tokens at the same position or after the error 91 | // noinspection Java8CollectionRemoveIf 92 | for (Iterator it = tokens.iterator(); it.hasNext();) { 93 | SemanticToken token = it.next(); 94 | 95 | if (token.end > error.character) it.remove(); 96 | } 97 | 98 | // Add the error token starting at the error position going to the end of the source 99 | tokens.add(new SemanticToken(SemanticTokenType.Error, error.character, source.length())); 100 | } 101 | else { 102 | result.accept(new Visitor(tokens)); 103 | } 104 | 105 | // Sort tokens 106 | tokens.sort(Comparator.comparingInt(token -> token.start)); 107 | } 108 | 109 | private static class Visitor extends AbstractExprVisitor { 110 | private final List tokens; 111 | 112 | public Visitor(List tokens) { 113 | this.tokens = tokens; 114 | } 115 | 116 | @Override 117 | public void visitVariable(Expr.Variable expr) { 118 | if (!(expr.parent instanceof Expr.Get)) { 119 | tokens.add(new SemanticToken(SemanticTokenType.Identifier, expr.end - expr.name.length(), expr.end)); 120 | } 121 | 122 | super.visitVariable(expr); 123 | } 124 | 125 | @Override 126 | public void visitGet(Expr.Get expr) { 127 | if (expr.getObject() instanceof Expr.Variable) { 128 | Expr.Variable varExpr = (Expr.Variable) expr.getObject(); 129 | tokens.add(new SemanticToken(SemanticTokenType.Map, varExpr.start, varExpr.end)); 130 | } 131 | else if (expr.getObject() instanceof Expr.Get) { 132 | Expr.Get getExpr = (Expr.Get) expr.getObject(); 133 | tokens.add(new SemanticToken(SemanticTokenType.Map, getExpr.end - getExpr.name.length(), getExpr.end)); 134 | } 135 | 136 | if (!(expr.parent instanceof Expr.Get)) { 137 | tokens.add(new SemanticToken(SemanticTokenType.Identifier, expr.end - expr.name.length(), expr.end)); 138 | } 139 | 140 | super.visitGet(expr); 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/utils/SemanticTokenType.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.utils; 2 | 3 | /** The type of a {@link SemanticToken}. Can be used to determine the token's color for syntax highlighting. */ 4 | public enum SemanticTokenType { 5 | Dot, 6 | Comma, 7 | Operator, 8 | String, 9 | Number, 10 | Keyword, 11 | Paren, 12 | Brace, 13 | Identifier, 14 | Map, 15 | Section, 16 | Error 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/utils/Stack.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.utils; 2 | 3 | public class Stack { 4 | @SuppressWarnings("unchecked") 5 | private T[] items = (T[]) new Object[8]; 6 | private int size; 7 | 8 | public void clear() { 9 | for (int i = 0; i < size; i++) items[i] = null; 10 | size = 0; 11 | } 12 | 13 | @SuppressWarnings("unchecked") 14 | public void push(T item) { 15 | if (size >= items.length) { 16 | T[] newItems = (T[]) new Object[items.length * 2]; 17 | System.arraycopy(items, 0, newItems, 0, items.length); 18 | items = newItems; 19 | } 20 | 21 | items[size++] = item; 22 | } 23 | 24 | public T pop() { 25 | T item = items[--size]; 26 | items[size] = null; 27 | return item; 28 | } 29 | 30 | public T peek() { 31 | return items[size - 1]; 32 | } 33 | 34 | public T peek(int offset) { 35 | return items[size - 1 - offset]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/utils/StarscriptError.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.utils; 2 | 3 | public class StarscriptError extends RuntimeException { 4 | public StarscriptError(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/utils/VariableReplacementTransformer.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.utils; 2 | 3 | import org.meteordev.starscript.compiler.Expr; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.function.Supplier; 8 | 9 | /** 10 | * Replaces usages of variables or get expressions with a different expression. Doesn't support functions.

11 | * 12 | * Examples:
13 | * addReplacer("player", () -> "foo"); 14 | * addReplacer("player.name", () -> "foo.bar"); 15 | */ 16 | public class VariableReplacementTransformer extends AbstractExprVisitor { 17 | private final Map> replacers = new HashMap<>(); 18 | private final StringBuilder sb = new StringBuilder(); 19 | 20 | public void addReplacer(String name, Supplier supplier) { 21 | replacers.put(name, supplier); 22 | } 23 | 24 | @Override 25 | public void visitVariable(Expr.Variable expr) { 26 | tryReplace(expr, expr.name); 27 | } 28 | 29 | @Override 30 | public void visitGet(Expr.Get expr) { 31 | String name = getFullName(expr); 32 | if (name != null) tryReplace(expr, name); 33 | } 34 | 35 | private void tryReplace(Expr expr, String name) { 36 | Supplier replacer = replacers.get(name); 37 | if (replacer == null) return; 38 | 39 | Expr replacement = createReplacement(replacer.get()); 40 | expr.replace(replacement); 41 | } 42 | 43 | private Expr createReplacement(String replacement) { 44 | String[] parts = replacement.split("\\."); 45 | if (parts.length == 0) throw new IllegalStateException("Cannot replace with an empty replacement"); 46 | 47 | Expr expr = null; 48 | 49 | for (int i = 0; i < parts.length; i++) { 50 | if (i == 0) expr = new Expr.Variable(0, 0, parts[i]); 51 | else expr = new Expr.Get(0, 0, expr, parts[i]); 52 | } 53 | 54 | return expr; 55 | } 56 | 57 | private String getFullName(Expr.Get expr) { 58 | try { 59 | getFullNameImpl(expr); 60 | } 61 | catch (IllegalStateException ignored) { 62 | sb.setLength(0); 63 | return null; 64 | } 65 | 66 | String name = sb.toString(); 67 | sb.setLength(0); 68 | 69 | return name; 70 | } 71 | 72 | private void getFullNameImpl(Expr.Get expr) { 73 | if (expr.getObject() instanceof Expr.Get) getFullNameImpl((Expr.Get) expr.getObject()); 74 | else if (expr.getObject() instanceof Expr.Variable) sb.append(((Expr.Variable) expr.getObject()).name); 75 | else throw new IllegalStateException(); 76 | 77 | sb.append('.'); 78 | sb.append(expr.name); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/value/Value.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.value; 2 | 3 | import org.meteordev.starscript.utils.SFunction; 4 | 5 | import java.util.function.Supplier; 6 | 7 | /** Class that holds any starscript value. */ 8 | public class Value { 9 | private static final Value NULL = new Value(ValueType.Null); 10 | private static final Value TRUE = new Boolean(true); 11 | private static final Value FALSE = new Boolean(false); 12 | 13 | public final ValueType type; 14 | 15 | private Value(ValueType type) { 16 | this.type = type; 17 | } 18 | 19 | public static Value null_() { 20 | return NULL; 21 | } 22 | public static Value bool(boolean bool) { 23 | return bool ? TRUE : FALSE; 24 | } 25 | public static Value number(double number) { 26 | return new Number(number); 27 | } 28 | public static Value string(String string) { 29 | return new VString(string); 30 | } 31 | public static Value function(SFunction function) { 32 | return new Function(function); 33 | } 34 | public static Value map(ValueMap fields) { 35 | return new Map(fields); 36 | } 37 | public static Value object(java.lang.Object object) { 38 | return new Object(object); 39 | } 40 | 41 | public boolean isNull() { 42 | return type == ValueType.Null; 43 | } 44 | public boolean isBool() { 45 | return type == ValueType.Boolean; 46 | } 47 | public boolean isNumber() { 48 | return type == ValueType.Number; 49 | } 50 | public boolean isString() { 51 | return type == ValueType.String; 52 | } 53 | public boolean isFunction() { 54 | return type == ValueType.Function; 55 | } 56 | public boolean isMap() { 57 | return type == ValueType.Map; 58 | } 59 | public boolean isObject() { 60 | return type == ValueType.Object; 61 | } 62 | 63 | public boolean getBool() { 64 | return ((Boolean) this).bool; 65 | } 66 | public double getNumber() { 67 | return ((Number) this).number; 68 | } 69 | public String getString() { 70 | return ((VString) this).string; 71 | } 72 | public SFunction getFunction() { 73 | return ((Function) this).function; 74 | } 75 | public ValueMap getMap() { 76 | return ((Map) this).fields; 77 | } 78 | public java.lang.Object getObject() { 79 | return ((Object) this).object; 80 | } 81 | 82 | public boolean isTruthy() { 83 | switch (type) { 84 | default: 85 | case Null: return false; 86 | case Boolean: return getBool(); 87 | case Number: 88 | case String: 89 | case Function: 90 | case Map: 91 | case Object: return true; 92 | } 93 | } 94 | 95 | @Override 96 | public boolean equals(java.lang.Object o) { 97 | if (this == o) return true; 98 | if (o == null || getClass() != o.getClass()) return false; 99 | 100 | Value value = (Value) o; 101 | if (type != value.type) return false; 102 | 103 | switch (type) { 104 | case Null: return true; 105 | case Boolean: return getBool() == value.getBool(); 106 | case Number: return getNumber() == value.getNumber(); 107 | case String: return getString().equals(value.getString()); 108 | case Function: return getFunction() == value.getFunction(); 109 | case Map: return getMap() == value.getMap(); 110 | case Object: return getObject().equals(value.getObject()); 111 | default: return false; 112 | } 113 | } 114 | 115 | @Override 116 | public int hashCode() { 117 | int result = 31 * super.hashCode(); 118 | 119 | switch (type) { 120 | case Boolean: result += java.lang.Boolean.hashCode(getBool()); break; 121 | case Number: result += Double.hashCode(getNumber()); break; 122 | case String: result += getString().hashCode(); break; 123 | case Function: result += getFunction().hashCode(); break; 124 | case Map: result += getMap().hashCode(); break; 125 | case Object: result += getObject().hashCode(); break; 126 | } 127 | 128 | return result; 129 | } 130 | 131 | @Override 132 | public String toString() { 133 | switch (type) { 134 | case Null: return "null"; 135 | case Boolean: return getBool() ? "true" : "false"; 136 | case Number: { double n = getNumber(); return n % 1 == 0 ? Integer.toString((int) n) : Double.toString(n); } 137 | case String: return getString(); 138 | case Function: return ""; 139 | case Map: { 140 | Supplier s = getMap().getRaw("_toString"); 141 | return s == null ? "" : s.get().toString(); 142 | } 143 | case Object: return getObject().toString(); 144 | default: return ""; 145 | } 146 | } 147 | 148 | private static class Boolean extends Value { 149 | private final boolean bool; 150 | 151 | private Boolean(boolean bool) { 152 | super(ValueType.Boolean); 153 | this.bool = bool; 154 | } 155 | } 156 | 157 | private static class Number extends Value { 158 | private final double number; 159 | 160 | private Number(double number) { 161 | super(ValueType.Number); 162 | this.number = number; 163 | } 164 | } 165 | 166 | private static class VString extends Value { 167 | private final String string; 168 | 169 | private VString(String string) { 170 | super(ValueType.String); 171 | this.string = string; 172 | } 173 | } 174 | 175 | private static class Function extends Value { 176 | private final SFunction function; 177 | 178 | public Function(SFunction function) { 179 | super(ValueType.Function); 180 | this.function = function; 181 | } 182 | } 183 | 184 | private static class Map extends Value { 185 | private final ValueMap fields; 186 | 187 | public Map(ValueMap fields) { 188 | super(ValueType.Map); 189 | this.fields = fields; 190 | } 191 | } 192 | 193 | private static class Object extends Value { 194 | private final java.lang.Object object; 195 | 196 | public Object(java.lang.Object object) { 197 | super(ValueType.Object); 198 | this.object = object; 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/value/ValueMap.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.value; 2 | 3 | import org.meteordev.starscript.utils.SFunction; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | import java.util.Set; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.function.Supplier; 10 | 11 | /** Simpler wrapper around a map that goes from {@link String} to {@link Supplier} for {@link Value}. */ 12 | public class ValueMap { 13 | private final Map> values = new ConcurrentHashMap<>(); 14 | 15 | /** 16 | * Sets a variable supplier for the provided name.

17 | * 18 | * Dot Notation:
19 | * If the name contains a dot it is automatically split into separate value maps. For example the name 'player.name' will not put a single string value with the name 'player.name' into this value map but another value map with the name 'player' and then inside that a string value with the name 'name'. If there already is a value named 'player' and it is a map, it just adds to that existing map, otherwise it replaces the value. 20 | */ 21 | public ValueMap set(String name, Supplier supplier) { 22 | int dotI = name.indexOf('.'); 23 | 24 | if (dotI >= 0) { 25 | // Split name based on the dot 26 | String name1 = name.substring(0, dotI); 27 | String name2 = name.substring(dotI + 1); 28 | 29 | // Get the map 30 | ValueMap map; 31 | Supplier valueSupplier = getRaw(name1); 32 | 33 | if (valueSupplier == null) { 34 | map = new ValueMap(); 35 | setRaw(name1, () -> Value.map(map)); 36 | } 37 | else { 38 | Value value = valueSupplier.get(); 39 | 40 | if (value.isMap()) map = value.getMap(); 41 | else { 42 | map = new ValueMap(); 43 | setRaw(name1, () -> Value.map(map)); 44 | } 45 | } 46 | 47 | // Set the supplier 48 | map.set(name2, supplier); 49 | } 50 | else setRaw(name, supplier); 51 | 52 | return this; 53 | } 54 | 55 | /** Sets a variable supplier that always returns the same value for the provided name.

See {@link #set(String, Supplier)} for dot notation. */ 56 | public ValueMap set(String name, Value value) { 57 | set(name, () -> value); 58 | return this; 59 | } 60 | 61 | /** Sets a boolean variable supplier that always returns the same value for the provided name.

See {@link #set(String, Supplier)} for dot notation. */ 62 | public ValueMap set(String name, boolean bool) { 63 | return set(name, Value.bool(bool)); 64 | } 65 | 66 | /** Sets a number variable supplier that always returns the same value for the provided name.

See {@link #set(String, Supplier)} for dot notation. */ 67 | public ValueMap set(String name, double number) { 68 | return set(name, Value.number(number)); 69 | } 70 | 71 | /** Sets a string variable supplier that always returns the same value for the provided name.

See {@link #set(String, Supplier)} for dot notation. */ 72 | public ValueMap set(String name, String string) { 73 | return set(name, Value.string(string)); 74 | } 75 | 76 | /** Sets a function variable supplier that always returns the same value for the provided name.

See {@link #set(String, Supplier)} for dot notation. */ 77 | public ValueMap set(String name, SFunction function) { 78 | return set(name, Value.function(function)); 79 | } 80 | 81 | /** Sets a map variable supplier that always returns the same value for the provided name.

See {@link #set(String, Supplier)} for dot notation. */ 82 | public ValueMap set(String name, ValueMap map) { 83 | return set(name, Value.map(map)); 84 | } 85 | 86 | /** Sets an object variable supplier that always returns the same value for the provided name.

See {@link #set(String, Supplier)} for dot notation. */ 87 | public ValueMap set(String name, Object object) { 88 | return set(name, Value.object(object)); 89 | } 90 | 91 | /** 92 | * Gets the variable supplier for the provided name.

93 | * 94 | * Dot Notation:
95 | * If the name is for example 'player.name' then it gets a value with the name 'player' from this map and calls .get() with 'name' on the second map. If 'player' is not a map then returns null. See {@link #set(String, Supplier)}. 96 | */ 97 | public Supplier get(String name) { 98 | int dotI = name.indexOf('.'); 99 | 100 | if (dotI >= 0) { 101 | // Split name based on the dot 102 | String name1 = name.substring(0, dotI); 103 | String name2 = name.substring(dotI + 1); 104 | 105 | // Get child value 106 | Supplier valueSupplier = getRaw(name1); 107 | if (valueSupplier == null) return null; 108 | 109 | // Make sure the child value is a map 110 | Value value = valueSupplier.get(); 111 | if (!value.isMap()) return null; 112 | 113 | // Get value from the child map 114 | return value.getMap().get(name2); 115 | } 116 | 117 | return getRaw(name); 118 | } 119 | 120 | /** Gets the variable supplier for the provided name. */ 121 | public Supplier getRaw(String name) { 122 | return values.get(name); 123 | } 124 | 125 | /** Sets the variable supplier for the provided name. */ 126 | public Supplier setRaw(String name, Supplier supplier) { 127 | return values.put(name, supplier); 128 | } 129 | 130 | /** Removes the variable supplier for the provided name. */ 131 | public Supplier removeRaw(String name) { 132 | return values.remove(name); 133 | } 134 | 135 | /** Returns a set of all variable names. */ 136 | public Set keys() { 137 | return values.keySet(); 138 | } 139 | 140 | /** Removes all values from this map. */ 141 | public void clear() { 142 | values.clear(); 143 | } 144 | 145 | /** 146 | * Removes a single value with the specified name from this map and returns the removed value.

147 | * 148 | * Dot Notation:
149 | * If the name is for example 'player.name' then it attempts to get a value with the name 'player' from this map and calls .remove("name") on the second map. If `player` is not a map then the last param is removed. See {@link #set(String, Supplier)}. 150 | */ 151 | public Supplier remove(String name) { 152 | int dotI = name.indexOf('.'); 153 | 154 | if (dotI >= 0) { 155 | // Split name based on the dot 156 | String name1 = name.substring(0, dotI); 157 | String name2 = name.substring(dotI + 1); 158 | 159 | // Get child value 160 | Supplier valueSupplier = getRaw(name1); 161 | if (valueSupplier == null) return null; 162 | else { 163 | // Make sure the child value is a map 164 | Value value = valueSupplier.get(); 165 | if (!value.isMap()) return removeRaw(name1); 166 | else return value.getMap().remove(name2); 167 | } 168 | } 169 | else return removeRaw(name); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/org/meteordev/starscript/value/ValueType.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript.value; 2 | 3 | public enum ValueType { 4 | Null, 5 | Boolean, 6 | Number, 7 | String, 8 | Function, 9 | Map, 10 | Object 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/org/meteordev/starscript/Benchmark.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript; 2 | 3 | import org.meteordev.starscript.compiler.Compiler; 4 | import org.meteordev.starscript.compiler.Parser; 5 | import org.openjdk.jmh.annotations.*; 6 | import org.openjdk.jmh.infra.Blackhole; 7 | import org.openjdk.jmh.runner.Runner; 8 | import org.openjdk.jmh.runner.RunnerException; 9 | import org.openjdk.jmh.runner.options.Options; 10 | import org.openjdk.jmh.runner.options.OptionsBuilder; 11 | import org.openjdk.jmh.runner.options.TimeValue; 12 | 13 | import java.util.Formatter; 14 | import java.util.concurrent.TimeUnit; 15 | 16 | /* 17 | Here are the results of the benchmark below ran on my machine with JDK 17.0.5 18 | 19 | Benchmark Mode Cnt Score Error Units 20 | Benchmark.format thrpt 3 1,417 � 0,766 ops/us 21 | Benchmark.formatter thrpt 3 2,161 � 0,448 ops/us 22 | Benchmark.starscript thrpt 3 7,423 � 1,771 ops/us 23 | Benchmark.format avgt 3 0,707 � 0,262 us/op 24 | Benchmark.formatter avgt 3 0,469 � 0,273 us/op 25 | Benchmark.starscript avgt 3 0,141 � 0,051 us/op 26 | */ 27 | 28 | @BenchmarkMode({Mode.AverageTime, Mode.Throughput}) 29 | @OutputTimeUnit(TimeUnit.MICROSECONDS) 30 | @State(Scope.Benchmark) 31 | public class Benchmark { 32 | public static void main(String[] args) throws RunnerException { 33 | Options opt = new OptionsBuilder() 34 | .warmupIterations(3) 35 | .measurementIterations(3) 36 | .warmupTime(TimeValue.seconds(3)) 37 | .measurementTime(TimeValue.seconds(3)) 38 | .forks(1) 39 | .build(); 40 | 41 | new Runner(opt).run(); 42 | } 43 | 44 | public final String formatSource = "FPS: %.0f"; 45 | public final String starscriptSource = "FPS: {round(fps)}"; 46 | 47 | public StringBuilder sb; 48 | 49 | private Formatter formatter; 50 | 51 | public Script script; 52 | public Starscript ss; 53 | 54 | @Setup 55 | public void setup() { 56 | sb = new StringBuilder(); 57 | 58 | // Format 59 | formatter = new Formatter(sb); 60 | 61 | // Starscript 62 | script = Compiler.compile(Parser.parse(starscriptSource)); 63 | 64 | ss = new Starscript(); 65 | StandardLib.init(ss); 66 | ss.set("name", "MineGame159"); 67 | ss.set("fps", 59.68223); 68 | } 69 | 70 | @org.openjdk.jmh.annotations.Benchmark 71 | public void format(Blackhole bh) { 72 | bh.consume(String.format(formatSource, 59.68223)); 73 | } 74 | 75 | @org.openjdk.jmh.annotations.Benchmark 76 | public void formatter(Blackhole bh) { 77 | sb.setLength(0); 78 | bh.consume(formatter.format(formatSource, 59.68223).toString()); 79 | } 80 | 81 | @org.openjdk.jmh.annotations.Benchmark 82 | public void starscript(Blackhole bh) { 83 | bh.consume(ss.run(script, sb).toString()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/org/meteordev/starscript/Main.java: -------------------------------------------------------------------------------- 1 | package org.meteordev.starscript; 2 | 3 | import org.meteordev.starscript.compiler.Compiler; 4 | import org.meteordev.starscript.compiler.Parser; 5 | import org.meteordev.starscript.utils.Error; 6 | import org.meteordev.starscript.value.Value; 7 | import org.meteordev.starscript.value.ValueMap; 8 | 9 | public class Main { 10 | private static final boolean USE_DOT_NOTATION = true; 11 | 12 | public static void main(String[] args) { 13 | String source = "Name: {player.name} Age: {player.age()}"; 14 | 15 | Parser.Result result = Parser.parse(source); 16 | Script script = Compiler.compile(result); 17 | 18 | script.decompile(); 19 | System.out.println(); 20 | 21 | if (result.hasErrors()) { 22 | for (Error error : result.errors) System.out.println(error); 23 | System.out.println(); 24 | } 25 | 26 | Starscript ss = new Starscript(); 27 | StandardLib.init(ss); 28 | 29 | if (USE_DOT_NOTATION) { 30 | ss.set("player.name", "MineGame159"); 31 | ss.set("player.age", (ss1, agrCount) -> Value.number(5)); 32 | } 33 | else { 34 | ss.set("player", new ValueMap() 35 | .set("name", "MineGame159") 36 | .set("age", (ss1, agrCount) -> Value.number(5)) 37 | ); 38 | } 39 | 40 | System.out.println("Input: " + source); 41 | System.out.println("Output: " + ss.run(script)); 42 | 43 | ss.remove("player.name"); 44 | 45 | System.out.println("Output #2: " + ss.run(script)); 46 | } 47 | } 48 | --------------------------------------------------------------------------------