Skip to content

Wasm: Implement checked behaviors. #5002

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +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.asInstanceOfs == CheckedBehavior.Unchecked &&
coreSpec.semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked &&
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}."
)
require(
coreSpec.semantics.strictFloats,
"The WebAssembly backend only supports strict float semantics."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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)
Expand Down Expand Up @@ -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))
Expand All @@ -387,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)
}
}
Expand All @@ -408,7 +427,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),
Expand Down Expand Up @@ -522,6 +541,50 @@ class ClassEmitter(coreSpec: CoreSpec) {
fb.buildAndAddToModule()
}

/** Generate the cast function for an interface.
*
* When `asInstanceOfs` are checked, the expression `asInstanceOf[<interface>]`
* 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)
Expand Down Expand Up @@ -598,6 +661,56 @@ class ClassEmitter(coreSpec: CoreSpec) {
fb.buildAndAddToModule()
}

/** Generate the cast function for a class.
*
* When `asInstanceOfs` are checked, the expression `asInstanceOf[<class>]`
* 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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note]
We reach here if

  • (1) the objParam is JS number
  • (2) the objParam is neither JS number nor an instance of java.lang.Number

typeTest(DoubleRef) will be true for (1) because it calls the tD: (x) => typeof x === 'number'.

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)

Expand All @@ -613,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)

Expand All @@ -622,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) {
Comment on lines +741 to +748
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note]
We reach this point when the globalInstance for the class is not yet available. If semantics.moduleInit is set to Compliant, module access will have no effect because it assumes the module has already been initialized by storeModules.

If semantics.moduleInit is set to Fatal, throw an exception.

// 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)
Expand Down Expand Up @@ -685,8 +825,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 = {
Expand Down Expand Up @@ -1258,10 +1401,12 @@ 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.")
val IsInstance = UTF8String("is.")
val AsInstance = UTF8String("as.")

// Shared with JS backend -- string
val TopLevelExport = UTF8String("e.")
Expand Down
Loading