microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.23k stars 12.52k forks source link

int32 type returned by bitwise operators #32188

Open calebmer opened 5 years ago

calebmer commented 5 years ago

Search Terms

bitwise operators, integer type, int32

Suggestion

Add an int32 subtype for number returned by TypeScript from bitwise operators.

function coerce(n: number): int32 {
  return n | 0;
}

JavaScript bitwise operators coerce arguments to int32 and the return type of those operators will always be an int32.

This would not be a breaking change since int32 would be a subtype of number. Only bitwise operations would change to return int32s.

EDIT: I now recommend calling the type BitwiseInt32 so that user’s won’t see the type as a generic integer type.

Use Cases

Helps applications which are trying to take advantage of JavaScript implementations int32 optimizations. An application could ask for an int32 parameter to force coercion with n | 0.

Checklist

My suggestion meets these guidelines:

MartinJohns commented 5 years ago

Duplicate of #195.

calebmer commented 5 years ago

This is a much more limited proposal. I only care about bitwise operators. I want to use the type system to force developers to coerce to an int32 with n | 0 in performance critical code.

I wouldn’t advise most developers use this feature.

To that end, I’d instead recommend naming the type BitwiseInt32 to discourage usage of this type as a general integer type like #195 wants.

calebmer commented 5 years ago

In JavaScript, you can safely represent integers up to 253-1. However, for bitwise operators you only have a range of integers -231 through 231-1. If there were to be an int type you might want it to cover the full JavaScript safe integer range. However, for bitwise operations specifically you want a tighter range.

RyanCavanaugh commented 5 years ago

I guess the problem here is that people would generally expect *, -, and + to follow int32 + int32 = int32, even though that is definitely not something that could be guaranteed.

It seems like you really just want a brand here?

calebmer commented 5 years ago

It’s actually desirable that the type for BitwiseInt32 + BitwiseInt32 = number because that forces the user to then coerce with | 0. So if you want to write a type safe addition you’d need:

function addInt32(x: BitwiseInt32, y: BitwiseInt32): BitwiseInt32 {
  return x + y | 0;
}

Which has the right properties if you’re looking to do int32 math in JavaScript.

calebmer commented 5 years ago

That’s why I recommend calling the type BitwiseInt32 instead of int32. You don’t want the user to expect int32 + int32 = int32. If you make the name more ugly, then hopefully user’s won’t attach their assumptions to what the type should do.

nmain commented 5 years ago

What about a uint32 type for >>> 0?

shicks commented 5 years ago

Would #33290 help in defining/implementing this? I'd like to see a handful of these types defined. At the very least, int, uint32, and int32. Unlike #195, I would argue that these should definitely not affect the emit (a non-starter). This would enable a number of things:

  1. I would expect int + int = int but not int32 + int32 = int32. The latter should fall back on the former.
  2. Better safety and IDE type hints for the results of | 0 or >>> 0 and others.
  3. With int (and maybe other similar types for other functions), Number.isInteger could be made into a type guard, and then the false branch of if (Number.isInteger(stringOrNumber)) would correctly still include number (see #21199).
make-github-pseudonymous-again commented 3 years ago

I would like #195. I want to use int32 for performance reasons: I have one use case where numbers up to 2**31-1 is enough (array indexes, I know these can go up to Number.MAX_SAFE_INTEGER but I do not care about handling numbers that large), and where coercing parameters and the output of every arithmetic operation by adding | 0 (for instance, ++i ~ (i = i+1|0)) yields significant time saving and produces compiled code almost twice smaller even though more subroutine calls are inlined (10KB instead of 17KB, I used Indicium to witness that).

I think what I have experienced can be extrapolated. I have the feeling that this addition would make basic algorithms in JavaScript much less resource consuming (if you agree to limit yourself to inputs of reasonable size). This claim would need to be evaluated.

I agree this example use case is a niche. I do not want int32 to be the new number. Even more so if it means errors due to implicit integer division or integer overflow.

Indeed a workaround is to have a branded int32 type, a coercion function, and arithmetic functions. But I have sufficiently many arithmetic operations in my code that I fancy leaving the responsibility to TypeScript would not be a luxury. I am probably wrong. Maybe all we need is a tiny library which includes said type and functions.

Nervetheless, if this responsibility would be shifted to TypeScript, I can see two problems in code emission:

PS: If going the workaround route then explicit inlining hints would be relevant (see https://github.com/microsoft/TypeScript/issues/661 for instance). The current v8 is excellent at inlining what needs to be inlined, I do not deny it, I have seen it in action and it is impressive. That does not contradict wanting more control on code emission. The better argument would be that implementing this takes time both for conception and maintenance, and has limited applicability. If we want to argue code size, replacing a(x,y) by x+y|0 uses one less character but a(a(x,y),z) would need parentheses (x+y|0)+z|0 and now we are equal, inlining a(a(a(x,y),z),w) emits something with one more character. Not sure if output size is relevant. The case (i = i+1|0) would be better served by a macro or a code transform as you cannot wrap the assignment in a function call but if we cannot have that then (i = f(i)) already saves one character because the right operand is constant. Hence we cannot argue inlining will always save bytes, nor can we argue it will always use more. A reasonable argument against implementing inlining hints in TypeScript is that minifiers can do that job, giving even more control (although, at the moment, terser produces an IIFE for some (each?) inlined call, and there is no way to guide the process).

Rudxain commented 1 year ago

I'd like to point out the fact that JS "canonically" has these types: Int32Array and Uint32Array. Sometimes, I must use these arrays as "wrappers" for those primitive types, because they offer "implicit coercion on overflow". But having to do arithmetic on a constant index of 0 is too awkward:

// BEGIN BOILERPLATE
interface FixedInt32Array<T extends number> extends Int32Array { 
    length: T;
}
interface Int32ArrayConstructor {
    new<T extends number>(length: T): FixedInt32Array<T>;
}

interface FixedUint32Array<T extends number> extends Uint32Array { 
    length: T;
}
interface Uint32ArrayConstructor {
    new<T extends number>(length: T): FixedUint32Array<T>;
}

type Int32 = FixedInt32Array<1>;
type Uint32 = FixedUint32Array<1>;
// END BOILERPLATE

const
    // verbose initialize to 0
    n: Uint32 = new Uint32Array(1),
    // can't do `Uint32Array([3])`, because of type signature
    m: Uint32 = new Uint32Array(1);
// values still mutable, despite `const`
m[0] = 3

const addU32 = (a: Uint32, b: Uint32): Uint32 => {
    // can't reuse `a`, because of side-effects
    const ret: Uint32 = new Uint32Array(1);
    // `a[0] + b[0]` can overflow, so we use `ret` for safety
    ret[0] = a[0] + b[0];
    return ret;
}

// annoying `[0]`, everywhere
console.log(addU32(n, m)[0]);

( some code borrowed from https://github.com/microsoft/TypeScript/issues/18471#issuecomment-329588688 )

By that point, devs would just import a package such as this. Or use something like

Number(BigInt.asUintN(32, BigInt(x + y)))