Skip to content

Commit ea74ce1

Browse files
committed
Fix binary expression parsing
1 parent 9141ddb commit ea74ce1

File tree

2 files changed

+98
-38
lines changed

2 files changed

+98
-38
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/******************************************************************************
2+
* Copyright 2025 TypeFox GmbH
3+
* This program and the accompanying materials are made available under the
4+
* terms of the MIT License, which is available in the project root.
5+
******************************************************************************/
6+
7+
import { describe, expect, test } from 'vitest';
8+
import { createArithmeticsServices } from '../src/language-server/arithmetics-module.js';
9+
import { EmptyFileSystem } from 'langium';
10+
import type { Evaluation, Module } from '../src/language-server/generated/ast.js';
11+
import { isBinaryExpression, isFunctionCall, isNumberLiteral, type Expression } from '../src/language-server/generated/ast.js';
12+
13+
describe('Test the arithmetics parsing', () => {
14+
15+
const services = createArithmeticsServices(EmptyFileSystem);
16+
const parser = services.arithmetics.parser.LangiumParser;
17+
18+
function printExpression(expr: Expression): string {
19+
if (isBinaryExpression(expr)) {
20+
return '(' + printExpression(expr.left) + ' ' + expr.operator + ' ' + printExpression(expr.right) + ')';
21+
} else if (isNumberLiteral(expr)) {
22+
return expr.value.toString();
23+
} else if (isFunctionCall(expr)) {
24+
return expr.func.$refText;
25+
}
26+
return '';
27+
}
28+
29+
function parseExpression(text: string): Expression {
30+
const parseResult = parser.parse('module test ' + text);
31+
return ((parseResult.value as Module).statements[0] as Evaluation).expression;
32+
}
33+
34+
test('Single expression', () => {
35+
const expr = parseExpression('1');
36+
expect(printExpression(expr)).toBe('1');
37+
});
38+
39+
test('Binary expression', () => {
40+
const expr = parseExpression('1 + 2 ^ 3 * 4 % 5');
41+
expect(printExpression(expr)).toBe('(1 + ((2 ^ 3) * (4 % 5)))');
42+
});
43+
44+
test('Nested expression', () => {
45+
const expr = parseExpression('(1 + 2) ^ 3');
46+
// Assert that the nested expression is correctly represented in the AST
47+
// If the expression parsing would be too eager, the result would be (1 + (2 ^ 3))
48+
expect(printExpression(expr)).toBe('((1 + 2) ^ 3)');
49+
});
50+
});

packages/langium/src/parser/langium-parser.ts

+48-38
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { AbstractElement, Action, Assignment, InfixRule, ParserRule } from
1010
import { isInfixRule } from '../languages/generated/ast.js';
1111
import type { Linker } from '../references/linker.js';
1212
import type { LangiumCoreServices } from '../services.js';
13-
import type { AstNode, AstReflection, CompositeCstNode, CstNode, Mutable } from '../syntax-tree.js';
13+
import type { AstNode, AstReflection, CompositeCstNode, CstNode } from '../syntax-tree.js';
1414
import type { Lexer, LexerResult } from './lexer.js';
1515
import type { IParserConfig } from './parser-config.js';
1616
import type { ValueConverter } from './value-converter.js';
@@ -427,45 +427,55 @@ export class LangiumParser extends AbstractLangiumParser {
427427
// Simply return the expression as is.
428428
return obj.parts[0];
429429
}
430-
let expression = {
431-
$type: obj.$type,
432-
left: obj.parts[0],
433-
operator: obj.operators[0],
434-
right: obj.parts[1]
435-
};
436-
let lastPrecedence = precedence.get(expression.operator) ?? 0;
437-
for (let i = 1; i < obj.operators.length; i++) {
438-
const op = obj.operators[i];
439-
const next = obj.parts[i + 1];
440-
const currentPrecedence = precedence.get(op) ?? 0;
441-
if (currentPrecedence <= lastPrecedence) {
442-
// If the current precendence is higher (i.e. the rank is lower)
443-
// We simply create a new node and append the previous expression to the left
444-
expression = {
445-
$type: obj.$type,
446-
left: expression,
447-
operator: op,
448-
right: next
449-
};
450-
} else {
451-
// If the precedence is lower, we need to rewrite the previous node
452-
// For that, we move the previous right node to the left side of the new node
453-
const rewrite = {
454-
$type: obj.$type,
455-
left: expression.right,
456-
operator: op,
457-
right: next
458-
};
459-
// This new node now becomes the right side of the previous node
460-
expression.right = rewrite;
430+
// Find the operator with the lowest precedence (highest value in precedence map)
431+
let lowestPrecedenceIdx = 0;
432+
let lowestPrecedenceValue = -1;
433+
434+
for (let i = 0; i < obj.operators.length; i++) {
435+
const operator = obj.operators[i];
436+
const precedenceValue = precedence.get(operator) ?? Infinity;
437+
438+
// If we find an operator with lower precedence or equal precedence
439+
// (for left-to-right evaluation), update our tracking
440+
if (precedenceValue > lowestPrecedenceValue) {
441+
lowestPrecedenceValue = precedenceValue;
442+
lowestPrecedenceIdx = i;
461443
}
462-
lastPrecedence = currentPrecedence;
463444
}
464-
// In theory, we could rebuild the CST for the infix expression
465-
// However, there is no real benefit for it, and it might be costly (performance-wise)
466-
// We might want to revisit this decision in the future.
467-
(expression as Mutable<AstNode>).$cstNode = obj.$cstNode;
468-
return expression;
445+
446+
// Split the expression at the lowest precedence operator
447+
const leftOperators = obj.operators.slice(0, lowestPrecedenceIdx);
448+
const rightOperators = obj.operators.slice(lowestPrecedenceIdx + 1);
449+
450+
const leftParts = obj.parts.slice(0, lowestPrecedenceIdx + 1);
451+
const rightParts = obj.parts.slice(lowestPrecedenceIdx + 1);
452+
453+
// Create sub-expressions
454+
const leftInfix: InfixElement = {
455+
$type: obj.$type,
456+
$cstNode: obj.$cstNode,
457+
parts: leftParts,
458+
operators: leftOperators
459+
};
460+
const rightInfix: InfixElement = {
461+
$type: obj.$type,
462+
$cstNode: obj.$cstNode,
463+
parts: rightParts,
464+
operators: rightOperators
465+
};
466+
467+
// Recursively build the left and right subtrees
468+
const leftTree = this.constructInfix(leftInfix, precedence);
469+
const rightTree = this.constructInfix(rightInfix, precedence);
470+
471+
// Create the final binary expression
472+
return {
473+
$type: obj.$type,
474+
$cstNode: obj.$cstNode,
475+
left: leftTree,
476+
operator: obj.operators[lowestPrecedenceIdx],
477+
right: rightTree
478+
};
469479
}
470480

471481
private getAssignment(feature: AbstractElement): AssignmentElement {

0 commit comments

Comments
 (0)