Skip to content

Numeric Range Types #29119

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
wants to merge 7 commits into from
Closed

Conversation

ECrownofFire
Copy link

Based on #15480, specifically #15480 (comment) by @AnyhowStep.

This likely needs some more plumbing and more test cases, especially for intersections and unions involving ranges. Ideally I would like for intersections and unions to merge ranges as applicable if they overlap, but similar to subtype reduction in unions, it may not be necessary or wanted. Though it may be good to at least do elimination similar to literals and primitives (e.g. 1 | number -> number and 1 & number -> 1).

This is of course a bit more limited than the suggestions in #15480, mainly in that it lacks an int type (I considered that a separate issue). This makes it less directly applicable to some use cases, but still provides some much stronger guarantees.

So for a couple quick examples: var foo: (> 0); denotes a number that must be positive. (< 0) | (> 0) denotes any number but zero. And (>= -1) & (<= 1) is any number between -1 and 1, inclusive.

A couple limitations/notes:

  1. Performing any sort of arithmetic with a range type will cause the result to degenerate to a plain number. I have an alternate branch that does implement this, but e.g. Design Meeting Notes for 4/15/2016 #8112 and numeric literal types disrespect arithmetics #15645 indicate that type arithmetic in general is undesired, so I dropped those commits from this PR.

  2. There's no support for anything but a number literal in the range type. This means that, for example, you can't use generics like type Gt<T> = (> T);. This was mostly to simplify parsing and such, but it's probably something that could be added. Ideally this would also support the same sort of expressions that enum member initializers do, though that would likely require some reorganizing of checker.ts to genericize evaluate().

  3. As the compiler doesn't support bigints, ranges also don't support them.

And a couple possibilities/wish list items for the future:

  1. Flow analysis, such as narrowing numbers to ranges based on comparison checks. Definitely a "nice to have" thing, and could be added later. Could also do things like say, if you have var foo: (>= 0), then you know if (foo < 0) can never be true, and that branch is unreachable.

  2. Syntax for other range styles, such as the proposed a..b, which in interval notation would be [a, b], or (>= a) & (<= b).

@msftclas
Copy link

msftclas commented Dec 21, 2018

CLA assistant check
All CLA requirements met.

@ajafff
Copy link
Contributor

ajafff commented Dec 21, 2018

some more items for the wish list:

  • the length property of open-ended tuples can be represented as number range
  • lib.xxx.d.ts could be updated, e.g. Array#length, Map#size, Array#indexOf/findIndex, etc.

@calebsander
Copy link
Contributor

As the compiler doesn't support bigints, ranges also don't support them

Although the compiler is written to run on environments where BigInt is not supported, it would certainly be possible to implement bigint ranges. The compiler represents bigint literals as base-10 strings and there is a parePseudoBigInt() function that standardizes any bigint literal (e.g. 0x0000FFFFn) into its base-10 representation (e.g. 65535). I don't think it should be too hard to compare two of these PseudoBigInt values: if the values have different numbers of digits, the one with more digits is larger; if they have the same number of digits, they can be compared lexicographically.

@ECrownofFire ECrownofFire reopened this Jan 4, 2019
@weswigham
Copy link
Member

As the compiler doesn't support bigints, ranges also don't support them.

Well, we support bigint types, though...

@ECrownofFire
Copy link
Author

Hm, actually yeah it should be possible to set it up in a way that doesn't require BigInt support, I'll take a look at doing that.

@RyanCavanaugh RyanCavanaugh added the Experiment A fork with an experimental idea which might not make it into master label Jan 25, 2019
@nikeee
Copy link
Contributor

nikeee commented Jan 24, 2020

While type-level arithmetics are rejected, this PR seems like an operation a % N (with N being a constant literal and a being a range/number/union of numbers) could actually be useful to create or narrow a range. % can only narrow the type down and in the worst case, default to do nothing.

The syntax of a range is `(OP N)`, where OP is one of <, <=, >, or >=,
and N is a numeric literal. The parentheses are mandatory. The parser
refers to these as a HalfRangeType.

The rules for them are as follows:

Assigning to a range from a number or an enum is not allowed. In the
future, enums may be supported directly, while numbers will have to be
done through control flow analysis.

Assigning to a range from a number literal (including enum literals) is
allowed if the literal is within the range. Assigning from BigInt
literals is not supported as the compiler only treats them as strings.

Assigning from a range to another is only allowed if the source range is
contained entirely within the target range. For example, assigning from
(>= 0) to (> 0) is not allowed, because 0 would be valid in the source
but not the target. Assigning from (> 0) to (> 1) is not allowed, but
the reverse is.

Assigning from a range to a generic number is always allowed. Assigning
to an enum is not.

Internally, the compiler represents all ranges as "full" ranges, e.g.
[0, 1) in interval notation. In the future this will be used in union
and intersection types to "merge" ranges. For example, the union of
(>= 0) and (< 1) should be [0, 1), while the intersection should be
`never`. These should still work normally, but there isn't any special
support for them that would support such narrowing.

Right now, these "full" ranges are not actually supported in the syntax,
nor is this planned.

Since this is a first draft, there's not yet any support for language
services, and there's very limited diagnostic information available.

In the future, control flow analysis will be done to automatically widen
and narrow numbers and ranges as necessary.
Assuming strict null checks are turned on, a range can only be falsy if
it contains 0.
Just in case they're produced from something else, they should be output
into something that makes sense. For obvious reasons, a range with no
min and no max is just the number type, while one with both a min and a
max is output as an intersection of two half ranges.
For example, (>= 1) is a subtype of (> 0). This doesn't really matter in
most cases though, because unions don't often reduce subtypes, and that
is the only time where the subtype relation is actually used.
Forgot to when doing the falsiness thing, oops.
@sandersn
Copy link
Member

sandersn commented Feb 3, 2020

I'm cleaning up our PR backlog and this is one is fairly stale, so I'm going to close it.

@ECrownofFire The next step is to create an issue with a complete proposal for numeric range types. You'll need to consider assignability between range types and number types, which can get fairly tricky for type parameters with constraints that are range types. Expect considerable bikeshed discussion on syntax as well, since language designers' favourite quibble is lexical syntax.

@sandersn sandersn closed this Feb 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Experiment A fork with an experimental idea which might not make it into master
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants