Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -628,9 +628,25 @@ private void replaceMethodCall(ImMethodCall mc) {
ImExprs arguments = JassIm.ImExprs(receiver);
arguments.addAll(mc.getArguments().removeAll());

ImFunction dispatch = dispatchFuncs.get(mc.getMethod());
ImMethod method = mc.getMethod();
// Fast path: with unchecked dispatch, a monomorphic method call can be lowered
// directly to its implementation function.
if (!checkedDispatch
&& !method.getIsAbstract()
&& method.getSubMethods().isEmpty()) {
mc.replaceBy(JassIm.ImFunctionCall(
mc.getTrace(),
method.getImplementation(),
JassIm.ImTypeArguments(),
arguments,
false,
CallType.NORMAL));
return;
}

ImFunction dispatch = dispatchFuncs.get(method);
if (dispatch == null) {
throw new CompileError(mc.attrTrace().attrSource(), "Could not find dispatch for " + mc.getMethod().getName());
throw new CompileError(mc.attrTrace().attrSource(), "Could not find dispatch for " + method.getName());
}
mc.replaceBy(JassIm.ImFunctionCall(mc.getTrace(), dispatch, JassIm.ImTypeArguments(), arguments, false, CallType.NORMAL));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,8 @@ private boolean isGlobalInitStmt(ImSet s, ImVar v) {
}

private void collectGenericUsages(Element element) {
// Cache expensive recursive submethod checks within this traversal.
Map<ImMethod, Boolean> hasGenericSubmethodCache = new IdentityHashMap<>();
element.accept(new Element.DefaultVisitor() {
@Override
public void visit(ImFunctionCall f) {
Expand All @@ -967,7 +969,22 @@ public void visit(ImFunctionCall f) {
@Override
public void visit(ImMethodCall mc) {
super.visit(mc);
if (!mc.getTypeArguments().isEmpty()) {
ImMethod method = mc.getMethod();
boolean hasTypeArgs = !mc.getTypeArguments().isEmpty();
boolean needsDispatchSpecialization = false;
// If type args are present, specialization is unconditional, so avoid extra checks.
if (!hasTypeArgs) {
// Interface/base dispatch methods can be non-generic but still require specialization
// when they dispatch to generic implementors.
needsDispatchSpecialization = methodImplementationIsGeneric(method);
if (!needsDispatchSpecialization) {
needsDispatchSpecialization = hasGenericSubmethodCache.computeIfAbsent(
method,
EliminateGenerics.this::hasGenericSubmethodImplementation
);
}
}
if (hasTypeArgs || needsDispatchSpecialization) {
dbg("COLLECT GenericMethodCall: method=" + mc.getMethod().getName() + " " + id(mc.getMethod())
+ " impl=" + (mc.getMethod().getImplementation() == null ? "null" : (mc.getMethod().getImplementation().getName() + " " + id(mc.getMethod().getImplementation())))
+ " owningClass=" + (mc.getMethod().attrClass() == null ? "null" : (mc.getMethod().attrClass().getName() + " " + id(mc.getMethod().attrClass())))
Expand Down Expand Up @@ -1109,6 +1126,30 @@ public void visit(ImTypeIdOfClass f) {
});
}

private boolean methodImplementationIsGeneric(ImMethod method) {
ImFunction implementation = method.getImplementation();
return implementation != null && !implementation.getTypeVariables().isEmpty();
}

private boolean hasGenericSubmethodImplementation(ImMethod method) {
return hasGenericSubmethodImplementation(method, Collections.newSetFromMap(new IdentityHashMap<>()));
}

private boolean hasGenericSubmethodImplementation(ImMethod method, Set<ImMethod> visited) {
if (!visited.add(method)) {
return false;
}
for (ImMethod subMethod : method.getSubMethods()) {
if (methodImplementationIsGeneric(subMethod)) {
return true;
}
if (hasGenericSubmethodImplementation(subMethod, visited)) {
return true;
}
}
return false;
}

static boolean isGenericType(ImType type) {
return type.match(new ImType.Matcher<Boolean>() {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2091,6 +2091,49 @@ public void fullArrayListTest() throws IOException {
assertEquals(count, 2);

assertFalse(compiled.contains("ArrayList_nextFreeIndex_"));
// In checked mode, virtual method calls should go through dispatch.
assertTrue(compiled.contains("dispatch_ArrayList"));
}

@Test
public void fullArrayListTestUncheckedDispatch() throws IOException {
test().withStdLib().uncheckedDispatch().executeProg().executeTests().file(new File(TEST_DIR + "arrayList.wurst"));

String compiled = Files.toString(
new File(TEST_OUTPUT_PATH + "GenericsWithTypeclassesTests_fullArrayListTestUncheckedDispatch_no_opts.jim"),
Charsets.UTF_8);

// In unchecked dispatch mode, monomorphic ArrayList<T>.get should be lowered directly.
assertTrue(compiled.contains("ArrayList_get"));
assertFalse(compiled.contains("dispatch_ArrayList"));
}

@Test
public void fullReactiveGenericDispatchTest() throws IOException {
test().withStdLib().executeProg().executeTests().file(new File(TEST_DIR + "reactiveGenericsDispatch.wurst"));

String compiled = Files.toString(
new File(TEST_OUTPUT_PATH + "GenericsWithTypeclassesTests_fullReactiveGenericDispatchTest_no_opts.jim"),
Charsets.UTF_8);

// In checked mode, polymorphic interface calls are dispatched and guarded.
assertTrue(compiled.contains("dispatch_ReactiveSource_ReactiveSource_subscribe"));
assertTrue(compiled.contains("Nullpointer exception when calling ReactiveSource.subscribe"));
assertTrue(compiled.contains("Called ReactiveSource.subscribe on invalid object."));
}

@Test
public void fullReactiveGenericDispatchTestUncheckedDispatch() throws IOException {
test().withStdLib().uncheckedDispatch().executeProg().executeTests().file(new File(TEST_DIR + "reactiveGenericsDispatch.wurst"));

String compiled = Files.toString(
new File(TEST_OUTPUT_PATH + "GenericsWithTypeclassesTests_fullReactiveGenericDispatchTestUncheckedDispatch_no_opts.jim"),
Charsets.UTF_8);

// ReactiveSource is polymorphic, so dispatch remains, but safety checks should be removed.
assertTrue(compiled.contains("dispatch_ReactiveSource_ReactiveSource_subscribe"));
assertFalse(compiled.contains("Nullpointer exception when calling ReactiveSource.subscribe"));
assertFalse(compiled.contains("Called ReactiveSource.subscribe on invalid object."));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class TestConfig {
private boolean stopOnFirstError = true;
private boolean runCompiletimeFunctions;
private boolean testLua = false;
private boolean uncheckedDispatch = false;

TestConfig(String name) {
this.name = name;
Expand Down Expand Up @@ -123,6 +124,15 @@ public TestConfig executeProgOnlyAfterTransforms(boolean b) {
return this;
}

public TestConfig uncheckedDispatch() {
return uncheckedDispatch(true);
}

public TestConfig uncheckedDispatch(boolean b) {
this.uncheckedDispatch = b;
return this;
}

TestConfig expectError(String expectedError) {
this.expectedError = expectedError;
return this;
Expand Down Expand Up @@ -186,6 +196,9 @@ private CompilationResult testScript() {
if (withStdLib) {
runArgs = runArgs.with("-lib", StdLib.getLib());
}
if (uncheckedDispatch) {
runArgs = runArgs.with("-uncheckedDispatch");
}
if (runCompiletimeFunctions) {
runArgs = runArgs.with("-runcompiletimefunctions");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package ReactiveGenericDispatchRegression
import NoWurst
import Integer
import Wurstunit

interface ReactiveSource
function subscribe(Observer observer)
function unsubscribe(Observer observer)

class Observer
int dependencyChanges = 0

function trackSource(ReactiveSource source)
source.subscribe(this)

function untrackSource(ReactiveSource source)
source.unsubscribe(this)

function onDependencyChanged()
dependencyChanges += 1


class Signal<T:> implements ReactiveSource
private T value
private Observer subscriber = null

construct(T initial)
value = initial

function set(T newValue)
value = newValue
if subscriber != null
subscriber.onDependencyChanged()

function subscriberCount() returns int
return subscriber == null ? 0 : 1

override function subscribe(Observer observer)
subscriber = observer

override function unsubscribe(Observer observer)
if subscriber == observer
subscriber = null


class PlainSource implements ReactiveSource
private Observer subscriber = null

function fire()
if subscriber != null
subscriber.onDependencyChanged()

function subscriberCount() returns int
return subscriber == null ? 0 : 1

override function subscribe(Observer observer)
subscriber = observer

override function unsubscribe(Observer observer)
if subscriber == observer
subscriber = null

init
let genericObserver = new Observer()
let genericSignal = new Signal<int>(0)
genericObserver.trackSource(genericSignal)
if genericSignal.subscriberCount() != 1
testFail("generic subscribe dispatch failed")
genericSignal.set(1)
if genericObserver.dependencyChanges != 1
testFail("generic notify dispatch failed")
genericObserver.untrackSource(genericSignal)
if genericSignal.subscriberCount() != 0
testFail("generic unsubscribe dispatch failed")

let plainObserver = new Observer()
let plainSource = new PlainSource()
plainObserver.trackSource(plainSource)
if plainSource.subscriberCount() != 1
testFail("plain subscribe dispatch failed")
plainSource.fire()
if plainObserver.dependencyChanges != 1
testFail("plain notify dispatch failed")
plainObserver.untrackSource(plainSource)
if plainSource.subscriberCount() != 0
testFail("plain unsubscribe dispatch failed")

destroy genericSignal
destroy genericObserver
destroy plainSource
destroy plainObserver
testSuccess()
Loading