From 506b976acfd49e693a20f664ca9d3e966eb9ee9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 24 Jun 2024 20:04:58 +0200 Subject: [PATCH 1/8] Wasm: Implement checked asInstanceOfs. --- Jenkinsfile | 14 + .../backend/WebAssemblyLinkerBackend.scala | 1 - .../backend/wasmemitter/ClassEmitter.scala | 110 ++++- .../backend/wasmemitter/CoreWasmLib.scala | 377 +++++++++++++++++- .../linker/backend/wasmemitter/Emitter.scala | 46 ++- .../backend/wasmemitter/FunctionEmitter.scala | 37 +- .../backend/wasmemitter/LoaderContent.scala | 8 + .../backend/wasmemitter/Preprocessor.scala | 8 +- .../backend/wasmemitter/SpecialNames.scala | 7 + .../linker/backend/wasmemitter/VarGen.scala | 7 + .../backend/wasmemitter/WasmContext.scala | 5 +- project/Build.scala | 28 +- 12 files changed, 600 insertions(+), 48 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index a217171026..9e1b827a81 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -431,6 +431,20 @@ def Tasks = [ 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ 'set scalaJSStage in Global := FullOptStage' \ $testSuite$v/test && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ + $testSuite$v/test && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ + 'set scalaJSStage in Global := FullOptStage' \ + $testSuite$v/test && + sbtretry ++$scala \ + 'set Global/enableWasmEverywhere := true' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= makeCompliant' \ + 'set scalaJSLinkerConfig in $testSuite.v$v ~= (_.withOptimizer(false))' \ + $testSuite$v/test && sbtretry ++$scala \ 'set Global/enableWasmEverywhere := true' \ testingExample$v/testHtml && diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala index 2e614993f1..eb264753d4 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala @@ -37,7 +37,6 @@ final class WebAssemblyLinkerBackend(config: LinkerBackendImpl.Config) s"The WebAssembly backend only supports ES modules; was ${coreSpec.moduleKind}." ) require( - coreSpec.semantics.asInstanceOfs == CheckedBehavior.Unchecked && coreSpec.semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked && coreSpec.semantics.arrayStores == CheckedBehavior.Unchecked && coreSpec.semantics.negativeArraySizes == CheckedBehavior.Unchecked && 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 f669847fe2..3d6c1d3c45 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 @@ -20,6 +20,7 @@ import org.scalajs.ir.OriginalName.NoOriginalName import org.scalajs.ir.Trees._ import org.scalajs.ir.Types._ +import org.scalajs.linker.interface.CheckedBehavior import org.scalajs.linker.interface.unstable.RuntimeClassNameMapperImpl import org.scalajs.linker.standard.{CoreSpec, LinkedClass, LinkedTopLevelExport} @@ -37,6 +38,7 @@ import WasmContext._ class ClassEmitter(coreSpec: CoreSpec) { import ClassEmitter._ + import coreSpec.semantics def genClassDef(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { val classInfo = ctx.getClassInfo(clazz.className) @@ -373,6 +375,12 @@ class ClassEmitter(coreSpec: CoreSpec) { genCloneFunction(clazz) } + // Generate cast functions + if (clazz.hasInstanceTests && semantics.asInstanceOfs != CheckedBehavior.Unchecked) { + if (className != ObjectClass) + genClassCastFunction(clazz) + } + // Generate the module accessor if (clazz.kind == ClassKind.ModuleClass && clazz.hasInstances) { val heapType = watpe.HeapType(genTypeID.forClass(clazz.className)) @@ -408,7 +416,7 @@ class ClassEmitter(coreSpec: CoreSpec) { case None => genTypeID.typeData case Some(s) => genTypeID.forVTable(s.name) } - val structType = watpe.StructType(CoreWasmLib.typeDataStructFields ::: vtableFields) + val structType = watpe.StructType(ctx.coreLib.typeDataStructFields ::: vtableFields) val subType = watpe.SubType( typeID, makeDebugName(ns.VTable, className), @@ -522,6 +530,50 @@ class ClassEmitter(coreSpec: CoreSpec) { fb.buildAndAddToModule() } + /** Generate the cast function for an interface. + * + * When `asInstanceOfs` are checked, the expression `asInstanceOf[]` + * will be compiled to a CALL to the function generated by this method. + */ + private def genInterfaceCastFunction(clazz: LinkedClass)( + implicit ctx: WasmContext): Unit = { + assert(clazz.kind == ClassKind.Interface) + + val className = clazz.className + val resultType = TypeTransformer.transformClassType(className, nullable = true) + + val fb = new FunctionBuilder( + ctx.moduleBuilder, + genFunctionID.asInstance(ClassType(className, nullable = true)), + makeDebugName(ns.AsInstance, className), + clazz.pos + ) + val objParam = fb.addParam("obj", watpe.RefType.anyref) + fb.setResultType(resultType) + + fb.block() { successLabel => + // Succeed if null + fb += wa.LocalGet(objParam) + fb += wa.BrOnNull(successLabel) + + // Succeed if the instance test succeeds + fb += wa.Call(genFunctionID.instanceTest(className)) + fb += wa.BrIf(successLabel) + + // If we get here, it's a CCE + fb += wa.LocalGet(objParam) + fb += wa.GlobalGet(genGlobalID.forVTable(className)) + fb += wa.Call(genFunctionID.classCastException) + fb += wa.Unreachable + } + + fb += wa.LocalGet(objParam) + if (resultType != watpe.RefType.anyref) + fb += wa.RefCast(resultType) + + fb.buildAndAddToModule() + } + private def genNewDefaultFunc(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { val className = clazz.name.name val classInfo = ctx.getClassInfo(className) @@ -598,6 +650,56 @@ class ClassEmitter(coreSpec: CoreSpec) { fb.buildAndAddToModule() } + /** Generate the cast function for a class. + * + * When `asInstanceOfs` are checked, the expression `asInstanceOf[]` + * will be compiled to a CALL to the function generated by this method. + */ + private def genClassCastFunction(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { + val className = clazz.className + + val resultType = TypeTransformer.transformClassType(className, nullable = true) + + val fb = new FunctionBuilder( + ctx.moduleBuilder, + genFunctionID.asInstance(ClassType(clazz.className, nullable = true)), + makeDebugName(ns.AsInstance, className), + clazz.pos + ) + val objParam = fb.addParam("obj", watpe.RefType.anyref) + fb.setResultType(resultType) + + fb.block(resultType) { successLabel => + fb += wa.LocalGet(objParam) + + if (className == SpecialNames.JLNumberClass) { + /* jl.Number is special, because it is the only non-Object *class* + * that is an ancestor of a hijacked class. + */ + fb += wa.BrOnCast(successLabel, watpe.RefType.anyref, + watpe.RefType.nullable(genTypeID.forClass(SpecialNames.JLNumberClass))) + + /* The `obj` still on the stack will be used for: + * a) the result in the true case + * b) consistency with non-Number in the false case + */ + + fb += wa.LocalGet(objParam) + fb += wa.Call(genFunctionID.typeTest(DoubleRef)) + fb += wa.BrIf(successLabel) + } else { + fb += wa.BrOnCast(successLabel, watpe.RefType.anyref, resultType) + } + + // If we get here, it's a CCE -- `obj` is still on the stack + fb += wa.GlobalGet(genGlobalID.forVTable(className)) + fb += wa.Call(genFunctionID.classCastException) + fb += wa.Unreachable + } + + fb.buildAndAddToModule() + } + private def genModuleAccessor(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { assert(clazz.kind == ClassKind.ModuleClass) @@ -685,8 +787,11 @@ class ClassEmitter(coreSpec: CoreSpec) { itableType ) - if (clazz.hasInstanceTests) + if (clazz.hasInstanceTests) { genInterfaceInstanceTest(clazz) + if (semantics.asInstanceOfs != CheckedBehavior.Unchecked) + genInterfaceCastFunction(clazz) + } } private def genJSClass(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = { @@ -1262,6 +1367,7 @@ object ClassEmitter { val JSClassValueCache = UTF8String("b.") val TypeData = UTF8String("d.") val IsInstance = UTF8String("is.") + val AsInstance = UTF8String("as.") // Shared with JS backend -- string val TopLevelExport = UTF8String("e.") 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 855c37f888..e9c9f0d3a3 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 @@ -13,9 +13,13 @@ package org.scalajs.linker.backend.wasmemitter import org.scalajs.ir.Names._ +import org.scalajs.ir.OriginalName.NoOriginalName import org.scalajs.ir.Trees.{JSUnaryOp, JSBinaryOp, MemberNamespace} import org.scalajs.ir.Types.{Type => _, ArrayType => _, _} -import org.scalajs.ir.{OriginalName, Position} +import org.scalajs.ir.{OriginalName, Position, Types => irtpe} + +import org.scalajs.linker.interface.CheckedBehavior +import org.scalajs.linker.standard.CoreSpec import org.scalajs.linker.backend.webassembly._ import org.scalajs.linker.backend.webassembly.Instructions._ @@ -27,8 +31,9 @@ import EmbeddedConstants._ import VarGen._ import TypeTransformer._ -object CoreWasmLib { +final class CoreWasmLib(coreSpec: CoreSpec) { import RefType.anyref + import coreSpec.semantics private implicit val noPos: Position = Position.NoPosition @@ -363,6 +368,7 @@ object CoreWasmLib { addHelperImport(genFunctionID.isString, List(anyref), List(Int32)) addHelperImport(genFunctionID.jsValueType, List(RefType.any), List(Int32)) + addHelperImport(genFunctionID.jsValueDescription, List(anyref), List(RefType.any)) addHelperImport(genFunctionID.bigintHashCode, List(RefType.any), List(Int32)) addHelperImport( genFunctionID.symbolDescription, @@ -590,6 +596,15 @@ object CoreWasmLib { genCreateClassOf() genGetClassOf() genArrayTypeData() + + if (semantics.asInstanceOfs != CheckedBehavior.Unchecked) { + genValueDescription() + genClassCastException() + genPrimitiveAsInstances() + genArrayAsInstances() + } + + genIsInstanceExternal() genIsInstance() genIsAssignableFromExternal() genIsAssignableFrom() @@ -908,7 +923,7 @@ object CoreWasmLib { fb += Call(genFunctionID.jsObjectPush) // "isInstance": closure(isInstance, typeData) fb ++= ctx.stringPool.getConstantStringInstr("isInstance") - fb += ctx.refFuncWithDeclaration(genFunctionID.isInstance) + fb += ctx.refFuncWithDeclaration(genFunctionID.isInstanceExternal) fb += LocalGet(typeDataParam) fb += Call(genFunctionID.closure) fb += Call(genFunctionID.jsObjectPush) @@ -982,6 +997,273 @@ object CoreWasmLib { fb.buildAndAddToModule() } + /** `valueDescription: anyref -> (ref any)` (a string). + * + * Returns a safe string description of a value. This helper is never called + * for `value === null`. As implemented, it would return `"object"` if it were. + */ + private def genValueDescription()(implicit ctx: WasmContext): Unit = { + val objectType = RefType(genTypeID.ObjectStruct) + + val fb = newFunctionBuilder(genFunctionID.valueDescription) + val valueParam = fb.addParam("value", anyref) + fb.setResultType(RefType.any) + + fb.block(anyref) { notOurObjectLabel => + fb.block(objectType) { isCharLabel => + fb.block(objectType) { isLongLabel => + // If it not our object, jump out of notOurObject + fb += LocalGet(valueParam) + fb += BrOnCastFail(notOurObjectLabel, anyref, objectType) + + // If is a long or char box, jump out to the appropriate label + fb += BrOnCast(isLongLabel, objectType, RefType(genTypeID.forClass(SpecialNames.LongBoxClass))) + fb += BrOnCast(isCharLabel, objectType, RefType(genTypeID.forClass(SpecialNames.CharBoxClass))) + + // Get and return the class name + fb += StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) + fb += ReturnCall(genFunctionID.typeDataName) + } + + // Return the constant string "long" + fb ++= ctx.stringPool.getConstantStringInstr("long") + fb += Return + } + + // Return the constant string "char" + fb ++= ctx.stringPool.getConstantStringInstr("char") + fb += Return + } + + // When it is not one of our objects, use the JS helper + fb += Call(genFunctionID.jsValueDescription) + + fb.buildAndAddToModule() + } + + /** `classCastException: [anyref, (ref typeData)] -> void`. + * + * This function always throws. It should be followed by an `unreachable` + * statement. + */ + private def genClassCastException()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.classCastException) + val objParam = fb.addParam("obj", anyref) + val typeDataParam = fb.addParam("typeData", typeDataType) + + maybeWrapInUBE(fb, semantics.asInstanceOfs) { + genNewScalaClass(fb, ClassCastExceptionClass, SpecialNames.StringArgConstructorName) { + fb += LocalGet(objParam) + fb += Call(genFunctionID.valueDescription) + + fb ++= ctx.stringPool.getConstantStringInstr(" cannot be cast to ") + fb += Call(genFunctionID.stringConcat) + + fb += LocalGet(typeDataParam) + fb += Call(genFunctionID.typeDataName) + fb += Call(genFunctionID.stringConcat) + } + } + + fb += ExternConvertAny + fb += Throw(genTagID.exception) + + fb.buildAndAddToModule() + } + + /** Generates the `asInstance` functions for primitive types. + */ + private def genPrimitiveAsInstances()(implicit ctx: WasmContext): Unit = { + val primTypesWithAsInstances: List[PrimType] = List( + UndefType, + BooleanType, + CharType, + ByteType, + ShortType, + IntType, + LongType, + FloatType, + DoubleType, + StringType + ) + + for (primType <- primTypesWithAsInstances) { + // asInstanceOf[PrimType] + genPrimitiveOrBoxedClassAsInstance(primType, targetTpe = primType, isUnbox = true) + + // asInstanceOf[BoxedClass] + val boxedClassType = ClassType(PrimTypeToBoxedClass(primType), nullable = true) + genPrimitiveOrBoxedClassAsInstance(primType, targetTpe = boxedClassType, isUnbox = false) + } + } + + /** Common logic for primitives and boxed classes in `genPrimitiveAsInstances`. */ + private def genPrimitiveOrBoxedClassAsInstance(primType: PrimType, + targetTpe: irtpe.Type, isUnbox: Boolean)( + implicit ctx: WasmContext): Unit = { + + val origName = OriginalName("as." + targetTpe.show()) + + val resultType = TypeTransformer.transformSingleType(targetTpe) + + val fb = newFunctionBuilder(genFunctionID.asInstance(targetTpe), origName) + val objParam = fb.addParam("obj", RefType.anyref) + fb.setResultType(resultType) + + fb.block() { objIsNullLabel => + primType match { + // For char and long, use br_on_cast_fail to test+cast to the box class + case CharType | LongType => + val boxClass = + if (primType == CharType) SpecialNames.CharBoxClass + else SpecialNames.LongBoxClass + val structTypeID = genTypeID.forClass(boxClass) + + fb.block(RefType.anyref) { castFailLabel => + fb += LocalGet(objParam) + fb += BrOnNull(objIsNullLabel) + fb += BrOnCastFail(castFailLabel, RefType.anyref, RefType(structTypeID)) + + // Extract the `value` field if unboxing + if (isUnbox) { + val fieldName = FieldName(boxClass, SpecialNames.valueFieldSimpleName) + fb += StructGet(structTypeID, genFieldID.forClassInstanceField(fieldName)) + } + + fb += Return + } + + // For all other types, use type test, and separately unbox if required + case _ => + fb += LocalGet(objParam) + fb += BrOnNull(objIsNullLabel) + + // if obj.isInstanceOf[primType] + primType match { + case UndefType => + fb += Call(genFunctionID.isUndef) + case StringType => + fb += Call(genFunctionID.isString) + case primType: PrimTypeWithRef => + fb += Call(genFunctionID.typeTest(primType.primRef)) + } + fb.ifThen() { + // then, unbox if required then return + if (isUnbox) { + primType match { + case UndefType => + fb += GlobalGet(genGlobalID.undef) + case StringType => + fb += LocalGet(objParam) + fb += RefAsNonNull + case primType: PrimTypeWithRef => + fb += LocalGet(objParam) + fb += Call(genFunctionID.unbox(primType.primRef)) + } + } else { + fb += LocalGet(objParam) + } + + fb += Return + } + + // Fall through for CCE + fb += LocalGet(objParam) + } + + // If we get here, it is a CCE + fb += GlobalGet(genGlobalID.forVTable(PrimTypeToBoxedClass(primType))) + fb += Call(genFunctionID.classCastException) + fb += Unreachable + } + + // obj is null -- load the zero of the target type (which is `null` for boxed classes) + fb += SWasmGen.genZeroOf(targetTpe) + + fb.buildAndAddToModule() + } + + private def genArrayAsInstances()(implicit ctx: WasmContext): Unit = { + for (baseRef <- arrayBaseRefs) + genBaseArrayAsInstance(baseRef) + + genAsSpecificRefArray() + } + + private def genBaseArrayAsInstance(baseRef: NonArrayTypeRef)(implicit ctx: WasmContext): Unit = { + val arrayTypeRef = ArrayTypeRef(baseRef, 1) + + val wasmTypeID = genTypeID.forArrayClass(arrayTypeRef) + val resultType = RefType.nullable(wasmTypeID) + + val fb = newFunctionBuilder( + genFunctionID.asInstance(irtpe.ArrayType(arrayTypeRef, nullable = true)), + OriginalName("asArray." + baseRef.displayName) + ) + val objParam = fb.addParam("obj", anyref) + fb.setResultType(resultType) + + fb.block(resultType) { successLabel => + fb += LocalGet(objParam) + fb += BrOnCast(successLabel, anyref, resultType) + + // If we get here, it's a CCE -- `obj` is still on the stack + fb += GlobalGet(genGlobalID.forVTable(baseRef)) + fb += I32Const(1) + fb += Call(genFunctionID.arrayTypeData) + fb += Call(genFunctionID.classCastException) + fb += Unreachable + } + + fb.buildAndAddToModule() + } + + private def genAsSpecificRefArray()(implicit ctx: WasmContext): Unit = { + val refArrayStructTypeID = genTypeID.forArrayClass(ArrayTypeRef(ClassRef(ObjectClass), 1)) + val resultType = RefType.nullable(refArrayStructTypeID) + + val fb = newFunctionBuilder(genFunctionID.asSpecificRefArray) + val objParam = fb.addParam("obj", anyref) + val arrayTypeDataParam = fb.addParam("arrayTypeData", RefType(genTypeID.typeData)) + fb.setResultType(resultType) + + val refArrayLocal = fb.addLocal("refArray", RefType(refArrayStructTypeID)) + + fb.block(resultType) { successLabel => + fb.block() { isNullLabel => + fb.block(anyref) { failureLabel => + // If obj is null, return null + fb += LocalGet(objParam) + fb += BrOnNull(isNullLabel) + + // Otherwise, if we cannot cast to ObjectArray, fail + fb += BrOnCastFail(failureLabel, RefType.any, RefType(refArrayStructTypeID)) + fb += LocalTee(refArrayLocal) // leave it on the stack for BrIf or for fall through to CCE + + // Otherwise, test assignability of the array type + fb += LocalGet(arrayTypeDataParam) + fb += LocalGet(refArrayLocal) + fb += StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) + fb += Call(genFunctionID.isAssignableFrom) + + // If true, jump to success + fb += BrIf(successLabel) + } + + // If we get here, it's a CCE -- `obj` is still on the stack + fb += LocalGet(arrayTypeDataParam) + fb += Call(genFunctionID.classCastException) + fb += Unreachable // for clarity; technically redundant since the stacks align + } + + fb += RefNull(HeapType.None) + } + + fb.buildAndAddToModule() + } + /** `arrayTypeData: (ref typeData), i32 -> (ref vtable.java.lang.Object)`. * * Returns the typeData/vtable of an array with `dims` dimensions over the given typeData. `dims` @@ -1116,13 +1398,34 @@ object CoreWasmLib { fb.buildAndAddToModule() } - /** `isInstance: (ref typeData), anyref -> anyref` (a boxed boolean). + /** `isInstanceExternal: (ref typeData), anyref -> anyref` (a boxed boolean). * * Tests whether the given value is a non-null instance of the given type. * * Specified by `"isInstance"` at * [[https://lampwww.epfl.ch/~doeraene/sjsir-semantics/#sec-sjsir-createclassdataof]]. */ + private def genIsInstanceExternal()(implicit ctx: WasmContext): Unit = { + val fb = newFunctionBuilder(genFunctionID.isInstanceExternal) + val typeDataParam = fb.addParam("typeData", RefType(genTypeID.typeData)) + val valueParam = fb.addParam("value", RefType.anyref) + fb.setResultType(anyref) + + fb += LocalGet(typeDataParam) + fb += LocalGet(valueParam) + fb += Call(genFunctionID.isInstance) + fb += Call(genFunctionID.box(BooleanRef)) + + fb.buildAndAddToModule() + } + + /** `isInstance: (ref typeData), anyref -> i32` (a boolean). + * + * Tests whether the given value is a non-null instance of the given type. + * + * Internal implementation of `isInstanceExternal`, returning a primitive + * `i32` instead of a boxed boolean. + */ private def genIsInstance()(implicit ctx: WasmContext): Unit = { import genFieldID.typeData._ @@ -1132,7 +1435,7 @@ object CoreWasmLib { val fb = newFunctionBuilder(genFunctionID.isInstance) val typeDataParam = fb.addParam("typeData", typeDataType) val valueParam = fb.addParam("value", RefType.anyref) - fb.setResultType(anyref) + fb.setResultType(Int32) val valueNonNullLocal = fb.addLocal("valueNonNull", RefType.any) val specialInstanceTypesLocal = fb.addLocal("specialInstanceTypes", Int32) @@ -1208,7 +1511,6 @@ object CoreWasmLib { // Call the function fb += CallRef(genTypeID.isJSClassInstanceFuncType) - fb += Call(genFunctionID.box(BooleanRef)) fb += Return } fb += Drop // drop `value` which was left on the stack @@ -1232,7 +1534,7 @@ object CoreWasmLib { fb.block(RefType.any) { nonNullLabel => fb += LocalGet(valueParam) fb += BrOnNonNull(nonNullLabel) - fb += GlobalGet(genGlobalID.bFalse) + fb += I32Const(0) fb += Return } fb += LocalSet(valueNonNullLocal) @@ -1278,7 +1580,6 @@ object CoreWasmLib { fb.ifThen() { // then return true fb += I32Const(1) - fb += Call(genFunctionID.box(BooleanRef)) fb += Return } } @@ -1295,7 +1596,7 @@ object CoreWasmLib { fb += BrOnCast(ourObjectLabel, RefType.any, objectRefType) // on cast fail, return false - fb += GlobalGet(genGlobalID.bFalse) + fb += I32Const(0) fb += Return } fb += StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) @@ -1304,8 +1605,6 @@ object CoreWasmLib { fb += Call(genFunctionID.isAssignableFrom) } - fb += Call(genFunctionID.box(BooleanRef)) - fb.buildAndAddToModule() } @@ -1461,7 +1760,7 @@ object CoreWasmLib { fb.buildAndAddToModule() } - /** `checkCast: (ref typeData), anyref -> anyref`. + /** `checkCast: (ref typeData), anyref -> []`. * * Casts the given value to the given type; subject to undefined behaviors. */ @@ -1471,13 +1770,34 @@ object CoreWasmLib { val fb = newFunctionBuilder(genFunctionID.checkCast) val typeDataParam = fb.addParam("typeData", typeDataType) val valueParam = fb.addParam("value", RefType.anyref) - fb.setResultType(RefType.anyref) - /* Given that we only implement `CheckedBehavior.Unchecked` semantics for - * now, this is always the identity. - */ + if (semantics.asInstanceOfs != CheckedBehavior.Unchecked) { + fb.block() { successLabel => + // If typeData.kind == KindJSType, succeed + fb += LocalGet(typeDataParam) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) + fb += I32Const(KindJSType) + fb += I32Eq + fb += BrIf(successLabel) - fb += LocalGet(valueParam) + // If value is null, succeed + fb += LocalGet(valueParam) + fb += RefIsNull // consumes `value`, unlike `BrOnNull` which would leave it on the stack + fb += BrIf(successLabel) + + // If isInstance(typeData, value), succeed + fb += LocalGet(typeDataParam) + fb += LocalGet(valueParam) + fb += Call(genFunctionID.isInstance) + fb += BrIf(successLabel) + + // Otherwise, it is a CCE + fb += LocalGet(valueParam) + fb += LocalGet(typeDataParam) + fb += Call(genFunctionID.classCastException) + fb += Unreachable // for clarity; technically redundant since the stacks align + } + } fb.buildAndAddToModule() } @@ -2324,4 +2644,27 @@ object CoreWasmLib { fb.buildAndAddToModule() } + private def maybeWrapInUBE(fb: FunctionBuilder, behavior: CheckedBehavior)( + genExceptionInstance: => Unit): Unit = { + if (behavior == CheckedBehavior.Fatal) { + genNewScalaClass(fb, SpecialNames.UndefinedBehaviorErrorClass, + SpecialNames.ThrowableArgConsructorName) { + genExceptionInstance + } + } else { + genExceptionInstance + } + } + + private def genNewScalaClass(fb: FunctionBuilder, cls: ClassName, ctor: MethodName)( + genCtorArgs: => Unit): Unit = { + val instanceLocal = fb.addLocal(NoOriginalName, RefType(genTypeID.forClass(cls))) + + fb += Call(genFunctionID.newDefault(cls)) + fb += LocalTee(instanceLocal) + genCtorArgs + fb += Call(genFunctionID.forMethod(MemberNamespace.Constructor, cls, ctor)) + fb += LocalGet(instanceLocal) + } + } 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 0a477f6a59..145739a409 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 @@ -44,10 +44,13 @@ import org.scalajs.linker.backend.javascript.ByteArrayWriter final class Emitter(config: Emitter.Config) { import Emitter._ - private val classEmitter = new ClassEmitter(config.coreSpec) + private val coreSpec = config.coreSpec + + private val coreLib = new CoreWasmLib(coreSpec) + private val classEmitter = new ClassEmitter(coreSpec) val symbolRequirements: SymbolRequirement = - Emitter.symbolRequirements(config.coreSpec) + Emitter.symbolRequirements(coreSpec) val injectedIRFiles: Seq[IRFile] = PrivateLibHolder.files @@ -77,13 +80,13 @@ final class Emitter(config: Emitter.Config) { val moduleInitializers = module.initializers.toList implicit val ctx: WasmContext = - Preprocessor.preprocess(sortedClasses, topLevelExports) + Preprocessor.preprocess(coreSpec, coreLib, sortedClasses, topLevelExports) - CoreWasmLib.genPreClasses() + coreLib.genPreClasses() genExternalModuleImports(module) sortedClasses.foreach(classEmitter.genClassDef(_)) topLevelExports.foreach(classEmitter.genTopLevelExport(_)) - CoreWasmLib.genPostClasses() + coreLib.genPostClasses() genStartFunction(sortedClasses, moduleInitializers, topLevelExports) @@ -383,16 +386,37 @@ object Emitter { * linker frontend will dead-code eliminate our box classes. */ private def symbolRequirements(coreSpec: CoreSpec): SymbolRequirement = { - val factory = SymbolRequirement.factory("wasm") + import coreSpec.semantics._ + import CheckedBehavior._ + import SpecialNames._ + + val factory = SymbolRequirement.factory("emitter") + import factory._ + + def cond(p: Boolean)(v: => SymbolRequirement): SymbolRequirement = + if (p) v else none() + + def isAnyFatal(behaviors: CheckedBehavior*): Boolean = + behaviors.contains(Fatal) + + multiple( + cond(asInstanceOfs != Unchecked) { + instantiateClass(ClassCastExceptionClass, StringArgConstructorName) + }, + + cond(isAnyFatal(asInstanceOfs, arrayIndexOutOfBounds, arrayStores, + negativeArraySizes, nullPointers, stringIndexOutOfBounds)) { + instantiateClass(UndefinedBehaviorErrorClass, + ThrowableArgConsructorName) + }, - factory.multiple( // TODO Ideally we should not require these, but rather adapt to their absence - factory.instantiateClass(ClassClass, AnyArgConstructorName), - factory.instantiateClass(JSExceptionClass, AnyArgConstructorName), + instantiateClass(ClassClass, AnyArgConstructorName), + instantiateClass(JSExceptionClass, AnyArgConstructorName), // See genIdentityHashCode in HelperFunctions - factory.callMethodStatically(BoxedDoubleClass, hashCodeMethodName), - factory.callMethodStatically(BoxedStringClass, hashCodeMethodName) + callMethodStatically(BoxedDoubleClass, hashCodeMethodName), + callMethodStatically(BoxedStringClass, hashCodeMethodName) ) } 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 5fecd5dbd2..8ce4030ea8 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 @@ -22,6 +22,7 @@ import org.scalajs.ir.OriginalName.NoOriginalName import org.scalajs.ir.Trees._ import org.scalajs.ir.Types._ +import org.scalajs.linker.interface.CheckedBehavior import org.scalajs.linker.backend.emitter.Transients import org.scalajs.linker.backend.webassembly._ @@ -291,6 +292,9 @@ private class FunctionEmitter private ( )(implicit ctx: WasmContext) { import FunctionEmitter._ + private val coreSpec = ctx.coreSpec + import coreSpec.semantics + private var closureIdx: Int = 0 private var currentEnv: Env = paramsEnv @@ -1786,7 +1790,38 @@ private class FunctionEmitter private ( private def genAsInstanceOf(tree: AsInstanceOf): Type = { val AsInstanceOf(expr, targetTpe) = tree - genCast(expr, targetTpe, tree.pos) + if (semantics.asInstanceOfs == CheckedBehavior.Unchecked) + genCast(expr, targetTpe, tree.pos) + else + genCheckedCast(expr, targetTpe, tree.pos) + } + + private def genCheckedCast(expr: Tree, targetTpe: Type, pos: Position): Type = { + genTree(expr, AnyType) + + markPosition(pos) + + targetTpe match { + case AnyType | ClassType(ObjectClass, true) => + // no-op + () + + case ArrayType(arrayTypeRef, true) => + arrayTypeRef match { + case ArrayTypeRef(ClassRef(ObjectClass) | _: PrimRef, 1) => + // For primitive arrays and exactly Array[Object], we have a dedicated function + fb += wa.Call(genFunctionID.asInstance(targetTpe)) + case _ => + // For other array types, we must use the generic function + genLoadArrayTypeData(fb, arrayTypeRef) + fb += wa.Call(genFunctionID.asSpecificRefArray) + } + + case _ => + fb += wa.Call(genFunctionID.asInstance(targetTpe)) + } + + targetTpe } private def genCast(expr: Tree, targetTpe: Type, pos: Position): Type = { diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala index 01614586c1..d6b338eca0 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala @@ -172,6 +172,14 @@ const scalaJSHelpers = { return $JSValueTypeOther; }, + // JS side of the `valueDescription` helper + // TODO: only emit this when required by checked behaviors + jsValueDescription: ((x) => + (typeof x === 'number') + ? (Object.is(x, -0) ? "number(-0)" : ("number(" + x + ")")) + : (typeof x) + ), + // Identity hash code bigintHashCode, symbolDescription: (x) => { 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 1aa1ea6f2f..2568fcb1fd 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 @@ -19,13 +19,14 @@ import org.scalajs.ir.Trees._ import org.scalajs.ir.Types._ import org.scalajs.ir.{ClassKind, Traversers} -import org.scalajs.linker.standard.{LinkedClass, LinkedTopLevelExport} +import org.scalajs.linker.standard.{CoreSpec, LinkedClass, LinkedTopLevelExport} import EmbeddedConstants._ import WasmContext._ object Preprocessor { - def preprocess(classes: List[LinkedClass], tles: List[LinkedTopLevelExport]): WasmContext = { + def preprocess(coreSpec: CoreSpec, coreLib: CoreWasmLib, + classes: List[LinkedClass], tles: List[LinkedTopLevelExport]): WasmContext = { val staticFieldMirrors = computeStaticFieldMirrors(tles) val specialInstanceTypes = computeSpecialInstanceTypes(classes) @@ -62,7 +63,8 @@ object Preprocessor { // sort for stability val reflectiveProxyIDs = definedReflectiveProxyNames.toList.sorted.zipWithIndex.toMap - new WasmContext(classInfos, reflectiveProxyIDs, itableBucketCount) + new WasmContext(coreSpec, coreLib, classInfos, reflectiveProxyIDs, + itableBucketCount) } private def computeStaticFieldMirrors( diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala index 9a060bc3b4..00b858c39e 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala @@ -28,9 +28,14 @@ object SpecialNames { val CharBoxCtor = MethodName.constructor(List(CharRef)) val LongBoxCtor = MethodName.constructor(List(LongRef)) + val JLNumberClass = ClassName("java.lang.Number") + // js.JavaScriptException, for WrapAsThrowable and UnwrapFromThrowable val JSExceptionClass = ClassName("scala.scalajs.js.JavaScriptException") + val UndefinedBehaviorErrorClass = + ClassName("org.scalajs.linker.runtime.UndefinedBehaviorError") + // Field names val valueFieldSimpleName = SimpleFieldName("value") @@ -40,6 +45,8 @@ object SpecialNames { // Method names val AnyArgConstructorName = MethodName.constructor(List(ClassRef(ObjectClass))) + val StringArgConstructorName = MethodName.constructor(List(ClassRef(BoxedStringClass))) + val ThrowableArgConsructorName = MethodName.constructor(List(ClassRef(ThrowableClass))) val hashCodeMethodName = MethodName("hashCode", Nil, IntRef) 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 aafdf2f98b..fc25891dc2 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 @@ -77,6 +77,8 @@ object VarGen { final case class clone(className: ClassName) extends FunctionID final case class cloneArray(arrayBaseRef: NonArrayTypeRef) extends FunctionID + final case class asInstance(targetTpe: Type) extends FunctionID + final case class isJSClassInstance(className: ClassName) extends FunctionID final case class loadJSClass(className: ClassName) extends FunctionID final case class createJSClassOf(className: ClassName) extends FunctionID @@ -133,6 +135,7 @@ object VarGen { case object isString extends JSHelperFunctionID case object jsValueType extends JSHelperFunctionID + case object jsValueDescription extends JSHelperFunctionID case object bigintHashCode extends JSHelperFunctionID case object symbolDescription extends JSHelperFunctionID case object idHashCodeGet extends JSHelperFunctionID @@ -220,6 +223,10 @@ object VarGen { case object createClassOf extends FunctionID case object getClassOf extends FunctionID case object arrayTypeData extends FunctionID + case object valueDescription extends FunctionID + case object classCastException extends FunctionID + case object asSpecificRefArray extends FunctionID + case object isInstanceExternal extends FunctionID case object isInstance extends FunctionID case object isAssignableFromExternal extends FunctionID case object isAssignableFrom extends FunctionID 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 aa230752a3..f5eb6dd89e 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 @@ -25,8 +25,7 @@ import org.scalajs.ir.Types._ import org.scalajs.linker.interface.ModuleInitializer import org.scalajs.linker.interface.unstable.ModuleInitializerImpl -import org.scalajs.linker.standard.LinkedTopLevelExport -import org.scalajs.linker.standard.LinkedClass +import org.scalajs.linker.standard.{CoreSpec, LinkedClass, LinkedTopLevelExport} import org.scalajs.linker.backend.webassembly.ModuleBuilder import org.scalajs.linker.backend.webassembly.{Instructions => wa} @@ -38,6 +37,8 @@ import VarGen._ import org.scalajs.ir.OriginalName final class WasmContext( + val coreSpec: CoreSpec, + val coreLib: CoreWasmLib, classInfo: Map[ClassName, WasmContext.ClassInfo], reflectiveProxies: Map[MethodName, Int], val itablesLength: Int diff --git a/project/Build.scala b/project/Build.scala index 5ccf4893b2..63990ee237 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -59,16 +59,23 @@ object ExposedValues extends AutoPlugin { settingKey("enable the WebAssembly backend everywhere, including additional required linker config") // set scalaJSLinkerConfig in someProject ~= makeCompliant - val makeCompliant: StandardConfig => StandardConfig = { - _.withSemantics { semantics => - semantics - .withAsInstanceOfs(CheckedBehavior.Compliant) - .withArrayIndexOutOfBounds(CheckedBehavior.Compliant) - .withArrayStores(CheckedBehavior.Compliant) - .withNegativeArraySizes(CheckedBehavior.Compliant) - .withNullPointers(CheckedBehavior.Compliant) - .withStringIndexOutOfBounds(CheckedBehavior.Compliant) - .withModuleInit(CheckedBehavior.Compliant) + val makeCompliant: StandardConfig => StandardConfig = { prev => + if (prev.experimentalUseWebAssembly) { + prev.withSemantics { semantics => + semantics + .withAsInstanceOfs(CheckedBehavior.Compliant) + } + } else { + prev.withSemantics { semantics => + semantics + .withAsInstanceOfs(CheckedBehavior.Compliant) + .withArrayIndexOutOfBounds(CheckedBehavior.Compliant) + .withArrayStores(CheckedBehavior.Compliant) + .withNegativeArraySizes(CheckedBehavior.Compliant) + .withNullPointers(CheckedBehavior.Compliant) + .withStringIndexOutOfBounds(CheckedBehavior.Compliant) + .withModuleInit(CheckedBehavior.Compliant) + } } } @@ -166,7 +173,6 @@ object MyScalaJSPlugin extends AutoPlugin { .withModuleKind(ModuleKind.ESModule) .withSemantics { sems => sems - .withAsInstanceOfs(Unchecked) .withArrayIndexOutOfBounds(Unchecked) .withArrayStores(Unchecked) .withNegativeArraySizes(Unchecked) From 89dddcdf76ad11b7de54179afc0afd49018a008f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 24 Jun 2024 23:10:34 +0200 Subject: [PATCH 2/8] Wasm: Implement checked stringIndexOutOfBounds. --- .../backend/WebAssemblyLinkerBackend.scala | 1 - .../backend/wasmemitter/CoreWasmLib.scala | 40 +++++++++++++++++++ .../linker/backend/wasmemitter/Emitter.scala | 5 +++ .../backend/wasmemitter/FunctionEmitter.scala | 5 ++- .../backend/wasmemitter/SpecialNames.scala | 1 + .../linker/backend/wasmemitter/VarGen.scala | 1 + project/Build.scala | 2 +- 7 files changed, 52 insertions(+), 3 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala index eb264753d4..3bac291a05 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala @@ -41,7 +41,6 @@ final class WebAssemblyLinkerBackend(config: LinkerBackendImpl.Config) coreSpec.semantics.arrayStores == CheckedBehavior.Unchecked && coreSpec.semantics.negativeArraySizes == CheckedBehavior.Unchecked && coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked && - coreSpec.semantics.stringIndexOutOfBounds == CheckedBehavior.Unchecked && coreSpec.semantics.moduleInit == CheckedBehavior.Unchecked, "The WebAssembly backend currently only supports CheckedBehavior.Unchecked semantics; " + s"was ${coreSpec.semantics}." 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 e9c9f0d3a3..c542d820dc 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 @@ -604,6 +604,10 @@ final class CoreWasmLib(coreSpec: CoreSpec) { genArrayAsInstances() } + if (semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked) { + genCheckedStringCharAt() + } + genIsInstanceExternal() genIsInstance() genIsAssignableFromExternal() @@ -1398,6 +1402,42 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb.buildAndAddToModule() } + /** `checkedStringCharAt: (ref any), i32 -> i32`. + * + * Accesses a char of a string by index. Used when stringIndexOutOfBounds + * are checked. + */ + private def genCheckedStringCharAt()(implicit ctx: WasmContext): Unit = { + val fb = newFunctionBuilder(genFunctionID.checkedStringCharAt) + val strParam = fb.addParam("str", RefType.any) + val indexParam = fb.addParam("index", Int32) + fb.setResultType(Int32) + + // if index unsigned_>= str.length + fb += LocalGet(indexParam) + fb += LocalGet(strParam) + fb += Call(genFunctionID.stringLength) + fb += I32GeU // unsigned comparison makes negative values of index larger than the length + fb.ifThen() { + // then, throw a StringIndexOutOfBoundsException + maybeWrapInUBE(fb, semantics.stringIndexOutOfBounds) { + genNewScalaClass(fb, StringIndexOutOfBoundsExceptionClass, + SpecialNames.IntArgConstructorName) { + fb += LocalGet(indexParam) + } + } + fb += ExternConvertAny + fb += Throw(genTagID.exception) + } + + // otherwise, read the char + fb += LocalGet(strParam) + fb += LocalGet(indexParam) + fb += Call(genFunctionID.stringCharAt) + + fb.buildAndAddToModule() + } + /** `isInstanceExternal: (ref typeData), anyref -> anyref` (a boxed boolean). * * Tests whether the given value is a non-null instance of the given type. 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 145739a409..cbf929e0af 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 @@ -404,6 +404,11 @@ object Emitter { instantiateClass(ClassCastExceptionClass, StringArgConstructorName) }, + cond(stringIndexOutOfBounds != Unchecked) { + instantiateClass(StringIndexOutOfBoundsExceptionClass, + IntArgConstructorName) + }, + cond(isAnyFatal(asInstanceOfs, arrayIndexOutOfBounds, arrayStores, negativeArraySizes, nullPointers, stringIndexOutOfBounds)) { instantiateClass(UndefinedBehaviorErrorClass, 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 8ce4030ea8..13a288ddbf 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 @@ -1323,7 +1323,10 @@ private class FunctionEmitter private ( genTree(lhs, StringType) genTree(rhs, IntType) markPosition(tree) - fb += wa.Call(genFunctionID.stringCharAt) + if (semantics.stringIndexOutOfBounds == CheckedBehavior.Unchecked) + fb += wa.Call(genFunctionID.stringCharAt) + else + fb += wa.Call(genFunctionID.checkedStringCharAt) CharType case _ => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala index 00b858c39e..946532dfce 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/SpecialNames.scala @@ -46,6 +46,7 @@ object SpecialNames { val AnyArgConstructorName = MethodName.constructor(List(ClassRef(ObjectClass))) val StringArgConstructorName = MethodName.constructor(List(ClassRef(BoxedStringClass))) + val IntArgConstructorName = MethodName.constructor(List(IntRef)) val ThrowableArgConsructorName = MethodName.constructor(List(ClassRef(ThrowableClass))) val hashCodeMethodName = MethodName("hashCode", Nil, IntRef) 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 fc25891dc2..7e4f3f7fd8 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 @@ -226,6 +226,7 @@ object VarGen { case object valueDescription extends FunctionID case object classCastException extends FunctionID case object asSpecificRefArray extends FunctionID + case object checkedStringCharAt extends FunctionID case object isInstanceExternal extends FunctionID case object isInstance extends FunctionID case object isAssignableFromExternal extends FunctionID diff --git a/project/Build.scala b/project/Build.scala index 63990ee237..39b880a573 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -64,6 +64,7 @@ object ExposedValues extends AutoPlugin { prev.withSemantics { semantics => semantics .withAsInstanceOfs(CheckedBehavior.Compliant) + .withStringIndexOutOfBounds(CheckedBehavior.Compliant) } } else { prev.withSemantics { semantics => @@ -177,7 +178,6 @@ object MyScalaJSPlugin extends AutoPlugin { .withArrayStores(Unchecked) .withNegativeArraySizes(Unchecked) .withNullPointers(Unchecked) - .withStringIndexOutOfBounds(Unchecked) .withModuleInit(Unchecked) } } else { From 833f778f6aa4f48d18be4cfcedc6d12e0a9f6277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Mon, 24 Jun 2024 23:41:59 +0200 Subject: [PATCH 3/8] Wasm: Implement checked moduleInit. --- .../backend/WebAssemblyLinkerBackend.scala | 3 +- .../backend/wasmemitter/ClassEmitter.scala | 41 ++++++++++++++++++- .../backend/wasmemitter/CoreWasmLib.scala | 28 +++++++++++++ .../linker/backend/wasmemitter/Emitter.scala | 5 +++ .../linker/backend/wasmemitter/VarGen.scala | 2 + project/Build.scala | 2 +- 6 files changed, 77 insertions(+), 4 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala index 3bac291a05..da77b1bebc 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala @@ -40,8 +40,7 @@ final class WebAssemblyLinkerBackend(config: LinkerBackendImpl.Config) coreSpec.semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked && coreSpec.semantics.arrayStores == CheckedBehavior.Unchecked && coreSpec.semantics.negativeArraySizes == CheckedBehavior.Unchecked && - coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked && - coreSpec.semantics.moduleInit == CheckedBehavior.Unchecked, + coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked, "The WebAssembly backend currently only supports CheckedBehavior.Unchecked semantics; " + s"was ${coreSpec.semantics}." ) 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 3d6c1d3c45..23582e35e4 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 @@ -395,6 +395,17 @@ class ClassEmitter(coreSpec: CoreSpec) { ) ctx.addGlobal(global) + if (semantics.moduleInit != CheckedBehavior.Unchecked) { + val initFlagGlobal = wamod.Global( + genGlobalID.forModuleInitFlag(className), + makeDebugName(ns.ModuleInitFlag, className), + isMutable = true, + watpe.Int32, + wa.Expr(List(wa.I32Const(0))) + ) + ctx.addGlobal(initFlagGlobal) + } + genModuleAccessor(clazz) } } @@ -715,7 +726,10 @@ class ClassEmitter(coreSpec: CoreSpec) { makeDebugName(ns.ModuleAccessor, className), clazz.pos ) - fb.setResultType(resultType) + if (semantics.moduleInit == CheckedBehavior.Compliant) + fb.setResultType(resultType.toNullable) + else + fb.setResultType(resultType) val instanceLocal = fb.addLocal("instance", resultType) @@ -724,6 +738,30 @@ class ClassEmitter(coreSpec: CoreSpec) { fb += wa.GlobalGet(globalInstanceID) fb += wa.BrOnNonNull(nonNullLabel) + // check ongoing initialization + if (semantics.moduleInit != CheckedBehavior.Unchecked) { + val initFlagID = genGlobalID.forModuleInitFlag(className) + + // if being initialized + fb += wa.GlobalGet(initFlagID) + fb.ifThen() { + if (semantics.moduleInit == CheckedBehavior.Compliant) { + // then, return null + fb += wa.RefNull(watpe.HeapType.None) + fb += wa.Return + } else { + // then, throw + fb += wa.GlobalGet(genGlobalID.forVTable(className)) + fb += wa.Call(genFunctionID.throwModuleInitError) + fb += wa.Unreachable // for clarity; technically redundant since the stacks align + } + } + + // mark as being initialized + fb += wa.I32Const(1) + fb += wa.GlobalSet(initFlagID) + } + // create an instance and call its constructor fb += wa.Call(genFunctionID.newDefault(className)) fb += wa.LocalTee(instanceLocal) @@ -1363,6 +1401,7 @@ object ClassEmitter { // Shared with JS backend -- className val ModuleAccessor = UTF8String("m.") val ModuleInstance = UTF8String("n.") + val ModuleInitFlag = UTF8String("ni.") val JSClassAccessor = UTF8String("a.") val JSClassValueCache = UTF8String("b.") val TypeData = UTF8String("d.") 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 c542d820dc..118ec474a6 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 @@ -608,6 +608,10 @@ final class CoreWasmLib(coreSpec: CoreSpec) { genCheckedStringCharAt() } + if (semantics.moduleInit == CheckedBehavior.Fatal) { + genThrowModuleInitError() + } + genIsInstanceExternal() genIsInstance() genIsAssignableFromExternal() @@ -1438,6 +1442,30 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb.buildAndAddToModule() } + /** `throwModuleInitError: [] -> []` (always throws). + * + * Throws an `UndefinedBehaviorError` for a module initialization error. + */ + private def genThrowModuleInitError()(implicit ctx: WasmContext): Unit = { + val fb = newFunctionBuilder(genFunctionID.throwModuleInitError) + val typeDataParam = fb.addParam("typeData", RefType(genTypeID.typeData)) + + genNewScalaClass(fb, SpecialNames.UndefinedBehaviorErrorClass, + SpecialNames.StringArgConstructorName) { + fb ++= ctx.stringPool.getConstantStringInstr("Initializer of ") + fb += LocalGet(typeDataParam) + fb += Call(genFunctionID.typeDataName) + fb += Call(genFunctionID.stringConcat) + fb ++= ctx.stringPool.getConstantStringInstr( + " called before completion of its super constructor") + fb += Call(genFunctionID.stringConcat) + } + fb += ExternConvertAny + fb += Throw(genTagID.exception) + + fb.buildAndAddToModule() + } + /** `isInstanceExternal: (ref typeData), anyref -> anyref` (a boxed boolean). * * Tests whether the given value is a non-null instance of the given type. 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 cbf929e0af..bd484fa73f 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 @@ -415,6 +415,11 @@ object Emitter { ThrowableArgConsructorName) }, + cond(moduleInit == Fatal) { + instantiateClass(UndefinedBehaviorErrorClass, + StringArgConstructorName) + }, + // TODO Ideally we should not require these, but rather adapt to their absence instantiateClass(ClassClass, AnyArgConstructorName), instantiateClass(JSExceptionClass, AnyArgConstructorName), 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 7e4f3f7fd8..b3bd680d35 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 @@ -27,6 +27,7 @@ object VarGen { object genGlobalID { final case class forImportedModule(moduleName: String) extends GlobalID final case class forModuleInstance(className: ClassName) extends GlobalID + final case class forModuleInitFlag(className: ClassName) extends GlobalID final case class forJSClassValue(className: ClassName) extends GlobalID final case class forVTable(typeRef: NonArrayTypeRef) extends GlobalID @@ -227,6 +228,7 @@ object VarGen { case object classCastException extends FunctionID case object asSpecificRefArray extends FunctionID case object checkedStringCharAt extends FunctionID + case object throwModuleInitError extends FunctionID case object isInstanceExternal extends FunctionID case object isInstance extends FunctionID case object isAssignableFromExternal extends FunctionID diff --git a/project/Build.scala b/project/Build.scala index 39b880a573..2e83b60638 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -65,6 +65,7 @@ object ExposedValues extends AutoPlugin { semantics .withAsInstanceOfs(CheckedBehavior.Compliant) .withStringIndexOutOfBounds(CheckedBehavior.Compliant) + .withModuleInit(CheckedBehavior.Compliant) } } else { prev.withSemantics { semantics => @@ -178,7 +179,6 @@ object MyScalaJSPlugin extends AutoPlugin { .withArrayStores(Unchecked) .withNegativeArraySizes(Unchecked) .withNullPointers(Unchecked) - .withModuleInit(Unchecked) } } else { baseConfig From 41b259b1d3f123d71daa5486916b336b68023856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 25 Jun 2024 11:01:50 +0200 Subject: [PATCH 4/8] Wasm: Implement checked arrayIndexOutOfBounds. --- .../backend/WebAssemblyLinkerBackend.scala | 1 - .../backend/wasmemitter/CoreWasmLib.scala | 215 ++++++++++++++++++ .../linker/backend/wasmemitter/Emitter.scala | 5 + .../backend/wasmemitter/FunctionEmitter.scala | 67 +++--- .../linker/backend/wasmemitter/VarGen.scala | 16 ++ project/Build.scala | 2 +- 6 files changed, 277 insertions(+), 29 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala index da77b1bebc..184d41089b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala @@ -37,7 +37,6 @@ final class WebAssemblyLinkerBackend(config: LinkerBackendImpl.Config) s"The WebAssembly backend only supports ES modules; was ${coreSpec.moduleKind}." ) require( - coreSpec.semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked && coreSpec.semantics.arrayStores == CheckedBehavior.Unchecked && coreSpec.semantics.negativeArraySizes == CheckedBehavior.Unchecked && coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked, 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 118ec474a6..7939bd41df 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 @@ -604,6 +604,12 @@ final class CoreWasmLib(coreSpec: CoreSpec) { genArrayAsInstances() } + if (semantics.arrayIndexOutOfBounds != CheckedBehavior.Unchecked) { + genThrowArrayIndexOutOfBoundsException() + genArrayGets() + genArraySets() + } + if (semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked) { genCheckedStringCharAt() } @@ -1272,6 +1278,140 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb.buildAndAddToModule() } + /** `throwArrayIndexOutOfBoundsException: i32 -> void`. + * + * This function always throws. It should be followed by an `unreachable` + * statement. + */ + private def genThrowArrayIndexOutOfBoundsException()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.throwArrayIndexOutOfBoundsException) + val indexParam = fb.addParam("index", Int32) + + maybeWrapInUBE(fb, semantics.arrayIndexOutOfBounds) { + genNewScalaClass(fb, ArrayIndexOutOfBoundsExceptionClass, + SpecialNames.StringArgConstructorName) { + fb += LocalGet(indexParam) + fb += Call(genFunctionID.intToString) + } + } + fb += ExternConvertAny + fb += Throw(genTagID.exception) + + fb.buildAndAddToModule() + } + + /** Generates the `arrayGet.x` functions. */ + private def genArrayGets()(implicit ctx: WasmContext): Unit = { + for (baseRef <- arrayBaseRefs) + genArrayGet(baseRef) + } + + /** `arrayGet.x: (ref null xArray), i32 -> x`. */ + private def genArrayGet(baseRef: NonArrayTypeRef)(implicit ctx: WasmContext): Unit = { + val origName = OriginalName("arrayGet." + charCodeForOriginalName(baseRef)) + + val arrayTypeRef = ArrayTypeRef(baseRef, 1) + val arrayStructTypeID = genTypeID.forArrayClass(arrayTypeRef) + val underlyingTypeID = genTypeID.underlyingOf(arrayTypeRef) + + val elemWasmType = baseRef match { + case PrimRef(tpe) => transformSingleType(tpe) + case ClassRef(_) => anyref + } + + val fb = newFunctionBuilder(genFunctionID.arrayGet(baseRef), origName) + val arrayParam = fb.addParam("array", RefType.nullable(arrayStructTypeID)) + val indexParam = fb.addParam("index", Int32) + fb.setResultType(elemWasmType) + + val underlyingLocal = fb.addLocal("underlying", RefType(underlyingTypeID)) + + // Get the underlying array + fb += LocalGet(arrayParam) + fb += StructGet(arrayStructTypeID, genFieldID.objStruct.arrayUnderlying) + fb += LocalTee(underlyingLocal) + + // if underlying.length unsigned_<= index + fb += ArrayLen + fb += LocalGet(indexParam) + fb += I32LeU + fb.ifThen() { + // then throw ArrayIndexOutOfBoundsException + fb += LocalGet(indexParam) + fb += Call(genFunctionID.throwArrayIndexOutOfBoundsException) + fb += Unreachable + } + + // Load the underlying and index + fb += LocalGet(underlyingLocal) + fb += LocalGet(indexParam) + + // Use the appropriate variant of array.get for sign extension + baseRef match { + case BooleanRef | CharRef => + fb += ArrayGetU(underlyingTypeID) + case ByteRef | ShortRef => + fb += ArrayGetS(underlyingTypeID) + case _ => + fb += ArrayGet(underlyingTypeID) + } + + fb.buildAndAddToModule() + } + + /** Generates the `arraySet.x` functions. */ + private def genArraySets()(implicit ctx: WasmContext): Unit = { + for (baseRef <- arrayBaseRefs) + genArraySet(baseRef) + } + + /** `arraySet.x: (ref null xArray), i32, x -> []`. */ + private def genArraySet(baseRef: NonArrayTypeRef)(implicit ctx: WasmContext): Unit = { + val origName = OriginalName("arraySet." + charCodeForOriginalName(baseRef)) + + val arrayTypeRef = ArrayTypeRef(baseRef, 1) + val arrayStructTypeID = genTypeID.forArrayClass(arrayTypeRef) + val underlyingTypeID = genTypeID.underlyingOf(arrayTypeRef) + + val elemWasmType = baseRef match { + case PrimRef(tpe) => transformSingleType(tpe) + case ClassRef(_) => anyref + } + + val fb = newFunctionBuilder(genFunctionID.arraySet(baseRef), origName) + val arrayParam = fb.addParam("array", RefType.nullable(arrayStructTypeID)) + val indexParam = fb.addParam("index", Int32) + val valueParam = fb.addParam("value", elemWasmType) + + val underlyingLocal = fb.addLocal("underlying", RefType(underlyingTypeID)) + + // Get the underlying array + fb += LocalGet(arrayParam) + fb += StructGet(arrayStructTypeID, genFieldID.objStruct.arrayUnderlying) + fb += LocalTee(underlyingLocal) + + // if underlying.length unsigned_<= index + fb += ArrayLen + fb += LocalGet(indexParam) + fb += I32LeU + fb.ifThen() { + // then throw ArrayIndexOutOfBoundsException + fb += LocalGet(indexParam) + fb += Call(genFunctionID.throwArrayIndexOutOfBoundsException) + fb += Unreachable + } + + // Store the value + fb += LocalGet(underlyingLocal) + fb += LocalGet(indexParam) + fb += LocalGet(valueParam) + fb += ArraySet(underlyingTypeID) + + fb.buildAndAddToModule() + } + /** `arrayTypeData: (ref typeData), i32 -> (ref vtable.java.lang.Object)`. * * Returns the typeData/vtable of an array with `dims` dimensions over the given typeData. `dims` @@ -2639,12 +2779,74 @@ final class CoreWasmLib(coreSpec: CoreSpec) { } private def genArrayCopyFunctions()(implicit ctx: WasmContext): Unit = { + if (semantics.arrayIndexOutOfBounds != CheckedBehavior.Unchecked) + genArrayCopyCheckBounds() + for (baseRef <- arrayBaseRefs) genSpecializedArrayCopy(baseRef) genGenericArrayCopy() } + /** `arrayCopyCheckBounds: [i32, i32, i32, i32, i32] -> []`. + * + * Checks all the bounds for an `arrayCopy`. Arguments correspond to the + * arguments of the `arrayCopy`, where arrays are replaced by their lengths. + */ + private def genArrayCopyCheckBounds()(implicit ctx: WasmContext): Unit = { + val fb = newFunctionBuilder(genFunctionID.arrayCopyCheckBounds) + val srcLengthParam = fb.addParam("srcLength", Int32) + val srcPosParam = fb.addParam("srcPos", Int32) + val destLengthParam = fb.addParam("destLength", Int32) + val destPosParam = fb.addParam("destPos", Int32) + val lengthParam = fb.addParam("length", Int32) + + fb.block() { failureLabel => + /* if (srcPos < 0) || (destPos < 0) || (length < 0), fail + * we test all of those with a single branch as follows: + * ((srcPos | destPos | length) & 0x80000000) != 0 + */ + fb += LocalGet(srcPosParam) + fb += LocalGet(destPosParam) + fb += I32Or + fb += LocalGet(lengthParam) + fb += I32Or + fb += I32Const(0x80000000) + fb += I32And + fb += BrIf(failureLabel) + + // if srcPos > (srcLength - length), fail + fb += LocalGet(srcPosParam) + fb += LocalGet(srcLengthParam) + fb += LocalGet(lengthParam) + fb += I32Sub + fb += I32GtS + fb += BrIf(failureLabel) + + // if destPos > (destLength - length), fail + fb += LocalGet(destPosParam) + fb += LocalGet(destLengthParam) + fb += LocalGet(lengthParam) + fb += I32Sub + fb += I32GtS + fb += BrIf(failureLabel) + + // otherwise, succeed + fb += Return + } + + maybeWrapInUBE(fb, semantics.arrayIndexOutOfBounds) { + genNewScalaClass(fb, ArrayIndexOutOfBoundsExceptionClass, + SpecialNames.StringArgConstructorName) { + fb += RefNull(HeapType.None) + } + } + fb += ExternConvertAny + fb += Throw(genTagID.exception) + + fb.buildAndAddToModule() + } + /** Generates a specialized arrayCopy for the array class with the given base. */ private def genSpecializedArrayCopy(baseRef: NonArrayTypeRef)(implicit ctx: WasmContext): Unit = { val originalName = OriginalName("arrayCopy." + charCodeForOriginalName(baseRef)) @@ -2661,6 +2863,19 @@ final class CoreWasmLib(coreSpec: CoreSpec) { val destPosParam = fb.addParam("destPos", Int32) val lengthParam = fb.addParam("length", Int32) + if (semantics.arrayIndexOutOfBounds != CheckedBehavior.Unchecked) { + fb += LocalGet(srcParam) + fb += StructGet(arrayStructTypeID, genFieldID.objStruct.arrayUnderlying) + fb += ArrayLen + fb += LocalGet(srcPosParam) + fb += LocalGet(destParam) + fb += StructGet(arrayStructTypeID, genFieldID.objStruct.arrayUnderlying) + fb += ArrayLen + fb += LocalGet(destPosParam) + fb += LocalGet(lengthParam) + fb += Call(genFunctionID.arrayCopyCheckBounds) + } + fb += LocalGet(destParam) fb += StructGet(arrayStructTypeID, genFieldID.objStruct.arrayUnderlying) fb += LocalGet(destPosParam) 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 bd484fa73f..b67679c858 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 @@ -404,6 +404,11 @@ object Emitter { instantiateClass(ClassCastExceptionClass, StringArgConstructorName) }, + cond(arrayIndexOutOfBounds != Unchecked) { + instantiateClass(ArrayIndexOutOfBoundsExceptionClass, + StringArgConstructorName) + }, + cond(stringIndexOutOfBounds != Unchecked) { instantiateClass(StringIndexOutOfBoundsExceptionClass, IntArgConstructorName) 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 13a288ddbf..324f4c7f8b 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 @@ -548,16 +548,23 @@ private class FunctionEmitter private ( genTreeAuto(array) array.tpe match { case ArrayType(arrayTypeRef, _) => - // Get the underlying array; implicit trap on null - markPosition(tree) - fb += wa.StructGet( - genTypeID.forArrayClass(arrayTypeRef), - genFieldID.objStruct.arrayUnderlying - ) - genTree(index, IntType) - genTree(rhs, lhs.tpe) - markPosition(tree) - fb += wa.ArraySet(genTypeID.underlyingOf(arrayTypeRef)) + if (semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked) { + // Get the underlying array; implicit trap on null + markPosition(tree) + fb += wa.StructGet( + genTypeID.forArrayClass(arrayTypeRef), + genFieldID.objStruct.arrayUnderlying + ) + genTree(index, IntType) + genTree(rhs, lhs.tpe) + markPosition(tree) + fb += wa.ArraySet(genTypeID.underlyingOf(arrayTypeRef)) + } else { + genTree(index, IntType) + genTree(rhs, lhs.tpe) + markPosition(tree) + fb += wa.Call(genFunctionID.arraySetFor(arrayTypeRef)) + } case NothingType => // unreachable () @@ -2631,26 +2638,32 @@ private class FunctionEmitter private ( array.tpe match { case ArrayType(arrayTypeRef, _) => - // Get the underlying array; implicit trap on null - fb += wa.StructGet( - genTypeID.forArrayClass(arrayTypeRef), - genFieldID.objStruct.arrayUnderlying - ) + if (semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked) { + // Get the underlying array; implicit trap on null + fb += wa.StructGet( + genTypeID.forArrayClass(arrayTypeRef), + genFieldID.objStruct.arrayUnderlying + ) - // Load the index - genTree(index, IntType) + // Load the index + genTree(index, IntType) - markPosition(tree) + markPosition(tree) - // Use the appropriate variant of array.get for sign extension - val typeIdx = genTypeID.underlyingOf(arrayTypeRef) - arrayTypeRef match { - case ArrayTypeRef(BooleanRef | CharRef, 1) => - fb += wa.ArrayGetU(typeIdx) - case ArrayTypeRef(ByteRef | ShortRef, 1) => - fb += wa.ArrayGetS(typeIdx) - case _ => - fb += wa.ArrayGet(typeIdx) + // Use the appropriate variant of array.get for sign extension + val typeIdx = genTypeID.underlyingOf(arrayTypeRef) + arrayTypeRef match { + case ArrayTypeRef(BooleanRef | CharRef, 1) => + fb += wa.ArrayGetU(typeIdx) + case ArrayTypeRef(ByteRef | ShortRef, 1) => + fb += wa.ArrayGetS(typeIdx) + case _ => + fb += wa.ArrayGet(typeIdx) + } + } else { + genTree(index, IntType) + markPosition(tree) + fb += wa.Call(genFunctionID.arrayGetFor(arrayTypeRef)) } /* If it is a reference array type whose element type does not translate 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 b3bd680d35..43517afab2 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 @@ -80,6 +80,20 @@ object VarGen { final case class asInstance(targetTpe: Type) extends FunctionID + final case class arrayGet(baseRef: NonArrayTypeRef) extends FunctionID + + def arrayGetFor(arrayTypeRef: ArrayTypeRef): arrayGet = arrayTypeRef match { + case ArrayTypeRef(base: PrimRef, 1) => arrayGet(base) + case _ => arrayGet(ClassRef(ObjectClass)) + } + + final case class arraySet(baseRef: NonArrayTypeRef) extends FunctionID + + def arraySetFor(arrayTypeRef: ArrayTypeRef): arraySet = arrayTypeRef match { + case ArrayTypeRef(base: PrimRef, 1) => arraySet(base) + case _ => arraySet(ClassRef(ObjectClass)) + } + final case class isJSClassInstance(className: ClassName) extends FunctionID final case class loadJSClass(className: ClassName) extends FunctionID final case class createJSClassOf(className: ClassName) extends FunctionID @@ -227,6 +241,7 @@ object VarGen { case object valueDescription extends FunctionID case object classCastException extends FunctionID case object asSpecificRefArray extends FunctionID + case object throwArrayIndexOutOfBoundsException extends FunctionID case object checkedStringCharAt extends FunctionID case object throwModuleInitError extends FunctionID case object isInstanceExternal extends FunctionID @@ -253,6 +268,7 @@ object VarGen { SpecializedArrayCopyID(baseRef) } + case object arrayCopyCheckBounds extends FunctionID case object genericArrayCopy extends FunctionID } diff --git a/project/Build.scala b/project/Build.scala index 2e83b60638..a56d5aa371 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -64,6 +64,7 @@ object ExposedValues extends AutoPlugin { prev.withSemantics { semantics => semantics .withAsInstanceOfs(CheckedBehavior.Compliant) + .withArrayIndexOutOfBounds(CheckedBehavior.Compliant) .withStringIndexOutOfBounds(CheckedBehavior.Compliant) .withModuleInit(CheckedBehavior.Compliant) } @@ -175,7 +176,6 @@ object MyScalaJSPlugin extends AutoPlugin { .withModuleKind(ModuleKind.ESModule) .withSemantics { sems => sems - .withArrayIndexOutOfBounds(Unchecked) .withArrayStores(Unchecked) .withNegativeArraySizes(Unchecked) .withNullPointers(Unchecked) From cff558ff8616a372accf049f3f44e9555a951dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 25 Jun 2024 13:17:47 +0200 Subject: [PATCH 5/8] Wasm: Implement checked arrayStores. --- .../backend/WebAssemblyLinkerBackend.scala | 1 - .../backend/wasmemitter/CoreWasmLib.scala | 237 ++++++++++++++++-- .../linker/backend/wasmemitter/Emitter.scala | 5 + .../backend/wasmemitter/FunctionEmitter.scala | 8 +- .../linker/backend/wasmemitter/VarGen.scala | 2 + project/Build.scala | 2 +- 6 files changed, 226 insertions(+), 29 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala index 184d41089b..c3276f550e 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala @@ -37,7 +37,6 @@ final class WebAssemblyLinkerBackend(config: LinkerBackendImpl.Config) s"The WebAssembly backend only supports ES modules; was ${coreSpec.moduleKind}." ) require( - coreSpec.semantics.arrayStores == CheckedBehavior.Unchecked && coreSpec.semantics.negativeArraySizes == CheckedBehavior.Unchecked && coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked, "The WebAssembly backend currently only supports CheckedBehavior.Unchecked semantics; " + 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 7939bd41df..f89d1d7e67 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 @@ -597,17 +597,26 @@ final class CoreWasmLib(coreSpec: CoreSpec) { genGetClassOf() genArrayTypeData() - if (semantics.asInstanceOfs != CheckedBehavior.Unchecked) { + if (semantics.asInstanceOfs != CheckedBehavior.Unchecked || + semantics.arrayStores != CheckedBehavior.Unchecked) { genValueDescription() + } + + if (semantics.asInstanceOfs != CheckedBehavior.Unchecked) { genClassCastException() genPrimitiveAsInstances() genArrayAsInstances() } + if (semantics.arrayStores != CheckedBehavior.Unchecked) + genThrowArrayStoreException() + if (semantics.arrayIndexOutOfBounds != CheckedBehavior.Unchecked) { genThrowArrayIndexOutOfBoundsException() genArrayGets() genArraySets() + } else if (semantics.arrayStores != CheckedBehavior.Unchecked) { + genArraySet(ClassRef(ObjectClass)) } if (semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked) { @@ -1278,6 +1287,28 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb.buildAndAddToModule() } + /** `throwArrayStoreException: anyref -> void`. + * + * This function always throws. It should be followed by an `unreachable` + * statement. + */ + private def genThrowArrayStoreException()(implicit ctx: WasmContext): Unit = { + val fb = newFunctionBuilder(genFunctionID.throwArrayStoreException) + val valueParam = fb.addParam("value", anyref) + + maybeWrapInUBE(fb, semantics.arrayStores) { + genNewScalaClass(fb, ArrayStoreExceptionClass, + SpecialNames.StringArgConstructorName) { + fb += LocalGet(valueParam) + fb += Call(genFunctionID.valueDescription) + } + } + fb += ExternConvertAny + fb += Throw(genTagID.exception) + + fb.buildAndAddToModule() + } + /** `throwArrayIndexOutOfBoundsException: i32 -> void`. * * This function always throws. It should be followed by an `unreachable` @@ -1390,17 +1421,66 @@ final class CoreWasmLib(coreSpec: CoreSpec) { // Get the underlying array fb += LocalGet(arrayParam) fb += StructGet(arrayStructTypeID, genFieldID.objStruct.arrayUnderlying) - fb += LocalTee(underlyingLocal) - // if underlying.length unsigned_<= index - fb += ArrayLen - fb += LocalGet(indexParam) - fb += I32LeU - fb.ifThen() { - // then throw ArrayIndexOutOfBoundsException + // Bounds check + if (semantics.arrayIndexOutOfBounds != CheckedBehavior.Unchecked) { + fb += LocalTee(underlyingLocal) + + // if underlying.length unsigned_<= index + fb += ArrayLen fb += LocalGet(indexParam) - fb += Call(genFunctionID.throwArrayIndexOutOfBoundsException) - fb += Unreachable + fb += I32LeU + fb.ifThen() { + // then throw ArrayIndexOutOfBoundsException + fb += LocalGet(indexParam) + fb += Call(genFunctionID.throwArrayIndexOutOfBoundsException) + fb += Unreachable + } + } else { + fb += LocalSet(underlyingLocal) + } + + // Store check + if (semantics.arrayStores != CheckedBehavior.Unchecked && + baseRef.isInstanceOf[ClassRef]) { + val componentTypeDataLocal = fb.addLocal("componentTypeData", RefType(genTypeID.typeData)) + + fb.block() { successLabel => + // Get the component type data + fb += LocalGet(arrayParam) + fb += StructGet(arrayStructTypeID, genFieldID.objStruct.vtable) + fb += StructGet(genTypeID.ObjectVTable, genFieldID.typeData.componentType) + fb += RefAsNonNull + fb += LocalTee(componentTypeDataLocal) + + // Fast path: if componentTypeData eq typeDataOf[jl.Object], succeed + fb += GlobalGet(genGlobalID.forVTable(ClassRef(ObjectClass))) + fb += RefEq + fb += BrIf(successLabel) + + // If componentTypeData.kind == KindJSType, succeed + fb += LocalGet(componentTypeDataLocal) + fb += StructGet(genTypeID.typeData, genFieldID.typeData.kind) + fb += I32Const(KindJSType) + fb += I32Eq + fb += BrIf(successLabel) + + // If value is null, succeed + fb += LocalGet(valueParam) + fb += RefIsNull + fb += BrIf(successLabel) + + // If isInstance(componentTypeData, value), succeed + fb += LocalGet(componentTypeDataLocal) + fb += LocalGet(valueParam) + fb += Call(genFunctionID.isInstance) + fb += BrIf(successLabel) + + // Otherwise, it is a store exception + fb += LocalGet(valueParam) + fb += Call(genFunctionID.throwArrayStoreException) + fb += Unreachable // for clarity; technically redundant since the stacks align + } } // Store the value @@ -2782,6 +2862,9 @@ final class CoreWasmLib(coreSpec: CoreSpec) { if (semantics.arrayIndexOutOfBounds != CheckedBehavior.Unchecked) genArrayCopyCheckBounds() + if (semantics.arrayStores != CheckedBehavior.Unchecked) + genSlowRefArrayCopy() + for (baseRef <- arrayBaseRefs) genSpecializedArrayCopy(baseRef) @@ -2847,6 +2930,72 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb.buildAndAddToModule() } + /** `slowRefArrayCopy: [ArrayObject, i32, ArrayObject, i32, i32] -> []` + * + * Used when the type of the dest is not assignable from the type of the source. + * Performs an `arraySet` call for every element in order to detect + * `ArrayStoreException`s. + * + * Bounds are already known to be valid. + */ + private def genSlowRefArrayCopy()(implicit ctx: WasmContext): Unit = { + val baseRef = ClassRef(ObjectClass) + val arrayTypeRef = ArrayTypeRef(baseRef, 1) + val arrayStructTypeID = genTypeID.forArrayClass(arrayTypeRef) + val arrayClassType = RefType.nullable(arrayStructTypeID) + val underlyingArrayTypeID = genTypeID.underlyingOf(arrayTypeRef) + + val fb = newFunctionBuilder(genFunctionID.slowRefArrayCopy) + val srcParam = fb.addParam("src", arrayClassType) + val srcPosParam = fb.addParam("srcPos", Int32) + val destParam = fb.addParam("dest", arrayClassType) + val destPosParam = fb.addParam("destPos", Int32) + val lengthParam = fb.addParam("length", Int32) + + val srcUnderlyingLocal = fb.addLocal("srcUnderlying", RefType(underlyingArrayTypeID)) + val iLocal = fb.addLocal("i", Int32) + + // srcUnderlying := src.underlying + fb += LocalGet(srcParam) + fb += StructGet(arrayStructTypeID, genFieldID.objStruct.arrayUnderlying) + fb += LocalSet(srcUnderlyingLocal) + + // i := 0 + fb += I32Const(0) + fb += LocalSet(iLocal) + + // while i != length + fb.whileLoop() { + fb += LocalGet(iLocal) + fb += LocalGet(lengthParam) + fb += I32Ne + } { + // arraySet.O(dest, destPos + i, srcUnderlying(srcPos + i)) + + fb += LocalGet(destParam) + + fb += LocalGet(destPosParam) + fb += LocalGet(iLocal) + fb += I32Add + + fb += LocalGet(srcUnderlyingLocal) + fb += LocalGet(srcPosParam) + fb += LocalGet(iLocal) + fb += I32Add + fb += ArrayGet(underlyingArrayTypeID) + + fb += Call(genFunctionID.arraySet(baseRef)) + + // i := i + 1 + fb += LocalGet(iLocal) + fb += I32Const(1) + fb += I32Add + fb += LocalSet(iLocal) + } + + fb.buildAndAddToModule() + } + /** Generates a specialized arrayCopy for the array class with the given base. */ private def genSpecializedArrayCopy(baseRef: NonArrayTypeRef)(implicit ctx: WasmContext): Unit = { val originalName = OriginalName("arrayCopy." + charCodeForOriginalName(baseRef)) @@ -2876,6 +3025,25 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb += Call(genFunctionID.arrayCopyCheckBounds) } + if (baseRef.isInstanceOf[ClassRef] && semantics.arrayStores != CheckedBehavior.Unchecked) { + // if !isAssignableFrom(dest.vtable, src.vtable) + fb += LocalGet(destParam) + fb += StructGet(arrayStructTypeID, genFieldID.objStruct.vtable) + fb += LocalGet(srcParam) + fb += StructGet(arrayStructTypeID, genFieldID.objStruct.vtable) + fb += Call(genFunctionID.isAssignableFrom) // contains a fast-path for `eq` vtables + fb += I32Eqz + fb.ifThen() { + // then, delegate to the slow copy method + fb += LocalGet(srcParam) + fb += LocalGet(srcPosParam) + fb += LocalGet(destParam) + fb += LocalGet(destPosParam) + fb += LocalGet(lengthParam) + fb += ReturnCall(genFunctionID.slowRefArrayCopy) + } + } + fb += LocalGet(destParam) fb += StructGet(arrayStructTypeID, genFieldID.objStruct.arrayUnderlying) fb += LocalGet(destPosParam) @@ -2900,29 +3068,46 @@ final class CoreWasmLib(coreSpec: CoreSpec) { val anyrefToAnyrefBlockType = fb.sigToBlockType(FunctionType(List(RefType.anyref), List(RefType.anyref))) - // Dispatch done based on the type of src - fb += LocalGet(srcParam) + // note: this block is never used for Unchecked arrayStores, but it does not hurt much + fb.block(anyref) { mismatchLabel => + // Dispatch done based on the type of src + fb += LocalGet(srcParam) - for (baseRef <- arrayBaseRefs) { - val arrayTypeRef = ArrayTypeRef(baseRef, 1) - val arrayStructTypeID = genTypeID.forArrayClass(arrayTypeRef) - val nonNullArrayClassType = RefType(arrayStructTypeID) + for (baseRef <- arrayBaseRefs) { + val arrayTypeRef = ArrayTypeRef(baseRef, 1) + val arrayStructTypeID = genTypeID.forArrayClass(arrayTypeRef) + val nonNullArrayClassType = RefType(arrayStructTypeID) - fb.block(anyrefToAnyrefBlockType) { notThisArrayTypeLabel => - fb += BrOnCastFail(notThisArrayTypeLabel, RefType.anyref, nonNullArrayClassType) + fb.block(anyrefToAnyrefBlockType) { notThisArrayTypeLabel => + fb += BrOnCastFail(notThisArrayTypeLabel, RefType.anyref, nonNullArrayClassType) - fb += LocalGet(srcPosParam) - fb += LocalGet(destParam) - fb += RefCast(nonNullArrayClassType) - fb += LocalGet(destPosParam) - fb += LocalGet(lengthParam) + fb += LocalGet(srcPosParam) + fb += LocalGet(destParam) + if (semantics.arrayStores == CheckedBehavior.Unchecked) + fb += RefCast(nonNullArrayClassType) + else + fb += BrOnCastFail(mismatchLabel, anyref, nonNullArrayClassType) + fb += LocalGet(destPosParam) + fb += LocalGet(lengthParam) - fb += ReturnCall(genFunctionID.specializedArrayCopy(arrayTypeRef)) + fb += ReturnCall(genFunctionID.specializedArrayCopy(arrayTypeRef)) + } } } - // Trap if `src` was not an instance of any of the array class types - fb += Unreachable + // Mismatch of array types, or either array was not an array + if (semantics.arrayStores == CheckedBehavior.Unchecked) { + fb += Unreachable // trap + } else { + maybeWrapInUBE(fb, semantics.arrayStores) { + genNewScalaClass(fb, ArrayStoreExceptionClass, + SpecialNames.StringArgConstructorName) { + fb += RefNull(HeapType.None) + } + } + fb += ExternConvertAny + fb += Throw(genTagID.exception) + } fb.buildAndAddToModule() } 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 b67679c858..91796f5b7e 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 @@ -409,6 +409,11 @@ object Emitter { StringArgConstructorName) }, + cond(arrayStores != Unchecked) { + instantiateClass(ArrayStoreExceptionClass, + StringArgConstructorName) + }, + cond(stringIndexOutOfBounds != Unchecked) { instantiateClass(StringIndexOutOfBoundsExceptionClass, IntArgConstructorName) 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 324f4c7f8b..b421b8b9b1 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 @@ -548,7 +548,13 @@ private class FunctionEmitter private ( genTreeAuto(array) array.tpe match { case ArrayType(arrayTypeRef, _) => - if (semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked) { + def isPrimArray = arrayTypeRef match { + case ArrayTypeRef(_: PrimRef, 1) => true + case _ => false + } + + if (semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked && + (semantics.arrayStores == CheckedBehavior.Unchecked || isPrimArray)) { // Get the underlying array; implicit trap on null markPosition(tree) fb += wa.StructGet( 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 43517afab2..949455faa4 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 @@ -241,6 +241,7 @@ object VarGen { case object valueDescription extends FunctionID case object classCastException extends FunctionID case object asSpecificRefArray extends FunctionID + case object throwArrayStoreException extends FunctionID case object throwArrayIndexOutOfBoundsException extends FunctionID case object checkedStringCharAt extends FunctionID case object throwModuleInitError extends FunctionID @@ -269,6 +270,7 @@ object VarGen { } case object arrayCopyCheckBounds extends FunctionID + case object slowRefArrayCopy extends FunctionID case object genericArrayCopy extends FunctionID } diff --git a/project/Build.scala b/project/Build.scala index a56d5aa371..d6ad8df80c 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -65,6 +65,7 @@ object ExposedValues extends AutoPlugin { semantics .withAsInstanceOfs(CheckedBehavior.Compliant) .withArrayIndexOutOfBounds(CheckedBehavior.Compliant) + .withArrayStores(CheckedBehavior.Compliant) .withStringIndexOutOfBounds(CheckedBehavior.Compliant) .withModuleInit(CheckedBehavior.Compliant) } @@ -176,7 +177,6 @@ object MyScalaJSPlugin extends AutoPlugin { .withModuleKind(ModuleKind.ESModule) .withSemantics { sems => sems - .withArrayStores(Unchecked) .withNegativeArraySizes(Unchecked) .withNullPointers(Unchecked) } From e22e40e8e5d910e433c627ce5d4c6ff5dce6e333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 25 Jun 2024 19:50:45 +0200 Subject: [PATCH 6/8] Wasm: Implement checked negativeArraySizes. --- .../backend/WebAssemblyLinkerBackend.scala | 1 - .../backend/wasmemitter/CoreWasmLib.scala | 42 +++++++++++++++++++ .../linker/backend/wasmemitter/Emitter.scala | 5 +++ .../backend/wasmemitter/FunctionEmitter.scala | 20 +++++++++ .../linker/backend/wasmemitter/VarGen.scala | 1 + project/Build.scala | 2 +- 6 files changed, 69 insertions(+), 2 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala index c3276f550e..b0d5c407f1 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala @@ -37,7 +37,6 @@ final class WebAssemblyLinkerBackend(config: LinkerBackendImpl.Config) s"The WebAssembly backend only supports ES modules; was ${coreSpec.moduleKind}." ) require( - coreSpec.semantics.negativeArraySizes == CheckedBehavior.Unchecked && coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked, "The WebAssembly backend currently only supports CheckedBehavior.Unchecked semantics; " + s"was ${coreSpec.semantics}." 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 f89d1d7e67..424444d50c 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 @@ -619,6 +619,10 @@ final class CoreWasmLib(coreSpec: CoreSpec) { genArraySet(ClassRef(ObjectClass)) } + if (semantics.negativeArraySizes != CheckedBehavior.Unchecked) { + genThrowNegativeArraySizeException() + } + if (semantics.stringIndexOutOfBounds != CheckedBehavior.Unchecked) { genCheckedStringCharAt() } @@ -1333,6 +1337,30 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb.buildAndAddToModule() } + /** `throwNegativeArraySizeException: i32 -> void`. + * + * This function always throws. It should be followed by an `unreachable` + * statement. + */ + private def genThrowNegativeArraySizeException()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.throwNegativeArraySizeException) + val sizeParam = fb.addParam("size", Int32) + + maybeWrapInUBE(fb, semantics.negativeArraySizes) { + genNewScalaClass(fb, NegativeArraySizeExceptionClass, + SpecialNames.StringArgConstructorName) { + fb += LocalGet(sizeParam) + fb += Call(genFunctionID.intToString) + } + } + fb += ExternConvertAny + fb += Throw(genTagID.exception) + + fb.buildAndAddToModule() + } + /** Generates the `arrayGet.x` functions. */ private def genArrayGets()(implicit ctx: WasmContext): Unit = { for (baseRef <- arrayBaseRefs) @@ -2419,6 +2447,7 @@ final class CoreWasmLib(coreSpec: CoreSpec) { * def newArrayObject(arrayTypeData, lengths, lengthIndex) { * // create an array of the right primitive type * val len = lengths(lengthIndex) + * // possibly: check negative array size * switch (arrayTypeData.componentType.kind) { * // for primitives, return without recursion * case KindBoolean => new Array[Boolean](len) @@ -2459,6 +2488,19 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb += LocalGet(lengthIndexParam) fb += ArrayGet(genTypeID.i32Array) + // Check negative array size + if (semantics.negativeArraySizes != CheckedBehavior.Unchecked) { + fb += LocalTee(lenLocal) + fb += I32Const(0) + fb += I32LtS + fb.ifThen() { + fb += LocalGet(lenLocal) + fb += Call(genFunctionID.throwNegativeArraySizeException) + fb += Unreachable + } + fb += LocalGet(lenLocal) + } + // componentTypeData := ref_as_non_null(arrayTypeData.componentType) // switch (componentTypeData.kind) val switchClauseSig = FunctionType( 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 91796f5b7e..bd4fa6697a 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 @@ -414,6 +414,11 @@ object Emitter { StringArgConstructorName) }, + cond(negativeArraySizes != Unchecked) { + instantiateClass(NegativeArraySizeExceptionClass, + StringArgConstructorName) + }, + cond(stringIndexOutOfBounds != Unchecked) { instantiateClass(StringIndexOutOfBoundsExceptionClass, IntArgConstructorName) 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 b421b8b9b1..672bf26edd 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 @@ -2605,6 +2605,26 @@ private class FunctionEmitter private ( genTree(lengths.head, IntType) markPosition(tree) + if (semantics.negativeArraySizes != CheckedBehavior.Unchecked) { + lengths.head match { + case IntLiteral(lengthValue) if lengthValue >= 0 => + () // always good + case _ => + // if length < 0 + val lengthLocal = addSyntheticLocal(watpe.Int32) + fb += wa.LocalTee(lengthLocal) + fb += wa.I32Const(0) + fb += wa.I32LtS + fb.ifThen() { + // then throw NegativeArraySizeException + fb += wa.LocalGet(lengthLocal) + fb += wa.Call(genFunctionID.throwNegativeArraySizeException) + fb += wa.Unreachable + } + fb += wa.LocalGet(lengthLocal) + } + } + val underlyingArrayType = genTypeID.underlyingOf(arrayTypeRef) fb += wa.ArrayNewDefault(underlyingArrayType) 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 949455faa4..de6494b653 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 @@ -243,6 +243,7 @@ object VarGen { case object asSpecificRefArray extends FunctionID case object throwArrayStoreException extends FunctionID case object throwArrayIndexOutOfBoundsException extends FunctionID + case object throwNegativeArraySizeException extends FunctionID case object checkedStringCharAt extends FunctionID case object throwModuleInitError extends FunctionID case object isInstanceExternal extends FunctionID diff --git a/project/Build.scala b/project/Build.scala index d6ad8df80c..a979a5d46e 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -66,6 +66,7 @@ object ExposedValues extends AutoPlugin { .withAsInstanceOfs(CheckedBehavior.Compliant) .withArrayIndexOutOfBounds(CheckedBehavior.Compliant) .withArrayStores(CheckedBehavior.Compliant) + .withNegativeArraySizes(CheckedBehavior.Compliant) .withStringIndexOutOfBounds(CheckedBehavior.Compliant) .withModuleInit(CheckedBehavior.Compliant) } @@ -177,7 +178,6 @@ object MyScalaJSPlugin extends AutoPlugin { .withModuleKind(ModuleKind.ESModule) .withSemantics { sems => sems - .withNegativeArraySizes(Unchecked) .withNullPointers(Unchecked) } } else { From 58af468534ea2373d1b85fb9f6e803c40d1e18ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 25 Jun 2024 23:15:45 +0200 Subject: [PATCH 7/8] Wasm: Implement checked nullPointers. --- .../backend/WebAssemblyLinkerBackend.scala | 5 - .../backend/wasmemitter/CoreWasmLib.scala | 43 ++- .../linker/backend/wasmemitter/Emitter.scala | 4 + .../backend/wasmemitter/FunctionEmitter.scala | 260 +++++++++++++++--- .../linker/backend/wasmemitter/VarGen.scala | 1 + .../backend/webassembly/FunctionBuilder.scala | 3 + project/Build.scala | 35 +-- 7 files changed, 273 insertions(+), 78 deletions(-) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala index b0d5c407f1..4ee5222d90 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/WebAssemblyLinkerBackend.scala @@ -36,11 +36,6 @@ final class WebAssemblyLinkerBackend(config: LinkerBackendImpl.Config) coreSpec.moduleKind == ModuleKind.ESModule, s"The WebAssembly backend only supports ES modules; was ${coreSpec.moduleKind}." ) - require( - coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked, - "The WebAssembly backend currently only supports CheckedBehavior.Unchecked semantics; " + - s"was ${coreSpec.semantics}." - ) require( coreSpec.semantics.strictFloats, "The WebAssembly backend only supports strict float semantics." 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 424444d50c..3bfe0a05cd 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 @@ -627,6 +627,10 @@ final class CoreWasmLib(coreSpec: CoreSpec) { genCheckedStringCharAt() } + if (semantics.nullPointers != CheckedBehavior.Unchecked) { + genThrowNullPointerException() + } + if (semantics.moduleInit == CheckedBehavior.Fatal) { genThrowModuleInitError() } @@ -1361,6 +1365,26 @@ final class CoreWasmLib(coreSpec: CoreSpec) { fb.buildAndAddToModule() } + /** `throwNullPointerException: void -> void`. + * + * This function always throws. It should be followed by an `unreachable` + * statement. + */ + private def genThrowNullPointerException()(implicit ctx: WasmContext): Unit = { + val typeDataType = RefType(genTypeID.typeData) + + val fb = newFunctionBuilder(genFunctionID.throwNullPointerException) + + maybeWrapInUBE(fb, semantics.nullPointers) { + genNewScalaClass(fb, NullPointerExceptionClass, NoArgConstructorName) { + } + } + fb += ExternConvertAny + fb += Throw(genTagID.exception) + + fb.buildAndAddToModule() + } + /** Generates the `arrayGet.x` functions. */ private def genArrayGets()(implicit ctx: WasmContext): Unit = { for (baseRef <- arrayBaseRefs) @@ -2248,10 +2272,21 @@ final class CoreWasmLib(coreSpec: CoreSpec) { val valueParam = fb.addParam("value", RefType.any) fb.setResultType(RefType.any) - fb += LocalGet(valueParam) - fb += Call(genFunctionID.anyGetTypeData) - fb += RefAsNonNull // NPE for null.getName() - fb += ReturnCall(genFunctionID.typeDataName) + if (semantics.nullPointers == CheckedBehavior.Unchecked) { + fb += LocalGet(valueParam) + fb += Call(genFunctionID.anyGetTypeData) + fb += RefAsNonNull // NPE for null.getName() + fb += ReturnCall(genFunctionID.typeDataName) + } else { + fb.block() { npeLabel => + fb += LocalGet(valueParam) + fb += Call(genFunctionID.anyGetTypeData) + fb += BrOnNull(npeLabel) // NPE for null.getName() + fb += ReturnCall(genFunctionID.typeDataName) + } + fb += Call(genFunctionID.throwNullPointerException) + fb += Unreachable + } fb.buildAndAddToModule() } 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 bd4fa6697a..ab80622a00 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 @@ -419,6 +419,10 @@ object Emitter { StringArgConstructorName) }, + cond(nullPointers != Unchecked) { + instantiateClass(NullPointerExceptionClass, NoArgConstructorName) + }, + cond(stringIndexOutOfBounds != Unchecked) { instantiateClass(StringIndexOutOfBoundsExceptionClass, IntArgConstructorName) 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 672bf26edd..d503596e5b 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 @@ -117,15 +117,17 @@ object FunctionEmitter { List(preSuperEnvType) ) - emitter.genBlockStats(ctorBody.beforeSuper) { - // Build and return the preSuperEnv struct - for (varDef <- preSuperDecls) { - val localID = (emitter.lookupLocal(varDef.name.name): @unchecked) match { - case VarStorage.Local(localID) => localID + emitter.returnWithNPEScope() { + emitter.genBlockStats(ctorBody.beforeSuper) { + // Build and return the preSuperEnv struct + for (varDef <- preSuperDecls) { + val localID = (emitter.lookupLocal(varDef.name.name): @unchecked) match { + case VarStorage.Local(localID) => localID + } + emitter.fb += wa.LocalGet(localID) } - emitter.fb += wa.LocalGet(localID) + emitter.fb += wa.StructNew(preSuperEnvStructTypeID) } - emitter.fb += wa.StructNew(preSuperEnvStructTypeID) } emitter.fb.buildAndAddToModule() @@ -295,6 +297,7 @@ private class FunctionEmitter private ( private val coreSpec = ctx.coreSpec import coreSpec.semantics + private var currentNPELabel: Option[wanme.LabelID] = null private var closureIdx: Int = 0 private var currentEnv: Env = paramsEnv @@ -304,6 +307,155 @@ private class FunctionEmitter private ( private def receiverStorage: VarStorage.Local = _receiverStorage.getOrElse(throw new Error("Cannot access to the receiver in this context.")) + /** Opens a new scope in which NPEs can be thrown by jumping to the NPE label. + * + * When NPEs are unchecked this is a no-op (other than calling `body`). + * + * Otherwise, this method logically generates the following code: + * + * {{{ + * block resultType $noNPELabel + * block $npeLabel + * body + * br $noNPELabel + * end + * call $throwNullPointerException + * unreachable + * end + * }}} + * + * Inside the `body`, it is therefore possible to throw an NPE by jumping to + * the `npeLabel`. This is typically done through a `br_on_null $npeLabel` + * instruction. + * + * If the `npeLabel` is not actually requested while generated `body`, the + * surrounding code is skipped. + */ + private def withNPEScope[A](resultType: List[watpe.Type])(body: => A): A = { + if (semantics.nullPointers == CheckedBehavior.Unchecked) { + body + } else { + val savedNPELabel = currentNPELabel + + currentNPELabel = None + val startIndex = fb.markCurrentInstructionIndex() + + val result = body + + for (npeLabel <- currentNPELabel) { + val noNPELabel = fb.genLabel() + + // Go back and open the two blocks + val blockType = fb.sigToBlockType(watpe.FunctionType(Nil, resultType)) + fb.insertAll( + startIndex, + List( + wa.Block(blockType, Some(noNPELabel)), + wa.Block(wa.BlockType.ValueType(), Some(npeLabel)) + ) + ) + + // Add the code after the body in the normal way + fb += wa.Br(noNPELabel) + fb += wa.End // npeLabel + fb += wa.Call(genFunctionID.throwNullPointerException) + fb += wa.Unreachable + fb += wa.End // noNPELabel + } + + currentNPELabel = savedNPELabel + result + } + } + + /** Like `withNPEScope`, but `return` for the success path. + * + * This alternative can be used instead of `withNPEScope` when the `body` + * is what gets returned from the current function. It generates better code + * for that common case, namely: + * + * {{{ + * block $npeLabel + * body + * return + * end + * call $throwNullPointerException + * unreachable + * }}} + */ + private def returnWithNPEScope[A]()(body: => A): A = { + if (semantics.nullPointers == CheckedBehavior.Unchecked) { + body + } else { + val savedNPELabel = currentNPELabel + + currentNPELabel = None + val startIndex = fb.markCurrentInstructionIndex() + + val result = body + + for (npeLabel <- currentNPELabel) { + // Go back and open the block + fb.insert(startIndex, wa.Block(wa.BlockType.ValueType(), Some(npeLabel))) + + // Add the code after the body in the normal way + fb += wa.Return + fb += wa.End // npeLabel + fb += wa.Call(genFunctionID.throwNullPointerException) + fb += wa.Unreachable + } + + currentNPELabel = savedNPELabel + result + } + } + + private def getNPELabel(): wanme.LabelID = { + assert(semantics.nullPointers != CheckedBehavior.Unchecked) + currentNPELabel.getOrElse { + val label = fb.genLabel() + currentNPELabel = Some(label) + label + } + } + + /** Emits a `ref.as_non_null` or an NPE check if required for the given `Tree`. + * + * This method does not emit `tree`. It only uses it to determine whether + * a check is required. + */ + private def genAsNonNullOrNPEFor(tree: Tree): Unit = { + if (tree.tpe.isNullable) { + if (tree.tpe == NullType) + genNPE() + else if (semantics.nullPointers != CheckedBehavior.Unchecked) + fb += wa.BrOnNull(getNPELabel()) + else + fb += wa.RefAsNonNull + } + } + + /** Emits an NPE check if required for the given `Tree`, otherwise nothing. + * + * This method does not emit `tree`. It only uses it to determine whether + * a check is required. + * + * Unlike `genAsNonNullOrNPE`, after this codegen the value on the stack is + * still statically typed as nullable at the Wasm level. + */ + private def genCheckNonNullFor(tree: Tree): Unit = { + if (tree.tpe.isNullable && semantics.nullPointers != CheckedBehavior.Unchecked) + fb += wa.BrOnNull(getNPELabel()) + } + + /** Emits an unconditional NPE. */ + private def genNPE(): Unit = { + if (semantics.nullPointers == CheckedBehavior.Unchecked) + fb += wa.Unreachable + else + fb += wa.Br(getNPELabel()) + } + private def withNewLocal[A](name: LocalName, originalName: OriginalName, tpe: watpe.Type)( body: wanme.LocalID => A ): A = { @@ -372,12 +524,18 @@ private class FunctionEmitter private ( private def markPosition(tree: Tree): Unit = markPosition(tree.pos) - def genBody(tree: Tree, expectedType: Type): Unit = - genTree(tree, expectedType) + def genBody(tree: Tree, expectedType: Type): Unit = { + returnWithNPEScope() { + genTree(tree, expectedType) + } + } def genTreeAuto(tree: Tree): Unit = genTree(tree, tree.tpe) + def genTreeToAny(tree: Tree): Unit = + genTree(tree, if (tree.tpe.isNullable) AnyType else AnyNotNullType) + def genTree(tree: Tree, expectedType: Type): Unit = { val generatedType: Type = tree match { case t: Literal => genLiteral(t, expectedType) @@ -518,8 +676,9 @@ private class FunctionEmitter private ( * point, so we can trap as NPE. */ markPosition(tree) - fb += wa.Unreachable + genNPE() } else { + genCheckNonNullFor(qualifier) genTree(rhs, lhs.tpe) markPosition(tree) fb += wa.StructSet( @@ -553,10 +712,11 @@ private class FunctionEmitter private ( case _ => false } + genCheckNonNullFor(array) + if (semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked && (semantics.arrayStores == CheckedBehavior.Unchecked || isPrimArray)) { - // Get the underlying array; implicit trap on null - markPosition(tree) + // Get the underlying array fb += wa.StructGet( genTypeID.forArrayClass(arrayTypeRef), genFieldID.objStruct.arrayUnderlying @@ -576,7 +736,7 @@ private class FunctionEmitter private ( () case NullType => markPosition(tree) - fb += wa.Unreachable + genNPE() case _ => throw new IllegalArgumentException( s"ArraySelect.array must be an array type, but has type ${array.tpe}") @@ -649,7 +809,7 @@ private class FunctionEmitter private ( case NullType => genTree(receiver, NullType) - fb += wa.Unreachable // trap + genNPE() NothingType case _ if method.name.isReflectiveProxy => @@ -707,8 +867,8 @@ private class FunctionEmitter private ( */ // Load receiver and arguments - genTree(receiver, AnyType) - fb += wa.RefAsNonNull + genTreeToAny(receiver) + genAsNonNullOrNPEFor(receiver) fb += wa.LocalTee(receiverLocalForDispatch) genArgs(args, methodName) @@ -770,7 +930,7 @@ private class FunctionEmitter private ( */ def genReceiverNotNull(): Unit = { genTreeAuto(receiver) - fb += wa.RefAsNonNull + genAsNonNullOrNPEFor(receiver) } /* Generates a resolved call to a method of a hijacked class. @@ -792,7 +952,7 @@ private class FunctionEmitter private ( */ genTreeAuto(receiver) markPosition(tree) - fb += wa.Unreachable // NPE + genNPE() } else if (!receiverClassInfo.isAncestorOfHijackedClass) { // Standard dispatch codegen genReceiverNotNull() @@ -1033,7 +1193,7 @@ private class FunctionEmitter private ( case NullType => genTree(receiver, NullType) markPosition(tree) - fb += wa.Unreachable // trap + genNPE() NothingType case _ => @@ -1048,15 +1208,15 @@ private class FunctionEmitter private ( BoxedClassToPrimType.get(targetClassName) match { case None => - genTree(receiver, ClassType(targetClassName, nullable = true)) - fb += wa.RefAsNonNull + genTree(receiver, ClassType(targetClassName, nullable = receiver.tpe.isNullable)) + genAsNonNullOrNPEFor(receiver) case Some(primReceiverType) => if (receiver.tpe == primReceiverType) { genTreeAuto(receiver) } else { - genTree(receiver, AnyType) - fb += wa.RefAsNonNull + genTreeToAny(receiver) + genAsNonNullOrNPEFor(receiver) genUnbox(primReceiverType) } } @@ -1152,8 +1312,9 @@ private class FunctionEmitter private ( * However we necessarily have a `null` receiver if we reach this point, * so we can trap as NPE. */ - fb += wa.Unreachable + genNPE() } else { + genCheckNonNullFor(qualifier) fb += wa.StructGet( genTypeID.forClass(className), genFieldID.forClassInstanceField(fieldName) @@ -1976,12 +2137,13 @@ private class FunctionEmitter private ( genTreeAuto(expr) markPosition(tree) - fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) // implicit trap on null + genCheckNonNullFor(expr) + fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) fb += wa.Call(genFunctionID.getClassOf) } else { - genTree(expr, AnyType) + genTreeToAny(expr) markPosition(tree) - fb += wa.RefAsNonNull + genAsNonNullOrNPEFor(expr) fb += wa.Call(genFunctionID.anyGetClass) } @@ -2128,7 +2290,9 @@ private class FunctionEmitter private ( if (UseLegacyExceptionsForTryCatch) { markPosition(tree) fb += wa.Try(fb.sigToBlockType(Sig(Nil, resultType))) - genTree(block, expectedType) + withNPEScope(resultType) { + genTree(block, expectedType) + } markPosition(tree) fb += wa.Catch(genTagID.exception) withNewLocal(errVarName, errVarOrigName, watpe.RefType.anyref) { exceptionLocal => @@ -2150,7 +2314,9 @@ private class FunctionEmitter private ( fb.tryTable(watpe.RefType.externref)( List(wa.CatchClause.Catch(genTagID.exception, catchLabel)) ) { - genTree(block, expectedType) + withNPEScope(resultType) { + genTree(block, expectedType) + } markPosition(tree) fb += wa.Br(doneLabel) } @@ -2325,11 +2491,11 @@ private class FunctionEmitter private ( val UnwrapFromThrowable(expr) = tree fb.block(watpe.RefType.anyref) { doneLabel => - genTree(expr, ClassType(ThrowableClass, nullable = true)) + genTreeAuto(expr) markPosition(tree) - fb += wa.RefAsNonNull + genAsNonNullOrNPEFor(expr) // if !expr.isInstanceOf[js.JavaScriptException], then br $done fb += wa.BrOnCastFail( @@ -2567,7 +2733,8 @@ private class FunctionEmitter private ( array.tpe match { case ArrayType(arrayTypeRef, _) => - // Get the underlying array; implicit trap on null + // Get the underlying array + genCheckNonNullFor(array) fb += wa.StructGet( genTypeID.forArrayClass(arrayTypeRef), genFieldID.objStruct.arrayUnderlying @@ -2580,7 +2747,7 @@ private class FunctionEmitter private ( // unreachable NothingType case NullType => - fb += wa.Unreachable + genNPE() NothingType case _ => throw new IllegalArgumentException( @@ -2660,12 +2827,12 @@ private class FunctionEmitter private ( genTreeAuto(array) - markPosition(tree) - array.tpe match { case ArrayType(arrayTypeRef, _) => + genCheckNonNullFor(array) + if (semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked) { - // Get the underlying array; implicit trap on null + // Get the underlying array fb += wa.StructGet( genTypeID.forArrayClass(arrayTypeRef), genFieldID.objStruct.arrayUnderlying @@ -2717,7 +2884,7 @@ private class FunctionEmitter private ( // unreachable NothingType case NullType => - fb += wa.Unreachable + genNPE() NothingType case _ => throw new IllegalArgumentException( @@ -2808,17 +2975,17 @@ private class FunctionEmitter private ( case NullType => genTree(expr, NullType) - fb += wa.Unreachable // trap for NPE + genNPE() NothingType case exprType => val exprLocal = addSyntheticLocal(watpe.RefType(genTypeID.ObjectStruct)) - genTree(expr, ClassType(CloneableClass, nullable = true)) + genTreeAuto(expr) markPosition(tree) - fb += wa.RefAsNonNull + genAsNonNullOrNPEFor(expr) fb += wa.LocalTee(exprLocal) fb += wa.LocalGet(exprLocal) @@ -3024,6 +3191,11 @@ private class FunctionEmitter private ( private def genTransient(tree: Transient): Type = { tree.value match { + case Transients.CheckNotNull(expr) => + genTreeAuto(expr) + genAsNonNullOrNPEFor(expr) + tree.tpe + case Transients.Cast(expr, tpe) => genCast(expr, tpe, tree.pos) @@ -3031,9 +3203,9 @@ private class FunctionEmitter private ( genSystemArrayCopy(tree, value) case Transients.ObjectClassName(obj) => - genTree(obj, AnyType) + genTreeToAny(obj) markPosition(tree) - fb += wa.RefAsNonNull // trap on NPE + genAsNonNullOrNPEFor(obj) fb += wa.Call(genFunctionID.anyGetClassName) StringType @@ -3485,7 +3657,9 @@ private class FunctionEmitter private ( fb.tryTable()(List(wa.CatchClause.CatchAllRef(catchLabel))) { // try block enterTryFinally(entry) { - genTree(tryBlock, expectedType) + withNPEScope(resultType) { + genTree(tryBlock, expectedType) + } } markPosition(tree) 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 de6494b653..f7fed865f8 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 @@ -244,6 +244,7 @@ object VarGen { case object throwArrayStoreException extends FunctionID case object throwArrayIndexOutOfBoundsException extends FunctionID case object throwNegativeArraySizeException extends FunctionID + case object throwNullPointerException extends FunctionID case object checkedStringCharAt extends FunctionID case object throwModuleInitError extends FunctionID case object isInstanceExternal extends FunctionID diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/FunctionBuilder.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/FunctionBuilder.scala index fff0a74acd..25162bc9f9 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/FunctionBuilder.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/webassembly/FunctionBuilder.scala @@ -140,6 +140,9 @@ final class FunctionBuilder( def insert(index: InstructionIndex, instr: Instr): Unit = instrs.insert(index.value, instr) + def insertAll(index: InstructionIndex, instrs: List[Instr]): Unit = + this.instrs.insertAll(index.value, instrs) + // Helpers to build structured control flow def sigToBlockType(sig: FunctionType): BlockType = sig match { diff --git a/project/Build.scala b/project/Build.scala index a979a5d46e..3d9a259fe3 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -60,27 +60,15 @@ object ExposedValues extends AutoPlugin { // set scalaJSLinkerConfig in someProject ~= makeCompliant val makeCompliant: StandardConfig => StandardConfig = { prev => - if (prev.experimentalUseWebAssembly) { - prev.withSemantics { semantics => - semantics - .withAsInstanceOfs(CheckedBehavior.Compliant) - .withArrayIndexOutOfBounds(CheckedBehavior.Compliant) - .withArrayStores(CheckedBehavior.Compliant) - .withNegativeArraySizes(CheckedBehavior.Compliant) - .withStringIndexOutOfBounds(CheckedBehavior.Compliant) - .withModuleInit(CheckedBehavior.Compliant) - } - } else { - prev.withSemantics { semantics => - semantics - .withAsInstanceOfs(CheckedBehavior.Compliant) - .withArrayIndexOutOfBounds(CheckedBehavior.Compliant) - .withArrayStores(CheckedBehavior.Compliant) - .withNegativeArraySizes(CheckedBehavior.Compliant) - .withNullPointers(CheckedBehavior.Compliant) - .withStringIndexOutOfBounds(CheckedBehavior.Compliant) - .withModuleInit(CheckedBehavior.Compliant) - } + prev.withSemantics { semantics => + semantics + .withAsInstanceOfs(CheckedBehavior.Compliant) + .withArrayIndexOutOfBounds(CheckedBehavior.Compliant) + .withArrayStores(CheckedBehavior.Compliant) + .withNegativeArraySizes(CheckedBehavior.Compliant) + .withNullPointers(CheckedBehavior.Compliant) + .withStringIndexOutOfBounds(CheckedBehavior.Compliant) + .withModuleInit(CheckedBehavior.Compliant) } } @@ -172,14 +160,9 @@ object MyScalaJSPlugin extends AutoPlugin { .withMinify(enableMinifyEverywhere.value) if (enableWasmEverywhere.value) { - import CheckedBehavior.Unchecked baseConfig .withExperimentalUseWebAssembly(true) .withModuleKind(ModuleKind.ESModule) - .withSemantics { sems => - sems - .withNullPointers(Unchecked) - } } else { baseConfig } From 906276cf772282757fb7c7720e3ec8c421d3cce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 29 Aug 2024 23:37:19 +0200 Subject: [PATCH 8/8] Wasm: Fix unboxing `null` to the primitive type `string`. This is a theoretical issue. We have no source syntax to generate an `asInstanceOf[string]`. This was discovered while auditing all the `RefAsNonNull`s in `FunctionEmitter` to implement checked NPEs. --- .../linker/backend/wasmemitter/FunctionEmitter.scala | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 d503596e5b..0a9874f32c 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 @@ -2082,7 +2082,11 @@ private class FunctionEmitter private ( fb += wa.GlobalGet(genGlobalID.undef) case StringType => - fb += wa.RefAsNonNull + val sig = watpe.FunctionType(List(watpe.RefType.anyref), List(watpe.RefType.any)) + fb.block(sig) { nonNullLabel => + fb += wa.BrOnNonNull(nonNullLabel) + fb += wa.GlobalGet(genGlobalID.emptyString) + } case CharType | LongType => // Extract the `value` field (the only field) out of the box class.