From 3c477b8be29786701508ac851a5fcf84f1af757b Mon Sep 17 00:00:00 2001 From: yblanken Date: Thu, 23 Jul 2026 09:16:17 +0200 Subject: [PATCH 1/9] [#9] Added Null-Coalescing and Ternary-conditional operators --- .../tests/CustomConverterTest.xtend | 119 ++++++++++++-- .../tests/ExpressionEvaluatorBasicTest.xtend | 24 +-- .../ExpressionEvaluatorComplexTest.xtend | 4 +- .../ExpressionEvaluatorFunctionTest.xtend | 2 +- .../tests/ExpressionEvaluatorTestBase.xtend | 1 + .../nl/esi/xtext/expressions/Expression.xtext | 20 ++- .../evaluation/ExpressionEvaluator.xtend | 17 +- .../formatting2/ExpressionFormatter.xtend | 18 ++- .../plantuml/ExpressionsUmlGenerator.xtend | 8 + .../utilities/ExpressionsComparator.xtend | 5 + .../expressions/utilities/ProposalHelper.java | 150 ------------------ .../validation/ExpressionValidator.xtend | 21 ++- 12 files changed, 209 insertions(+), 180 deletions(-) diff --git a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/CustomConverterTest.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/CustomConverterTest.xtend index 1c4dae3..c0cf358 100644 --- a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/CustomConverterTest.xtend +++ b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/CustomConverterTest.xtend @@ -10,6 +10,7 @@ package nl.esi.xtext.expressions.tests import com.google.inject.Inject +import java.net.URI import java.util.Optional import java.util.UUID import nl.esi.xtext.expressions.conversion.IExpressionConverter @@ -22,6 +23,7 @@ import org.eclipse.xtext.resource.XtextResourceSet import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.net.URISyntaxException /** * Tests that custom converters can be registered and used to handle @@ -46,10 +48,12 @@ class CustomConverterTest extends ExpressionEvaluatorTestBase { var handler = registry.getURIHandler() resourceSet.URIConverter?.URIHandlers?.add(0, handler) - // Register the sample library + // Register the sample libraries registry.addLibraryFunctions(SampleLibraryWithUUID) - // Add the converter + registry.addLibraryFunctions(SampleLibraryWithURI) + // Add the converters registry.addConverter(new UUIDConverter) + registry.addConverter(new URIConverter) initialized = true } @@ -73,7 +77,11 @@ class CustomConverterTest extends ExpressionEvaluatorTestBase { Assertions.assertAll( [Assertions.assertTrue(content.contains("function uuid fromString("), "fromString")], [Assertions.assertTrue(content.contains("function string uuidToString("), "uuidToString")], - [Assertions.assertTrue(content.contains("function bool isValidUUID("), "isValidUUID")] + [Assertions.assertTrue(content.contains("function bool isValidUUID("), "isValidUUID")], + [Assertions.assertTrue(content.contains("function string getScheme("), "getScheme")], + [Assertions.assertTrue(content.contains("function string getHost("), "getHost")], + [Assertions.assertTrue(content.contains("function int getPort("), "getPort")], + [Assertions.assertTrue(content.contains("function string getPath("), "getPath")] ) } @@ -92,9 +100,13 @@ class CustomConverterTest extends ExpressionEvaluatorTestBase { @Test def void call_uuidToString_convertsUUIDToString() { assertEval(''' + type uuid based on string + uuid id = "550e8400-e29b-41d4-a716-446655440000" string result = "550e8400-e29b-41d4-a716-446655440000" ''', ''' + type uuid based on string + uuid id = "550e8400-e29b-41d4-a716-446655440000" string result = uuidToString(id) ''') @@ -126,6 +138,28 @@ class CustomConverterTest extends ExpressionEvaluatorTestBase { bool result = isValidUUID("") ''') } + + @Test + def void call_getComponents_getsComponentsOfAnURI() { + assertEval(''' + type uri based on string + + uri github = "https://github.com:443/TNO/XPlus" + string scheme = "https" + string host = "github.com" + int port = 443 + string path = "/TNO/XPlus" + ''', ''' + type uri based on string + + uri github = "https://github.com:443/TNO/XPlus" + string scheme = getScheme(github) + string host = getHost(github) + int port = getPort(github) + string path = getPath(github) + ''') + } + } /** @@ -198,15 +232,82 @@ class UUIDConverter implements IExpressionConverter { override Optional toExpression(Object object, Type type) { // Only convert UUID objects - if (!(object instanceof UUID)){ + if (object instanceof UUID){ + // Check if target type is string-like + var context = IEvaluationContext.EMPTY; + val result = context.toExpression(object.toString) + if (result !== null) { + return Optional.of(result) + } + } + return Optional.empty() + } +} + +/** + * Sample library with URI-related functions. + * This demonstrates how a library might use custom Java types + * that require converters to work with the expression language. + */ +class SampleLibraryWithURI { + def static getScheme(URI uri) { + uri.scheme + } + + def static getHost(URI uri) { + uri.host + } + + def static getPort(URI uri) { + uri.port + } + + def static getPath(URI uri) { + uri.path + } +} + +/** + * Custom converter for UUID type. + * Converts between Expression (string literals) and java.util.UUID objects. + */ +class URIConverter implements IExpressionConverter { + + override Optional toObject(Expression expression, Class targetType) { + // Only convert to URI type + if (!targetType.equals(URI)){ return Optional.empty() } - - // Check if target type is string-like + + // Handle null + if (expression === null) { + return Optional.empty() + } + var context = IEvaluationContext.EMPTY; - val result = context.toExpression(object.toString) - if (result !== null) { - return Optional.of(result) + // Convert string expression to URI + try { + val value = context.asString(expression) + if (value === null || value.empty) { + return Optional.empty() + } + val uri = new URI(value) + return Optional.of(uri) + } catch (URISyntaxException e) { + // Invalid URI format + return Optional.empty() + } + } + + override Optional toExpression(Object object, Type type) { + // Only convert URI objects + if (object instanceof URI) { + // Check if target type is string-like + var context = IEvaluationContext.EMPTY; + val result = context.toExpression(object.toString) + if (result !== null) { + return Optional.of(result) + } } return Optional.empty() } diff --git a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorBasicTest.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorBasicTest.xtend index ba04994..7248afe 100644 --- a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorBasicTest.xtend +++ b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorBasicTest.xtend @@ -111,15 +111,19 @@ class ExpressionEvaluatorBasicTest extends ExpressionEvaluatorTestBase { assertEval(''' bool v_eq_null_1 = true bool v_eq_null_2 = false + bool v_eq_null_3 = false bool v_neq_null_1 = false bool v_neq_null_2 = true + bool v_neq_null_3 = true ''', ''' bool v_eq_null_1 = null == null bool v_eq_null_2 = null == 1 + bool v_eq_null_3 = "" == null bool v_neq_null_1 = null != null bool v_neq_null_2 = null != 1 + bool v_neq_null_3 = "" != null ''') } @@ -218,16 +222,16 @@ class ExpressionEvaluatorBasicTest extends ExpressionEvaluatorTestBase { real v_add_real_1 = 3.3 real v_add_real_2 = 6.6 - int v_sub_real_1 = 1.1 - int v_sub_real_2 = - 2.2 - int v_sub_real_3 = 1.3 + real v_sub_real_1 = 1.1 + real v_sub_real_2 = - 2.2 + real v_sub_real_3 = 1.3 ''', ''' real v_add_real_1 = 1.1 + 2.2 real v_add_real_2 = 1.1 + 2.2 + 3.3 - int v_sub_real_1 = 2.2 - 1.1 - int v_sub_real_2 = 2.2 - 4.4 - int v_sub_real_3 = 10.10 - 5.5 - 3.3 + real v_sub_real_1 = 2.2 - 1.1 + real v_sub_real_2 = 2.2 - 4.4 + real v_sub_real_3 = 10.10 - 5.5 - 3.3 ''') } @@ -235,11 +239,11 @@ class ExpressionEvaluatorBasicTest extends ExpressionEvaluatorTestBase { def void level4String() { // Resolved variable assertEval(''' - real v_add_string_1 = "aabb" - real v_add_string_2 = "aabbcc" + string v_add_string_1 = "aabb" + string v_add_string_2 = "aabbcc" ''', ''' - real v_add_string_1 = "aa" + "bb" - real v_add_string_2 = "aa" + "bb" + "cc" + string v_add_string_1 = "aa" + "bb" + string v_add_string_2 = "aa" + "bb" + "cc" ''') } } diff --git a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend index 1198bc5..3d703f9 100644 --- a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend +++ b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend @@ -305,7 +305,7 @@ class ExpressionEvaluatorComplexTest extends ExpressionEvaluatorTestBase { @Test def void expressionMinus() { assertEval('int a = -1', 'int a = -1') - assertEval('int a = -1.0', 'int a = -1.0') + assertEval('real a = -1.0', 'real a = -1.0') } @Test @@ -337,7 +337,7 @@ class ExpressionEvaluatorComplexTest extends ExpressionEvaluatorTestBase { @Test def void expressionPlus() { assertEval('int a = 1', 'int a = +1') - assertEval('int a = 1.0', 'int a = +1.0') + assertEval('real a = 1.0', 'real a = +1.0') // Resolved variable assertEval(''' diff --git a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorFunctionTest.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorFunctionTest.xtend index 9710f24..d05cfd7 100644 --- a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorFunctionTest.xtend +++ b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorFunctionTest.xtend @@ -86,7 +86,7 @@ class ExpressionEvaluatorFunctionTest extends ExpressionEvaluatorTestBase { tss = [ "Hello", "Test!" ] } bool contains = contains(t.tss, "Hello") - bool notContains = contains(t.tis, "1") + bool notContains = contains(t.tis, 1) ''') } diff --git a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorTestBase.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorTestBase.xtend index 71924ee..3c7b779 100644 --- a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorTestBase.xtend +++ b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorTestBase.xtend @@ -49,6 +49,7 @@ abstract class ExpressionEvaluatorTestBase { protected def String eval(String input) { val expressions = parser.parse(input) Assertions.assertTrue(expressions.eResource.errors.isEmpty, '''Unexpected errors in input: «expressions.eResource.errors.join(", ")»''') + EcoreUtil3.validate(expressions) Assertions.assertEquals(expressions.variables.size, expressions.variables.map[variable.name].toSet.size, 'Variables cannot be declared multiple times') val context = expressions.variables.toMap([variable], [expression]) for (assignment : expressions.variables.reject[expression === null]) { diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/Expression.xtext b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/Expression.xtext index 19aa9c5..f66d0c0 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/Expression.xtext +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/Expression.xtext @@ -87,7 +87,9 @@ ExpressionLevel5 returns Expression: // Left associativity ExpressionLevel6 returns Expression: // Right associativity ExpressionLevel7 - ( ({ExpressionPower.left=current} "^" right=ExpressionLevel6) + ( ({ExpressionPower.left=current} "^" right=ExpressionLevel6) + | ({ExpressionNullCoalescing.left=current} "??" right=ExpressionLevel6) + | ({ExpressionConditional.left=current} "?" middle=ExpressionLevel6 ":" right=ExpressionLevel6) )? ; @@ -112,8 +114,6 @@ ExpressionLevel8 returns Expression: ({ExpressionMapRW.map = current} '[' key = Expression ('->' value = Expression)? ']'))* ; - - ExpressionLevel9 returns Expression: ExpressionBracket | ExpressionConstantBool | @@ -197,7 +197,18 @@ Pair: // --- add extra superclasses in metamodel ------- -ExpressionBinary returns Expression: +ExpressionTernary returns Expression: + {ExpressionTernary} + left=Expression + middle=Expression + right=Expression +; + +ConcreteExpressionTernary returns ExpressionTernary: + {ExpressionConditional} +; + +ExpressionBinary returns Expression: {ExpressionBinary} ; @@ -218,6 +229,7 @@ ConcreteExpressionBinary returns ExpressionBinary: | {ExpressionMinimum} | {ExpressionModulo} | {ExpressionPower} + | {ExpressionNullCoalescing} ; ExpressionUnary returns Expression: diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend index 01352f7..5c5a50b 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend @@ -16,6 +16,7 @@ import nl.esi.xtext.expressions.expression.ExpressionAddition import nl.esi.xtext.expressions.expression.ExpressionAnd import nl.esi.xtext.expressions.expression.ExpressionAny import nl.esi.xtext.expressions.expression.ExpressionBracket +import nl.esi.xtext.expressions.expression.ExpressionConditional import nl.esi.xtext.expressions.expression.ExpressionConstantBool import nl.esi.xtext.expressions.expression.ExpressionConstantInt import nl.esi.xtext.expressions.expression.ExpressionConstantReal @@ -37,6 +38,7 @@ import nl.esi.xtext.expressions.expression.ExpressionModulo import nl.esi.xtext.expressions.expression.ExpressionMultiply import nl.esi.xtext.expressions.expression.ExpressionNEqual import nl.esi.xtext.expressions.expression.ExpressionNot +import nl.esi.xtext.expressions.expression.ExpressionNullCoalescing import nl.esi.xtext.expressions.expression.ExpressionNullLiteral import nl.esi.xtext.expressions.expression.ExpressionOr import nl.esi.xtext.expressions.expression.ExpressionPackage @@ -48,11 +50,11 @@ import nl.esi.xtext.expressions.expression.ExpressionSubtraction import nl.esi.xtext.expressions.expression.ExpressionVariable import nl.esi.xtext.expressions.expression.ExpressionVector import nl.esi.xtext.expressions.functions.ExpressionFunctionsRegistry +import nl.esi.xtext.expressions.functions.ExpressionFunctionsRegistry.NoMatchingFunctionFoundException import org.eclipse.emf.common.util.EList import org.eclipse.emf.ecore.EObject import org.eclipse.emf.ecore.EReference import org.eclipse.emf.ecore.util.EcoreUtil -import nl.esi.xtext.expressions.functions.ExpressionFunctionsRegistry.NoMatchingFunctionFoundException @Singleton class ExpressionEvaluator { @@ -256,6 +258,19 @@ class ExpressionEvaluator { ?: expression.calcIfReal[l, r | l.pow(r.intValueExact)] } + protected dispatch def Expression doEvaluate(ExpressionNullCoalescing expression, extension IEvaluationContext context) { + if (expression.left.isValue) { + return expression.left instanceof ExpressionNullLiteral ? expression.right : expression.left + } + } + + protected dispatch def Expression doEvaluate(ExpressionConditional expression, extension IEvaluationContext context) { + val leftValue = asBool(expression.left); + if (leftValue !== null) { + return leftValue ? expression.middle : expression.right + } + } + // Unary protected dispatch def Expression doEvaluate(ExpressionNot expression, extension IEvaluationContext context) { diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/formatting2/ExpressionFormatter.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/formatting2/ExpressionFormatter.xtend index fb32d69..e420fac 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/formatting2/ExpressionFormatter.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/formatting2/ExpressionFormatter.xtend @@ -45,6 +45,8 @@ import nl.esi.xtext.expressions.expression.VariableDecl import nl.esi.xtext.expressions.services.ExpressionGrammarAccess import nl.esi.xtext.types.formatting2.TypesFormatter import org.eclipse.xtext.formatting2.IFormattableDocument +import nl.esi.xtext.expressions.expression.ExpressionConditional +import nl.esi.xtext.expressions.expression.ExpressionNullCoalescing class ExpressionFormatter extends TypesFormatter { @@ -164,11 +166,25 @@ class ExpressionFormatter extends TypesFormatter { //----------------------------------- ExpressionLevel6 def dispatch void format(ExpressionPower expr, extension IFormattableDocument document) { - expr.regionFor.keyword(expressionLevel6Access.circumflexAccentKeyword_1_1).surround(oneSpace) + expr.regionFor.keyword(expressionLevel6Access.circumflexAccentKeyword_1_0_1).surround(oneSpace) expr.right.format; expr.left.format; } + def dispatch void format(ExpressionNullCoalescing expr, extension IFormattableDocument document) { + expr.regionFor.keyword(expressionLevel6Access.questionMarkQuestionMarkKeyword_1_1_1).surround(oneSpace) + expr.right.format; + expr.left.format; + } + + def dispatch void format(ExpressionConditional expr, extension IFormattableDocument document) { + expr.regionFor.keyword(expressionLevel6Access.questionMarkKeyword_1_2_1).surround(oneSpace) + expr.regionFor.keyword(expressionLevel6Access.colonKeyword_1_2_3).surround(oneSpace) + expr.right.format; + expr.middle.format; + expr.left.format; + } + //----------------------------------- ExpressionLevel7 def dispatch void format(ExpressionNot expr, extension IFormattableDocument document) { diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/generator/plantuml/ExpressionsUmlGenerator.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/generator/plantuml/ExpressionsUmlGenerator.xtend index 980d3c6..181304f 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/generator/plantuml/ExpressionsUmlGenerator.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/generator/plantuml/ExpressionsUmlGenerator.xtend @@ -44,6 +44,8 @@ import nl.esi.xtext.expressions.expression.ExpressionVariable import nl.esi.xtext.expressions.expression.ExpressionVector import org.eclipse.xtext.generator.IFileSystemAccess import nl.esi.xtext.types.generator.XPlusGenerator +import nl.esi.xtext.expressions.expression.ExpressionNullCoalescing +import nl.esi.xtext.expressions.expression.ExpressionConditional class ExpressionsUmlGenerator extends XPlusGenerator{ @@ -105,6 +107,12 @@ class ExpressionsUmlGenerator extends XPlusGenerator{ def dispatch CharSequence generateExpression(ExpressionPower expr) '''«generateExpression(expr.left)» ^ «generateExpression(expr.right)»''' + def dispatch CharSequence generateExpression(ExpressionNullCoalescing expr) + '''«generateExpression(expr.left)» ?? «generateExpression(expr.right)»''' + + def dispatch CharSequence generateExpression(ExpressionConditional expr) + '''«generateExpression(expr.left)» ? «generateExpression(expr.middle)» : «generateExpression(expr.right)»''' + def dispatch CharSequence generateExpression(ExpressionMinus expr) '''-«generateExpression(expr.sub)»''' diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsComparator.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsComparator.xtend index 96d6699..822b767 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsComparator.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsComparator.xtend @@ -27,6 +27,7 @@ import nl.esi.xtext.expressions.expression.ExpressionVariable import nl.esi.xtext.expressions.expression.ExpressionVector import nl.esi.xtext.expressions.expression.Variable import nl.esi.xtext.types.utilities.TypesComparator +import nl.esi.xtext.expressions.expression.ExpressionTernary class ExpressionsComparator extends TypesComparator { @@ -34,6 +35,10 @@ class ExpressionsComparator extends TypesComparator { v1.name == v2.name && v1.type.sameAs(v2.type) } + def dispatch boolean compare(ExpressionTernary exp1, ExpressionTernary exp2){ + exp1.left.sameAs(exp2.left) && exp1.middle.sameAs(exp2.middle) && exp1.right.sameAs(exp2.right) + } + def dispatch boolean compare(ExpressionBinary exp1, ExpressionBinary exp2){ exp1.left.sameAs(exp2.left) && exp1.right.sameAs(exp2.right) } diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ProposalHelper.java b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ProposalHelper.java index 75ba2ee..e5e3105 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ProposalHelper.java +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ProposalHelper.java @@ -12,7 +12,6 @@ import static nl.esi.xtext.types.utilities.TypeUtilities.getAllFields; import java.util.List; -import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -20,40 +19,6 @@ import com.google.common.base.Predicates; -import nl.esi.xtext.expressions.expression.Expression; -import nl.esi.xtext.expressions.expression.ExpressionAddition; -import nl.esi.xtext.expressions.expression.ExpressionAnd; -import nl.esi.xtext.expressions.expression.ExpressionAny; -import nl.esi.xtext.expressions.expression.ExpressionBracket; -import nl.esi.xtext.expressions.expression.ExpressionConstantBool; -import nl.esi.xtext.expressions.expression.ExpressionConstantInt; -import nl.esi.xtext.expressions.expression.ExpressionConstantReal; -import nl.esi.xtext.expressions.expression.ExpressionConstantString; -import nl.esi.xtext.expressions.expression.ExpressionDivision; -import nl.esi.xtext.expressions.expression.ExpressionEnumLiteral; -import nl.esi.xtext.expressions.expression.ExpressionEqual; -import nl.esi.xtext.expressions.expression.ExpressionGeq; -import nl.esi.xtext.expressions.expression.ExpressionGreater; -import nl.esi.xtext.expressions.expression.ExpressionLeq; -import nl.esi.xtext.expressions.expression.ExpressionLess; -import nl.esi.xtext.expressions.expression.ExpressionMap; -import nl.esi.xtext.expressions.expression.ExpressionMapRW; -import nl.esi.xtext.expressions.expression.ExpressionMaximum; -import nl.esi.xtext.expressions.expression.ExpressionMinimum; -import nl.esi.xtext.expressions.expression.ExpressionMinus; -import nl.esi.xtext.expressions.expression.ExpressionModulo; -import nl.esi.xtext.expressions.expression.ExpressionMultiply; -import nl.esi.xtext.expressions.expression.ExpressionNEqual; -import nl.esi.xtext.expressions.expression.ExpressionNot; -import nl.esi.xtext.expressions.expression.ExpressionNullLiteral; -import nl.esi.xtext.expressions.expression.ExpressionOr; -import nl.esi.xtext.expressions.expression.ExpressionPlus; -import nl.esi.xtext.expressions.expression.ExpressionPower; -import nl.esi.xtext.expressions.expression.ExpressionRecord; -import nl.esi.xtext.expressions.expression.ExpressionRecordAccess; -import nl.esi.xtext.expressions.expression.ExpressionSubtraction; -import nl.esi.xtext.expressions.expression.ExpressionVariable; -import nl.esi.xtext.expressions.expression.ExpressionVector; import nl.esi.xtext.expressions.expression.TypeAnnotation; import nl.esi.xtext.types.types.EnumTypeDecl; import nl.esi.xtext.types.types.MapTypeConstructor; @@ -166,119 +131,4 @@ private static String createDefaultValue(TypeDecl type, String targetName, Strin throw new UnsupportedTypeException(type); } - - static String expression(Expression expression, Function variablePrefix) { - if (expression instanceof ExpressionConstantInt) { - return Long.toString(((ExpressionConstantInt) expression).getValue()); - } else if (expression instanceof ExpressionConstantString) { - return String.format("\"%s\"", ((ExpressionConstantString) expression).getValue()); - } else if (expression instanceof ExpressionNot) { - return String.format("not (%s)", expression(((ExpressionNot) expression).getSub(), variablePrefix)); - } else if (expression instanceof ExpressionConstantReal) { - return Double.toString(((ExpressionConstantReal) expression).getValue()); - } else if (expression instanceof ExpressionConstantBool) { - return ((ExpressionConstantBool) expression).isValue() ? "True" : "False"; - } else if (expression instanceof ExpressionAny) { - return "\"*\""; - } else if (expression instanceof ExpressionAddition) { - ExpressionAddition e = (ExpressionAddition) expression; - return String.format("%s + %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionSubtraction) { - ExpressionSubtraction e = (ExpressionSubtraction) expression; - return String.format("%s - %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionMultiply) { - ExpressionMultiply e = (ExpressionMultiply) expression; - return String.format("%s * %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionDivision) { - ExpressionDivision e = (ExpressionDivision) expression; - return String.format("%s / %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionModulo) { - ExpressionModulo e = (ExpressionModulo) expression; - return String.format("%s % %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionMinimum) { - ExpressionMinimum e = (ExpressionMinimum) expression; - return String.format("min(%s, %s)", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionMaximum) { - ExpressionMaximum e = (ExpressionMaximum) expression; - return String.format("max(%s, %s)", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionPower) { - ExpressionPower e = (ExpressionPower) expression; - return String.format("pow(%s, %s)", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionVariable) { - ExpressionVariable v = (ExpressionVariable) expression; - // return String.format("%s%s", variablePrefix.apply(v.getVariable().getName()), v.getVariable().getName()); - return String.format("%s", variablePrefix.apply(v.getVariable().getName())); - } else if (expression instanceof ExpressionGreater) { - ExpressionGreater e = (ExpressionGreater) expression; - return String.format("%s > %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionLess) { - ExpressionLess e = (ExpressionLess) expression; - return String.format("%s < %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionLeq) { - ExpressionLeq e = (ExpressionLeq) expression; - return String.format("%s <= %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionGeq) { - ExpressionGeq e = (ExpressionGeq) expression; - return String.format("%s >= %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionEqual) { - ExpressionEqual e = (ExpressionEqual) expression; - return String.format("%s == %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionNEqual) { - ExpressionNEqual e = (ExpressionNEqual) expression; - return String.format("%s != %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionAnd) { - ExpressionAnd e = (ExpressionAnd) expression; - return String.format("%s and %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionOr) { - ExpressionOr e = (ExpressionOr) expression; - return String.format("%s or %s", expression(e.getLeft(), variablePrefix), expression(e.getRight(), variablePrefix)); - } else if (expression instanceof ExpressionEnumLiteral) { - ExpressionEnumLiteral e = (ExpressionEnumLiteral) expression; - return String.format("\"%s:%s\"", e.getType().getName(), e.getLiteral().getName()); - } else if (expression instanceof ExpressionNullLiteral) { - return "null"; - } else if (expression instanceof ExpressionVector) { - ExpressionVector e = (ExpressionVector) expression; - return String.format("[%s]", e.getElements().stream().map(ee -> expression (ee, variablePrefix)).collect(Collectors.joining(", "))); - } else if (expression instanceof ExpressionMinus) { - ExpressionMinus e = (ExpressionMinus) expression; - return String.format("%s * -1", expression(e.getSub(), variablePrefix)); - } else if (expression instanceof ExpressionPlus) { - ExpressionPlus e = (ExpressionPlus) expression; - return expression(e.getSub(), variablePrefix); - } else if (expression instanceof ExpressionBracket) { - ExpressionBracket e = (ExpressionBracket) expression; - return expression(e.getSub(), variablePrefix); - } else if (expression instanceof ExpressionMap) { - ExpressionMap e = (ExpressionMap) expression; - return String.format("{%s}", e.getPairs().stream().map(p -> { - String key = expression(p.getKey(), variablePrefix); - String value = expression(p.getValue(), variablePrefix); - return String.format("%s: %s", key, value); - }).collect(Collectors.joining(", "))); - } else if (expression instanceof ExpressionMapRW) { - ExpressionMapRW e = (ExpressionMapRW) expression; - String map = expression(e.getMap(), variablePrefix); - String key = expression(e.getKey(), variablePrefix); - if (e.getValue() == null) { - return String.format("%s[%s]", map, key); - } else { - String value = expression(e.getValue(), variablePrefix); - return String.format("{**%s, **{%s: %s}}", map, key, value); - } - } else if (expression instanceof ExpressionRecord) { - ExpressionRecord e = (ExpressionRecord) expression; - return String.format("{%s}", e.getFields().stream().map(p -> { - String key = p.getRecordField().getName(); - String value = expression(p.getExp(), variablePrefix); - return String.format("\"%s\": %s", key, value); - }).collect(Collectors.joining(", "))); - } else if (expression instanceof ExpressionRecordAccess) { - ExpressionRecordAccess e = (ExpressionRecordAccess) expression; - String map = expression(e.getRecord(), variablePrefix); - return String.format("%s[\"%s\"]", map, e.getField().getName()); - } - - throw new RuntimeException("Not supported"); - } } diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend index 196d283..5058fcd 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend @@ -53,6 +53,8 @@ import org.eclipse.xtext.validation.Check import static extension nl.esi.xtext.types.utilities.TypeUtilities.* import static extension nl.esi.xtext.expressions.utilities.ExpressionsUtilities.* +import nl.esi.xtext.expressions.expression.ExpressionNullCoalescing +import nl.esi.xtext.expressions.expression.ExpressionConditional /* * This class mainly captures the XPlus type system for expressions. Constraints are not formulated @@ -133,13 +135,14 @@ class ExpressionValidator extends AbstractExpressionValidator { } } ExpressionAddition | - ExpressionSubtraction | + ExpressionSubtraction | ExpressionMultiply | ExpressionDivision | ExpressionModulo | ExpressionPower | ExpressionMinimum | - ExpressionMaximum : { + ExpressionMaximum | + ExpressionNullCoalescing: { val leftType = e.left.typeOf val rightType = e.right.typeOf if(leftType === null || rightType === null) {return} @@ -163,6 +166,20 @@ class ExpressionValidator extends AbstractExpressionValidator { error("Type mismatch: expected type int or real", ExpressionPackage.Literals.EXPRESSION_BINARY__LEFT) } + } + ExpressionConditional: { + val leftType = e.left.typeOf + val middleType = e.left.typeOf + val rightType = e.right.typeOf + + if(leftType === null || middleType === null || rightType === null) {return} + if(!leftType.identical(BasicTypes.getBoolType(e))) { + error("Type mismatch: expected type bool", ExpressionPackage.Literals.EXPRESSION_TERNARY__LEFT) + } + if(!middleType.synonym(rightType)){ + error("Arguments must be of compatible types", e.eContainer, e.eContainingFeature) + return + } } ExpressionMinus | ExpressionPlus : { From 40a8438e21a032b486912caf816f1af466bfc1a3 Mon Sep 17 00:00:00 2001 From: yblanken Date: Tue, 28 Jul 2026 17:06:44 +0200 Subject: [PATCH 2/9] [#9] Added unit-test --- .../ExpressionEvaluatorComplexTest.xtend | 126 ++++++++++++++++++ .../tests/ExpressionValidationTest.xtend | 19 +++ .../utilities/ExpressionsUtilities.xtend | 11 +- .../validation/ExpressionValidator.xtend | 15 ++- .../xtext/types/utilities/TypeUtilities.xtend | 54 +++++++- 5 files changed, 215 insertions(+), 10 deletions(-) diff --git a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend index 3d703f9..932d643 100644 --- a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend +++ b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend @@ -357,4 +357,130 @@ class ExpressionEvaluatorComplexTest extends ExpressionEvaluatorTestBase { int b = +a ''') } + + @Test + def void expressionNullCoalescing() { + assertEval('int a = 1', 'int a = 1 ?? 2') + assertEval('real a = 2.0', 'real a = null ?? 2.0') + + // Resolved variable + assertEval(''' + int a = 1 + int b = null + int c = 1 + + int x = null + int y = 5 + int z = 5 + ''', ''' + int a = 1 + int b = null + int c = a ?? b + + int x = null + int y = 5 + int z = x ?? y + ''') + + // Unresolved variable + assertEval(''' + int a + int b = 1 + int c = a ?? 1 + + int x = 4 + int y + int z = 4 + ''', ''' + int a + int b = 1 + int c = a ?? b + + int x = 4 + int y + int z = x ?? y + ''') + } + + @Test + def void expressionConditional() { + assertEval('int a = 1', 'int a = true ? 1 : 2') + assertEval('real a = 2.0', 'real a = false ? null : 2.0') + + // Resolved variable + assertEval(''' + bool a = true + int b = 1 + int c = 2 + int d = 1 + + bool f = true + int g = null + int h = 2 + int i = null + + bool j = false + int k = 1 + int l = null + int m = null + + bool w = null + int x = 1 + int y = 2 + int z = null ? 1 : 2 + ''', ''' + bool a = true + int b = 1 + int c = 2 + int d = a ? b : c + + bool f = true + int g = null + int h = 2 + int i = f ? g : h + + bool j = false + int k = 1 + int l = null + int m = j ? k : l + + bool w = null + int x = 1 + int y = 2 + int z = w ? x : y + ''') + + // Unresolved variable + assertEval(''' + bool a + int b = 1 + int c = 2 + int d = a ? 1 : 2 + + bool f = true + int g + int h = 2 + int i = g + + bool j = false + int k = 1 + int l + int m = l + ''', ''' + bool a + int b = 1 + int c = 2 + int d = a ? b : c + + bool f = true + int g + int h = 2 + int i = f ? g : h + + bool j = false + int k = 1 + int l + int m = j ? k : l + ''') + } } diff --git a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionValidationTest.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionValidationTest.xtend index 86397a1..0e4be28 100644 --- a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionValidationTest.xtend +++ b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionValidationTest.xtend @@ -210,6 +210,25 @@ class ExpressionValidationTest { ''') } + @Test + def void expressionNullCoalescing() { + validate(''' + int i = 1 ?? 2 + real r = null ?? 2.0 + string a = "a" ?? null + ''') + } + + @Test + def void expressionConditional() { + validate(''' + int i = true ? 1 : 2 + real r = false ? 1.0 : 2.0 + bool b = true ? true : false + string a = b ? "a" : null + ''') + } + private def validate(String text) { val result = parseHelper.parse(text) diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsUtilities.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsUtilities.xtend index 84bf985..4a757b7 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsUtilities.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsUtilities.xtend @@ -71,6 +71,8 @@ import static nl.esi.xtext.common.lang.utilities.EcoreUtil3.* import static extension nl.esi.xtext.types.utilities.TypeUtilities.* import static extension org.eclipse.emf.ecore.util.EcoreUtil.* import java.util.ArrayList +import nl.esi.xtext.expressions.expression.ExpressionNullCoalescing +import nl.esi.xtext.expressions.expression.ExpressionConditional class ExpressionsUtilities { static extension val ExpressionFactory EXPRESSION_FACTORY = ExpressionFactory.eINSTANCE @@ -180,7 +182,12 @@ class ExpressionsUtilities { else null } - + ExpressionNullCoalescing: { + e.left.typeOf.getCommonType(e.right.typeOf) + } + ExpressionConditional: { + e.middle.typeOf.getCommonType(e.right.typeOf) + } } } @@ -274,7 +281,7 @@ class ExpressionsUtilities { return result } - def static TypeObject inferTypeBinaryArithmetic(ExpressionBinary e){ + private def static TypeObject inferTypeBinaryArithmetic(ExpressionBinary e){ val leftType = e.left.typeOf val rightType = e.right.typeOf switch(e){ diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend index 5058fcd..10e045d 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend @@ -141,8 +141,7 @@ class ExpressionValidator extends AbstractExpressionValidator { ExpressionModulo | ExpressionPower | ExpressionMinimum | - ExpressionMaximum | - ExpressionNullCoalescing: { + ExpressionMaximum: { val leftType = e.left.typeOf val rightType = e.right.typeOf if(leftType === null || rightType === null) {return} @@ -167,16 +166,24 @@ class ExpressionValidator extends AbstractExpressionValidator { } } + ExpressionNullCoalescing: { + val leftType = e.left.typeOf + val rightType = e.right.typeOf + if(leftType === null || rightType === null) {return} + if(e.typeOf === null) { + error("Arguments must be of compatible types", e.eContainer, e.eContainingFeature) + return + } + } ExpressionConditional: { val leftType = e.left.typeOf val middleType = e.left.typeOf val rightType = e.right.typeOf - if(leftType === null || middleType === null || rightType === null) {return} if(!leftType.identical(BasicTypes.getBoolType(e))) { error("Type mismatch: expected type bool", ExpressionPackage.Literals.EXPRESSION_TERNARY__LEFT) } - if(!middleType.synonym(rightType)){ + if(e.typeOf === null) { error("Arguments must be of compatible types", e.eContainer, e.eContainingFeature) return } diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/utilities/TypeUtilities.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/utilities/TypeUtilities.xtend index acf7ba7..a38086a 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/utilities/TypeUtilities.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/utilities/TypeUtilities.xtend @@ -265,7 +265,49 @@ class TypeUtilities { return null } - def static boolean identical(TypeObject t1, TypeObject t2) { + def static TypeObject getCommonType(TypeObject t1, TypeObject t2) { + if (t1 === null || t2 === null) return null + + if (t1.subTypeOf(t2)) return t2 + + if (t2.subTypeOf(t1)) return t1 + + if (t1 instanceof SimpleTypeDecl) { + if (t2 instanceof SimpleTypeDecl) { + return t1.base.getCommonType(t2) + } + } + + if (t1 instanceof RecordTypeDecl) { + if (t2 instanceof RecordTypeDecl) { + return t1.parent.getCommonType(t2) + } + } + + if (t1 instanceof VectorTypeConstructor) { + if (t2 instanceof VectorTypeConstructor) { + if(t1.dimensions.size == t2.dimensions.size) { + val elementType = t1.elementType.getCommonType(t2.elementType) + if (elementType !== null) { + return vectorOf(elementType) + } + } + } + } + + // FIXME: Add support for maps +// if (t1 instanceof MapTypeConstructor) { +// if (t2 instanceof MapTypeConstructor) { +// val keyType = t1.keyType.getCommonType(t2.keyType) +// val valueType = t1.valueType.getCommonType(t2.valueType) +// if (keyType !== null && valueType !== null) { +// return mapOf(keyType, valueType) +// } +// } +// } + } + + def static boolean identical(TypeObject t1, TypeObject t2) { if(t1 === null || t2 === null) return false if (t1 instanceof SimpleTypeDecl) @@ -350,9 +392,13 @@ class TypeUtilities { } def static dispatch VectorTypeConstructor vectorOf(VectorTypeDecl vtd) { - val vtc = EcoreUtil.copy(vtd.constructor) - vtc.dimensions += TypesFactory.eINSTANCE.createDimension - return vtc + return vectorOf(vtd.constructor) + } + + def static dispatch VectorTypeConstructor vectorOf(VectorTypeConstructor vtc) { + return EcoreUtil.copy(vtc) => [ + dimensions += TypesFactory.eINSTANCE.createDimension + ] } def static MapTypeConstructor mapOf(TypeDecl keyType, TypeDecl valueType) { From 99825ce95dae2d5b315d89fdc48eb7c392eb45e3 Mon Sep 17 00:00:00 2001 From: yblanken Date: Wed, 29 Jul 2026 08:30:15 +0200 Subject: [PATCH 3/9] [#9] RHS of null-coalescing and ternary operators should not be evaluated always --- .../tests/ExpressionEvaluatorComplexTest.xtend | 6 +++--- .../evaluation/ExpressionEvaluator.xtend | 13 +++++++++---- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend index 932d643..5f12dc2 100644 --- a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend +++ b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend @@ -386,7 +386,7 @@ class ExpressionEvaluatorComplexTest extends ExpressionEvaluatorTestBase { assertEval(''' int a int b = 1 - int c = a ?? 1 + int c = a ?? b int x = 4 int y @@ -427,7 +427,7 @@ class ExpressionEvaluatorComplexTest extends ExpressionEvaluatorTestBase { bool w = null int x = 1 int y = 2 - int z = null ? 1 : 2 + int z = null ? x : y ''', ''' bool a = true int b = 1 @@ -455,7 +455,7 @@ class ExpressionEvaluatorComplexTest extends ExpressionEvaluatorTestBase { bool a int b = 1 int c = 2 - int d = a ? 1 : 2 + int d = a ? b : c bool f = true int g diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend index 5c5a50b..e48549a 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend @@ -81,8 +81,11 @@ class ExpressionEvaluator { } protected def boolean shouldOptimize(EReference eReference, EObject eObject) { - return switch (eReference) { - case ExpressionPackage.Literals.EXPRESSION_RECORD_ACCESS__RECORD: false + return switch (eObject) { + ExpressionNullCoalescing case eReference == ExpressionPackage.Literals.EXPRESSION_BINARY__RIGHT, + ExpressionConditional case eReference == ExpressionPackage.Literals.EXPRESSION_TERNARY__MIDDLE, + ExpressionConditional case eReference == ExpressionPackage.Literals.EXPRESSION_TERNARY__RIGHT, + case eReference == ExpressionPackage.Literals.EXPRESSION_RECORD_ACCESS__RECORD: false default: true } } @@ -260,14 +263,16 @@ class ExpressionEvaluator { protected dispatch def Expression doEvaluate(ExpressionNullCoalescing expression, extension IEvaluationContext context) { if (expression.left.isValue) { - return expression.left instanceof ExpressionNullLiteral ? expression.right : expression.left + // Note that the RHS will only be evaluated if the LHS evaluates to null + return expression.left instanceof ExpressionNullLiteral ? expression.right.evaluate(context) : expression.left } } protected dispatch def Expression doEvaluate(ExpressionConditional expression, extension IEvaluationContext context) { val leftValue = asBool(expression.left); if (leftValue !== null) { - return leftValue ? expression.middle : expression.right + // Note that the middle and right expressions will only be evaluated when the left expression evaluates to a boolean value + return leftValue ? expression.middle.evaluate(context) : expression.right.evaluate(context) } } From 747fc9d32046cc82ab650986fcd2ce404b2365ce Mon Sep 17 00:00:00 2001 From: yblanken Date: Wed, 29 Jul 2026 10:37:57 +0200 Subject: [PATCH 4/9] [#9] Added null safe record access chaining --- .../ExpressionEvaluatorComplexTest.xtend | 59 +++++++++++++++++++ .../nl/esi/xtext/expressions/Expression.xtext | 2 +- .../evaluation/ExpressionEvaluator.xtend | 2 + .../formatting2/ExpressionFormatter.xtend | 2 +- 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend index 5f12dc2..07fa614 100644 --- a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend +++ b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend @@ -113,6 +113,65 @@ class ExpressionEvaluatorComplexTest extends ExpressionEvaluatorTestBase { ''') } + @Test + def void recordAccess() { + val types = ''' + record S { + T t + } + + record T { + string ts + } + ''' + + assertEval(''' + «types» + + S a = null + S b = S { + t = null + } + S c = S { + t = T { + ts = "Hello World!" + } + } + + string u = a.t.ts + string v = b.t.ts + string w = "Hello World!" + + string x = null + string y = null + string z = "Hello World!" + + string nullCoalescing = "My default" + ''', ''' + «types» + + S a = null + S b = S { + t = null + } + S c = S { + t = T { + ts = "Hello World!" + } + } + + string u = a.t.ts + string v = b.t.ts + string w = c.t.ts + + string x = a?.t?.ts + string y = b?.t?.ts + string z = c?.t?.ts + + string nullCoalescing = b?.t?.ts ?? "My default" + ''') + } + @Test def void complexExpression() { val types = ''' diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/Expression.xtext b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/Expression.xtext index f66d0c0..fbda83a 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/Expression.xtext +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/Expression.xtext @@ -110,7 +110,7 @@ ExpressionPlus: ; ExpressionLevel8 returns Expression: - ExpressionLevel9 (({ExpressionRecordAccess.record = current} '.' field = [types::RecordField | ID]) | + ExpressionLevel9 (({ExpressionRecordAccess.record = current} (nullSafe?='?.' | '.') field = [types::RecordField | ID]) | ({ExpressionMapRW.map = current} '[' key = Expression ('->' value = Expression)? ']'))* ; diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend index e48549a..44d10a6 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/evaluation/ExpressionEvaluator.xtend @@ -145,6 +145,8 @@ class ExpressionEvaluator { if (recordExpression instanceof ExpressionRecord) { // TODO: Should we throw an Exception when the field is not associated with a value? return recordExpression.fields.findFirst[recordField == expression.field]?.exp + } else if (expression.nullSafe && recordExpression instanceof ExpressionNullLiteral) { + return recordExpression } } diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/formatting2/ExpressionFormatter.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/formatting2/ExpressionFormatter.xtend index e420fac..855c89f 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/formatting2/ExpressionFormatter.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/formatting2/ExpressionFormatter.xtend @@ -206,7 +206,7 @@ class ExpressionFormatter extends TypesFormatter { //----------------------------------- ExpressionLevel8 def dispatch void format(ExpressionRecordAccess expr, extension IFormattableDocument document) { - expr.regionFor.keyword(expressionLevel8Access.fullStopKeyword_1_0_1).surround(noSpace) + expr.regionFor.keyword(expressionLevel8Access.fullStopKeyword_1_0_1_1).surround(noSpace) } //----------------------------------- ExpressionLevel9 From 546dcf9b5af8a2f8d714e39d09e5de0f863a5c0d Mon Sep 17 00:00:00 2001 From: Yuri Blankenstein Date: Wed, 29 Jul 2026 13:57:17 +0200 Subject: [PATCH 5/9] [#9] Improved variable declaration validation --- .../expressions/utilities/ExpressionsUtilities.xtend | 10 +++------- .../expressions/validation/ExpressionValidator.xtend | 8 ++++---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsUtilities.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsUtilities.xtend index 4b5f560..fd74a1a 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsUtilities.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsUtilities.xtend @@ -9,6 +9,7 @@ */ package nl.esi.xtext.expressions.utilities +import java.util.ArrayList import java.util.Collection import java.util.List import java.util.Map @@ -19,6 +20,7 @@ import nl.esi.xtext.expressions.expression.ExpressionAnd import nl.esi.xtext.expressions.expression.ExpressionAny import nl.esi.xtext.expressions.expression.ExpressionBinary import nl.esi.xtext.expressions.expression.ExpressionBracket +import nl.esi.xtext.expressions.expression.ExpressionConditional import nl.esi.xtext.expressions.expression.ExpressionConstantBool import nl.esi.xtext.expressions.expression.ExpressionConstantInt import nl.esi.xtext.expressions.expression.ExpressionConstantReal @@ -41,6 +43,7 @@ import nl.esi.xtext.expressions.expression.ExpressionModulo import nl.esi.xtext.expressions.expression.ExpressionMultiply import nl.esi.xtext.expressions.expression.ExpressionNEqual import nl.esi.xtext.expressions.expression.ExpressionNot +import nl.esi.xtext.expressions.expression.ExpressionNullCoalescing import nl.esi.xtext.expressions.expression.ExpressionNullLiteral import nl.esi.xtext.expressions.expression.ExpressionOr import nl.esi.xtext.expressions.expression.ExpressionPlus @@ -70,9 +73,6 @@ import static nl.esi.xtext.common.lang.utilities.EcoreUtil3.* import static extension nl.esi.xtext.types.utilities.TypeUtilities.* import static extension org.eclipse.emf.ecore.util.EcoreUtil.* -import java.util.ArrayList -import nl.esi.xtext.expressions.expression.ExpressionNullCoalescing -import nl.esi.xtext.expressions.expression.ExpressionConditional class ExpressionsUtilities { static extension val ExpressionFactory EXPRESSION_FACTORY = ExpressionFactory.eINSTANCE @@ -249,10 +249,6 @@ class ExpressionsUtilities { return result.map[asType].toList } - def static boolean isAssignableFrom(TypeObject lhs, Expression rhs) { - TypeUtilities.subTypeOf(lhs, rhs.typeOf) || rhs instanceof ExpressionNullLiteral - } - def static List getFunctionArgs(ExpressionFunctionCall efc){ // don't validate here, that is part of the validator if (efc.function === null) { diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend index 10e045d..49a8df3 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend @@ -80,10 +80,10 @@ class ExpressionValidator extends AbstractExpressionValidator { //Type checking @Check def checkVariableDecl(VariableDecl vd){ - val lhs = vd.variable.type.typeObject - var rhs = vd.expression - if (rhs !== null && !lhs.isAssignableFrom(rhs)) { - error('''Type mismatch: declared type '«lhs.typeName»' does not match the expected type '«rhs.typeOf.typeName»' ''', ExpressionPackage.Literals.VARIABLE_DECL__VARIABLE) + val lhs = vd?.variable?.type?.typeObject + val rhs = vd?.expression?.typeOf + if(lhs !== null && rhs !== null && !rhs.subTypeOf(lhs)){ + error('''Type mismatch: declared type '«lhs.typeName»' does not match the expected type '«rhs.typeName»' ''', ExpressionPackage.Literals.VARIABLE_DECL__VARIABLE) } } From 28231968e8963bce839baaea29e9c72bbc33ca14 Mon Sep 17 00:00:00 2001 From: yblanken Date: Wed, 29 Jul 2026 12:06:31 +0200 Subject: [PATCH 6/9] [#9] Support getCommonType for Map types --- .../ExpressionEvaluatorComplexTest.xtend | 8 ++--- .../xtext/types/utilities/TypeUtilities.xtend | 30 ++++++++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend index 07fa614..cd5069f 100644 --- a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend +++ b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorComplexTest.xtend @@ -422,7 +422,7 @@ class ExpressionEvaluatorComplexTest extends ExpressionEvaluatorTestBase { assertEval('int a = 1', 'int a = 1 ?? 2') assertEval('real a = 2.0', 'real a = null ?? 2.0') - // Resolved variable + // A resolved variable ( example: int a ==1) used in (c = a ?? b) must be reduced to c =1 assertEval(''' int a = 1 int b = null @@ -441,7 +441,7 @@ class ExpressionEvaluatorComplexTest extends ExpressionEvaluatorTestBase { int z = x ?? y ''') - // Unresolved variable + // An unresolved / undefined variable (example int a) should not be reduced assertEval(''' int a int b = 1 @@ -466,7 +466,7 @@ class ExpressionEvaluatorComplexTest extends ExpressionEvaluatorTestBase { assertEval('int a = 1', 'int a = true ? 1 : 2') assertEval('real a = 2.0', 'real a = false ? null : 2.0') - // Resolved variable + // A Ternary expression that can be evaluated ( example bool a = true) must be reduced to corresponding true or `false`` expression assertEval(''' bool a = true int b = 1 @@ -509,7 +509,7 @@ class ExpressionEvaluatorComplexTest extends ExpressionEvaluatorTestBase { int z = w ? x : y ''') - // Unresolved variable + // Unresolved variable should not be evaluated/reduced assertEval(''' bool a int b = 1 diff --git a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/utilities/TypeUtilities.xtend b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/utilities/TypeUtilities.xtend index 31cdef4..dae4b55 100644 --- a/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/utilities/TypeUtilities.xtend +++ b/bundles/nl.esi.xtext.types/src/nl/esi/xtext/types/utilities/TypeUtilities.xtend @@ -56,6 +56,15 @@ class TypeUtilities { } } + def static TypeDecl asTypeDecl(TypeObject t) { + return switch (t) { + TypeDecl: t + MapTypeConstructor: TypesFactory.eINSTANCE.createMapTypeDecl => [constructor = t] + VectorTypeConstructor: TypesFactory.eINSTANCE.createVectorTypeDecl => [constructor = t] + default: throw new IllegalArgumentException('''Unsupported type: «t?.class?.name»''') + } + } + /* * Some useful predicates */ @@ -285,12 +294,14 @@ class TypeUtilities { if (t1 instanceof SimpleTypeDecl) { if (t2 instanceof SimpleTypeDecl) { + // Climbing the base type for t1 will find the common ancestor (if any) return t1.base.getCommonType(t2) } } if (t1 instanceof RecordTypeDecl) { if (t2 instanceof RecordTypeDecl) { + // Climbing the parent type for t1 will find the common ancestor (if any) return t1.parent.getCommonType(t2) } } @@ -306,16 +317,15 @@ class TypeUtilities { } } - // FIXME: Add support for maps -// if (t1 instanceof MapTypeConstructor) { -// if (t2 instanceof MapTypeConstructor) { -// val keyType = t1.keyType.getCommonType(t2.keyType) -// val valueType = t1.valueType.getCommonType(t2.valueType) -// if (keyType !== null && valueType !== null) { -// return mapOf(keyType, valueType) -// } -// } -// } + if (t1 instanceof MapTypeConstructor) { + if (t2 instanceof MapTypeConstructor) { + val keyType = t1.keyType.getCommonType(t2.keyType) + val valueType = t1.valueType.typeObject.getCommonType(t2.valueType.typeObject) + if (keyType !== null && valueType !== null) { + return mapOf(keyType.asTypeDecl, valueType.asTypeDecl) + } + } + } } def static boolean identical(TypeObject t1, TypeObject t2) { From 11913a0ca62a5ab1ea81e627f93cba622e40b1c9 Mon Sep 17 00:00:00 2001 From: yblanken Date: Wed, 29 Jul 2026 14:14:08 +0200 Subject: [PATCH 7/9] [#9] Add validation tests for getCommonType --- .../tests/ExpressionValidationTest.xtend | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionValidationTest.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionValidationTest.xtend index 0055a1d..53258fe 100644 --- a/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionValidationTest.xtend +++ b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionValidationTest.xtend @@ -13,6 +13,7 @@ package nl.esi.xtext.expressions.tests import com.google.inject.Inject +import nl.esi.xtext.common.lang.utilities.EcoreUtil3 import nl.esi.xtext.expressions.expression.ExpressionModel import org.eclipse.xtext.testing.InjectWith import org.eclipse.xtext.testing.extensions.InjectionExtension @@ -20,7 +21,6 @@ import org.eclipse.xtext.testing.util.ParseHelper import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.^extension.ExtendWith -import nl.esi.xtext.common.lang.utilities.EcoreUtil3 @ExtendWith(InjectionExtension) @InjectWith(ExpressionInjectorProvider) @@ -257,6 +257,69 @@ class ExpressionValidationTest { ''') } + @Test + def void expressionGetCommonType() { + val typedVars = ''' + record A { + string a + } + record B extends A { + string b + } + record C extends A { + string c + } + + A a = A { + a = "a" + } + B b = B { + a = "b", + b = "b" + } + C c = C { + a = "c", + c = "c" + } + ''' + + // Single values + validate(''' + «typedVars» + + B bb = b ?? b + C cc = c ?? c + + // Downcast + A aa = b ?? b + A aa = c ?? b + ''') + + // Lists + validate(''' + «typedVars» + + B[] bb = [b] ?? [b] + C[] cc = [c] ?? [c] + + // Downcast + A[] aa = [b] ?? [b] + A[] aa = [c] ?? [b] + ''') + + // Maps + validate(''' + «typedVars» + + map bb = >{ 1 -> b } ?? >{ 1 -> b } + map cc = >{ 1 -> c } ?? >{ 1 -> c } + + // Downcast + map aa = >{ 1 -> b } ?? >{ 1 -> b } + map aa = >{ 1 -> c } ?? >{ 1 -> b } + ''') + } + private def validate(String text) { val result = parseHelper.parse(text) From 7be6c77e9ac1d2b1fac60ca57220c62c42406c9e Mon Sep 17 00:00:00 2001 From: yblanken Date: Wed, 29 Jul 2026 14:55:29 +0200 Subject: [PATCH 8/9] [#13] Add deprecation warning for 'at' function --- .../xtext/expressions/validation/ExpressionValidator.xtend | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend index 49a8df3..72e9b55 100644 --- a/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend +++ b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/validation/ExpressionValidator.xtend @@ -379,4 +379,10 @@ class ExpressionValidator extends AbstractExpressionValidator { error('Several record types with this name exist. Use an explicit interface name.', ExpressionPackage.Literals.EXPRESSION_RECORD__TYPE) } + @Check + def checkDeprecatedAtFunctionCall(ExpressionFunctionCall call) { + if (call?.function?.name == "at") { + warning("The 'at' function is deprecated and will be removed in the next version, please use the 'set' function instead.", ExpressionPackage.Literals.EXPRESSION_FUNCTION_CALL__FUNCTION) + } + } } \ No newline at end of file From 3382d69d5ed3f10d2d8a1b084122ab7b1d55c7ea Mon Sep 17 00:00:00 2001 From: yblanken Date: Wed, 29 Jul 2026 15:31:46 +0200 Subject: [PATCH 9/9] [#9] Added documentation --- .../adoc/nl.esi.xtext.expressions.adoc | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/bundles/nl.esi.xtext.expressions/adoc/nl.esi.xtext.expressions.adoc b/bundles/nl.esi.xtext.expressions/adoc/nl.esi.xtext.expressions.adoc index 085e75f..1bf1969 100644 --- a/bundles/nl.esi.xtext.expressions/adoc/nl.esi.xtext.expressions.adoc +++ b/bundles/nl.esi.xtext.expressions/adoc/nl.esi.xtext.expressions.adoc @@ -52,7 +52,7 @@ bool flag = true * Logical: `and`, `or`, `not` (case-insensitive: `AND`/`and`, `OR`/`or`, `NOT`/`not`) * Comparison: `==`, `!=`, `<`, `<=`, `>`, `>=` * Arithmetic: `+`, `-`, `*`, `/`, `mod`, `^`, `max`, `min` -* Postfix: record field access `record.field`, map access `map[key]`, map write `map[key -> value]` +* Postfix: record field access `record.field`, null-safe record field access `record?.field`, map access `map[key]`, map write `map[key -> value]` === Function Declarations @@ -198,14 +198,30 @@ map v2 = m[2 -> "c"] The `null` literal represents the absence of a value. It can be used with any type. +The nullish coalescing `??` operator is a logical operator that returns its right-hand side operand when its left-hand side operand is `null`, and otherwise returns its left-hand side operand. + +---- +int a = null +int b = a ?? 10 // b now holds the value 10 +---- + +=== Ternary Expression + +The conditional (ternary) operator is the only operator that takes three operands: a condition followed by a question mark `?`, then an expression to execute if the condition is truthy followed by a colon `:`, and finally the expression to execute if the condition is falsy. +This operator is frequently used as an alternative to an `if...else` statement. + +`string x = flag ? "Hello" : "Goodbye"` + === Operator Precedence Operators are evaluated in the following order (highest to lowest): . Parentheses and literals -. Postfix: record field access `record.field`, map access `map[key]`, map write `map[key -> value]` +. Postfix: (null-safe) record field access `record.field` or `record?.field`, map access `map[key]`, map write `map[key -> value]` . Unary: `not`, unary `+`, unary `-` . Power: `^` (right-associative) +. Null-coalescing: `??` (right-associative) +. Conditional (ternary): `? :` (right-associative) . Multiplicative: `*`, `/`, `mod`, `max`, `min` (left-associative) . Additive: `+`, `-` (left-associative) . Comparison: `<`, `<=`, `>`, `>=` (left-associative) @@ -220,7 +236,7 @@ Operators are evaluated in the following order (highest to lowest): * Map write access uses the syntax `map[key -> value]`. * Type annotation is required for vector and map construction. * Logical operators `and`, `or`, and `not` are case-insensitive and accept both lowercase and uppercase forms (e.g., `AND`, `and`, `OR`, `or`, `NOT`, `not`). -* Operator associativity: most operators are left-associative; the power operator `^` is right-associative. +* Operator associativity: most operators are left-associative; the power `^`, null-coalescing `??` and Conditional (ternary) `? :` operators are right-associative. * Variables can be declared with optional initialization: `Type name = expression` or just `Type name`. NOTE: When invoking functions with custom Java types, `IExpressionConverter` instances are evaluated in order until one successfully converts the argument. The first successful conversion is used. This allows custom converters to handle specific types while falling back to default converters for built-in types. See <> for details on implementing custom converters. \ No newline at end of file