Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ jobs:
ci:
name: CI
runs-on: ${{ matrix.os }}
continue-on-error: true
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -44,7 +43,8 @@ jobs:
with:
repository: WebAssembly/wasi-testsuite
path: wasi-testsuite
ref: prod/testsuite-base
# Keep in sync with the default ref in wasi-test-gen-plugin.
ref: 6345da2237c562d9a94e281332c653b5528fdd52
- name: Set up Java
uses: actions/setup-java@v5
with:
Expand All @@ -55,6 +55,23 @@ jobs:
run: mvn -B clean install
env:
MAVEN_OPTS: "-ea"
- name: Collect ARM JVM diagnostics
if: failure() && runner.os == 'Linux' && runner.arch == 'ARM64'
run: |
lscpu > ci-cpu.txt
java -XX:+PrintFlagsFinal -version > ci-jvm.txt 2>&1
- name: Upload failure diagnostics
if: failure()
uses: actions/upload-artifact@v7
with:
name: ci-failure-${{ matrix.os }}-java-${{ matrix.version }}
path: |
**/target/surefire-reports/
runtime-tests/target/generated-resources/compiled-wast/comments/
runtime/target/runtime-*.jar
wasm-tools/target/wasm-tools-*.jar
ci-cpu.txt
ci-jvm.txt

docs-ci:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -84,7 +101,6 @@ jobs:
test-results:
name: Test Results
runs-on: ubuntu-latest
continue-on-error: true
strategy:
fail-fast: false
matrix:
Expand All @@ -104,7 +120,8 @@ jobs:
with:
repository: WebAssembly/wasi-testsuite
path: wasi-testsuite
ref: prod/testsuite-base
# Keep in sync with the default ref in wasi-test-gen-plugin.
ref: 6345da2237c562d9a94e281332c653b5528fdd52
- name: Set up Java
uses: actions/setup-java@v5
with:
Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/teavm.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: NGE TeaVM Runtime

on:
push:
branches: [ main ]
pull_request:

permissions:
contents: read

jobs:
runtime:
name: Runtime and build-time modules (${{ matrix.backend }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
backend: [javascript, wasm-gc]
steps:
- name: Checkout sources
uses: actions/checkout@v6
- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: '25'
cache: maven
- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
- name: Compile and execute with the NGE TeaVM fork
run: mvn -B -ntp -U -Dteavm-tests -Dteavm.backend=${{ matrix.backend }} -pl teavm-tests -am verify
- name: Upload generated TeaVM application
if: always()
uses: actions/upload-artifact@v7
with:
name: teavm-runtime-${{ matrix.backend }}
path: teavm-tests/target/${{ matrix.backend }}/
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public static TestModule of(String classpath) {
parsed = Wat2Wasm.parse(is);
} catch (RuntimeException e) {
throw new MalformedException(
e.getMessage() + HACK_MATCH_ALL_MALFORMED_EXCEPTION_TEXT);
e.getMessage() + HACK_MATCH_ALL_MALFORMED_EXCEPTION_TEXT, e);
}
return of(Parser.parse(parsed));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,22 @@ public final class Compiler {
private static final MethodType MACHINE_CALL_METHOD_TYPE =
methodType(long[].class, Instance.class, Memory.class, int.class, long[].class);

private static final int MAX_MACHINE_CALL_METHODS = 1024; // must be power of two
// C2 JIT's HugeMethodLimit (default 8KB) — methods exceeding this get degraded optimization.
// Dispatch chunks are sized to stay under this limit for full C2 compilation.
// Estimated upper bound: tableswitch offset (4) + label + invokestatic (~5) + areturn (1) +
// overhead
private static final int ESTIMATED_BYTES_PER_DISPATCH_ENTRY = 40;
private static final int MAX_DISPATCH_METHODS =
dispatchChunkSize(Integer.getInteger("chicory.hugeMethodLimit", 8000));
private static final int MAX_CALL_INDIRECT_METHODS = 1024; // must be power of two

static int dispatchChunkSize(int hugeMethodLimit) {
// Keep chunking valid even for zero, negative or very large property values.
int entries =
Math.max(1, Math.min(1024, hugeMethodLimit / ESTIMATED_BYTES_PER_DISPATCH_ENTRY));
return Integer.highestOneBit(entries);
}

// 1024*12 was empirically determined to work for the 50K small wasm functions.
// So lets start there and halve it until we find a size that works.
// This should give us the biggest class size possible.
Expand Down Expand Up @@ -547,15 +562,23 @@ private byte[] compileClass() {
}

// value_xxx() bridges for multi-value return
// Deduplicate by method name, not by type list, because different
// reference types (e.g. RefNull(funcref) vs RefNull(externref)) produce
// the same JVM method name and signature.
var seenValueMethods = new java.util.HashSet<String>();
var returnTypes =
functionTypes.stream()
.map(FunctionType::returns)
.filter(types -> types.size() > 1)
.collect(toSet());
for (var types : returnTypes) {
var methodName = valueMethodName(types);
if (!seenValueMethods.add(methodName)) {
continue;
}
emitFunction(
classWriter,
valueMethodName(types),
methodName,
valueMethodType(types),
true,
asm -> {
Expand Down Expand Up @@ -773,11 +796,11 @@ private void compileMachineCallClass() {

// static implementation for Machine.call()
Consumer<InstructionAdapter> callMethod;
if (functionTypes.size() < MAX_MACHINE_CALL_METHODS) {
if (functionTypes.size() < MAX_DISPATCH_METHODS) {
callMethod = asm -> compileMachineCallInvoke(asm, 0, functionTypes.size());
} else {
// Best value that worked with the 50K small wasm functions
var maxMachineCallMethods = MAX_MACHINE_CALL_METHODS << 2;
var maxMachineCallMethods = MAX_DISPATCH_METHODS << 2;
maxMachineCallMethods =
loadChunkedClass(
functionTypes.size(),
Expand Down Expand Up @@ -1034,7 +1057,7 @@ private void compileCallIndirect(
asm.load(instance, OBJECT_TYPE);

// Can we fit the impl in a single method?
if (validIds.size() <= MAX_MACHINE_CALL_METHODS) {
if (validIds.size() <= MAX_CALL_INDIRECT_METHODS) {

int[] keys = validIds.stream().mapToInt(x -> x).toArray();
Label[] labels = validIds.stream().map(x -> new Label()).toArray(Label[]::new);
Expand All @@ -1061,10 +1084,10 @@ private void compileCallIndirect(
.appendParameterTypes(Memory.class, Instance.class, int.class);

// Best value that worked with the 50K small wasm functions
var maxMachineCallMethods = MAX_MACHINE_CALL_METHODS << 2;
var maxCallIndirectMethods = MAX_CALL_INDIRECT_METHODS << 2;
loadChunkedClass(
functionTypes.size(),
maxMachineCallMethods,
maxCallIndirectMethods,
(collector, start, end, chunkSize) ->
compileExtraClass(
collector,
Expand All @@ -1085,8 +1108,8 @@ private void compileCallIndirect(
end));
}));

assert Integer.bitCount(maxMachineCallMethods) == 1; // power of two
int shift = Integer.numberOfTrailingZeros(maxMachineCallMethods);
assert Integer.bitCount(maxCallIndirectMethods) == 1; // power of two
int shift = Integer.numberOfTrailingZeros(maxCallIndirectMethods);

// switch (funcId >> shift)
Label[] labels = new Label[((functionTypes.size() - 1) >> shift) + 1];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public void scopeRestore() {
types.push(restore.getFirst());
}

public int scopeStackSize(Instruction scope) {
public Integer scopeStackSize(Instruction scope) {
return scopes.get(scope);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public static class TryCatchBlock {
// types of values below the try scope that need to be saved/restored
int savedStackSlotBase;
List<ValType> savedStackTypes;
// set to true when TRY_CATCH_BLOCK was emitted (i.e., block is reachable)
boolean registered;

public TryCatchBlock(
AnnotatedInstruction ins,
Expand All @@ -113,6 +115,12 @@ public AnalysisResult analyze(int funcId) {
int nextLabel = body.instructions().size();
int trySaveSlotOffset = 0;
List<CompilerInstruction> result = new ArrayList<>();
// Collect TRY_CATCH_BLOCK instructions during the forward pass and
// prepend them in reverse order at the end. JVM tries exception handlers
// in registration order, so inner (nested) handlers must come before
// outer ones. The forward pass encounters outer try_tables first, so we
// reverse the collected list to get inner-first order.
List<CompilerInstruction> tryCatchBlockInstructions = new ArrayList<>();

// find label targets
Set<Integer> labels = new HashSet<>();
Expand Down Expand Up @@ -152,12 +160,6 @@ public AnalysisResult analyze(int funcId) {

var block = new TryCatchBlock(ins, start, end, handle, after, afterCatchLabels);
tryCatchBlocks.put(ins.address(), block);
result.add(
new CompilerInstruction(
CompilerOpCode.TRY_CATCH_BLOCK,
block.start,
block.end,
block.handler));
}
}

Expand All @@ -181,6 +183,19 @@ public AnalysisResult analyze(int funcId) {

exitBlockDepth = -1;
if (ins.opcode() == OpCode.END) {
// Emit try-catch handler labels even in unreachable code,
// otherwise ASM's computeAllFrames fails with NPE because
// visitTryCatchBlock was called but the handler label was
// never visited.
if (ins.scope().opcode() == OpCode.TRY_TABLE) {
var tryCatchBlock = tryCatchBlocks.remove(ins.scope().address());
if (tryCatchBlock != null && tryCatchBlock.registered) {
// TRY_TABLE was reachable but END is unreachable —
// still need to emit handler labels since the
// try-catch range was already registered with ASM
analyzeTryCatchEnd(result, tryCatchBlock);
}
}
stack.scopeRestore();
}
}
Expand Down Expand Up @@ -357,6 +372,16 @@ public AnalysisResult analyze(int funcId) {
}

stack.enterScope(ins.scope(), blockType(ins));
// Collect TRY_CATCH_BLOCK (emitted at end in reverse order).
// Only register reachable TRY_TABLE blocks — unreachable ones
// are skipped, avoiding ASM NPE from unvisited labels.
tryCatchBlock.registered = true;
tryCatchBlockInstructions.add(
new CompilerInstruction(
CompilerOpCode.TRY_CATCH_BLOCK,
tryCatchBlock.start,
tryCatchBlock.end,
tryCatchBlock.handler));
result.add(
new CompilerInstruction(CompilerOpCode.LABEL, tryCatchBlock.start));
break;
Expand Down Expand Up @@ -656,6 +681,12 @@ public AnalysisResult analyze(int funcId) {
result.add(new CompilerInstruction(CompilerOpCode.RETURN, ids(functionType.returns())));

stack.verifyEmpty();

// Prepend TRY_CATCH_BLOCK instructions in reverse order so that inner
// (nested) handlers are registered before outer ones in the JVM.
reverse(tryCatchBlockInstructions);
result.addAll(0, tryCatchBlockInstructions);

return new AnalysisResult(result, computeMaxTempSlots(result));
}

Expand Down Expand Up @@ -1513,8 +1544,14 @@ private Optional<CompilerInstruction> unwindStack(
var types = forward ? blockType.returns() : blockType.params();
int keep = types.size();

// scope may have been exited already (unreachable code after unconditional branch)
var scopeSize = stack.scopeStackSize(scope);
if (scopeSize == null) {
return Optional.empty();
}

// for a backward jump, the initial loop parameters are dropped
int drop = stack.types().size() - stack.scopeStackSize(scope);
int drop = stack.types().size() - scopeSize;

// do not drop the return values for a forward jump
if (forward) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,26 @@

public class CallTest {

@Test
public void indirectTailCallCanTargetAnotherInstance() {
var module =
Parser.parse(
CorpusResources.getResource("compiled/cross-instance-tail-call.wat.wasm"));
for (boolean compiledTarget : new boolean[] {false, true}) {
var targetBuilder = Instance.builder(module);
if (compiledTarget) {
targetBuilder.withMachineFactory(MachineFactoryCompiler::compile);
}
var target = targetBuilder.build();
var caller =
Instance.builder(module)
.withMachineFactory(MachineFactoryCompiler::compile)
.build();
caller.table(0).setRef(0, 0, target);
assertArrayEquals(new long[] {42}, caller.export("run").apply(41));
}
}

@Test
public void callLotsOfArgs() throws InterruptedException {
var module = Parser.parse(CorpusResources.getResource("compiled/lots-of-args.wat.wasm"));
Expand Down
Loading
Loading