function readBinOpExpression()

in src/powerquery-parser/parser/parsers/combinatorialParser.ts [64:236]


function readBinOpExpression(
    state: ParseState,
    parser: Parser,
    nodeKind: Ast.NodeKind,
): Ast.TBinOpExpression | Ast.TUnaryExpression | Ast.TNullablePrimitiveType {
    state.maybeCancellationToken?.throwIfCancelled();
    ParseStateUtils.startContext(state, nodeKind);
    const placeholderContextId: number = Assert.asDefined(state.maybeCurrentContextNode).id;

    // operators/operatorConstants are of length N
    // expressions are of length N + 1
    let operators: Constant.TBinOpExpressionOperator[] = [];
    let operatorConstants: Ast.IConstant<Constant.TBinOpExpressionOperator>[] = [];

    let expressions: (Ast.TBinOpExpression | Ast.TUnaryExpression | Ast.TNullablePrimitiveType)[] = [
        parser.readUnaryExpression(state, parser),
    ];

    let maybeOperator: Constant.TBinOpExpressionOperator | undefined =
        ConstantUtils.maybeBinOpExpressionOperatorKindFrom(state.maybeCurrentTokenKind);

    while (maybeOperator !== undefined) {
        const operator: Constant.TBinOpExpressionOperator = maybeOperator;
        operators.push(operator);

        operatorConstants.push(
            NaiveParseSteps.readTokenKindAsConstant<Constant.TBinOpExpressionOperator>(
                state,
                Assert.asDefined(state.maybeCurrentTokenKind),
                maybeOperator,
            ),
        );

        switch (operator) {
            case Constant.KeywordConstant.As:
            case Constant.KeywordConstant.Is:
                expressions.push(parser.readNullablePrimitiveType(state, parser));
                break;

            default:
                expressions.push(parser.readUnaryExpression(state, parser));
        }

        maybeOperator = ConstantUtils.maybeBinOpExpressionOperatorKindFrom(state.maybeCurrentTokenKind);
    }

    // There was a single TUnaryExpression, not a TBinOpExpression.
    if (expressions.length === 1) {
        ParseStateUtils.deleteContext(state, placeholderContextId);

        return expressions[0];
    }

    // Build up the Ast by using the lowest precedence operator and the two adjacent expressions,
    // which might be previously built TBinOpExpression nodes.
    const nodeIdMapCollection: NodeIdMap.Collection = state.contextState.nodeIdMapCollection;
    const newNodeThreshold: number = state.contextState.idCounter;

    let placeholderContextChildren: ReadonlyArray<number> = MapUtils.assertGet(
        nodeIdMapCollection.childIdsById,
        placeholderContextId,
    );

    while (operators.length) {
        let minPrecedenceIndex: number = -1;
        let minPrecedence: number = Number.MAX_SAFE_INTEGER;

        for (let index: number = 0; index < operators.length; index += 1) {
            const currentPrecedence: number = ConstantUtils.binOpExpressionOperatorPrecedence(operators[index]);

            if (minPrecedence > currentPrecedence) {
                minPrecedence = currentPrecedence;
                minPrecedenceIndex = index;
            }
        }

        const newBinOpExpressionId: number = ParseContextUtils.nextId(state.contextState);

        const left: TypeScriptUtils.StripReadonly<
            Ast.TBinOpExpression | Ast.TUnaryExpression | Ast.TNullablePrimitiveType
        > = expressions[minPrecedenceIndex];

        const operator: Constant.TBinOpExpressionOperator = operators[minPrecedenceIndex];

        const operatorConstant: TypeScriptUtils.StripReadonly<Ast.IConstant<Constant.TBinOpExpressionOperator>> =
            operatorConstants[minPrecedenceIndex];

        const right: TypeScriptUtils.StripReadonly<
            Ast.TBinOpExpression | Ast.TUnaryExpression | Ast.TNullablePrimitiveType
        > = expressions[minPrecedenceIndex + 1];

        left.maybeAttributeIndex = 0;
        operatorConstant.maybeAttributeIndex = 1;
        right.maybeAttributeIndex = 2;

        const leftTokenRange: Token.TokenRange = left.tokenRange;
        const rightTokenRange: Token.TokenRange = right.tokenRange;

        const newBinOpExpression: Ast.TBinOpExpression = {
            kind: binOpExpressionNodeKindFrom(operator),
            id: newBinOpExpressionId,
            // maybeAttributeIndex is fixed after all TBinOpExpressions have been created.
            maybeAttributeIndex: 0,
            tokenRange: {
                tokenIndexStart: leftTokenRange.tokenIndexStart,
                tokenIndexEnd: rightTokenRange.tokenIndexEnd,
                positionStart: leftTokenRange.positionStart,
                positionEnd: rightTokenRange.positionEnd,
            },
            isLeaf: false,
            left: left as Ast.TBinOpExpression,
            operator,
            operatorConstant,
            right,
        } as Ast.TBinOpExpression;

        operators = ArrayUtils.removeAtIndex(operators, minPrecedenceIndex);
        operatorConstants = ArrayUtils.removeAtIndex(operatorConstants, minPrecedenceIndex);

        expressions = expressions = [
            ...expressions.slice(0, minPrecedenceIndex),
            newBinOpExpression,
            ...expressions.slice(minPrecedenceIndex + 2),
        ];

        // Correct the parentIds for the nodes combined into newBinOpExpression.
        nodeIdMapCollection.parentIdById.set(left.id, newBinOpExpressionId);
        nodeIdMapCollection.parentIdById.set(operatorConstant.id, newBinOpExpressionId);
        nodeIdMapCollection.parentIdById.set(right.id, newBinOpExpressionId);

        // Assign the nodeIdMap values for newBinOpExpression.
        nodeIdMapCollection.childIdsById.set(newBinOpExpressionId, [left.id, operatorConstant.id, right.id]);
        nodeIdMapCollection.astNodeById.set(newBinOpExpressionId, newBinOpExpression);

        const maybeIdsForSpecificNodeKind: Set<number> | undefined = nodeIdMapCollection.idsByNodeKind.get(
            newBinOpExpression.kind,
        );

        if (maybeIdsForSpecificNodeKind) {
            maybeIdsForSpecificNodeKind.add(newBinOpExpression.id);
        } else {
            nodeIdMapCollection.idsByNodeKind.set(newBinOpExpression.kind, new Set([newBinOpExpression.id]));
        }

        // All TUnaryExpression and operatorConstants start by being placed under the context node.
        // They need to be removed for deleteContext(placeholderContextId) to succeed.
        placeholderContextChildren = ArrayUtils.removeFirstInstance(placeholderContextChildren, operatorConstant.id);

        if (left.id <= newNodeThreshold) {
            placeholderContextChildren = ArrayUtils.removeFirstInstance(placeholderContextChildren, left.id);
        }

        if (right.id <= newNodeThreshold) {
            placeholderContextChildren = ArrayUtils.removeFirstInstance(placeholderContextChildren, right.id);
        }

        nodeIdMapCollection.childIdsById.set(placeholderContextId, placeholderContextChildren);
    }

    const lastExpression: Ast.TBinOpExpression | Ast.TUnaryExpression | Ast.TNullablePrimitiveType = expressions[0];

    Assert.isTrue(AstUtils.isTBinOpExpression(lastExpression), `lastExpression should be a TBinOpExpression`, {
        lastExpressionId: lastExpression.id,
        lastExpressionKind: lastExpression.kind,
    });

    nodeIdMapCollection.childIdsById.set(placeholderContextId, [lastExpression.id]);
    nodeIdMapCollection.parentIdById.set(lastExpression.id, placeholderContextId);

    ParseStateUtils.deleteContext(state, placeholderContextId);

    return lastExpression;
}