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..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 = ''' @@ -305,7 +364,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 +396,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(''' @@ -357,4 +416,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 ?? b + + 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 ? x : y + ''', ''' + 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 ? b : c + + 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/ExpressionEvaluatorFunctionTest.xtend b/bundles/nl.esi.xtext.expressions.tests/src/nl/esi/xtext/expressions/tests/ExpressionEvaluatorFunctionTest.xtend index fffba8d..6d847a6 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.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 3092ed5..0055a1d 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 @@ -238,6 +238,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/Expression.xtext b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/Expression.xtext index 19aa9c5..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 @@ -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) )? ; @@ -108,12 +110,10 @@ 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)? ']'))* ; - - 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..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 @@ -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 { @@ -79,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 } } @@ -140,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 } } @@ -256,6 +263,21 @@ class ExpressionEvaluator { ?: expression.calcIfReal[l, r | l.pow(r.intValueExact)] } + protected dispatch def Expression doEvaluate(ExpressionNullCoalescing expression, extension IEvaluationContext context) { + if (expression.left.isValue) { + // 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) { + // 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) + } + } + // 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..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 @@ -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) { @@ -190,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 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/ExpressionsUtilities.xtend b/bundles/nl.esi.xtext.expressions/src/nl/esi/xtext/expressions/utilities/ExpressionsUtilities.xtend index c02abee..4b5f560 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/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..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 @@ -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,13 @@ class ExpressionValidator extends AbstractExpressionValidator { } } ExpressionAddition | - ExpressionSubtraction | + ExpressionSubtraction | ExpressionMultiply | ExpressionDivision | ExpressionModulo | ExpressionPower | ExpressionMinimum | - ExpressionMaximum : { + ExpressionMaximum: { val leftType = e.left.typeOf val rightType = e.right.typeOf if(leftType === null || rightType === null) {return} @@ -164,6 +166,28 @@ 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(e.typeOf === null) { + error("Arguments must be of compatible types", e.eContainer, e.eContainingFeature) + return + } + } ExpressionMinus | ExpressionPlus : { val t = e.sub.typeOf 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 42edfce..31cdef4 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 @@ -276,7 +276,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) @@ -361,9 +403,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) {