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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 125 additions & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4238,6 +4238,30 @@ namespace ts {
if (type.flags & TypeFlags.Substitution) {
return typeToTypeNodeHelper((<SubstitutionType>type).typeVariable, context);
}
if (type.flags & TypeFlags.Range) {
if ((<RangeType>type).min && !(<RangeType>type).max) {
const operator = (<RangeType>type).minOpen ? createToken(SyntaxKind.GreaterThanToken) : createToken(SyntaxKind.GreaterThanEqualsToken);
const basis = createLiteral((<RangeType>type).min!.value);
return createHalfRangeTypeNode(operator, basis);
}
else if (!(<RangeType>type).min && (<RangeType>type).max) {
const operator = (<RangeType>type).maxOpen ? createToken(SyntaxKind.LessThanToken) : createToken(SyntaxKind.LessThanEqualsToken);
const basis = createLiteral((<RangeType>type).max!.value);
return createHalfRangeTypeNode(operator, basis);
}
else if ((<RangeType>type).min && (<RangeType>type).max) {
const min = <RangeType>createType(TypeFlags.Range);
min.min = (<RangeType>type).min;
min.minOpen = (<RangeType>type).minOpen;
const max = <RangeType>createType(TypeFlags.Range);
max.max = (<RangeType>type).max;
max.maxOpen = (<RangeType>type).maxOpen;
return createIntersectionTypeNode([typeToTypeNodeHelper(min, context), typeToTypeNodeHelper(max, context)]);
}
else if (!(<RangeType>type).min && !(<RangeType>type).max) {
return createKeywordTypeNode(SyntaxKind.NumberKeyword);
}
}

return Debug.fail("Should be unreachable.");

Expand Down Expand Up @@ -13217,6 +13241,37 @@ namespace ts {
return links.resolvedType;
}

function getRangeTypeFromHalfRangeTypeNode(node: HalfRangeTypeNode): Type {
const links = getNodeLinks(node);
if (!links.resolvedType) {
let value: NumberLiteralType;
if (isPrefixUnaryExpression(node.basis)) {
value = <NumberLiteralType>getLiteralType(-(node.basis.operand as NumericLiteral).text);
}
else {
value = <NumberLiteralType>getLiteralType(+node.basis.text);
}

const type = <RangeType>createType(TypeFlags.Range);

switch (node.operator.kind) {
case SyntaxKind.LessThanToken:
type.maxOpen = true;
// falls through
case SyntaxKind.LessThanEqualsToken:
type.max = value;
break;
case SyntaxKind.GreaterThanToken:
type.minOpen = true;
// falls through
case SyntaxKind.GreaterThanEqualsToken:
type.min = value;
}
links.resolvedType = type;
}
return links.resolvedType;
}

function getTypeFromTypeNode(node: TypeNode): Type {
switch (node.kind) {
case SyntaxKind.AnyKeyword:
Expand Down Expand Up @@ -13298,6 +13353,8 @@ namespace ts {
return getTypeFromInferTypeNode(<InferTypeNode>node);
case SyntaxKind.ImportType:
return getTypeFromImportTypeNode(<ImportTypeNode>node);
case SyntaxKind.HalfRangeType:
return getRangeTypeFromHalfRangeTypeNode(<HalfRangeTypeNode>node);
// This function assumes that an identifier or qualified name is a type expression
// Callers should first ensure this by calling isTypeNode
case SyntaxKind.Identifier:
Expand Down Expand Up @@ -14696,9 +14753,53 @@ namespace ts {
if (s & (TypeFlags.Number | TypeFlags.NumberLiteral) && !(s & TypeFlags.EnumLiteral) && (
t & TypeFlags.Enum || t & TypeFlags.NumberLiteral && t & TypeFlags.EnumLiteral)) return true;
}
if (s & TypeFlags.NumberLike && t & TypeFlags.Range) {
return isRangeTypeRelatedTo(source, <RangeType>target, relation);
}
return false;
}

function isRangeTypeRelatedTo(source: Type, target: RangeType, relation: Map<RelationComparisonResult>) {
if (relation === comparableRelation) return true;
if (relation === assignableRelation || relation === subtypeRelation) {
if (source.flags & TypeFlags.Range) {
return isRangeContainedBy(<RangeType>source, target);
}
if (source.flags & TypeFlags.NumberLiteral) {
return isValueInRange(target, (<NumberLiteralType>source).value);
}
}
return false;
}

function isRangeContainedBy(source: RangeType, target: RangeType) {
if (!source.min && target.min) return false;
if (!source.max && target.max) return false;
if (source.min && target.min) {
if ((source.min.value === target.min.value && !source.minOpen && target.minOpen
|| source.min.value < target.min.value)) {
return false;
}
}
if (source.max && target.max) {
if ((source.max.value === target.max.value && !source.maxOpen && target.maxOpen
|| source.max.value > target.max.value)) {
return false;
}
}
return true;
}

function isValueInRange(range: RangeType, value: number) {
if (range.min && (range.minOpen && value === range.min.value || value < range.min.value)) {
return false;
}
if (range.max && (range.maxOpen && value === range.max.value || value > range.max.value)) {
return false;
}
return true;
}

function isTypeRelatedTo(source: Type, target: Type, relation: Map<RelationComparisonResult>) {
if (isFreshLiteralType(source)) {
source = (<FreshableType>source).regularType;
Expand Down Expand Up @@ -15229,6 +15330,15 @@ namespace ts {
}
}
}
if (flags & TypeFlags.Range) {
// assert types
const src = <RangeType>source;
const tgt = <RangeType>target;
if (src.min === tgt.min && src.minOpen === tgt.minOpen
&& src.max === tgt.max && tgt.maxOpen === tgt.maxOpen) {
return Ternary.True;
}
}
return Ternary.False;
}

Expand Down Expand Up @@ -17125,13 +17235,15 @@ namespace ts {

// Returns the String, Number, Boolean, StringLiteral, NumberLiteral, BooleanLiteral, Void, Undefined, or Null
// flags for the string, number, boolean, "", 0, false, void, undefined, or null types respectively. Returns
// no flags for all other types (including non-falsy literal types).
// the Range flag for ranges if they can possibly contain 0, and no flags for all other types (including
// non-falsy literal types).
function getFalsyFlags(type: Type): TypeFlags {
return type.flags & TypeFlags.Union ? getFalsyFlagsOfTypes((<UnionType>type).types) :
type.flags & TypeFlags.StringLiteral ? (<StringLiteralType>type).value === "" ? TypeFlags.StringLiteral : 0 :
type.flags & TypeFlags.NumberLiteral ? (<NumberLiteralType>type).value === 0 ? TypeFlags.NumberLiteral : 0 :
type.flags & TypeFlags.BigIntLiteral ? isZeroBigInt(<BigIntLiteralType>type) ? TypeFlags.BigIntLiteral : 0 :
type.flags & TypeFlags.BooleanLiteral ? (type === falseType || type === regularFalseType) ? TypeFlags.BooleanLiteral : 0 :
type.flags & TypeFlags.Range ? isValueInRange(<RangeType>type, 0) ? TypeFlags.Range : 0 :
type.flags & TypeFlags.PossiblyFalsy;
}

Expand Down Expand Up @@ -18788,6 +18900,12 @@ namespace ts {
if (flags & (TypeFlags.Number | TypeFlags.Enum)) {
return strictNullChecks ? TypeFacts.NumberStrictFacts : TypeFacts.NumberFacts;
}
if (flags & TypeFlags.Range) {
const maybeZero = isValueInRange(<RangeType>type, 0);
return strictNullChecks ?
maybeZero ? TypeFacts.NumberStrictFacts : TypeFacts.NonZeroNumberStrictFacts :
maybeZero ? TypeFacts.NumberFacts : TypeFacts.NonZeroNumberFacts;
}
if (flags & TypeFlags.NumberLiteral) {
const isZero = (<NumberLiteralType>type).value === 0;
return strictNullChecks ?
Expand Down Expand Up @@ -29122,6 +29240,10 @@ namespace ts {
forEach(node.types, checkSourceElement);
}

function checkHalfRangeType(node: HalfRangeTypeNode) {
forEachChild(node, checkSourceElement);
}

function checkIndexedAccessIndexType(type: Type, accessNode: IndexedAccessTypeNode | ElementAccessExpression) {
if (!(type.flags & TypeFlags.IndexedAccess)) {
return type;
Expand Down Expand Up @@ -33627,6 +33749,8 @@ namespace ts {
case SyntaxKind.UnionType:
case SyntaxKind.IntersectionType:
return checkUnionOrIntersectionType(<UnionOrIntersectionTypeNode>node);
case SyntaxKind.HalfRangeType:
return checkHalfRangeType(<HalfRangeTypeNode>node);
case SyntaxKind.ParenthesizedType:
case SyntaxKind.OptionalType:
case SyntaxKind.RestType:
Expand Down
10 changes: 10 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,8 @@ namespace ts {
return emitIndexedAccessType(<IndexedAccessTypeNode>node);
case SyntaxKind.MappedType:
return emitMappedType(<MappedTypeNode>node);
case SyntaxKind.HalfRangeType:
return emitHalfRangeType(<HalfRangeTypeNode>node);
case SyntaxKind.LiteralType:
return emitLiteralType(<LiteralTypeNode>node);
case SyntaxKind.ImportType:
Expand Down Expand Up @@ -2196,6 +2198,14 @@ namespace ts {
writePunctuation("}");
}

function emitHalfRangeType(node: HalfRangeTypeNode) {
writePunctuation("(");
writeTokenNode(node.operator, writeOperator);
writeSpace();
emit(node.basis);
writePunctuation(")");
}

function emitLiteralType(node: LiteralTypeNode) {
emitExpression(node.literal);
}
Expand Down
14 changes: 14 additions & 0 deletions src/compiler/factoryPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,20 @@ namespace ts {
: node;
}

export function createHalfRangeTypeNode(operator: HalfRangeTypeNode["operator"], basis: HalfRangeTypeNode["basis"]) {
const node = createSynthesizedNode(SyntaxKind.HalfRangeType) as HalfRangeTypeNode;
node.operator = operator;
node.basis = basis;
return node;
}

export function updateHalfRangeTypeNode(node: HalfRangeTypeNode, operator: HalfRangeTypeNode["operator"], basis: HalfRangeTypeNode["basis"]) {
return node.operator !== operator
|| node.basis !== basis
? updateNode(createHalfRangeTypeNode(operator, basis), node)
: node;
}

export function createLiteralTypeNode(literal: LiteralTypeNode["literal"]) {
const node = createSynthesizedNode(SyntaxKind.LiteralType) as LiteralTypeNode;
node.literal = literal;
Expand Down
57 changes: 55 additions & 2 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,9 @@ namespace ts {
visitNode(cbNode, (<MappedTypeNode>node).typeParameter) ||
visitNode(cbNode, (<MappedTypeNode>node).questionToken) ||
visitNode(cbNode, (<MappedTypeNode>node).type);
case SyntaxKind.HalfRangeType:
return visitNode(cbNode, (<HalfRangeTypeNode>node).operator) ||
visitNode(cbNode, (<HalfRangeTypeNode>node).basis);
case SyntaxKind.LiteralType:
return visitNode(cbNode, (<LiteralTypeNode>node).literal);
case SyntaxKind.ObjectBindingPattern:
Expand Down Expand Up @@ -2993,6 +2996,42 @@ namespace ts {
return finishNode(node);
}

function parseParenthesizedOrHalfRangeType(): TypeNode {
if (lookAhead(isStartOfHalfRangeType)) {
return tryParse(parseHalfRangeType) || parseParenthesizedType();
}
return parseParenthesizedType();
}

function parseHalfRangeType(): TypeNode | undefined {
const node = <HalfRangeTypeNode>createNode(SyntaxKind.HalfRangeType);
parseExpected(SyntaxKind.OpenParenToken);
reScanGreaterToken();
node.operator = parseTokenNode<HalfRangeTypeNode["operator"]>();

let unaryMinus: PrefixUnaryExpression | undefined;
if (token() === SyntaxKind.MinusToken) {
unaryMinus = <PrefixUnaryExpression>createNode(SyntaxKind.PrefixUnaryExpression);
unaryMinus.operator = SyntaxKind.MinusToken;
nextToken();
}

if (token() !== SyntaxKind.NumericLiteral) {
return undefined;
}
let basis: NumericLiteral | PrefixUnaryExpression = <NumericLiteral>parseLiteralNode();

if (unaryMinus !== undefined) {
unaryMinus.operand = basis;
finishNode(unaryMinus);
basis = unaryMinus;
}

node.basis = basis;
parseExpected(SyntaxKind.CloseParenToken);
return finishNode(node);
}

function parseParenthesizedType(): TypeNode {
const node = <ParenthesizedTypeNode>createNode(SyntaxKind.ParenthesizedType);
parseExpected(SyntaxKind.OpenParenToken);
Expand Down Expand Up @@ -3119,7 +3158,7 @@ namespace ts {
case SyntaxKind.OpenBracketToken:
return parseTupleType();
case SyntaxKind.OpenParenToken:
return parseParenthesizedType();
return parseParenthesizedOrHalfRangeType();
case SyntaxKind.ImportKeyword:
return parseImportType();
case SyntaxKind.AssertsKeyword:
Expand Down Expand Up @@ -3173,7 +3212,7 @@ namespace ts {
case SyntaxKind.OpenParenToken:
// Only consider '(' the start of a type if followed by ')', '...', an identifier, a modifier,
// or something that starts a type. We don't want to consider things like '(1)' a type.
return !inStartOfParameter && lookAhead(isStartOfParenthesizedOrFunctionType);
return !inStartOfParameter && (lookAhead(isStartOfHalfRangeType) || lookAhead(isStartOfParenthesizedOrFunctionType));
default:
return isIdentifier();
}
Expand All @@ -3184,6 +3223,20 @@ namespace ts {
return token() === SyntaxKind.CloseParenToken || isStartOfParameter(/*isJSDocParameter*/ false) || isStartOfType();
}

function isStartOfHalfRangeType(): boolean {
nextToken();
switch (token()) {
case SyntaxKind.LessThanToken:
case SyntaxKind.LessThanEqualsToken:
return true;
case SyntaxKind.GreaterThanToken:
reScanGreaterToken();
return token() === SyntaxKind.GreaterThanToken || token() === SyntaxKind.GreaterThanEqualsToken;
default:
return false;
}
}

function parsePostfixTypeOrHigher(): TypeNode {
let type = parseNonArrayType();
while (!scanner.hasPrecedingLineBreak()) {
Expand Down
Loading