Support bridges in interfaces. Fix for KT-12416: Missed bridges in js backend
#KT-12416 Fixed
This commit is contained in:
@@ -17,7 +17,7 @@
|
||||
package org.jetbrains.kotlin.backend.common.bridges
|
||||
|
||||
import org.jetbrains.kotlin.utils.DFS
|
||||
import java.util.HashSet
|
||||
import java.util.*
|
||||
|
||||
interface FunctionHandle {
|
||||
val isDeclaration: Boolean
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.jetbrains.kotlin.backend.common.bridges
|
||||
|
||||
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.Modality
|
||||
import org.jetbrains.kotlin.resolve.DescriptorUtils
|
||||
@@ -25,9 +26,10 @@ import org.jetbrains.kotlin.resolve.calls.callResolverUtil.isOrOverridesSynthesi
|
||||
|
||||
fun <Signature> generateBridgesForFunctionDescriptor(
|
||||
descriptor: FunctionDescriptor,
|
||||
signature: (FunctionDescriptor) -> Signature
|
||||
signature: (FunctionDescriptor) -> Signature,
|
||||
isBodyOwner: (DeclarationDescriptor) -> Boolean
|
||||
): Set<Bridge<Signature>> {
|
||||
return generateBridges(DescriptorBasedFunctionHandle(descriptor), { signature(it.descriptor) })
|
||||
return generateBridges(DescriptorBasedFunctionHandle(descriptor, isBodyOwner), { signature(it.descriptor) })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,8 +49,19 @@ fun <Signature> generateBridgesForFunctionDescriptor(
|
||||
* can generate a bridge near an implementation (of course, in case it has a super-declaration with a different signature). Ultimately this
|
||||
* eases the process of determining what bridges are already generated in our supertypes and need to be inherited, not regenerated.
|
||||
*/
|
||||
data class DescriptorBasedFunctionHandle(val descriptor: FunctionDescriptor) : FunctionHandle {
|
||||
private val overridden = descriptor.overriddenDescriptors.map { DescriptorBasedFunctionHandle(it.original) }
|
||||
class DescriptorBasedFunctionHandle(
|
||||
val descriptor: FunctionDescriptor,
|
||||
/*
|
||||
To generate proper bridges for non-abstract function
|
||||
we should know if the function declaration and its definition in the target platform are the same or not.
|
||||
For JS and JVM8 they are placed in interface classes and we need generate bridge for such function ('isAbstract' will return false).
|
||||
For JVM6 target function body generated in separate place (DefaultImpl) and method in interface is abstract
|
||||
For JVM6 target function body is generated in a separate place (DefaultImpls) and
|
||||
the method in the interface is abstract so we must not generate bridges for such cases.
|
||||
*/
|
||||
isBodyOwner: (DeclarationDescriptor) -> Boolean
|
||||
) : FunctionHandle {
|
||||
private val overridden = descriptor.overriddenDescriptors.map { DescriptorBasedFunctionHandle(it.original, isBodyOwner) }
|
||||
|
||||
override val isDeclaration: Boolean =
|
||||
descriptor.kind.isReal ||
|
||||
@@ -56,9 +69,17 @@ data class DescriptorBasedFunctionHandle(val descriptor: FunctionDescriptor) : F
|
||||
|
||||
override val isAbstract: Boolean =
|
||||
descriptor.modality == Modality.ABSTRACT ||
|
||||
DescriptorUtils.isInterface(descriptor.containingDeclaration)
|
||||
isBodyOwner(descriptor.containingDeclaration)
|
||||
|
||||
override fun getOverridden() = overridden
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return descriptor.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is DescriptorBasedFunctionHandle && descriptor == other.descriptor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@ import java.util.Set;
|
||||
|
||||
import static org.jetbrains.kotlin.builtins.KotlinBuiltIns.isNullableAny;
|
||||
import static org.jetbrains.kotlin.codegen.AsmUtil.*;
|
||||
import static org.jetbrains.kotlin.codegen.JvmCodegenUtil.isAnnotationOrJvm6Interface;
|
||||
import static org.jetbrains.kotlin.codegen.JvmCodegenUtil.isJvm8InterfaceMember;
|
||||
import static org.jetbrains.kotlin.codegen.serialization.JvmSerializationBindings.METHOD_FOR_FUNCTION;
|
||||
import static org.jetbrains.kotlin.descriptors.CallableMemberDescriptor.Kind.DECLARATION;
|
||||
import static org.jetbrains.kotlin.descriptors.annotations.AnnotationUseSiteTarget.*;
|
||||
@@ -95,6 +97,13 @@ public class FunctionCodegen {
|
||||
private final ClassBuilder v;
|
||||
private final MemberCodegen<?> memberCodegen;
|
||||
|
||||
private final Function1<DeclarationDescriptor, Boolean> IS_PURE_INTERFACE_CHECKER = new Function1<DeclarationDescriptor, Boolean>() {
|
||||
@Override
|
||||
public Boolean invoke(DeclarationDescriptor descriptor) {
|
||||
return JvmCodegenUtil.isAnnotationOrJvm6Interface(descriptor, state);
|
||||
}
|
||||
};
|
||||
|
||||
public FunctionCodegen(
|
||||
@NotNull CodegenContext owner,
|
||||
@NotNull ClassBuilder v,
|
||||
@@ -565,7 +574,7 @@ public class FunctionCodegen {
|
||||
public void generateBridges(@NotNull FunctionDescriptor descriptor) {
|
||||
if (descriptor instanceof ConstructorDescriptor) return;
|
||||
if (owner.getContextKind() == OwnerKind.DEFAULT_IMPLS) return;
|
||||
if (isInterface(descriptor.getContainingDeclaration())) return;
|
||||
if (isAnnotationOrJvm6Interface(descriptor.getContainingDeclaration(), state)) return;
|
||||
|
||||
// equals(Any?), hashCode(), toString() never need bridges
|
||||
if (isMethodOfAny(descriptor)) return;
|
||||
@@ -579,7 +588,8 @@ public class FunctionCodegen {
|
||||
if (!isSpecial) {
|
||||
bridgesToGenerate = ImplKt.generateBridgesForFunctionDescriptor(
|
||||
descriptor,
|
||||
getSignatureMapper(typeMapper)
|
||||
getSignatureMapper(typeMapper),
|
||||
IS_PURE_INTERFACE_CHECKER
|
||||
);
|
||||
if (!bridgesToGenerate.isEmpty()) {
|
||||
PsiElement origin = descriptor.getKind() == DECLARATION ? getSourceFromDescriptor(descriptor) : null;
|
||||
@@ -594,7 +604,8 @@ public class FunctionCodegen {
|
||||
else {
|
||||
Set<BridgeForBuiltinSpecial<Method>> specials = BuiltinSpecialBridgesUtil.generateBridgesForBuiltinSpecial(
|
||||
descriptor,
|
||||
getSignatureMapper(typeMapper)
|
||||
getSignatureMapper(typeMapper),
|
||||
IS_PURE_INTERFACE_CHECKER
|
||||
);
|
||||
|
||||
if (!specials.isEmpty()) {
|
||||
@@ -914,7 +925,12 @@ public class FunctionCodegen {
|
||||
iv.invokespecial(parentInternalName, delegateTo.getName(), delegateTo.getDescriptor());
|
||||
}
|
||||
else {
|
||||
iv.invokevirtual(v.getThisName(), delegateTo.getName(), delegateTo.getDescriptor());
|
||||
if (isJvm8InterfaceMember(descriptor, state)) {
|
||||
iv.invokeinterface(v.getThisName(), delegateTo.getName(), delegateTo.getDescriptor());
|
||||
}
|
||||
else {
|
||||
iv.invokevirtual(v.getThisName(), delegateTo.getName(), delegateTo.getDescriptor());
|
||||
}
|
||||
}
|
||||
|
||||
StackValue.coerce(delegateTo.getReturnType(), bridge.getReturnType(), iv);
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.jetbrains.kotlin.backend.common.bridges.DescriptorBasedFunctionHandle
|
||||
import org.jetbrains.kotlin.backend.common.bridges.findAllReachableDeclarations
|
||||
import org.jetbrains.kotlin.backend.common.bridges.findConcreteSuperDeclaration
|
||||
import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.FunctionDescriptor
|
||||
import org.jetbrains.kotlin.descriptors.Modality
|
||||
import org.jetbrains.kotlin.load.java.BuiltinMethodsWithSpecialGenericSignature
|
||||
@@ -46,14 +47,15 @@ class BridgeForBuiltinSpecial<out Signature : Any>(
|
||||
object BuiltinSpecialBridgesUtil {
|
||||
@JvmStatic fun <Signature : Any> generateBridgesForBuiltinSpecial(
|
||||
function: FunctionDescriptor,
|
||||
signatureByDescriptor: (FunctionDescriptor) -> Signature
|
||||
signatureByDescriptor: (FunctionDescriptor) -> Signature,
|
||||
isBodyOwner: (DeclarationDescriptor) -> Boolean
|
||||
): Set<BridgeForBuiltinSpecial<Signature>> {
|
||||
|
||||
val functionHandle = DescriptorBasedFunctionHandle(function)
|
||||
val functionHandle = DescriptorBasedFunctionHandle(function, isBodyOwner)
|
||||
val fake = !functionHandle.isDeclaration
|
||||
val overriddenBuiltin = function.getOverriddenBuiltinReflectingJvmDescriptor()!!
|
||||
|
||||
val reachableDeclarations = findAllReachableDeclarations(function)
|
||||
val reachableDeclarations = findAllReachableDeclarations(function, isBodyOwner)
|
||||
|
||||
// e.g. `getSize()I`
|
||||
val methodItself = signatureByDescriptor(function)
|
||||
@@ -79,8 +81,8 @@ object BuiltinSpecialBridgesUtil {
|
||||
|
||||
if (fake) {
|
||||
for (overridden in function.overriddenDescriptors.map { it.original }) {
|
||||
if (!DescriptorBasedFunctionHandle(overridden).isAbstract) {
|
||||
commonBridges.removeAll(findAllReachableDeclarations(overridden).map(signatureByDescriptor))
|
||||
if (!DescriptorBasedFunctionHandle(overridden, isBodyOwner).isAbstract) {
|
||||
commonBridges.removeAll(findAllReachableDeclarations(overridden, isBodyOwner).map(signatureByDescriptor))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +91,7 @@ object BuiltinSpecialBridgesUtil {
|
||||
|
||||
// Can be null if special builtin is final (e.g. 'name' in Enum)
|
||||
// because there should be no stubs for override in subclasses
|
||||
val superImplementationDescriptor = findSuperImplementationForStubDelegation(function, fake)
|
||||
val superImplementationDescriptor = findSuperImplementationForStubDelegation(function, fake, isBodyOwner)
|
||||
if (superImplementationDescriptor != null) {
|
||||
bridges.add(BridgeForBuiltinSpecial(methodItself, signatureByDescriptor(superImplementationDescriptor), isDelegateToSuper = true))
|
||||
}
|
||||
@@ -121,16 +123,23 @@ object BuiltinSpecialBridgesUtil {
|
||||
}
|
||||
|
||||
|
||||
private fun findSuperImplementationForStubDelegation(function: FunctionDescriptor, fake: Boolean): FunctionDescriptor? {
|
||||
private fun findSuperImplementationForStubDelegation(
|
||||
function: FunctionDescriptor,
|
||||
fake: Boolean,
|
||||
isBodyOwner: (DeclarationDescriptor) -> Boolean
|
||||
): FunctionDescriptor? {
|
||||
if (function.modality != Modality.OPEN || !fake) return null
|
||||
val implementation = findConcreteSuperDeclaration(DescriptorBasedFunctionHandle(function)).descriptor
|
||||
val implementation = findConcreteSuperDeclaration(DescriptorBasedFunctionHandle(function, isBodyOwner)).descriptor
|
||||
if (DescriptorUtils.isInterface(implementation.containingDeclaration)) return null
|
||||
|
||||
return implementation
|
||||
}
|
||||
|
||||
private fun findAllReachableDeclarations(functionDescriptor: FunctionDescriptor): MutableSet<FunctionDescriptor> =
|
||||
findAllReachableDeclarations(DescriptorBasedFunctionHandle(functionDescriptor)).map { it.descriptor }.toMutableSet()
|
||||
private fun findAllReachableDeclarations(
|
||||
functionDescriptor: FunctionDescriptor,
|
||||
isBodyOwner: (DeclarationDescriptor) -> Boolean
|
||||
): MutableSet<FunctionDescriptor> =
|
||||
findAllReachableDeclarations(DescriptorBasedFunctionHandle(functionDescriptor, isBodyOwner)).map { it.descriptor }.toMutableSet()
|
||||
|
||||
private fun <Signature> CallableMemberDescriptor.getSpecialBridgeSignatureIfExists(
|
||||
signatureByDescriptor: (FunctionDescriptor) -> Signature
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
interface A<T> {
|
||||
fun foo(t: T, u: Int) = "A"
|
||||
}
|
||||
|
||||
interface B<T, U> {
|
||||
fun foo(t: T, u: U) = "B"
|
||||
}
|
||||
|
||||
interface Z1 : A<String>, B<String, Int> {
|
||||
override fun foo(t: String, u: Int) = "Z1"
|
||||
}
|
||||
|
||||
interface Z2 : B<String, Int>, A<String> {
|
||||
override fun foo(t: String, u: Int) = "Z2"
|
||||
}
|
||||
|
||||
class Z1C : Z1 {
|
||||
|
||||
}
|
||||
|
||||
class Z2C : Z2 {
|
||||
|
||||
}
|
||||
|
||||
fun box(): String {
|
||||
val z1 = Z1C()
|
||||
val z2 = Z2C()
|
||||
val z1a: A<String> = z1
|
||||
val z1b: B<String, Int> = z1
|
||||
val z2a: A<String> = z2
|
||||
val z2b: B<String, Int> = z2
|
||||
return when {
|
||||
z1.foo("", 0) != "Z1" -> "Fail #1"
|
||||
z1a.foo("", 0) != "Z1" -> "Fail #2"
|
||||
z1b.foo("", 0) != "Z1" -> "Fail #3" //FAIL
|
||||
z2.foo("", 0) != "Z2" -> "Fail #4"
|
||||
z2a.foo("", 0) != "Z2" -> "Fail #5"
|
||||
z2b.foo("", 0) != "Z2" -> "Fail #6"
|
||||
else -> "OK"
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
interface A<T> {
|
||||
fun foo(t: T, u: Int) = "A"
|
||||
}
|
||||
|
||||
interface B<T, U> {
|
||||
fun foo(t: T, u: U) = "B"
|
||||
}
|
||||
|
||||
interface Z1 : A<String>, B<String, Int> {
|
||||
override fun foo(t: String, u: Int) = "Z1"
|
||||
}
|
||||
|
||||
interface Z2 : B<String, Int>, A<String> {
|
||||
override fun foo(t: String, u: Int) = "Z2"
|
||||
}
|
||||
|
||||
|
||||
class Z1Class : Z1 {
|
||||
}
|
||||
|
||||
class Z2Class : Z2 {
|
||||
}
|
||||
|
||||
fun box(): String {
|
||||
val z1 = Z1Class()
|
||||
val z2 = Z2Class()
|
||||
val z1a: A<String> = z1
|
||||
val z1b: B<String, Int> = z1
|
||||
val z2a: A<String> = z2
|
||||
val z2b: B<String, Int> = z2
|
||||
return when {
|
||||
z1.foo("", 0) != "Z1" -> "Fail #1"
|
||||
z1a.foo("", 0) != "Z1" -> "Fail #2"
|
||||
z1b.foo("", 0) != "Z1" -> "Fail #3" //FAIL
|
||||
z2.foo("", 0) != "Z2" -> "Fail #4"
|
||||
z2a.foo("", 0) != "Z2" -> "Fail #5"
|
||||
z2b.foo("", 0) != "Z2" -> "Fail #6"
|
||||
else -> "OK"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// KOTLIN_CONFIGURATION_FLAGS: +JVM.JVM_8_TARGET
|
||||
interface Test<T> {
|
||||
fun test(p: T): T {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
class TestClass : Test<String> {
|
||||
override fun test(p: String): String {
|
||||
return p + "K"
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> execute(t: Test<T>, p: T): T {
|
||||
return t.test(p)
|
||||
}
|
||||
|
||||
fun box(): String {
|
||||
return execute(TestClass(), "O")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// KOTLIN_CONFIGURATION_FLAGS: +JVM.JVM_8_TARGET
|
||||
interface Test<T> {
|
||||
fun test(p: T): T {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
interface Test2: Test<String> {
|
||||
override fun test(p: String): String {
|
||||
return p + "K"
|
||||
}
|
||||
}
|
||||
|
||||
class TestClass : Test2 {
|
||||
}
|
||||
|
||||
fun <T> execute(t: Test<T>, p: T): T {
|
||||
return t.test(p)
|
||||
}
|
||||
|
||||
fun box(): String {
|
||||
return execute(TestClass(), "O")
|
||||
}
|
||||
|
||||
+12
@@ -127,6 +127,18 @@ public class BlackBoxWithJava8CodegenTestGenerated extends AbstractBlackBoxCodeg
|
||||
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/java8/box/jvm8"), Pattern.compile("^(.+)\\.kt$"), true);
|
||||
}
|
||||
|
||||
@TestMetadata("bridgeInClass.kt")
|
||||
public void testBridgeInClass() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/java8/box/jvm8/bridgeInClass.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("bridgeInInterface.kt")
|
||||
public void testBridgeInInterface() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/java8/box/jvm8/bridgeInInterface.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("simpleCall.kt")
|
||||
public void testSimpleCall() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/java8/box/jvm8/simpleCall.kt");
|
||||
|
||||
@@ -994,6 +994,12 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("kt12416.kt")
|
||||
public void testKt12416() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/bridges/kt12416.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("kt1939.kt")
|
||||
public void testKt1939() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/bridges/kt1939.kt");
|
||||
@@ -1162,6 +1168,12 @@ public class BlackBoxCodegenTestGenerated extends AbstractBlackBoxCodegenTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("twoParentsWithDifferentMethodsTwoBridges2.kt")
|
||||
public void testTwoParentsWithDifferentMethodsTwoBridges2() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/bridges/twoParentsWithDifferentMethodsTwoBridges2.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("twoParentsWithTheSameMethodOneBridge.kt")
|
||||
public void testTwoParentsWithTheSameMethodOneBridge() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/bridges/twoParentsWithTheSameMethodOneBridge.kt");
|
||||
|
||||
@@ -125,6 +125,12 @@ public class BridgeTestGenerated extends AbstractBridgeTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("kt12416.kt")
|
||||
public void testKt12416() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/bridges/kt12416.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("kt1939.kt")
|
||||
public void testKt1939() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/bridges/kt1939.kt");
|
||||
@@ -287,6 +293,12 @@ public class BridgeTestGenerated extends AbstractBridgeTest {
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("twoParentsWithDifferentMethodsTwoBridges2.kt")
|
||||
public void testTwoParentsWithDifferentMethodsTwoBridges2() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/bridges/twoParentsWithDifferentMethodsTwoBridges2.kt");
|
||||
doTest(fileName);
|
||||
}
|
||||
|
||||
@TestMetadata("twoParentsWithTheSameMethodOneBridge.kt")
|
||||
public void testTwoParentsWithTheSameMethodOneBridge() throws Exception {
|
||||
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/box/bridges/twoParentsWithTheSameMethodOneBridge.kt");
|
||||
|
||||
+7
-2
@@ -77,6 +77,8 @@ class ClassTranslator private constructor(
|
||||
|
||||
private fun isTrait(): Boolean = descriptor.kind == ClassKind.INTERFACE
|
||||
|
||||
private fun isAnnotation(): Boolean = descriptor.kind == ClassKind.ANNOTATION_CLASS
|
||||
|
||||
private fun generateClassCreateInvocationArguments() {
|
||||
val properties = SmartList<JsPropertyInitializer>()
|
||||
val staticProperties = SmartList<JsPropertyInitializer>()
|
||||
@@ -369,7 +371,7 @@ class ClassTranslator private constructor(
|
||||
}
|
||||
|
||||
private fun generatedBridgeMethods(properties: MutableList<JsPropertyInitializer>) {
|
||||
if (isTrait()) return
|
||||
if (isAnnotation()) return
|
||||
|
||||
generateBridgesToTraitImpl(properties)
|
||||
|
||||
@@ -387,7 +389,10 @@ class ClassTranslator private constructor(
|
||||
private fun generateOtherBridges(properties: MutableList<JsPropertyInitializer>) {
|
||||
for (memberDescriptor in descriptor.defaultType.memberScope.getContributedDescriptors()) {
|
||||
if (memberDescriptor is FunctionDescriptor) {
|
||||
val bridgesToGenerate = generateBridgesForFunctionDescriptor(memberDescriptor, identity())
|
||||
val bridgesToGenerate = generateBridgesForFunctionDescriptor(memberDescriptor, identity()) {
|
||||
//There is no DefaultImpls in js backend so if method non-abstract it should be recognized as non-abstract on bridges calculation
|
||||
false
|
||||
}
|
||||
|
||||
for (bridge in bridgesToGenerate) {
|
||||
generateBridge(bridge, properties)
|
||||
|
||||
Reference in New Issue
Block a user