Skip to content

Add support for F#-style Pipeline Operator #38305

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
146 changes: 144 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27829,6 +27829,10 @@ namespace ts {
let effectiveParameterCount = getParameterCount(signature);
let effectiveMinimumArguments = getMinArgumentCount(signature);

if (isPipelineApplicationExpression(node)) {
return true;
}
else
if (node.kind === SyntaxKind.TaggedTemplateExpression) {
argCount = args.length;
if (node.template.kind === SyntaxKind.TemplateExpression) {
Expand Down Expand Up @@ -28358,7 +28362,9 @@ namespace ts {
if (isJsxOpeningLikeElement(node)) {
return node.attributes.properties.length > 0 || (isJsxOpeningElement(node) && node.parent.children.length > 0) ? [node.attributes] : emptyArray;
}
const args = node.arguments || emptyArray;
const args = isPipelineApplicationExpression(node)
? [node.argument]
: node.arguments || emptyArray;
const spreadIndex = getSpreadArgumentIndex(args);
if (spreadIndex >= 0) {
// Create synthetic arguments from spreads of tuple types.
Expand Down Expand Up @@ -29096,7 +29102,7 @@ namespace ts {
}
else {
let relatedInformation: DiagnosticRelatedInformation | undefined;
if (node.arguments.length === 1) {
if (isPipelineApplicationExpression(node) || node.arguments.length === 1) {
const text = getSourceFileOfNode(node).text;
if (isLineBreak(text.charCodeAt(skipTrivia(text, node.expression.end, /* stopAfterLineBreak */ true) - 1))) {
relatedInformation = createDiagnosticForNode(node.expression, Diagnostics.Are_you_missing_a_semicolon);
Expand Down Expand Up @@ -29131,6 +29137,85 @@ namespace ts {
return resolveCall(node, callSignatures, candidatesOutArray, checkMode, callChainFlags);
}

function resolvePipelineApplicationExpression(node: PipelineApplicationExpression, candidatesOutArray: Signature[] | undefined, checkMode: CheckMode): Signature {
let funcType = checkExpression(node.expression);

funcType = checkNonNullTypeWithReporter(
funcType,
node.expression,
reportCannotInvokePossiblyNullOrUndefinedError
);

if (funcType === silentNeverType) {
return silentNeverSignature;
}

const apparentType = getApparentType(funcType);
if (apparentType === errorType) {
// Another error has already been reported
return resolveErrorCall(node);
}

// Technically, this signatures list may be incomplete. We are taking the apparent type,
// but we are not including call signatures that may have been added to the Object or
// Function interface, since they have none by default. This is a bit of a leap of faith
// that the user will not add any.
const callSignatures = getSignaturesOfType(apparentType, SignatureKind.Call);
const numConstructSignatures = getSignaturesOfType(apparentType, SignatureKind.Construct).length;

// TS 1.0 Spec: 4.12
// In an untyped function call no TypeArgs are permitted, Args can be any argument list, no contextual
// types are provided for the argument expressions, and the result is always of type Any.
if (isUntypedFunctionCall(funcType, apparentType, callSignatures.length, numConstructSignatures)) {
// The unknownType indicates that an error already occurred (and was reported). No
// need to report another error in this case.
// if (funcType !== errorType && node.typeArguments) {
// error(node, Diagnostics.Untyped_function_calls_may_not_accept_type_arguments);
// }
return resolveUntypedCall(node);
}
// If FuncExpr's apparent type(section 3.8.1) is a function type, the call is a typed function call.
// TypeScript employs overload resolution in typed function calls in order to support functions
// with multiple call signatures.
if (!callSignatures.length) {
if (numConstructSignatures) {
error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType));
}
else {
let relatedInformation: DiagnosticRelatedInformation | undefined;
const text = getSourceFileOfNode(node).text;
if (isLineBreak(text.charCodeAt(skipTrivia(text, node.expression.end, /* stopAfterLineBreak */ true) - 1))) {
relatedInformation = createDiagnosticForNode(node.expression, Diagnostics.Are_you_missing_a_semicolon);
}
invocationError(node.expression, apparentType, SignatureKind.Call, relatedInformation);
}
return resolveErrorCall(node);
}
// When a call to a generic function is an argument to an outer call to a generic function for which
// inference is in process, we have a choice to make. If the inner call relies on inferences made from
// its contextual type to its return type, deferring the inner call processing allows the best possible
// contextual type to accumulate. But if the outer call relies on inferences made from the return type of
// the inner call, the inner call should be processed early. There's no sure way to know which choice is
// right (only a full unification algorithm can determine that), so we resort to the following heuristic:
// If no type arguments are specified in the inner call and at least one call signature is generic and
// returns a function type, we choose to defer processing. This narrowly permits function composition
// operators to flow inferences through return types, but otherwise processes calls right away. We
// use the resolvingSignature singleton to indicate that we deferred processing. This result will be
// propagated out and eventually turned into nonInferrableType (a type that is assignable to anything and
// from which we never make inferences).
if (checkMode & CheckMode.SkipGenericFunctions && callSignatures.some(isGenericFunctionReturningFunction)) {
skippedGenericFunction(node, checkMode);
return resolvingSignature;
}
// If the function is explicitly marked with `@class`, then it must be constructed.
if (callSignatures.some(sig => isInJSFile(sig.declaration) && !!getJSDocClassTag(sig.declaration!))) {
error(node, Diagnostics.Value_of_type_0_is_not_callable_Did_you_mean_to_include_new, typeToString(funcType));
return resolveErrorCall(node);
}

return resolveCall(node, callSignatures, candidatesOutArray, checkMode, SignatureFlags.None);
}

function isGenericFunctionReturningFunction(signature: Signature) {
return !!(signature.typeParameters && isFunctionType(getReturnTypeOfSignature(signature)));
}
Expand Down Expand Up @@ -29588,6 +29673,8 @@ namespace ts {
case SyntaxKind.JsxOpeningElement:
case SyntaxKind.JsxSelfClosingElement:
return resolveJsxOpeningLikeElement(node, candidatesOutArray, checkMode);
case SyntaxKind.PipelineApplicationExpression:
return resolvePipelineApplicationExpression(node, candidatesOutArray, checkMode);
}
throw Debug.assertNever(node, "Branch in 'resolveSignature' should be unreachable.");
}
Expand Down Expand Up @@ -29838,6 +29925,59 @@ namespace ts {
}
}

/**
* Syntactically and semantically checks a pipeline expression.
* @param node The call/new expression to be checked.
* @returns On success, the expression's signature's return type. On failure, anyType.
*/
function checkPipelineApplicationExpression(node: PipelineApplicationExpression, checkMode?: CheckMode): Type {
// if (!checkGrammarTypeArguments(node, node.typeArguments)) checkGrammarArguments(node.arguments);

const signature = getResolvedSignature(node, /*candidatesOutArray*/ undefined, checkMode);
if (signature === resolvingSignature) {
// CheckMode.SkipGenericFunctions is enabled and this is a call to a generic function that
// returns a function type. We defer checking and return nonInferrableType.
return nonInferrableType;
}

if (node.expression.kind === SyntaxKind.SuperKeyword) {
return voidType;
}

// In JavaScript files, calls to any identifier 'require' are treated as external module imports
if (isInJSFile(node) && isCommonJsRequire(node)) {
return resolveExternalModuleTypeByLiteral(node.argument as StringLiteral);
}

const returnType = getReturnTypeOfSignature(signature);
// Treat any call to the global 'Symbol' function that is part of a const variable or readonly property
// as a fresh unique symbol literal type.
if (returnType.flags & TypeFlags.ESSymbolLike && isSymbolOrSymbolForCall(node)) {
return getESSymbolLikeTypeForNode(walkUpParenthesizedExpressions(node.parent));
}
if (node.parent.kind === SyntaxKind.ExpressionStatement &&
returnType.flags & TypeFlags.Void && getTypePredicateOfSignature(signature)) {
if (!isDottedName(node.expression)) {
error(node.expression, Diagnostics.Assertions_require_the_call_target_to_be_an_identifier_or_qualified_name);
}
// else if (!getEffectsSignature(node)) {
// const diagnostic = error(node.expression, Diagnostics.Assertions_require_every_name_in_the_call_target_to_be_declared_with_an_explicit_type_annotation);
// getTypeOfDottedName(node.expression, diagnostic);
// }
}

if (isInJSFile(node)) {
const jsSymbol = getSymbolOfExpando(node, /* allowDeclaration */ true);
if (jsSymbol && !!jsSymbol.exports?.size) {
const jsAssignmentType = createAnonymousType(jsSymbol, jsSymbol.exports, emptyArray, emptyArray, undefined, undefined);
jsAssignmentType.objectFlags |= ObjectFlags.JSLiteral;
return getIntersectionType([returnType, jsAssignmentType]);
}
}

return returnType;
}

function isSymbolOrSymbolForCall(node: Node) {
if (!isCallExpression(node)) return false;
let left = node.expression;
Expand Down Expand Up @@ -32635,6 +32775,8 @@ namespace ts {
// falls through
case SyntaxKind.NewExpression:
return checkCallExpression(<CallExpression>node, checkMode);
case SyntaxKind.PipelineApplicationExpression:
return checkPipelineApplicationExpression(<PipelineApplicationExpression>node, checkMode);
case SyntaxKind.TaggedTemplateExpression:
return checkTaggedTemplateExpression(<TaggedTemplateExpression>node);
case SyntaxKind.ParenthesizedExpression:
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6456,6 +6456,12 @@ namespace ts {
visitTypeNodeList(node.typeArguments),
visitExpressionList(node.arguments));

case SyntaxKind.PipelineApplicationExpression:
Debug.type<PipelineApplicationExpression>(node);
return factory.updatePipelineApplicationExpression(node,
visitExpression(node.expression),
visitTypeNodeList(node.typeArguments),
visitExpression(node.argument));
case SyntaxKind.NewExpression:
Debug.type<NewExpression>(node);
return factory.updateNewExpression(node,
Expand Down
24 changes: 24 additions & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,8 @@ namespace ts {
updateNonNullChain,
createMetaProperty,
updateMetaProperty,
createPipelineApplicationExpression,
updatePipelineApplicationExpression,
createTemplateSpan,
updateTemplateSpan,
createSemicolonClassElement,
Expand Down Expand Up @@ -3060,6 +3062,28 @@ namespace ts {
: node;
}

// @api
function createPipelineApplicationExpression(expression: Expression, typeArguments: readonly TypeNode[] | undefined, argument: Expression): PipelineApplicationExpression {
const node = createBaseExpression<PipelineApplicationExpression>(SyntaxKind.PipelineApplicationExpression);
node.argument = parenthesizerRules().parenthesizeExpressionOfExpressionStatement(argument);
node.expression = parenthesizerRules().parenthesizeExpressionOfExpressionStatement(expression);
node.typeArguments = typeArguments && parenthesizerRules().parenthesizeTypeArguments(typeArguments);;
node.transformFlags |=
propagateChildFlags(node.argument) |
propagateChildFlags(node.expression) |
TransformFlags.ContainsPipeline;
return node;
}

// @api
function updatePipelineApplicationExpression(node: PipelineApplicationExpression, expression: Expression, typeArguments: readonly TypeNode[] | undefined, argument: Expression): PipelineApplicationExpression {
return node.expression !== expression
|| node.argument !== argument
|| node.typeArguments !== typeArguments
? update(createPipelineApplicationExpression(expression, typeArguments, argument), node)
: node;
}

//
// Misc
//
Expand Down
25 changes: 23 additions & 2 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ namespace ts {
visitNode(cbNode, (<CallExpression>node).questionDotToken) ||
visitNodes(cbNode, cbNodes, (<CallExpression>node).typeArguments) ||
visitNodes(cbNode, cbNodes, (<CallExpression>node).arguments);
case SyntaxKind.PipelineApplicationExpression:
return visitNode(cbNode, (<PipelineApplicationExpression>node).argument) ||
visitNode(cbNode, (<PipelineApplicationExpression>node).barGreaterThanToken) ||
visitNode(cbNode, (<PipelineApplicationExpression>node).expression) ||
visitNodes(cbNode, cbNodes, (<PipelineApplicationExpression>node).typeArguments) ;
case SyntaxKind.TaggedTemplateExpression:
return visitNode(cbNode, (<TaggedTemplateExpression>node).tag) ||
visitNode(cbNode, (<TaggedTemplateExpression>node).questionDotToken) ||
Expand Down Expand Up @@ -4025,7 +4030,7 @@ namespace ts {
// binary expression here, so we pass in the 'lowest' precedence here so that it matches
// and consumes anything.
const pos = getNodePos();
const expr = parseBinaryExpressionOrHigher(OperatorPrecedence.Lowest);
const expr = parseBinaryExpressionOrHigher(/*precedence*/ 1);

// To avoid a look-ahead, we did not handle the case of an arrow function with a single un-parenthesized
// parameter ('x => ...') above. We handle it here by checking if the parsed expression was a single
Expand Down Expand Up @@ -4430,7 +4435,23 @@ namespace ts {
return node;
}

function parseConditionalExpressionRest(leftOperand: Expression, pos: number): Expression {
function parsePipelineApplicationExpression(leftOperand: Expression): Expression {
return finishNode(
factory.createPipelineApplicationExpression(
parseBinaryExpressionOrHigher(/*precedence*/ 1),
/*typeArguments*/ undefined,
leftOperand
),
leftOperand.pos
);
}

function parseConditionalExpressionRest(startLeftOperand: Expression, pos: number): Expression {
let leftOperand = startLeftOperand;
while (parseOptionalToken(SyntaxKind.BarGreaterThanToken)) {
leftOperand = parsePipelineApplicationExpression(leftOperand);
}

// Note: we are passed in an expression which was produced from parseBinaryExpressionOrHigher.
const questionToken = parseOptionalToken(SyntaxKind.QuestionToken);
if (!questionToken) {
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ namespace ts {
"~": SyntaxKind.TildeToken,
"&&": SyntaxKind.AmpersandAmpersandToken,
"||": SyntaxKind.BarBarToken,
"|>": SyntaxKind.BarGreaterThanToken,
"?": SyntaxKind.QuestionToken,
"??": SyntaxKind.QuestionQuestionToken,
"?.": SyntaxKind.QuestionDotToken,
Expand Down Expand Up @@ -1992,6 +1993,9 @@ namespace ts {
if (text.charCodeAt(pos + 1) === CharacterCodes.equals) {
return pos += 2, token = SyntaxKind.BarEqualsToken;
}
if (text.charCodeAt(pos + 1) === CharacterCodes.greaterThan) {
return pos += 2, token = SyntaxKind.BarGreaterThanToken;
}
pos++;
return token = SyntaxKind.BarToken;
case CharacterCodes.closeBrace:
Expand Down
1 change: 1 addition & 0 deletions src/compiler/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ namespace ts {

transformers.push(transformTypeScript);
transformers.push(transformClassFields);
transformers.push(transformPipeline);

if (getJSXTransformEnabled(compilerOptions)) {
transformers.push(transformJsx);
Expand Down
28 changes: 28 additions & 0 deletions src/compiler/transformers/pipeline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*@internal*/
namespace ts {
export const transformPipeline: TransformerFactory<SourceFile> =
(context: TransformationContext) => {
const {
factory,
} = context;
return sourceFile => {
const findPipelineVisitor = (node: Node): Node => {
if (node.transformFlags & TransformFlags.ContainsPipeline) {
if (isPipelineApplicationExpression(node)) {
const call = factory.createCallExpression(
visitNode(node.expression, findPipelineVisitor),
/*typeArguments*/ undefined,
[visitNode(node.argument, findPipelineVisitor)]
);
setSourceMapRange(call, node);
setCommentRange(call, node);
return call;
}
return visitEachChild(node, findPipelineVisitor, context);
};
return node;
};
return visitNode(sourceFile, findPipelineVisitor);
};
};
}
Loading