Skip to content
This repository has been archived by the owner on Jul 12, 2024. It is now read-only.

Commit

Permalink
Generate source maps.
Browse files Browse the repository at this point in the history
It turns out that source maps are supported for WebAssembly.
Positions in the generated code are interpreted as

* a single line, and
* columns correspond to offsets in the `.wasm` file.

We reuse the Scala.js facility to create source maps per se. We feed
it the right data from the binary writer.

To be consistent with how the Scala.js linker works when emitting
JavaScript, we define a subclass of `WasmBinaryWriter` that handles
source maps in addition of the regular output.

In our model, we store positions as fake `WasmInstr` every time the
position (potentially) changes. This is in contrast with the
JavaScript backend, which stores a `pos` in every `js.Tree`.
There are typically several Wasm instructions that encode any
particular `ir.Trees.Tree` (and therefore correspond to one
position), so this is probably a better trade-off.
  • Loading branch information
sjrd committed Apr 20, 2024
1 parent 9eb1a99 commit f8c3631
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 61 deletions.
37 changes: 33 additions & 4 deletions wasm/src/main/scala/WebAssemblyLinkerBackend.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import org.scalajs.ir.Names._
import org.scalajs.logging.Logger

import org.scalajs.linker._
import org.scalajs.linker.backend.javascript.SourceMapWriter
import org.scalajs.linker.interface._
import org.scalajs.linker.interface.unstable._
import org.scalajs.linker.standard._

import org.scalajs.linker.backend.webassembly.SourceMapWriterAccess

import wasm.ir2wasm._
import wasm.ir2wasm.SpecialNames._
import wasm.wasm4s._
Expand Down Expand Up @@ -111,6 +114,7 @@ final class WebAssemblyLinkerBackend(

val watFileName = s"$moduleID.wat"
val wasmFileName = s"$moduleID.wasm"
val sourceMapFileName = s"$wasmFileName.map"
val jsFileName = OutputPatternsImpl.jsFile(linkerConfig.outputPatterns, moduleID)
val loaderJSFileName = OutputPatternsImpl.jsFile(linkerConfig.outputPatterns, "__loader")

Expand All @@ -119,9 +123,12 @@ final class WebAssemblyLinkerBackend(
loaderJSFileName,
jsFileName
)
val filesToProduce =
if (linkerConfig.prettyPrint) filesToProduce0 + watFileName
val filesToProduce1 =
if (linkerConfig.sourceMap) filesToProduce0 + sourceMapFileName
else filesToProduce0
val filesToProduce =
if (linkerConfig.prettyPrint) filesToProduce1 + watFileName
else filesToProduce1

def maybeWriteWatFile(): Future[Unit] = {
if (linkerConfig.prettyPrint) {
Expand All @@ -135,8 +142,30 @@ final class WebAssemblyLinkerBackend(

def writeWasmFile(): Future[Unit] = {
val emitDebugInfo = !linkerConfig.minify
val binaryOutput = new converters.WasmBinaryWriter(wasmModule, emitDebugInfo).write()
outputImpl.writeFull(wasmFileName, ByteBuffer.wrap(binaryOutput))

if (linkerConfig.sourceMap) {
val sourceMapWriter = new SourceMapWriterAccess.ByteArrayWriterBox()

val wasmFileURI = s"./$wasmFileName"
val sourceMapURI = s"./$sourceMapFileName"

val smWriter =
sourceMapWriter.createSourceMapWriter(wasmFileURI, linkerConfig.relativizeSourceMapBase)
val binaryOutput = new converters.WasmBinaryWriter.WithSourceMap(
wasmModule,
emitDebugInfo,
smWriter,
sourceMapURI
).write()
smWriter.complete()

outputImpl.writeFull(wasmFileName, ByteBuffer.wrap(binaryOutput)).flatMap { _ =>
outputImpl.writeFull(sourceMapFileName, sourceMapWriter.toByteBuffer())
}
} else {
val binaryOutput = new converters.WasmBinaryWriter(wasmModule, emitDebugInfo).write()
outputImpl.writeFull(wasmFileName, ByteBuffer.wrap(binaryOutput))
}
}

def writeLoaderFile(): Future[Unit] =
Expand Down
92 changes: 70 additions & 22 deletions wasm/src/main/scala/converters/WasmBinaryWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import scala.annotation.tailrec
import java.io.OutputStream
import java.io.ByteArrayOutputStream

import org.scalajs.ir.Position
import org.scalajs.linker.backend.javascript.SourceMapWriter

import wasm.wasm4s._
import wasm.wasm4s.Names._
import wasm.wasm4s.Types._
import wasm.wasm4s.WasmInstr.{BlockType, END}

final class WasmBinaryWriter(module: WasmModule, emitDebugInfo: Boolean) {
class WasmBinaryWriter(module: WasmModule, emitDebugInfo: Boolean) {
import WasmBinaryWriter._

private val typeIdxValues: Map[WasmTypeName, Int] =
Expand Down Expand Up @@ -56,6 +59,11 @@ final class WasmBinaryWriter(module: WasmModule, emitDebugInfo: Boolean) {
finally localIdxValues = saved
}

protected def emitStartFuncPosition(buf: Buffer, pos: Position): Unit = ()
protected def emitPosition(buf: Buffer, pos: Position): Unit = ()
protected def emitEndFuncPosition(buf: Buffer): Unit = ()
protected def emitSourceMapSection(buf: Buffer): Unit = ()

def write(): Array[Byte] = {
val fullOutput = new Buffer()

Expand Down Expand Up @@ -88,6 +96,8 @@ final class WasmBinaryWriter(module: WasmModule, emitDebugInfo: Boolean) {
if (emitDebugInfo)
writeCustomSection(fullOutput, "name")(writeNameCustomSection(_))

emitSourceMapSection(fullOutput)

fullOutput.result()
}

Expand All @@ -96,7 +106,7 @@ final class WasmBinaryWriter(module: WasmModule, emitDebugInfo: Boolean) {
fullOutput.byteLengthSubSection(f)
}

private def writeCustomSection(fullOutput: Buffer, customSectionName: String)(
protected final def writeCustomSection(fullOutput: Buffer, customSectionName: String)(
f: Buffer => Unit
): Unit = {
writeSection(fullOutput, SectionCustom) { buf =>
Expand Down Expand Up @@ -254,6 +264,8 @@ final class WasmBinaryWriter(module: WasmModule, emitDebugInfo: Boolean) {
}

private def writeFunc(buf: Buffer, func: WasmFunction): Unit = {
emitStartFuncPosition(buf, func.pos)

buf.vec(func.locals.filter(!_.isParameter)) { local =>
buf.u32(1)
writeType(buf, local.typ)
Expand All @@ -262,6 +274,8 @@ final class WasmBinaryWriter(module: WasmModule, emitDebugInfo: Boolean) {
withLocalIdxValues(func.locals.map(_.name).zipWithIndex.toMap) {
writeExpr(buf, func.body)
}

emitEndFuncPosition(buf)
}

private def writeType(buf: Buffer, typ: WasmStorageType): Unit = {
Expand Down Expand Up @@ -327,28 +341,34 @@ final class WasmBinaryWriter(module: WasmModule, emitDebugInfo: Boolean) {
}

private def writeInstr(buf: Buffer, instr: WasmInstr): Unit = {
val opcode = instr.opcode
if (opcode <= 0xFF) {
buf.byte(opcode.toByte)
} else {
assert(
opcode <= 0xFFFF,
s"cannot encode an opcode longer than 2 bytes yet: ${opcode.toHexString}"
)
buf.byte((opcode >>> 8).toByte)
buf.byte(opcode.toByte)
}

writeInstrImmediates(buf, instr)

instr match {
case instr: WasmInstr.StructuredLabeledInstr =>
// We must register even the `None` labels, because they contribute to relative numbering
labelsInScope ::= instr.label
case END =>
labelsInScope = labelsInScope.tail
case WasmInstr.PositionMark(pos) =>
emitPosition(buf, pos)

case _ =>
()
val opcode = instr.opcode
if (opcode <= 0xFF) {
buf.byte(opcode.toByte)
} else {
assert(
opcode <= 0xFFFF,
s"cannot encode an opcode longer than 2 bytes yet: ${opcode.toHexString}"
)
buf.byte((opcode >>> 8).toByte)
buf.byte(opcode.toByte)
}

writeInstrImmediates(buf, instr)

instr match {
case instr: WasmInstr.StructuredLabeledInstr =>
// We must register even the `None` labels, because they contribute to relative numbering
labelsInScope ::= instr.label
case END =>
labelsInScope = labelsInScope.tail
case _ =>
()
}
}
}

Expand Down Expand Up @@ -429,6 +449,9 @@ final class WasmBinaryWriter(module: WasmModule, emitDebugInfo: Boolean) {
writeBrOnCast(labelIdx, from, to)
case BR_ON_CAST_FAIL(labelIdx, from, to) =>
writeBrOnCast(labelIdx, from, to)

case PositionMark(pos) =>
throw new AssertionError(s"Unexpected $instr")
}
}

Expand Down Expand Up @@ -595,4 +618,29 @@ object WasmBinaryWriter {
}
}
}

final class WithSourceMap(
module: WasmModule,
emitDebugInfo: Boolean,
sourceMapWriter: SourceMapWriter,
sourceMapURI: String
) extends WasmBinaryWriter(module, emitDebugInfo) {

override protected def emitStartFuncPosition(buf: Buffer, pos: Position): Unit =
sourceMapWriter.startNode(buf.currentGlobalOffset, pos)

override protected def emitPosition(buf: Buffer, pos: Position): Unit = {
sourceMapWriter.endNode(buf.currentGlobalOffset)
sourceMapWriter.startNode(buf.currentGlobalOffset, pos)
}

override protected def emitEndFuncPosition(buf: Buffer): Unit =
sourceMapWriter.endNode(buf.currentGlobalOffset)

override protected def emitSourceMapSection(buf: Buffer): Unit = {
writeCustomSection(buf, "sourceMappingURL") { buf =>
buf.name(sourceMapURI)
}
}
}
}
38 changes: 24 additions & 14 deletions wasm/src/main/scala/converters/WasmTextWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -329,23 +329,30 @@ class WasmTextWriter {

private def writeInstr(instr: WasmInstr)(implicit b: WatBuilder): Unit = {
instr match {
case END | ELSE | _: CATCH | CATCH_ALL => b.deindent()
case _ => ()
}
b.newLine()
b.appendElement(instr.mnemonic)
instr match {
case instr: StructuredLabeledInstr =>
instr.label.foreach(writeLabelIdx(_))
case _ =>
case WasmInstr.PositionMark(_) =>
// ignore
()
}

writeInstrImmediates(instr)
case _ =>
instr match {
case END | ELSE | _: CATCH | CATCH_ALL => b.deindent()
case _ => ()
}
b.newLine()
b.appendElement(instr.mnemonic)
instr match {
case instr: StructuredLabeledInstr =>
instr.label.foreach(writeLabelIdx(_))
case _ =>
()
}

instr match {
case _: StructuredLabeledInstr | ELSE | _: CATCH | CATCH_ALL => b.indent()
case _ => ()
writeInstrImmediates(instr)

instr match {
case _: StructuredLabeledInstr | ELSE | _: CATCH | CATCH_ALL => b.indent()
case _ => ()
}
}
}

Expand Down Expand Up @@ -419,6 +426,9 @@ class WasmTextWriter {
writeLabelIdx(labelIdx)
writeType(from)
writeType(to)

case PositionMark(pos) =>
throw new AssertionError(s"Unexpected $instr")
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion wasm/src/main/scala/ir2wasm/HelperFunctions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package wasm.ir2wasm
import org.scalajs.ir.{Trees => IRTrees}
import org.scalajs.ir.{Types => IRTypes}
import org.scalajs.ir.{Names => IRNames}
import org.scalajs.ir.{ClassKind, Position}
import org.scalajs.linker.standard.LinkedClass
import org.scalajs.ir.ClassKind

import wasm.wasm4s._
import wasm.wasm4s.WasmContext._
Expand All @@ -17,6 +17,7 @@ import EmbeddedConstants._
import TypeTransformer._

object HelperFunctions {
private implicit val noPos: Position = Position.NoPosition

def genGlobalHelpers()(implicit ctx: WasmContext): Unit = {
genStringLiteral()
Expand Down
10 changes: 10 additions & 0 deletions wasm/src/main/scala/ir2wasm/WasmBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,8 @@ class WasmBuilder(coreSpec: CoreSpec) {
}

private def genLoadModuleFunc(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = {
implicit val pos = clazz.pos

assert(clazz.kind == ClassKind.ModuleClass)
val ctor = clazz.methods
.find(_.methodName.isConstructor)
Expand Down Expand Up @@ -934,6 +936,8 @@ class WasmBuilder(coreSpec: CoreSpec) {
}

private def genLoadJSClassFunction(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = {
implicit val pos = clazz.pos

val cachedJSClassGlobal = WasmGlobal(
WasmGlobalName.forJSClassValue(clazz.className),
WasmRefType.anyref,
Expand Down Expand Up @@ -964,6 +968,8 @@ class WasmBuilder(coreSpec: CoreSpec) {
}

private def genLoadJSModuleFunction(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = {
implicit val pos = clazz.pos

val className = clazz.className
val cacheGlobalName = WasmGlobalName.forModuleInstance(className)

Expand Down Expand Up @@ -1005,6 +1011,8 @@ class WasmBuilder(coreSpec: CoreSpec) {
private def transformTopLevelMethodExportDef(
exportDef: IRTrees.TopLevelMethodExportDef
)(implicit ctx: WasmContext): Unit = {
implicit val pos = exportDef.pos

val method = exportDef.methodDef
val exportedName = exportDef.topLevelExportName

Expand Down Expand Up @@ -1070,6 +1078,8 @@ class WasmBuilder(coreSpec: CoreSpec) {
clazz: LinkedClass,
method: IRTrees.MethodDef
)(implicit ctx: WasmContext): WasmFunction = {
implicit val pos = method.pos

val functionName = Names.WasmFunctionName(
method.flags.namespace,
clazz.name.name,
Expand Down
Loading

0 comments on commit f8c3631

Please sign in to comment.