Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DRAFT ServiceLoader implementation for J2CL+Closure #245

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
43 changes: 43 additions & 0 deletions j2cl-maven-plugin/src/it/registry-sample/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>registry-sample</groupId>
<artifactId>registry-sample</artifactId>
<version>1.0</version>

<build>
<plugins>
<plugin>
<groupId>@project.groupId@</groupId>
<artifactId>@project.artifactId@</artifactId>
<version>@project.version@</version>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
<configuration>
<defines>
<exampleDefault>java</exampleDefault>
</defines>
</configuration>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
8 changes: 8 additions & 0 deletions j2cl-maven-plugin/src/it/registry-sample/postbuild.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
def js = new File(basedir, 'target/registry-sample-1.0/registry-sample/registry-sample.js').text

if (!js.contains('"Hello, Java!"')) {
throw new IllegalStateException('Contents weren\'t optimized correctly, no "Hello, Java!" string')
}
if (js.contains('python') || js.contains('Python')) {
throw new IllegalStateException('Contents weren\'t optimized correctly, \'python\' found in output')
}
3 changes: 3 additions & 0 deletions j2cl-maven-plugin/src/it/registry-sample/src/main/java/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
goog.require('hello');

setTimeout(()=>console.log(sayHello()), 100);
16 changes: 16 additions & 0 deletions j2cl-maven-plugin/src/it/registry-sample/src/main/java/java.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
goog.provide('java')
goog.require('language')
goog.require('serviceloader')

/**
*
* @constructor
* @implements ProgrammingLanguage
*/
function JavaLanguage() {

}
JavaLanguage.prototype.getNameStr = function() {
return "Java";
};
register('java', () => new JavaLanguage());
14 changes: 14 additions & 0 deletions j2cl-maven-plugin/src/it/registry-sample/src/main/java/language.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
goog.provide('language')

/**
*
* @interface
*/
function ProgrammingLanguage() {

}

/**
* @return {string}
*/
ProgrammingLanguage.prototype.getNameStr = function() {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
goog.provide('hello')
goog.require('language')
goog.require('serviceloader')

function sayHello() {
var defaultLanguage = lookupDefault();
return "Hello, " + defaultLanguage.getNameStr() + "!";
}
16 changes: 16 additions & 0 deletions j2cl-maven-plugin/src/it/registry-sample/src/main/java/python.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
goog.provide('py')
goog.require('language')
goog.require('serviceloader')

/**
*
* @constructor
* @implements ProgrammingLanguage
*/
function PythonLanguage() {

}
PythonLanguage.prototype.getNameStr = function() {
return "Python";
};
register('python', () => new PythonLanguage());
16 changes: 16 additions & 0 deletions j2cl-maven-plugin/src/it/registry-sample/src/main/java/registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
goog.provide('serviceloader')

var map = {};
function register(key, creator) {
map[key + '$j2cl$service$loader$key'] = creator;
}
function lookup(key) {
return map[key + '$j2cl$service$loader$key']();
}

// Specify a default, and allow it to be overridden at build time
/** @define {string} */
const exampleDefault = goog.define('exampleDefault', 'python');
function lookupDefault() {
return map[exampleDefault + '$j2cl$service$loader$key']();
}
16 changes: 14 additions & 2 deletions j2cl-tasks/src/main/java/com/vertispan/j2cl/tools/Closure.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import com.vertispan.j2cl.build.DiskCache;
import com.vertispan.j2cl.build.task.BuildLog;
import com.vertispan.j2cl.build.task.Input;
import com.vertispan.j2cl.tools.closure.ServiceLoadingPassConfig;

import javax.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand Down Expand Up @@ -175,7 +177,7 @@ public boolean compile(

final InProcessJsCompRunner jscompRunner;
synchronized (GLOBAL_CLOSURE_ARGS_LOCK) {
jscompRunner = new InProcessJsCompRunner(log, jscompArgs.toArray(new String[0]), jsCompiler, exportTestFunctions, checkAssertions);
jscompRunner = new InProcessJsCompRunner(log, jscompArgs.toArray(new String[0]), jsCompiler, exportTestFunctions, checkAssertions, compilationLevel);
}
jscompArgs.forEach(log::debug);
if (!jscompRunner.shouldRunCompiler()) {
Expand All @@ -195,11 +197,13 @@ static class InProcessJsCompRunner extends CommandLineRunner {
private final boolean exportTestFunctions;
private final boolean checkAssertions;
private final Compiler compiler;
private final CompilationLevel compilationLevel;
private Integer exitCode;

InProcessJsCompRunner(BuildLog log, String[] args, Compiler compiler, boolean exportTestFunctions, boolean checkAssertions) {
InProcessJsCompRunner(BuildLog log, String[] args, Compiler compiler, boolean exportTestFunctions, boolean checkAssertions, CompilationLevel compilationLevel) {
super(args);
this.compiler = compiler;
this.compilationLevel = compilationLevel;
this.compiler.setErrorManager(new SortingErrorManager(Collections.singleton(new LoggingErrorReportGenerator(compiler, log))));
this.exportTestFunctions = exportTestFunctions;
this.checkAssertions = checkAssertions;
Expand All @@ -226,6 +230,14 @@ protected CompilerOptions createOptions() {

return options;
}

@Override
protected void setRunOptions(CompilerOptions options) throws IOException {
super.setRunOptions(options);
if (compilationLevel == CompilationLevel.ADVANCED_OPTIMIZATIONS) {
this.compiler.setPassConfig(new ServiceLoadingPassConfig(options));
}
}
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.vertispan.j2cl.tools.closure;

import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.CompilerPass;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.TokenStream;

import java.util.Objects;

/**
* Modified copy of ConvertToDottedProperties. Changes from original:
* <ul>
* <li>Called early in the build rather than very late, so that the unused now-dotted properties can be optimized
* as need be.</li>
* <li>Limit implementation to not apply to property accessors. This lets us simplify code slightly, and won't
* apply to our use cases, but there would be no downside for supporting this.</li>
* <li>Only applies to certain property patterns, to avoid accidentally inlining string properties that this
* shouldn't apply to.</li>
* <li>NodeUtil's members are package-protected, so required members are inlined here.</li>
* </ul>
* <p>
* See <a href="https://groups.google.com/g/closure-compiler-discuss/c/3Gsd73xdt1U">mailing list discussion</a>.
*/
public class ConvertServiceLoaderProperties extends NodeTraversal.AbstractPostOrderCallback implements CompilerPass {

private final AbstractCompiler compiler;

public ConvertServiceLoaderProperties(AbstractCompiler compiler) {
this.compiler = compiler;
}

@Override
public void process(Node externs, Node root) {
NodeTraversal.traverse(compiler, root, this);
System.err.println("ConvertServiceLoaderProperties running");
}

@Override
public void visit(NodeTraversal t, Node n, Node parent) {
if (Objects.requireNonNull(n.getToken()) == Token.OPTCHAIN_GETELEM || n.getToken() == Token.GETELEM) {
Node left = n.getFirstChild();
Node right = left.getNext();
if (right.isStringLit() && right.getString().endsWith("$service$loader$key") && isValidPropertyName(FeatureSet.ES3, right.getString())) {
left.detach();
right.detach();

Node newGetProp =
n.isGetElem()
? IR.getprop(left, right.getString())
: (n.isOptionalChainStart()
? IR.startOptChainGetprop(left, right.getString())
: IR.continueOptChainGetprop(left, right.getString()));
n.replaceWith(newGetProp);
compiler.reportChangeToEnclosingScope(newGetProp);
}
}
}

public static boolean isValidPropertyName(FeatureSet mode, String name) {
if (isValidSimpleName(name)) {
return true;
} else {
return mode.has(FeatureSet.Feature.KEYWORDS_AS_PROPERTIES) && TokenStream.isKeyword(name);
}
}
static boolean isValidSimpleName(String name) {
return TokenStream.isJSIdentifier(name)
&& !TokenStream.isKeyword(name)
// no Unicode escaped characters - some browsers are less tolerant
// of Unicode characters that might be valid according to the
// language spec.
// Note that by this point, Unicode escapes have been converted
// to UTF-16 characters, so we're only searching for character
// values, not escapes.
&& isLatin(name);
}
static boolean isLatin(String s) {
int len = s.length();
for (int index = 0; index < len; index++) {
char c = s.charAt(index);
if (c > LARGEST_BASIC_LATIN) {
return false;
}
}
return true;
}
static final char LARGEST_BASIC_LATIN = 0x7f;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.vertispan.j2cl.tools.closure;

import com.google.javascript.jscomp.AbstractCompiler;
import com.google.javascript.jscomp.CompilerPass;
import com.google.javascript.jscomp.DiagnosticType;
import com.google.javascript.jscomp.NodeTraversal;
import com.google.javascript.jscomp.parsing.parser.FeatureSet;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import org.jspecify.nullness.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import static com.vertispan.j2cl.tools.closure.ConvertServiceLoaderProperties.isValidPropertyName;

/**
* Find and replaces references to keyed service loader requests with their implementations,
* avoiding the need to compile in the entire map of known entries.
*/
public class InlineServiceLoaderEntries extends NodeTraversal.AbstractPostOrderCallback implements CompilerPass {
static final DiagnosticType J2CL_VERTISPAN_SERVICELOADER_UNKNOWN_NODE =
DiagnosticType.warning("J2CL_VERTISPAN_SERVICELOADER_UNKNOWN_NODE",
"Unexpected node type");

public static final String KEY_SUFFIX = "$service$loader$key";
private final AbstractCompiler compiler;

private final Map<String, Node> writes = new HashMap<>();
private final Map<String, Node> reads = new HashMap<>();

public InlineServiceLoaderEntries(AbstractCompiler compiler) {
this.compiler = compiler;
}

@Override
public void process(Node externs, Node root) {
// Collect all references GETELEM references where the string literal ends with our specifies suffix
NodeTraversal.traverse(compiler, root, this);

// Match writes to corresponding reads, replacing reads with the inlined body of the written call.
// Leave the writes intact in case of dynamic (or unsolvable) reads
rewriteStaticReads();
}

@Override
public void visit(NodeTraversal t, Node n, @Nullable Node parent) {
if (Objects.requireNonNull(n.getToken()) == Token.OPTCHAIN_GETELEM || n.getToken() == Token.GETELEM) {
Node left = n.getFirstChild();
Node right = Objects.requireNonNull(left.getNext());
if (right.isStringLit() && right.getString().endsWith(KEY_SUFFIX) && isValidPropertyName(FeatureSet.ES3, right.getString())) {
if (Objects.requireNonNull(parent).getToken() == Token.ASSIGN) {
// record write
System.out.println("write " + compiler.toSource(parent));
writes.put(right.getString(), n.getNext().getFirstChild().getNext().getNext().getFirstChild().getFirstChild());
} else if (parent.getToken() == Token.CALL) {
// record read
System.out.println("read " + compiler.toSource(parent));
reads.put(right.getString(), parent);
} else {
t.report(parent, J2CL_VERTISPAN_SERVICELOADER_UNKNOWN_NODE);
}
}
}
}

private void rewriteStaticReads() {
for (Map.Entry<String, Node> read : reads.entrySet()) {
Node write = writes.get(read.getKey());
if (write == null) {
continue;
}
Node replacement = write.cloneTree(true);
read.getValue().replaceWith(replacement);
compiler.reportChangeToEnclosingScope(replacement);
}
}
}
Loading