Skip to content

Commit 2808fcf

Browse files
authored
Add a ternary operator 'replace' (#521)
* feat: ✨ Added a replace ternary operator
1 parent 3874ff1 commit 2808fcf

File tree

13 files changed

+344
-9
lines changed

13 files changed

+344
-9
lines changed

apps/docs/docs/dev/04-guides/04-expressions-and-operators.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ As a small practical example, you may also have a look at the [arithmetics examp
1111

1212
## Expressions in the grammar
1313

14-
Expressions in the Jayvee grammar are defined in [`expression.langium`](https://github.com/jvalue/jayvee/blob/main/libs/language-server/src/grammar/expression.langium) and consist of operators (unary / binary) and literals.
15-
Unary operators only have a single operand (e.g. the `not` operator) whereas binary operators require two operands (e.g. the `*` operator).
14+
Expressions in the Jayvee grammar are defined in [`expression.langium`](https://github.com/jvalue/jayvee/blob/main/libs/language-server/src/grammar/expression.langium) and consist of operators (unary / binary / ternary) and literals.
15+
Unary operators only have a single operand (e.g. the `not` operator), binary operators require two operands (e.g. the `*` operator) and ternary operators require three operands.
1616

1717
The grammar is written in a way that literals end up in the leaves of the resulting AST and the nodes above represent the operators.
1818

apps/docs/docs/user/expressions.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ Expressions get evaluated at runtime by the interpreter to a [Built-in ValueType
1515

1616
The following expression is evaluated to the `integer` `10`: `(2 + 3) * 2`
1717

18-
The following expression is evaluated to the `integer` `3`: `floor (3.14)`
19-
2018
The following expression is evaluated to the `boolean` `true`: `"Example" == "Example"`
2119

20+
The following expression is evaluated to the `text` `I love Datypus`: `"I love platypuses" replace /platypuses/ with "Datypus"`
21+
2222
### List of Operators
2323

2424
#### Arithmetics (binary operators)
@@ -58,6 +58,9 @@ The following expression is evaluated to the `boolean` `true`: `"Example" == "Ex
5858
- `matches` for a regex match, e.g., `"A07" matches /^[A-Z0-9]*$/` evaluates to `true`
5959
- `in` for inclusion in an array, e.g., `"a" in ["a", "b", "c"]` evaluates to `true`
6060

61+
#### Text manipulation (ternary operators)
62+
- `replace [...] with [...]` replaces regex matches in a text with a string
63+
6164
### Operator Details
6265

6366
#### `in` Operator

libs/language-server/src/grammar/expression.langium

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ import './terminal'
1010
import './transform'
1111

1212
Expression:
13-
OrExpression;
13+
ReplaceExpression;
1414

1515
// The nesting of the following rules implies the precedence of the operators:
1616

17+
ReplaceExpression infers Expression:
18+
OrExpression ({infer TernaryExpression.first=current} operator='replace' second=OrExpression 'with' third=OrExpression)*;
19+
1720
OrExpression infers Expression:
1821
AndExpression ({infer BinaryExpression.left=current} operator='or' right=AndExpression)*;
1922

libs/language-server/src/lib/ast/expressions/evaluation.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
isReferenceLiteral,
2727
isRegexLiteral,
2828
isRuntimeParameterLiteral,
29+
isTernaryExpression,
2930
isTransformDefinition,
3031
isTransformPortDefinition,
3132
isUnaryExpression,
@@ -41,6 +42,7 @@ import { type InternalValueRepresentation } from './internal-value-representatio
4142
// eslint-disable-next-line import/no-cycle
4243
import {
4344
binaryOperatorRegistry,
45+
ternaryOperatorRegistry,
4446
unaryOperatorRegistry,
4547
} from './operator-registry';
4648
import { isEveryValueDefined } from './typeguards';
@@ -223,6 +225,11 @@ export function evaluateExpression(
223225
const evaluator = binaryOperatorRegistry[operator].evaluation;
224226
return evaluator.evaluate(expression, evaluationContext, strategy, context);
225227
}
228+
if (isTernaryExpression(expression)) {
229+
const operator = expression.operator;
230+
const evaluator = ternaryOperatorRegistry[operator].evaluation;
231+
return evaluator.evaluate(expression, evaluationContext, strategy, context);
232+
}
226233
assertUnreachable(expression);
227234
}
228235

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-only
4+
5+
// eslint-disable-next-line import/no-cycle
6+
import { DefaultTernaryOperatorEvaluator } from '../operator-evaluator';
7+
import { REGEXP_TYPEGUARD, STRING_TYPEGUARD } from '../typeguards';
8+
9+
export class ReplaceOperatorEvaluator extends DefaultTernaryOperatorEvaluator<
10+
string,
11+
RegExp,
12+
string,
13+
string
14+
> {
15+
constructor() {
16+
super('replace', STRING_TYPEGUARD, REGEXP_TYPEGUARD, STRING_TYPEGUARD);
17+
}
18+
override doEvaluate(
19+
firstValue: string,
20+
secondValue: RegExp,
21+
thirdValue: string,
22+
): string {
23+
return firstValue.replace(secondValue, thirdValue);
24+
}
25+
}

libs/language-server/src/lib/ast/expressions/operator-evaluator.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@
55
import { strict as assert } from 'assert';
66

77
import { ValidationContext } from '../../validation/validation-context';
8-
import { BinaryExpression, UnaryExpression } from '../generated/ast';
8+
import {
9+
BinaryExpression,
10+
TernaryExpression,
11+
UnaryExpression,
12+
} from '../generated/ast';
913
// eslint-disable-next-line import/no-cycle
1014
import {
1115
BinaryExpressionOperator,
16+
TernaryExpressionOperator,
1217
UnaryExpressionOperator,
1318
} from '../model-util';
1419

@@ -23,7 +28,7 @@ import {
2328
} from './internal-value-representation';
2429

2530
export interface OperatorEvaluator<
26-
E extends UnaryExpression | BinaryExpression,
31+
E extends UnaryExpression | BinaryExpression | TernaryExpression,
2732
> {
2833
readonly operator: E['operator'];
2934

@@ -189,3 +194,82 @@ export abstract class BooleanShortCircuitOperatorEvaluator
189194
return this.doEvaluate(leftValue, rightValue);
190195
}
191196
}
197+
198+
export abstract class DefaultTernaryOperatorEvaluator<
199+
FirstValue extends InternalValueRepresentation,
200+
SecondValue extends InternalValueRepresentation,
201+
ThirdValue extends InternalValueRepresentation,
202+
ReturnValue extends InternalValueRepresentation,
203+
> implements OperatorEvaluator<TernaryExpression>
204+
{
205+
constructor(
206+
public readonly operator: TernaryExpressionOperator,
207+
private readonly firstValueTypeguard: InternalValueRepresentationTypeguard<FirstValue>,
208+
private readonly secondValueTypeguard: InternalValueRepresentationTypeguard<SecondValue>,
209+
private readonly thirdValueTypeguard: InternalValueRepresentationTypeguard<ThirdValue>,
210+
) {}
211+
212+
protected abstract doEvaluate(
213+
firstValue: FirstValue,
214+
secondValue: SecondValue,
215+
thirdValue: ThirdValue,
216+
expression: TernaryExpression,
217+
context: ValidationContext | undefined,
218+
): ReturnValue | undefined;
219+
220+
evaluate(
221+
expression: TernaryExpression,
222+
evaluationContext: EvaluationContext,
223+
strategy: EvaluationStrategy,
224+
validationContext: ValidationContext | undefined,
225+
): ReturnValue | undefined {
226+
// The following linting exception can be removed when a second ternary operator is added
227+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
228+
assert(expression.operator === this.operator);
229+
230+
const firstValue = evaluateExpression(
231+
expression.first,
232+
evaluationContext,
233+
validationContext,
234+
strategy,
235+
);
236+
237+
if (strategy === EvaluationStrategy.LAZY && firstValue === undefined) {
238+
return undefined;
239+
}
240+
241+
const secondValue = evaluateExpression(
242+
expression.second,
243+
evaluationContext,
244+
validationContext,
245+
strategy,
246+
);
247+
248+
const thirdValue = evaluateExpression(
249+
expression.third,
250+
evaluationContext,
251+
validationContext,
252+
strategy,
253+
);
254+
255+
if (
256+
firstValue === undefined ||
257+
secondValue === undefined ||
258+
thirdValue === undefined
259+
) {
260+
return undefined;
261+
}
262+
263+
assert(this.firstValueTypeguard(firstValue));
264+
assert(this.secondValueTypeguard(secondValue));
265+
assert(this.thirdValueTypeguard(thirdValue));
266+
267+
return this.doEvaluate(
268+
firstValue,
269+
secondValue,
270+
thirdValue,
271+
expression,
272+
validationContext,
273+
);
274+
}
275+
}

libs/language-server/src/lib/ast/expressions/operator-registry.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44

55
/* eslint-disable import/no-cycle */
66

7-
import { BinaryExpression, UnaryExpression } from '../generated/ast';
7+
import {
8+
BinaryExpression,
9+
TernaryExpression,
10+
UnaryExpression,
11+
} from '../generated/ast';
812
import {
913
BinaryExpressionOperator,
14+
TernaryExpressionOperator,
1015
UnaryExpressionOperator,
1116
} from '../model-util';
1217

@@ -30,6 +35,7 @@ import { NotOperatorEvaluator } from './evaluators/not-operator-evaluator';
3035
import { OrOperatorEvaluator } from './evaluators/or-operator-evaluator';
3136
import { PlusOperatorEvaluator } from './evaluators/plus-operator-evaluator';
3237
import { PowOperatorEvaluator } from './evaluators/pow-operator-evaluator';
38+
import { ReplaceOperatorEvaluator } from './evaluators/replace-operator-evaluator';
3339
import { RootOperatorEvaluator } from './evaluators/root-operator-evaluator';
3440
import { RoundOperatorEvaluator } from './evaluators/round-operator-evaluator';
3541
import { SqrtOperatorEvaluator } from './evaluators/sqrt-operator-evaluator';
@@ -38,6 +44,7 @@ import { XorOperatorEvaluator } from './evaluators/xor-operator-evaluator';
3844
import { OperatorEvaluator } from './operator-evaluator';
3945
import {
4046
BinaryOperatorTypeComputer,
47+
TernaryOperatorTypeComputer,
4148
UnaryOperatorTypeComputer,
4249
} from './operator-type-computer';
4350
import { BasicArithmeticOperatorTypeComputer } from './type-computers/basic-arithmetic-operator-type-computer';
@@ -50,6 +57,7 @@ import { LogicalOperatorTypeComputer } from './type-computers/logical-operator-t
5057
import { MatchesOperatorTypeComputer } from './type-computers/matches-operator-type-computer';
5158
import { NotOperatorTypeComputer } from './type-computers/not-operator-type-computer';
5259
import { RelationalOperatorTypeComputer } from './type-computers/relational-operator-type-computer';
60+
import { ReplaceOperatorTypeComputer } from './type-computers/replace-operator-type-computer';
5361
import { SignOperatorTypeComputer } from './type-computers/sign-operator-type-computer';
5462
import { SqrtOperatorTypeComputer } from './type-computers/sqrt-operator-type-computer';
5563

@@ -174,3 +182,18 @@ export const binaryOperatorRegistry: Record<
174182
evaluation: new OrOperatorEvaluator(),
175183
},
176184
};
185+
186+
export interface TernaryOperatorEntry {
187+
typeInference: TernaryOperatorTypeComputer;
188+
evaluation: OperatorEvaluator<TernaryExpression>;
189+
}
190+
191+
export const ternaryOperatorRegistry: Record<
192+
TernaryExpressionOperator,
193+
TernaryOperatorEntry
194+
> = {
195+
replace: {
196+
typeInference: new ReplaceOperatorTypeComputer(),
197+
evaluation: new ReplaceOperatorEvaluator(),
198+
},
199+
};

libs/language-server/src/lib/ast/expressions/operator-type-computer.ts

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
// SPDX-License-Identifier: AGPL-3.0-only
44

55
import { ValidationContext } from '../../validation/validation-context';
6-
import { BinaryExpression, UnaryExpression } from '../generated/ast';
6+
import {
7+
BinaryExpression,
8+
TernaryExpression,
9+
UnaryExpression,
10+
} from '../generated/ast';
711
import { type Valuetype } from '../wrappers/value-type/valuetype';
812

913
export interface UnaryOperatorTypeComputer {
@@ -127,3 +131,100 @@ export function generateUnexpectedTypeMessage(
127131
) {
128132
return `The operand needs to be of type ${expectedType.getName()} but is of type ${actualType.getName()}`;
129133
}
134+
135+
export abstract class DefaultTernaryOperatorTypeComputer
136+
implements TernaryOperatorTypeComputer
137+
{
138+
constructor(
139+
protected readonly expectedFirstOperandType: Valuetype,
140+
protected readonly expectedSecondOperandType: Valuetype,
141+
protected readonly expectedThirdOperandType: Valuetype,
142+
) {}
143+
144+
computeType(
145+
firstOperandType: Valuetype,
146+
secondOperandType: Valuetype,
147+
thirdOperandType: Valuetype,
148+
expression: TernaryExpression,
149+
context: ValidationContext | undefined,
150+
): Valuetype | undefined {
151+
let typeErrorOccurred = false;
152+
153+
if (!firstOperandType.isConvertibleTo(this.expectedFirstOperandType)) {
154+
context?.accept(
155+
'error',
156+
generateUnexpectedTypeMessage(
157+
this.expectedFirstOperandType,
158+
firstOperandType,
159+
),
160+
{
161+
node: expression.first,
162+
},
163+
);
164+
typeErrorOccurred = true;
165+
}
166+
167+
if (!secondOperandType.isConvertibleTo(this.expectedSecondOperandType)) {
168+
context?.accept(
169+
'error',
170+
generateUnexpectedTypeMessage(
171+
this.expectedSecondOperandType,
172+
secondOperandType,
173+
),
174+
{
175+
node: expression.second,
176+
},
177+
);
178+
typeErrorOccurred = true;
179+
}
180+
181+
if (!thirdOperandType.isConvertibleTo(this.expectedThirdOperandType)) {
182+
context?.accept(
183+
'error',
184+
generateUnexpectedTypeMessage(
185+
this.expectedThirdOperandType,
186+
thirdOperandType,
187+
),
188+
{
189+
node: expression.third,
190+
},
191+
);
192+
typeErrorOccurred = true;
193+
}
194+
195+
if (typeErrorOccurred) {
196+
return undefined;
197+
}
198+
199+
return this.doComputeType(
200+
firstOperandType,
201+
secondOperandType,
202+
thirdOperandType,
203+
);
204+
}
205+
206+
protected abstract doComputeType(
207+
firstOperandType: Valuetype,
208+
secondOperandType: Valuetype,
209+
thirdOperandType: Valuetype,
210+
): Valuetype;
211+
}
212+
213+
export interface TernaryOperatorTypeComputer {
214+
/**
215+
* Computes the type of a ternary operator by the type of its operands.
216+
* @param firstType the type of the first operand
217+
* @param secondType the type of the second operand
218+
* @param thirdType the type of the third operand
219+
* @param expression the expression to use for diagnostics
220+
* @param context the validation context to use for diagnostics
221+
* @returns the resulting type of the operator or `undefined` if the type could not be inferred
222+
*/
223+
computeType(
224+
firstType: Valuetype,
225+
secondType: Valuetype,
226+
thirdType: Valuetype,
227+
expression: TernaryExpression,
228+
context: ValidationContext | undefined,
229+
): Valuetype | undefined;
230+
}

0 commit comments

Comments
 (0)