From 3173a989d8984795a37f5befc0856db64f08e390 Mon Sep 17 00:00:00 2001 From: Andreas Karlsson Date: Mon, 31 Aug 2026 14:56:05 +0200 Subject: [PATCH 1/6] feat: optionally use WASM function names for compiled methods --- .../compiler/MachineFactoryCompiler.java | 5 ++ .../endive/compiler/internal/Compiler.java | 57 +++++++++---- .../compiler/internal/CompilerUtil.java | 46 +++++++++- .../run/endive/compiler/internal/Context.java | 10 ++- .../endive/compiler/internal/Emitters.java | 3 +- .../compiler/internal/CompilerUtilTest.java | 48 +++++++++++ .../compiler/internal/DebugNamesTest.java | 83 +++++++++++++++++++ .../compiler/internal/InterruptionTest.java | 2 +- 8 files changed, 233 insertions(+), 21 deletions(-) create mode 100644 compiler/src/test/java/run/endive/compiler/internal/CompilerUtilTest.java create mode 100644 compiler/src/test/java/run/endive/compiler/internal/DebugNamesTest.java diff --git a/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java b/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java index 1fb842dc9..935ba137e 100644 --- a/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java +++ b/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java @@ -112,6 +112,11 @@ public Builder withInterpretedFunctions(Set interpretedFunctions) { return this; } + public Builder withUseDebugNames(boolean useDebugNames) { + compilerBuilder.withUseDebugNames(useDebugNames); + return this; + } + public Builder withCache(Cache cache) { this.cache = cache; return this; diff --git a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java index 28bc1f91b..89af1c520 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java @@ -30,6 +30,7 @@ import static run.endive.compiler.internal.CompilerUtil.emitInvokeVirtual; import static run.endive.compiler.internal.CompilerUtil.emitJvmToLong; import static run.endive.compiler.internal.CompilerUtil.emitLongToJvm; +import static run.endive.compiler.internal.CompilerUtil.extractFuncId; import static run.endive.compiler.internal.CompilerUtil.hasTooManyParameters; import static run.endive.compiler.internal.CompilerUtil.internalClassName; import static run.endive.compiler.internal.CompilerUtil.jvmReturnType; @@ -90,6 +91,7 @@ import run.endive.wasm.types.ExternalType; import run.endive.wasm.types.FunctionBody; import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.NameCustomSection; import run.endive.wasm.types.OpCode; import run.endive.wasm.types.ValType; @@ -161,6 +163,7 @@ public final class Compiler { private final boolean[] tailCallTypes; private final boolean moduleHasTailCalls; private final boolean moduleHasObjectRefs; + private final NameCustomSection debugNameSection; private boolean useBridgeClasses; private IntFunction callIndirectClassResolver; @@ -170,7 +173,8 @@ private Compiler( int maxFunctionsPerClass, InterpreterFallback interpreterFallback, Set interpretedFunctions, - Supplier classCollectorFactory) { + Supplier classCollectorFactory, + boolean useDebugNames) { this.className = requireNonNull(className, "className"); this.module = requireNonNull(module, "module"); this.analyzer = new WasmAnalyzer(module); @@ -202,6 +206,7 @@ private Compiler( this.functionTypes.stream() .anyMatch(ft -> ft.hasObjectRefParams() || ft.hasObjectRefReturns()); this.maxFunctionsPerClass = maxFunctionsPerClass; + this.debugNameSection = useDebugNames ? module.nameSection() : null; } private Set collectCallRefTypeIds() { @@ -229,6 +234,7 @@ public static final class Builder { private InterpreterFallback interpreterFallback; private Set interpretedFunctions; private Supplier classCollectorFactory; + private boolean useDebugNames; private Builder(WasmModule module) { this.module = module; @@ -259,6 +265,11 @@ public Builder withClassCollectorFactory(Supplier classCollector return this; } + public Builder withUseDebugNames(boolean useDebugNames) { + this.useDebugNames = useDebugNames; + return this; + } + public Compiler build() { var className = this.className; if (className == null) { @@ -280,7 +291,8 @@ public Compiler build() { maxFunctionsPerClass, interpreterFallback, interpretedFunctions, - classCollectorFactory); + classCollectorFactory, + useDebugNames); } } @@ -351,10 +363,8 @@ private void compileExtraClasses() { break; } catch (MethodTooLargeException e) { String methodName = e.getMethodName(); - if (methodName.startsWith("func_")) { - // Add the method to interpreted function list... and try again. - var funcId = Integer.parseInt(methodName.substring("func_".length())); - + int funcId = extractFuncId(methodName); + if (funcId >= 0) { String functionDescription = "WASM function index: " + funcId; if (module.nameSection() != null) { String name = module.nameSection().nameOfFunction(funcId); @@ -496,7 +506,7 @@ private Consumer emitFunctionGroup(int start, int end, String inte if (i < functionImports) { emitFunction( classWriter, - methodNameForFunc(funcId), + methodNameForFunc(funcId, debugNameSection), methodTypeFor(type), true, asm -> compileHostFunction(funcId, type, asm)); @@ -507,7 +517,7 @@ private Consumer emitFunctionGroup(int start, int end, String inte emitFunction( classWriter, - methodNameForFunc(funcId), + methodNameForFunc(funcId, debugNameSection), methodTypeFor(type), true, asm -> @@ -689,8 +699,8 @@ private boolean isFuncTypeMatch(int expectedTypeId, int funcIdx, FunctionType ex private static RuntimeException handleMethodTooLarge( MethodTooLargeException e, WasmModule module) { String name = e.getMethodName(); - if (name.startsWith("func_") && module.nameSection() != null) { - int funcId = Integer.parseInt(name.split("_", -1)[1]); + int funcId = extractFuncId(name); + if (funcId >= 0 && module.nameSection() != null) { String function = module.nameSection().nameOfFunction(funcId); if (function != null) { name += " (" + function + ")"; @@ -1392,7 +1402,11 @@ private void compileCallFunction(int funcId, FunctionType type, InstructionAdapt asm.load(0, OBJECT_TYPE); emitInvokeFunction( - asm, internalClassName(classNameForFuncGroup(className, funcId)), funcId, type); + asm, + internalClassName(classNameForFuncGroup(className, funcId)), + funcId, + type, + debugNameSection); // box the result into long[] Class returnType = jvmReturnType(type); @@ -1482,7 +1496,11 @@ private void compileCallWithRefsFunction( asm.load(0, OBJECT_TYPE); emitInvokeFunction( - asm, internalClassName(classNameForFuncGroup(className, funcId)), funcId, type); + asm, + internalClassName(classNameForFuncGroup(className, funcId)), + funcId, + type, + debugNameSection); // Build CallResult from the function's JVM return value Class returnType = jvmReturnType(type); @@ -1681,7 +1699,11 @@ private void compileCallIndirect( // return func_0(a, b, memory, callerInstance); asm.mark(labels[i]); emitInvokeFunction( - asm, classNameForFuncGroup(internalClassName, keys[i]), keys[i], type); + asm, + classNameForFuncGroup(internalClassName, keys[i]), + keys[i], + type, + debugNameSection); asm.areturn(getType(jvmReturnType(type))); } @@ -1829,7 +1851,11 @@ private void compileCallIndirectApply( // return func_0(a, b, memory, callerInstance); asm.mark(labels[i]); emitInvokeFunction( - asm, classNameForFuncGroup(internalClassName, keys[i]), keys[i], type); + asm, + classNameForFuncGroup(internalClassName, keys[i]), + keys[i], + type, + debugNameSection); asm.areturn(getType(jvmReturnType(type))); asm.areturn(OBJECT_TYPE); } @@ -2105,7 +2131,8 @@ private void compileFunction( tailCallFunctions, tailCallTypes, useBridgeClasses ? callIndirectClassResolver : typeId -> internalClassName, - analysis.maxTempSlots()); + analysis.maxTempSlots(), + debugNameSection != null); int localsCount = type.params().size(); if (hasTooManyParameters(type)) { diff --git a/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java b/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java index d46b7178e..3e0ab3d97 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java +++ b/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java @@ -24,6 +24,7 @@ import run.endive.wasm.types.ExternalType; import run.endive.wasm.types.FunctionBody; import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.NameCustomSection; import run.endive.wasm.types.TagImport; import run.endive.wasm.types.ValType; import run.endive.wasm.types.Value; @@ -282,11 +283,15 @@ public static void emitInvokeVirtual(MethodVisitor asm, Method method) { } public static void emitInvokeFunction( - MethodVisitor asm, String internalClassName, int funcId, FunctionType functionType) { + MethodVisitor asm, + String internalClassName, + int funcId, + FunctionType functionType, + NameCustomSection nameSection) { asm.visitMethodInsn( Opcodes.INVOKESTATIC, internalClassName, - methodNameForFunc(funcId), + methodNameForFunc(funcId, nameSection), methodTypeFor(functionType).toMethodDescriptorString(), false); } @@ -298,10 +303,45 @@ public static String valueMethodName(List types) { .collect(joining("_")); } - public static String methodNameForFunc(int funcId) { + public static String methodNameForFunc(int funcId, NameCustomSection nameSection) { + if (nameSection != null) { + String name = nameSection.nameOfFunction(funcId); + if (name != null && !name.isEmpty()) { + String sanitized = sanitizeWasmName(name); + if (!sanitized.isEmpty()) { + return sanitized + "_" + funcId; + } + } + } return "func_" + funcId; } + static String sanitizeWasmName(String name) { + StringBuilder sb = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + // see https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.2.2 for reference + if (c == '.' || c == ';' || c == '[' || c == '/' || c == '<' || c == '>') { + sb.append('_'); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + static int extractFuncId(String methodName) { + int lastUnderscore = methodName.lastIndexOf('_'); + if (lastUnderscore < 0) { + return -1; + } + try { + return Integer.parseInt(methodName.substring(lastUnderscore + 1)); + } catch (NumberFormatException e) { + return -1; + } + } + static String callMethodName(int funcId) { return "call_" + funcId; } diff --git a/compiler/src/main/java/run/endive/compiler/internal/Context.java b/compiler/src/main/java/run/endive/compiler/internal/Context.java index 6cb7a1ef2..3150d0b9d 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Context.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Context.java @@ -9,6 +9,7 @@ import run.endive.wasm.WasmModule; import run.endive.wasm.types.FunctionBody; import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.NameCustomSection; import run.endive.wasm.types.TypeSection; import run.endive.wasm.types.ValType; @@ -33,6 +34,7 @@ final class Context { private final int tempSlot; private final int trySaveBaseSlot; private final IntFunction callIndirectClassResolver; + private final boolean useDebugNames; public Context( WasmModule module, @@ -46,7 +48,8 @@ public Context( boolean[] tailCallFunctions, boolean[] tailCallTypes, IntFunction callIndirectClassResolver, - int maxTempSlots) { + int maxTempSlots, + boolean useDebugNames) { this.module = module; this.internalClassName = internalClassName; this.maxFunctionsPerClass = maxFunctionsPerClass; @@ -58,6 +61,7 @@ public Context( this.tailCallFunctions = tailCallFunctions; this.tailCallTypes = tailCallTypes; this.callIndirectClassResolver = callIndirectClassResolver; + this.useDebugNames = useDebugNames; // compute JVM slot indices for WASM locals List slots = new ArrayList<>(type.params().size() + body.localTypes().size()); @@ -122,6 +126,10 @@ public TypeSection typeSection() { return module.typeSection(); } + public NameCustomSection nameSection() { + return useDebugNames ? module.nameSection() : null; + } + public int getId() { return funcId; } diff --git a/compiler/src/main/java/run/endive/compiler/internal/Emitters.java b/compiler/src/main/java/run/endive/compiler/internal/Emitters.java index 7c10aabe2..fa0ea56c7 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Emitters.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Emitters.java @@ -399,7 +399,8 @@ public static void CALL(Context ctx, CompilerInstruction ins, InstructionAdapter asm, ctx.classNameForFuncGroup(ctx.internalClassName(), funcId), funcId, - functionType); + functionType, + ctx.nameSection()); if (ctx.needsTailCallCheck(funcId)) { emitTailCallCheck(ctx, asm, functionType); diff --git a/compiler/src/test/java/run/endive/compiler/internal/CompilerUtilTest.java b/compiler/src/test/java/run/endive/compiler/internal/CompilerUtilTest.java new file mode 100644 index 000000000..4deeb73b3 --- /dev/null +++ b/compiler/src/test/java/run/endive/compiler/internal/CompilerUtilTest.java @@ -0,0 +1,48 @@ +package run.endive.compiler.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static run.endive.compiler.internal.CompilerUtil.extractFuncId; +import static run.endive.compiler.internal.CompilerUtil.methodNameForFunc; +import static run.endive.compiler.internal.CompilerUtil.sanitizeWasmName; + +import org.junit.jupiter.api.Test; + +public class CompilerUtilTest { + + @Test + public void methodNameWithoutNameSection() { + assertEquals("func_0", methodNameForFunc(0, null)); + assertEquals("func_42", methodNameForFunc(42, null)); + } + + @Test + public void sanitizeReplacesIllegalChars() { + assertEquals("foo", sanitizeWasmName("foo")); + assertEquals("a_b_c", sanitizeWasmName("a.b/c")); + assertEquals("a_b_c_d_e_f", sanitizeWasmName("a.b;c[df")); + } + + @Test + public void sanitizePreservesUnderscoresAndDashes() { + assertEquals("my_func", sanitizeWasmName("my_func")); + assertEquals("my-func", sanitizeWasmName("my-func")); + } + + @Test + public void extractFuncIdFromSimpleName() { + assertEquals(0, extractFuncId("func_0")); + assertEquals(42, extractFuncId("func_42")); + } + + @Test + public void extractFuncIdFromNamedMethod() { + assertEquals(0, extractFuncId("foo_0")); + assertEquals(5, extractFuncId("my_func_5")); + } + + @Test + public void extractFuncIdReturnsNegativeForInvalid() { + assertEquals(-1, extractFuncId("nounderscore")); + assertEquals(-1, extractFuncId("func_abc")); + } +} diff --git a/compiler/src/test/java/run/endive/compiler/internal/DebugNamesTest.java b/compiler/src/test/java/run/endive/compiler/internal/DebugNamesTest.java new file mode 100644 index 000000000..a2cf39ed0 --- /dev/null +++ b/compiler/src/test/java/run/endive/compiler/internal/DebugNamesTest.java @@ -0,0 +1,83 @@ +package run.endive.compiler.internal; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import run.endive.compiler.MachineFactoryCompiler; +import run.endive.corpus.CorpusResources; +import run.endive.runtime.Instance; +import run.endive.wasm.Parser; + +public class DebugNamesTest { + + @Test + public void defaultDoesNotUseDebugNames() { + var module = Parser.parse(CorpusResources.getResource("compiled/branching.wat.wasm")); + var result = Compiler.builder(module).build().compile(); + var methods = funcGroupMethods(result); + + assertFalse( + methods.stream().anyMatch(n -> n.startsWith("foo_")), + "Default mode should not produce named methods, got: " + methods); + } + + @Test + public void debugNamesProduceNamedMethods() { + var module = Parser.parse(CorpusResources.getResource("compiled/branching.wat.wasm")); + var result = Compiler.builder(module).withUseDebugNames(true).build().compile(); + var methods = funcGroupMethods(result); + + assertTrue( + methods.stream().anyMatch(n -> n.startsWith("foo_")), + "Expected a method starting with 'foo_', got: " + methods); + } + + @Test + public void debugNamesExecuteCorrectly() throws InterruptedException { + var module = Parser.parse(CorpusResources.getResource("compiled/branching.wat.wasm")); + var instance = + Instance.builder(module) + .withMachineFactory( + MachineFactoryCompiler.builder(module) + .withUseDebugNames(true) + .compile()) + .build(); + + var function = instance.export("foo"); + assertArrayEquals(new long[] {42}, function.apply(0)); + assertArrayEquals(new long[] {99}, function.apply(1)); + } + + private static List funcGroupMethods(CompilerResult result) { + var methods = new ArrayList(); + for (var entry : result.classBytes().entrySet()) { + if (!entry.getKey().contains("FuncGroup")) { + continue; + } + var reader = new ClassReader(entry.getValue()); + reader.accept( + new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod( + int access, + String name, + String descriptor, + String signature, + String[] exceptions) { + methods.add(name); + return null; + } + }, + 0); + } + return methods; + } +} diff --git a/compiler/src/test/java/run/endive/compiler/internal/InterruptionTest.java b/compiler/src/test/java/run/endive/compiler/internal/InterruptionTest.java index e1bec61fc..46c7162eb 100644 --- a/compiler/src/test/java/run/endive/compiler/internal/InterruptionTest.java +++ b/compiler/src/test/java/run/endive/compiler/internal/InterruptionTest.java @@ -83,7 +83,7 @@ private static void waitForWasmExecution(Thread thread, int funcIdx) var className = element.getClassName(); var methodName = element.getMethodName(); if (className.startsWith(Compiler.DEFAULT_CLASS_NAME + "FuncGroup_") - && methodName.equals(methodNameForFunc(funcIdx))) { + && methodName.equals(methodNameForFunc(funcIdx, null))) { return; } } From 46eca27373660d74b070cba894178407588ed2fe Mon Sep 17 00:00:00 2001 From: Andreas Karlsson Date: Mon, 31 Aug 2026 15:35:49 +0200 Subject: [PATCH 2/6] update compiler docs --- docs/docs/execution/runtime-compiler.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/docs/execution/runtime-compiler.md b/docs/docs/execution/runtime-compiler.md index 29698bead..c95b01c70 100644 --- a/docs/docs/execution/runtime-compiler.md +++ b/docs/docs/execution/runtime-compiler.md @@ -118,6 +118,22 @@ var instance = Instance.builder(module). Typically, you can obtain the list of the functions by running the compiler once with `InterpreterFallback.WARN` +### Debug Names + +By default, the compiler names compiled methods `func_0`, `func_1`, etc. If the WASM module includes a name section, you can opt in to using the original function names in compiled method names. This improves readability of stack traces, profiler output, and error messages. + +```java +var instance = Instance.builder(module). + withMachineFactory( + MachineFactoryCompiler.builder(module) + .withUseDebugNames(true) + .compile() + ). + build(); +``` + +Characters not allowed in JVM method names are replaced with underscores. The numeric function index is always preserved as a suffix (e.g. `my_func_42`), so tools can recover the original function by index. + ### Caveats Please note that compiling and executing Wasm modules at runtime requires: From f824711fcfaf08f61169d945246981afced92d28 Mon Sep 17 00:00:00 2001 From: Andreas Karlsson Date: Thu, 3 Sep 2026 01:30:33 +0200 Subject: [PATCH 3/6] method prefix proposal --- .../compiler/MachineFactoryCompiler.java | 8 ++- .../run/endive/compiler/MethodPrefixer.java | 60 +++++++++++++++++++ .../endive/compiler/internal/Compiler.java | 52 +++++++++------- .../compiler/internal/CompilerUtil.java | 32 +++++----- .../run/endive/compiler/internal/Context.java | 11 ++-- .../endive/compiler/internal/Emitters.java | 5 +- .../compiler/internal/CompilerUtilTest.java | 34 ++++++++++- .../compiler/internal/InterruptionTest.java | 2 +- ...NamesTest.java => MethodPrefixerTest.java} | 55 +++++++++++++++-- docs/docs/execution/runtime-compiler.md | 20 +++++-- 10 files changed, 216 insertions(+), 63 deletions(-) create mode 100644 compiler/src/main/java/run/endive/compiler/MethodPrefixer.java rename compiler/src/test/java/run/endive/compiler/internal/{DebugNamesTest.java => MethodPrefixerTest.java} (58%) diff --git a/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java b/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java index 935ba137e..f4a1e543b 100644 --- a/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java +++ b/compiler/src/main/java/run/endive/compiler/MachineFactoryCompiler.java @@ -112,8 +112,12 @@ public Builder withInterpretedFunctions(Set interpretedFunctions) { return this; } - public Builder withUseDebugNames(boolean useDebugNames) { - compilerBuilder.withUseDebugNames(useDebugNames); + /** + * Sets the {@link MethodPrefixer} used to name the compiled methods. Defaults to + * {@link MethodPrefixer#defaultPrefixer()}. + */ + public Builder withMethodPrefixer(MethodPrefixer methodPrefixer) { + compilerBuilder.withMethodPrefixer(methodPrefixer); return this; } diff --git a/compiler/src/main/java/run/endive/compiler/MethodPrefixer.java b/compiler/src/main/java/run/endive/compiler/MethodPrefixer.java new file mode 100644 index 000000000..b45a18246 --- /dev/null +++ b/compiler/src/main/java/run/endive/compiler/MethodPrefixer.java @@ -0,0 +1,60 @@ +package run.endive.compiler; + +import run.endive.wasm.WasmModule; + +/** + * Supplies the human readable prefix used when naming the JVM method compiled for a WASM function. + * + *

The compiler derives every method name as {@code _}. The prefixer + * only controls the prefix; the compiler owns the rest of the name. That split keeps two + * invariants that the rest of the compiler and any external tooling can rely on, regardless of + * what a prefixer returns: + * + *

    + *
  • method names are unique, because the function id is unique + *
  • the function id can always be recovered by parsing the {@code _} suffix + *
+ * + *

Characters that are illegal in a JVM method name ({@code . ; [ / < >}, see + * JVM Spec + * ยง4.2.2) are replaced with {@code _}. A prefixer that needs to preserve the original name + * exactly can avoid the substitution by encoding those characters itself, for example by + * percent-encoding them. + * + *

The prefix is a hint for humans reading a thread dump or a profile. Tooling should never + * parse it; it should use the function id instead. + */ +@FunctionalInterface +public interface MethodPrefixer { + + /** + * The prefix used when no prefixer is configured, and the fallback whenever a prefixer returns + * {@code null}, an empty string, or a string that sanitizes to nothing. + */ + String DEFAULT_PREFIX = "func"; + + /** + * Returns the prefix for the method compiled for {@code funcId}, or {@code null} to use + * {@link #DEFAULT_PREFIX}. + * + * @param funcId the WASM function index, covering imported and defined functions + * @param module the module being compiled + */ + String getMethodPrefix(int funcId, WasmModule module); + + /** Returns the default prefixer, naming every method {@value #DEFAULT_PREFIX}. */ + static MethodPrefixer defaultPrefixer() { + return (funcId, module) -> DEFAULT_PREFIX; + } + + /** + * Returns a prefixer that uses the function name from the module's name custom section, falling + * back to {@link #DEFAULT_PREFIX} for functions without one. + */ + static MethodPrefixer fromNameSection() { + return (funcId, module) -> { + var nameSection = module.nameSection(); + return nameSection == null ? null : nameSection.nameOfFunction(funcId); + }; + } +} diff --git a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java index 89af1c520..39f8a0b00 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java @@ -80,6 +80,7 @@ import org.objectweb.asm.Type; import org.objectweb.asm.commons.InstructionAdapter; import run.endive.compiler.InterpreterFallback; +import run.endive.compiler.MethodPrefixer; import run.endive.runtime.CallResult; import run.endive.runtime.Instance; import run.endive.runtime.Machine; @@ -91,7 +92,6 @@ import run.endive.wasm.types.ExternalType; import run.endive.wasm.types.FunctionBody; import run.endive.wasm.types.FunctionType; -import run.endive.wasm.types.NameCustomSection; import run.endive.wasm.types.OpCode; import run.endive.wasm.types.ValType; @@ -163,7 +163,7 @@ public final class Compiler { private final boolean[] tailCallTypes; private final boolean moduleHasTailCalls; private final boolean moduleHasObjectRefs; - private final NameCustomSection debugNameSection; + private final String[] methodNames; private boolean useBridgeClasses; private IntFunction callIndirectClassResolver; @@ -174,7 +174,7 @@ private Compiler( InterpreterFallback interpreterFallback, Set interpretedFunctions, Supplier classCollectorFactory, - boolean useDebugNames) { + MethodPrefixer methodPrefixer) { this.className = requireNonNull(className, "className"); this.module = requireNonNull(module, "module"); this.analyzer = new WasmAnalyzer(module); @@ -206,7 +206,17 @@ private Compiler( this.functionTypes.stream() .anyMatch(ft -> ft.hasObjectRefParams() || ft.hasObjectRefReturns()); this.maxFunctionsPerClass = maxFunctionsPerClass; - this.debugNameSection = useDebugNames ? module.nameSection() : null; + // Resolve every method name up front, so that the prefixer is consulted exactly once per + // function and definitions and call sites cannot disagree. + var prefixer = requireNonNullElse(methodPrefixer, MethodPrefixer.defaultPrefixer()); + this.methodNames = new String[this.functionTypes.size()]; + for (int funcId = 0; funcId < this.methodNames.length; funcId++) { + this.methodNames[funcId] = methodNameForFunc(funcId, prefixer, module); + } + } + + private String methodName(int funcId) { + return methodNames[funcId]; } private Set collectCallRefTypeIds() { @@ -234,7 +244,7 @@ public static final class Builder { private InterpreterFallback interpreterFallback; private Set interpretedFunctions; private Supplier classCollectorFactory; - private boolean useDebugNames; + private MethodPrefixer methodPrefixer; private Builder(WasmModule module) { this.module = module; @@ -265,8 +275,8 @@ public Builder withClassCollectorFactory(Supplier classCollector return this; } - public Builder withUseDebugNames(boolean useDebugNames) { - this.useDebugNames = useDebugNames; + public Builder withMethodPrefixer(MethodPrefixer methodPrefixer) { + this.methodPrefixer = methodPrefixer; return this; } @@ -292,7 +302,7 @@ public Compiler build() { interpreterFallback, interpretedFunctions, classCollectorFactory, - useDebugNames); + methodPrefixer); } } @@ -506,7 +516,7 @@ private Consumer emitFunctionGroup(int start, int end, String inte if (i < functionImports) { emitFunction( classWriter, - methodNameForFunc(funcId, debugNameSection), + methodName(funcId), methodTypeFor(type), true, asm -> compileHostFunction(funcId, type, asm)); @@ -517,7 +527,7 @@ private Consumer emitFunctionGroup(int start, int end, String inte emitFunction( classWriter, - methodNameForFunc(funcId, debugNameSection), + methodName(funcId), methodTypeFor(type), true, asm -> @@ -1404,9 +1414,8 @@ private void compileCallFunction(int funcId, FunctionType type, InstructionAdapt emitInvokeFunction( asm, internalClassName(classNameForFuncGroup(className, funcId)), - funcId, - type, - debugNameSection); + methodName(funcId), + type); // box the result into long[] Class returnType = jvmReturnType(type); @@ -1498,9 +1507,8 @@ private void compileCallWithRefsFunction( emitInvokeFunction( asm, internalClassName(classNameForFuncGroup(className, funcId)), - funcId, - type, - debugNameSection); + methodName(funcId), + type); // Build CallResult from the function's JVM return value Class returnType = jvmReturnType(type); @@ -1701,9 +1709,8 @@ private void compileCallIndirect( emitInvokeFunction( asm, classNameForFuncGroup(internalClassName, keys[i]), - keys[i], - type, - debugNameSection); + methodName(keys[i]), + type); asm.areturn(getType(jvmReturnType(type))); } @@ -1853,9 +1860,8 @@ private void compileCallIndirectApply( emitInvokeFunction( asm, classNameForFuncGroup(internalClassName, keys[i]), - keys[i], - type, - debugNameSection); + methodName(keys[i]), + type); asm.areturn(getType(jvmReturnType(type))); asm.areturn(OBJECT_TYPE); } @@ -2132,7 +2138,7 @@ private void compileFunction( tailCallTypes, useBridgeClasses ? callIndirectClassResolver : typeId -> internalClassName, analysis.maxTempSlots(), - debugNameSection != null); + this::methodName); int localsCount = type.params().size(); if (hasTooManyParameters(type)) { diff --git a/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java b/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java index 3e0ab3d97..2e8fde3b3 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java +++ b/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java @@ -18,13 +18,13 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; +import run.endive.compiler.MethodPrefixer; import run.endive.runtime.Instance; import run.endive.runtime.Memory; import run.endive.wasm.WasmModule; import run.endive.wasm.types.ExternalType; import run.endive.wasm.types.FunctionBody; import run.endive.wasm.types.FunctionType; -import run.endive.wasm.types.NameCustomSection; import run.endive.wasm.types.TagImport; import run.endive.wasm.types.ValType; import run.endive.wasm.types.Value; @@ -285,13 +285,12 @@ public static void emitInvokeVirtual(MethodVisitor asm, Method method) { public static void emitInvokeFunction( MethodVisitor asm, String internalClassName, - int funcId, - FunctionType functionType, - NameCustomSection nameSection) { + String methodName, + FunctionType functionType) { asm.visitMethodInsn( Opcodes.INVOKESTATIC, internalClassName, - methodNameForFunc(funcId, nameSection), + methodName, methodTypeFor(functionType).toMethodDescriptorString(), false); } @@ -303,17 +302,20 @@ public static String valueMethodName(List types) { .collect(joining("_")); } - public static String methodNameForFunc(int funcId, NameCustomSection nameSection) { - if (nameSection != null) { - String name = nameSection.nameOfFunction(funcId); - if (name != null && !name.isEmpty()) { - String sanitized = sanitizeWasmName(name); - if (!sanitized.isEmpty()) { - return sanitized + "_" + funcId; - } - } + /** + * Builds the JVM method name for a WASM function as {@code _}. The + * prefixer only supplies the prefix, so the {@code _} suffix always makes the name + * unique and keeps the function id recoverable via {@link #extractFuncId(String)}. + */ + public static String methodNameForFunc(int funcId, MethodPrefixer prefixer, WasmModule module) { + String prefix = prefixer == null ? null : prefixer.getMethodPrefix(funcId, module); + if (prefix != null) { + prefix = sanitizeWasmName(prefix); + } + if (prefix == null || prefix.isEmpty()) { + prefix = MethodPrefixer.DEFAULT_PREFIX; } - return "func_" + funcId; + return prefix + "_" + funcId; } static String sanitizeWasmName(String name) { diff --git a/compiler/src/main/java/run/endive/compiler/internal/Context.java b/compiler/src/main/java/run/endive/compiler/internal/Context.java index 3150d0b9d..bdbd6b09f 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Context.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Context.java @@ -9,7 +9,6 @@ import run.endive.wasm.WasmModule; import run.endive.wasm.types.FunctionBody; import run.endive.wasm.types.FunctionType; -import run.endive.wasm.types.NameCustomSection; import run.endive.wasm.types.TypeSection; import run.endive.wasm.types.ValType; @@ -34,7 +33,7 @@ final class Context { private final int tempSlot; private final int trySaveBaseSlot; private final IntFunction callIndirectClassResolver; - private final boolean useDebugNames; + private final IntFunction methodNames; public Context( WasmModule module, @@ -49,7 +48,7 @@ public Context( boolean[] tailCallTypes, IntFunction callIndirectClassResolver, int maxTempSlots, - boolean useDebugNames) { + IntFunction methodNames) { this.module = module; this.internalClassName = internalClassName; this.maxFunctionsPerClass = maxFunctionsPerClass; @@ -61,7 +60,7 @@ public Context( this.tailCallFunctions = tailCallFunctions; this.tailCallTypes = tailCallTypes; this.callIndirectClassResolver = callIndirectClassResolver; - this.useDebugNames = useDebugNames; + this.methodNames = methodNames; // compute JVM slot indices for WASM locals List slots = new ArrayList<>(type.params().size() + body.localTypes().size()); @@ -126,8 +125,8 @@ public TypeSection typeSection() { return module.typeSection(); } - public NameCustomSection nameSection() { - return useDebugNames ? module.nameSection() : null; + public String methodNameForFunc(int funcId) { + return methodNames.apply(funcId); } public int getId() { diff --git a/compiler/src/main/java/run/endive/compiler/internal/Emitters.java b/compiler/src/main/java/run/endive/compiler/internal/Emitters.java index fa0ea56c7..5d5ae6481 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Emitters.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Emitters.java @@ -398,9 +398,8 @@ public static void CALL(Context ctx, CompilerInstruction ins, InstructionAdapter emitInvokeFunction( asm, ctx.classNameForFuncGroup(ctx.internalClassName(), funcId), - funcId, - functionType, - ctx.nameSection()); + ctx.methodNameForFunc(funcId), + functionType); if (ctx.needsTailCallCheck(funcId)) { emitTailCallCheck(ctx, asm, functionType); diff --git a/compiler/src/test/java/run/endive/compiler/internal/CompilerUtilTest.java b/compiler/src/test/java/run/endive/compiler/internal/CompilerUtilTest.java index 4deeb73b3..3c20a41ae 100644 --- a/compiler/src/test/java/run/endive/compiler/internal/CompilerUtilTest.java +++ b/compiler/src/test/java/run/endive/compiler/internal/CompilerUtilTest.java @@ -6,13 +6,40 @@ import static run.endive.compiler.internal.CompilerUtil.sanitizeWasmName; import org.junit.jupiter.api.Test; +import run.endive.compiler.MethodPrefixer; public class CompilerUtilTest { @Test - public void methodNameWithoutNameSection() { - assertEquals("func_0", methodNameForFunc(0, null)); - assertEquals("func_42", methodNameForFunc(42, null)); + public void methodNameWithoutPrefixer() { + assertEquals("func_0", methodNameForFunc(0, null, null)); + assertEquals("func_42", methodNameForFunc(42, null, null)); + } + + @Test + public void methodNameUsesPrefix() { + MethodPrefixer prefixer = (funcId, module) -> "foo"; + assertEquals("foo_0", methodNameForFunc(0, prefixer, null)); + assertEquals("foo_42", methodNameForFunc(42, prefixer, null)); + } + + @Test + public void methodNameSanitizesPrefix() { + MethodPrefixer prefixer = (funcId, module) -> "a.b/c"; + assertEquals("a_b_c_7", methodNameForFunc(7, prefixer, null)); + } + + @Test + public void methodNameFallsBackToDefaultPrefix() { + assertEquals("func_3", methodNameForFunc(3, (funcId, module) -> null, null)); + assertEquals("func_3", methodNameForFunc(3, (funcId, module) -> "", null)); + } + + @Test + public void percentEncodedPrefixSurvivesSanitization() { + // A prefixer that encodes the illegal characters itself keeps the name reversible. + MethodPrefixer prefixer = (funcId, module) -> "core%2Efmt%2FFormatter"; + assertEquals("core%2Efmt%2FFormatter_9", methodNameForFunc(9, prefixer, null)); } @Test @@ -38,6 +65,7 @@ public void extractFuncIdFromSimpleName() { public void extractFuncIdFromNamedMethod() { assertEquals(0, extractFuncId("foo_0")); assertEquals(5, extractFuncId("my_func_5")); + assertEquals(9, extractFuncId("core%2Efmt%2FFormatter_9")); } @Test diff --git a/compiler/src/test/java/run/endive/compiler/internal/InterruptionTest.java b/compiler/src/test/java/run/endive/compiler/internal/InterruptionTest.java index 46c7162eb..aa85cd10a 100644 --- a/compiler/src/test/java/run/endive/compiler/internal/InterruptionTest.java +++ b/compiler/src/test/java/run/endive/compiler/internal/InterruptionTest.java @@ -83,7 +83,7 @@ private static void waitForWasmExecution(Thread thread, int funcIdx) var className = element.getClassName(); var methodName = element.getMethodName(); if (className.startsWith(Compiler.DEFAULT_CLASS_NAME + "FuncGroup_") - && methodName.equals(methodNameForFunc(funcIdx, null))) { + && methodName.equals(methodNameForFunc(funcIdx, null, null))) { return; } } diff --git a/compiler/src/test/java/run/endive/compiler/internal/DebugNamesTest.java b/compiler/src/test/java/run/endive/compiler/internal/MethodPrefixerTest.java similarity index 58% rename from compiler/src/test/java/run/endive/compiler/internal/DebugNamesTest.java rename to compiler/src/test/java/run/endive/compiler/internal/MethodPrefixerTest.java index a2cf39ed0..4c292ce88 100644 --- a/compiler/src/test/java/run/endive/compiler/internal/DebugNamesTest.java +++ b/compiler/src/test/java/run/endive/compiler/internal/MethodPrefixerTest.java @@ -12,27 +12,35 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import run.endive.compiler.MachineFactoryCompiler; +import run.endive.compiler.MethodPrefixer; import run.endive.corpus.CorpusResources; import run.endive.runtime.Instance; import run.endive.wasm.Parser; -public class DebugNamesTest { +public class MethodPrefixerTest { @Test - public void defaultDoesNotUseDebugNames() { + public void defaultUsesFuncPrefix() { var module = Parser.parse(CorpusResources.getResource("compiled/branching.wat.wasm")); var result = Compiler.builder(module).build().compile(); var methods = funcGroupMethods(result); + assertTrue( + methods.stream().anyMatch(n -> n.startsWith("func_")), + "Expected the default \"func\" prefix, got: " + methods); assertFalse( methods.stream().anyMatch(n -> n.startsWith("foo_")), "Default mode should not produce named methods, got: " + methods); } @Test - public void debugNamesProduceNamedMethods() { + public void nameSectionPrefixerProducesNamedMethods() { var module = Parser.parse(CorpusResources.getResource("compiled/branching.wat.wasm")); - var result = Compiler.builder(module).withUseDebugNames(true).build().compile(); + var result = + Compiler.builder(module) + .withMethodPrefixer(MethodPrefixer.fromNameSection()) + .build() + .compile(); var methods = funcGroupMethods(result); assertTrue( @@ -41,13 +49,48 @@ public void debugNamesProduceNamedMethods() { } @Test - public void debugNamesExecuteCorrectly() throws InterruptedException { + public void customPrefixerIsApplied() { + var module = Parser.parse(CorpusResources.getResource("compiled/branching.wat.wasm")); + var result = + Compiler.builder(module) + .withMethodPrefixer((funcId, m) -> "wasm") + .build() + .compile(); + var methods = funcGroupMethods(result); + + assertTrue( + methods.stream().anyMatch(n -> n.startsWith("wasm_")), + "Expected a method starting with 'wasm_', got: " + methods); + } + + @Test + public void everyMethodNameKeepsTheFuncIdSuffix() { + var module = Parser.parse(CorpusResources.getResource("compiled/branching.wat.wasm")); + var result = + Compiler.builder(module) + // a deliberately hostile prefixer: illegal characters, digits, underscores + .withMethodPrefixer((funcId, m) -> "a.b_1/c<9>") + .build() + .compile(); + + for (var name : funcGroupMethods(result)) { + if (!name.startsWith("a_b_1_c_9_")) { + continue; + } + assertTrue( + CompilerUtil.extractFuncId(name) >= 0, + "Could not recover the func id from: " + name); + } + } + + @Test + public void namedMethodsExecuteCorrectly() { var module = Parser.parse(CorpusResources.getResource("compiled/branching.wat.wasm")); var instance = Instance.builder(module) .withMachineFactory( MachineFactoryCompiler.builder(module) - .withUseDebugNames(true) + .withMethodPrefixer(MethodPrefixer.fromNameSection()) .compile()) .build(); diff --git a/docs/docs/execution/runtime-compiler.md b/docs/docs/execution/runtime-compiler.md index c95b01c70..85bb1ccf3 100644 --- a/docs/docs/execution/runtime-compiler.md +++ b/docs/docs/execution/runtime-compiler.md @@ -118,21 +118,33 @@ var instance = Instance.builder(module). Typically, you can obtain the list of the functions by running the compiler once with `InterpreterFallback.WARN` -### Debug Names +### Method Names -By default, the compiler names compiled methods `func_0`, `func_1`, etc. If the WASM module includes a name section, you can opt in to using the original function names in compiled method names. This improves readability of stack traces, profiler output, and error messages. +By default, the compiler names compiled methods `func_0`, `func_1`, etc. A `MethodPrefixer` lets you +replace the `func` prefix with something more recognisable, which improves readability of thread +dumps, profiler output and stack traces. `MethodPrefixer.fromNameSection()` uses the function names +from the module's name section, where present: ```java var instance = Instance.builder(module). withMachineFactory( MachineFactoryCompiler.builder(module) - .withUseDebugNames(true) + .withMethodPrefixer(MethodPrefixer.fromNameSection()) .compile() ). build(); ``` -Characters not allowed in JVM method names are replaced with underscores. The numeric function index is always preserved as a suffix (e.g. `my_func_42`), so tools can recover the original function by index. +A prefixer supplies only the prefix; the compiler always appends `_` to produce the method +name (e.g. `my_func_42`). That keeps method names unique whatever the prefixer returns, and keeps the +WASM function index recoverable from any method name. + +Characters that are illegal in JVM method names (`. ; [ / < >`) are replaced with underscores. A +prefixer that needs to preserve names exactly can avoid the substitution by encoding those +characters itself, for example by percent-encoding them. + +The prefix is a hint for humans inspecting a compiled method name. Tools should not parse it, and +should use the function index instead. ### Caveats From d8a64ed86c39213ad8c68387b6553456e54afd72 Mon Sep 17 00:00:00 2001 From: andreatp Date: Thu, 3 Sep 2026 11:49:32 +0100 Subject: [PATCH 4/6] Fix the docs snippet and resolve func ids by exact lookup --- .../endive/compiler/internal/Compiler.java | 28 ++++++++++++++----- .../compiler/internal/CompilerUtil.java | 12 +++++++- .../compiler/internal/MethodPrefixerTest.java | 7 ++++- docs/docs/execution/runtime-compiler.md | 8 ++++++ 4 files changed, 46 insertions(+), 9 deletions(-) diff --git a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java index 39f8a0b00..1a0eca53c 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java @@ -30,7 +30,6 @@ import static run.endive.compiler.internal.CompilerUtil.emitInvokeVirtual; import static run.endive.compiler.internal.CompilerUtil.emitJvmToLong; import static run.endive.compiler.internal.CompilerUtil.emitLongToJvm; -import static run.endive.compiler.internal.CompilerUtil.extractFuncId; import static run.endive.compiler.internal.CompilerUtil.hasTooManyParameters; import static run.endive.compiler.internal.CompilerUtil.internalClassName; import static run.endive.compiler.internal.CompilerUtil.jvmReturnType; @@ -164,6 +163,7 @@ public final class Compiler { private final boolean moduleHasTailCalls; private final boolean moduleHasObjectRefs; private final String[] methodNames; + private final Map funcIdsByMethodName; private boolean useBridgeClasses; private IntFunction callIndirectClassResolver; @@ -210,8 +210,10 @@ private Compiler( // function and definitions and call sites cannot disagree. var prefixer = requireNonNullElse(methodPrefixer, MethodPrefixer.defaultPrefixer()); this.methodNames = new String[this.functionTypes.size()]; + this.funcIdsByMethodName = new HashMap<>(this.methodNames.length); for (int funcId = 0; funcId < this.methodNames.length; funcId++) { this.methodNames[funcId] = methodNameForFunc(funcId, prefixer, module); + this.funcIdsByMethodName.put(this.methodNames[funcId], funcId); } } @@ -219,6 +221,19 @@ private String methodName(int funcId) { return methodNames[funcId]; } + /** + * Returns the id of the WASM function compiled into {@code methodName}, or {@code -1} when the + * name is not a compiled function body. + * + *

Names are matched exactly rather than parsed, so the bridges emitted next to the function + * bodies ({@code call_*}, {@code callWithRefs_*}) and the dispatch helpers ({@code + * call_indirect_*}, {@code call_dispatch_*}) never resolve to a function id, whatever a {@link + * MethodPrefixer} names the bodies themselves. + */ + private int funcIdForMethodName(String methodName) { + return funcIdsByMethodName.getOrDefault(methodName, -1); + } + private Set collectCallRefTypeIds() { var result = new HashSet(); int funcCount = module.functionSection().functionCount(); @@ -373,7 +388,7 @@ private void compileExtraClasses() { break; } catch (MethodTooLargeException e) { String methodName = e.getMethodName(); - int funcId = extractFuncId(methodName); + int funcId = funcIdForMethodName(methodName); if (funcId >= 0) { String functionDescription = "WASM function index: " + funcId; if (module.nameSection() != null) { @@ -553,7 +568,7 @@ private Consumer emitFunctionGroup(int start, int end, String inte } } } catch (MethodTooLargeException e) { - throw handleMethodTooLarge(e, module); + throw handleMethodTooLarge(e); } } }; @@ -686,7 +701,7 @@ private byte[] compileClass() { try { return binaryWriter.toByteArray(); } catch (MethodTooLargeException e) { - throw handleMethodTooLarge(e, module); + throw handleMethodTooLarge(e); } } @@ -706,10 +721,9 @@ private boolean isFuncTypeMatch(int expectedTypeId, int funcIdx, FunctionType ex return expectedType.equals(functionTypes.get(funcIdx)); } - private static RuntimeException handleMethodTooLarge( - MethodTooLargeException e, WasmModule module) { + private RuntimeException handleMethodTooLarge(MethodTooLargeException e) { String name = e.getMethodName(); - int funcId = extractFuncId(name); + int funcId = funcIdForMethodName(name); if (funcId >= 0 && module.nameSection() != null) { String function = module.nameSection().nameOfFunction(funcId); if (function != null) { diff --git a/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java b/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java index 2e8fde3b3..86eed42f3 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java +++ b/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java @@ -322,7 +322,8 @@ static String sanitizeWasmName(String name) { StringBuilder sb = new StringBuilder(name.length()); for (int i = 0; i < name.length(); i++) { char c = name.charAt(i); - // see https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.2.2 for reference + // see https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.2.2 for + // reference if (c == '.' || c == ';' || c == '[' || c == '/' || c == '<' || c == '>') { sb.append('_'); } else { @@ -332,6 +333,15 @@ static String sanitizeWasmName(String name) { return sb.toString(); } + /** + * Recovers the WASM function id from a compiled method name by parsing the {@code _} + * suffix, or returns {@code -1} when the name does not end in one. + * + *

This is the reference implementation of the contract external tooling relies on to map a + * method name in a thread dump or profile back to a WASM function. The compiler itself does not + * use it: it resolves ids by exact lookup, which also excludes the bridge methods that happen to + * end in a number. + */ static int extractFuncId(String methodName) { int lastUnderscore = methodName.lastIndexOf('_'); if (lastUnderscore < 0) { diff --git a/compiler/src/test/java/run/endive/compiler/internal/MethodPrefixerTest.java b/compiler/src/test/java/run/endive/compiler/internal/MethodPrefixerTest.java index 4c292ce88..e105582b7 100644 --- a/compiler/src/test/java/run/endive/compiler/internal/MethodPrefixerTest.java +++ b/compiler/src/test/java/run/endive/compiler/internal/MethodPrefixerTest.java @@ -73,7 +73,12 @@ public void everyMethodNameKeepsTheFuncIdSuffix() { .build() .compile(); - for (var name : funcGroupMethods(result)) { + var methods = funcGroupMethods(result); + assertTrue( + methods.stream().anyMatch(n -> n.startsWith("a_b_1_c_9_")), + "No method used the sanitized prefix, got: " + methods); + + for (var name : methods) { if (!name.startsWith("a_b_1_c_9_")) { continue; } diff --git a/docs/docs/execution/runtime-compiler.md b/docs/docs/execution/runtime-compiler.md index 85bb1ccf3..7e6da5f89 100644 --- a/docs/docs/execution/runtime-compiler.md +++ b/docs/docs/execution/runtime-compiler.md @@ -126,6 +126,14 @@ dumps, profiler output and stack traces. `MethodPrefixer.fromNameSection()` uses from the module's name section, where present: ```java +import run.endive.compiler.MachineFactoryCompiler; +import run.endive.compiler.MethodPrefixer; +import run.endive.runtime.Instance; +import run.endive.wasm.Parser; +import run.endive.wasm.WasmModule; +import java.io.File; + +var module = Parser.parse(new File("your.wasm")); var instance = Instance.builder(module). withMachineFactory( MachineFactoryCompiler.builder(module) From b4fb7e10bcfee04f0c98349a1e05f3f27fe3cf91 Mon Sep 17 00:00:00 2001 From: andreatp Date: Thu, 3 Sep 2026 13:00:35 +0100 Subject: [PATCH 5/6] Trim method name javadoc --- .../main/java/run/endive/compiler/internal/Compiler.java | 9 ++------- .../java/run/endive/compiler/internal/CompilerUtil.java | 9 ++------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java index 1a0eca53c..bd34a6636 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java @@ -222,13 +222,8 @@ private String methodName(int funcId) { } /** - * Returns the id of the WASM function compiled into {@code methodName}, or {@code -1} when the - * name is not a compiled function body. - * - *

Names are matched exactly rather than parsed, so the bridges emitted next to the function - * bodies ({@code call_*}, {@code callWithRefs_*}) and the dispatch helpers ({@code - * call_indirect_*}, {@code call_dispatch_*}) never resolve to a function id, whatever a {@link - * MethodPrefixer} names the bodies themselves. + * Returns the id of the WASM function compiled into {@code methodName}, or {@code -1} when no + * function was compiled under that name. */ private int funcIdForMethodName(String methodName) { return funcIdsByMethodName.getOrDefault(methodName, -1); diff --git a/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java b/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java index 86eed42f3..5228f6f1c 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java +++ b/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java @@ -334,13 +334,8 @@ static String sanitizeWasmName(String name) { } /** - * Recovers the WASM function id from a compiled method name by parsing the {@code _} - * suffix, or returns {@code -1} when the name does not end in one. - * - *

This is the reference implementation of the contract external tooling relies on to map a - * method name in a thread dump or profile back to a WASM function. The compiler itself does not - * use it: it resolves ids by exact lookup, which also excludes the bridge methods that happen to - * end in a number. + * Returns the WASM function id parsed from the {@code _} suffix of a compiled method + * name, or {@code -1} when the name does not end in one. */ static int extractFuncId(String methodName) { int lastUnderscore = methodName.lastIndexOf('_'); From 401b8b4ece24f7af02337eff21e1592f538dd951 Mon Sep 17 00:00:00 2001 From: andreatp Date: Thu, 3 Sep 2026 13:14:55 +0100 Subject: [PATCH 6/6] Scan the method names instead of indexing them in reverse --- .../java/run/endive/compiler/internal/Compiler.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java index bd34a6636..0d874286d 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Compiler.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Compiler.java @@ -163,7 +163,6 @@ public final class Compiler { private final boolean moduleHasTailCalls; private final boolean moduleHasObjectRefs; private final String[] methodNames; - private final Map funcIdsByMethodName; private boolean useBridgeClasses; private IntFunction callIndirectClassResolver; @@ -210,10 +209,8 @@ private Compiler( // function and definitions and call sites cannot disagree. var prefixer = requireNonNullElse(methodPrefixer, MethodPrefixer.defaultPrefixer()); this.methodNames = new String[this.functionTypes.size()]; - this.funcIdsByMethodName = new HashMap<>(this.methodNames.length); for (int funcId = 0; funcId < this.methodNames.length; funcId++) { this.methodNames[funcId] = methodNameForFunc(funcId, prefixer, module); - this.funcIdsByMethodName.put(this.methodNames[funcId], funcId); } } @@ -226,7 +223,12 @@ private String methodName(int funcId) { * function was compiled under that name. */ private int funcIdForMethodName(String methodName) { - return funcIdsByMethodName.getOrDefault(methodName, -1); + for (int funcId = 0; funcId < methodNames.length; funcId++) { + if (methodNames[funcId].equals(methodName)) { + return funcId; + } + } + return -1; } private Set collectCallRefTypeIds() {