diff --git a/bundles/nl.esi.xplus.doc.design/adoc/_section_xtext_languages.adoc b/bundles/nl.esi.xplus.doc.design/adoc/_section_xtext_languages.adoc index 9f1f5d8..7a59a59 100644 --- a/bundles/nl.esi.xplus.doc.design/adoc/_section_xtext_languages.adoc +++ b/bundles/nl.esi.xplus.doc.design/adoc/_section_xtext_languages.adoc @@ -44,3 +44,5 @@ include::{extdocdir}/nl.esi.xtext.types.adoc[leveloffset=2] include::{extdocdir}/nl.esi.xtext.expressions.adoc[leveloffset=2] include::{extdocdir}/expression-functions-framework-design.adoc[leveloffset=2] + +include::{extdocdir}/nl.esi.xtext.actions.adoc[leveloffset=2] diff --git a/bundles/nl.esi.xtext.actions.tests/.classpath b/bundles/nl.esi.xtext.actions.tests/.classpath new file mode 100644 index 0000000..e0cf7ad --- /dev/null +++ b/bundles/nl.esi.xtext.actions.tests/.classpath @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/nl.esi.xtext.actions.tests/.project b/bundles/nl.esi.xtext.actions.tests/.project new file mode 100644 index 0000000..13ccce8 --- /dev/null +++ b/bundles/nl.esi.xtext.actions.tests/.project @@ -0,0 +1,34 @@ + + + nl.esi.xtext.actions.tests + + + + + + org.eclipse.xtext.ui.shared.xtextBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.xtext.ui.shared.xtextNature + org.eclipse.jdt.core.javanature + org.eclipse.pde.PluginNature + + diff --git a/bundles/nl.esi.xtext.actions.tests/.settings/org.eclipse.core.resources.prefs b/bundles/nl.esi.xtext.actions.tests/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..99f26c0 --- /dev/null +++ b/bundles/nl.esi.xtext.actions.tests/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/bundles/nl.esi.xtext.actions.tests/.settings/org.eclipse.jdt.core.prefs b/bundles/nl.esi.xtext.actions.tests/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..cc4eaa5 --- /dev/null +++ b/bundles/nl.esi.xtext.actions.tests/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,7 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 +org.eclipse.jdt.core.compiler.compliance=21 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=21 diff --git a/bundles/nl.esi.xtext.actions.tests/META-INF/MANIFEST.MF b/bundles/nl.esi.xtext.actions.tests/META-INF/MANIFEST.MF new file mode 100644 index 0000000..3170c8f --- /dev/null +++ b/bundles/nl.esi.xtext.actions.tests/META-INF/MANIFEST.MF @@ -0,0 +1,18 @@ +Manifest-Version: 1.0 +Automatic-Module-Name: nl.esi.xtext.actions.tests +Bundle-ManifestVersion: 2 +Bundle-Name: Actions Lang Tests +Bundle-Vendor: TNO-ESI +Bundle-Version: 0.3.0.qualifier +Bundle-SymbolicName: nl.esi.xtext.actions.tests;singleton:=true +Bundle-ActivationPolicy: lazy +Require-Bundle: nl.esi.xtext.actions, + org.eclipse.xtext.testing, + org.eclipse.xtext.xbase.testing, + org.eclipse.xtext.xbase.lib;bundle-version="2.36.0" +Import-Package: org.junit.jupiter.api;version="[5.1.0,6.0.0)", + org.junit.jupiter.api.extension;version="[5.1.0,6.0.0)", + org.junit.jupiter.api.function;version="[5.1.0,6.0.0)", + org.opentest4j +Bundle-RequiredExecutionEnvironment: JavaSE-21 +Export-Package: nl.esi.xtext.actions.tests;x-internal=true diff --git a/bundles/nl.esi.xtext.actions.tests/build.properties b/bundles/nl.esi.xtext.actions.tests/build.properties new file mode 100644 index 0000000..7d28f0e --- /dev/null +++ b/bundles/nl.esi.xtext.actions.tests/build.properties @@ -0,0 +1,16 @@ +# +# Copyright (c) 2024, 2026 TNO-ESI +# +# This program and the accompanying materials are made +# available under the terms of the Eclipse Public License 2.0 +# which is available at https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# + +source.. = src/,\ + src-gen/,\ + xtend-gen/ +bin.includes = .,\ + META-INF/ +bin.excludes = **/*.xtend diff --git a/bundles/nl.esi.xtext.actions.tests/src-gen/.gitignore b/bundles/nl.esi.xtext.actions.tests/src-gen/.gitignore new file mode 100644 index 0000000..5e7d273 --- /dev/null +++ b/bundles/nl.esi.xtext.actions.tests/src-gen/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/bundles/nl.esi.xtext.actions.tests/src/nl/esi/xtext/actions/tests/ActionsParsingTest.xtend b/bundles/nl.esi.xtext.actions.tests/src/nl/esi/xtext/actions/tests/ActionsParsingTest.xtend new file mode 100644 index 0000000..8b881c2 --- /dev/null +++ b/bundles/nl.esi.xtext.actions.tests/src/nl/esi/xtext/actions/tests/ActionsParsingTest.xtend @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024, 2026 TNO-ESI + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +/* + * generated by Xtext 2.36.0 + */ +package nl.esi.xtext.actions.tests + +import com.google.inject.Inject +import nl.esi.xtext.actions.actions.ActionModel +import nl.esi.xtext.common.lang.utilities.EcoreUtil3 +import org.eclipse.xtext.testing.InjectWith +import org.eclipse.xtext.testing.extensions.InjectionExtension +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 + +@ExtendWith(InjectionExtension) +@InjectWith(ActionsInjectorProvider) +class ActionsParsingTest { + @Inject + ParseHelper parseHelper + + @Test + def void loadModel() { + val result = parseHelper.parse(''' + bool test = true + if test then test := false fi + ''') + Assertions.assertNotNull(result) + val errors = result.eResource.errors + Assertions.assertTrue(errors.isEmpty, '''Unexpected errors: «errors.join(", ")»''') + } + + @Test + def void saveModel() { + val input = ''' + int test = abs(-1) + for int i in [1,2,3] do test := test + 1 end-for''' + val result = parseHelper.parse(input) + EcoreUtil3.validate(result) + + val txt = EcoreUtil3.serialize(result); + Assertions.assertEquals(input, txt) + } + +} diff --git a/bundles/nl.esi.xtext.actions.tests/src/nl/esi/xtext/actions/tests/ActionsValidationTest.xtend b/bundles/nl.esi.xtext.actions.tests/src/nl/esi/xtext/actions/tests/ActionsValidationTest.xtend new file mode 100644 index 0000000..aa75f08 --- /dev/null +++ b/bundles/nl.esi.xtext.actions.tests/src/nl/esi/xtext/actions/tests/ActionsValidationTest.xtend @@ -0,0 +1,136 @@ +/** + * Copyright (c) 2024, 2026 TNO-ESI + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package nl.esi.xtext.actions.tests + +import com.google.inject.Inject +import nl.esi.xtext.actions.actions.ActionModel +import nl.esi.xtext.common.lang.utilities.EcoreUtil3 +import org.eclipse.xtext.testing.InjectWith +import org.eclipse.xtext.testing.extensions.InjectionExtension +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 + +@ExtendWith(InjectionExtension) +@InjectWith(ActionsInjectorProvider) +class ActionsValidationTest { + @Inject + ParseHelper actionParseHelper + + // === Assignment Action Tests === + + @Test + def void testAssignmentAction() { + validate(''' + int x + x := 5 + ''') + } + + @Test + def void testAssignmentActionIncrement() { + validate(''' + int x + x := x + 1 + ''') + } + + // === For Action Tests === + + @Test + def void testForAction() { + validate(''' + int[] numbers = [1, 2, 3] + int total + for int n in numbers + do + total := total + n + end-for + ''') + } + + // === If Action Tests === + + @Test + def void testIfActionSimple() { + validate(''' + int x + x := 10 + int result + if x > 5 + then + result := 1 + fi + ''') + } + + @Test + def void testIfActionWithElse() { + validate(''' + int x + x := 10 + int result + if x > 5 + then + result := 1 + else + result := 0 + fi + ''') + } + + // === Record Field Assignment Action Tests === + + @Test + def void testRecordFieldAssignment() { + validate(''' + record Point { int x, int y } + Point p = Point{x = 0, y = 0} + p.x := 10 + p.y := 20 + ''') + } + + @Test + def void testNestedRecordFieldAssignment() { + validate(''' + record Inner { int value } + record Outer { Inner inner } + Inner i = Inner{value = 0} + Outer o = Outer{inner = i} + o.inner.value := 42 + ''') + } + + // === Function Call Action Tests === + + @Test + def void testFunctionCallAction() { + validate(''' + function int sum(int[] values) + int[] data = [1, 2, 3] + sum(data) + ''') + } + + private def validate(String text) { + val result = actionParseHelper.parse(text) + + Assertions.assertNotNull(result) + var errors = result.eResource.errors + Assertions.assertTrue(errors.isEmpty, '''Unexpected parsing errors: «errors.join(", ")»''') + EcoreUtil3.validate(result) + errors = result.eResource.errors + Assertions.assertTrue(errors.isEmpty, '''Unexpected validation errors: «errors.join(", ")»''') + // test serialization + EcoreUtil3.serialize(result) + } +} diff --git a/bundles/nl.esi.xtext.actions.tests/xtend-gen/.gitignore b/bundles/nl.esi.xtext.actions.tests/xtend-gen/.gitignore new file mode 100644 index 0000000..5e7d273 --- /dev/null +++ b/bundles/nl.esi.xtext.actions.tests/xtend-gen/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/bundles/nl.esi.xtext.actions/adoc/nl.esi.xtext.actions.adoc b/bundles/nl.esi.xtext.actions/adoc/nl.esi.xtext.actions.adoc new file mode 100644 index 0000000..e97edbc --- /dev/null +++ b/bundles/nl.esi.xtext.actions/adoc/nl.esi.xtext.actions.adoc @@ -0,0 +1,144 @@ +//// + // Copyright (c) 2024, 2026 TNO-ESI + // + // This program and the accompanying materials are made + // available under the terms of the Eclipse Public License 2.0 + // which is available at https://www.eclipse.org/legal/epl-2.0/ + // + // SPDX-License-Identifier: EPL-2.0 +//// + +[[nl.esi.xtext.actions]] += Actions +:lang: en + +[.normal] + +== Action Language Overview + +Actions are imperative statements used to describe behavior in models. They include assignments, conditional logic, loops, function calls, and record field updates. Actions operate on variables and expressions defined using the <> language. + +*Supported action types:* assignment, for-loop, if-then-else, record field assignment, function call. + + +=== Assignment Action + +An assignment action updates the value of a previously declared variable using the `:=` operator: + +.... +variable := expression +.... + +Examples: + +.... +int x +x := 5 +x := x + 1 +.... + +=== For Action + +A for-loop iterates over the elements of a collection, executing a block of actions for each element: + +.... +for Type name in expression + do + actions +end-for +.... + +Examples: + +.... +int[] numbers = [1, 2, 3] +int total +for int n in numbers + do + total := total + n +end-for +.... + +=== If Action + +A conditional action executes a block of actions when a guard expression evaluates to true. An optional `else` branch is executed when the guard is false: + +.... +if guard +then + actions +fi + +if guard +then + actions +else + actions +fi +.... + +Examples: + +.... +int x +x := 10 +int result +if x > 5 +then + result := 1 +else + result := 0 +fi +.... + +=== Record Field Assignment Action + +A record field assignment updates a specific field of a record stored in a variable. The left-hand side uses dot notation to access the field: + +.... +variable.field := expression +.... + +Nested field access is also supported: + +.... +variable.field1.field2 := expression +.... + +Examples: + +.... +record Point { int x, int y } +Point p = Point{x = 0, y = 0} +p.x := 10 +p.y := 20 +.... + +NOTE: Only fields of records stored in variables can be assigned. Collection element access such as `collection[i].field := value` is not supported. + +=== Function Call Action + +A function call can be used as a standalone action (statement). The function call syntax is the same as in expressions: + +.... +function_name(args) +.... + +Examples: + +.... +function int sum(int[] values) +int[] data = [1, 2, 3] +sum(data) +.... + +NOTE: Function call actions use the same syntax as expression function calls. See <> for details on function calls, named arguments, and template functions. + +=== Notes + +* Actions operate on variables declared using the Expressions language. +* The `:=` operator is used for assignment (not `=`, which is used for initialization in variable declarations). +* For-loops require an iterable expression (e.g., a vector). +* If-actions require a boolean guard expression. +* Record field assignments only work on variables, not on collection elements. +* Function calls can be used both as expressions (returning a value) and as standalone actions. \ No newline at end of file diff --git a/bundles/nl.esi.xtext.actions/build.properties b/bundles/nl.esi.xtext.actions/build.properties index 1d9f886..675fb1d 100644 --- a/bundles/nl.esi.xtext.actions/build.properties +++ b/bundles/nl.esi.xtext.actions/build.properties @@ -14,7 +14,8 @@ source.. = src/,\ bin.includes = model/generated/,\ .,\ META-INF/,\ - plugin.xml + plugin.xml,\ + adoc/ bin.excludes = **/*.mwe2,\ **/*.xtend additional.bundles = org.eclipse.xtext.xbase,\ diff --git a/bundles/nl.esi.xtext.actions/src/nl/esi/xtext/actions/Actions.xtext b/bundles/nl.esi.xtext.actions/src/nl/esi/xtext/actions/Actions.xtext index c27a8b3..3d4bc32 100644 --- a/bundles/nl.esi.xtext.actions/src/nl/esi/xtext/actions/Actions.xtext +++ b/bundles/nl.esi.xtext.actions/src/nl/esi/xtext/actions/Actions.xtext @@ -9,12 +9,24 @@ */ grammar nl.esi.xtext.actions.Actions with nl.esi.xtext.expressions.Expression -generate actions "http://www.esi.nlxtext/actions/Actions" +generate actions "http://www.esi.nl/xtext/actions/Actions" import "http://www.esi.nl/xtext/common/lang/Base" as base import "http://www.esi.nl/xtext/types/Types" as types import "http://www.esi.nl/xtext/expressions/Expression" as expr +/** + * Root container solely used for testing. + */ +ActionModel returns expr::ExpressionModel: {ActionModel} + imports += Import* + ( types += TypeDecl + | variables += VariableDecl + | functions += FunctionDecl + | actions += Action + )* +; + /* * Actions are statements, acts of sending one or more events, or a declaration of * an expectation to observe a number of events that satisfy a pattern. @@ -36,7 +48,7 @@ FunctionCall: ; AssignmentAction: - assignment =[expr::Variable | ID] ':=' exp=Expression (symbolic?='symbolic-ref')? + assignment =[expr::Variable | ID] ':=' exp=Expression ; ForAction: @@ -53,14 +65,15 @@ IfAction: ; RecordFieldAssignmentAction: - fieldAccess = FieldAccessExp ':=' exp=Expression (symbolic?='symbolic-ref')? + fieldAccess = FieldAccessExp ':=' exp=Expression ; /* - * In XPlus we can only change field values of records that are stored in a variable. - * Since we cannot access elements in collections, syntax like collection[i].a := v is not supported + * Restricted variant of ExpressionLevel8 from Expression.xtext, used as an + * assignment target. Only variable-rooted record field paths (e.g., v.a.b) are + * valid l-values. Map element access (e.g., m[k] or m[k].a) is not supported + * because collection elements cannot be assigned to in XPlus. */ - // TODO: Why not just reuse ExpressionRecordAccess here? FieldAccessExp returns expr::Expression: ExpressionVariable ({expr::ExpressionRecordAccess.record = current} '.' field = [types::RecordField | ID])+ ; diff --git a/bundles/nl.esi.xtext.actions/src/nl/esi/xtext/actions/GenerateActions.mwe2 b/bundles/nl.esi.xtext.actions/src/nl/esi/xtext/actions/GenerateActions.mwe2 index ccffa3b..1b10a1f 100644 --- a/bundles/nl.esi.xtext.actions/src/nl/esi/xtext/actions/GenerateActions.mwe2 +++ b/bundles/nl.esi.xtext.actions/src/nl/esi/xtext/actions/GenerateActions.mwe2 @@ -21,6 +21,9 @@ Workflow { project = StandardProjectConfig { baseName = "nl.esi.xtext.actions" rootPath = rootPath + runtimeTest = { + enabled = true + } eclipsePlugin = { enabled = true }