From 4307e3a671546dcb87198f51cfbf13788d97a0fb Mon Sep 17 00:00:00 2001 From: Octavia Togami Date: Fri, 24 Apr 2020 21:46:03 -0400 Subject: [PATCH] Transpile using Babel This is pretty slow right now, but works as a proof-of-concept. --- worldedit-core/build.gradle.kts | 2 +- .../scripting/RhinoCraftScriptEngine.java | 12 +- .../compat/BabelScriptTranspiler.java | 83 ++++++++++++ .../scripting/compat/RemoteScript.java | 120 ++++++++++++++++++ .../scripting/compat/ScriptTranspiler.java | 38 ++++++ .../sk89q/worldedit/util/net/HttpRequest.java | 18 ++- worldedit-forge/build.gradle.kts | 4 +- 7 files changed, 265 insertions(+), 12 deletions(-) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/BabelScriptTranspiler.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/RemoteScript.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/ScriptTranspiler.java diff --git a/worldedit-core/build.gradle.kts b/worldedit-core/build.gradle.kts index 0b17c0202..f88a2a6ac 100644 --- a/worldedit-core/build.gradle.kts +++ b/worldedit-core/build.gradle.kts @@ -31,7 +31,7 @@ dependencies { "compile"(project(":worldedit-libs:core")) "compile"("de.schlichtherle:truezip:6.8.3") "compile"("net.java.truevfs:truevfs-profile-default_2.13:0.12.1") - "compile"("org.mozilla:rhino:1.7.11") + "compile"("org.mozilla:rhino-runtime:1.7.12") "compile"("org.yaml:snakeyaml:1.23") "compile"("com.google.guava:guava:21.0") "compile"("com.google.code.findbugs:jsr305:3.0.2") diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/RhinoCraftScriptEngine.java b/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/RhinoCraftScriptEngine.java index 8cad2670b..8d56d86db 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/RhinoCraftScriptEngine.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/RhinoCraftScriptEngine.java @@ -19,7 +19,10 @@ package com.sk89q.worldedit.scripting; +import com.google.common.io.CharStreams; import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.scripting.compat.BabelScriptTranspiler; +import com.sk89q.worldedit.scripting.compat.ScriptTranspiler; import org.mozilla.javascript.Context; import org.mozilla.javascript.ImporterTopLevel; import org.mozilla.javascript.JavaScriptException; @@ -28,11 +31,13 @@ import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.WrappedException; +import javax.script.ScriptException; +import java.io.StringReader; import java.util.Map; -import javax.script.ScriptException; - public class RhinoCraftScriptEngine implements CraftScriptEngine { + + private static final ScriptTranspiler TRANSPILER = new BabelScriptTranspiler(); private int timeLimit; @Override @@ -48,6 +53,7 @@ public class RhinoCraftScriptEngine implements CraftScriptEngine { @Override public Object evaluate(String script, String filename, Map args) throws ScriptException, Throwable { + String transpiled = CharStreams.toString(TRANSPILER.transpile(new StringReader(script))); RhinoContextFactory factory = new RhinoContextFactory(timeLimit); Context cx = factory.enterContext(); cx.setClassShutter(new MinecraftHidingClassShutter()); @@ -59,7 +65,7 @@ public class RhinoCraftScriptEngine implements CraftScriptEngine { Context.javaToJS(entry.getValue(), scope)); } try { - return cx.evaluateString(scope, script, filename, 1, null); + return cx.evaluateString(scope, transpiled, filename, 1, null); } catch (Error e) { throw new ScriptException(e.getMessage()); } catch (RhinoException e) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/BabelScriptTranspiler.java b/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/BabelScriptTranspiler.java new file mode 100644 index 000000000..5fe83c587 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/BabelScriptTranspiler.java @@ -0,0 +1,83 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.scripting.compat; + +import com.google.common.io.CharStreams; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContextFactory; +import org.mozilla.javascript.Function; +import org.mozilla.javascript.Scriptable; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.concurrent.TimeUnit; + +public class BabelScriptTranspiler implements ScriptTranspiler { + + private static final RemoteScript BABEL = new RemoteScript( + "https://unpkg.com/@babel/standalone@7.9/babel.min.js", + "babel.min.js", + new RemoteScript( + "https://unpkg.com/core-js-bundle@3.6.5/index.js", + "core-js-bundle.js" + ), + new RemoteScript( + "https://unpkg.com/regenerator-runtime@0.13.5/runtime.js", + "regenerator-runtime.js" + ) + ); + + private final ContextFactory contextFactory = new ContextFactory() { + @Override + protected Context makeContext() { + Context context = super.makeContext(); + context.setLanguageVersion(Context.VERSION_ES6); + return context; + } + }; + private final Function executeBabel; + + public BabelScriptTranspiler() { + Scriptable babel = BABEL.getScope(); + executeBabel = contextFactory.call(ctx -> { + ctx.setOptimizationLevel(9); + String execBabelSource = "function(source) {\n" + + "return Babel.transform(source, { presets: ['env'] }).code;\n" + + "}\n"; + return ctx.compileFunction( + babel, execBabelSource, "", 1, null + ); + }); + } + + @Override + public Reader transpile(Reader script) throws IOException { + long startTranspile = System.nanoTime(); + Scriptable babel = BABEL.getScope(); + String source = CharStreams.toString(script); + String result = (String) contextFactory.call(ctx -> + executeBabel.call(ctx, babel, null, new Object[] { source }) + ); + System.err.println(result); + System.err.println("Took " + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTranspile)); + return new StringReader(result); + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/RemoteScript.java b/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/RemoteScript.java new file mode 100644 index 000000000..ee59db817 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/RemoteScript.java @@ -0,0 +1,120 @@ +package com.sk89q.worldedit.scripting.compat; + +import com.google.common.collect.ImmutableList; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.util.net.HttpRequest; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContextFactory; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.TopLevel; + +import java.io.IOException; +import java.io.Reader; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; + +public class RemoteScript { + + private static final int MAX_REDIRECTS = 100; + + + private final ContextFactory contextFactory = new ContextFactory() { + @Override + protected boolean hasFeature(Context cx, int featureIndex) { + if (featureIndex == Context.FEATURE_OLD_UNDEF_NULL_THIS) { + return true; + } + return super.hasFeature(cx, featureIndex); + } + + @Override + protected Context makeContext() { + Context context = super.makeContext(); + context.setLanguageVersion(Context.VERSION_ES6); + return context; + } + }; + private final Path cacheDir = WorldEdit.getInstance() + .getWorkingDirectoryFile("craftscripts/.cache").toPath(); + private final URL source; + private final String cacheFileName; + private final Path cachePath; + private final List dependencies; + + private volatile Scriptable cachedScope; + + public RemoteScript(String source, String cacheFileName, RemoteScript... dependencies) { + this.source = HttpRequest.url(source); + this.cacheFileName = cacheFileName; + this.cachePath = cacheDir.resolve(cacheFileName); + this.dependencies = ImmutableList.copyOf(dependencies); + } + + private synchronized void ensureCached() throws IOException { + if (!Files.exists(cacheDir)) { + Files.createDirectories(cacheDir); + } + if (!Files.exists(cachePath)) { + boolean downloadedBabel = false; + int redirects = 0; + URL url = source; + while (redirects < MAX_REDIRECTS && !downloadedBabel) { + try (HttpRequest request = HttpRequest.get(url)) { + request.execute(); + request.expectResponseCode(200, 301, 302); + if (request.getResponseCode() > 300) { + redirects++; + url = HttpRequest.url(request.getSingleHeaderValue("Location")); + continue; + } + request.saveContent(cachePath.toFile()); + downloadedBabel = true; + } + } + checkState(downloadedBabel, "Too many redirects following: %s", url); + checkState(Files.exists(cachePath), "Failed to actually download %s", cacheFileName); + } + } + + protected synchronized void loadIntoScope(Context ctx, Scriptable scope) { + try { + ensureCached(); + try (Reader reader = Files.newBufferedReader(cachePath)) { + ctx.evaluateReader(scope, reader, cacheFileName, 1, null); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Get a scope that the script has been evaluated in. + * + * @return the scope + */ + public synchronized Scriptable getScope() { + if (cachedScope != null) { + return cachedScope; + } + + // parse + execute standalone script to load it into the scope + cachedScope = contextFactory.call(ctx -> { + ScriptableObject scriptable = new TopLevel(); + Scriptable newScope = ctx.initStandardObjects(scriptable); + ctx.setOptimizationLevel(9); + for (RemoteScript dependency : dependencies) { + dependency.loadIntoScope(ctx, newScope); + } + loadIntoScope(ctx, newScope); + return newScope; + }); + + return cachedScope; + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/ScriptTranspiler.java b/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/ScriptTranspiler.java new file mode 100644 index 000000000..884b9cef4 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/scripting/compat/ScriptTranspiler.java @@ -0,0 +1,38 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * Copyright (C) WorldEdit team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.scripting.compat; + +import java.io.IOException; +import java.io.Reader; + +/** + * Transpile a script from one (version) of a language to another. + */ +public interface ScriptTranspiler { + + /** + * Given input {@code script}, return the transpiled script. + * + * @param script the script to transpile + * @return the new script + */ + Reader transpile(Reader script) throws IOException; + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/util/net/HttpRequest.java b/worldedit-core/src/main/java/com/sk89q/worldedit/util/net/HttpRequest.java index 8421210e4..ba0e7fef9 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/util/net/HttpRequest.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/util/net/HttpRequest.java @@ -43,6 +43,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static com.google.common.base.Preconditions.checkState; + public class HttpRequest implements Closeable { private static final int CONNECT_TIMEOUT = 1000 * 5; @@ -200,6 +202,13 @@ public class HttpRequest implements Closeable { return conn.getResponseCode(); } + public String getSingleHeaderValue(String header) { + checkState(conn != null, "No connection has been made"); + + // maybe we should check for multi-header? + return conn.getHeaderField(header); + } + /** * Get the input stream. * @@ -214,9 +223,8 @@ public class HttpRequest implements Closeable { * * @return the buffered response * @throws java.io.IOException on I/O error - * @throws InterruptedException on interruption */ - public BufferedResponse returnContent() throws IOException, InterruptedException { + public BufferedResponse returnContent() throws IOException { if (inputStream == null) { throw new IllegalArgumentException("No input stream available"); } @@ -239,9 +247,8 @@ public class HttpRequest implements Closeable { * @param file the file * @return this object * @throws java.io.IOException on I/O error - * @throws InterruptedException on interruption */ - public HttpRequest saveContent(File file) throws IOException, InterruptedException { + public HttpRequest saveContent(File file) throws IOException { Closer closer = Closer.create(); try { @@ -262,9 +269,8 @@ public class HttpRequest implements Closeable { * @param out the output stream * @return this object * @throws java.io.IOException on I/O error - * @throws InterruptedException on interruption */ - public HttpRequest saveContent(OutputStream out) throws IOException, InterruptedException { + public HttpRequest saveContent(OutputStream out) throws IOException { BufferedInputStream bis; try { diff --git a/worldedit-forge/build.gradle.kts b/worldedit-forge/build.gradle.kts index 9441e7abb..23d52162d 100644 --- a/worldedit-forge/build.gradle.kts +++ b/worldedit-forge/build.gradle.kts @@ -88,10 +88,10 @@ tasks.named("shadowJar") { include(dependency("org.apache.logging.log4j:log4j-slf4j-impl")) include(dependency("de.schlichtherle:truezip")) include(dependency("net.java.truevfs:truevfs-profile-default_2.13")) - include(dependency("org.mozilla:rhino")) + include(dependency("org.mozilla:rhino-runtime")) } minimize { - exclude(dependency("org.mozilla:rhino")) + exclude(dependency("org.mozilla:rhino-runtime")) } }