Skip to content
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

Proposal: int types #4639

Closed
ivogabe opened this issue Sep 4, 2015 · 53 comments
Closed

Proposal: int types #4639

ivogabe opened this issue Sep 4, 2015 · 53 comments
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript

Comments

@ivogabe
Copy link
Contributor

ivogabe commented Sep 4, 2015

This proposal introduces four new number types: int, uint, int<N> and uint<N>, where N is any positive integer literal. An int is signed, it can contain negative integers, positive integers and zero. An uint is an unsigned integer, it can contain positive integers and zero. int<N> and uint<N> are integers limited to N bits:

  • An int<N> can contain integers in the range -Math.pow(2, N-1) ... Math.pow(2, N-1) - 1.
  • An uint<N> can contain integers in the range 0 ... Math.pow(2, N) - 1

This proposal doesn't use type information for emit. That means this doesn't break compilation using isolatedModules.

Note: since JavaScript uses 64 bit floating point numbers, not all integers can be used at runtime. Declaring a variable with type like uint<1000> doesn't mean it can actually store a number like Math.pow(2, 999). These types are here for completeness and they can be used in type widening as used in type inference of binary operators. Most languages only support integer types with 8, 16, 32 and 64 bits. Integers with other size are supported because they are used in the type inference algorithm.

Goals

  • No emit based on type information
  • Report issues (with overflow or implicit floating point to integer casts) at compile time

References

Ideas were borrowed from:

  • asm.js
  • LLJS
  • C

Overview

int and uint are both subtypes of number. These types are the easiest integer types. They are not limited to a certain amount of bits. int<N> and uint<N> are subtypes of int and uint. These types have a limited size.

In languages like C, the result type of an operator is usually the same as the input type. That means you can get strange behavior, and possibly bugs:

uint8_t a = 200;
int b = a + a; // = 144 = 400 % 256, not 400
float c = 1 / 2; // = 0, not 0.5

To mimic that behavior we would need to add type converters everywhere in the emitted code, and those heavily rely on type information. We don't want that, so instead we widen the return types. That means there is no runtime overhead. For example, adding two values of type uint<8> would result in uint<9>. To assign that to a uint<8>, you can use an conversion function, like uint<8>(x). That function converts x to an uint<8>.

This design means that operations like x++, x += 10 and x /= 2 are not allowed, as they are not safe. Instead you can use x = uint<8>(x + 1) and x = uint<8>(x / 2).

int and uint allow more operations. Since they are not defined with a limited size, x++ and x += 10 are allowed. x-- is only allowed on an int, as an uint might become negative. Below is an overview of which operators are allowed on which number types.

Since emit isn't based on type information, integers can be used in generics. They can also be used in unions.

Assignability

  • int and uint are assignable to number
  • uint is assignable to int
  • int<N> is assignable to int and number
  • uint<N> is assignable to int, uint and number
  • int<N> is assignable to int<M> iff N <= M
  • uint<N> is assignable to uint<M> iff N <= M
  • int<N> is not assignable to uint<M> for all N, M
  • uint<N> is assignable to int<M> iff N < M
  • Infinity, -Infinity and NaN are not assignable to any integer type.
  • Literals are assignable if they don't have a decimal dot or an exponent, and fit in the range of the target type.
  • Enums are assignable if all defined values are assignable to the target type

If a type is not assignable to some other type, you can use a normal cast (<int<8>> x or x as int<8>), which has no impact at runtime, or a cast function (int<8>(x)).

Cast function

A cast function takes a number and converts it to the target integer type.

Syntax:

int<N>(x);
uint<N>(x);
int(x); // Alias of int<32>(x);
uint(x); // Alias of uint<32>(x);

Note: even though an int doesn't have a fixed size, we use the 32 bit cast function as that's easy and fast JavaScript.

Semantics:

  • Let x be the argument of the cast, converted to a number.
  • Round x (towards zero, -2.9 -> -2)
  • Truncate x to the target int / uint type.

This gives the same behavior as type casts in languages like C. If the operand is not a number, TS should give a compile time error. Emit should succeed (unless --noEmitOnError is set), the operand should be converted to a number at runtime, using same semantics as +x. undefined and null should also be converted the same way.

Implemented in TypeScript:

function castToInt(n: int, x: number) {
    x = +x;
    const m = Math.pow(2, n - 1);
    if (x > 0) {
        x = Math.floor(x);
        while (x > m) x -= 2 * m;
    } else {
        x = Math.ceil(x);
        while (x < -m) x += 2 * m;
    }
    return x;
}
function castToUint(n: int, x: number) {
    x = +x;
    const m = Math.pow(2, n);
    if (x > 0) {
        x = Math.floor(x);
        while (x > m) x -= m;
    } else {
        x = Math.ceil(x);
        while (x < 0) x += m;
    }
    return x;
}

These functions are not always used in the generated code. When n <= 32, these functions are not needed.
The generated code:

If n === 32:

int<32>(x); // TS
x | 0; // JS

uint<32>(x); // TS
x >>> 0; // JS

If n < 32,

uint(x);
uint<n>(x);
(x | 0) & b; // where b = pow(2, n) - 1

int(x);
int<n>(x);
(x | 0) << a >> a;
// where a = 32 - n

// Examples:
int<8>(x);
(x | 0) << 24 >> 24

uint<8>(x);
(x | 0) & 255;

Question: can we make the emit of int<n>(x) better? The current isn't very nice and performs bad.
Solved it using http://blog.vjeux.com/2013/javascript/conversion-from-uint8-to-int8-x-24.html

If n > 32:

uint<n>(x);
__castToUint(n, x);

int<n>(x);
__castToInt(n, x);

__castToUint and __castToInt are the functions above, emitted as helper functions.

You can only use these cast functions in call expressions:

int(x); // Ok
let intFunction = int; // Error
[1].map(int); // Error

We cannot solve that with helper functions, as int wouldn't be equal to int if they don't come from the same file when using external modules.
Instead we should dissallow this.

Type inference

Introducing integer types can break existing code, like this:

let x = 1;
x = 1.5;

But we do want to type 1 as an int:

let y: int = 1; // Don't show error that `number` is not assignable to `int`

There are several options:

  1. If a type of a variable is infered from a numeric literal, always infer to number.
  2. If a type of a variable is infered from a numeric literal, always infer to int. This would break some code, but we could accept that since this gives the most predictable behavior.
  3. Only infer to an integer if at least one part of the expression (variable, function call, ...) is explicitly typed as an integer.

In option two we infer to int, as let a = 0 would otherwise infer to uint<1>, which would mean that the variable can only contain
0 and 1.

Examples of option 3:

let a: int = 1;
let b = a + 3; // int
let c = 1 + 1; // number

function returnInt(): int {
    return 1;
}
let d = returnInt() - 1; // int
let e = int(1) * 1; // int
let f = <int> 1 + 1; // int
let g = <number> a + 1; // number

A literal will be infered to the smallest integer type that can contain the number. Examples:

0 -> uint<1>
1 -> uint<1>
2 -> uint<2>
-1 -> int<1>
-2 -> int<2>

Operators:

Operators should always infer to the smallest integer type that can contain all possible values.

(int, int<N>, uint or uint<N>) + number -> number
int + any integer type -> int
uint + (uint or uint<N>) -> uint
int<N> + int<M> -> int<max(N, M) + 1>
int<N> + uint<M> -> int<max(N, M + 1) + 1>
uint<N> + uint<M> -> uint<max(N, M) + 1>

- is almost the same as +, with two exceptions:
uint - (uint or uint<N>) -> int
uint<N> - uint<M> -> int<max(N, M) + 1>

int * (uint, int) -> int
uint * uint -> uint
int<N> * int<M> -> int<N + M>
(int<N + M - 1> is not big enough, consider -2^(N-1) * -2^(M-1) = 2^(N + M - 2)

/ always return `number`

int % (int, uint, int<N>, uint<N>) -> int
uint % (int, uint, int<N>, uint<N>) -> uint
int<N> % (int, uint) -> int<N>
uint<N> % (int, uint) -> uint<N>
int<N> % int<M> -> int<min(N, M)>
int<N> % uint<M> -> int<min(N, M+1)>
uint<N> % int<M> -> uint<max(min(N, M - 1), 1)>
uint<N> % uint<M> -> uint<min(N, M)>

int & (int or int<N>) -> int<32>
int<N> & int<M> -> int<min(N, M, 32)>
uint<N> follows the rules of int<N+1>
(uint & uint !== uint, for example 4294967295 & 4294967295 === -1)

| and ^ have the same behavior as &

~int -> int
~uint -> int
~int<N> -> int<N>
~uint<N> -> int<N + 1>

<< always returns an int<32>

(number, uint) >> any -> uint<32>
int >> any -> int<32>
int<N> >> any -> int<min(N, 32)>
uint<N> >> any -> uint<min(N, 32)>

(number, uint or int) >> any -> uint<32>
int<N> >>> any -> uint<32> (Consider -1 (int<1>), -1 >>> 0 === 429467295 === max value of uint<32>
uint<N> >>> any -> uint<min(N - 1, 32)>

Certain assignment operators are not supported on integer types. In short:
Let Op be an operator. x Op= y is allowed iff x = x Op y is allowed.
That means that the following operators are not supported and usage will give an error:

int: x /= y

uint: x /= y, x--, --x

int<N>, uint<N>: x /= y, x++, ++x, x--, --x, x += y, x -= y, x *= y

int<N>
if N < 32, then: x <<= y
if N <= 32, then: x >>>= y

uint<N>
if N < 32, then: x <<= y, x >>>= y

Breaking changes

Type inference can change depending on how it will be implemented. Also changing existing definitions can break things:

let arr = [];
let length = arr.length;
length = 1.5;

Changing the definition of the length property to a uint would break this code, though I don't think this pattern will be used a lot.
Such problems can easily be fixed using a type annotation (in this case let length: number = arr.length;).

Questions

  1. Should integer types be namespaced under number? Thus, let x: int vs. let x: number.int
  2. Which syntax should be used for sized integers? int<8>/number.int<8> or int8/number.int8?
  3. Can we make the emit of int<n>(x) (n < 32) better? The current looks and performs bad. Solved using http://blog.vjeux.com/2013/javascript/conversion-from-uint8-to-int8-x-24.html
  4. How should type inference be handled? See the question above, in paragraph called 'Type inference'.
  5. Can undefined and null be assigned to an integer type? The conversion functions convert them to 0. Allowing undefined would mean that int + int could be NaN (if one operand is undefined), while NaN is not assignable to an int. I'd say that undefined and null shouldn't be assignable to an integer type, and that declaring a variable (also class property) with an integer type and without an initializer would be an error.

All feedback is welcome. If you're responding to one of these questions, please include the number of the question. If this proposal will be accepted, I can try to create a PR for this.

@rotemdan
Copy link

rotemdan commented Sep 5, 2015

For the more common cases of uint8, uint16, uint32, int8, int16, int32 there are simple tricks/bit "twiddles" that can make the truncations much faster/simpler. I tried to make them fit in one statement (sacrificing a tiny bit of performance in some), to make them better candidates for inlining (either through the JS runtime or TypeScript itself).

Unsigned:

function toUint8(num: number): uint8 {
    return num & 255;
}

function toUint16(num: number): uint16 {
    return num & 65535;
}

function toUint32(num: number): uint32 {
    return (num | 0) >= 0 ? (num | 0) : (num | 0) + 4294967296;
}

Signed:

function toInt8(num: number): int8 {
    return (num & 255) < 128 ? (num & 255) : (num & 255) - 256;
}

function toInt16(num: number): int16 {
    return (num & 65535) < 32768 ? (num & 65535) : (num & 65535) - 65536;
}

function toInt32(num: number): int32 {
    return num | 0;
}

These functions would truncate to the least significant 8, 16 or 32 integer bits of any floating point number. Numbers outside of the range [-2^52..2^52 - 1] would still work but would be naturally limited in resolution.

I've ran some automated testing for these with millions of random values against the expected truncation behavior in typed arrays and they seem to give exactly similar results including when the number is NaN, -Infinity and +Infinity, null, undefined, or any other type, including a string or an object. anything | 0 and anything & num always resolve to 0 in these cases [with the special exception of boolean where true is automatically converted to 1 and false to 0].


There is an alternative method to do this that may or may not be faster (depending on the runtime), but would require runtime support for typed arrays (only available in IE10+):

Unsigned:

uint8Array_dummy = new Uint8Array(1);
uint16Array_dummy = new Uint16Array(1);
uint32Array_dummy = new Uint32Array(1);

function toUint8(num: number): uint8 {
    uint8Array_dummy[0] = num; 
    return uint8Array_dummy[0];
}

function toUint16(num: number): uint16 {
    uint16Array_dummy[0] = num; 
    return uint16Array_dummy[0];
}

function toUint32(num: number): uint32 {
    uint32Array_dummy[0] = num; 
    return uint32Array_dummy[0];
}

Signed:

int8Array_dummy = new Int8Array(1);
int16Array_dummy = new Int16Array(1);
int32Array_dummy = new Int32Array(1);

function toInt8(num: number): int8 {
    int8Array_dummy[0] = num; 
    return int8Array_dummy[0];
}

function toInt16(num: number): int16 {
    int16Array_dummy[0] = num; 
    return int16Array_dummy[0];
}

function toInt32(num: number): int32 {
    int32Array_dummy[0] = num; 
    return int32Array_dummy[0];
}

Anyway, this should be used as a reference to test the correctness of the truncations.

[Edit: I retested the first version and it does seem to return 0for NaN, Infinity, undefined etc. as expected, the problems happened in previous versions of the functions]

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 5, 2015

I've changed the emit for int<n>(x) where n < 32. It now uses bit shifting as explained here. This is probably the best solution for question 3.

@rotemdan Those conversions exist for all integer types up to 32 bits. You can see them all in the section "Cast function". TS should inline these functions, and browsers can optimize these constructs. The castToInt and castToUint are functions that demonstrate the expected behavior, the alternatives (like x | 0) follow that behavior.

@rotemdan
Copy link

rotemdan commented Sep 5, 2015

@ivogabe

Great find! so let's try to summarize the simplest, most efficient implementations we have so far for the most important cases:

Unsigned:

  • uint8: num & 255
  • uint16: num & 65535
  • uint32: num >>> 0

Signed:

  • int8: (num << 24) >> 24
  • int16: (num << 16) >> 16
  • int32: num | 0

I ran them through the automated tests (1M random FP values up to about +/- 2^71) and they seem to work correctly.

It seems reasonable to have the behavior standardized against assignments to typed arrays so these should all yield 0 for NaN, undefined, string etc. not sure about boolean (right now some of these will give 1 for true but that's a really minor detail).

[Edit: I confirmed typedArray[0] = true would set the value to 1, so there is no need for a correction there.]

Another minor detail: I'm not sure if adding | 0 is needed for to be recognized as integers by asm.js (I'm not very familiar with it), I assume the JS runtime could be "smart" enough to recognize them?

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 5, 2015

@rotemdan Most JS engines can optimize | 0 very well, but other binary operators are less optimized. I haven't measured the difference though. NaN, +/-Infinity, undefined and null should be converted to 0, strings are first converted to a number (eg "120" -> 120, "1.5" -> 1.5 -> 1). For booleans, true is converted to 1 and false to 0. That behavior is consistent between the proposed casts and typed arrays.

@rotemdan
Copy link

rotemdan commented Sep 5, 2015

You mentioned that operations like x++, x += 10 and x /= 2 are not allowed without a cast. I understand the desire for safety but I still think that having an automatic cast/truncation on assignment is still reasonable for practical purposes (and is actually a useful/desirable thing to have for some applications).

I mean that:

let num: uint8 = 255;
num = num + 1;

Would emit:

var num = 255;
num = num + 1;
num &= 255;

The automatic casts would only happen on assignments, "anonymously typed" intermediate results would not be effected:

let num: uint8 = 255;
num = (num * num) + (num * 2);

Still emits only a single truncation:

var num = 255;
num = (num * num) + (num * 2);
num &= 255;

The reason I wanted to "optimize" and simplify the casts (including the logic for them) as much as possible was because I wanted to make them extremely cheap for these purposes (In terms of performance, I think the operations turned up cheap enough to be called very frequently, including in tight loops).

Since typed arrays automatically truncate on assignment and don't error on overflows or even on type mismatches, it seems like a reasonable compromise to have a somewhat more permissive logic. Not having these automatic casts would make it difficult to work with these types in practice (in some cases an explicit cast would be needed at every single line).

It is still possible to limit automatic casts/truncations to only work between integer types, though they would not be required to be compatible:

let num: uint8 = 255;

num = num * 23; // OK, truncated to least significant 8 bits on assignment. ("modulo 2^8").

num = num * 23.23423425; // Error, incompatible numeric types
num = <uint8> num * 23.23423425; // OK

Edit: It is possible to limit this even further to only apply to anonymous/intermediate expressions. Assignments between named variables may still be strict:

let num1: uint8 = 45;
let num2: uint16 = 11533;

num1 = num2; // Error: incompatible numeric types
num1 = <uint8> num2; // OK

It might be possible to strengthen it back with even more restrictions. I guess there's a balance here between safety and usability.

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 5, 2015

@rotemdan The behavior you suggest would require emit based on type information. That means isolatedModules / transpile (compilation per file in stead of one compilation of the whole project) cannot be used with integers. The key feature of this proposal is that it does not use type information for emit. That way the feature fits better in TypeScript.

If you want less restrictions you can use int or uint, which are not restricted to a specific size. You can write this:

let x: uint = 0;
x++;

The int and uint types give less guarantees (the only guarantees are that a value is round and in case of an uint, not negative), but are easier to use. In most cases you can use these types.

@dead-claudia
Copy link

@ivogabe Very correct.

I would think it would make more sense to just start out with four types:

  • int - A 32-bit signed integer. This would go in line with most of JavaScript's bitwise operators.
  • uint - A 32-bit unsigned integer. This would account for the following cases (and a few others on the Math object):
    • int >>> int -> uint
    • uint >>> (int | uint) -> uint
    • int + int -> uint
    • int - int -> uint
  • float - Math.fround(x) -> float
  • double - Everything else.

This is very similar to the asm.js types, and are already heavily optimized (V8 and Spidermonkey both generate type-specific code already for non-asm.js code that rely on these types). Matter of fact, almost all the asm.js types can be represented by these types + the type system.

asm.js TypeScript
void void
signed int
unsigned uint
int `int
fixnum int & uint
intish `int
double double
double? `double
float float
float? `float
floatish `float
extern any

Note: asm.js requires intish to be coerced back into int or uint and floatish to be coerced back into double or float. TypeScript wouldn't require it, but people can still remain explicit when necessary.

As for implementation, there is no realistic way to guarantee these types without relying on explicit coercions.

int -> x | 0
uint -> x >>> 0
int & uint -> x & <int & uint> y // e.g. y = 0x7fffffff
float -> Math.fround(x) or a Float32Array member
double -> +x
// int, uint, and float are each subtypes of double
// double is a subtype of number

function f(x: number): int {
  // Error!
  // return x
  return x | 0
}

Even asm.js has to use such coercions to keep the math correct. Unlike asm.js, though, you don't have to coerce everything, so you don't have to write expr(x) | 0 a bunch of times. You also don't have to coerce your arguments, as the argument types are checked statically.

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 6, 2015

Floats and doubles would indeed be a good addition. For those who don't see the advantage, I'd suggest reading this: https://blog.mozilla.org/javascript/2013/11/07/efficient-float32-arithmetic-in-javascript/. I would call these types float32 and float64, as that's more in line with the names for typed arrays and SIMD in JS.

The reason I chose to introduce more integer types is type safety. int32 + int32 isn't always an int32, so we must either add a conversion around it (implicitly), which would require type information, or widen the type to an int33. I chose the latter. The user would need to add the conversions himself. If he doesn't want that, he can use the 'unsized' integer type int, which doesn't check the size.

My proposal introduces syntactic sugar for those casts, as I'd rather write int(x) than x | 0 and uint<8>(x) than (x | 0) & 256. Of course this can be extended to floats: float32(x) -> Math.fround(x) and float64(x) -> +x.

@IMPinball int + int -> uint isn't always true, consider 1 + -3 === -2

@dead-claudia
Copy link

@ivogabe I forgot about that... Oops. ;)

And I'm aware that type casts that end up in the emit would definitely be useful. But I'm taking into account the TypeScript compiler devs generally steer away from it.

If you want to create a third party compiler (a la CoffeeScriptRedux) with that as a nonstandard extension, go for it!

@dead-claudia
Copy link

Or even edit the current one to do that, that'd still be fine.

@dead-claudia
Copy link

And as for the link, I read it while creating this post. (I optimized the polyfill shortly after.)

One thing that will have to be addressed with yours is right shifts with integers smaller than 32-bit. That can easily bring garbage in. And that would require those types making it into the emit.

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 6, 2015

@IMPinball At first I found it strange that they didn't want to use type info, but now I think they're right about that. Using isolatedModules compilation times can get decreased from a few seconds to less than a second. Introducing a feature that would break that would be a very bad idea. Someone of the team can explain this better probably.

I don't really like the idea of forking, it's not hard to create a fork, maintenance is usually the problem.

One thing that will have to be addressed with yours is right shifts with integers smaller than 32-bit. That can easily bring garbage in. And that would require those types making it into the emit.

I'm not sure what you mean with this, can you clarify that? Right shift (x >> y) removes bits and is sign preserving, so it's guaranteed that abs(x >> y) <= abs(x). Left shift doesn't have such guarantee, it should always infer to int<32>.

@dead-claudia
Copy link

@ivogabe

Where the shifts can lead to unexpected behavior is this:

Let's assume only the rightmost 8 bits are used. JavaScript numbers are only 32-bit, but for the sake of brevity, let's assume they're 16-bit.

Let's try a left shift:

          |-------| <-- The digits we care about
0000 0110 1100 0111 <-- x
0001 1011 0001 1100 <-- x << 2, okay if uint8

0000 0110 1100 0111 <-- original x
0000 0001 1011 0001 <-- x >> 2, naive
0000 0000 0011 0001 <-- x >> 2, correct

The example of the left shift introduces artifacts for signedness because then if you try to add, it's adding a positive. The example of the right shift is that you're literally introducing garbage into the program, like what you can get from allocating a raw memory pointer without zeroing the region out first.

Now, let's assume only the leftmost 8 bits are used. That'll address the issue of sign propagation and addition, subtraction, etc., but what about left and right shifts?

|-------| <-- The part we are using
1101 1001 1100 0000 <-- x
0110 0111 0000 0000 <-- x << 2, naive
0110 0100 0000 0000 <-- x << 2, correct

Now, there's a similar garbage problem with left shifts. Simply because the other half wasn't zeroed out first.

Obviously, these are only 16-bit integers and JavaScript uses 32-bit for its bitwise operations, but the argument could easily be expanded to cover those.

In order to fix this, you need those types to correct the emit. And I didn't get into addition, subtraction, etc., because that's already been made.


As another nit, I don't like the generic syntax, because a) you'd have to special case the parser (numbers aren't valid identifiers for generics), b) they're a finite, limited set of types, not some sort of parameterized type, and c) it doesn't seem right with the rest of the language, as primitives are just a simple identifier word.

@dead-claudia
Copy link

And every time I can recall a suggestion that would use/require type info in the emit, they've declined it. Including that of optimizing for types (they declared that out of scope).

@ivogabe
Copy link
Contributor Author

ivogabe commented Sep 7, 2015

@IMPinball I don't think you understand the idea. If the rightmost 8 bits are used, I think you mean we're using an int8. If that's the case, the value must be in the range -128, 127. That means the 8 rightmost bits are used to represent the value, the other bits have the same value as the 8th bit, which is 0 for a positive number and 1 for a negative number. There's no situation where only the leftmost 8 bits are used. There is no difference in representation of the same number in different types (though the JS engine might do that internally).

In short, by using compile time checks we know that a number is of a certain size. You can trick the type system with casts (<int8> 100000), but that's already possible with the types we have today. If there are no compile errors and you don't use such casts, types should always be right. The compiler doesn't add implicit casts, but instead it errors on places where the user should add them.

When you use the >> operator on an int<N> or uint<N>, the result will be int<min(N, 32)> or uint<min(N, 31)>, since JS uses 32 bit signed integers for bitwise operators. The << operator always returns an int<32>, we have no guarantees of what the result might look like.

I'll demonstrate this using some code:

let a: int32;
let b: int8;

b = 200; // Compile time error, 200 not in the range of an int8
b = 100; // Ok, since -128 <= 100 <= 127

// b >> a should return an int8, so this is ok 
b = b >> a;

// b << a should return an int32, so
a = b << a; // this is ok
b = b << a; // and this is a (compile time) error
b = int8(b << a); // but this is ok

The int8( ... ) call converts the operand to an int8. This is emitted as ((b << a) | 0) << 24 >> 24. It removes the garbage of the 24 bits on the left.

I'm not yet sure about the syntax, too. I chose the generics style, since it's a family of types that have a small difference between them. You can describe generics on interface or objects the same way. There is no limit on the size of the integer, you can declare an int<100>, but not all values of such int are safe in JS, since JS uses a double to store the value. All integers up to int<53> are safe. An int<54> is almost safe, only -Math.pow(2, 53) is not safe. Also I didn't like adding a big (~ infinite) list of types top level. That could be solved by namespacing them like number.int8. Would like to hear what everyone thinks about this.

@xooxdoo
Copy link

xooxdoo commented Apr 26, 2016

When will this be implemented ?

@dead-claudia
Copy link

dead-claudia commented Apr 28, 2016

@xooxdoo I don't think this got to any conclusive decision, and for what it's worth, this is probably not as important to me as some of the other more recent additions like non-nullable types.

@PaulBGD
Copy link

PaulBGD commented Jul 3, 2016

I'm with this, one thing I've found lacking is more types of numbers in typescript. There's definitely a difference between an byte, int, float, and double but there's no difference in typescript at the moment. I'd love to require a specific type of number than a generic number type.

@quanterion
Copy link

I think addition of int and float types will pave the way to efficient WebAssembly code generation in the future

@yahiko00
Copy link

yahiko00 commented Jul 22, 2016

TypeScript needs more refined types for numbers indeed. At least int and float to begin with.

@PaulBGD
Copy link

PaulBGD commented Jul 22, 2016

@yahiko00 Well javascript numbers are by default doubles, so we should probably have a double type to start with.

@yahiko00
Copy link

yahiko00 commented Jul 22, 2016

As a tribute to Turbo Pascal, I would suggest integer and real for the names of these new numeric types. The advantage is they do not suggest a specific implementation (number of words), in the same way as number.

@dead-claudia
Copy link

dead-claudia commented Jul 23, 2016

@yahiko00 I'd personally prefer int over integer (3 vs 7 characters to type), but I agree. I disagree that real should exist, because number is already sort-of a real in practice (it's technically mandated by the ECMAScript standard to be a IEEE 754 double-precision floating-point number), but having mandated integers would be nice.

I will note that int + int should yield int, though, just for convenience, even though it's technically incorrect for anything that adds up higher than 253-1.

@PaulBGD
Copy link

PaulBGD commented Jul 23, 2016

Int over Integer is also preferable, since the typed array classes all start with "Int".

@RyanCavanaugh RyanCavanaugh removed the In Discussion Not yet reached consensus label Mar 7, 2017
@RyanCavanaugh
Copy link
Member

Holding off on this until ES gets the int64 type or we get Web Assembly as a compile target

@MichaReiser
Copy link

That's a pity. I'm actually working on a TS to WebAssembly prototype as part of my master thesis and hoped that this feature would land soon so that I can take profit of using ints in the computation instead of doubles.

In this case, I might have to fall back to a simple implementation that just adds int as a TS type but without changing the emitting of JS Code and type inference.

@dead-claudia
Copy link

@RyanCavanaugh 64-bit ints aren't happening, bigints are now being tried.

@mihailik
Copy link
Contributor

Apparently, current TS already supports a subset of my proposal above:

type byte = 0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89|90|91|92|93|94|95|96|97|98|99|100|101|102|103|104|105|106|107|108|109|110|111|112|113|114|115|116|117|118|119|120|121|122|123|124|125|126|127|128|129|130|131|132|133|134|135|136|137|138|139|140|141|142|143|144|145|146|147|148|149|150|151|152|153|154|155|156|157|158|159|160|161|162|163|164|165|166|167|168|169|170|171|172|173|174|175|176|177|178|179|180|181|182|183|184|185|186|187|188|189|190|191|192|193|194|195|196|197|198|199|200|201|202|203|204|205|206|207|208|209|210|211|212|213|214|215|216|217|218|219|220|221|222|223|224|225|226|227|228|229|230|231|232|233|234|235|236|237|238|239|240|241|242|243|244|245|246|247|248|249|250|251|252|253|254|255;

type sbyte = -128|-127|-126|-125|-124|-123|-122|-121|-120|-119|-118|-117|-116|-115|-114|-113|-112|-111|-110|-109|-108|-107|-106|-105|-104|-103|-102|-101|-100|-99|-98|-97|-96|-95|-94|-93|-92|-91|-90|-89|-88|-87|-86|-85|-84|-83|-82|-81|-80|-79|-78|-77|-76|-75|-74|-73|-72|-71|-70|-69|-68|-67|-66|-65|-64|-63|-62|-61|-60|-59|-58|-57|-56|-55|-54|-53|-52|-51|-50|-49|-48|-47|-46|-45|-44|-43|-42|-41|-40|-39|-38|-37|-36|-35|-34|-33|-32|-31|-30|-29|-28|-27|-26|-25|-24|-23|-22|-21|-20|-19|-18|-17|-16|-15|-14|-13|-12|-11|-10|-9|-8|-7|-6|-5|-4|-3|-2|-1|0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38|39|40|41|42|43|44|45|46|47|48|49|50|51|52|53|54|55|56|57|58|59|60|61|62|63|64|65|66|67|68|69|70|71|72|73|74|75|76|77|78|79|80|81|82|83|84|85|86|87|88|89|90|91|92|93|94|95|96|97|98|99|100|101|102|103|104|105|106|107|108|109|110|111|112|113|114|115|116|117|118|119|120|121|122|123|124|125|126|127;


function takeByte(x: byte): byte {
    x++;
    return x + 1;
//  ~~~~~~~~~~~~~   Type 'number' is not assignable to type 'byte'.
}

takeByte(0);
takeByte(10);
takeByte(888);
//       ~~~   Argument of type '888' is not assignable to parameter of type 'byte'.


function byte2sbyte(x: byte): sbyte {
  if (this) return x + 1;
//          ~~~~~~~~~~~~~  Type 'number' is not assignable to type 'byte'.
  else return x;
//     ~~~~~~~~~   Type 'byte' is not assignable to type 'sbyte'.
//                   Type '128' is not assignable to type sbyte.
}

The missing pieces are:

  • Can't do it for the adult grown-up integers, need actual compiler support
  • Explicit coercions still require type assertions (i.e. myNumber & 0xFF don't produce byte) — unlike ASM.js which has coercions that ensure output is always integer (or float).
  • Looks like TSC has a bug (see first line of takeByte function above), increment operator shouldn't be allowed on literal union.

@trusktr
Copy link
Contributor

trusktr commented May 1, 2017

ints would be nice. For example, consider this class:

class KeyFrameManager {
  currentFrame:number
  // the rest of class exposes API for switching to different frames of a key-framed animation. 
}

There's currently no way to enforce that frames should be whole numbers, which means if a developer introduces a calculation that produces a non-whole number there could be strange bugs.

Let us enforce this. 👍 For example:

class KeyFrameManager {
  currentFrame:int
  // the rest of class exposes API for switching to different frames of a key-framed animation. 
}

This is simply a matter of enforcing program correctness for me, I don't care that numbers are actually all the same in JavaScript.

@yahiko00
Copy link

Here is a definition file for numeric types used in AssemblyScript, an experimental compiler from TypeScript to WebAssembly, written in TypeScript: https://github.com/dcodeIO/AssemblyScript/blob/master/assembly.d.ts

This could be a good basis for the future numerical types in the official TypeScript compiler.
Also, until this time, I plan to use this definition file for my projects.

@matthew-dean
Copy link

@yahiko00 There's also TurboScript which does this: https://github.com/01alchemist/TurboScript

@yahiko00
Copy link

That's right. I've also had a look at this project but it seems a little but more "experimental" than AssemblyScript, and its definition file is much less complete.

@dead-claudia
Copy link

@yahiko00 The main difference is that TurboScript aims to be a bit more fully featured and is closer to a TS derivative rather than AssemblyScript is (which is more or less just a retargeted TS).

@yahiko00
Copy link

@isiahmeadows You are probably right since I am discovering these repos. Although, I like the idea of AssemblyScript to be a subset of TypeScript. Maybe I am wrong, but I think, in the future, this could be easier to merge AssemblyScript into the official TypeScript compiler.

@yahiko00
Copy link

Here is another project, wasm-util, from TypeScript to WebAssembly: https://github.com/rsms/wasm-util

@tarcieri
Copy link

In case you missed #15096 I think TC39 BigInt is the best way forward to get true integers in TypeScript.

This proposal would enable an arbitrary precision integer type and could also hint to JavaScript VMs when they could use e.g. native 64-bit integers, enabling things like an int64 and uint64 type.

The proposal is presently stage 2 and has multi-vendor support. In theory the syntax is mostly stable at this point.

There's a proposal to support it in Babel as well: babel/proposals#2

@benatkin
Copy link

benatkin commented Mar 7, 2018

I like how TypeScript follows the spec and tries not to add too much to it, but an integer type is just so fundamental. Would love minimal integer support that's based on safe integers in ES6. Here's a concise description of it: http://2ality.com/2015/04/numbers-math-es6.html

@benatkin
Copy link

benatkin commented Mar 7, 2018

GraphQL supports signed 32 bit integers and doesn't address other integer types. IMO that's a great starting point. http://graphql.org/learn/schema/

@tarcieri
Copy link

tarcieri commented Mar 7, 2018

@benatkin if TC39 BigInt ever ships, there's a pretty big drawback to defining integer types based on number: TC39 BigInt is a separate type at the VM level, and one which for which an exception is raised if mixed number/BigInt arithmetic is attempted.

If people were to add int32 types to their type declarations for untyped JavaScript code which are actually applying constraints to number, and TypeScript were to switch to TC39 BigInts as the underlying JavaScript backing type for int32, then all code which has int32 in their type declarations would be broken.

The TC39 BigInt proposal is now stage 3 and I think stands a pretty decent chance of eventually shipping, so I think it's probably best to "reserve" any potential integer types for ones which are actually integers at the VM level.

@ivogabe
Copy link
Contributor Author

ivogabe commented Jun 8, 2018

BigInt is scheduled for TS 3.0 (#15096) 🎉! Adding different syntax for "double based integers" as proposed in this issue would be confusing, so I think we can close this. See also #195.

@ivogabe ivogabe closed this as completed Jun 8, 2018
amrdraz added a commit to amrdraz/bitwise that referenced this issue Oct 3, 2018
So generally speaking other than using the slightly shorter array on the Bits and BinaryBits types

Not much can be done regarding the unsigned int types.
Your salvation may come with this proposal should the day come microsoft/TypeScript#15480

But Typescript will probably not support base uintx types as this discussion ended 
microsoft/TypeScript#4639
essentially JS is expecting BigInt and other types may be added someday and so no extra primitives will be added until T39 adds them

on a side note this regex proposal should it get implemented would be interesting 
microsoft/TypeScript#6579
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Revisit An issue worth coming back to Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests