Skip to content

Compiler traps on array.len of a null array where the interpreter runs the same module fine (Kotlin/Wasm) #139

Description

@philip-segerfast

A Kotlin/Wasm module runs correctly on InterpreterMachine and traps on MachineFactoryCompiler, on the same bytes:

run.endive.runtime.TrapException: null array reference
	at run.endive.$gen.CompiledMachineShaded.arrayLen(Shaded.java:1170)
	at run.endive.$gen.CompiledMachineFuncGroup_0.func_940(Unknown Source)
	at run.endive.$gen.CompiledMachineFuncGroup_0.func_941(Unknown Source)
	at run.endive.$gen.CompiledMachineFuncGroup_0.func_939(Unknown Source)
	at run.endive.$gen.CompiledMachineFuncGroup_0.func_901(Unknown Source)
	at run.endive.$gen.CompiledMachineFuncGroup_0.func_886(Unknown Source)
	...
	at run.endive.$gen.CompiledMachine.call_indirect_89(wasm)

Endive 1.0.1, JDK 21, macOS aarch64. I have not tried it on main.

Reproducing

Everything is in https://github.com/philip-segerfast/endive-foldchars-repro — one command, no setup beyond a JDK:

./mvnw -q compile exec:java
interp   OK
compiled TrapException: null array reference

It builds one Instance per machine and runs _start on each, against run.endive:*:1.0.1 from Maven Central. Every import is stubbed from its declared type and returns zeroes, except call_host, which writes a fixed error string into memory — that is what sends the guest down its failure path.

Where it traps

Printing the module with wasm-tools print names the frames:

frame function
func_940 kotlin.String.foldChars — the array.len
func_941 kotlin.String.<get-chars>
func_939 kotlin.String.get
func_901 kotlin.text.encodeUtf8
func_886 kotlin.text.encodeToByteArray

Kotlin/Wasm represents a string concatenation as a rope: the $kotlin.String struct has $leftIfInSum pointing at the left operand and $_chars holding the right operand's characters. <get-chars> flattens it on first use:

struct.get $kotlin.String $leftIfInSum
ref.is_null
i32.eqz
if
  call $kotlin.String.foldChars
end
struct.get $kotlin.String $_chars

foldChars walks the $leftIfInSum chain, and the trap is on its read of each link:

local.get $currentLeftString
struct.get $kotlin.String $_chars
local.tee $currentLeftStringChars
array.len                            ;; traps here

and it finishes by writing the flattened array back:

struct.set $kotlin.String $_chars
ref.null none
struct.set $kotlin.String $leftIfInSum

Every node in that chain should have a non-null $_chars — a sum node carries the right operand's characters, a folded node carries its own. For the compiler to reach array.len on null, I think either a struct.set $_chars from an earlier fold is not visible to a later read, or a struct.get $_chars is yielding null where the interpreter yields the array. I have not been able to tell which, and that is a guess about your compiler rather than something I have shown.

What it is not

These all behave identically on both machines, so they are ruled out:

  • Exception handling in general — try/catch, throwing and catching through try_table, reading the message.
  • A minimal hand-written try_table module.
  • vararg with no arguments.
  • String concatenation of a boolean on its own.

Minimisation, and where I got stuck

The module is 405 KB, nearly all Kotlin stdlib, which I know is not a nice thing to hand you. Two attempts to reduce it failed:

wasm-tools shrink made no progress at all — 0.00%. The predicate is in the repo as Predicate.java; it exits 0 only when the module still runs interpreted and traps compiled with this message. I checked it accepts the failing module and rejects both a passing module and an empty one. My reading is that in a GC module the recursive type graph is interdependent enough that nearly every removal fails validation, so the reducer has nowhere to go.

A hand-written module with the same shape does not reproduce it. wat/rope-model.wat is 424 bytes with a struct holding a mutable nullable array field and a link field, the same walk, the same array.len, array.copy, and the same write-back. It behaves identically on both machines. So whatever is going wrong needs more than that shape — the real module has ~1500 functions and reaches this through call_indirect.

If there is a better way to reduce a GC module against a predicate, I am happy to run it.

Context

I hit this while evaluating compiled execution for a Minecraft mod that runs Kotlin compiled to Wasm. It is not blocking me — I have kept that mode opt-in and default to the interpreter — so there is no urgency from my side. I am reporting it because the compiler and the interpreter disagreeing about what a program means seemed worth knowing about, and because it appeared on the first non-trivial Kotlin program I compiled, on an ordinary error path.

Happy to test a patch, provide more traces, or try things against main if that would help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions