Skip to content

BREAKING CHANGE: Enable non trapping float to int convertions by default #2107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
2 changes: 1 addition & 1 deletion cli/asc.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@
"description": [
"Enables WebAssembly features being disabled by default.",
"",
" nontrapping-f2i Non-trapping float to integer ops.",
" bulk-memory Bulk memory operations.",
" simd SIMD types and operations.",
" threads Threading and atomic operations.",
Expand All @@ -239,6 +238,7 @@
"",
" mutable-globals Mutable global imports and exports.",
" sign-extension Sign-extension operations",
" nontrapping-f2i Non-trapping float to integer ops.",
""
],
"type": "S",
Expand Down
94 changes: 64 additions & 30 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ export class Options {
/** Global aliases, mapping alias names as the key to internal names to be aliased as the value. */
globalAliases: Map<string,string> | null = null;
/** Features to activate by default. These are the finished proposals. */
features: Feature = Feature.MUTABLE_GLOBALS | Feature.SIGN_EXTENSION;
features: Feature = Feature.MUTABLE_GLOBALS | Feature.SIGN_EXTENSION | Feature.NONTRAPPING_F2I;
/** If true, disallows unsafe features in user code. */
noUnsafe: bool = false;
/** If true, enables pedantic diagnostics. */
Expand Down Expand Up @@ -428,7 +428,7 @@ export class Compiler extends DiagnosticEmitter {
}
var featureFlags: FeatureFlags = 0;
if (options.hasFeature(Feature.SIGN_EXTENSION)) featureFlags |= FeatureFlags.SignExt;
if (options.hasFeature(Feature.MUTABLE_GLOBALS)) featureFlags |= FeatureFlags.MutableGloabls;
if (options.hasFeature(Feature.MUTABLE_GLOBALS)) featureFlags |= FeatureFlags.MutableGlobals;
if (options.hasFeature(Feature.NONTRAPPING_F2I)) featureFlags |= FeatureFlags.TruncSat;
if (options.hasFeature(Feature.BULK_MEMORY)) featureFlags |= FeatureFlags.BulkMemory;
if (options.hasFeature(Feature.SIMD)) featureFlags |= FeatureFlags.SIMD;
Expand Down Expand Up @@ -10271,38 +10271,72 @@ export class Compiler extends DiagnosticEmitter {
: expr;
}
case TypeKind.F32: {
// 0 < abs(bitCast(x)) <= bitCast(Infinity) or
// (reinterpret<u32>(x) & 0x7FFFFFFF) - 1 <= 0x7F800000 - 1
//
// and finally:
// (reinterpret<u32>(x) << 1) - (1 << 1) <= ((0x7F800000 - 1) << 1)
return module.binary(BinaryOp.LeU32,
module.binary(BinaryOp.SubI32,
module.binary(BinaryOp.ShlI32,
module.unary(UnaryOp.ReinterpretF32ToI32, expr),
module.i32(1)
if (
this.options.shrinkLevelHint > 1 &&
this.options.hasFeature(Feature.NONTRAPPING_F2I)
) {
// Use more compact but slower 5-byte (3 bytes in best case) approach
// !!(i32.trunc_sat_f32_u(f32.ceil(f32.abs(x))))
return module.unary(UnaryOp.EqzI32,
module.unary(UnaryOp.EqzI32,
module.unary(UnaryOp.TruncSatF32ToU32,
module.unary(UnaryOp.CeilF32,
module.unary(UnaryOp.AbsF32, expr)
)
)
)
);
} else {
// 0 < abs(bitCast(x)) <= bitCast(Infinity) or
// (reinterpret<u32>(x) & 0x7FFFFFFF) - 1 <= 0x7F800000 - 1
//
// and finally:
// (reinterpret<u32>(x) << 1) - (1 << 1) <= ((0x7F800000 - 1) << 1)
return module.binary(BinaryOp.LeU32,
module.binary(BinaryOp.SubI32,
module.binary(BinaryOp.ShlI32,
module.unary(UnaryOp.ReinterpretF32ToI32, expr),
module.i32(1)
),
module.i32(2) // 1 << 1
),
module.i32(2) // 1 << 1
),
module.i32(0xFEFFFFFE) // (0x7F800000 - 1) << 1
);
module.i32(0xFEFFFFFE) // (0x7F800000 - 1) << 1
);
}
}
case TypeKind.F64: {
// 0 < abs(bitCast(x)) <= bitCast(Infinity) or
// (reinterpret<u64>(x) & 0x7FFFFFFFFFFFFFFF) - 1 <= 0x7FF0000000000000 - 1
//
// and finally:
// (reinterpret<u64>(x) << 1) - (1 << 1) <= ((0x7FF0000000000000 - 1) << 1)
return module.binary(BinaryOp.LeU64,
module.binary(BinaryOp.SubI64,
module.binary(BinaryOp.ShlI64,
module.unary(UnaryOp.ReinterpretF64ToI64, expr),
module.i64(1)
if (
this.options.shrinkLevelHint > 1 &&
this.options.hasFeature(Feature.NONTRAPPING_F2I)
) {
// Use more compact but slower 5-byte (3 bytes in best case) approach
// !!(i32.trunc_sat_f64_u(f64.ceil(f64.abs(x))))
return module.unary(UnaryOp.EqzI32,
module.unary(UnaryOp.EqzI32,
module.unary(UnaryOp.TruncSatF64ToU32,
module.unary(UnaryOp.CeilF64,
module.unary(UnaryOp.AbsF64, expr)
)
)
)
);
} else {
// 0 < abs(bitCast(x)) <= bitCast(Infinity) or
// (reinterpret<u64>(x) & 0x7FFFFFFFFFFFFFFF) - 1 <= 0x7FF0000000000000 - 1
//
// and finally:
// (reinterpret<u64>(x) << 1) - (1 << 1) <= ((0x7FF0000000000000 - 1) << 1)
return module.binary(BinaryOp.LeU64,
module.binary(BinaryOp.SubI64,
module.binary(BinaryOp.ShlI64,
module.unary(UnaryOp.ReinterpretF64ToI64, expr),
module.i64(1)
),
module.i64(2) // 1 << 1
),
module.i64(2) // 1 << 1
),
module.i64(0xFFFFFFFE, 0xFFDFFFFF) // (0x7FF0000000000000 - 1) << 1
);
module.i64(0xFFFFFFFE, 0xFFDFFFFF) // (0x7FF0000000000000 - 1) << 1
);
}
}
case TypeKind.V128: {
return module.unary(UnaryOp.AnyTrueV128, expr);
Expand Down
2 changes: 1 addition & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export namespace TypeRef {
export enum FeatureFlags {
MVP = 0 /* _BinaryenFeatureMVP */,
Atomics = 1 /* _BinaryenFeatureAtomics */,
MutableGloabls = 2 /* _BinaryenFeatureMutableGlobals */,
MutableGlobals = 2 /* _BinaryenFeatureMutableGlobals */,
TruncSat = 4 /* _BinaryenFeatureNontrappingFPToInt */,
SIMD = 8 /* _BinaryenFeatureSIMD128 */,
BulkMemory = 16 /* _BinaryenFeatureBulkMemory */,
Expand Down
66 changes: 47 additions & 19 deletions std/assembly/typedarray.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1894,7 +1894,10 @@ function WRAP<TArray extends ArrayBufferView, T>(

// @ts-ignore: decorator
@inline
function SET<TArray extends ArrayBufferView, T, UArray extends ArrayBufferView, U>(
function SET<
TArray extends ArrayBufferView, T extends number,
UArray extends ArrayBufferView, U extends number
>(
target: TArray,
source: UArray,
offset: i32 = 0
Expand All @@ -1906,29 +1909,41 @@ function SET<TArray extends ArrayBufferView, T, UArray extends ArrayBufferView,

// Uncaught RangeError: offset is out of bounds
if (offset < 0) throw new RangeError(E_INDEXOUTOFRANGE);
if (source.length + offset > target.length) throw new RangeError(E_INDEXOUTOFRANGE);

let slen = source.length;
let tlen = target.length;

if (slen + offset > tlen) throw new RangeError(E_INDEXOUTOFRANGE);

let targetDataStart = target.dataStart + (<usize>offset << alignof<T>());
let sourceDataStart = source.dataStart;

// if the types align and match, use memory.copy() instead of manual loop
if (isInteger<T>() == isInteger<U>() && alignof<T>() == alignof<U>() &&
!(target instanceof Uint8ClampedArray && isSigned<U>())) {
memory.copy(
target.dataStart + (<usize>offset << alignof<T>()),
source.dataStart,
source.byteLength
);
if (
isInteger<T>() == isInteger<U>() &&
alignof<T>() == alignof<U>() &&
!(target instanceof Uint8ClampedArray && isSigned<U>())
) {
memory.copy(targetDataStart, sourceDataStart, source.byteLength);
} else {
let targetDataStart = target.dataStart + (<usize>offset << alignof<T>());
let sourceDataStart = source.dataStart;
let count = source.length;
for (let i = 0; i < count; i++) {
for (let i = 0; i < slen; i++) {
// if TArray is Uint8ClampedArray, then values must be clamped
if (target instanceof Uint8ClampedArray) {
if (isFloat<U>()) {
let value = load<U>(sourceDataStart + (<usize>i << alignof<U>()));
store<T>(
targetDataStart + (<usize>i << alignof<T>()),
isFinite<U>(value) ? <T>max<U>(0, min<U>(255, value)) : <T>0
);
if (!ASC_FEATURE_NONTRAPPING_F2I) {
store<T>(
targetDataStart + (<usize>i << alignof<T>()),
// @ts-ignore: cast
isFinite<U>(value) ? <T>max<U>(0, min<U>(255, value)) : <T>0
);
} else {
store<T>(
targetDataStart + (<usize>i << alignof<T>()),
// @ts-ignore: cast
<T>max<U>(0, min<U>(255, value))
);
}
} else {
let value = load<U>(sourceDataStart + (<usize>i << alignof<U>()));
if (!isSigned<U>()) {
Expand All @@ -1954,12 +1969,25 @@ function SET<TArray extends ArrayBufferView, T, UArray extends ArrayBufferView,
// if U is a float, then casting float to int must include a finite check
} else if (isFloat<U>() && !isFloat<T>()) {
let value = load<U>(sourceDataStart + (<usize>i << alignof<U>()));
// @ts-ignore: cast to T is valid for numeric types here
store<T>(targetDataStart + (<usize>i << alignof<T>()), isFinite<U>(value) ? <T>value : 0);

if (!ASC_FEATURE_NONTRAPPING_F2I) {
store<T>(
targetDataStart + (<usize>i << alignof<T>()),
// @ts-ignore: cast
isFinite<U>(value) ? <T>value : <T>0
);
} else {
store<T>(
targetDataStart + (<usize>i << alignof<T>()),
// @ts-ignore: cast
<T>value
);
}
} else if (isFloat<T>() && !isFloat<U>()) {
// @ts-ignore: In this case the <T> conversion is required
store<T>(targetDataStart + (<usize>i << alignof<T>()), <T>load<U>(sourceDataStart + (<usize>i << alignof<U>())));
} else {
// @ts-ignore: cast
store<T>(targetDataStart + (<usize>i << alignof<T>()), load<U>(sourceDataStart + (<usize>i << alignof<U>())));
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/compiler/asc-constants.untouched.wat
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
(global $~lib/ASC_SHRINK_LEVEL i32 (i32.const 0))
(global $~lib/ASC_FEATURE_SIGN_EXTENSION i32 (i32.const 1))
(global $~lib/ASC_FEATURE_MUTABLE_GLOBALS i32 (i32.const 1))
(global $~lib/ASC_FEATURE_NONTRAPPING_F2I i32 (i32.const 0))
(global $~lib/ASC_FEATURE_NONTRAPPING_F2I i32 (i32.const 1))
(global $~lib/ASC_FEATURE_BULK_MEMORY i32 (i32.const 0))
(global $~lib/ASC_FEATURE_SIMD i32 (i32.const 0))
(global $~lib/ASC_FEATURE_THREADS i32 (i32.const 0))
Expand Down Expand Up @@ -43,7 +43,7 @@
drop
i32.const 1
drop
i32.const 0
i32.const 1
drop
i32.const 0
drop
Expand Down
2 changes: 1 addition & 1 deletion tests/compiler/binary.untouched.wat
Original file line number Diff line number Diff line change
Expand Up @@ -2654,7 +2654,7 @@
f64.convert_i64_s
f64.const 1
call $~lib/math/NativeMath.pow
i64.trunc_f64_s
i64.trunc_sat_f64_s
global.set $binary/I
global.get $binary/I
i64.const 1
Expand Down
5 changes: 5 additions & 0 deletions tests/compiler/bool-Oz.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"asc_flags": [
"-Oz"
]
}
6 changes: 6 additions & 0 deletions tests/compiler/bool-Oz.optimized.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
(module
(memory $0 1)
(data (i32.const 12) ",")
(data (i32.const 24) "\01\00\00\00\14\00\00\00b\00o\00o\00l\00-\00O\00z\00.\00t\00s")
(export "memory" (memory $0))
)
61 changes: 61 additions & 0 deletions tests/compiler/bool-Oz.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
var f = <f32>2;
assert(<bool>f == true);
var f0 = <f32>+0.0;
assert(<bool>f0 == false);
var f1 = <f32>-0.0;
assert(<bool>f1 == false);
var f2 = <f32>+NaN;
assert(<bool>f2 == false);
var f3 = <f32>-NaN;
assert(<bool>f3 == false);
var f4 = +f32.MAX_VALUE;
assert(<bool>f4 == true);
var f5 = -f32.MAX_VALUE;
assert(<bool>f5 == true);
var f6 = <f32>+Infinity;
assert(<bool>f6 == true);
var f7 = <f32>-Infinity;
assert(<bool>f7 == true);
var f8 = +f32.MIN_VALUE;
assert(<bool>f8 == true);
var f9 = -f32.MIN_VALUE;
assert(<bool>f9 == true);
var f10 = reinterpret<f32>(1);
assert(<bool>f10 == true);
var f11 = reinterpret<f32>(0x7F800000 - 1);
assert(<bool>f11 == true);
var f12 = reinterpret<f32>(0x7F800000 + 1);
assert(<bool>f12 == false);
var f13 = reinterpret<f32>(0xFF800000 + 1);
assert(<bool>f13 == false);

var F = <f64>2;
assert(<bool>F == true);
var F0 = <f64>+0.0;
assert(<bool>F0 == false);
var F1 = <f64>-0.0;
assert(<bool>F1 == false);
var F2 = <f64>+NaN;
assert(<bool>F2 == false);
var F3 = <f64>-NaN;
assert(<bool>F3 == false);
var F4 = +f64.MAX_VALUE;
assert(<bool>F4 == true);
var F5 = -f64.MAX_VALUE;
assert(<bool>F5 == true);
var F6 = +Infinity;
assert(<bool>F6 == true);
var F7 = -Infinity;
assert(<bool>F7 == true);
var F8 = +f64.MIN_VALUE;
assert(<bool>F8 == true);
var F9 = -f64.MIN_VALUE;
assert(<bool>F9 == true);
var F10 = reinterpret<f64>(1);
assert(<bool>F10 == true);
var F11 = reinterpret<f64>(0x7FF0000000000000 - 1);
assert(<bool>F11 == true);
var F12 = reinterpret<f64>(0x7FF0000000000000 + 1);
assert(<bool>F12 == false);
var F13 = reinterpret<f64>(0xFFF0000000000000 + 1);
assert(<bool>F13 == false);
6 changes: 6 additions & 0 deletions tests/compiler/bool-Oz.untouched.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
(module
(memory $0 1)
(data (i32.const 12) ",")
(data (i32.const 24) "\01\00\00\00\14\00\00\00b\00o\00o\00l\00-\00O\00z\00.\00t\00s")
(export "memory" (memory $0))
)
2 changes: 1 addition & 1 deletion tests/compiler/number.untouched.wat
Original file line number Diff line number Diff line change
Expand Up @@ -5186,7 +5186,7 @@
f64.add
local.set $16
local.get $16
i32.trunc_f64_s
i32.trunc_sat_f64_s
local.set $15
local.get $15
local.get $15
Expand Down
Loading