[wasm] Reimplement jiterpreter float-to-int conversions (#81923)
authorKatelyn Gadd <kg@luminance.org>
Fri, 10 Feb 2023 16:24:45 +0000 (08:24 -0800)
committerGitHub <noreply@github.com>
Fri, 10 Feb 2023 16:24:45 +0000 (08:24 -0800)
Reimplement float-to-int conversions in the jiterpreter by generating a series of conditional checks that match what clang does, in order to work around the wasm conversion opcode being unspecified for nan/inf.
Fix bug in jiterpreter instrumentation that would suppress the dumped module blob and error message if an unhandled error occurred while generating a block.
Fixes #81900

src/mono/mono/mini/interp/jiterpreter.c
src/mono/wasm/runtime/cwraps.ts
src/mono/wasm/runtime/jiterpreter-support.ts
src/mono/wasm/runtime/jiterpreter-trace-generator.ts
src/mono/wasm/runtime/jiterpreter.ts

index f93cb1e..4b76477 100644 (file)
@@ -121,6 +121,26 @@ mono_jiterp_encode_leb52 (unsigned char * destination, double doubleValue, int v
        }
 }
 
+EMSCRIPTEN_KEEPALIVE int
+mono_jiterp_encode_leb_signed_boundary (unsigned char * destination, int bits, int sign) {
+       if (!destination)
+               return 0;
+
+       int64_t value;
+       switch (bits) {
+               case 32:
+                       value = sign >= 0 ? INT_MAX : INT_MIN;
+                       break;
+               case 64:
+                       value = sign >= 0 ? INT64_MAX : INT64_MIN;
+                       break;
+               default:
+                       return 0;
+       }
+
+       return mono_jiterp_encode_leb64_ref(destination, &value, TRUE);
+}
+
 // Many of the following functions implement various opcodes or provide support for opcodes
 //  so that jiterpreter traces don't have to inline dozens of wasm instructions worth of
 //  complex logic - these are designed to match interp.c
@@ -384,7 +404,7 @@ mono_jiterp_box_ref (MonoVTable *vtable, MonoObject **dest, void *src, gboolean
 }
 
 EMSCRIPTEN_KEEPALIVE int
-mono_jiterp_conv_ovf (void *dest, void *src, int opcode) {
+mono_jiterp_conv (void *dest, void *src, int opcode) {
        switch (opcode) {
                case MINT_CONV_OVF_I4_I8: {
                        gint64 val = *(gint64*)src;
@@ -432,6 +452,17 @@ mono_jiterp_conv_ovf (void *dest, void *src, int opcode) {
                        }
                        return 0;
                }
+
+               case MINT_CONV_OVF_I8_R8:
+               case MINT_CONV_OVF_I8_R4: {
+                       double val;
+                       if (opcode == MINT_CONV_OVF_I8_R4)
+                               val = *(float*)src;
+                       else
+                               val = *(double*)src;
+
+                       return mono_try_trunc_i64(val, dest);
+               }
        }
 
        // TODO: return 0 on success and a unique bailout code on failure?
index c77eae3..17e04c0 100644 (file)
@@ -106,6 +106,7 @@ const fn_signatures: SigLine[] = [
     [true, "mono_jiterp_get_offset_of_array_data", "number", []],
     [false, "mono_jiterp_encode_leb52", "number", ["number", "number", "number"]],
     [false, "mono_jiterp_encode_leb64_ref", "number", ["number", "number", "number"]],
+    [false, "mono_jiterp_encode_leb_signed_boundary", "number", ["number", "number", "number"]],
     [true, "mono_jiterp_type_is_byref", "number", ["number"]],
     [true, "mono_jiterp_get_size_of_stackval", "number", []],
     [true, "mono_jiterp_parse_option", "number", ["string"]],
@@ -248,6 +249,10 @@ export interface t_Cwraps {
     // Returns bytes written (or 0 if writing failed)
     // Source is the address of a 64-bit int or uint
     mono_jiterp_encode_leb64_ref(destination: VoidPtr, source: VoidPtr, valueIsSigned: number): number;
+    // Returns bytes written (or 0 if writing failed)
+    // bits is either 32 or 64 (the size of the value)
+    // sign is >= 0 for INTnn_MAX and < 0 for INTnn_MIN
+    mono_jiterp_encode_leb_signed_boundary(destination: VoidPtr, bits: number, sign: number): number;
     mono_jiterp_type_is_byref(type: MonoType): number;
     mono_jiterp_get_size_of_stackval(): number;
     mono_jiterp_type_get_raw_value_size(type: MonoType): number;
index 845c73f..9c33caf 100644 (file)
@@ -141,6 +141,10 @@ export class WasmBuilder {
         return this.current.appendF64(value);
     }
 
+    appendBoundaryValue (bits: number, sign: number) {
+        return this.current.appendBoundaryValue(bits, sign);
+    }
+
     appendULeb (value: number | MintOpcodePtr) {
         return this.current.appendULeb(<any>value);
     }
@@ -517,7 +521,7 @@ export class WasmBuilder {
 
     getArrayView (fullCapacity?: boolean) {
         if (this.stackSize > 1)
-            throw new Error("Stack not empty");
+            throw new Error("Jiterpreter block stack not empty");
         return this.stack[0].getArrayView(fullCapacity);
     }
 
@@ -603,6 +607,17 @@ export class BlobBuilder {
         return result;
     }
 
+    appendBoundaryValue (bits: number, sign: number) {
+        if (this.size + 8 >= this.capacity)
+            throw new Error("Buffer full");
+
+        const bytesWritten = cwraps.mono_jiterp_encode_leb_signed_boundary(<any>(this.buffer + this.size), bits, sign);
+        if (bytesWritten < 1)
+            throw new Error(`Failed to encode ${bits} bit boundary value with sign ${sign}`);
+        this.size += bytesWritten;
+        return bytesWritten;
+    }
+
     appendULeb (value: number) {
         if (this.size + 8 >= this.capacity)
             throw new Error("Buffer full");
index 58d8de4..35959b9 100644 (file)
@@ -652,14 +652,16 @@ export function generate_wasm_body (
             case MintOpcode.MINT_CONV_OVF_U4_I8:
             case MintOpcode.MINT_CONV_OVF_I4_U8:
             case MintOpcode.MINT_CONV_OVF_I4_R8:
+            case MintOpcode.MINT_CONV_OVF_I8_R8:
             case MintOpcode.MINT_CONV_OVF_I4_R4:
+            case MintOpcode.MINT_CONV_OVF_I8_R4:
             case MintOpcode.MINT_CONV_OVF_U4_I4:
                 builder.block();
                 // dest, src
                 append_ldloca(builder, getArgU16(ip, 1), 8, true);
                 append_ldloca(builder, getArgU16(ip, 2), 0);
                 builder.i32_const(opcode);
-                builder.callImport("conv_ovf");
+                builder.callImport("conv");
                 // If the conversion succeeded, continue, otherwise bailout
                 builder.appendU8(WasmOpcode.br_if);
                 builder.appendULeb(0);
@@ -667,6 +669,87 @@ export function generate_wasm_body (
                 builder.endBlock();
                 break;
 
+            /*
+             *  The native conversion opcodes for these are not specified for nan/inf, and v8
+             *  chooses to throw, so we have to do some tricks to identify non-finite values
+             *  and substitute INTnn_MIN, like clang would.
+             *  This attempts to reproduce what clang does in -O3 with no special flags set:
+             *
+             *  f64 -> i64
+             *
+             *  block
+             *  local.get       0
+             *  f64.abs
+             *  f64.const       0x1p63
+             *  f64.lt
+             *  i32.eqz
+             *  br_if           0                               # 0: down to label0
+             *  local.get       0
+             *  i64.trunc_f64_s
+             *  return
+             *  end_block                               # label0:
+             *  i64.const       -9223372036854775808
+             *
+             *  f32 -> i32
+             *
+             *  block
+             *  local.get       0
+             *  f32.abs
+             *  f32.const       0x1p31
+             *  f32.lt
+             *  i32.eqz
+             *  br_if           0                               # 0: down to label3
+             *  local.get       0
+             *  i32.trunc_f32_s
+             *  return
+             *  end_block                               # label3:
+             *  i32.const       -2147483648
+             */
+            case MintOpcode.MINT_CONV_I4_R4:
+            case MintOpcode.MINT_CONV_I4_R8:
+            case MintOpcode.MINT_CONV_I8_R4:
+            case MintOpcode.MINT_CONV_I8_R8: {
+                const isF32 = (opcode === MintOpcode.MINT_CONV_I4_R4) ||
+                        (opcode === MintOpcode.MINT_CONV_I8_R4),
+                    isI64 = (opcode === MintOpcode.MINT_CONV_I8_R4) ||
+                        (opcode === MintOpcode.MINT_CONV_I8_R8),
+                    limit = isI64
+                        ? 9223372036854775807 // this will round up to 0x1p63
+                        : 2147483648, // this is 0x1p31 exactly
+                    tempLocal = isF32 ? "temp_f32" : "temp_f64";
+
+                // Pre-load locals for the result store at the end
+                builder.local("pLocals");
+
+                // Load src
+                append_ldloc(builder, getArgU16(ip, 2), isF32 ? WasmOpcode.f32_load : WasmOpcode.f64_load);
+                builder.local(tempLocal, WasmOpcode.tee_local);
+
+                // Detect whether the value is within the representable range for the target type
+                builder.appendU8(isF32 ? WasmOpcode.f32_abs : WasmOpcode.f64_abs);
+                builder.appendU8(isF32 ? WasmOpcode.f32_const : WasmOpcode.f64_const);
+                if (isF32)
+                    builder.appendF32(limit);
+                else
+                    builder.appendF64(limit);
+                builder.appendU8(isF32 ? WasmOpcode.f32_lt : WasmOpcode.f64_lt);
+
+                // Select value via an if block that returns the result
+                builder.block(isI64 ? WasmValtype.i64 : WasmValtype.i32, WasmOpcode.if_);
+                // Value in range so truncate it to the appropriate type
+                builder.local(tempLocal);
+                builder.appendU8(floatToIntTable[opcode]);
+                builder.appendU8(WasmOpcode.else_);
+                // Value out of range so load the appropriate boundary value
+                builder.appendU8(isI64 ? WasmOpcode.i64_const : WasmOpcode.i32_const);
+                builder.appendBoundaryValue(isI64 ? 64 : 32, -1);
+                builder.endBlock();
+
+                append_stloc_tail(builder, getArgU16(ip, 1), isI64 ? WasmOpcode.i64_store : WasmOpcode.i32_store);
+
+                break;
+            }
+
             case MintOpcode.MINT_ADD_MUL_I4_IMM:
             case MintOpcode.MINT_ADD_MUL_I8_IMM: {
                 const isI32 = opcode === MintOpcode.MINT_ADD_MUL_I4_IMM;
@@ -793,8 +876,11 @@ export function generate_wasm_body (
             // For debugging
             if (emitPadding)
                 builder.appendU8(WasmOpcode.nop);
-        } else
+        } else {
+            if (instrumentedTraceId)
+                console.log(`instrumented trace ${traceName} aborted for opcode ${opname} @${_ip}`);
             record_abort(traceIp, _ip, traceName, opcode);
+        }
     }
 
     if (emitPadding)
@@ -1429,6 +1515,13 @@ type OpRec3 = [WasmOpcode, WasmOpcode, WasmOpcode];
 // operator, lhsLoadOperator, rhsLoadOperator, storeOperator
 type OpRec4 = [WasmOpcode, WasmOpcode, WasmOpcode, WasmOpcode];
 
+const floatToIntTable : { [opcode: number]: WasmOpcode } = {
+    [MintOpcode.MINT_CONV_I4_R4]: WasmOpcode.i32_trunc_s_f32,
+    [MintOpcode.MINT_CONV_I8_R4]: WasmOpcode.i64_trunc_s_f32,
+    [MintOpcode.MINT_CONV_I4_R8]: WasmOpcode.i32_trunc_s_f64,
+    [MintOpcode.MINT_CONV_I8_R8]: WasmOpcode.i64_trunc_s_f64,
+};
+
 // thanks for making this as complex as possible, typescript
 const unopTable : { [opcode: number]: OpRec3 | undefined } = {
     [MintOpcode.MINT_CEQ0_I4]:    [WasmOpcode.i32_eqz,   WasmOpcode.i32_load, WasmOpcode.i32_store],
@@ -1457,11 +1550,6 @@ const unopTable : { [opcode: number]: OpRec3 | undefined } = {
     [MintOpcode.MINT_CONV_R8_R4]: [WasmOpcode.f64_promote_f32,   WasmOpcode.f32_load, WasmOpcode.f64_store],
     [MintOpcode.MINT_CONV_R4_R8]: [WasmOpcode.f32_demote_f64,    WasmOpcode.f64_load, WasmOpcode.f32_store],
 
-    [MintOpcode.MINT_CONV_I4_R4]: [WasmOpcode.i32_trunc_s_f32,   WasmOpcode.f32_load, WasmOpcode.i32_store],
-    [MintOpcode.MINT_CONV_I8_R4]: [WasmOpcode.i64_trunc_s_f32,   WasmOpcode.f32_load, WasmOpcode.i64_store],
-    [MintOpcode.MINT_CONV_I4_R8]: [WasmOpcode.i32_trunc_s_f64,   WasmOpcode.f64_load, WasmOpcode.i32_store],
-    [MintOpcode.MINT_CONV_I8_R8]: [WasmOpcode.i64_trunc_s_f64,   WasmOpcode.f64_load, WasmOpcode.i64_store],
-
     [MintOpcode.MINT_CONV_I8_I4]: [WasmOpcode.nop,               WasmOpcode.i64_load32_s, WasmOpcode.i64_store],
     [MintOpcode.MINT_CONV_I8_U4]: [WasmOpcode.nop,               WasmOpcode.i64_load32_u, WasmOpcode.i64_store],
 
index 79bf154..0455fcb 100644 (file)
@@ -251,7 +251,7 @@ function getTraceImports () {
         ["newobj_i", "newobj_i", getRawCwrap("mono_jiterp_try_newobj_inlined")],
         ["ld_del_ptr", "ld_del_ptr", getRawCwrap("mono_jiterp_ld_delegate_method_ptr")],
         ["ldtsflda", "ldtsflda", getRawCwrap("mono_jiterp_ldtsflda")],
-        ["conv_ovf", "conv_ovf", getRawCwrap("mono_jiterp_conv_ovf")],
+        ["conv", "conv", getRawCwrap("mono_jiterp_conv")],
         ["relop_fp", "relop_fp", getRawCwrap("mono_jiterp_relop_fp")],
         ["safepoint", "safepoint", getRawCwrap("mono_jiterp_auto_safepoint")],
         ["hashcode", "hashcode", getRawCwrap("mono_jiterp_get_hashcode")],
@@ -461,7 +461,7 @@ function initialize_builder (builder: WasmBuilder) {
         }, WasmValtype.void, true
     );
     builder.defineType(
-        "conv_ovf", {
+        "conv", {
             "destination": WasmValtype.i32,
             "source": WasmValtype.i32,
             "opcode": WasmValtype.i32,
@@ -630,8 +630,9 @@ function generate_wasm (
             "math_lhs32": WasmValtype.i32,
             "math_rhs32": WasmValtype.i32,
             "math_lhs64": WasmValtype.i64,
-            "math_rhs64": WasmValtype.i64
-            // "tempi64": WasmValtype.i64
+            "math_rhs64": WasmValtype.i64,
+            "temp_f32": WasmValtype.f32,
+            "temp_f64": WasmValtype.f64,
         });
 
         if (emitPadding) {
@@ -751,6 +752,11 @@ function generate_wasm (
             console.log(`// MONO_WASM: ${traceName} generated, blob follows //`);
             let s = "", j = 0;
             try {
+                // We may have thrown an uncaught exception while inside a block,
+                //  so we need to pop it for getArrayView to work.
+                while (builder.activeBlocks > 0)
+                    builder.endBlock();
+
                 if (builder.inSection)
                     builder.endSection();
             } catch {