From 1b989f249659c7878595afbbe94a5d4aa62bc929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 3 Apr 2025 17:28:29 +0200 Subject: [PATCH] Merge the itables into the corresponding vtables. This is a trade-off on memory and code size. The big advantage is that run-time instances of our classes use one fewer word each, since we do not have an `itables` field anymore. There could be some execution time improvements as a side effect, since we have one fewer word to initialize. There are two disadvantages, but IMO they are comparatively minor: * We cannot use the "one empty itables to rule them all" trick anymore. Classes that implement no interfaces need to list their N `null` values. This has both a code size impact and a constant run-time memory cost. * We cannot share the itables of array classes either. This has no impact on code size (there is a single place in the code where we initialize those fields), but it does add run-time footprint to the vtables of each array type that gets created. Interface method calls are not impacted, neither in the code we generate, nor in their run-time performance. Virtual method calls are only impacted insofar as the index of virtual method pointers is bumped up by the number of itable slots. This can add one more byte to some calls, due to the var-length encoding. If we get very unlucky, it could also push some vtable method pointers past a pre-fetch boundary, slowing down the calls at run-time. But I don't really think that's plausible. --- .../backend/wasmemitter/ClassEmitter.scala | 139 +++++++----------- .../backend/wasmemitter/CoreWasmLib.scala | 48 +----- .../linker/backend/wasmemitter/Emitter.scala | 1 - .../backend/wasmemitter/FunctionEmitter.scala | 9 +- .../backend/wasmemitter/Preprocessor.scala | 7 - .../linker/backend/wasmemitter/README.md | 48 +++--- .../linker/backend/wasmemitter/SWasmGen.scala | 11 +- .../linker/backend/wasmemitter/VarGen.scala | 14 +- .../backend/wasmemitter/WasmContext.scala | 3 +- 9 files changed, 97 insertions(+), 183 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala index 88cdbdbae8..645974dfa0 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/ClassEmitter.scala @@ -49,7 +49,7 @@ class ClassEmitter(coreSpec: CoreSpec) { if (classInfo.hasRuntimeTypeInfo && !(clazz.kind.isClass && clazz.hasDirectInstances)) { // Gen typeData -- for concrete Scala classes, we do it as part of the vtable generation instead val typeDataFieldValues = genTypeDataFieldValues(clazz, Nil) - genTypeDataGlobal(clazz.className, genTypeID.typeData, typeDataFieldValues, Nil) + genTypeDataGlobal(clazz.className, genTypeID.typeData, typeDataFieldValues, Nil, Nil) } // Declare static fields @@ -138,15 +138,6 @@ class ClassEmitter(coreSpec: CoreSpec) { } } - /** Generate common itable global for all array classes. */ - def genGlobalArrayClassItable()(implicit ctx: WasmContext): Unit = { - genGlobalClassItable( - genGlobalID.arrayClassITable, ctx.getClassInfo(ObjectClass), - List(SerializableClass, CloneableClass), - OriginalName(genGlobalID.arrayClassITable.toString()) - ) - } - private def genIsJSClassInstanceFunction(clazz: LinkedClass)( implicit ctx: WasmContext): Option[wanme.FunctionID] = { implicit val noPos: Position = Position.NoPosition @@ -330,10 +321,11 @@ class ClassEmitter(coreSpec: CoreSpec) { } private def genTypeDataGlobal(className: ClassName, typeDataTypeID: wanme.TypeID, - typeDataFieldValues: List[wa.Instr], vtableElems: List[wa.RefFunc])( + typeDataFieldValues: List[wa.Instr], itableSlots: List[wa.Instr], + vtableElems: List[wa.RefFunc])( implicit ctx: WasmContext): Unit = { val instrs: List[wa.Instr] = - typeDataFieldValues ::: vtableElems ::: wa.StructNew(typeDataTypeID) :: Nil + typeDataFieldValues ::: itableSlots ::: vtableElems ::: wa.StructNew(typeDataTypeID) :: Nil ctx.addGlobal( wamod.Global( genGlobalID.forVTable(className), @@ -356,19 +348,17 @@ class ClassEmitter(coreSpec: CoreSpec) { val isAbstractClass = !clazz.hasDirectInstances - // Generate the vtable and itable for concrete classes + // Generate the vtable for concrete classes if (!isAbstractClass) { // Generate an actual vtable, which we integrate into the typeData val reflectiveProxies = classInfo.resolvedMethodInfos.valuesIterator.filter(_.methodName.isReflectiveProxy).toList val typeDataFieldValues = genTypeDataFieldValues(clazz, reflectiveProxies) + val itableSlots = genItableSlots(classInfo, clazz.ancestors) val vtableElems = classInfo.tableEntries.map { methodName => wa.RefFunc(classInfo.resolvedMethodInfos(methodName).tableEntryID) } - genTypeDataGlobal(className, vtableTypeID, typeDataFieldValues, vtableElems) - - // Generate the itable - genGlobalClassItable(clazz) + genTypeDataGlobal(className, vtableTypeID, typeDataFieldValues, itableSlots, vtableElems) } // Declare the struct type for the class @@ -378,12 +368,6 @@ class ClassEmitter(coreSpec: CoreSpec) { watpe.RefType(vtableTypeID), isMutable = false ) - val itablesField = watpe.StructField( - genFieldID.objStruct.itables, - itablesOriginalName, - watpe.RefType(genTypeID.itables), - isMutable = false - ) val fields = classInfo.allFieldDefs.map { field => watpe.StructField( genFieldID.forClassInstanceField(field.name.name), @@ -405,7 +389,7 @@ class ClassEmitter(coreSpec: CoreSpec) { } val structTypeID = genTypeID.forClass(className) val superType = clazz.superClass.map(s => genTypeID.forClass(s.name)) - val structType = watpe.StructType(vtableField :: itablesField :: fields ::: jlClassDataField) + val structType = watpe.StructType(vtableField :: fields ::: jlClassDataField) val subType = watpe.SubType( structTypeID, makeDebugName(ns.ClassInstance, className), @@ -461,6 +445,14 @@ class ClassEmitter(coreSpec: CoreSpec) { implicit ctx: WasmContext): wanme.TypeID = { val className = classInfo.name val typeID = genTypeID.forVTable(className) + val itableSlotFields = (0 until ctx.itablesLength).map { i => + watpe.StructField( + genFieldID.vtableStruct.itableSlot(i), + OriginalName.NoOriginalName, + watpe.RefType.nullable(watpe.HeapType.Struct), + isMutable = false + ) + }.toList val vtableFields = classInfo.tableEntries.map { methodName => watpe.StructField( @@ -474,7 +466,7 @@ class ClassEmitter(coreSpec: CoreSpec) { case None => genTypeID.typeData case Some(s) => genTypeID.forVTable(s.name) } - val structType = watpe.StructType(ctx.coreLib.typeDataStructFields ::: vtableFields) + val structType = watpe.StructType(ctx.coreLib.typeDataStructFields ::: itableSlotFields ::: vtableFields) val subType = watpe.SubType( typeID, makeDebugName(ns.VTable, className), @@ -525,10 +517,10 @@ class ClassEmitter(coreSpec: CoreSpec) { /* Test whether the itable at the target interface's slot is indeed an * instance of that interface's itable struct type. */ - fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.itables) + fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) fb += wa.StructGet( - genTypeID.itables, - genFieldID.itablesStruct.itableSlot(classInfo.itableIdx) + genTypeID.ObjectVTable, + genFieldID.vtableStruct.itableSlot(classInfo.itableIdx) ) fb += wa.RefTest(watpe.RefType(genTypeID.forITable(className))) fb += wa.Return @@ -642,12 +634,6 @@ class ClassEmitter(coreSpec: CoreSpec) { fb.setResultType(watpe.RefType(structTypeID)) fb += wa.GlobalGet(genGlobalID.forVTable(className)) - - if (classInfo.classImplementsAnyInterface) - fb += wa.GlobalGet(genGlobalID.forITable(className)) - else - fb += wa.GlobalGet(genGlobalID.emptyITable) - classInfo.allFieldDefs.foreach { f => fb += genZeroOf(f.ftpe) } @@ -689,9 +675,8 @@ class ClassEmitter(coreSpec: CoreSpec) { fb += wa.RefCast(structRefType) fb += wa.LocalSet(fromTypedLocal) - // Push vtable and itables on the stack (there is at least Cloneable in the itables) + // Push the vtable on the stack fb += wa.GlobalGet(genGlobalID.forVTable(className)) - fb += wa.GlobalGet(genGlobalID.forITable(className)) // Push every field of `fromTyped` on the stack info.allFieldDefs.foreach { field => @@ -822,55 +807,6 @@ class ClassEmitter(coreSpec: CoreSpec) { fb.buildAndAddToModule() } - /** Generates the global instance of the class itable. - * - * If the class implements no interface at all, we skip this step. Instead, - * we will use the unique `emptyITable` as itable for this class. - */ - private def genGlobalClassItable(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { - val className = clazz.className - val classInfo = ctx.getClassInfo(className) - if (classInfo.classImplementsAnyInterface) { - genGlobalClassItable( - genGlobalID.forITable(className), - classInfo, - clazz.ancestors, - makeDebugName(ns.ITable, classInfo.name) - ) - } - } - - private def genGlobalClassItable(classITableGlobalID: wanme.GlobalID, - classInfoForResolving: WasmContext.ClassInfo, ancestors: List[ClassName], - originalName: OriginalName)( - implicit ctx: WasmContext): Unit = { - val itablesInit = Array.fill[List[wa.Instr]](ctx.itablesLength) { - List(wa.RefNull(watpe.HeapType.Struct)) - } - val resolvedMethodInfos = classInfoForResolving.resolvedMethodInfos - - for { - ancestor <- ancestors - // Use getClassInfoOption in case the reachability analysis got rid of those interfaces - interfaceInfo <- ctx.getClassInfoOption(ancestor) - if interfaceInfo.isInterface - } { - val init = interfaceInfo.tableEntries.map { method => - wa.RefFunc(resolvedMethodInfos(method).tableEntryID) - } :+ wa.StructNew(genTypeID.forITable(ancestor)) - itablesInit(interfaceInfo.itableIdx) = init - } - - val global = wamod.Global( - classITableGlobalID, - originalName, - isMutable = false, - watpe.RefType(genTypeID.itables), - wa.Expr(itablesInit.flatten.toList :+ wa.StructNew(genTypeID.itables)) - ) - ctx.addGlobal(global) - } - private def genInterface(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { assert(clazz.kind == ClassKind.Interface) // gen itable type @@ -1563,5 +1499,36 @@ object ClassEmitter { private val thisOriginalName: OriginalName = OriginalName("this") private val vtableOriginalName: OriginalName = OriginalName("vtable") - private val itablesOriginalName: OriginalName = OriginalName("itables") + + /** Generates the itable slots of a class. + * + * @param classInfoForResolving + * The `ClassInfo` from which to resolve methods. This is normally the + * class info of the class for which we are generating the itable slots. + * For the itable slots of array classes, it must be the info of `jl.Object`. + * @param ancestors + * The list of ancestors of the target class. + */ + def genItableSlots(classInfoForResolving: WasmContext.ClassInfo, + ancestors: List[ClassName])( + implicit ctx: WasmContext): List[wa.Instr] = { + val itablesInit = Array.fill[List[wa.Instr]](ctx.itablesLength) { + List(wa.RefNull(watpe.HeapType.Struct)) + } + val resolvedMethodInfos = classInfoForResolving.resolvedMethodInfos + + for { + ancestor <- ancestors + // Use getClassInfoOption in case the reachability analysis got rid of those interfaces + interfaceInfo <- ctx.getClassInfoOption(ancestor) + if interfaceInfo.isInterface + } { + val init = interfaceInfo.tableEntries.map { method => + wa.RefFunc(resolvedMethodInfos(method).tableEntryID) + } :+ wa.StructNew(genTypeID.forITable(ancestor)) + itablesInit(interfaceInfo.itableIdx) = init + } + + itablesInit.flatten.toList + } } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala index 50d9ac0dff..cc5fb760ea 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/CoreWasmLib.scala @@ -117,7 +117,6 @@ final class CoreWasmLib(coreSpec: CoreSpec, globalInfo: LinkedGlobalInfo) { genImports() - genEmptyITable() genPrimitiveTypeDataGlobals() genHelperDefinitions() @@ -175,20 +174,6 @@ final class CoreWasmLib(coreSpec: CoreSpec, globalInfo: LinkedGlobalInfo) { ArrayType(FieldType(RefType(genTypeID.typeData), isMutable = false)) ) - genCoreType( - genTypeID.itables, - StructType( - (0 until ctx.itablesLength).map { i => - StructField( - genFieldID.itablesStruct.itableSlot(i), - OriginalName.NoOriginalName, - RefType.nullable(HeapType.Struct), - isMutable = false - ) - }.toList - ) - ) - genCoreType( genTypeID.reflectiveProxies, ArrayType(FieldType(RefType(genTypeID.reflectiveProxy), isMutable = false)) @@ -234,12 +219,6 @@ final class CoreWasmLib(coreSpec: CoreSpec, globalInfo: LinkedGlobalInfo) { RefType(vtableTypeID), isMutable = false ) - val itablesField = StructField( - genFieldID.objStruct.itables, - OriginalName(genFieldID.objStruct.itables.toString()), - RefType(genTypeID.itables), - isMutable = false - ) val typeRefsWithArrays: List[(TypeID, TypeID)] = List( @@ -266,7 +245,7 @@ final class CoreWasmLib(coreSpec: CoreSpec, globalInfo: LinkedGlobalInfo) { val superType = genTypeID.ObjectStruct val structType = StructType( - List(vtableField, itablesField, underlyingArrayField) + List(vtableField, underlyingArrayField) ) val subType = SubType(structTypeID, origName, isFinal = true, Some(superType), structType) ctx.mainRecType.addSubType(subType) @@ -431,18 +410,6 @@ final class CoreWasmLib(coreSpec: CoreSpec, globalInfo: LinkedGlobalInfo) { // --- Global definitions --- - private def genEmptyITable()(implicit ctx: WasmContext): Unit = { - ctx.addGlobal( - Global( - genGlobalID.emptyITable, - OriginalName(genGlobalID.emptyITable.toString()), - isMutable = false, - RefType(genTypeID.itables), - Expr(List(StructNewDefault(genTypeID.itables))) - ) - ) - } - private def genPrimitiveTypeDataGlobals()(implicit ctx: WasmContext): Unit = { import genFieldID.typeData._ @@ -498,7 +465,6 @@ final class CoreWasmLib(coreSpec: CoreSpec, globalInfo: LinkedGlobalInfo) { val boxStruct = genTypeID.forClass(boxClassName) val instrs: List[Instr] = List( GlobalGet(genGlobalID.forVTable(boxClassName)), - GlobalGet(genGlobalID.forITable(boxClassName)), zeroValueInstr, StructNew(boxStruct) ) @@ -1566,7 +1532,11 @@ final class CoreWasmLib(coreSpec: CoreSpec, globalInfo: LinkedGlobalInfo) { // reflectiveProxies, empty since all methods of array classes exist in jl.Object fb += ArrayNewFixed(genTypeID.reflectiveProxies, 0) + // itable slots val objectClassInfo = ctx.getClassInfo(ObjectClass) + fb ++= ClassEmitter.genItableSlots(objectClassInfo, List(SerializableClass, CloneableClass)) + + // vtable items fb ++= objectClassInfo.tableEntries.map { methodName => ctx.refFuncWithDeclaration(objectClassInfo.resolvedMethodInfos(methodName).tableEntryID) } @@ -2256,17 +2226,16 @@ final class CoreWasmLib(coreSpec: CoreSpec, globalInfo: LinkedGlobalInfo) { fb += StructGet(genTypeID.ClassStruct, genFieldID.classData) fb += LocalTee(componentTypeDataLocal) - // Load the vtable and itables of the ArrayClass instance we will create + // Load the vtable of the ArrayClass instance we will create fb += I32Const(1) - fb += Call(genFunctionID.arrayTypeData) // vtable - fb += GlobalGet(genGlobalID.arrayClassITable) // itables + fb += Call(genFunctionID.arrayTypeData) // Load the length fb += LocalGet(lengthParam) // switch (componentTypeData.kind) val switchClauseSig = FunctionType( - List(arrayTypeDataType, RefType(genTypeID.itables), Int32), + List(arrayTypeDataType, Int32), List(RefType(genTypeID.ObjectStruct)) ) fb.switch(switchClauseSig) { () => @@ -2807,7 +2776,6 @@ final class CoreWasmLib(coreSpec: CoreSpec, globalInfo: LinkedGlobalInfo) { // Build the result arrayStruct fb += LocalGet(fromLocal) fb += StructGet(arrayStructTypeID, genFieldID.objStruct.vtable) // vtable - fb += GlobalGet(genGlobalID.arrayClassITable) // itable fb += LocalGet(resultUnderlyingLocal) fb += StructNew(arrayStructTypeID) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala index ec913fe9cb..58facb4619 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Emitter.scala @@ -90,7 +90,6 @@ final class Emitter(config: Emitter.Config) { coreLib.genPreClasses() sortedClasses.foreach(classEmitter.genClassDef(_)) topLevelExports.foreach(classEmitter.genTopLevelExport(_)) - classEmitter.genGlobalArrayClassItable() coreLib.genPostClasses() genStartFunction(sortedClasses, moduleInitializers, topLevelExports) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala index 9ff845757f..12a4913928 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala @@ -1190,10 +1190,10 @@ private class FunctionEmitter private ( // Generates an itable-based dispatch. def genITableDispatch(): Unit = { fb += wa.LocalGet(receiverLocalForDispatch) - fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.itables) + fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) fb += wa.StructGet( - genTypeID.itables, - genFieldID.itablesStruct.itableSlot(receiverClassInfo.itableIdx) + genTypeID.ObjectVTable, + genFieldID.vtableStruct.itableSlot(receiverClassInfo.itableIdx) ) fb += wa.RefCast(watpe.RefType(genTypeID.forITable(receiverClassInfo.name))) fb += wa.StructGet( @@ -2722,7 +2722,6 @@ private class FunctionEmitter private ( fb += wa.LocalSet(primLocal) fb += wa.GlobalGet(genGlobalID.forVTable(boxClassName)) - fb += wa.GlobalGet(genGlobalID.forITable(boxClassName)) fb += wa.LocalGet(primLocal) fb += wa.StructNew(genTypeID.forClass(boxClassName)) @@ -3001,7 +3000,7 @@ private class FunctionEmitter private ( markPosition(tree) - genLoadVTableAndITableForArray(fb, arrayTypeRef) + genLoadArrayTypeData(fb, arrayTypeRef) // vtable // Create the underlying array genTree(length, IntType) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala index 7221cab491..34e5f44f9a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/Preprocessor.scala @@ -140,12 +140,6 @@ object Preprocessor { } } - // Does this Scala class implement any interface? - val classImplementsAnyInterface = { - (kind.isClass || kind == ClassKind.HijackedClass) && - (clazz.interfaces.nonEmpty || superClass.exists(_.classImplementsAnyInterface)) - } - /* Should we emit a vtable/typeData global for this class? * * There are essentially three reasons for which we need them: @@ -215,7 +209,6 @@ object Preprocessor { kind, clazz.jsClassCaptures, allFieldDefs, - classImplementsAnyInterface, clazz.hasInstances, !clazz.hasDirectInstances, hasRuntimeTypeInfo, diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/README.md b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/README.md index a565099a88..d1830d355e 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/README.md +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/README.md @@ -77,10 +77,10 @@ Our "type representation" for `nothing` is therefore to make sure that we are in ### Basic structure We use GC `struct`s to represent instances of classes. -The structs start with a `vtable` reference and an `itables` reference, which are followed by user-defined fields. +The structs start with a `vtable` reference, which is followed by user-defined fields. The declared supertypes of those `struct`s follow the *class* hierarchy (ignoring interfaces). -The `vtable` and `itables` references are immutable. +The `vtable` reference is immutable. User-defined fields are always mutable as the WebAssembly level, since they are mutated in the constructors. For example, given the following IR classes: @@ -100,13 +100,11 @@ We define the following GC structs: ```wat (type $c.A (sub $c.java.lang.Object (struct (field $vtable (ref $v.A)) - (field $itables (ref $itables)) (field $f.A.x (mut i32))) )) (type $c.B (sub $c.A (struct (field $vtable (ref $v.B)) - (field $itables (ref $itables)) (field $f.A.x (mut i32)) (field $f.B.y (mut f64))) )) @@ -197,6 +195,7 @@ Documentation for the meaning of each field can be found in `VarGen.genFieldID.t The vtable of our object model follows a standard layout: * The class meta data, then +* Slots for itable pointers (see below for interface method calls), then * Function pointers for the virtual methods, from `jl.Object` down to the current class. vtable structs form a subtyping hierarchy that mirrors the class hierarchy, so that `$v.B` is a subtype of `$v.A`. @@ -229,12 +228,14 @@ we get ```wat (type $v.A (sub $v.java.lang.Object (struct ;; ... class metadata + ;; ... itable slots ;; ... methods of jl.Object (field $m.foo_I_I (ref $4)) ))) (type $v.helloworld.B (sub $v.A (struct ;; ... class metadata + ;; ... itable slots ;; ... methods of jl.Object (field $m.foo_I_I (ref $4)) (field $m.bar_D_D (ref $6)) @@ -278,17 +279,24 @@ A virtual call to `a.foo(1)` is compiled as you would expect: lookup the functio ### itables and interface method calls -The itables field contains the method tables for interface call dispatch. -It is an instance of the following struct type: +Before the function pointers for virtual call dispatch, each vtable contains the method tables for interface call dispatch. +They have one more level of indirection than for virtual calls. +There are `N` immutable fields of type `structref`, which are the *itable slots*: ```wat -(type $itables (struct (field $slot1 structref) ... (field $slotN structref))) +(type $v.A (sub $v.java.lang.Object (struct + ;; ... class metadata + (field $itableSlot1 structref) + ... + (field $itableSlotN structref) + ;; ... virtual method pointers +))) ``` As a first approximation, we assign a distinct index to every interface in the program. -The index maps to a slot of the itables struct of the instance. +The index maps to an itable slot. At the index of a given interface `Intf`, we find a `(ref $it.Intf)` whose fields are the method table entries of `Intf`. -Like for vtables, we use the "table entry bridges" in the itables, i.e., the functions where the receiver is of type `(ref any)`. +Like for virtual calls, we use the "table entry bridges" in the itables, i.e., the functions where the receiver is of type `(ref any)`. For example, given @@ -323,7 +331,7 @@ This slot allocation is implemented in `Preprocessor.assignBuckets`. Since Wasm structs only support single inheritance in their subtyping relationships, we have to transform every interface type as `(ref null jl.Object)` (the common supertype of all interfaces). This does not turn out to be a problem for interface method calls, since they pass through the `itables` struct anyway, and use the table entry bridges which take `(ref any)` as argument. -Given the above structure, an interface method call to `intf.foo(1)` is compiled as expected: lookup the function reference in the appropriate slot of the `itables` struct, then call it. +Given the above structure, an interface method call to `intf.foo(1)` is compiled as expected: lookup the function reference in the appropriate itable slot of the vtable, then call it. ### Reflective calls @@ -476,9 +484,10 @@ All array "classes" follow the same structure: * They actually extend `jl.Object` * Their vtable type is the same as `jl.Object` -* They each have their own vtable value for the differing metadata, although the method table entries are the same as in `jl.Object` +* They each have their own vtable value for the differing metadata, although their method table entries are all the same * This is also true for reference types: the vtables are dynamically created at run-time on first use (they are values and share the same type, so that we can do) -* Their `itables` field points to a common itables struct with entries for `jl.Cloneable` and `j.io.Serializable` +* Their virtual method pointers are the same as in `jl.Object` +* Their itable slots are all the same, with entries for `jl.Cloneable` and `j.io.Serializable` * They have a unique "user-land" field `$underlyingArray`, which is a Wasm array of its values: * For primitives, they are primitive arrays, such as `(array mut i32)` * For references, they are all the same type `(array mut anyref)` @@ -496,18 +505,15 @@ Concretely, here are the relevant Wasm definitions: (type $BooleanArray (sub final $c.java.lang.Object (struct (field $vtable (ref $v.java.lang.Object)) - (field $itables (ref $itables)) (field $arrayUnderlying (ref $i8Array)) ))) (type $CharArray (sub final $c.java.lang.Object (struct (field $vtable (ref $v.java.lang.Object)) - (field $itables (ref $itables)) (field $arrayUnderlying (ref $i16Array)) ))) ... (type $ObjectArray (sub final $c.java.lang.Object (struct (field $vtable (ref $v.java.lang.Object)) - (field $itables (ref $itables)) (field $arrayUnderlying (ref $anyArray)) ))) ``` @@ -550,18 +556,15 @@ For type definitions, we use the following ordering: For global definitions, we use the following ordering: -1. A unique "empty" itables global for classes that do not implement any interface (`$emptyITable`) -2. The typeData of the primitive types (e.g., `$d.I`) -3. For each linked class, in the same ancestor count-based order: +1. The typeData of the primitive types (e.g., `$d.I`) +2. For each linked class, in the same ancestor count-based order: 1. In no particular order, if applicable: * Its typeData/vtable global (e.g., `$d.java.lang.Object`), which may refer to the typeData of ancestors, so the order between classes is important - * Its itables global (e.g., `$it.java.lang.Class`) * Static field definitions * Definitions of `Symbol`s for the "names" of private JS fields * The module instance * The cached JS class value -4. Cached values of boxed zero values (such as `$bZeroChar`), which refer to the vtable and itables globals of the box classes -5. The itables global of array classes (namely, `$arrayClassITable`) +3. Cached values of boxed zero values (such as `$bZeroChar`), which refer to the vtable globals of the box classes ## Miscellaneous @@ -581,7 +584,6 @@ It looks like the following: (result (ref $c.C)) global.get $d.C ;; the global vtable for class C - global.get $it.C ;; the global itables for class C i32.const 0 ;; zero of type int f64.const 0.0 ;; zero of type double struct.new $c.C ;; allocate a $c.C initialized with all of the above @@ -591,7 +593,7 @@ It looks like the following: It would be nice to do the following instead: 1. Allocate a `$c.C` entirely initialized with zeros, using `struct.new_default` -2. Set the `$vtable` and `$itables` fields +2. Set the `$vtable` field This would have a constant code size cost, irrespective of the amount of fields in `C`. Unfortunately, we cannot do this because the `$vtable` field is immutable. diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SWasmGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SWasmGen.scala index 7ac01cf64d..02ee2ca7ba 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SWasmGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SWasmGen.scala @@ -62,18 +62,9 @@ object SWasmGen { fb += Call(genFunctionID.arrayTypeData) } - /** Gen code to load the vtable and the itable of the given array type. */ - def genLoadVTableAndITableForArray(fb: FunctionBuilder, arrayTypeRef: ArrayTypeRef): Unit = { - // Load the typeData of the resulting array type. It is the vtable of the resulting object. - genLoadArrayTypeData(fb, arrayTypeRef) - - // Load the itables for the array type - fb += GlobalGet(genGlobalID.arrayClassITable) - } - def genArrayValue(fb: FunctionBuilder, arrayTypeRef: ArrayTypeRef, length: Int)( genElems: => Unit): Unit = { - genLoadVTableAndITableForArray(fb, arrayTypeRef) + genLoadArrayTypeData(fb, arrayTypeRef) // vtable // Create the underlying array genElems diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala index e9416b3071..09dacde150 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/VarGen.scala @@ -37,7 +37,6 @@ object VarGen { forVTable(ClassRef(className)) } - final case class forITable(className: ClassName) extends GlobalID final case class forStaticField(fieldName: FieldName) extends GlobalID final case class forJSPrivateField(fieldName: FieldName) extends GlobalID @@ -45,8 +44,6 @@ object VarGen { case object bZeroChar extends GlobalID case object bZeroLong extends GlobalID - case object emptyITable extends GlobalID - case object arrayClassITable extends GlobalID case object lastIDHashCode extends GlobalID /** A `GlobalID` for a JS helper global. @@ -231,14 +228,9 @@ object VarGen { object objStruct { case object vtable extends FieldID - case object itables extends FieldID case object arrayUnderlying extends FieldID } - object itablesStruct { - final case class itableSlot(i: Int) extends FieldID - } - object reflectiveProxy { case object methodID extends FieldID case object funcRef extends FieldID @@ -343,6 +335,11 @@ object VarGen { case object reflectiveProxies extends FieldID } + /** Extension of `typeData` for vtables, starting with `jl.Object`. */ + object vtableStruct { + final case class itableSlot(i: Int) extends FieldID + } + /** The magic `data` field of type `(ref typeData)`, injected into `jl.Class`. */ case object classData extends FieldID @@ -405,7 +402,6 @@ object VarGen { } case object typeDataArray extends TypeID - case object itables extends TypeID case object reflectiveProxies extends TypeID // primitive array types, underlying the Array[T] classes diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala index 6d64b1bbbd..6f0551921a 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/WasmContext.scala @@ -256,7 +256,6 @@ object WasmContext { val kind: ClassKind, val jsClassCaptures: Option[List[ParamDef]], val allFieldDefs: List[FieldDef], - val classImplementsAnyInterface: Boolean, val hasInstances: Boolean, val isAbstract: Boolean, val hasRuntimeTypeInfo: Boolean, @@ -271,7 +270,7 @@ object WasmContext { override def toString(): String = s"ClassInfo(${name.nameString})" - /** Returns the index of this interface's itable in the classes' interface tables. + /** Returns the index of this interface's itable in the classes' vtables. * * Only interfaces that have instances get an itable index. */