diff --git a/core/src/main/java/org/teavm/backend/wasm/WasmDebugInfoLevel.java b/core/src/main/java/org/teavm/backend/wasm/WasmDebugInfoLevel.java new file mode 100644 index 000000000..6f36b923a --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/WasmDebugInfoLevel.java @@ -0,0 +1,22 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm; + +public enum WasmDebugInfoLevel { + NONE, + DEOBFUSCATION, + FULL +} diff --git a/core/src/main/java/org/teavm/backend/wasm/WasmDebugInfoLocation.java b/core/src/main/java/org/teavm/backend/wasm/WasmDebugInfoLocation.java new file mode 100644 index 000000000..d1ea0b937 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/WasmDebugInfoLocation.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm; + +public enum WasmDebugInfoLocation { + EMBEDDED, + EXTERNAL +} diff --git a/core/src/main/java/org/teavm/backend/wasm/WasmGCTarget.java b/core/src/main/java/org/teavm/backend/wasm/WasmGCTarget.java index 32d9b66f8..47ed4c9ba 100644 --- a/core/src/main/java/org/teavm/backend/wasm/WasmGCTarget.java +++ b/core/src/main/java/org/teavm/backend/wasm/WasmGCTarget.java @@ -21,6 +21,9 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.teavm.backend.wasm.debug.DebugLines; +import org.teavm.backend.wasm.debug.ExternalDebugFile; +import org.teavm.backend.wasm.debug.GCDebugInfoBuilder; import org.teavm.backend.wasm.gc.TeaVMWasmGCHost; import org.teavm.backend.wasm.gc.WasmGCDependencies; import org.teavm.backend.wasm.generate.gc.WasmGCDeclarationsGenerator; @@ -63,6 +66,8 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost { private BoundCheckInsertion boundCheckInsertion = new BoundCheckInsertion(); private boolean strict; private boolean obfuscated; + private WasmDebugInfoLocation debugLocation; + private WasmDebugInfoLevel debugLevel; private List intrinsicFactories = new ArrayList<>(); private Map customIntrinsics = new HashMap<>(); private List customTypeMapperFactories = new ArrayList<>(); @@ -77,6 +82,14 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost { this.strict = strict; } + public void setDebugLevel(WasmDebugInfoLevel debugLevel) { + this.debugLevel = debugLevel; + } + + public void setDebugLocation(WasmDebugInfoLocation debugLocation) { + this.debugLocation = debugLocation; + } + @Override public void addIntrinsicFactory(WasmGCIntrinsicFactory intrinsicFactory) { intrinsicFactories.add(intrinsicFactory); @@ -172,6 +185,7 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost { customGeneratorFactories, customCustomGenerators, controller.getProperties()); var intrinsics = new WasmGCIntrinsics(classes, controller.getServices(), intrinsicFactories, customIntrinsics); + var debugInfoBuilder = new GCDebugInfoBuilder(); var declarationsGenerator = new WasmGCDeclarationsGenerator( module, classes, @@ -231,7 +245,7 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost { customGenerators.contributeToModule(module); adjustModuleMemory(module); - emitWasmFile(module, buildTarget, outputName); + emitWasmFile(module, buildTarget, outputName, debugInfoBuilder); } private void adjustModuleMemory(WasmModule module) { @@ -248,13 +262,22 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost { module.setMaxMemorySize(pages); } - private void emitWasmFile(WasmModule module, BuildTarget buildTarget, String outputName) throws IOException { + private void emitWasmFile(WasmModule module, BuildTarget buildTarget, String outputName, + GCDebugInfoBuilder debugInfoBuilder) throws IOException { var binaryWriter = new WasmBinaryWriter(); + DebugLines debugLines = null; + if (debugLevel != WasmDebugInfoLevel.NONE) { + debugLines = debugInfoBuilder.lines(); + } var binaryRenderer = new WasmBinaryRenderer(binaryWriter, WasmBinaryVersion.V_0x1, obfuscated, - null, null, null, null, WasmBinaryStatsCollector.EMPTY); + null, null, debugLines, null, WasmBinaryStatsCollector.EMPTY); optimizeIndexes(module); module.prepareForRendering(); - binaryRenderer.render(module); + if (debugLocation == WasmDebugInfoLocation.EMBEDDED) { + binaryRenderer.render(module, debugInfoBuilder::build); + } else { + binaryRenderer.render(module); + } var data = binaryWriter.getData(); if (!outputName.endsWith(".wasm")) { outputName += ".wasm"; @@ -262,6 +285,14 @@ public class WasmGCTarget implements TeaVMTarget, TeaVMWasmGCHost { try (var output = buildTarget.createResource(outputName)) { output.write(data); } + if (debugLocation == WasmDebugInfoLocation.EXTERNAL) { + var debugInfoData = ExternalDebugFile.write(debugInfoBuilder.build()); + if (debugInfoData != null) { + try (var output = buildTarget.createResource(outputName + ".tdbg")) { + output.write(debugInfoData); + } + } + } } private void optimizeIndexes(WasmModule module) { diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/ExternalDebugFile.java b/core/src/main/java/org/teavm/backend/wasm/debug/ExternalDebugFile.java new file mode 100644 index 000000000..7ef7b7a39 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/debug/ExternalDebugFile.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.debug; + +import java.util.List; +import org.teavm.backend.wasm.model.WasmCustomSection; +import org.teavm.backend.wasm.render.WasmBinaryWriter; + +public final class ExternalDebugFile { + private ExternalDebugFile() { + } + + public static byte[] write(List sections) { + if (sections.isEmpty()) { + return null; + } + var writer = new WasmBinaryWriter(); + writer.writeInt32(0x67626474); + writer.writeInt32(1); + for (var section : sections) { + var data = section.getData(); + writer.writeAsciiString(section.getName()); + writer.writeLEB(data.length); + writer.writeBytes(data); + } + return writer.getData(); + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/debug/GCDebugInfoBuilder.java b/core/src/main/java/org/teavm/backend/wasm/debug/GCDebugInfoBuilder.java new file mode 100644 index 000000000..c190eaf05 --- /dev/null +++ b/core/src/main/java/org/teavm/backend/wasm/debug/GCDebugInfoBuilder.java @@ -0,0 +1,80 @@ +/* + * Copyright 2024 Alexey Andreev. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.teavm.backend.wasm.debug; + +import java.util.ArrayList; +import java.util.List; +import org.teavm.backend.wasm.model.WasmCustomSection; + +public class GCDebugInfoBuilder { + private DebugStringsBuilder strings; + private DebugFilesBuilder files; + private DebugPackagesBuilder packages; + private DebugClassesBuilder classes; + private DebugMethodsBuilder methods; + private DebugLinesBuilder lines; + + public GCDebugInfoBuilder() { + strings = new DebugStringsBuilder(); + files = new DebugFilesBuilder(strings); + packages = new DebugPackagesBuilder(strings); + classes = new DebugClassesBuilder(packages, strings); + methods = new DebugMethodsBuilder(classes, strings); + lines = new DebugLinesBuilder(files, methods); + } + + public DebugStrings strings() { + return strings; + } + + public DebugFiles files() { + return files; + } + + public DebugPackages packages() { + return packages; + } + + public DebugClasses classes() { + return classes; + } + + public DebugMethods methods() { + return methods; + } + + public DebugLines lines() { + return lines; + } + + public List build() { + var result = new ArrayList(); + addSection(result, strings); + addSection(result, files); + addSection(result, packages); + addSection(result, classes); + addSection(result, methods); + addSection(result, lines); + return result; + } + + private void addSection(List sections, DebugSectionBuilder builder) { + if (builder.isEmpty()) { + return; + } + sections.add(new WasmCustomSection(builder.name(), builder.build())); + } +} diff --git a/core/src/main/java/org/teavm/backend/wasm/disasm/Disassembler.java b/core/src/main/java/org/teavm/backend/wasm/disasm/Disassembler.java index 90024f351..ea7538318 100644 --- a/core/src/main/java/org/teavm/backend/wasm/disasm/Disassembler.java +++ b/core/src/main/java/org/teavm/backend/wasm/disasm/Disassembler.java @@ -21,7 +21,17 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; import java.util.function.Consumer; +import org.teavm.backend.wasm.debug.info.LineInfo; +import org.teavm.backend.wasm.debug.parser.DebugClassParser; +import org.teavm.backend.wasm.debug.parser.DebugFileParser; +import org.teavm.backend.wasm.debug.parser.DebugLinesParser; +import org.teavm.backend.wasm.debug.parser.DebugMethodParser; +import org.teavm.backend.wasm.debug.parser.DebugPackageParser; +import org.teavm.backend.wasm.debug.parser.DebugSectionParser; +import org.teavm.backend.wasm.debug.parser.DebugStringParser; import org.teavm.backend.wasm.parser.AddressListener; import org.teavm.backend.wasm.parser.CodeSectionParser; import org.teavm.backend.wasm.parser.FunctionSectionListener; @@ -41,9 +51,27 @@ public final class Disassembler { private WasmHollowFunctionType[] functionTypes; private int[] functionTypeRefs; private int importFunctionCount; + private Map debugSectionParsers = new HashMap<>(); + private DebugLinesParser debugLines; + private LineInfo lineInfo; public Disassembler(DisassemblyWriter writer) { this.writer = writer; + installDebugParsers(); + } + + private void installDebugParsers() { + var strings = addDebugSection(new DebugStringParser()); + var files = addDebugSection(new DebugFileParser(strings)); + var packages = addDebugSection(new DebugPackageParser(strings)); + var classes = addDebugSection(new DebugClassParser(strings, packages)); + var methods = addDebugSection(new DebugMethodParser(strings, classes)); + debugLines = addDebugSection(new DebugLinesParser(files, methods)); + } + + private T addDebugSection(T section) { + debugSectionParsers.put(section.name(), section); + return section; } public void startModule() { @@ -65,18 +93,25 @@ public final class Disassembler { public void read(byte[] bytes) { var nameAccumulator = new NameAccumulatingSectionListener(); var input = new ByteArrayAsyncInputStream(bytes); - var nameParser = createNameParser(input, nameAccumulator); - input.readFully(nameParser::parse); + var preparationParser = createPreparationParser(input, nameAccumulator); + input.readFully(preparationParser::parse); + lineInfo = debugLines.getLineInfo(); input = new ByteArrayAsyncInputStream(bytes); var parser = createParser(input, nameAccumulator.buildProvider()); input.readFully(parser::parse); } - public ModuleParser createNameParser(AsyncInputStream input, NameSectionListener listener) { + public ModuleParser createPreparationParser(AsyncInputStream input, NameSectionListener listener) { return new ModuleParser(input) { @Override protected Consumer getSectionConsumer(int code, int pos, String name) { + if (code == 0) { + var debugSection = debugSectionParsers.get(name); + if (debugSection != null) { + return debugSection::parse; + } + } return Disassembler.this.getNameSectionConsumer(code, name, listener); } }; @@ -139,11 +174,14 @@ public final class Disassembler { disassembler.setFunctionTypes(functionTypes); disassembler.setFunctionTypeRefs(functionTypeRefs); writer.setAddressOffset(pos); + writer.setDebugLines(lineInfo); + writer.startSection(); writer.write("(; code section size: " + bytes.length + " ;)").eol(); var sectionParser = new CodeSectionParser(disassembler); sectionParser.setFunctionIndexOffset(importFunctionCount); sectionParser.parse(writer.addressListener, bytes); writer.flush(); + writer.setDebugLines(null); }; } else { return null; diff --git a/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyHTMLWriter.java b/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyHTMLWriter.java index 61b90628d..34d8a9a94 100644 --- a/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyHTMLWriter.java +++ b/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyHTMLWriter.java @@ -24,7 +24,11 @@ public class DisassemblyHTMLWriter extends DisassemblyWriter { @Override public DisassemblyWriter prologue() { - return writeExact("
");
+        writeExact("\n");
+        writeExact("\n");
+        return writeExact("
");
     }
 
     @Override
@@ -82,4 +86,14 @@ public class DisassemblyHTMLWriter extends DisassemblyWriter {
         writeExact(s);
         return this;
     }
+
+    @Override
+    protected void startAnnotation() {
+        writeExact("");
+    }
+
+    @Override
+    protected void endAnnotation() {
+        writeExact("");
+    }
 }
diff --git a/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyWriter.java b/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyWriter.java
index ed57a6e76..6336368c9 100644
--- a/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyWriter.java
+++ b/core/src/main/java/org/teavm/backend/wasm/disasm/DisassemblyWriter.java
@@ -16,16 +16,27 @@
 package org.teavm.backend.wasm.disasm;
 
 import java.io.PrintWriter;
+import org.teavm.backend.wasm.debug.info.LineInfo;
+import org.teavm.backend.wasm.debug.info.LineInfoCommandVisitor;
+import org.teavm.backend.wasm.debug.info.LineInfoEnterCommand;
+import org.teavm.backend.wasm.debug.info.LineInfoExitCommand;
+import org.teavm.backend.wasm.debug.info.LineInfoFileCommand;
+import org.teavm.backend.wasm.debug.info.LineInfoLineCommand;
 import org.teavm.backend.wasm.parser.AddressListener;
 
 public abstract class DisassemblyWriter {
     private PrintWriter out;
     private boolean withAddress;
     private int indentLevel;
+    private int addressWithinSection;
     private int address;
     private boolean hasAddress;
     private boolean lineStarted;
     private int addressOffset;
+    private LineInfo debugLines;
+    private int currentSequenceIndex;
+    private int currentCommandIndex = -1;
+    private int lineInfoIndent;
 
     public DisassemblyWriter(PrintWriter out) {
         this.out = out;
@@ -39,6 +50,15 @@ public abstract class DisassemblyWriter {
         this.addressOffset = addressOffset;
     }
 
+    public void setDebugLines(LineInfo debugLines) {
+        this.debugLines = debugLines;
+    }
+
+    public void startSection() {
+        addressWithinSection = -1;
+        currentSequenceIndex = 0;
+    }
+
     public DisassemblyWriter address() {
         hasAddress = true;
         return this;
@@ -63,6 +83,9 @@ public abstract class DisassemblyWriter {
     private void startLine() {
         if (!lineStarted) {
             lineStarted = true;
+            if (debugLines != null) {
+                printDebugLine();
+            }
             if (withAddress) {
                 if (hasAddress) {
                     hasAddress = false;
@@ -77,6 +100,85 @@ public abstract class DisassemblyWriter {
         }
     }
 
+    private void printDebugLine() {
+        if (currentSequenceIndex >= debugLines.sequences().size()) {
+            return;
+        }
+        var force = false;
+        if (currentCommandIndex < 0) {
+            if (addressWithinSection < debugLines.sequences().get(currentSequenceIndex).startAddress()) {
+                return;
+            }
+            currentCommandIndex = 0;
+            force = true;
+        } else {
+            if (addressWithinSection >= debugLines.sequences().get(currentSequenceIndex).endAddress()) {
+                printSingleDebugAnnotation("");
+                ++currentSequenceIndex;
+                currentCommandIndex = -1;
+                lineInfoIndent = 0;
+                return;
+            }
+        }
+
+        var sequence = debugLines.sequences().get(currentSequenceIndex);
+        if (currentCommandIndex >= sequence.commands().size()) {
+            return;
+        }
+        var command = sequence.commands().get(currentCommandIndex);
+        if (!force) {
+            if (currentCommandIndex + 1 < sequence.commands().size()
+                    && addressWithinSection >= sequence.commands().get(currentCommandIndex + 1).address()) {
+                command = sequence.commands().get(++currentCommandIndex);
+            } else {
+                return;
+            }
+        }
+
+        command.acceptVisitor(new LineInfoCommandVisitor() {
+            @Override
+            public void visit(LineInfoEnterCommand command) {
+                printSingleDebugAnnotation(" at " + command.method().fullName());
+                ++lineInfoIndent;
+            }
+
+            @Override
+            public void visit(LineInfoExitCommand command) {
+                --lineInfoIndent;
+            }
+
+            @Override
+            public void visit(LineInfoFileCommand command) {
+                if (command.file() == null) {
+                    printSingleDebugAnnotation("at :" + command.line());
+                } else {
+                    printSingleDebugAnnotation(" at " + command.file().name() + ":" + command.line());
+                }
+            }
+
+            @Override
+            public void visit(LineInfoLineCommand command) {
+                printSingleDebugAnnotation(" at " + command.line());
+            }
+        });
+    }
+
+    private void printSingleDebugAnnotation(String text) {
+        out.print("                ");
+        for (int i = 0; i < indentLevel; ++i) {
+            out.print("  ");
+        }
+        for (int i = 0; i < lineInfoIndent; ++i) {
+            out.print("  ");
+        }
+        startAnnotation();
+        out.print("(;");
+        write(text);
+        out.print(" ;)");
+        endAnnotation();
+        out.print("\n");
+    }
+
     private void printAddress() {
         out.print("(; ");
         for (int i = 7; i >= 0; --i) {
@@ -108,15 +210,20 @@ public abstract class DisassemblyWriter {
 
     public abstract DisassemblyWriter epilogue();
 
+    protected void startAnnotation() {
+    }
+
+    protected void endAnnotation() {
+    }
+
     public void flush() {
         out.flush();
     }
 
-
-
     public final AddressListener addressListener = new AddressListener() {
         @Override
         public void address(int address) {
+            addressWithinSection = address;
             DisassemblyWriter.this.address = address + addressOffset;
         }
     };
diff --git a/tools/junit/src/main/java/org/teavm/junit/WebAssemblyGCPlatformSupport.java b/tools/junit/src/main/java/org/teavm/junit/WebAssemblyGCPlatformSupport.java
index 6fb04b7aa..87e20c6a1 100644
--- a/tools/junit/src/main/java/org/teavm/junit/WebAssemblyGCPlatformSupport.java
+++ b/tools/junit/src/main/java/org/teavm/junit/WebAssemblyGCPlatformSupport.java
@@ -32,6 +32,8 @@ import java.util.Map;
 import java.util.StringTokenizer;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
+import org.teavm.backend.wasm.WasmDebugInfoLevel;
+import org.teavm.backend.wasm.WasmDebugInfoLocation;
 import org.teavm.backend.wasm.WasmGCTarget;
 import org.teavm.backend.wasm.disasm.Disassembler;
 import org.teavm.backend.wasm.disasm.DisassemblyHTMLWriter;
@@ -69,6 +71,8 @@ class WebAssemblyGCPlatformSupport extends TestPlatformSupport {
             var target = new WasmGCTarget();
             target.setObfuscated(false);
             target.setStrict(true);
+            target.setDebugLevel(WasmDebugInfoLevel.DEOBFUSCATION);
+            target.setDebugLocation(WasmDebugInfoLocation.EMBEDDED);
             var sourceDirs = System.getProperty(SOURCE_DIRS);
             if (sourceDirs != null) {
                 var dirs = new ArrayList();