diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..c1772feff --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +runtime/src/test/resources/example.smap eol=lf \ No newline at end of file diff --git a/build-time-compiler/src/main/java/com/dylibso/chicory/build/time/compiler/Config.java b/build-time-compiler/src/main/java/com/dylibso/chicory/build/time/compiler/Config.java index db8c4cf12..07a4039e4 100644 --- a/build-time-compiler/src/main/java/com/dylibso/chicory/build/time/compiler/Config.java +++ b/build-time-compiler/src/main/java/com/dylibso/chicory/build/time/compiler/Config.java @@ -1,6 +1,7 @@ package com.dylibso.chicory.build.time.compiler; import com.dylibso.chicory.compiler.InterpreterFallback; +import com.dylibso.chicory.runtime.DebugParser; import java.nio.file.Path; import java.util.Set; import java.util.StringJoiner; @@ -41,6 +42,11 @@ public final class Config { */ private final Set interpretedFunctions; + /** + * the debug parser to use to extract debug symbols from the wasm module. + */ + private final DebugParser debugParser; + private Config( Path wasmFile, String name, @@ -48,7 +54,8 @@ private Config( Path targetSourceFolder, Path targetWasmFolder, InterpreterFallback interpreterFallback, - Set interpretedFunctions) { + Set interpretedFunctions, + DebugParser debugParser) { this.wasmFile = wasmFile; this.name = name; this.targetClassFolder = targetClassFolder; @@ -56,6 +63,7 @@ private Config( this.targetWasmFolder = targetWasmFolder; this.interpreterFallback = interpreterFallback; this.interpretedFunctions = interpretedFunctions; + this.debugParser = debugParser; } public Path wasmFile() { @@ -90,6 +98,10 @@ public static Builder builder() { return new Builder(); } + public DebugParser debugParser() { + return debugParser; + } + @SuppressWarnings("StringSplitter") public String getPackageName() { var split = name.split("\\."); @@ -114,6 +126,7 @@ public static final class Builder { private Path targetWasmFolder; private InterpreterFallback interpreterFallback = InterpreterFallback.FAIL; private Set interpretedFunctions; + private DebugParser debugParser; private Builder() {} @@ -152,6 +165,11 @@ public Builder withInterpretedFunctions(Set interpretedFunctions) { return this; } + public Builder withDebugParser(DebugParser debugParser) { + this.debugParser = debugParser; + return this; + } + public Config build() { return new Config( wasmFile, @@ -160,7 +178,8 @@ public Config build() { targetSourceFolder, targetWasmFolder, interpreterFallback, - interpretedFunctions); + interpretedFunctions, + debugParser); } } } diff --git a/build-time-compiler/src/main/java/com/dylibso/chicory/build/time/compiler/Generator.java b/build-time-compiler/src/main/java/com/dylibso/chicory/build/time/compiler/Generator.java index a46ddf1ff..566d268e3 100644 --- a/build-time-compiler/src/main/java/com/dylibso/chicory/build/time/compiler/Generator.java +++ b/build-time-compiler/src/main/java/com/dylibso/chicory/build/time/compiler/Generator.java @@ -8,12 +8,14 @@ import com.dylibso.chicory.compiler.internal.Compiler; import com.dylibso.chicory.runtime.Instance; import com.dylibso.chicory.runtime.Machine; +import com.dylibso.chicory.runtime.Stratum; import com.dylibso.chicory.wasm.Parser; import com.dylibso.chicory.wasm.WasmModule; import com.dylibso.chicory.wasm.WasmWriter; import com.dylibso.chicory.wasm.types.OpCode; import com.dylibso.chicory.wasm.types.RawSection; import com.dylibso.chicory.wasm.types.SectionId; +import com.dylibso.chicory.wasm.types.UnknownCustomSection; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.Modifier; import com.github.javaparser.ast.NodeList; @@ -36,6 +38,7 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; @@ -55,6 +58,7 @@ public Set generateResources() throws IOException { var compiler = Compiler.builder(module) .withClassName(machineName) + .withDebugParser(config.debugParser()) .withInterpreterFallback(config.interpreterFallback()) .withInterpretedFunctions(config.interpretedFunctions()) .build(); @@ -70,6 +74,12 @@ public Set generateResources() throws IOException { Files.write(targetFile, entry.getValue()); } + for (Map.Entry entry : result.extraResources().entrySet()) { + var resourceName = entry.getKey(); + var targetFile = config.targetWasmFolder().resolve(resourceName); + Files.write(targetFile, entry.getValue()); + } + return result.interpretedFunctions(); } @@ -117,15 +127,34 @@ public void generateMetaWasm(Set interpretedFunctions) throws IOExcepti var out = new ByteArrayOutputStream(); var importFuncs = module.importSection().importCount(); int count = module.codeSection().functionBodyCount(); + writeVarUInt32(out, count); + + var inputStratum = Stratum.builder("WASM").build(); + if (config.debugParser() != null) { + inputStratum = config.debugParser().apply(module); + } + final var outputStratum = Stratum.builder("WASM"); + var actual = readVarUInt32(source); assert count == actual; for (int i = 0; i < count; i++) { var funcId = importFuncs + i; if (interpretedFunctions.contains(funcId)) { + var bodySize = (int) readVarUInt32(source); + + if (!inputStratum.isEmpty()) { + var newFunctionAddress = out.size(); + remapFunctionStratum( + module, + funcId, + inputStratum, + outputStratum, + newFunctionAddress, + bodySize); + } // Copy over the original function body from the source - var bodySize = (int) readVarUInt32(source); writeVarUInt32(out, bodySize); var bodyBytes = new byte[bodySize]; source.get(bodyBytes); @@ -151,6 +180,14 @@ public void generateMetaWasm(Set interpretedFunctions) throws IOExcepti } } writer.writeSection(SectionId.CODE, out.toByteArray()); + if (!outputStratum.isEmpty()) { + var smap = Stratum.toSMapString("interpreted.wasm", outputStratum); + writer.writeSection( + UnknownCustomSection.builder() + .withName("SMAP") + .withBytes(smap.getBytes(StandardCharsets.UTF_8)) + .build()); + } } else if (section.sectionId() != SectionId.CUSTOM) { writer.writeSection((RawSection) section); } @@ -164,6 +201,64 @@ public void generateMetaWasm(Set interpretedFunctions) throws IOExcepti Files.write(newWasmFile, writer.bytes()); } + private static void remapFunctionStratum( + WasmModule module, + int funcId, + Stratum inputStratum, + Stratum.Builder outputStratum, + int newFunctionAddress, + int functionSize) { + String debugFunctionName = null; + Stratum.Line lastLine = null; + var lastLineAddress = -1; + var originalFunctionAddress = -1; + + var instructions = module.codeSection().getFunctionBody(funcId).instructions(); + for (var ins : instructions) { + var relativeAddress = ins.address() - module.codeSection().address(); + + if (originalFunctionAddress == -1) { + originalFunctionAddress = relativeAddress; + } + + if (debugFunctionName == null) { + debugFunctionName = inputStratum.getFunctionMapping(relativeAddress); + outputStratum.withFunctionMapping( + debugFunctionName, newFunctionAddress, newFunctionAddress + functionSize); + } + + var line = inputStratum.getInputLine(relativeAddress); + if (line != null) { + // did we get a new line mapping? + if (lastLine != null && !line.equals(lastLine)) { + var newAddress = lastLineAddress - originalFunctionAddress + newFunctionAddress; + var instructionSize = relativeAddress - lastLineAddress; + outputStratum.withLineMapping( + lastLine.fileName(), + lastLine.filePath(), + lastLine.line(), + lastLine.count(), + newAddress, + instructionSize); + } + lastLine = line; + lastLineAddress = relativeAddress; + } + } + + if (lastLine != null) { + var newAddress = lastLineAddress - originalFunctionAddress + newFunctionAddress; + var instructionSize = 1; + outputStratum.withLineMapping( + lastLine.fileName(), + lastLine.filePath(), + lastLine.line(), + lastLine.count(), + newAddress, + instructionSize); + } + } + private static void createFolders(Path filesFolder, String[] split) throws IOException { for (int i = 0; i < (split.length - 1); i++) { filesFolder = filesFolder.resolve(split[i]); diff --git a/compiler-maven-plugin/pom.xml b/compiler-maven-plugin/pom.xml index 9a875bc30..6b2bcf230 100644 --- a/compiler-maven-plugin/pom.xml +++ b/compiler-maven-plugin/pom.xml @@ -22,6 +22,10 @@ com.dylibso.chicory compiler + + com.dylibso.chicory + runtime + org.apache.maven maven-core diff --git a/compiler-maven-plugin/src/main/java/com/dylibso/chicory/build/time/maven/ChicoryCompilerGenMojo.java b/compiler-maven-plugin/src/main/java/com/dylibso/chicory/build/time/maven/ChicoryCompilerGenMojo.java index 723be8f1c..3e2cec0c9 100644 --- a/compiler-maven-plugin/src/main/java/com/dylibso/chicory/build/time/maven/ChicoryCompilerGenMojo.java +++ b/compiler-maven-plugin/src/main/java/com/dylibso/chicory/build/time/maven/ChicoryCompilerGenMojo.java @@ -3,8 +3,11 @@ import com.dylibso.chicory.build.time.compiler.Config; import com.dylibso.chicory.build.time.compiler.Generator; import com.dylibso.chicory.compiler.InterpreterFallback; +import com.dylibso.chicory.runtime.DebugParser; import java.io.File; import java.io.IOException; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; import java.util.Set; import org.apache.maven.model.Resource; import org.apache.maven.plugin.AbstractMojo; @@ -76,6 +79,19 @@ public class ChicoryCompilerGenMojo extends AbstractMojo { @Override public void execute() throws MojoExecutionException { + + DebugParser debugParser = null; + // Use the DebugParser if it's on the classpath. + try { + for (var service : ServiceLoader.load(DebugParser.class)) { + debugParser = service; + getLog().info("Using debug parser: " + debugParser.getClass().getName()); + break; + } + } catch (ServiceConfigurationError ignore) { + debugParser = null; + } + getLog().info("Generating AOT classes for " + name + " from " + wasmFile); var config = @@ -87,6 +103,7 @@ public void execute() throws MojoExecutionException { .withTargetWasmFolder(targetWasmFolder.toPath()) .withInterpreterFallback(interpreterFallback) .withInterpretedFunctions(interpretedFunctions) + .withDebugParser(debugParser) .build(); var generator = new Generator(config); diff --git a/compiler-tests/src/test/java/com/dylibso/chicory/testing/MethodTooLargeTest.java b/compiler-tests/src/test/java/com/dylibso/chicory/testing/MethodTooLargeTest.java index 8fc975a2a..4961b00be 100644 --- a/compiler-tests/src/test/java/com/dylibso/chicory/testing/MethodTooLargeTest.java +++ b/compiler-tests/src/test/java/com/dylibso/chicory/testing/MethodTooLargeTest.java @@ -20,6 +20,7 @@ import org.objectweb.asm.util.TraceClassVisitor; public class MethodTooLargeTest { + public static final String CLASS_NAME = "com.dylibso.chicory.$gen.CompiledMachine"; @Test public void testBigFunc() { @@ -27,7 +28,7 @@ public void testBigFunc() { byte[] wasm = Wat2Wasm.parse(wat); var module = Parser.parse(wasm); - var result = Compiler.builder(module).build().compile(); + var result = Compiler.builder(module).withClassName(CLASS_NAME).build().compile(); // We only verify that the resulting class contains the fallback to interpreter verifyClass(result.classBytes(), true); diff --git a/compiler-tests/src/test/resources/com/dylibso/chicory/testing/MethodTooLargeTest.testBigFunc.approved.txt b/compiler-tests/src/test/resources/com/dylibso/chicory/testing/MethodTooLargeTest.testBigFunc.approved.txt index 9d155aa8a..295c69be4 100644 --- a/compiler-tests/src/test/resources/com/dylibso/chicory/testing/MethodTooLargeTest.testBigFunc.approved.txt +++ b/compiler-tests/src/test/resources/com/dylibso/chicory/testing/MethodTooLargeTest.testBigFunc.approved.txt @@ -3,7 +3,14 @@ private final Lcom/dylibso/chicory/runtime/Instance; instance private final Lcom/dylibso/chicory/runtime/internal/CompilerInterpreterMachine; compilerInterpreterMachine } + public final static INNERCLASS com/dylibso/chicory/runtime/Stratum$Line com/dylibso/chicory/runtime/Stratum Line + public final static INNERCLASS java/lang/invoke/MethodHandles$Lookup java/lang/invoke/MethodHandles Lookup + private final static Z memCopyWorkaround + + private static [Lcom/dylibso/chicory/runtime/Stratum; STRATA_BY_FUNC_GROUP + + private static Ljava/lang/String; funcGroupClassPrefix } final class com/dylibso/chicory/$gen/CompiledMachineFuncGroup_0 { diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/MachineFactoryCompiler.java b/compiler/src/main/java/com/dylibso/chicory/compiler/MachineFactoryCompiler.java index bb801dc16..d1cfe62ac 100644 --- a/compiler/src/main/java/com/dylibso/chicory/compiler/MachineFactoryCompiler.java +++ b/compiler/src/main/java/com/dylibso/chicory/compiler/MachineFactoryCompiler.java @@ -1,6 +1,7 @@ package com.dylibso.chicory.compiler; import com.dylibso.chicory.compiler.internal.MachineFactory; +import com.dylibso.chicory.runtime.DebugParser; import com.dylibso.chicory.runtime.Instance; import com.dylibso.chicory.runtime.Machine; import com.dylibso.chicory.wasm.WasmModule; @@ -25,19 +26,20 @@ private MachineFactoryCompiler() { * *

* Every instance created by the builder will pay the cost of compiling the module. - *

* * @see #compile(WasmModule) If you want to compile the module only once for multiple instances. */ public static Machine compile(Instance instance) { - return compile(instance.module()).apply(instance); + return builder(instance.module()) + .withDebugParser(instance.debugParser()) + .compile() + .apply(instance); } /** * Compiles a machine factory that can used in instance builders. * The module is only compiled once and the machine factory is reused for every * instance created by the builder. - *

*

      * var module  = Parser.parse(is);
      * var builder = Instance.builder(module)
@@ -55,7 +57,6 @@ public static Function compile(WasmModule module) {
      * The builder allows you to configure the compiler options used to compile the module to
      * byte code.
      * This should be used when you want to create multiple instances of the same module.
-     * 

*

      * var module  = Parser.parse(is);
      * var builder = Instance.builder(module)
@@ -101,6 +102,11 @@ public Builder withInterpretedFunctions(Set interpretedFunctions) {
             return this;
         }
 
+        public Builder withDebugParser(DebugParser debugParser) {
+            compilerBuilder.withDebugParser(debugParser);
+            return this;
+        }
+
         public Function compile() {
             var result = compilerBuilder.build().compile();
             return new MachineFactory(module, result.machineFactory());
diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Compiler.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Compiler.java
index ea3b98789..4c473c872 100644
--- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Compiler.java
+++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Compiler.java
@@ -29,6 +29,7 @@
 import static com.dylibso.chicory.compiler.internal.ShadedRefs.CALL_INDIRECT;
 import static com.dylibso.chicory.compiler.internal.ShadedRefs.CALL_INDIRECT_ON_INTERPRETER;
 import static com.dylibso.chicory.compiler.internal.ShadedRefs.CHECK_INTERRUPTION;
+import static com.dylibso.chicory.compiler.internal.ShadedRefs.INIT;
 import static com.dylibso.chicory.compiler.internal.ShadedRefs.INSTANCE_MEMORY;
 import static com.dylibso.chicory.compiler.internal.ShadedRefs.INSTANCE_TABLE;
 import static com.dylibso.chicory.compiler.internal.ShadedRefs.TABLE_INSTANCE;
@@ -45,6 +46,7 @@
 import static java.lang.invoke.MethodType.methodType;
 import static java.util.Objects.requireNonNull;
 import static java.util.Objects.requireNonNullElse;
+import static java.util.Objects.requireNonNullElseGet;
 import static java.util.stream.Collectors.toSet;
 import static org.objectweb.asm.Type.INT_TYPE;
 import static org.objectweb.asm.Type.LONG_TYPE;
@@ -56,9 +58,11 @@
 import static org.objectweb.asm.commons.InstructionAdapter.OBJECT_TYPE;
 
 import com.dylibso.chicory.compiler.InterpreterFallback;
+import com.dylibso.chicory.runtime.DebugParser;
 import com.dylibso.chicory.runtime.Instance;
 import com.dylibso.chicory.runtime.Machine;
 import com.dylibso.chicory.runtime.Memory;
+import com.dylibso.chicory.runtime.Stratum;
 import com.dylibso.chicory.runtime.WasmException;
 import com.dylibso.chicory.runtime.internal.CompilerInterpreterMachine;
 import com.dylibso.chicory.wasm.ChicoryException;
@@ -77,6 +81,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import org.objectweb.asm.ClassReader;
@@ -92,7 +97,8 @@
 
 public final class Compiler {
 
-    public static final String DEFAULT_CLASS_NAME = "com.dylibso.chicory.$gen.CompiledMachine";
+    public static final String DEFAULT_CLASS_NAME = "com.dylibso.chicory.$gen%d.CompiledMachine";
+    public static final AtomicInteger CLASS_COUNTER = new AtomicInteger(0);
     private static final Type LONG_ARRAY_TYPE = Type.getType(long[].class);
     private static final Type INT_ARRAY_TYPE = Type.getType(int[].class);
     private static final Type AOT_INTERPRETER_MACHINE_TYPE =
@@ -119,19 +125,42 @@ public final class Compiler {
     private final InterpreterFallback interpreterFallback;
     private final List functionTypes;
     private final Map extraClasses = new LinkedHashMap<>();
+    private final Map extraResources = new LinkedHashMap<>();
     private int maxFunctionsPerClass;
     private final HashSet interpretedFunctions;
 
+    private final DebugContext debugContext;
+    private int funcGroupCount;
+
+    // Debug information
+    static final class DebugContext {
+        final Stratum inputStratum;
+        final Stratum.Builder outputStratum = Stratum.builder("WASM");
+        int nextOutputLineNo = 1;
+
+        DebugContext(Stratum stratum) {
+            this.inputStratum = requireNonNull(stratum, "stratum");
+        }
+
+        void reset() {
+            outputStratum.clear();
+            nextOutputLineNo = 1;
+        }
+    }
+
     private Compiler(
             WasmModule module,
             String className,
             int maxFunctionsPerClass,
             InterpreterFallback interpreterFallback,
-            Set interpretedFunctions) {
+            Set interpretedFunctions,
+            Stratum stratum) {
         this.className = requireNonNull(className, "className");
         this.module = requireNonNull(module, "module");
         this.analyzer = new WasmAnalyzer(module);
         this.functionImports = module.importSection().count(ExternalType.FUNCTION);
+        this.debugContext =
+                new DebugContext(requireNonNullElseGet(stratum, () -> Stratum.builder("").build()));
 
         if (interpretedFunctions == null || interpretedFunctions.isEmpty()) {
             this.interpretedFunctions = new HashSet<>();
@@ -163,6 +192,7 @@ public static final class Builder {
         private int maxFunctionsPerClass;
         private InterpreterFallback interpreterFallback;
         private Set interpretedFunctions;
+        private DebugParser debugParser;
 
         private Builder(WasmModule module) {
             this.module = module;
@@ -173,6 +203,11 @@ public Builder withClassName(String className) {
             return this;
         }
 
+        public Builder withDebugParser(DebugParser debugParser) {
+            this.debugParser = debugParser;
+            return this;
+        }
+
         public Builder withMaxFunctionsPerClass(int maxFunctionsPerClass) {
             this.maxFunctionsPerClass = maxFunctionsPerClass;
             return this;
@@ -191,30 +226,41 @@ public Builder withInterpretedFunctions(Set interpretedFunctions) {
         public Compiler build() {
             var className = this.className;
             if (className == null) {
-                className = DEFAULT_CLASS_NAME;
+                className = String.format(DEFAULT_CLASS_NAME, CLASS_COUNTER.getAndIncrement());
             }
 
             int maxFunctionsPerClass = this.maxFunctionsPerClass;
             if (maxFunctionsPerClass <= 0) {
                 maxFunctionsPerClass = DEFAULT_MAX_FUNCTIONS_PER_CLASS;
             }
+
+            Stratum stratum = null;
+            if (debugParser != null) {
+                stratum = debugParser.apply(module);
+            }
+
             return new Compiler(
                     module,
                     className,
                     maxFunctionsPerClass,
                     interpreterFallback,
-                    interpretedFunctions);
+                    interpretedFunctions,
+                    stratum);
         }
     }
 
     public CompilerResult compile() {
+        for (var entry : extraResources.entrySet()) {
+            classLoader.addResource(entry.getKey(), entry.getValue());
+        }
         var bytes = compileClass();
         var factory = createMachineFactory(bytes);
 
         Map classBytes = new LinkedHashMap<>();
         classBytes.put(className, bytes);
         classBytes.putAll(extraClasses);
-        return new CompilerResult(factory, classBytes, Set.copyOf(interpretedFunctions));
+        return new CompilerResult(
+                factory, classBytes, Set.copyOf(interpretedFunctions), extraResources);
     }
 
     private Function createMachineFactory(byte[] classBytes) {
@@ -287,18 +333,24 @@ private void compileExtraClasses() {
         //
         var originalMaxFunctionsPerClass = maxFunctionsPerClass;
         while (true) {
+            funcGroupCount = 0;
             try {
                 maxFunctionsPerClass =
                         loadChunkedClass(
                                 totalFunctions,
                                 maxFunctionsPerClass,
-                                (start, end, chunkSize) -> {
+                                (start, end, chunkSize, extraResources) -> {
+                                    funcGroupCount++;
                                     maxFunctionsPerClass = chunkSize;
                                     String className = classNameForFuncGroup(start);
                                     return compileExtraClass(
                                             className,
                                             emitFunctionGroup(
-                                                    start, end, internalClassName(this.className)));
+                                                    start,
+                                                    end,
+                                                    internalClassName(this.className),
+                                                    extraResources,
+                                                    className));
                                 });
                 break;
             } catch (MethodTooLargeException e) {
@@ -346,7 +398,7 @@ private void compileExtraClasses() {
     }
 
     interface ChunkedClassEmitter {
-        byte[] emit(int start, int end, int chunkSize);
+        byte[] emit(int start, int end, int chunkSize, Map extraResources);
     }
 
     /**
@@ -364,6 +416,8 @@ interface ChunkedClassEmitter {
     int loadChunkedClass(int size, int chunkSize, ChunkedClassEmitter emitter) {
         ArrayList generated = new ArrayList();
         WasmClassLoader classLoader = new WasmClassLoader();
+        Map extraResources = new LinkedHashMap<>();
+
         while (true) {
             try {
                 int chunks = (size / chunkSize) + (size % chunkSize == 0 ? 0 : 1);
@@ -371,7 +425,7 @@ int loadChunkedClass(int size, int chunkSize, ChunkedClassEmitter emitter) {
                     var start = i * chunkSize;
                     var end = min(start + chunkSize, size);
 
-                    byte[] bytes = emitter.emit(start, end, chunkSize);
+                    byte[] bytes = emitter.emit(start, end, chunkSize, extraResources);
                     loadClass(classLoader, bytes);
                     generated.add(bytes);
                 }
@@ -382,11 +436,13 @@ int loadChunkedClass(int size, int chunkSize, ChunkedClassEmitter emitter) {
                     throw e;
                 }
                 generated.clear();
+                extraResources.clear();
                 classLoader = new WasmClassLoader();
             }
         }
         for (var bytes : generated) {
             loadExtraClass(bytes);
+            this.extraResources.putAll(extraResources);
         }
         return chunkSize;
     }
@@ -395,8 +451,14 @@ private String classNameForFuncGroup(int funcId) {
         return "FuncGroup_" + (funcId / maxFunctionsPerClass);
     }
 
-    private Consumer emitFunctionGroup(int start, int end, String internalClassName) {
+    private Consumer emitFunctionGroup(
+            int start,
+            int end,
+            String internalClassName,
+            Map extraResources,
+            String funcGroupClassName) {
         return (classWriter) -> {
+            debugContext.reset();
             for (int i = start; i < end; i++) {
                 FunctionBody body = null;
                 try {
@@ -437,6 +499,18 @@ private Consumer emitFunctionGroup(int start, int end, String inte
                     throw handleMethodTooLarge(e, module);
                 }
             }
+            if (!debugContext.outputStratum.isEmpty()) {
+                // add the source map entries to the class
+                var baseName = internalClassName(className + funcGroupClassName);
+                var sourceFile = baseName + ".java";
+                String smap = Stratum.toSMapString(sourceFile, debugContext.outputStratum);
+                extraResources.put(
+                        baseName + ".smap", smap.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+
+                if (smap.length() < 65535) {
+                    classWriter.visitSource(internalClassName, smap);
+                }
+            }
         };
     }
 
@@ -472,6 +546,27 @@ private byte[] compileClass() {
                     null);
         }
 
+        // if we have debug info
+        if (!debugContext.inputStratum.isEmpty()) {
+
+            // emit a: public static final String[] SMAPS = new String[]{ FuncGroup_0.SMAP,
+            // FuncGroup_1.SMAP, ... };
+            classWriter.visitField(
+                    Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_STATIC,
+                    "SMAPS",
+                    getDescriptor(String[].class),
+                    null,
+                    null);
+
+            // static initializer for SMAPS field
+            emitFunction(
+                    classWriter,
+                    "",
+                    methodType(void.class),
+                    true,
+                    asm -> compileStaticInitializer(asm, internalClassName));
+        }
+
         // constructor
         emitFunction(
                 classWriter,
@@ -575,6 +670,23 @@ private static void emitCallSuper(InstructionAdapter asm) {
                 OBJECT_TYPE.getInternalName(), "", getMethodDescriptor(VOID_TYPE), false);
     }
 
+    private void compileStaticInitializer(InstructionAdapter asm, String internalClassName) {
+        asm.iconst(funcGroupCount);
+        asm.visitTypeInsn(Opcodes.ANEWARRAY, getInternalName(Class.class));
+
+        for (int i = 0; i < funcGroupCount; i++) {
+            asm.dup();
+            asm.iconst(i);
+            String className = internalClassName + classNameForFuncGroup(i * maxFunctionsPerClass);
+            asm.aconst(Type.getObjectType(className));
+            asm.astore(OBJECT_TYPE);
+        }
+
+        asm.aconst(internalClassName.replace("/", ".") + "FuncGroup_");
+        emitInvokeStatic(asm, INIT);
+        asm.areturn(VOID_TYPE);
+    }
+
     private void compileConstructor(InstructionAdapter asm, String internalClassName) {
         emitCallSuper(asm);
 
@@ -720,7 +832,7 @@ private byte[] compileMachineCallClass() {
                     loadChunkedClass(
                             functionTypes.size(),
                             maxMachineCallMethods,
-                            (start, end, chunkSize) ->
+                            (start, end, chunkSize, extraResources) ->
                                     compileExtraClass(
                                             classNameForDispatch(start),
                                             (cw) ->
@@ -983,7 +1095,7 @@ private void compileCallIndirect(
             loadChunkedClass(
                     functionTypes.size(),
                     maxMachineCallMethods,
-                    (start, end, chunkSize) ->
+                    (start, end, chunkSize, extraResources) ->
                             compileExtraClass(
                                     classNameForCallIndirect(typeId, start),
                                     (cw) -> {
@@ -1187,7 +1299,7 @@ private void compileFunction(
                         type,
                         body);
 
-        List instructions = analyzer.analyze(funcId);
+        List instructions = analyzer.analyze(funcId, debugContext);
 
         int localsCount = type.params().size();
         if (hasTooManyParameters(type)) {
@@ -1225,6 +1337,7 @@ private void compileFunction(
 
         // compile the function body
         for (CompilerInstruction ins : instructions) {
+
             switch (ins.opcode()) {
                 case LABEL:
                     Label label = labels.get(ins.operand(0));
@@ -1232,6 +1345,12 @@ private void compileFunction(
                         asm.mark(label);
                         visitedTargets.add(ins.operand(0));
                     }
+
+                    break;
+                case LINE_NUMBER:
+                    int line = (int) ins.operand(0);
+                    label = labels.get(ins.operand(1));
+                    asm.visitLineNumber(line, label);
                     break;
                 case GOTO:
                     if (visitedTargets.contains(ins.operand(0))) {
diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerInstruction.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerInstruction.java
index e8a62e331..456bc003b 100644
--- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerInstruction.java
+++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerInstruction.java
@@ -53,6 +53,8 @@ public long[] labelTargets() {
             case SWITCH:
             case TRY_CATCH_BLOCK:
                 return operands;
+            case LINE_NUMBER:
+                return new long[] {operands[1]};
             default:
                 return EMPTY;
         }
diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerOpCode.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerOpCode.java
index 265d07b0f..abc3aa9a8 100644
--- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerOpCode.java
+++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerOpCode.java
@@ -7,6 +7,7 @@
 
 enum CompilerOpCode {
     LABEL,
+    LINE_NUMBER,
     DROP_KEEP,
     TRAP,
     GOTO,
diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerResult.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerResult.java
index 9288eecfd..bb9dc2224 100644
--- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerResult.java
+++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/CompilerResult.java
@@ -11,14 +11,17 @@ public final class CompilerResult {
     private final Function machineFactory;
     private final Map classBytes;
     private final Set interpretedFunctions;
+    private final Map extraResources;
 
     public CompilerResult(
             Function machineFactory,
             Map classBytes,
-            Set interpretedFunctions) {
+            Set interpretedFunctions,
+            Map extraResources) {
         this.machineFactory = machineFactory;
         this.classBytes = classBytes;
         this.interpretedFunctions = interpretedFunctions;
+        this.extraResources = extraResources;
     }
 
     public Function machineFactory() {
@@ -32,4 +35,8 @@ public Map classBytes() {
     public Set interpretedFunctions() {
         return interpretedFunctions;
     }
+
+    public Map extraResources() {
+        return extraResources;
+    }
 }
diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Shaded.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Shaded.java
index 50414d82a..35819c242 100644
--- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Shaded.java
+++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Shaded.java
@@ -8,12 +8,15 @@
 import com.dylibso.chicory.runtime.MemCopyWorkaround;
 import com.dylibso.chicory.runtime.Memory;
 import com.dylibso.chicory.runtime.OpcodeImpl;
+import com.dylibso.chicory.runtime.Stratum;
 import com.dylibso.chicory.runtime.TrapException;
 import com.dylibso.chicory.runtime.WasmException;
 import com.dylibso.chicory.runtime.WasmRuntimeException;
 import com.dylibso.chicory.wasm.ChicoryException;
 import com.dylibso.chicory.wasm.InvalidException;
 import com.dylibso.chicory.wasm.types.FunctionType;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 
 /**
  * This class will get shaded into the compiled code.
@@ -173,23 +176,33 @@ public static int getAddr(int base, int offset) {
     }
 
     public static RuntimeException throwCallStackExhausted(StackOverflowError e) {
-        throw new ChicoryException("call stack exhausted", e);
+        ChicoryException error = new ChicoryException("call stack exhausted", e);
+        enhanceStackTrace(error);
+        throw error;
     }
 
     public static RuntimeException throwIndirectCallTypeMismatch() {
-        return new ChicoryException("indirect call type mismatch");
+        ChicoryException error = new ChicoryException("indirect call type mismatch");
+        enhanceStackTrace(error);
+        return error;
     }
 
     public static RuntimeException throwOutOfBoundsMemoryAccess() {
-        throw new WasmRuntimeException("out of bounds memory access");
+        WasmRuntimeException error = new WasmRuntimeException("out of bounds memory access");
+        enhanceStackTrace(error);
+        throw error;
     }
 
     public static RuntimeException throwTrapException() {
-        throw new TrapException("Trapped on unreachable instruction");
+        TrapException error = new TrapException("Trapped on unreachable instruction");
+        enhanceStackTrace(error);
+        throw error;
     }
 
     public static RuntimeException throwUnknownFunction(int index) {
-        throw new InvalidException(String.format("unknown function %d", index));
+        InvalidException error = new InvalidException(String.format("unknown function %d", index));
+        enhanceStackTrace(error);
+        throw error;
     }
 
     public static void checkInterruption() {
@@ -229,4 +242,63 @@ public static boolean exceptionMatches(WasmException exception, int tag, Instanc
                 && currentCatchTag.type().typesMatch(exceptionTag.type())
                 && currentCatchTag.type().returnsMatch(exceptionTag.type());
     }
+
+    private static Stratum[] STRATA_BY_FUNC_GROUP;
+    private static String funcGroupClassPrefix;
+
+    public static void init(Class[] classes, String funcGroupClassPrefix) {
+        Shaded.funcGroupClassPrefix = funcGroupClassPrefix;
+        STRATA_BY_FUNC_GROUP = new Stratum[classes.length];
+
+        for (int i = 0; i < classes.length; i++) {
+            var clazz = classes[i];
+            String resource = "/" + clazz.getName().replace('.', '/') + ".smap";
+            try (var is = clazz.getResourceAsStream(resource)) {
+                if (is == null) {
+                    throw new ChicoryException(
+                            "Class " + clazz.getName() + " does not have a .smap resource");
+                }
+                var smap = new String(is.readAllBytes(), StandardCharsets.UTF_8);
+                STRATA_BY_FUNC_GROUP[i] = Stratum.parseSMapString(smap);
+            } catch (IOException e) {
+                throw new ChicoryException(
+                        "Class " + clazz.getName() + " does not have a static initStrata method");
+            }
+        }
+    }
+
+    private static void enhanceStackTrace(Throwable e) {
+        if (STRATA_BY_FUNC_GROUP == null) {
+            return;
+        }
+
+        var elements = e.getStackTrace();
+        for (int i = 0; i < elements.length; i++) {
+            var element = elements[i];
+            if (element.getClassName().startsWith(funcGroupClassPrefix)) {
+
+                String suffix = element.getClassName().substring(funcGroupClassPrefix.length());
+                var group = Integer.parseInt(suffix);
+                if (group < STRATA_BY_FUNC_GROUP.length) {
+                    var stratum = STRATA_BY_FUNC_GROUP[group];
+                    var lineMapping = stratum.getInputLine(element.getLineNumber());
+                    if (lineMapping != null) {
+
+                        var path = lineMapping.filePath();
+                        String functionName = stratum.getFunctionMapping(element.getLineNumber());
+                        if (functionName == null) {
+                            functionName = element.getMethodName();
+                        }
+                        elements[i] =
+                                new StackTraceElement(
+                                        element.getClassName(),
+                                        functionName,
+                                        path,
+                                        (int) lineMapping.line());
+                    }
+                }
+            }
+        }
+        e.setStackTrace(elements);
+    }
 }
diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/ShadedRefs.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/ShadedRefs.java
index 3c5fb0f75..f5980bfaf 100644
--- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/ShadedRefs.java
+++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/ShadedRefs.java
@@ -53,6 +53,7 @@ public final class ShadedRefs {
     static final Method THROW_TRAP_EXCEPTION;
     static final Method THROW_UNKNOWN_FUNCTION;
     static final Method AOT_INTERPRETER_MACHINE_CALL;
+    static final Method INIT;
 
     // Exception handling methods
     static final Method CREATE_WASM_EXCEPTION;
@@ -176,6 +177,7 @@ public final class ShadedRefs {
                     Shaded.class.getMethod(
                             "exceptionMatches", WasmException.class, int.class, Instance.class);
 
+            INIT = Shaded.class.getMethod("init", Class[].class, String.class);
         } catch (NoSuchMethodException e) {
             throw new AssertionError(e);
         }
diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmAnalyzer.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmAnalyzer.java
index 6db6848cb..920c78520 100644
--- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmAnalyzer.java
+++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmAnalyzer.java
@@ -6,6 +6,7 @@
 import static java.util.stream.Collectors.toCollection;
 import static java.util.stream.Collectors.toUnmodifiableList;
 
+import com.dylibso.chicory.runtime.Stratum;
 import com.dylibso.chicory.wasm.ChicoryException;
 import com.dylibso.chicory.wasm.WasmModule;
 import com.dylibso.chicory.wasm.types.AnnotatedInstruction;
@@ -82,7 +83,7 @@ public TryCatchBlock(
     }
 
     @SuppressWarnings("checkstyle:modifiedcontrolvariable")
-    public List analyze(int funcId) {
+    public List analyze(int funcId, Compiler.DebugContext debugContext) {
         var functionType = functionTypes.get(funcId);
         var body = module.codeSection().getFunctionBody(funcId - functionImports);
         var stack = new TypeStack();
@@ -139,13 +140,45 @@ public List analyze(int funcId) {
         // implicit block for the function
         stack.enterScope(FUNCTION_SCOPE, FunctionType.of(List.of(), functionType.returns()));
 
+        var codeSectionAddress = module.codeSection().address();
+
         int exitBlockDepth = -1;
+        Stratum.Line lastLineMapping = null;
+        int startLineNo = -1;
+        String debugFunctionName = null;
+
         for (int idx = 0; idx < body.instructions().size(); idx++) {
             AnnotatedInstruction ins = body.instructions().get(idx);
 
-            if (labels.contains(idx)) {
+            // get the sourceMapIndex for the current instruction:
+            var lineMapping =
+                    debugContext.inputStratum.getInputLine(ins.address() - codeSectionAddress);
+            if (lineMapping != null && lineMapping != lastLineMapping) {
+                if (debugFunctionName == null) {
+                    debugFunctionName =
+                            debugContext.inputStratum.getFunctionMapping(
+                                    ins.address() - codeSectionAddress);
+                }
+
+                var outputLineNo = debugContext.nextOutputLineNo++;
+                if (startLineNo < 0) {
+                    startLineNo = outputLineNo;
+                }
+
+                String file = lineMapping.fileName();
+                String path = lineMapping.filePath();
+                debugContext.outputStratum.withLineMapping(
+                        file, path, lineMapping.line(), lineMapping.count(), outputLineNo, 1);
+
+                labels.add(idx);
+                result.add(new CompilerInstruction(CompilerOpCode.LABEL, idx));
+                result.add(
+                        new CompilerInstruction(
+                                CompilerOpCode.LINE_NUMBER, new long[] {outputLineNo, idx}));
+            } else if (labels.contains(idx)) {
                 result.add(new CompilerInstruction(CompilerOpCode.LABEL, idx));
             }
+            lastLineMapping = lineMapping;
 
             // skip instructions after unconditional control transfer
             if (exitBlockDepth >= 0) {
@@ -369,6 +402,18 @@ public List analyze(int funcId) {
         for (var type : reversed(functionType.returns())) {
             stack.pop(type);
         }
+
+        if (debugFunctionName != null && startLineNo >= 0) {
+            var endLineNo = debugContext.nextOutputLineNo++;
+            debugContext.outputStratum.withFunctionMapping(
+                    debugFunctionName, startLineNo, endLineNo);
+
+            var idx = body.instructions().size();
+            result.add(new CompilerInstruction(CompilerOpCode.LABEL, idx));
+            result.add(
+                    new CompilerInstruction(
+                            CompilerOpCode.LINE_NUMBER, new long[] {endLineNo, idx}));
+        }
         result.add(new CompilerInstruction(CompilerOpCode.RETURN, ids(functionType.returns())));
 
         stack.verifyEmpty();
diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmClassLoader.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmClassLoader.java
index 831974486..492d183e1 100644
--- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmClassLoader.java
+++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmClassLoader.java
@@ -1,15 +1,34 @@
 package com.dylibso.chicory.compiler.internal;
 
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
 import org.objectweb.asm.ClassReader;
 
 final class WasmClassLoader extends ClassLoader {
 
+    private final Map resources = new HashMap<>();
+
     public WasmClassLoader() {
         super(WasmClassLoader.class.getClassLoader());
     }
 
+    public void addResource(String name, byte[] data) {
+        resources.put(name, data);
+    }
+
     public Class loadFromBytes(byte[] bytes) {
         var name = new ClassReader(bytes).getClassName().replace('/', '.');
         return defineClass(name, bytes, 0, bytes.length);
     }
+
+    @Override
+    public InputStream getResourceAsStream(String name) {
+        byte[] data = resources.get(name);
+        if (data != null) {
+            return new ByteArrayInputStream(data);
+        }
+        return super.getResourceAsStream(name);
+    }
 }
diff --git a/compiler/src/test/java/com/dylibso/chicory/approvals/ApprovalTest.java b/compiler/src/test/java/com/dylibso/chicory/approvals/ApprovalTest.java
index 376a3aa45..ffa9a81b5 100644
--- a/compiler/src/test/java/com/dylibso/chicory/approvals/ApprovalTest.java
+++ b/compiler/src/test/java/com/dylibso/chicory/approvals/ApprovalTest.java
@@ -27,6 +27,7 @@
 
 // To approve everything use the env var: `APPROVAL_TESTS_USE_REPORTER=AutoApproveReporter`
 public class ApprovalTest {
+    public static final String CLASS_NAME = "com.dylibso.chicory.$gen.CompiledMachine";
 
     @Test
     public void verifyBranching() {
@@ -107,7 +108,12 @@ public void verifyExceptions() {
     public void functions10() {
         var module =
                 parse(getSystemClassLoader().getResourceAsStream("compiled/functions_10.wat.wasm"));
-        var result = Compiler.builder(module).withMaxFunctionsPerClass(5).build().compile();
+        var result =
+                Compiler.builder(module)
+                        .withClassName(CLASS_NAME)
+                        .withMaxFunctionsPerClass(5)
+                        .build()
+                        .compile();
         verifyClass(result.classBytes(), ApprovalTest::SKIP_Methods_CLASS);
     }
 
@@ -118,7 +124,7 @@ private static void verifyGeneratedBytecode(String name) {
     private static void verifyGeneratedBytecode(
             String name, Function classSkipper) {
         var module = parse(getSystemClassLoader().getResourceAsStream("compiled/" + name));
-        var result = Compiler.builder(module).build().compile();
+        var result = Compiler.builder(module).withClassName(CLASS_NAME).build().compile();
         verifyClass(result.classBytes(), classSkipper);
     }
 
diff --git a/compiler/src/test/java/com/dylibso/chicory/compiler/internal/InterruptionTest.java b/compiler/src/test/java/com/dylibso/chicory/compiler/internal/InterruptionTest.java
index a1169a50b..ed137094c 100644
--- a/compiler/src/test/java/com/dylibso/chicory/compiler/internal/InterruptionTest.java
+++ b/compiler/src/test/java/com/dylibso/chicory/compiler/internal/InterruptionTest.java
@@ -16,6 +16,8 @@
 import org.junit.jupiter.api.Test;
 
 public class InterruptionTest {
+    public static final String CLASS_NAME = "com.dylibso.chicory.$gen.CompiledMachine";
+
     @Test
     public void shouldInterruptLoop() throws InterruptedException {
         var module =
@@ -24,7 +26,10 @@ public void shouldInterruptLoop() throws InterruptedException {
                                 "/compiled/infinite-loop.c.wasm"));
         var instance =
                 Instance.builder(module)
-                        .withMachineFactory(MachineFactoryCompiler::compile)
+                        .withMachineFactory(
+                                MachineFactoryCompiler.builder(module)
+                                        .withClassName(CLASS_NAME)
+                                        .compile())
                         .build();
 
         var function = instance.export("run");
@@ -37,7 +42,10 @@ public void shouldInterruptCall() throws InterruptedException {
                 Parser.parse(InterruptionTest.class.getResourceAsStream("/compiled/power.c.wasm"));
         var instance =
                 Instance.builder(module)
-                        .withMachineFactory(MachineFactoryCompiler::compile)
+                        .withMachineFactory(
+                                MachineFactoryCompiler.builder(module)
+                                        .withClassName(CLASS_NAME)
+                                        .compile())
                         .build();
         var function = instance.export("run");
         assertInterruption(() -> function.apply(100), functionIdx(module, "run"));
@@ -85,7 +93,7 @@ private static void waitForWasmExecution(Thread thread, int funcIdx)
             for (StackTraceElement element : thread.getStackTrace()) {
                 var className = element.getClassName();
                 var methodName = element.getMethodName();
-                if (className.startsWith(Compiler.DEFAULT_CLASS_NAME + "FuncGroup_")
+                if (className.contains("CompiledMachineFuncGroup_")
                         && methodName.equals(methodNameForFunc(funcIdx))) {
                     return;
                 }
diff --git a/docs/docs/experimental/dwarf-debug-symbols.md.off b/docs/docs/experimental/dwarf-debug-symbols.md.off
new file mode 100644
index 000000000..62b833dda
--- /dev/null
+++ b/docs/docs/experimental/dwarf-debug-symbols.md.off
@@ -0,0 +1,157 @@
+---
+sidebar_position: 6
+sidebar_label: Dwarf Debug Symbols
+title: Dwarf Debug Symbols
+---
+
+# Dwarf Debug Symbols
+
+> Experimental – subject to change in future releases.
+
+Chicory can now read **DWARF** debug symbols embedded in a WebAssembly module and use them to enrich Java stack traces
+with the *original* source-level file names and line numbers that produced the trap.  This is extremely valuable when
+you are running modules generated from languages such as **Rust** or **Go** and need to identify exactly
+where a failure occurred.
+
+Without debug symbols you would normally see a stack trace that only references the Chicory Java interpreter implementation:
+
+```text
+com.dylibso.chicory.runtime.TrapException: Trapped on unreachable instruction
+	at com.dylibso.chicory.runtime.InterpreterMachine.THROW_UNREACHABLE(InterpreterMachine.java:2212)
+	at com.dylibso.chicory.runtime.InterpreterMachine.eval(InterpreterMachine.java:182)
+	at com.dylibso.chicory.runtime.InterpreterMachine.call(InterpreterMachine.java:100)
+	at com.dylibso.chicory.runtime.InterpreterMachine.CALL(InterpreterMachine.java:1715)
+    … more frames trimmed …
+```
+
+With DWARF support enabled the same failure is reported with source context from your WebAssembly module:
+
+```text
+com.dylibso.chicory.runtime.TrapException: Trapped on unreachable instruction
+	at 0x006721: chicory interpreter.rust_panic_with_hook(library/std/src/sys/pal/wasm/../unsupported/common.rs:28)
+	at 0x005cc6: chicory interpreter.{closure#0}(library/std/src/panicking.rs:699)
+	at 0x005c00: chicory interpreter.__rust_end_short_backtrace(library/std/src/sys/backtrace.rs:168)
+	at 0x00627d: chicory interpreter.begin_panic_handler(library/std/src/panicking.rs:697)
+	at 0x007e74: chicory interpreter.panic_nounwind_fmt(library/core/src/panicking.rs:117)
+	at 0x007ec8: chicory interpreter.panic_nounwind(library/core/src/panicking.rs:218)
+    … more frames trimmed …
+```
+
+The same applies when you compile the WASM module with the Chicory compiler – method names now resolve to their **Rust/Go** equivalents and link back to the original line number:
+
+```text
+com.dylibso.chicory.runtime.TrapException: Trapped on unreachable instruction
+	at com.dylibso.chicory.$gen.CompiledMachineShaded.throwTrapException(Shaded.java:195)
+	at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.rust_panic_with_hook(library/std/src/sys/pal/wasm/../unsupported/common.rs:28)
+	at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.{closure#0}(library/std/src/panicking.rs:699)
+	at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.__rust_end_short_backtrace(library/std/src/sys/backtrace.rs:168)
+	at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.begin_panic_handler(library/std/src/panicking.rs:697)
+	at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.panic_nounwind_fmt(library/core/src/panicking.rs:117)
+	at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.panic_nounwind(library/core/src/panicking.rs:218)
+    … more frames trimmed …
+
+```
+
+# Limitations
+
+* **Experimental** – APIs and behaviour may change without notice.
+* WASM has NOT standardized on Dwarf for debug symbols.  This may not work for all WASM modules.
+* The WebAssembly module must be compiled *with* debug info.
+* Performance impact is negligible at runtime, but there is a one-off cost during module loading while the DWARF sections are parsed.
+
+# Getting Started
+
+## 1. Add the Maven dependency
+
+```xml
+
+  com.dylibso.chicory
+  dwarf-rust-experimental
+
+```
+
+## 2. Enable the parser
+
+### Interpreter
+
+Usage with the interpreter:
+
+
+
+
+```java title="Interpreter setup"
+import com.dylibso.chicory.dwarf.rust.DebugParser;
+import com.dylibso.chicory.runtime.Instance;
+import com.dylibso.chicory.wasm.WasmModule;
+
+var module = Parser.parse(new File("your.wasm"));
+Instance instance = Instance.builder(module)
+        .withDebugParser(DebugParser::parse)
+        .build();
+```
+
+### Runtime Compiler
+
+Usage with the runtime compiler:
+
+```java title="AOT compiler setup"
+import com.dylibso.chicory.dwarf.rust.DebugParser;
+import com.dylibso.chicory.compiler.MachineFactoryCompiler;
+import com.dylibso.chicory.runtime.Instance;
+import com.dylibso.chicory.wasm.WasmModule;
+
+var module = Parser.parse(new File("your.wasm"));
+Instance instance = Instance.builder(module)
+        .withMachineFactory(
+                MachineFactoryCompiler.builder(module)
+                        .withDebugParser(DebugParser::parse)
+                        .compile())
+        .build();
+```
+
+> Note: The `withDebugParser` hook accepts any implementation of `DebugParser`, so you can plug in your own debug symbol parser if desired.
+
+### Build Time Compiler
+
+To enable the Debug parser in the build time compiler, add
+the `dwarf-rust-experimental` maven module as a dependency of
+the `chicory-compiler-maven-plugin`.
+
+```xml
+  
+    
+      
+        com.dylibso.chicory
+        chicory-compiler-maven-plugin
+        
+          
+            com.dylibso.chicory
+            dwarf-rust-experimental
+            ${project.version}
+          
+        
+        ...
+      
+    
+  
+```
+
+# Feedback
+
+We would love to hear how this feature works for you and which languages/tool-chains you need supported next.  Please file issues or start a discussion on GitHub.
+
+
diff --git a/docs/tests/approvals/docs-experimental-dwarf-debug-symbols.md.approved.txt b/docs/tests/approvals/docs-experimental-dwarf-debug-symbols.md.approved.txt
new file mode 100644
index 000000000..c6cac6926
--- /dev/null
+++ b/docs/tests/approvals/docs-experimental-dwarf-debug-symbols.md.approved.txt
@@ -0,0 +1 @@
+empty
diff --git a/docs/tests/index.test.ts b/docs/tests/index.test.ts
index 5611d920a..b3f0b06c5 100644
--- a/docs/tests/index.test.ts
+++ b/docs/tests/index.test.ts
@@ -26,6 +26,7 @@ describe("ApprovalTests", () => {
   });
 
   it.each(markdownFiles)('test %s', (f) => {
+    console.log("Testing", f);
     const jbangExec = jbang.exec(f);
     expect(jbangExec.code).toBe(0);
     if (jbangExec.stderr.toLowerCase().includes("error")) {
diff --git a/dwarf-rust-experimental/Dockerfile b/dwarf-rust-experimental/Dockerfile
new file mode 100644
index 000000000..486f65b01
--- /dev/null
+++ b/dwarf-rust-experimental/Dockerfile
@@ -0,0 +1,17 @@
+FROM debian:bookworm-slim
+
+# Use Docker's automatic build args for architecture and OS
+ARG TARGETARCH
+ARG TARGETOS
+
+RUN apt-get update && apt-get install -y \
+    build-essential \
+    curl \
+    && rm -rf /var/lib/apt/lists/*
+
+# Install Rust
+ENV RUSTUP_HOME=/usr/local/rustup
+ENV CARGO_HOME=/usr/local/cargo
+RUN curl -sSf https://sh.rustup.rs | sh -s -- -y
+ENV PATH=/usr/local/cargo/bin:$PATH
+RUN rustup target add wasm32-wasip1;
diff --git a/dwarf-rust-experimental/build.sh b/dwarf-rust-experimental/build.sh
new file mode 100755
index 000000000..e39e94f1f
--- /dev/null
+++ b/dwarf-rust-experimental/build.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+
+# Usage:
+#   ./build.sh [command]
+#
+# Commands:
+#   builder      Build the Docker image used for building the project
+#   bash       Start a bash shell in the build container
+#   default    Run the build inside the Docker container (default action)
+#   container  (internal) Run the build logic inside the container
+#   (no arg)   Same as 'default', runs the build in the container
+#
+# This script builds the Rust WASM project using a Docker container for a reproducible environment.
+#
+set -e
+cd "$(dirname "$0")"
+
+# Use the specified docker command or default to 'docker'
+DOCKER=${DOCKER:-docker}
+BUILDER_IMAGE=${BUILDER_IMAGE:-chicory/rust-builder}
+
+build_builder() {
+  ${DOCKER} build . --load --tag "$BUILDER_IMAGE"
+}
+
+run_docker() {
+  # Build the builder image if it does not exist
+  if ! ${DOCKER} image inspect "$BUILDER_IMAGE" >/dev/null 2>&1; then
+    echo "Docker image $BUILDER_IMAGE not found. Building with: ./build.sh builder"
+    build_builder
+  fi
+  ${DOCKER} run ${DOCKER_ARGS} -t -v"$PWD":/work -w /work $BUILDER_IMAGE "$@"
+}
+
+case "${1:-}" in
+  builder) # Rebuild the builder image...
+    build_builder
+    exit 0
+    ;;
+  container) # We are now running in the builder container...
+    shift
+    ;; # Fallthrough to the main build logic
+  bash) # start a bash shell in the container
+    shift
+    DOCKER_ARGS=-i run_docker bash "$@"
+    exit 0
+    ;;
+  default) # Use docker to run the build....
+    shift
+    run_docker ./build.sh container "$@"
+    exit 0
+    ;;
+  *)
+    run_docker ./build.sh container
+    exit 0
+    ;;
+esac
+
+#
+# Main build logic.. This gets run in the docker container..
+#
+cd ./rust
+cargo build --release --target wasm32-wasip1
\ No newline at end of file
diff --git a/dwarf-rust-experimental/pom.xml b/dwarf-rust-experimental/pom.xml
new file mode 100644
index 000000000..93750a44f
--- /dev/null
+++ b/dwarf-rust-experimental/pom.xml
@@ -0,0 +1,117 @@
+
+
+  4.0.0
+
+  
+    com.dylibso.chicory
+    chicory
+    999-SNAPSHOT
+  
+  dwarf-rust-experimental
+  jar
+
+  Chicory - Dwarf Debug Parser
+  Parse dwarf debug symbols from wasm modules
+
+  
+    
+      com.dylibso.chicory
+      log
+    
+    
+      com.dylibso.chicory
+      runtime
+    
+    
+      com.dylibso.chicory
+      wasi
+    
+    
+      com.dylibso.chicory
+      wasm
+    
+    
+      com.fasterxml.jackson.core
+      jackson-databind
+    
+    
+      com.approvaltests
+      approvaltests
+      test
+    
+    
+      com.dylibso.chicory
+      compiler
+      test
+    
+    
+      com.dylibso.chicory
+      wasm-corpus
+      test
+    
+
+    
+      org.junit.jupiter
+      junit-jupiter-api
+      test
+    
+
+  
+
+  
+    
+      
+        com.dylibso.chicory
+        chicory-compiler-maven-plugin
+        
+          
+            chicory-compile
+            
+              compile
+            
+            process-sources
+            
+              com.dylibso.chicory.dwarf.rust.internal.Wasm
+              ${project.basedir}/rust/target/wasm32-wasip1/release/dwarf-rust.wasm
+            
+          
+        
+      
+    
+  
+
+  
+    
+      build-rust
+      
+        
+          build-rust
+        
+      
+      
+        
+          
+            org.codehaus.mojo
+            exec-maven-plugin
+            3.1.0
+            
+              
+                run-build-sh
+                
+                  exec
+                
+                generate-sources
+                
+                  sh
+                  
+                    ./build.sh
+                  
+                
+              
+            
+          
+        
+      
+    
+  
+
diff --git a/dwarf-rust-experimental/rust/Cargo.lock b/dwarf-rust-experimental/rust/Cargo.lock
new file mode 100644
index 000000000..bda4ae1f5
--- /dev/null
+++ b/dwarf-rust-experimental/rust/Cargo.lock
@@ -0,0 +1,166 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "arrayvec"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9"
+dependencies = [
+ "nodrop",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "dwarf-rust"
+version = "0.1.0"
+dependencies = [
+ "fallible-iterator",
+ "gimli",
+ "indexmap",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "fallible-iterator"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
+
+[[package]]
+name = "gimli"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "162d18ae5f2e3b90a993d202f1ba17a5633c2484426f8bcae201f86194bacd00"
+dependencies = [
+ "arrayvec",
+ "byteorder",
+ "fallible-iterator",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "nodrop"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
+
+[[package]]
+name = "serde"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.219"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.140"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "syn"
+version = "2.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
diff --git a/dwarf-rust-experimental/rust/Cargo.toml b/dwarf-rust-experimental/rust/Cargo.toml
new file mode 100644
index 000000000..3cd3206dc
--- /dev/null
+++ b/dwarf-rust-experimental/rust/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "dwarf-rust"
+version = "0.1.0"
+description = "WebAssembly DWARF parser and location resolver"
+authors = ["Ingvar Stepanyan ", "Hiram Chirino "]
+edition = "2018"
+publish = false
+readme = "README"
+license = "BSD-style"
+
+[dependencies]
+gimli = { version = "0.19.0", default-features = false, features = ["std", "read"] }
+fallible-iterator = "0.2.0"
+indexmap = "1.3.0"
+serde = { version = "1.0.185", features = ["derive"] }
+serde_json = "1.0.64"
diff --git a/dwarf-rust-experimental/rust/LICENSE b/dwarf-rust-experimental/rust/LICENSE
new file mode 100644
index 000000000..b679c7b81
--- /dev/null
+++ b/dwarf-rust-experimental/rust/LICENSE
@@ -0,0 +1,29 @@
+// Copyright 2019 The Chromium Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+//    * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+//    * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+//    * Neither the name of Google LLC nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF
+
+ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/dwarf-rust-experimental/rust/LICENSES.deps b/dwarf-rust-experimental/rust/LICENSES.deps
new file mode 100644
index 000000000..ba4b73901
--- /dev/null
+++ b/dwarf-rust-experimental/rust/LICENSES.deps
@@ -0,0 +1,7 @@
+Licenses of dependencies as produced by `cargo-license`:
+
+(MIT OR Apache-2.0) AND Unicode-3.0 (1): unicode-ident
+Apache-2.0 OR BSL-1.0 (1): ryu
+Apache-2.0 OR MIT (15): arrayvec, autocfg, fallible-iterator, gimli, hashbrown, indexmap, itoa, nodrop, proc-macro2, quote, serde, serde_derive, serde_json, stable_deref_trait, syn
+BSD-style (1): wasm-source-map
+MIT OR Unlicense (2): byteorder, memchr
diff --git a/dwarf-rust-experimental/rust/README b/dwarf-rust-experimental/rust/README
new file mode 100644
index 000000000..a50532add
--- /dev/null
+++ b/dwarf-rust-experimental/rust/README
@@ -0,0 +1,6 @@
+Fork https://github.com/bmeurer/wasm-source-map
+that inputs a WebAssembly file on stdin and outputs source map data as json on stdout.
+
+To build for use by wasm, run:
+
+    cargo build --target=wasm32-wasip1 --release
\ No newline at end of file
diff --git a/dwarf-rust-experimental/rust/rustfmt.toml b/dwarf-rust-experimental/rust/rustfmt.toml
new file mode 100644
index 000000000..172647139
--- /dev/null
+++ b/dwarf-rust-experimental/rust/rustfmt.toml
@@ -0,0 +1,2 @@
+tab_spaces = 2
+newline_style = "Unix"
diff --git a/dwarf-rust-experimental/rust/src/apperror.rs b/dwarf-rust-experimental/rust/src/apperror.rs
new file mode 100644
index 000000000..cd620ce52
--- /dev/null
+++ b/dwarf-rust-experimental/rust/src/apperror.rs
@@ -0,0 +1,45 @@
+use std::{error, fmt};
+
+#[derive(Debug)]
+pub enum Error {
+  InvalidMagic,
+  UnsupportedVersion(u32),
+  MissingCodeSection,
+  Reader(gimli::Error),
+  InvalidPath(String),
+  Io(std::io::Error),
+  Json(serde_json::Error),
+  Internal(&'static str),
+}
+
+impl fmt::Display for Error {
+  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+    match self {
+      Error::InvalidMagic => write!(f, "WebAssembly magic mismatch."),
+      Error::UnsupportedVersion(v) => {
+        write!(f, "Unsupported WebAssembly version {}.", v)
+      }
+      Error::MissingCodeSection => write!(f, "Missing code section."),
+      Error::Reader(err) => write!(f, "{}", err),
+      Error::InvalidPath(err) => write!(f, "{}", err),
+      Error::Io(err) => write!(f, "io error: {}", err),
+      Error::Json(err) => write!(f, "json error: {}", err),
+      Error::Internal(msg) => write!(f, "internal error: {}", msg),
+    }
+  }
+}
+
+impl error::Error for Error {
+  fn source(&self) -> Option<&(dyn error::Error + 'static)> {
+    match self {
+      Error::Reader(err) => Some(err),
+      _ => None,
+    }
+  }
+}
+
+impl From for Error {
+  fn from(err: gimli::Error) -> Self {
+    Error::Reader(err)
+  }
+}
diff --git a/dwarf-rust-experimental/rust/src/main.rs b/dwarf-rust-experimental/rust/src/main.rs
new file mode 100644
index 000000000..ec586a6b0
--- /dev/null
+++ b/dwarf-rust-experimental/rust/src/main.rs
@@ -0,0 +1,446 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+mod apperror;
+mod wasm;
+
+use apperror::Error;
+use fallible_iterator::FallibleIterator;
+use gimli::{constants, Dwarf, EndianSlice, LineRow, LittleEndian, Reader};
+use indexmap::IndexMap;
+use serde::{Deserialize, Serialize};
+use std::collections::{BTreeMap, HashMap};
+use std::convert::TryInto;
+use std::io::Read;
+use wasm::{parse_sections, SectionKind};
+
+pub struct Pos {
+  line: u32,
+}
+
+#[derive(Default, Debug, Clone)]
+pub struct Line {
+  id: u64,
+  file_id: u32,
+  address: u64,
+  line: u32,
+  score: u32,
+}
+
+#[derive(Default, Debug, Clone)]
+pub struct ScoredSourceFile {
+  id: u32,
+  directory: Option,
+  file: String,
+  language: u16,
+  lines: Vec,
+}
+
+#[derive(Default, Debug, Clone)]
+pub struct ScoredSourceUnit {
+  name: String,
+  directory: String,
+  files: Vec,
+}
+
+#[derive(Default, Serialize, Deserialize, PartialEq, Debug, Clone)]
+pub struct SourceFile {
+  id: u32,
+  #[serde(skip_serializing_if = "Option::is_none")]
+  directory: Option,
+  file: String,
+  language: u16,
+}
+
+#[derive(Default, Serialize, Deserialize, PartialEq, Debug)]
+pub struct SourceUnit {
+  name: String,
+  directory: String,
+  files: Vec,
+}
+
+#[derive(Default, Serialize, Deserialize, PartialEq, Debug)]
+pub struct SourceResult {
+  #[serde(skip_serializing_if = "Option::is_none")]
+  units: Option>,
+  lines: Option>>,
+
+  #[serde(skip_serializing_if = "Option::is_none")]
+  functions: Option>>,
+
+  #[serde(skip_serializing_if = "Option::is_none")]
+  error: Option,
+}
+
+pub fn extract_source_info(src: R) -> Result {
+  let mut sections = HashMap::new();
+
+  for section in parse_sections(src.clone())?.iterator() {
+    let section = section?;
+
+    match section.kind {
+      SectionKind::Custom { name } => {
+        if name.starts_with(".debug_") {
+          sections.insert(name, section.payload);
+        }
+      }
+      _ => {}
+    }
+  }
+
+  let dwarf = Dwarf::load::<_, _, Error>(
+    |id| Ok(sections.get(id.name()).cloned().unwrap_or_default()),
+    |_| Ok(Default::default()),
+  )?;
+
+  let mut scored_source_units = vec![];
+
+  let mut iter = dwarf.units();
+  let mut best_scores: HashMap = HashMap::new();
+  let mut next_entry_id = 0u64;
+  let mut next_file_id = 0u32;
+  let mut functions: BTreeMap> = BTreeMap::new();
+
+  while let Some(unit) = iter.next()? {
+    let mut unit = dwarf.unit(unit)?;
+
+    let line_program = match unit.line_program.take() {
+      Some(line_program) => line_program,
+      None => continue,
+    };
+
+    let lang = {
+      let mut entries = unit.entries();
+      entries.next_entry()?;
+      match entries
+        .current()
+        .unwrap()
+        .attr_value(gimli::DW_AT_language)?
+      {
+        Some(gimli::AttributeValue::Language(lang)) => lang.0,
+        _ => 0,
+      }
+    };
+
+    // Extract function information from DWARF entries
+    {
+      let mut entries = unit.entries();
+      while let Some((_depth, entry)) = entries.next_dfs()? {
+        if entry.tag() == gimli::DW_TAG_subprogram {
+          // Get function name
+          let func_name = if let Some(attr) = entry.attr_value(gimli::DW_AT_name)? {
+            match attr {
+              gimli::AttributeValue::DebugStrRef(offset) => {
+                match dwarf.debug_str.get_str(offset) {
+                  Ok(name_str) => Some(name_str.to_string()?.to_string()),
+                  Err(_) => None,
+                }
+              }
+              gimli::AttributeValue::String(name_str) => {
+                Some(name_str.to_string()?.to_string())
+              }
+              _ => None,
+            }
+          } else {
+            None
+          };
+          
+
+          // Get low_pc (start address) and high_pc (end address or size)
+          let low_pc = if let Some(attr) = entry.attr_value(gimli::DW_AT_low_pc)? {
+            match attr {
+              gimli::AttributeValue::Addr(addr) => Some(addr),
+              _ => None,
+            }
+          } else {
+            None
+          };
+
+          let high_pc = if let Some(attr) = entry.attr_value(gimli::DW_AT_high_pc)? {
+            match attr {
+              gimli::AttributeValue::Addr(addr) => Some(addr),
+              gimli::AttributeValue::Udata(offset) => {
+                // high_pc can be an offset from low_pc
+                if let Some(low) = low_pc {
+                  Some(low + offset)
+                } else {
+                  None
+                }
+              }
+              _ => None,
+            }
+          } else {
+            None
+          };
+
+          // If we have all the information, add it to our functions map
+          if let (Some(func_name), Some(start), Some(end)) = (func_name, low_pc, high_pc) {
+            // Filter out synthetic/relocated addresses similar to line processing
+            const MAX_REALISTIC_WASM_ADDR: u64 = 0x40000000; // 1GB threshold
+            if start <= MAX_REALISTIC_WASM_ADDR && end <= MAX_REALISTIC_WASM_ADDR && start < end {
+              functions.insert(func_name, vec![start, end]);
+            }
+          }
+        }
+      }
+    }
+
+    let mut scored_source_files: IndexMap<(Option, String), ScoredSourceFile> =
+      IndexMap::new();
+
+    let mut rows = line_program.rows();
+
+    while let Some((header, row)) = rows.next_row()? {
+
+      let file = match row.file(header) {
+        Some(file) => file,
+        None => continue,
+      };
+
+      let pos = {
+        let mut line = match row.line() {
+          Some(line) => line.checked_sub(1).unwrap().try_into().unwrap(),
+          None => continue, // couldn't attribute instruction to any line
+        };
+
+        // It seems we need to add 1 to the line numbers for Rust.
+        let is_rust = lang == constants::DW_LANG_Rust.0;
+        if is_rust {
+          line += 1;
+        }
+
+        Pos { line }
+      };
+
+      let addr: u64 = row.address().try_into().unwrap();
+
+      // Filter out synthetic/relocated addresses that are too high to be realistic WASM code offsets
+      // Most WASM modules are much smaller than 1GB, so addresses > 0x40000000 are likely synthetic
+      const MAX_REALISTIC_WASM_ADDR: u64 = 0x40000000; // 1GB threshold
+      if addr > MAX_REALISTIC_WASM_ADDR {
+        continue;
+      }
+
+      let directory = if let Some(dir) = file.directory(header) {
+        let dir = dwarf.attr_string(&unit, dir)?;
+        let dir = dir.to_string()?;
+        let dir = dir.to_string();
+        Some(dir)
+      } else {
+        None
+      };
+
+      let filename = dwarf.attr_string(&unit, file.path_name())?;
+      let filename = filename.to_string()?;
+      let filename = filename.to_string();
+
+      let score = calculate_mapping_score(&directory, &filename, pos.line as u64, &row);
+
+      let file_entries = match scored_source_files.entry((directory.clone(), filename.clone())) {
+        indexmap::map::Entry::Occupied(entry) => entry.into_mut(),
+        indexmap::map::Entry::Vacant(entry) => {
+          let source_file = entry.insert(ScoredSourceFile {
+            id: next_file_id,
+            directory,
+            file: filename.to_string(),
+            language: lang,
+            lines: Vec::new(),
+          });
+          next_file_id += 1;
+          source_file
+        }
+      };
+
+      let line = Line {
+        id: next_entry_id,
+        file_id: file_entries.id,
+        address: addr,
+        line: pos.line,
+        // column: pos.column,
+        score,
+      };
+      next_entry_id += 1;
+
+      file_entries.lines.push(line.clone());
+      if let Some(existing) = best_scores.get(&line.address) {
+        if existing.score < line.score {
+          best_scores.insert(line.address, line);
+        }
+      } else {
+        best_scores.insert(line.address, line);
+      }
+    }
+
+    let source_unit = ScoredSourceUnit {
+      name: unit
+        .name
+        .as_ref()
+        .map(|x| x.to_string())
+        .transpose()?
+        .unwrap_or_default()
+        .to_string(),
+      directory: unit
+        .comp_dir
+        .as_ref()
+        .map(|x| x.to_string())
+        .transpose()?
+        .unwrap_or_default()
+        .to_string(),
+      files: scored_source_files.values().cloned().collect(),
+    };
+    if !source_unit.files.is_empty() {
+      scored_source_units.push(source_unit);
+    }
+  }
+
+  // Iterate through the source units and their files to and remove any
+  // files that don't have any lines in the best_scores map.
+  for unit in &mut scored_source_units {
+    for file in &mut unit.files {
+      file.lines.retain(|line| {
+        if let Some(best_line) = best_scores.get(&line.address) {
+          line.id == best_line.id
+        } else {
+          false
+        }
+      });
+    }
+    unit.files.retain(|file| !file.lines.is_empty());
+  }
+  scored_source_units.retain(|unit| !unit.files.is_empty());
+
+  // convert the scored source units to the final format
+  let res: Vec = scored_source_units
+    .into_iter()
+    .map(|unit| SourceUnit {
+      name: unit.name,
+      directory: unit.directory,
+      files: unit
+        .files
+        .into_iter()
+        .map(|file| SourceFile {
+          id: file.id,
+          directory: file.directory,
+          file: file.file,
+          language: file.language,
+        })
+        .collect(),
+    })
+    .collect();
+
+  let mut lines: Vec> = best_scores
+    .values()
+    .map(|line| vec![line.address, line.file_id as u64, line.line as u64])
+    .collect();
+  lines.sort_by_key(|line| line[0]);
+
+  Ok(SourceResult {
+    units: Some(res),
+    lines: Some(lines),
+    functions: if functions.is_empty() { None } else { Some(functions) },
+    error: None,
+  })
+}
+
+/// Calculate a score for how "good" a source mapping is. Higher scores are better.
+/// This helps determine which mapping to keep when multiple source locations map to the same address.
+fn calculate_mapping_score(
+  directory: &Option,
+  filename: &str,
+  line_number: u64,
+  row: &LineRow,
+) -> u32 {
+  let mut score = 1000u32; // Base score
+
+  if !row.is_stmt() {
+    score = score.saturating_sub(400); // Heavy penalty for non-statement locations
+  }
+
+  if row.prologue_end() || row.epilogue_begin() {
+    score = score.saturating_sub(300); // Heavy penalty for prologue/epilogue code
+  }
+
+  if row.basic_block() {
+    score = score.saturating_sub(200); // Penalty for basic block boundaries
+  }
+
+  // Strategy 1: Prefer user code over library/dependency code
+  if let Some(dir) = directory {
+    if dir.contains("/rustc/") || dir.contains("/rust/deps/") {
+      score -= 300; // Heavily penalize compiler/dependency paths
+    }
+    if dir.contains("library/") {
+      score -= 200; // Penalize standard library
+    }
+    if dir.starts_with("/") && !dir.contains("src") {
+      score -= 100; // Penalize absolute paths that don't look like source
+    }
+  }
+
+  // Strategy 2: Prefer certain file types
+  if filename.ends_with(".rs") {
+    score += 100; // Prefer Rust source files
+  }
+  if filename.contains("main.rs") || filename.contains("lib.rs") {
+    score += 50; // Prefer entry point files
+  }
+  if filename.contains("mod.rs") && !filename.contains("intrinsics") {
+    score += 30; // Prefer module files (but not intrinsics)
+  }
+
+  // Strategy 3: Avoid generated/internal files
+  if filename.contains("intrinsics") || filename.contains("panic") {
+    score -= 150; // Avoid compiler intrinsics and panic handlers
+  }
+  if filename.contains("macros.rs") {
+    score -= 100; // Macro definitions are often less useful for debugging
+  }
+  if filename.contains("impls.rs") || filename.contains("builders.rs") {
+    score -= 80; // Implementation details vs user code
+  }
+
+  // Strategy 4: Prefer specific line numbers over line 0
+  if line_number == 0 {
+    score -= 200; // Line 0 is often a catch-all or generated
+  } else if line_number < 10 {
+    score -= 50; // Very early lines might be imports/boilerplate
+  }
+
+  // Strategy 5: Prefer shorter, simpler paths (user code is typically closer to root)
+  if let Some(dir) = directory {
+    let path_depth = dir.matches('/').count();
+    if path_depth > 5 {
+      score -= (path_depth as u32 - 5) * 20; // Penalize deeply nested paths
+    }
+  }
+
+  score
+}
+
+fn run() -> Result<(), Error> {
+  let mut buffer = Vec::new();
+  std::io::stdin()
+    .read_to_end(&mut buffer)
+    .map_err(Error::Io)?;
+  let slice = EndianSlice::new(buffer.as_slice(), LittleEndian);
+  let result = extract_source_info(slice)?;
+  serde_json::to_writer(std::io::stdout(), &result).map_err(Error::Json)?;
+  Ok(())
+}
+
+fn main() {
+  run().unwrap_or_else(|err| {
+    let error_doc = SourceResult {
+      error: Some(err.to_string()),
+      units: None,
+      lines: None,
+      functions: None,
+    };
+    serde_json::to_writer(std::io::stdout(), &error_doc).unwrap_or_else(|err| {
+      eprintln!("Error: {}", err);
+      std::process::exit(2);
+    });
+    std::process::exit(1);
+  });
+}
diff --git a/dwarf-rust-experimental/rust/src/wasm.rs b/dwarf-rust-experimental/rust/src/wasm.rs
new file mode 100644
index 000000000..ed489f2a2
--- /dev/null
+++ b/dwarf-rust-experimental/rust/src/wasm.rs
@@ -0,0 +1,76 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+use crate::apperror::Error;
+use fallible_iterator::FallibleIterator;
+use gimli::{Reader, ReaderOffset};
+use std::convert::TryInto;
+use std::num::NonZeroU8;
+
+#[derive(Debug)]
+pub enum SectionKind {
+  Custom { name: String },
+  Standard,
+}
+
+#[derive(Debug)]
+pub struct Section {
+  pub kind: SectionKind,
+  pub payload: R,
+}
+
+pub fn parse_sections(
+  mut reader: R,
+) -> Result, Error = gimli::Error>, Error> {
+  struct Iterator {
+    reader: R,
+  }
+
+  impl FallibleIterator for Iterator {
+    type Item = Section;
+    type Error = gimli::Error;
+
+    fn next(&mut self) -> Result>, gimli::Error> {
+      if self.reader.is_empty() {
+        return Ok(None);
+      }
+
+      let id = self
+        .reader
+        .read_uleb128()?
+        .try_into()
+        .map_err(|_| gimli::Error::BadUnsignedLeb128)?;
+
+      let payload_len = ReaderOffset::from_u64(self.reader.read_uleb128()?)?;
+      let mut payload_reader = self.reader.split(payload_len)?;
+
+      let kind = match NonZeroU8::new(id) {
+        None => {
+          let name_len = ReaderOffset::from_u64(payload_reader.read_uleb128()?)?;
+          let name_reader = payload_reader.split(name_len)?;
+          SectionKind::Custom {
+            name: name_reader.to_string()?.into_owned(),
+          }
+        }
+        Some(_) => SectionKind::Standard,
+      };
+
+      Ok(Some(Section {
+        kind,
+        payload: payload_reader,
+      }))
+    }
+  }
+
+  if reader.read_u8_array::<[u8; 4]>()? != *b"\0asm" {
+    return Err(Error::InvalidMagic);
+  }
+
+  let version = reader.read_u32()?;
+  if version != 1 {
+    return Err(Error::UnsupportedVersion(version));
+  }
+
+  Ok(Iterator { reader })
+}
diff --git a/dwarf-rust-experimental/rust/target/wasm32-wasip1/release/dwarf-rust.wasm b/dwarf-rust-experimental/rust/target/wasm32-wasip1/release/dwarf-rust.wasm
new file mode 100755
index 000000000..67987f225
Binary files /dev/null and b/dwarf-rust-experimental/rust/target/wasm32-wasip1/release/dwarf-rust.wasm differ
diff --git a/dwarf-rust-experimental/src/main/java/com/dylibso/chicory/dwarf/rust/DebugParser.java b/dwarf-rust-experimental/src/main/java/com/dylibso/chicory/dwarf/rust/DebugParser.java
new file mode 100644
index 000000000..741a9c552
--- /dev/null
+++ b/dwarf-rust-experimental/src/main/java/com/dylibso/chicory/dwarf/rust/DebugParser.java
@@ -0,0 +1,201 @@
+package com.dylibso.chicory.dwarf.rust;
+
+import com.dylibso.chicory.dwarf.rust.internal.Wasm;
+import com.dylibso.chicory.log.Logger;
+import com.dylibso.chicory.log.SystemLogger;
+import com.dylibso.chicory.runtime.ImportValues;
+import com.dylibso.chicory.runtime.Instance;
+import com.dylibso.chicory.runtime.ParserException;
+import com.dylibso.chicory.runtime.Stratum;
+import com.dylibso.chicory.wasi.WasiOptions;
+import com.dylibso.chicory.wasi.WasiPreview1;
+import com.dylibso.chicory.wasm.WasmModule;
+import com.dylibso.chicory.wasm.WasmWriter;
+import com.dylibso.chicory.wasm.types.UnknownCustomSection;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public final class DebugParser implements com.dylibso.chicory.runtime.DebugParser {
+    private static final Logger logger = new SystemLogger();
+    private static final WasmModule MODULE = Wasm.load();
+
+    @Override
+    public Stratum apply(WasmModule wasmModule) {
+        return parse(wasmModule);
+    }
+
+    static final class SourceResult {
+        public String error;
+        public List units;
+        public List lines;
+        public Map functions;
+    }
+
+    static final class SourceUnit {
+        @SuppressWarnings("unused")
+        public String name;
+
+        @SuppressWarnings("unused")
+        public String directory;
+
+        public List files;
+    }
+
+    static final class SourceFile {
+        public int id;
+        public String directory;
+        public String file;
+
+        @SuppressWarnings("unused")
+        public int language;
+    }
+
+    private static final class FileInfo {
+        final String file;
+        final String path;
+
+        public FileInfo(String path, SourceFile file) {
+            this.file = file.file;
+            this.path = path;
+        }
+    }
+
+    private static byte[] toBytes(WasmModule module) {
+        var writer = new WasmWriter();
+        var keepers =
+                Set.of(
+                        ".debug_info",
+                        ".debug_line",
+                        ".debug_str",
+                        ".debug_aranges",
+                        ".debug_pubnames",
+                        ".debug_loc",
+                        ".debug_ranges",
+                        ".debug_abbrev",
+                        ".debug_pubtypes");
+        for (var section : module.customSections()) {
+            if (section instanceof UnknownCustomSection) {
+                if (keepers.contains(section.name())) {
+                    writer.writeSection((UnknownCustomSection) section);
+                }
+            }
+        }
+        writer.writeEmptyCodeSection();
+        return writer.bytes();
+    }
+
+    public static Stratum parse(WasmModule module) throws ParserException {
+        return parse(toBytes(module));
+    }
+
+    private static Stratum parse(byte[] wasm) throws ParserException {
+        try (InputStream is = new ByteArrayInputStream(wasm)) {
+            return parse(is);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+
+    private static Stratum parse(InputStream is) throws ParserException {
+        SourceResult result = getSourceResult(is);
+        var stratum = Stratum.builder("WASM");
+        HashMap sourceFilesByID = new HashMap<>();
+        for (var unit : result.units) {
+            for (var file : unit.files) {
+
+                String path = file.file;
+                if (path != null) {
+                    path = path.replace('\\', '/');
+                }
+                if (file.directory != null) {
+                    var dir = file.directory.replace('\\', '/');
+                    if (dir.endsWith("/")) {
+                        path = file.directory + path;
+                    } else {
+                        path = file.directory + "/" + path;
+                    }
+                }
+                sourceFilesByID.put(file.id, new FileInfo(path, file));
+            }
+        }
+
+        int size = result.lines.size();
+        for (int i = 0; i < size; i++) {
+            var location = result.lines.get(i);
+            long address = location[0];
+
+            // we treat each address as a line.
+            var outputLineCount = 1;
+            if (i + 1 < size) {
+                var nextAddress = result.lines.get(i + 1)[0];
+                outputLineCount = (int) (nextAddress - address);
+            }
+
+            int fileID = (int) location[1];
+            int line = (int) location[2];
+
+            var file = sourceFilesByID.get(fileID);
+            stratum.withLineMapping(file.file, file.path, line, 1, address, outputLineCount);
+        }
+
+        if (result.functions != null) {
+            for (var entry : result.functions.entrySet()) {
+                String functionName = entry.getKey();
+                long[] locations = entry.getValue();
+                if (locations.length < 2) {
+                    continue; // Invalid function data
+                }
+                long startAddress = locations[0];
+                long endAddress = locations[1];
+                stratum.withFunctionMapping(functionName, startAddress, endAddress);
+            }
+        }
+
+        return stratum.build();
+    }
+
+    static SourceResult getSourceResult(InputStream is) {
+        SourceResult result = null;
+        try (var stdoutStream = new ByteArrayOutputStream();
+                var stderrStream = new ByteArrayOutputStream()) {
+
+            WasiOptions wasiOpts =
+                    WasiOptions.builder()
+                            .withStdin(is)
+                            .withStdout(stdoutStream)
+                            .withStderr(stderrStream)
+                            .build();
+
+            try (var wasi =
+                    WasiPreview1.builder().withLogger(logger).withOptions(wasiOpts).build()) {
+                ImportValues imports =
+                        ImportValues.builder().addFunction(wasi.toHostFunctions()).build();
+                Instance.builder(MODULE)
+                        .withMachineFactory(Wasm::create)
+                        .withImportValues(imports)
+                        .build();
+            }
+
+            var stdoutString = new String(stdoutStream.toByteArray(), StandardCharsets.UTF_8);
+            ObjectMapper mapper = new ObjectMapper();
+            result = mapper.readValue(stdoutString, SourceResult.class);
+
+            if (result.error != null) {
+                throw new ParserException(result.error);
+            }
+
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+        return result;
+    }
+}
diff --git a/dwarf-rust-experimental/src/main/resources/META-INF/services/com.dylibso.chicory.runtime.DebugParser b/dwarf-rust-experimental/src/main/resources/META-INF/services/com.dylibso.chicory.runtime.DebugParser
new file mode 100644
index 000000000..b23939adf
--- /dev/null
+++ b/dwarf-rust-experimental/src/main/resources/META-INF/services/com.dylibso.chicory.runtime.DebugParser
@@ -0,0 +1 @@
+com.dylibso.chicory.dwarf.rust.DebugParser
diff --git a/dwarf-rust-experimental/src/test/java/com/dylibso/chicory/dwarf/rust/MachinesTest.java b/dwarf-rust-experimental/src/test/java/com/dylibso/chicory/dwarf/rust/MachinesTest.java
new file mode 100644
index 000000000..afba570c5
--- /dev/null
+++ b/dwarf-rust-experimental/src/test/java/com/dylibso/chicory/dwarf/rust/MachinesTest.java
@@ -0,0 +1,154 @@
+package com.dylibso.chicory.dwarf.rust;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.dylibso.chicory.compiler.MachineFactoryCompiler;
+import com.dylibso.chicory.runtime.Instance;
+import com.dylibso.chicory.runtime.TrapException;
+import com.dylibso.chicory.wasm.Parser;
+import com.dylibso.chicory.wasm.WasmModule;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+
+public final class MachinesTest {
+
+    private WasmModule loadModule(String fileName) {
+        return Parser.parse(getClass().getResourceAsStream("/" + fileName));
+    }
+
+    private String readStackTrace(Throwable t) {
+        StringWriter sw = new StringWriter();
+        t.printStackTrace(new PrintWriter(sw));
+        return sw.toString();
+    }
+
+    @Test
+    public void shouldEmitUnderstandableStackTraces() throws Exception {
+        var instance =
+                Instance.builder(loadModule("compiled/count_vowels.rs.wasm"))
+                        .withDebugParser(DebugParser::parse)
+                        .build();
+        var countVowels = instance.export("count_vowels");
+        var exception = assertThrows(TrapException.class, () -> countVowels.apply(0, -1));
+        var exceptionTxt = readStackTrace(exception);
+
+        // To generate the following stack track in rust, run:
+        //     ./wasm-corpus/run.sh run bash -c "cd rust/count_vowels; RUST_BACKTRACE=1 cargo test"
+        //
+        //   0: __rustc::rust_begin_unwind
+        //             at
+        // /rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/std/src/panicking.rs:697:5
+        //   1: core::panicking::panic_nounwind_fmt::runtime
+        //             at
+        // /rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/panicking.rs:117:22
+        //   2: core::panicking::panic_nounwind_fmt
+        //             at
+        // /rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/intrinsics/mod.rs:3241:9
+        //   3: core::panicking::panic_nounwind
+        //             at
+        // /rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/panicking.rs:218:5
+        //   4: core::slice::raw::from_raw_parts::precondition_check
+        //             at
+        // /rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/ub_checks.rs:68:21
+        //   5: core::slice::raw::from_raw_parts
+        //             at
+        // /rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/ub_checks.rs:75:17
+        //   6: count_vowels
+        //             at ./src/lib.rs:23:26
+        //   7: count_vowels::tests::test_count_vowels
+        //             at ./src/lib.rs:40:9
+
+        // It's not exactly 1-to-1, but it's close enough.
+        assertTrue(
+                exceptionTxt.contains(
+                        "at 0x00627d: chicory"
+                            + " interpreter.begin_panic_handler(library/std/src/panicking.rs:697)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at 0x007e74: chicory"
+                            + " interpreter.panic_nounwind_fmt(library/core/src/panicking.rs:117)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at 0x007ec8: chicory"
+                            + " interpreter.panic_nounwind(library/core/src/panicking.rs:218)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at 0x001cf6: chicory"
+                            + " interpreter.func_30(/rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/ub_checks.rs:68)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at 0x003948: chicory"
+                            + " interpreter.from_raw_parts(/rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/ub_checks.rs:75)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at 0x000d7f: chicory interpreter.count_vowels(src/lib.rs:23)"));
+    }
+
+    @Test
+    public void shouldEmitUnderstandableStackTracesCompiled() throws Exception {
+        WasmModule module = loadModule("compiled/count_vowels.rs.wasm");
+        var instance =
+                Instance.builder(module)
+                        .withDebugParser(DebugParser::parse)
+                        .withMachineFactory(
+                                MachineFactoryCompiler.builder(module)
+                                        .withClassName("com.dylibso.chicory.$gen.CompiledMachine")
+                                        .withDebugParser(DebugParser::parse)
+                                        .compile())
+                        .build();
+        var countVowels = instance.export("count_vowels");
+        var exception = assertThrows(TrapException.class, () -> countVowels.apply(0, -1));
+        var exceptionTxt = readStackTrace(exception);
+
+        assertTrue(
+                exceptionTxt.contains(
+                        "at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.begin_panic_handler(library/std/src/panicking.rs:697)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.panic_nounwind_fmt(library/core/src/panicking.rs:117)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.panic_nounwind(library/core/src/panicking.rs:218)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.func_30(/rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/ub_checks.rs:68)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.from_raw_parts(/rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/ub_checks.rs:75)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.count_vowels(src/lib.rs:23)"));
+    }
+
+    @Test
+    public void shouldEmitUnderstandableStackTracesCompiledAndInterpreted() throws Exception {
+        WasmModule module = loadModule("compiled/count_vowels.rs.wasm");
+        var instance =
+                Instance.builder(module)
+                        .withDebugParser(DebugParser::parse)
+                        .withMachineFactory(
+                                MachineFactoryCompiler.builder(module)
+                                        .withClassName("com.dylibso.chicory.$gen.CompiledMachine")
+                                        .withInterpretedFunctions(Set.of((54)))
+                                        .withDebugParser(DebugParser::parse)
+                                        .compile())
+                        .build();
+        var countVowels = instance.export("count_vowels");
+        var exception = assertThrows(TrapException.class, () -> countVowels.apply(0, -1));
+        var exceptionTxt = readStackTrace(exception);
+
+        assertTrue(
+                exceptionTxt.contains(
+                        "at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.func_30(/rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/ub_checks.rs:68)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at 0x003948: chicory"
+                            + " interpreter.from_raw_parts(/rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/ub_checks.rs:75)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at com.dylibso.chicory.$gen.CompiledMachineFuncGroup_0.count_vowels(src/lib.rs:23)"));
+    }
+}
diff --git a/dwarf-rust-experimental/src/test/java/com/dylibso/chicory/dwarf/rust/PackageSettings.java b/dwarf-rust-experimental/src/test/java/com/dylibso/chicory/dwarf/rust/PackageSettings.java
new file mode 100644
index 000000000..516609046
--- /dev/null
+++ b/dwarf-rust-experimental/src/test/java/com/dylibso/chicory/dwarf/rust/PackageSettings.java
@@ -0,0 +1,5 @@
+package com.dylibso.chicory.dwarf.rust;
+
+public class PackageSettings {
+    public String ApprovalBaseDirectory = "../resources";
+}
diff --git a/dwarf-rust-experimental/src/test/java/com/dylibso/chicory/dwarf/rust/RustParserTest.java b/dwarf-rust-experimental/src/test/java/com/dylibso/chicory/dwarf/rust/RustParserTest.java
new file mode 100644
index 000000000..80504916d
--- /dev/null
+++ b/dwarf-rust-experimental/src/test/java/com/dylibso/chicory/dwarf/rust/RustParserTest.java
@@ -0,0 +1,58 @@
+package com.dylibso.chicory.dwarf.rust;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.dylibso.chicory.wasm.Parser;
+import java.io.File;
+import java.io.StringWriter;
+import org.approvaltests.Approvals;
+import org.junit.jupiter.api.Test;
+
+public class RustParserTest {
+
+    @Test
+    public void shouldParseCountVowels() throws Exception {
+        var module =
+                Parser.parse(
+                        RustParserTest.class.getResourceAsStream("/compiled/count_vowels.rs.wasm"));
+        var result = DebugParser.parse(module);
+        assertNotNull(result);
+
+        // Just want peek at an entry and make sure it values look like
+        // what you would be normal for a source line...
+        var entry = result.getInputLine(300);
+        assertNotNull(entry);
+
+        var writer = new StringWriter();
+        writer.append("input file=").append(entry.fileName()).append("\n");
+        writer.append("input path=").append(entry.filePath()).append("\n");
+        writer.append("input line=").append(String.valueOf(entry.line())).append("\n");
+
+        Approvals.verify(writer.toString());
+    }
+
+    @Test
+    public void shouldGetSourceResult() throws Exception {
+        var result =
+                DebugParser.getSourceResult(
+                        RustParserTest.class.getResourceAsStream("/compiled/count_vowels.rs.wasm"));
+        assertNotNull(result);
+        assertNull(result.error);
+        assertFalse(result.units.isEmpty());
+        assertFalse(result.lines.isEmpty());
+        assertFalse(result.functions.isEmpty());
+    }
+
+    @Test
+    public void shouldParseWasmSourceInfo() throws Exception {
+
+        // this file does not contain debug info
+        var module = Parser.parse(new File("./rust/target/wasm32-wasip1/release/dwarf-rust.wasm"));
+        var result = DebugParser.parse(module);
+        assertNotNull(result);
+        assertTrue(result.isEmpty());
+    }
+}
diff --git a/dwarf-rust-experimental/src/test/resources/com/dylibso/chicory/dwarf/rust/RustParserTest.shouldParseCountVowels.approved.txt b/dwarf-rust-experimental/src/test/resources/com/dylibso/chicory/dwarf/rust/RustParserTest.shouldParseCountVowels.approved.txt
new file mode 100644
index 000000000..21db6183b
--- /dev/null
+++ b/dwarf-rust-experimental/src/test/resources/com/dylibso/chicory/dwarf/rust/RustParserTest.shouldParseCountVowels.approved.txt
@@ -0,0 +1,3 @@
+input file=error.rs
+input path=/rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/str/error.rs
+input line=45
diff --git a/machine-tests/pom.xml b/machine-tests/pom.xml
index 3825c1129..5993046ca 100644
--- a/machine-tests/pom.xml
+++ b/machine-tests/pom.xml
@@ -53,6 +53,13 @@
       
         com.dylibso.chicory
         chicory-compiler-maven-plugin
+        
+          
+            com.dylibso.chicory
+            dwarf-rust-experimental
+            ${project.version}
+          
+        
         
           
             compile-quickjs
@@ -84,6 +91,19 @@
               ${project.basedir}/../wabt/src/main/resources/wat2wasm
             
           
+          
+            compile-count-vowels
+            
+              compile
+            
+            
+              com.dylibso.chicory.testing.gen.CountVowels
+              ${project.basedir}/../wasm-corpus/src/main/resources/compiled/count_vowels.rs.wasm
+              
+                54
+              
+            
+          
         
       
     
diff --git a/machine-tests/src/test/java/com/dylibso/chicory/testing/MachinesTest.java b/machine-tests/src/test/java/com/dylibso/chicory/testing/MachinesTest.java
index fc1d09b89..2438a6203 100644
--- a/machine-tests/src/test/java/com/dylibso/chicory/testing/MachinesTest.java
+++ b/machine-tests/src/test/java/com/dylibso/chicory/testing/MachinesTest.java
@@ -15,6 +15,7 @@
 import com.dylibso.chicory.runtime.Store;
 import com.dylibso.chicory.runtime.TableInstance;
 import com.dylibso.chicory.runtime.TrapException;
+import com.dylibso.chicory.testing.gen.CountVowels;
 import com.dylibso.chicory.testing.gen.DynamicHelloJS;
 import com.dylibso.chicory.testing.gen.QuickJS;
 import com.dylibso.chicory.wabt.Wat2Wasm;
@@ -32,6 +33,8 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.nio.file.FileSystem;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
@@ -279,4 +282,31 @@ public void shouldCallIndirectAotToInterpreter() {
         var className = ex.getStackTrace()[0].getClassName();
         assertTrue(className.contains("InterpreterMachine"), className);
     }
+
+    private String readStackTrace(Throwable t) {
+        StringWriter sw = new StringWriter();
+        t.printStackTrace(new PrintWriter(sw));
+        return sw.toString();
+    }
+
+    @Test
+    public void shouldEmitUnderstandableStackTracesCompiledAndInterpreted() throws Exception {
+        WasmModule module = CountVowels.load();
+        var instance = Instance.builder(module).withMachineFactory(CountVowels::create).build();
+        var countVowels = instance.export("count_vowels");
+        var exception = assertThrows(TrapException.class, () -> countVowels.apply(0, -1));
+        var exceptionTxt = readStackTrace(exception);
+        System.out.println(exceptionTxt);
+
+        assertTrue(
+                exceptionTxt.contains(
+                        "at com.dylibso.chicory.testing.gen.CountVowelsMachineFuncGroup_0.func_30(/rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/ub_checks.rs:68)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at 0x00029b: chicory"
+                            + " interpreter.from_raw_parts(/rustc/17067e9ac6d7ecb70e50f92c1944e545188d2359/library/core/src/ub_checks.rs:75)"));
+        assertTrue(
+                exceptionTxt.contains(
+                        "at com.dylibso.chicory.testing.gen.CountVowelsMachineFuncGroup_0.count_vowels(src/lib.rs:23)"));
+    }
 }
diff --git a/pom.xml b/pom.xml
index 232da7ace..04953d96e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -868,6 +868,7 @@
         compiler-maven-plugin
         compiler-tests
         docs-lib
+        dwarf-rust-experimental
         fuzz
         jmh
         log
@@ -902,6 +903,7 @@
         cli
         compiler
         compiler-maven-plugin
+        dwarf-rust-experimental
         log
         runtime
         wabt
diff --git a/runtime/pom.xml b/runtime/pom.xml
index 18044188b..d63c4f2e2 100644
--- a/runtime/pom.xml
+++ b/runtime/pom.xml
@@ -23,6 +23,12 @@
       wasm-corpus
       test
     
+    
+      io.github.java-diff-utils
+      java-diff-utils
+      4.15
+      test
+    
     
       org.junit.jupiter
       junit-jupiter-api
diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/DebugParser.java b/runtime/src/main/java/com/dylibso/chicory/runtime/DebugParser.java
new file mode 100644
index 000000000..b29f4fe14
--- /dev/null
+++ b/runtime/src/main/java/com/dylibso/chicory/runtime/DebugParser.java
@@ -0,0 +1,6 @@
+package com.dylibso.chicory.runtime;
+
+import com.dylibso.chicory.wasm.WasmModule;
+import java.util.function.Function;
+
+public interface DebugParser extends Function {}
diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java b/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java
index 693ec5324..1e92c62c8 100644
--- a/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java
+++ b/runtime/src/main/java/com/dylibso/chicory/runtime/Instance.java
@@ -69,6 +69,8 @@ public class Instance {
 
     private final Map exnRefs;
 
+    DebugParser debugParser;
+
     Instance(
             WasmModule module,
             Global[] globalInitializers,
@@ -85,8 +87,10 @@ public class Instance {
             Function machineFactory,
             boolean initialize,
             boolean start,
-            ExecutionListener listener) {
+            ExecutionListener listener,
+            DebugParser debugParser) {
         this.module = module;
+        this.debugParser = debugParser;
         this.globalInitializers = globalInitializers.clone();
         this.globals = new GlobalInstance[globalInitializers.length];
         this.memory = memory;
@@ -358,6 +362,7 @@ public static final class Builder {
         private ExecutionListener listener;
         private ImportValues importValues;
         private Function machineFactory;
+        private DebugParser debugParser;
 
         private Builder(WasmModule module) {
             this.module = Objects.requireNonNull(module);
@@ -373,6 +378,11 @@ public Builder withStart(boolean s) {
             return this;
         }
 
+        public Builder withDebugParser(DebugParser debugParser) {
+            this.debugParser = debugParser;
+            return this;
+        }
+
         public Builder withMemoryLimits(MemoryLimits limits) {
             this.memoryLimits = limits;
             return this;
@@ -884,7 +894,12 @@ public Instance build() {
                     machineFactory,
                     initialize,
                     start,
-                    listener);
+                    listener,
+                    debugParser);
         }
     }
+
+    public DebugParser debugParser() {
+        return debugParser;
+    }
 }
diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java b/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java
index 7222009c5..b82456e57 100644
--- a/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java
+++ b/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java
@@ -4,17 +4,23 @@
 import static com.dylibso.chicory.wasm.types.Value.REF_NULL_VALUE;
 import static java.util.Objects.requireNonNullElse;
 
+import com.dylibso.chicory.runtime.internal.CompilerInterpreterMachine;
 import com.dylibso.chicory.wasm.ChicoryException;
 import com.dylibso.chicory.wasm.types.AnnotatedInstruction;
 import com.dylibso.chicory.wasm.types.CatchOpCode;
+import com.dylibso.chicory.wasm.types.CustomSection;
 import com.dylibso.chicory.wasm.types.FunctionType;
 import com.dylibso.chicory.wasm.types.Instruction;
 import com.dylibso.chicory.wasm.types.OpCode;
+import com.dylibso.chicory.wasm.types.UnknownCustomSection;
 import com.dylibso.chicory.wasm.types.ValType;
 import com.dylibso.chicory.wasm.types.Value;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayDeque;
+import java.util.ArrayList;
 import java.util.Deque;
 import java.util.List;
+import java.util.Set;
 
 /**
  * This is responsible for holding and interpreting the Wasm code.
@@ -27,10 +33,30 @@ public class InterpreterMachine implements Machine {
 
     private final Instance instance;
 
+    private final Stratum stratum;
+
     public InterpreterMachine(Instance instance) {
         this.instance = instance;
         stack = new MStack();
         this.callStack = new ArrayDeque<>();
+
+        // The SMAP custom section only gets added to WASM modules that are created by
+        // the build time compiler (see Generator.java).
+        // The module is meta-module and will only contain code for functions which
+        // will be interpreted (typically because they were too large to compile to bytecode).
+        CustomSection smapSection = instance.module().customSection("SMAP");
+        if (smapSection instanceof UnknownCustomSection) {
+            // this occurs when the build time compiler generates a new code section
+            // as the original debug mappings are stripped.
+            var smap =
+                    new String(
+                            ((UnknownCustomSection) smapSection).bytes(), StandardCharsets.UTF_8);
+            stratum = Stratum.parseSMapString(smap);
+        } else if (instance.debugParser() != null) {
+            stratum = instance.debugParser().apply(instance.module());
+        } else {
+            stratum = Stratum.builder("WASM").build();
+        }
     }
 
     @FunctionalInterface
@@ -168,7 +194,8 @@ protected void eval(MStack stack, Instance instance, Deque callStack
             instance.onExecution(instruction, stack);
             switch (opcode) {
                 case UNREACHABLE:
-                    throw new TrapException("Trapped on unreachable instruction");
+                    THROW_UNREACHABLE(callStack);
+                    break;
                 case NOP:
                     break;
                 case LOOP:
@@ -2188,6 +2215,105 @@ private static int numberOfValuesToReturn(Instance instance, AnnotatedInstructio
         return sizeOf(instance.type(typeId).returns());
     }
 
+    private void THROW_UNREACHABLE(Deque callStack) {
+        TrapException e = new TrapException("Trapped on unreachable instruction");
+        enhanceStackTrace(e, callStack);
+        throw e;
+    }
+
+    protected void enhanceStackTrace(Throwable e, Deque callStack) {
+        int size = callStack.size();
+        var funcIds = new int[size];
+        var addresses = new int[size];
+        for (int i = 0; i < size; i++) {
+            var frame = callStack.pop();
+            funcIds[i] = frame.funcId();
+            addresses[i] = frame.currentInstruction().address();
+        }
+
+        if (stratum.isEmpty()) {
+            return;
+        }
+
+        var wasmDebugInfoAdded = false;
+        var interpreterFramesFound = false;
+        var traces = new ArrayList<>();
+
+        var interpreterClassNames =
+                Set.of(
+                        InterpreterMachine.class.getName(),
+                        CompilerInterpreterMachine.class.getName());
+        for (var trace : List.of(e.getStackTrace())) {
+
+            if (interpreterFramesFound || !interpreterClassNames.contains(trace.getClassName())) {
+                // keep all the non-interpreter frames...
+                traces.add(trace);
+                if (wasmDebugInfoAdded) {
+                    // don't replace any more interpreter frames..
+                    interpreterFramesFound = true;
+                }
+                continue;
+            }
+
+            // we eat all the interpreter frames.
+            if (wasmDebugInfoAdded) {
+                continue;
+            }
+
+            // The following happens when we reach the first interpreter frame..
+            wasmDebugInfoAdded = true;
+
+            // add the wasm debug info.
+            var codeSectionAddress = instance.module().codeSection().address();
+            for (int i = 0; i < addresses.length; i++) {
+
+                var funcId = funcIds[i];
+
+                // A Stratum contains line and function mappings for the code section.
+                // A function mapping is:
+                //
+                //    [ start_address, end_address, function_name ]
+                //
+                // A line mapping is:
+                //
+                //    [ inputLineStart, inputLineCount, inputFileID, outputLineStart,
+                // outputLineCount ]
+                //
+                // where and addresses and outputLineStart is the WASM instruction address relative
+                // to
+                // the
+                // start of the code section address.
+
+                // This address is relative to the start of the wasm module.  We display this as
+                // it will match the output of `wasm-tools dump` and other tools.
+                var address = addresses[i];
+
+                // Convert to an address relative to the start of code section.
+                int addressRelativeToCodeSection = address - codeSectionAddress;
+                var functionName = stratum.getFunctionMapping(addressRelativeToCodeSection);
+                var lineMapping = stratum.getInputLine(addressRelativeToCodeSection);
+
+                String fileName = null;
+                int line = 0;
+                if (lineMapping != null) {
+                    fileName = lineMapping.filePath();
+                    line = (int) lineMapping.line();
+                } else {
+                    fileName = "{wasm}";
+                    line = address;
+                }
+
+                String className = String.format("0x%06x: chicory interpreter", address);
+                if (functionName == null) {
+                    functionName = String.format("func_%d", funcId);
+                }
+                traces.add(new StackTraceElement(className, functionName, fileName, line));
+            }
+        }
+
+        e.setStackTrace(traces.toArray(new StackTraceElement[0]));
+    }
+
     protected static StackFrame THROW_REF(
             Instance instance,
             int exceptionIdx,
diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/LineMapping.java b/runtime/src/main/java/com/dylibso/chicory/runtime/LineMapping.java
new file mode 100644
index 000000000..931543862
--- /dev/null
+++ b/runtime/src/main/java/com/dylibso/chicory/runtime/LineMapping.java
@@ -0,0 +1,132 @@
+package com.dylibso.chicory.runtime;
+
+public class LineMapping {
+
+    private final int lineFileID;
+    private final long inputStartLine;
+    private final long outputStartLine;
+    private final int inputLineCount;
+    private final int outputLineCount;
+
+    public LineMapping(
+            int lineFileID,
+            long inputStartLine,
+            long outputStartLine,
+            int inputLineCount,
+            int outputLineCount) {
+        this.inputLineCount = inputLineCount;
+        this.outputLineCount = outputLineCount;
+        this.lineFileID = lineFileID;
+        this.inputStartLine = inputStartLine;
+        this.outputStartLine = outputStartLine;
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    public Builder toBuilder() {
+        Builder builder = new Builder();
+        builder.lineFileID = lineFileID;
+        builder.inputStartLine = inputStartLine;
+        builder.outputStartLine = outputStartLine;
+        builder.inputLineCount = inputLineCount;
+        builder.outputLineCount = outputLineCount;
+        return builder;
+    }
+
+    public static final class Builder {
+        int lineFileID;
+        long inputStartLine = -1;
+        long outputStartLine = -1;
+        int inputLineCount = 1;
+        int outputLineCount = 1;
+
+        private Builder() {}
+
+        public Builder withInputStartLine(long inputStartLine) {
+            if (inputStartLine < 0) {
+                throw new IllegalArgumentException("" + inputStartLine);
+            }
+            this.inputStartLine = inputStartLine;
+            return this;
+        }
+
+        public Builder withOutputStartLine(long outputStartLine) {
+            if (outputStartLine < 0) {
+                throw new IllegalArgumentException("" + outputStartLine);
+            }
+            this.outputStartLine = outputStartLine;
+            return this;
+        }
+
+        public Builder withLineFileID(int lineFileID) {
+            if (lineFileID < 0) {
+                throw new IllegalArgumentException("" + lineFileID);
+            }
+            this.lineFileID = lineFileID;
+            return this;
+        }
+
+        public Builder withInputLineCount(int value) {
+            if (value < 0) {
+                throw new IllegalArgumentException("" + value);
+            }
+            this.inputLineCount = (int) value;
+            return this;
+        }
+
+        public Builder withOutputLineCount(int value) {
+            if (value < 0) {
+                throw new IllegalArgumentException("" + value);
+            }
+            this.outputLineCount = (int) value;
+            return this;
+        }
+
+        public LineMapping build() {
+            return new LineMapping(
+                    lineFileID, inputStartLine, outputStartLine, inputLineCount, outputLineCount);
+        }
+
+        public long inputStartLine() {
+            return inputStartLine;
+        }
+
+        public long outputStartLine() {
+            return outputStartLine;
+        }
+
+        public int lineFileID() {
+            return lineFileID;
+        }
+
+        public int inputLineCount() {
+            return inputLineCount;
+        }
+
+        public int outputLineCount() {
+            return outputLineCount;
+        }
+    }
+
+    public long inputStartLine() {
+        return inputStartLine;
+    }
+
+    public long outputStartLine() {
+        return outputStartLine;
+    }
+
+    public int lineFileID() {
+        return lineFileID;
+    }
+
+    public int inputLineCount() {
+        return inputLineCount;
+    }
+
+    public int outputLineCount() {
+        return outputLineCount;
+    }
+}
diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/ParserException.java b/runtime/src/main/java/com/dylibso/chicory/runtime/ParserException.java
new file mode 100644
index 000000000..79ce37111
--- /dev/null
+++ b/runtime/src/main/java/com/dylibso/chicory/runtime/ParserException.java
@@ -0,0 +1,7 @@
+package com.dylibso.chicory.runtime;
+
+public class ParserException extends RuntimeException {
+    public ParserException(String message) {
+        super(message);
+    }
+}
diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java b/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java
index 4287fb584..3c5cd7860 100644
--- a/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java
+++ b/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java
@@ -215,4 +215,8 @@ static void doControlTransfer(CtrlFrame ctrlFrame, MStack stack) {
             stack.push(value);
         }
     }
+
+    public AnnotatedInstruction currentInstruction() {
+        return currentInstruction;
+    }
 }
diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/Stratum.java b/runtime/src/main/java/com/dylibso/chicory/runtime/Stratum.java
new file mode 100644
index 000000000..710b231ae
--- /dev/null
+++ b/runtime/src/main/java/com/dylibso/chicory/runtime/Stratum.java
@@ -0,0 +1,365 @@
+package com.dylibso.chicory.runtime;
+
+import com.dylibso.chicory.runtime.internal.smap.Smap;
+import com.dylibso.chicory.runtime.internal.smap.SmapParser;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Stratum represents a mapping of source code lines to function names and addresses.
+ * 

+ * It is used to manage the relationship between input source files and their corresponding output lines, + * as well as to provide a way to retrieve function mappings based on line numbers. + *

+ */ +public final class Stratum { + + private final List fileNameList; + private final List filePathList; + private final List lineData; + private final List functionData; + + public static final class FunctionMapping { + private final String functionName; + private final long startLine; + private final long endLine; + + private FunctionMapping(String functionName, long startLine, long endLine) { + this.functionName = functionName; + this.startLine = startLine; + this.endLine = endLine; + } + + public String getFunctionName() { + return functionName; + } + + public long getStartLine() { + return startLine; + } + + public long getEndLine() { + return endLine; + } + } + + /** + * Creates a stratum builder with the specified name. + * + * @param name the name of the stratum + * @return a new Stratum instance + */ + public static Builder builder(String name) { + return new Builder(name); + } + + public static final class Builder { + private final String stratumName; + private final List fileNameList = new ArrayList<>(); + private final List filePathList = new ArrayList<>(); + private final HashMap filePathIdx = new HashMap<>(); + private final List lineData = new ArrayList<>(); + private final List functionData = new ArrayList<>(); + + private Builder(String name) { + this.stratumName = name; + } + + public List lineData() { + return lineData; + } + + public List fileNameList() { + return fileNameList; + } + + public List filePathList() { + return filePathList; + } + + public String stratumName() { + return stratumName; + } + + public List functionData() { + return functionData; + } + + public boolean isEmpty() { + return lineData.isEmpty(); + } + + public void clear() { + fileNameList.clear(); + filePathList.clear(); + filePathIdx.clear(); + lineData.clear(); + functionData.clear(); + } + + // ********************************************************************* + // Methods to add mapping information + + public Builder withFunctionMapping(String functionName, long startLine, long endLine) { + functionData.add(new Stratum.FunctionMapping(functionName, startLine, endLine)); + return this; + } + + /** + * Adds record of a new file, by filename and path. The path + * may be relative to a source compilation path. + * + * @param filename the filename to add, unqualified by path + * @param filePath the path for the filename, potentially relative + * to a source compilation path + */ + public int addFile(String filename, String filePath) { + var idx = filePathIdx.get(filePath); + if (idx != null) { + return idx; + } + var i = filePathList.size(); + filePathIdx.put(filePath, i); + filePathList.add(filePath); + fileNameList.add(filename); + return i; + } + + /** + * Adds complete information about a simple line mapping. Specify + * all the fields in this method; the back-end machinery takes care + * of printing only those that are necessary in the final SMAP. + * (My view is that fields are optional primarily for spatial efficiency, + * not for programmer convenience. Could always add utility methods + * later.) + * + * @param inputStartLine starting line in the source file + * (SMAP InputStartLine) + * @param inputFileName file name of the input file, unqualified by path + * @param inputFilePath the filepath (or name) from which the input comes + * (yields SMAP LineFileID) Use unqualified names + * carefully, and only when they uniquely identify a file. + * @param inputLineCount the number of lines in the input to map + * (SMAP LineFileCount) + * @param outputStartLine starting line in the output file + * (SMAP OutputStartLine) + * @param outputLineCount number of output lines to map to each + * input line (SMAP OutputLineIncrement). Given the + * fact that the name starts with "output", I continuously have + * the subconscious urge to call this field + * OutputLineExcrement. + */ + public Builder withLineMapping( + String inputFileName, + String inputFilePath, + long inputStartLine, + int inputLineCount, + long outputStartLine, + int outputLineCount) { + + if (outputStartLine == 0) { + throw new IllegalArgumentException("outputStartLine must be > 0"); + } + + int lineFileID = addFile(inputFileName, inputFilePath); + + // can we merge it into the previous line? + if (!lineData.isEmpty()) { + int i = lineData.size() - 1; + var li = lineData.get(i); + + // is this just increasing the input line count? + if (li.lineFileID == lineFileID + && inputStartLine == li.inputStartLine + li.inputLineCount + && outputLineCount == li.outputLineCount + && outputStartLine + == li.outputStartLine + + ((long) li.inputLineCount * li.outputLineCount)) { + li.withInputLineCount(li.inputLineCount + inputLineCount); + return this; + } + + // is this just increasing the output line count? + if (li.lineFileID == lineFileID + && inputStartLine == li.inputStartLine + && inputLineCount == 1 + && li.inputLineCount == 1 + && outputStartLine + == li.outputStartLine + + (long) li.inputLineCount * li.outputLineCount) { + li.withOutputLineCount( + (int) (outputStartLine - li.outputStartLine + outputLineCount)); + return this; + } + } + + // Add a new LineInfo + var li = LineMapping.builder(); + li.withInputStartLine(inputStartLine); + li.withInputLineCount(inputLineCount); + li.withOutputStartLine(outputStartLine); + li.withOutputLineCount(outputLineCount); + li.withLineFileID(lineFileID); + lineData.add(li); + return this; + } + + public Stratum build() { + + // Optimize the line data for lookups. + lineData.sort(Comparator.comparingLong(x -> x.outputStartLine)); + functionData.sort(Comparator.comparingLong(x -> x.startLine)); + + List builtLineData = new ArrayList<>(); + for (LineMapping.Builder lineDatum : lineData) { + builtLineData.add(lineDatum.build()); + } + return new Stratum(fileNameList, filePathList, builtLineData, functionData); + } + } + + /** + * Converts the given source file and stratum into a SMAP string as defined by the JSR-045 spec. + * + * @param sourceFile the name of the source file + * @param stratum the stratum to convert + * @return a string representation of the SMap + */ + public static String toSMapString(String sourceFile, Stratum.Builder stratum) { + return new Smap().withOutputFileName(sourceFile).withStratum(stratum, true).toString(); + } + + /** + * Parses the given SMAP string (as defined by the JSR-045 spec) and returns the + * default Stratum instance. + * + * @param smap the SMAP string to parse + * @return a Stratum instance representing the parsed SMAP + */ + public static Stratum parseSMapString(String smap) { + return SmapParser.parse(smap).getDefaultStratum().build(); + } + + private Stratum( + List fileNameList, + List filePathList, + List lineData, + List functionData) { + this.fileNameList = List.copyOf(fileNameList); + this.filePathList = List.copyOf(filePathList); + this.lineData = List.copyOf(lineData); + this.functionData = List.copyOf(functionData); + } + + public boolean isEmpty() { + return lineData.isEmpty(); + } + + public String getFunctionMapping(int outputLine) { + // do a ranged binary search on functionData to find the FunctionMapping that contains the + // outputLine + int left = 0; + int right = functionData.size() - 1; + + while (left <= right) { + int mid = left + (right - left) / 2; + Stratum.FunctionMapping midInfo = functionData.get(mid); + + if (outputLine >= midInfo.getStartLine() && outputLine <= midInfo.getEndLine()) { + return midInfo.getFunctionName(); + } else if (outputLine < midInfo.getStartLine()) { + right = mid - 1; + } else { + left = mid + 1; + } + } + return null; + } + + public static final class Line { + private final String fileName; + private final String filePath; + private final long line; + private final int count; + + public Line(String fileName, String filePath, long line, int count) { + this.fileName = fileName; + this.filePath = filePath; + this.line = line; + this.count = count; + } + + public String fileName() { + return fileName; + } + + public String filePath() { + return filePath; + } + + public long line() { + return line; + } + + public int count() { + return count; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + Line line1 = (Line) o; + return line == line1.line + && count == line1.count + && Objects.equals(fileName, line1.fileName) + && Objects.equals(filePath, line1.filePath); + } + + @Override + public int hashCode() { + return Objects.hash(fileName, filePath, line, count); + } + } + + public Line getInputLine(int outputLine) { + var l = getLineMapping(outputLine); + if (l != null) { + String fileName = fileNameList.get(l.lineFileID()); + String filePath = filePathList.get(l.lineFileID()); + return new Line(fileName, filePath, l.inputStartLine(), l.inputLineCount()); + } + return null; + } + + /** + * does a binary search for the LineMapping that contains the outputLine + */ + private LineMapping getLineMapping(int outputLine) { + // do a ranged binary search on lineData to find the LineInfo that contains the outputLine + int left = 0; + int right = lineData.size() - 1; + + while (left <= right) { + int mid = left + (right - left) / 2; + LineMapping midInfo = lineData.get(mid); + + long outputStart = midInfo.outputStartLine(); + long outputEnd = + outputStart + ((long) midInfo.inputLineCount() * midInfo.outputLineCount()) - 1; + + if (outputLine >= outputStart && outputLine <= outputEnd) { + return midInfo; + } else if (outputLine < outputStart) { + right = mid - 1; + } else { + left = mid + 1; + } + } + return null; + } +} diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/TrapException.java b/runtime/src/main/java/com/dylibso/chicory/runtime/TrapException.java index a7155687b..f241a892a 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/TrapException.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/TrapException.java @@ -3,7 +3,8 @@ import com.dylibso.chicory.wasm.ChicoryException; public class TrapException extends ChicoryException { - public TrapException(String msg) { - super(msg); + + public TrapException(String trappedOnUnreachableInstruction) { + super(trappedOnUnreachableInstruction); } } diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/internal/CompilerInterpreterMachine.java b/runtime/src/main/java/com/dylibso/chicory/runtime/internal/CompilerInterpreterMachine.java index 0af6bfe96..d14144529 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/internal/CompilerInterpreterMachine.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/internal/CompilerInterpreterMachine.java @@ -4,6 +4,7 @@ import com.dylibso.chicory.runtime.InterpreterMachine; import com.dylibso.chicory.runtime.MStack; import com.dylibso.chicory.runtime.StackFrame; +import com.dylibso.chicory.runtime.TrapException; import com.dylibso.chicory.runtime.WasmException; import com.dylibso.chicory.wasm.ChicoryException; import com.dylibso.chicory.wasm.types.FunctionType; @@ -82,6 +83,9 @@ protected void CALL(Operands operands) { stack.push(result); } } + } catch (TrapException e) { + enhanceStackTrace(e, callStack); + throw e; } catch (WasmException e) { // we need at least an empty frame var stackFrame = new StackFrame(instance, funcId, args); diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/internal/smap/Smap.java b/runtime/src/main/java/com/dylibso/chicory/runtime/internal/smap/Smap.java new file mode 100644 index 000000000..17caea0ba --- /dev/null +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/internal/smap/Smap.java @@ -0,0 +1,286 @@ +// Original came from: +// https://github.com/apache/sling-org-apache-sling-scripting-jsp/blob/4e0f12aab9c42a1475587800cefe6a39721020ec/src/main/java/org/apache/sling/scripting/jsp/jasper/compiler/SmapGenerator.java +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 com.dylibso.chicory.runtime.internal.smap; + +import com.dylibso.chicory.runtime.LineMapping; +import com.dylibso.chicory.runtime.Stratum; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a source map (SMAP), which serves to associate lines + * of the input JSP file(s) to lines in the generated servlet in the + * final .class file, according to the JSR-045 spec. + * + * @author Shawn Bayern + */ +public class Smap { + + public static final String FUNCTIONS_VENDOR_ID = "com.dylibso.chicory.functions"; + + /* + * The SMAP syntax is reasonably straightforward. The purpose of this + * class is currently twofold: + * - to provide a simple but low-level Java interface to build + * a logical SMAP + * - to serialize this logical SMAP for eventual inclusion directly + * into a .class file. + */ + + private String outputFileName; + private String defaultStratum = "Java"; + private final List strata = new ArrayList<>(); + private final List embedded = new ArrayList<>(); + private boolean doEmbedded = true; + + /** + * Sets the filename (without path information) for the generated + * source file. E.g., "foo$jsp.java". + */ + public Smap withOutputFileName(String x) { + outputFileName = x; + return this; + } + + /** + * Adds the given Stratum.Builder object, representing a Stratum.Builder with + * logically associated FileSection and LineSection blocks, to + * the current Smap. If default is true, this + * stratum is made the default stratum, overriding any previously + * set default. + * + * @param stratum the Stratum.Builder object to add + * @param defaultStratum if true, this Stratum.Builder is considered + * to represent the default SMAP stratum unless + * overwritten + */ + public Smap withStratum(Stratum.Builder stratum, boolean defaultStratum) { + strata.add(stratum); + if (defaultStratum) { + this.defaultStratum = stratum.stratumName(); + } + return this; + } + + /** + * Adds the given string as an embedded SMAP with the given stratum name. + * + * @param smap the SMAP to embed + * @param stratumName the name of the stratum output by the compilation + * that produced the smap to be embedded + */ + public void addSmap(String smap, String stratumName) { + embedded.add("*O " + stratumName + "\n" + smap + "*C " + stratumName + "\n"); + } + + /** + * Instructs the Smap whether to actually print any embedded + * SMAPs or not. Intended for situations without an SMAP resolver. + * + * @param status If false, ignore any embedded SMAPs. + */ + public void setDoEmbedded(boolean status) { + doEmbedded = status; + } + + // ********************************************************************* + // Methods for serializing the logical SMAP + + @Override + public String toString() { + // check state and initialize buffer + if (outputFileName == null) { + throw new IllegalStateException(); + } + StringBuilder out = new StringBuilder(); + + // start the SMAP + out.append("SMAP\n"); + out.append(outputFileName).append('\n'); + out.append(defaultStratum).append('\n'); + + // include embedded SMAPs + if (doEmbedded) { + for (String s : embedded) { + out.append(s); + } + } + + // print our StratumSections, FileSections, and LineSections + for (Stratum.Builder s : strata) { + out.append(stratumToString(s)); + } + + if (!getDefaultStratum().functionData().isEmpty()) { + out.append(toVendorString(getDefaultStratum())); + } + + // end the SMAP + out.append("*E\n"); + + return out.toString(); + } + + private Stratum.Builder optimize(Stratum.Builder builder) { + var lineData = builder.lineData(); + // Incorporate each LineInfo into the previous LineInfo's + // outputLineCount, if possible + int i = 0; + while (i < lineData.size() - 1) { + var li = lineData.get(i); + var liNext = lineData.get(i + 1); + if (li.lineFileID() == liNext.lineFileID() + && liNext.inputStartLine() == li.inputStartLine() + && liNext.inputLineCount() == 1 + && li.inputLineCount() == 1 + && liNext.outputStartLine() + == li.outputStartLine() + + (long) li.inputLineCount() * li.outputLineCount()) { + li.withOutputLineCount( + (int) + (liNext.outputStartLine() + - li.outputStartLine() + + liNext.outputLineCount())); + lineData.remove(i + 1); + } else { + i++; + } + } + + // Incorporate each LineInfo into the previous LineInfo's + // inputLineCount, if possible + i = 0; + while (i < lineData.size() - 1) { + var li = lineData.get(i); + var liNext = lineData.get(i + 1); + if (li.lineFileID() == liNext.lineFileID() + && liNext.inputStartLine() == li.inputStartLine() + li.inputLineCount() + && liNext.outputLineCount() == li.outputLineCount() + && liNext.outputStartLine() + == li.outputStartLine() + + (long) li.inputLineCount() * li.outputLineCount()) { + li.withInputLineCount(li.inputLineCount() + liNext.inputLineCount()); + lineData.remove(i + 1); + } else { + i++; + } + } + return builder; + } + + private String stratumToString(Stratum.Builder builder) { + + var s = optimize(builder); + // check state and initialize buffer + var fileNameList = s.fileNameList(); + var filePathList = s.filePathList(); + var lineData = s.lineData(); + + if (fileNameList.isEmpty() || lineData.isEmpty()) { + return ""; + } + + StringBuilder out = new StringBuilder(); + + // print StratumSection + out.append("*S ").append(s.stratumName()).append("\n"); + + // print FileSection + out.append("*F\n"); + int bound = fileNameList.size(); + for (int i = 0; i < bound; i++) { + String fileName = fileNameList.get(i); + String filePath = filePathList.get(i); + if (!fileName.equals(filePath)) { + out.append("+ ").append(i).append(" ").append(fileName).append("\n"); + out.append(filePath).append("\n"); + } else { + out.append(i).append(" ").append(fileName).append("\n"); + } + } + + // print LineSection + out.append("*L\n"); + bound = lineData.size(); + int lastFileID = 0; + + for (int i = 0; i < bound; i++) { + LineMapping.Builder li = lineData.get(i); + int fileID = li.lineFileID(); + var includeFileID = fileID != lastFileID; + out.append(getString(li, includeFileID)); + lastFileID = fileID; + } + + return out.toString(); + } + + private String getString(LineMapping.Builder line, boolean includeFileID) { + if (line.inputStartLine() == -1 || line.outputStartLine() == -1) { + throw new IllegalStateException(); + } + StringBuilder out = new StringBuilder(); + out.append(line.inputStartLine()); + if (includeFileID) { + out.append("#").append(line.lineFileID()); + } + if (line.inputLineCount() != 1) { + out.append(",").append(line.inputLineCount()); + } + out.append(":").append(line.outputStartLine()); + if (line.outputLineCount() != 1) { + out.append(",").append(line.outputLineCount()); + } + out.append('\n'); + return out.toString(); + } + + private String toVendorString(Stratum.Builder stratum) { + StringBuilder out = new StringBuilder(); + + // print StratumSection + + out.append("*V\n").append(Smap.FUNCTIONS_VENDOR_ID).append("\n"); + + for (Stratum.FunctionMapping fm : stratum.functionData()) { + String escapedFunctionName = fm.getFunctionName().replace('\n', '_'); + out.append(fm.getStartLine()) + .append(",") + .append(fm.getEndLine()) + .append("=") + .append(escapedFunctionName) + .append("\n"); + } + return out.toString(); + } + + public Stratum.Builder getDefaultStratum() { + for (Stratum.Builder s : strata) { + if (defaultStratum.equals(s.stratumName())) { + return s; + } + } + return null; + } + + public List getStrata() { + return strata; + } +} diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/internal/smap/SmapParser.java b/runtime/src/main/java/com/dylibso/chicory/runtime/internal/smap/SmapParser.java new file mode 100644 index 000000000..a049910df --- /dev/null +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/internal/smap/SmapParser.java @@ -0,0 +1,395 @@ +package com.dylibso.chicory.runtime.internal.smap; + +import static com.dylibso.chicory.runtime.internal.smap.Smap.FUNCTIONS_VENDOR_ID; + +import com.dylibso.chicory.runtime.ParserException; +import com.dylibso.chicory.runtime.Stratum; +import java.util.HashMap; +import java.util.Map; + +/** + * Parser for SMAP (Source Map) strings according to JSR-045 specification. + * Converts SMAP format into Smap objects. + */ +public final class SmapParser { + + private SmapParser() { + // Utility class - prevent instantiation + } + + /** + * Parses an SMAP string and returns a Smap object. + * + * @param smapString the SMAP string to parse + * @return a Smap object representing the parsed SMAP + * @throws ParserException if the SMAP string is invalid + */ + public static Smap parse(String smapString) throws ParserException { + if (smapString == null || smapString.trim().isEmpty()) { + throw new ParserException("SMAP string cannot be null or empty"); + } + + String[] lines = smapString.split("\\r?\\n"); + int lineIndex = 0; + + Smap generator = new Smap(); + + // Parse header + lineIndex = parseHeader(lines, lineIndex, generator); + + // Parse embedded SMAPs and sections + parseSections(lines, lineIndex, generator); + + return generator; + } + + private static int parseHeader(String[] lines, int lineIndex, Smap generator) + throws ParserException { + // First line should be "SMAP" + if (lineIndex >= lines.length || !lines[lineIndex].trim().equals("SMAP")) { + throw new ParserException("Expected 'SMAP' at line " + (lineIndex + 1)); + } + lineIndex++; + + // Second line is output filename + if (lineIndex >= lines.length) { + throw new ParserException("Expected output filename at line " + (lineIndex + 1)); + } + generator.withOutputFileName(lines[lineIndex].trim()); + lineIndex++; + + // Third line is default stratum + if (lineIndex >= lines.length) { + throw new ParserException("Expected default stratum at line " + (lineIndex + 1)); + } + // Note: default stratum is set when we add the first stratum with defaultStratum=true + // We read but don't need to store the default stratum from the header + lineIndex++; + + return lineIndex; + } + + private static void parseSections(String[] lines, int lineIndex, Smap generator) + throws ParserException { + while (lineIndex < lines.length) { + String line = lines[lineIndex].trim(); + + if (line.equals("*E")) { + // End section + break; + } else if (line.startsWith("*O ")) { + // Embedded SMAP open section + lineIndex = parseEmbeddedSmap(lines, lineIndex, generator); + } else if (line.startsWith("*S ")) { + // Stratum.Builder section + lineIndex = parseStratumSection(lines, lineIndex, generator); + } else if (line.startsWith("*V")) { + // Vendor section - skip for now + lineIndex = parseVendorSection(lines, lineIndex, generator); + } else if (line.startsWith("*")) { + // Unknown section - skip + lineIndex = skipUnknownSection(lines, lineIndex); + } else { + lineIndex++; + } + } + } + + private static int parseEmbeddedSmap(String[] lines, int lineIndex, Smap generator) + throws ParserException { + String openLine = lines[lineIndex].trim(); + if (!openLine.startsWith("*O ")) { + throw new ParserException( + "Expected embedded SMAP open section at line " + (lineIndex + 1)); + } + + String stratumName = openLine.substring(3).trim(); + lineIndex++; + + // Find the embedded SMAP content + StringBuilder embeddedSmapBuilder = new StringBuilder(); + boolean foundClosing = false; + + while (lineIndex < lines.length) { + String line = lines[lineIndex]; + + if (line.trim().equals("*C " + stratumName)) { + foundClosing = true; + lineIndex++; + break; + } + + embeddedSmapBuilder.append(line).append("\n"); + lineIndex++; + } + + if (!foundClosing) { + throw new ParserException("Missing closing section *C " + stratumName); + } + + generator.addSmap(embeddedSmapBuilder.toString(), stratumName); + return lineIndex; + } + + private static int parseStratumSection(String[] lines, int lineIndex, Smap generator) + throws ParserException { + String stratumLine = lines[lineIndex].trim(); + if (!stratumLine.startsWith("*S ")) { + throw new ParserException("Expected stratum section at line " + (lineIndex + 1)); + } + + String stratumName = stratumLine.substring(3).trim(); + Stratum.Builder stratum = Stratum.builder(stratumName); + lineIndex++; + + // Parse file and line sections for this stratum + Map fileIdToPath = new HashMap<>(); + Map fileIdToName = new HashMap<>(); + boolean hasFileSection = false; + boolean hasLineSection = false; + + while (lineIndex < lines.length) { + String line = lines[lineIndex].trim(); + + if (line.equals("*F")) { + // File section + lineIndex++; + lineIndex = parseFileSection(lines, lineIndex, stratum, fileIdToPath, fileIdToName); + hasFileSection = true; + } else if (line.equals("*L")) { + // Line section + lineIndex++; + lineIndex = parseLineSection(lines, lineIndex, stratum, fileIdToPath, fileIdToName); + hasLineSection = true; + } else if (line.startsWith("*")) { + // End of this stratum section + break; + } else { + lineIndex++; + } + } + + if (hasFileSection || hasLineSection) { + // Determine if this should be the default stratum (first stratum added) + boolean isDefault = stratumName.equals("JSP") || stratumName.equals("WASM"); + generator.withStratum(stratum, isDefault); + } + + return lineIndex; + } + + private static int parseFileSection( + String[] lines, + int lineIndex, + Stratum.Builder stratum, + Map fileIdToPath, + Map fileIdToName) + throws ParserException { + while (lineIndex < lines.length) { + String line = lines[lineIndex].trim(); + + if (line.startsWith("*")) { + // End of file section + break; + } + + if (line.isEmpty()) { + lineIndex++; + continue; + } + + if (line.startsWith("+ ")) { + // File with path: + fileId fileName + String[] parts = line.substring(2).split(" ", 2); + if (parts.length < 2) { + throw new ParserException( + "Invalid file entry with path at line " + (lineIndex + 1)); + } + + int fileId = Integer.parseInt(parts[0]); + String fileName = parts[1]; + lineIndex++; + + // Next line should be the path + if (lineIndex >= lines.length) { + throw new ParserException("Expected file path at line " + (lineIndex + 1)); + } + String filePath = lines[lineIndex].trim(); + + fileIdToName.put(fileId, fileName); + fileIdToPath.put(fileId, filePath); + stratum.addFile(fileName, filePath); + + } else { + // File without path: fileId fileName + String[] parts = line.split(" ", 2); + if (parts.length < 2) { + throw new ParserException("Invalid file entry at line " + (lineIndex + 1)); + } + + int fileId = Integer.parseInt(parts[0]); + String fileName = parts[1]; + + fileIdToName.put(fileId, fileName); + fileIdToPath.put(fileId, fileName); // Use filename as path + stratum.addFile(fileName, fileName); + } + + lineIndex++; + } + + return lineIndex; + } + + private static int parseLineSection( + String[] lines, + int lineIndex, + Stratum.Builder stratum, + Map fileIdToPath, + Map fileIdToName) + throws ParserException { + int lastFileId = 0; + + while (lineIndex < lines.length) { + String line = lines[lineIndex].trim(); + + if (line.startsWith("*")) { + // End of line section + break; + } + + if (line.isEmpty()) { + lineIndex++; + continue; + } + + // Parse line info: + // InputStartLine[#FileId][,RepeatCount]:OutputStartLine[,OutputLineIncrement] + int colonIndex = line.indexOf(':'); + if (colonIndex == -1) { + throw new ParserException( + "Invalid line info format at line " + (lineIndex + 1) + ": " + line); + } + + String inputPart = line.substring(0, colonIndex); + String outputPart = line.substring(colonIndex + 1); + + // Parse input part + long inputStartLine; + int fileId = + lastFileId; // Will be overridden if specified, defaults to 0 for first file + int inputLineCount = 1; + + int hashIndex = inputPart.indexOf('#'); + if (hashIndex != -1) { + inputStartLine = Long.parseLong(inputPart.substring(0, hashIndex)); + String fileAndCount = inputPart.substring(hashIndex + 1); + + int commaIndex = fileAndCount.indexOf(','); + if (commaIndex != -1) { + fileId = Integer.parseInt(fileAndCount.substring(0, commaIndex)); + inputLineCount = Integer.parseInt(fileAndCount.substring(commaIndex + 1)); + } else { + fileId = Integer.parseInt(fileAndCount); + } + } else { + int commaIndex = inputPart.indexOf(','); + if (commaIndex != -1) { + inputStartLine = Long.parseLong(inputPart.substring(0, commaIndex)); + inputLineCount = Integer.parseInt(inputPart.substring(commaIndex + 1)); + } else { + inputStartLine = Long.parseLong(inputPart); + } + } + + // Parse output part + long outputStartLine; + int outputLineCount = 1; + + int commaIndex = outputPart.indexOf(','); + if (commaIndex != -1) { + outputStartLine = Long.parseLong(outputPart.substring(0, commaIndex)); + outputLineCount = Integer.parseInt(outputPart.substring(commaIndex + 1)); + } else { + outputStartLine = Long.parseLong(outputPart); + } + + // Add line data to stratum + String fileName = fileIdToName.get(fileId); + String filePath = fileIdToPath.get(fileId); + + if (fileName == null) { + throw new ParserException( + "Unknown file ID " + fileId + " at line " + (lineIndex + 1)); + } + + stratum.withLineMapping( + fileName, + filePath, + inputStartLine, + inputLineCount, + outputStartLine, + outputLineCount); + lastFileId = fileId; + + lineIndex++; + } + + return lineIndex; + } + + private static int parseVendorSection(String[] lines, int lineIndex, Smap generator) { + lineIndex++; // Skip *V line + + if (lineIndex >= lines.length) { + return lineIndex; // No more lines to process + } + String vendor = lines[lineIndex].trim(); + if (vendor.equals(FUNCTIONS_VENDOR_ID)) { + lineIndex++; + while (lineIndex < lines.length) { + String line = lines[lineIndex].trim(); + // parse the line. it should be in %d,%d=%s format + String[] parts = line.split("=", 2); + if (parts.length == 2) { + String[] keys = parts[0].split(",", 2); + if (keys.length == 2) { + try { + long start = Long.parseLong(keys[0]); + long end = Long.parseLong(keys[1]); + String functionName = parts[1]; + generator + .getDefaultStratum() + .withFunctionMapping(functionName, start, end); + + } catch (NumberFormatException e) { + continue; + } + } + } + + lineIndex++; + } + } else { + lineIndex = skipTillSectionEnd(lines, lineIndex); + } + return lineIndex; + } + + private static int skipUnknownSection(String[] lines, int lineIndex) { + lineIndex++; // Skip section marker line + + return skipTillSectionEnd(lines, lineIndex); + } + + private static int skipTillSectionEnd(String[] lines, int lineIndex) { + while (lineIndex < lines.length) { + String line = lines[lineIndex].trim(); + if (line.startsWith("*")) { + break; + } + lineIndex++; + } + return lineIndex; + } +} diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/internal/smap/SmapParserTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/internal/smap/SmapParserTest.java new file mode 100644 index 000000000..a0a808629 --- /dev/null +++ b/runtime/src/test/java/com/dylibso/chicory/runtime/internal/smap/SmapParserTest.java @@ -0,0 +1,84 @@ +package com.dylibso.chicory.runtime.internal.smap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.dylibso.chicory.runtime.ParserException; +import com.dylibso.chicory.wasm.io.InputStreams; +import com.github.difflib.DiffUtils; +import com.github.difflib.UnifiedDiffUtils; +import com.github.difflib.patch.Patch; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import org.junit.jupiter.api.Test; + +public class SmapParserTest { + + @Test + public void testParseSimpleExample() throws ParserException { + // Example from JSR-045 specification + String smap = + "SMAP\n" + + "HelloServlet.java\n" + + "JSP\n" + + "*S JSP\n" + + "*F\n" + + "1 Hello.jsp\n" + + "2 greeting.jsp\n" + + "*L\n" + + "1#1,5:10,2\n" + + "1#2,2:20,2\n" + + "7#1,2:24,2\n" + + "*E\n"; + + Smap generator = SmapParser.parse(smap); + assertNotNull(generator); + + assertEquals( + "SMAP\n" + + "HelloServlet.java\n" + + "JSP\n" + + "*S JSP\n" + + "*F\n" + + "0 Hello.jsp\n" + + "1 greeting.jsp\n" + + "*L\n" + + "1,5:10,2\n" + + "1#1,2:20,2\n" + + "7#0,2:24,2\n" + + "*E\n", + generator.toString()); + } + + @Test + public void testParseExampleSmap() throws ParserException, IOException { + // Read the example.smap file from test resources + try (var inputStream = getClass().getResourceAsStream("/example.smap")) { + assertNotNull(inputStream, "example.smap resource not found"); + String actual = + new String(InputStreams.readAllBytes(inputStream), StandardCharsets.UTF_8); + + Smap generator = SmapParser.parse(actual); + assertNotNull(generator); + + String regenerated = generator.toString(); + assertNotNull(regenerated); + + assertEquals(actual, regenerated, () -> diff(actual, regenerated)); + } + } + + private String diff(String expected, String actual) { + var expectedLines = List.of(expected.split("\n")); + var actualLines = List.of(actual.split("\n")); + Patch patch = DiffUtils.diff(expectedLines, actualLines); + + var x = UnifiedDiffUtils.generateUnifiedDiff("x", "x", expectedLines, patch, 3); + return "diff of expected vs actual: " + + "========================================================\n" + + String.join("\n", x) + + "\n" + + "========================================================\n"; + } +} diff --git a/runtime/src/test/resources/example.smap b/runtime/src/test/resources/example.smap new file mode 100644 index 000000000..8431d6ec7 --- /dev/null +++ b/runtime/src/test/resources/example.smap @@ -0,0 +1,448 @@ +SMAP +com/dylibso/chicory/$gen/CompiledMachine.java +WASM +*S WASM +*F ++ 0 /usr/local/lib/tinygo/src/runtime/asm_tinygowasm.S +usr/local/lib/tinygo/src/runtime/asm_tinygowasm.S ++ 1 /usr/local/lib/tinygo/src/internal/task/task_asyncify_wasm.S +usr/local/lib/tinygo/src/internal/task/task_asyncify_wasm.S ++ 2 /usr/local/lib/tinygo/src/internal/task/task_asyncify.go +usr/local/lib/tinygo/src/internal/task/task_asyncify.go ++ 3 /usr/local/lib/tinygo/src/runtime/panic.go +usr/local/lib/tinygo/src/runtime/panic.go ++ 4 /usr/local/lib/tinygo/src/runtime/runtime.go +usr/local/lib/tinygo/src/runtime/runtime.go ++ 5 /usr/local/lib/tinygo/src/runtime/memhash_leveldb.go +usr/local/lib/tinygo/src/runtime/memhash_leveldb.go ++ 6 /usr/local/lib/tinygo/src/runtime/runtime_tinygowasm.go +usr/local/lib/tinygo/src/runtime/runtime_tinygowasm.go ++ 7 /usr/local/lib/tinygo/src/runtime/print.go +usr/local/lib/tinygo/src/runtime/print.go ++ 8 /usr/local/lib/tinygo/src/runtime/gc_blocks.go +usr/local/lib/tinygo/src/runtime/gc_blocks.go ++ 9 /usr/local/lib/tinygo/src/runtime/volatile/volatile.go +usr/local/lib/tinygo/src/runtime/volatile/volatile.go ++ 10 /usr/local/lib/tinygo/src/runtime/gc_stack_portable.go +usr/local/lib/tinygo/src/runtime/gc_stack_portable.go ++ 11 /usr/local/lib/tinygo/src/runtime/gc_globals.go +usr/local/lib/tinygo/src/runtime/gc_globals.go ++ 12 /usr/local/lib/tinygo/src/runtime/arch_tinygowasm.go +usr/local/lib/tinygo/src/runtime/arch_tinygowasm.go ++ 13 /usr/local/lib/tinygo/src/runtime/arch_tinygowasm_malloc.go +usr/local/lib/tinygo/src/runtime/arch_tinygowasm_malloc.go ++ 14 /usr/local/lib/tinygo/src/runtime/hashmap.go +usr/local/lib/tinygo/src/runtime/hashmap.go ++ 15 /usr/local/lib/tinygo/src/runtime/runtime_wasm_wasi.go +usr/local/lib/tinygo/src/runtime/runtime_wasm_wasi.go ++ 16 /usr/local/lib/tinygo/src/internal/task/queue.go +usr/local/lib/tinygo/src/internal/task/queue.go ++ 17 /usr/local/lib/tinygo/src/runtime/scheduler.go +usr/local/lib/tinygo/src/runtime/scheduler.go ++ 18 /usr/local/lib/tinygo/src/runtime/scheduler_any.go +usr/local/lib/tinygo/src/runtime/scheduler_any.go ++ 19 /usr/local/lib/tinygo/src/runtime/wait_other.go +usr/local/lib/tinygo/src/runtime/wait_other.go ++ 20 /usr/local/lib/tinygo/src/runtime/algorithm.go +usr/local/lib/tinygo/src/runtime/algorithm.go ++ 21 /usr/code/tinygo/log.go +usr/code/tinygo/log.go +*L +9:1 +22#1,2:2 +25,5:4 +31,3:9 +36,3:12 +40:15 +49,3:16 +53,4:19 +58:23 +60:24 +62:25 +64:26 +73,3:27 +77,4:30 +82,7:34 +91:41 +93:42 +95:43 +97:44 +88#2:45,6 +92:51 +94:52,2 +89:54 +94:55,3 +95:58 +133#3,2:59 +58,2:61 +58#4:63 +60:64 +59:65 +58:66 +60:67 +59:68 +61:69 +45#5:70,2 +47:72 +48:73,2 +49:75 +50:76,2 +47:78 +54:79,3 +57:82,3 +60:85,2 +61:87 +62:88,2 +65:90 +143#3,2:91 +67:93 +69:94 +69#6:95 +148#3,2:96 +13#7:98 +15:99 +289,2:100 +39#6:102,3 +40:105 +39:106,2 +42:108 +45:109 +43:110,2 +44,2:112 +47:114 +39:115 +283#8:116,3 +284:119 +286:120,2 +283:122 +290:123 +294:124 +283:125 +24#9:126 +81#2:127 +283#8:128 +48#10:129,2 +13#11:131 +283#8:132 +582:133 +283:134 +621:135,2 +622:137 +625:138 +283:139 +627:140 +633:141 +182,2:142,3 +643:148 +303:149,2 +304:151 +283:152 +304:153 +308:154 +621:155 +585:156 +584,3:157 +592:160 +283:161 +585:162,2 +313:164 +326:165 +343:166 +347:167 +346:168 +350:169 +352,2:170 +359:172 +360:173,2 +361:175 +112:176,2 +41#4:178 +320#8:179 +283:180 +294:181 +491:182 +190:183 +492:184 +102:185,2 +601:187 +607:188 +608:189,2 +612:191 +491:192 +495:193 +148:194,3 +149:197,4 +165,2:201,3 +173:207 +80#12,2:208 +86:210 +214#8,2:211 +214:213 +86#12:214 +223#8:215 +214:216 +224:217 +214:218 +225:219 +215:220 +30#4:221 +502#8:222 +504,2:223 +507:225 +509:226 +510:227,2 +112:229 +137,2:230 +522:232 +136:233 +140:234,2 +141:236 +140:237,2 +530:239 +190:240 +102:241,2 +540:243 +550:244 +552:245,2 +561:247 +563:248 +566:249 +574:250,2 +575:252 +528:253 +578:254 +156,2:255,3 +161:261 +245:262,2 +242:264 +246:265,2 +242:267 +245:268 +249:269,2 +250:271 +264:272 +123,2:273 +123:275,2 +131:277 +13#13:278 +17:279,2 +13:281,2 +17:283 +467#14:284,4 +468,2:288 +24#13:290,3 +31:293 +480#14:294,4 +481:298 +362:299 +492:300 +362:301 +492:302,2 +57:304 +492:305 +371:306 +374,2:307 +362:309 +205:310,3 +206:313 +376:314 +377:315,2 +362:317 +379:318 +362:319 +379:320,3 +381:323 +383:324 +41#4:325 +212#14:326,6 +213:332 +41#4:333 +386#14:334,3 +375:337 +391:338 +379:339 +48#3,3:340 +69#6:343 +36#13:344 +38:345,2 +36:347 +38:348 +42:349 +44:350 +42:351 +50:352,2 +42:354,2 +36#4:356 +42#13:357,2 +50:359 +57:360 +102#2:361 +19#15:362,3 +198#8:365 +102#2:366 +201#8,2:367 +201:369 +102#2:370 +201#8:371 +41#4:372 +47#2:373 +61,2:374 +102:376 +62:377 +47:378 +68:379 +65:380 +68:381 +70:382 +69:383,3 +65:386 +102:387 +19#16,2:388,2 +102#2:392 +23#16:393 +102#2:394 +22#16:395 +102#2:396 +24#16:397,2 +25:399 +162#17:400 +33#16:401,2 +38,2:403,2 +40:407 +42:408 +104#2:409 +106:410 +104:411 +60#10:412,4 +107#2:416,2 +111:418 +108,2:419 +21#15:421 +198#17:422 +113#2:423 +60#10:424,4 +115#2:428,4 +116:432 +22#18:433 +5#19,2:434 +23#18:436 +25,2:437 +63#17,2:439 +326#7,2:441 +341:443 +320#14:444 +330:445,2 +57:447 +333,2:448 +205:450,3 +206:453 +335:454 +212:455,5 +213:460 +336:461 +337:462,2 +339:464,3 +341:467 +30#4:468 +334#14:469 +346:470,2 +350:472 +41#4:473 +333#14:474 +339:475 +200:476 +186:477,3 +199:480 +198,2:481 +198,2:483 +192:485,2 +200:487 +398:488 +221:489 +154:490 +221:491 +165,2:492 +293:494,9 +29#20,2:503,2 +31:507 +19:508 +31:509 +19:510,2 +294#14,2:512 +298:514,2 +186:516,2 +301:518,3 +186:521 +306,2:522,3 +309:528 +406:529,2 +19#20:531 +407#14:532,4 +411:536,2 +414:538,2 +416,2:540 +186:542 +192:543 +186:544,2 +192:546 +421,2:547 +424:549,3 +426:552 +205:553,3 +206:556 +431:557 +30#4:558 +433#14:559,2 +212:561,5 +213:566 +437:567 +30#4:568 +438#14:569 +444:570,3 +440:573 +444,2:574,2 +310:578,6 +311:584 +314:585,17 +224:602,6 +228:608,2 +57:610 +235:611 +228:612 +235:613 +236:614,2 +205:616,3 +206:619 +237:620 +212:621,5 +213:626 +238:627 +239:628,2 +246:630,3 +248:633,3 +250:636 +30#4:637 +236#14:638 +256:639,2 +186:641,3 +278:644 +284:645,3 +279:648 +206:649 +282:650 +212:651,2 +213:653 +283:654 +30#4:655 +286#14:656 +30#4:657 +287#14:658 +261:659 +264:660,3 +265:663 +30#4:664 +266#14:665 +30#4:666 +267#14:667 +8#21,3:668 +*E diff --git a/wasm-corpus/Dockerfile b/wasm-corpus/Dockerfile index e842a5228..0bf1a9f55 100644 --- a/wasm-corpus/Dockerfile +++ b/wasm-corpus/Dockerfile @@ -63,5 +63,3 @@ ENV PATH="/opt/wabt/bin:${PATH}" RUN cargo install wasm-tools WORKDIR /usr/code - -ENTRYPOINT ["./compile.sh"] \ No newline at end of file diff --git a/wasm-corpus/run.sh b/wasm-corpus/run.sh index d7f7b52c1..00b3beac7 100755 --- a/wasm-corpus/run.sh +++ b/wasm-corpus/run.sh @@ -1,12 +1,17 @@ #!/bin/bash +cd $(dirname "$0") # run with `rebuild` to build the image +# run with `run` to run a command in the container # run with no args to compile everything if [[ "$1" = "rebuild" ]]; then docker build . --platform linux/amd64 -t chicory/wasm-corpus +elif [[ "$1" = "run" ]]; then + shift + docker run -it --platform linux/amd64 -v $(pwd)/src/main/resources:/usr/code --rm chicory/wasm-corpus "$@" else # Optionally takes the args `lang` (ex: wat, rust) and `file` (ex: br.wat) # both default to all - docker run --platform linux/amd64 -v $(pwd)/src/main/resources:/usr/code --rm chicory/wasm-corpus $1 $2 + docker run --platform linux/amd64 -v $(pwd)/src/main/resources:/usr/code --rm chicory/wasm-corpus ./compile.sh $1 $2 fi diff --git a/wasm-corpus/src/main/resources/compiled/count_vowels.rs.wasm b/wasm-corpus/src/main/resources/compiled/count_vowels.rs.wasm index 243f91df8..c13d949f4 100755 Binary files a/wasm-corpus/src/main/resources/compiled/count_vowels.rs.wasm and b/wasm-corpus/src/main/resources/compiled/count_vowels.rs.wasm differ diff --git a/wasm-corpus/src/main/resources/compiled/log.go.tiny.wasm b/wasm-corpus/src/main/resources/compiled/log.go.tiny.wasm new file mode 100755 index 000000000..09100a514 Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/log.go.tiny.wasm differ diff --git a/wasm-corpus/src/main/resources/rust/count_vowels/src/lib.rs b/wasm-corpus/src/main/resources/rust/count_vowels/src/lib.rs index 3dc347740..54943d544 100644 --- a/wasm-corpus/src/main/resources/rust/count_vowels/src/lib.rs +++ b/wasm-corpus/src/main/resources/rust/count_vowels/src/lib.rs @@ -29,4 +29,16 @@ pub extern "C" fn count_vowels(ptr: i32, len: i32) -> i32 { } } count +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_count_vowels() { + count_vowels(0, -1); + } + + } \ No newline at end of file diff --git a/wasm-corpus/src/main/resources/tinygo/log.go b/wasm-corpus/src/main/resources/tinygo/log.go new file mode 100644 index 000000000..de9b6dc7e --- /dev/null +++ b/wasm-corpus/src/main/resources/tinygo/log.go @@ -0,0 +1,15 @@ +package main + +//go:wasmimport env log +func log(x int32) int32 + +//go:wasm-module sum +//export add +func add(x, y int32) int32 { + r := x + y + log(r) + return r +} + +// main is required for the `wasi` target, even if it isn't used. +func main() {} diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/Parser.java b/wasm/src/main/java/com/dylibso/chicory/wasm/Parser.java index 8c8c78937..adb71fa7d 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/Parser.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/Parser.java @@ -283,7 +283,7 @@ private void parse(InputStream in, ParserListener listener, boolean decode) { var sectionSize = readVarUInt32(buffer); validator.validateSectionType(sectionId); - + var sectionAddress = buffer.position(); ByteBuffer sectionByteBuffer = buffer.asReadOnlyBuffer(); sectionByteBuffer.order(buffer.order()); @@ -377,7 +377,9 @@ private void parse(InputStream in, ParserListener listener, boolean decode) { } case SectionId.CODE: { - var codeSection = parseCodeSection(sectionByteBuffer, typeSection); + var codeSection = + parseCodeSection( + sectionByteBuffer, typeSection, sectionAddress); listener.onSection(codeSection); break; } @@ -863,11 +865,12 @@ private static List parseCodeSectionLocalTypes( return locals; } - private static CodeSection parseCodeSection(ByteBuffer buffer, TypeSection typeSection) { + private static CodeSection parseCodeSection( + ByteBuffer buffer, TypeSection typeSection, int sectionAddress) { var funcBodyCount = readVarUInt32(buffer); var root = new ControlTree(); - var codeSection = CodeSection.builder(); + var codeSection = CodeSection.builder().withSectionAddress(sectionAddress); // Parse individual function bodies in the code section for (int i = 0; i < funcBodyCount; i++) { diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/WasmWriter.java b/wasm/src/main/java/com/dylibso/chicory/wasm/WasmWriter.java index f2de85384..438e19e10 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/WasmWriter.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/WasmWriter.java @@ -5,7 +5,10 @@ import static java.lang.Integer.toUnsignedLong; import com.dylibso.chicory.wasm.types.RawSection; +import com.dylibso.chicory.wasm.types.SectionId; +import com.dylibso.chicory.wasm.types.UnknownCustomSection; import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; public final class WasmWriter { @@ -20,10 +23,28 @@ public void writeSection(RawSection section) { writeSection(section.sectionId(), section.contents()); } - public void writeSection(int sectionId, byte[] contents) { + public void writeSection(UnknownCustomSection section) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + writeName(out, section.name()); + writeSection(section.sectionId(), out.toByteArray(), section.bytes()); + } + + public void writeSection(int sectionId, byte[]... contents) { out.write(sectionId); - writeVarUInt32(out, contents.length); - out.writeBytes(contents); + int totalLength = 0; + for (byte[] content : contents) { + totalLength += content.length; + } + writeVarUInt32(out, totalLength); + for (byte[] content : contents) { + out.writeBytes(content); + } + } + + public void writeEmptyCodeSection() { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + writeVarUInt32(out, 0); + writeSection(SectionId.CODE, out.toByteArray()); } public byte[] bytes() { @@ -41,4 +62,10 @@ public static void writeVarUInt32(ByteArrayOutputStream out, int value) { x >>= 7; } } + + public static void writeName(ByteArrayOutputStream out, String name) { + var bytes = name.getBytes(StandardCharsets.UTF_8); + writeVarUInt32(out, bytes.length); + out.writeBytes(bytes); + } } diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/types/CodeSection.java b/wasm/src/main/java/com/dylibso/chicory/wasm/types/CodeSection.java index 5535c3552..f1b74b57a 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/types/CodeSection.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/types/CodeSection.java @@ -7,11 +7,17 @@ public final class CodeSection extends Section { private final List functionBodies; private final boolean requiresDataCount; + private final int address; - private CodeSection(List functionBodies, boolean requiresDataCount) { + private CodeSection(List functionBodies, boolean requiresDataCount, int address) { super(SectionId.CODE); this.functionBodies = List.copyOf(functionBodies); this.requiresDataCount = requiresDataCount; + this.address = address; + } + + public int address() { + return address; } public FunctionBody[] functionBodies() { @@ -37,6 +43,7 @@ public static Builder builder() { public static final class Builder { private final List functionBodies = new ArrayList<>(); private boolean requiresDataCount; + private int address; private Builder() {} @@ -58,7 +65,12 @@ public Builder setRequiresDataCount(boolean requiresDataCount) { } public CodeSection build() { - return new CodeSection(functionBodies, requiresDataCount); + return new CodeSection(functionBodies, requiresDataCount, address); + } + + public Builder withSectionAddress(int sectionAddress) { + this.address = sectionAddress; + return this; } } diff --git a/wasm/src/test/java/com/dylibso/chicory/wasm/ParserTest.java b/wasm/src/test/java/com/dylibso/chicory/wasm/ParserTest.java index b38cf7b86..42e878763 100644 --- a/wasm/src/test/java/com/dylibso/chicory/wasm/ParserTest.java +++ b/wasm/src/test/java/com/dylibso/chicory/wasm/ParserTest.java @@ -82,7 +82,8 @@ public void shouldParseFile() throws IOException { var instructions = func.instructions(); assertEquals(3, instructions.size()); - assertTrue(instructions.get(0).toString().contains("0x00000032: I32_CONST [42]")); + String string = instructions.get(0).toString(); + assertTrue(string.contains("0x00000032: I32_CONST [42]")); assertEquals(OpCode.I32_CONST, instructions.get(0).opcode()); assertEquals(42L, instructions.get(0).operand(0)); assertEquals(OpCode.CALL, instructions.get(1).opcode());