diff --git a/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/FeatureReferenceExpressionParserPostProcessor.java b/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/FeatureReferenceExpressionParserPostProcessor.java new file mode 100644 index 0000000000..fe9b4e0f7a --- /dev/null +++ b/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/FeatureReferenceExpressionParserPostProcessor.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * SysML 2 Pilot Implementation + * Copyright (c) 2026 Model Driven Solutions, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +package org.omg.kerml.xtext.postprocessing; + +import org.omg.sysml.lang.sysml.FeatureReferenceExpression; +import org.omg.sysml.util.TypeUtil; + +public class FeatureReferenceExpressionParserPostProcessor extends ExpressionParserPostProcessor { + + public FeatureReferenceExpressionParserPostProcessor(FeatureReferenceExpression element) { + super(element); + } + + @Override + public FeatureReferenceExpression getTarget() { + return (FeatureReferenceExpression)super.getTarget(); + } + + @Override + public void postProcess() { + super.postProcess(); + TypeUtil.addResultParameterTo(getTarget()); + } + +} diff --git a/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/InstantiationExpressionParserPostProcessor.java b/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/InstantiationExpressionParserPostProcessor.java new file mode 100644 index 0000000000..f802349ea5 --- /dev/null +++ b/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/InstantiationExpressionParserPostProcessor.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * SysML 2 Pilot Implementation + * Copyright (c) 2026 Model Driven Solutions, Inc. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +package org.omg.kerml.xtext.postprocessing; + +import org.omg.sysml.lang.sysml.InstantiationExpression; +import org.omg.sysml.util.TypeUtil; + +public class InstantiationExpressionParserPostProcessor extends ExpressionParserPostProcessor { + + public InstantiationExpressionParserPostProcessor(InstantiationExpression element) { + super(element); + } + + @Override + public InstantiationExpression getTarget() { + return (InstantiationExpression)super.getTarget(); + } + + @Override + public void postProcess() { + super.postProcess(); + TypeUtil.addResultParameterTo(getTarget()); + } + +} diff --git a/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/KerMLParserPostProcessorFactory.java b/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/KerMLParserPostProcessorFactory.java index 2f0cae8ea2..0c7b544eee 100644 --- a/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/KerMLParserPostProcessorFactory.java +++ b/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/KerMLParserPostProcessorFactory.java @@ -98,11 +98,21 @@ public ElementParserPostProcessor caseFeatureInverting(FeatureInverting element) return new FeatureInvertingParserPostProcessor(element); } + @Override + public ElementParserPostProcessor caseFeatureReferenceExpression(FeatureReferenceExpression element) { + return new FeatureReferenceExpressionParserPostProcessor(element); + } + @Override public ElementParserPostProcessor caseFeatureTyping(FeatureTyping element) { return new FeatureTypingParserPostProcessor(element); } + @Override + public ElementParserPostProcessor caseInstantiationExpression(InstantiationExpression element) { + return new InstantiationExpressionParserPostProcessor(element); + } + @Override public ElementParserPostProcessor caseImport(Import element) { return new ImportParserPostProcessor(element); diff --git a/org.omg.sysml.execution/src/org/omg/sysml/execution/expressions/ExpressionEvaluator.java b/org.omg.sysml.execution/src/org/omg/sysml/execution/expressions/ExpressionEvaluator.java index 8359fdeb28..27bb3374b0 100644 --- a/org.omg.sysml.execution/src/org/omg/sysml/execution/expressions/ExpressionEvaluator.java +++ b/org.omg.sysml.execution/src/org/omg/sysml/execution/expressions/ExpressionEvaluator.java @@ -72,6 +72,7 @@ public EList evaluateInvocation(InvocationExpression expression, Elemen protected InvocationExpression instantiateInvocation(InvocationExpression expression, Element target) { InvocationExpression instantiation = SysMLFactory.eINSTANCE.createInvocationExpression(); + TypeUtil.addResultParameterTo(instantiation); // Copy instantiatedType from original expression. Type instantiatedType = expression.getInstantiatedType(); diff --git a/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/ExpressionResultConsultationTest.java b/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/ExpressionResultConsultationTest.java new file mode 100644 index 0000000000..43a19dbf4c --- /dev/null +++ b/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/ExpressionResultConsultationTest.java @@ -0,0 +1,615 @@ +/******************************************************************************* + * SysML 2 Pilot Implementation + * Copyright (c) 2026 Obeo + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.omg.sysml.interactive.tests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.emf.ecore.xmi.impl.XMIResourceImpl; +import org.eclipse.xtext.EcoreUtil2; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; +import org.omg.sysml.interactive.SysMLInteractive; +import org.omg.sysml.interactive.SysMLInteractiveResult; +import org.omg.sysml.lang.sysml.ConstructorExpression; +import org.omg.sysml.lang.sysml.Element; +import org.omg.sysml.lang.sysml.Expression; +import org.omg.sysml.lang.sysml.Feature; +import org.omg.sysml.lang.sysml.FeatureDirectionKind; +import org.omg.sysml.lang.sysml.FeatureReferenceExpression; +import org.omg.sysml.lang.sysml.IndexExpression; +import org.omg.sysml.lang.sysml.InvocationExpression; +import org.omg.sysml.lang.sysml.Membership; +import org.omg.sysml.lang.sysml.Relationship; +import org.omg.sysml.lang.sysml.ReturnParameterMembership; +import org.omg.sysml.lang.sysml.SysMLFactory; +import org.omg.sysml.lang.sysml.SysMLPackage; +import org.omg.sysml.lang.sysml.Type; +import org.omg.sysml.util.ElementUtil; +import org.omg.sysml.util.FeatureUtil; +import org.omg.sysml.util.TypeUtil; +import org.omg.sysml.xtext.postprocessing.SysMLParserPostProcessorFactory; + +/** Tests the semantic contract of expression results across consultation and explicit transformation. */ +public class ExpressionResultConsultationTest { + + protected static SysMLInteractive interactive; + + /** Loads and resolves the standard library once for the textual scenarios. */ + @BeforeClass + public static void loadLibraries() { + interactive = SysMLInteractive.createInstance(); + interactive.setVerbose(false); + interactive.loadLibrary(Path.of(System.getProperty("libraryPath")).toAbsolutePath().toString()); + } + + /** Removes each test model while retaining the shared resolved libraries. */ + @After + public void removeModels() { + interactive.removeResource(); + } + + private Resource parse(String name, String text) throws Exception { + interactive.setResource(name); + interactive.parse(text); + Element rootElement = interactive.getRootElement(); + SysMLInteractiveResult result = new SysMLInteractiveResult(rootElement, Collections.emptyList()); + assertTrue("'" + text + "':\n" + result.formatIssues(), result.getIssues().isEmpty()); + return interactive.getResource(); + } + + private static T findByName(Resource resource, String name, Class kind) { + for (var contents = resource.getAllContents(); contents.hasNext();) { + var object = contents.next(); + if (kind.isInstance(object) && name.equals(kind.cast(object).getDeclaredName())) { + return kind.cast(object); + } + } + throw new AssertionError("Missing " + name); + } + + private static T findSingle(Resource resource, Class kind) { + T found = null; + for (var contents = resource.getAllContents(); contents.hasNext();) { + var object = contents.next(); + if (kind.isInstance(object)) { + if (found != null) { + throw new AssertionError("Expected exactly one " + kind.getSimpleName() + ", found a second"); + } + found = kind.cast(object); + } + } + if (found == null) { + throw new AssertionError("Missing " + kind.getSimpleName()); + } + return found; + } + + /** + * A feature reference exposes a stable output result typed by the referent's + * classifier after semantic transformation, including after cache clearing and + * repeated materialization. + */ + @Test + public void referenceResultRetainsReferentAndType() throws Exception { + Resource resource = parse("referenceResult.kerml", """ + package Reference { + classifier Value; + feature original : Value; + feature copy = original; + } + """); + Feature original = findByName(resource, "original", Feature.class); + Type value = findByName(resource, "Value", Type.class); + FeatureReferenceExpression expression = findSingle(resource, FeatureReferenceExpression.class); + + // The public result must specialize the exact referent, not an unrelated library feature. + Feature result = expression.getResult(); + assertNotNull(result); + // Result subsetting is populated by semantic transformation. + ElementUtil.transform(expression); + assertTrue(TypeUtil.getGeneralTypesOf(result).contains(original)); + assertTrue(result.getType().contains(value)); + assertStableThroughTransformation(resource, expression, result); + assertTrue(result.getType().contains(value)); + } + + /** + * A constructor result retains the instantiated classifier and its identity + * through repeated consultations and explicit materialization. + */ + @Test + public void constructorResultRetainsInstantiatedType() throws Exception { + Resource resource = parse("constructorResult.kerml", """ + package Construction { + classifier Product; + feature product = new Product(); + } + """); + ConstructorExpression expression = findSingle(resource, ConstructorExpression.class); + Type product = findByName(resource, "Product", Type.class); + + // The constructor's result denotes a Product both before and after transformation. + Feature result = expression.getResult(); + assertNotNull(result); + assertTrue(result.getType().contains(product)); + assertStableThroughTransformation(resource, expression, result); + assertTrue(result.getType().contains(product)); + } + + /** + * Invoking a behavior that is not a function exposes a result typed by that + * behavior after explicit semantic transformation. + */ + @Test + public void behaviorInvocationResultRetainsBehaviorType() throws Exception { + Resource resource = parse("behaviorResult.sysml", """ + action def Move; + part p { ref movement = Move(); } + """); + InvocationExpression invocation = findSingle(resource, InvocationExpression.class); + Type move = findByName(resource, "Move", Type.class); + + // This is the behavior-result rule, distinct from a function's return parameter. + Feature result = invocation.getResult(); + assertNotNull(result); + // Behavior result typing is populated by semantic transformation. + ElementUtil.transform(invocation); + assertTrue(result.getType().contains(move)); + assertStableThroughTransformation(resource, invocation, result); + assertTrue(result.getType().contains(move)); + } + + /** + * Consulting a function invocation's result must preserve positional arguments + * and the result's type when the argument list is queried before and after it. + */ + @Test + public void functionResultPreservesArgumentOrder() throws Exception { + Resource resource = parse("functionResult.sysml", """ + calc def Choose { + in leftValue : ScalarValues::Integer; + in rightValue : ScalarValues::Integer; + return selectedValue : ScalarValues::Integer = leftValue; + } + part p { ref choice = Choose(11, 22); } + """); + Feature choice = findByName(resource, "choice", Feature.class); + InvocationExpression invocation = (InvocationExpression) FeatureUtil.getValuationFor(choice).getValue(); + + // Resolve arguments first: result redefinitions participate in their positional ordering. + List arguments = List.copyOf(invocation.getArgument()); + assertEquals(2, arguments.size()); + assertEquals(List.of(11, 22), arguments.stream() + .map(argument -> ((org.omg.sysml.lang.sysml.LiteralInteger) argument).getValue()).toList()); + Feature result = invocation.getResult(); + assertNotNull(result); + assertEquals(List.of("ScalarValues::Integer"), result.getType().stream().map(Type::getQualifiedName).toList()); + assertStableThroughTransformation(resource, invocation, result); + assertEquals(arguments, invocation.getArgument()); + } + + /** + * An index result and its sequence result remain distinct and stable during + * nested consultations; the outer result retains the sequence element type. + */ + @Test + public void nestedResultConsultationsPreserveSequenceResult() throws Exception { + Resource resource = parse("nestedResult.kerml", """ + package Indexing { + classifier Value; + feature values : Value[*]; + feature selected = values#(1); + } + """); + IndexExpression expression = findSingle(resource, IndexExpression.class); + Type value = findByName(resource, "Value", Type.class); + + // Index transformation computes result typing through its sequence argument. + Feature result = expression.getResult(); + assertNotNull(result); + ElementUtil.transform(expression); + assertTrue(result.getType().contains(value)); + Expression sequence = expression.getArgument().get(0); + Feature sequenceResult = sequence.getResult(); + assertTrue(result != sequenceResult); + assertTrue(TypeUtil.getGeneralTypesOf(result).contains(sequenceResult)); + assertStableThroughTransformation(resource, expression, result); + assertSame(sequenceResult, sequence.getResult()); + } + + /** + * Programmatically created expressions explicitly prepare an output result + * once and return the same object on subsequent reads. + */ + @Test + public void explicitlyPreparedProgrammaticExpressionsProvideStableResults() { + // Incomplete editor states have no textual equivalent: no referent or instantiated type yet. + List expressions = List.of(SysMLFactory.eINSTANCE.createFeatureReferenceExpression(), + SysMLFactory.eINSTANCE.createInvocationExpression(), SysMLFactory.eINSTANCE.createConstructorExpression()); + for (Expression expression : expressions) { + assertNull(expression.getResult()); + assertTrue(expression.getOwnedRelationship().isEmpty()); + TypeUtil.addResultParameterTo(expression); + Feature result = expression.getResult(); + assertNotNull(result); + assertEquals(FeatureDirectionKind.OUT, result.getDirection()); + // No resource, parser or transformation is needed to retrieve an existing result again. + assertSame(result, expression.getResult()); + } + } + + /** + * An explicitly supplied return parameter is preserved for each expression + * kind; consulting the result must never replace it with a synthesized feature. + */ + @Test + public void explicitReturnParametersArePreserved() { + // Insert the explicit result directly to isolate the abstract-syntax contract from parsing. + List expressions = List.of(SysMLFactory.eINSTANCE.createFeatureReferenceExpression(), + SysMLFactory.eINSTANCE.createInvocationExpression(), SysMLFactory.eINSTANCE.createConstructorExpression()); + for (Expression expression : expressions) { + Feature explicit = SysMLFactory.eINSTANCE.createFeature(); + explicit.setDeclaredName("explicitResult"); + explicit.setDirection(FeatureDirectionKind.OUT); + ReturnParameterMembership membership = SysMLFactory.eINSTANCE.createReturnParameterMembership(); + membership.setOwnedMemberParameter(explicit); + expression.getOwnedRelationship().add(membership); + // Repeated preparation and reads must retain the supplied identity and membership. + TypeUtil.addResultParameterTo(expression); + SysMLParserPostProcessorFactory.getPostProcessor(expression).postProcess(); + assertSame(explicit, expression.getResult()); + assertSame(explicit, expression.getResult()); + assertEquals(List.of(membership), expression.getOwnedRelationship()); + } + } + + /** + * An XMI-loaded constructor receives a correctly typed result after explicit + * preparation; materialization remains idempotent without parser callbacks. + */ + @Test + public void xmiConstructorProvidesResultAfterExplicitPreparation() throws Exception { + Resource parsed = parse("xmiResult.kerml", """ + package Construction { + classifier Product; + feature product = new Product(); + } + """); + EcoreUtil2.resolveLazyCrossReferences(parsed, null); + ConstructorExpression source = findSingle(parsed, ConstructorExpression.class); + // Remove the result prepared by parser post-processing to serialize an unprepared abstract-syntax model. + source.getOwnedRelationship().removeIf(ReturnParameterMembership.class::isInstance); + Resource serialized = new XMIResourceImpl(URI.createURI("memory:/serialized.xmi")); + serialized.getContents().addAll(parsed.getContents()); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + serialized.save(bytes, Map.of()); + Resource loaded = new XMIResourceImpl(URI.createURI("memory:/loaded.xmi")); + try { + parsed.getResourceSet().getResources().add(loaded); + loaded.load(new ByteArrayInputStream(bytes.toByteArray()), Map.of()); + ConstructorExpression expression = findSingle(loaded, ConstructorExpression.class); + Type product = findByName(loaded, "Product", Type.class); + // XMI loading does not run the textual parser's post-processors. + assertFalse(expression.getOwnedRelationship().stream().anyMatch(ReturnParameterMembership.class::isInstance)); + expression.getResult(); + assertFalse(expression.getOwnedRelationship().stream().anyMatch(ReturnParameterMembership.class::isInstance)); + TypeUtil.addResultParameterTo(expression); + Feature result = expression.getResult(); + assertNotNull(result); + assertTrue(result.getType().contains(product)); + assertStableThroughTransformation(loaded, expression, result); + } finally { + loaded.unload(); + parsed.getResourceSet().getResources().remove(loaded); + } + } + + /** + * Parser preparation creates results before resolving reference and instantiation + * targets, remains idempotent, and leaves literal results inherited. + */ + @Test + public void parserPreparesResultsWithoutResolvingTargets() throws Exception { + Resource resource = parse("preparedResults.kerml", """ + package Prepared { + classifier Product; + feature original : Product; + feature copy = original; + feature product = new Product(); + function choose { return selected : Product; } + feature chosen = choose(); + feature literal = 1; + } + """); + for (Expression expression : List.of(findSingle(resource, FeatureReferenceExpression.class), + findSingle(resource, ConstructorExpression.class), findSingle(resource, InvocationExpression.class))) { + // Inspect stored relationships only: no getResult or semantic lookup may prepare the result. + ReturnParameterMembership resultMembership = expression.getOwnedRelationship().stream() + .filter(ReturnParameterMembership.class::isInstance).map(ReturnParameterMembership.class::cast) + .findFirst().orElseThrow(); + Feature result = resultMembership.getOwnedMemberParameter(); + assertEquals(FeatureDirectionKind.OUT, result.getDirection()); + Membership reference = expression.getOwnedRelationship().stream() + .filter(relationship -> relationship.eClass() == SysMLPackage.Literals.MEMBERSHIP) + .map(Membership.class::cast).findFirst().orElseThrow(); + EObject target = (EObject) reference.eGet(SysMLPackage.Literals.MEMBERSHIP__MEMBER_ELEMENT, false); + assertTrue(target.eIsProxy()); + List relationships = List.copyOf(expression.getOwnedRelationship()); + + // Re-entering parser preparation must neither resolve the target nor replace the result. + SysMLParserPostProcessorFactory.getPostProcessor(expression).postProcess(); + assertSame(target, reference.eGet(SysMLPackage.Literals.MEMBERSHIP__MEMBER_ELEMENT, false)); + assertTrue(target.eIsProxy()); + assertEquals(relationships, expression.getOwnedRelationship()); + assertSame(result, resultMembership.getOwnedMemberParameter()); + } + Expression literal = findSingle(resource, org.omg.sysml.lang.sysml.LiteralInteger.class); + assertFalse(literal.getOwnedRelationship().stream().anyMatch(ReturnParameterMembership.class::isInstance)); + assertNotNull(literal.getResult()); + } + + /** + * Removing a prepared result models an incomplete edit. Result and membership + * queries must not recreate it, including after clearing derived caches. + */ + @Test + public void consultationDoesNotRecreateRemovedResults() throws Exception { + Resource resource = parse("removedResults.kerml", """ + package Removed { + classifier Product; + feature original : Product; + feature copy = original; + feature product = new Product(); + function choose { return selected : Product; } + feature chosen = choose(); + } + """); + for (Expression expression : List.of(findSingle(resource, FeatureReferenceExpression.class), + findSingle(resource, ConstructorExpression.class), findSingle(resource, InvocationExpression.class))) { + assertTrue(expression.getOwnedRelationship().removeIf(ReturnParameterMembership.class::isInstance)); + invalidateEditedModel(resource); + List relationships = List.copyOf(expression.getOwnedRelationship()); + + // Inherited results may still be available; no owned result may be synthesized by a read. + expression.getResult(); + expression.getFeature(); + expression.getFeatureMembership(); + assertEquals(relationships, expression.getOwnedRelationship()); + assertFalse(expression.getOwnedRelationship().stream().anyMatch(ReturnParameterMembership.class::isInstance)); + } + } + + /** + * Retargeting a reference A -> B -> A changes its result specialization and type, + * while retaining the result prepared once before the edits. Model adapters + * are explicitly removed to invalidate caches, then semantic transformation is + * rerun to update result subsetting without replacing the structural result. + */ + @Test + public void preparedReferenceResultFollowsReferentEdits() throws Exception { + Resource resource = parse("editedReference.kerml", """ + package EditedReference { + classifier A; + classifier B; + feature a : A; + feature b : B; + feature copy = a; + } + """); + FeatureReferenceExpression expression = findSingle(resource, FeatureReferenceExpression.class); + Feature a = findByName(resource, "a", Feature.class); + Feature b = findByName(resource, "b", Feature.class); + Membership reference = expression.getOwnedMembership().stream() + .filter(membership -> membership.getMemberElement() == a).findFirst().orElseThrow(); + // Prepare the structural result once, as a parser or an editor creation command would. + TypeUtil.addResultParameterTo(expression); + Feature result = TypeUtil.getOwnedResultParameterOf(expression); + assertNotNull(result); + List relationships = List.copyOf(expression.getOwnedRelationship()); + + for (Feature referent : List.of(a, b, a)) { + // Equivalent successive texts: copy = a; copy = b; copy = a, without reparsing. + reference.setMemberElement(referent); + invalidateEditedModel(resource); + assertSame(referent, expression.getReferent()); + assertPreparedResultUnchanged(expression, result, relationships); + // Refresh result subsetting through the semantic transformation lifecycle. + ElementUtil.transform(expression); + assertEquals(referent.getType(), result.getType()); + List generalTypes = TypeUtil.getGeneralTypesOf(result); + assertTrue(generalTypes.contains(referent)); + // The old referent must disappear, rather than accumulate as another subsetting. + for (Feature other : List.of(a, b)) { + if (other != referent) { + assertFalse(generalTypes.contains(other)); + } + } + assertPreparedResultUnchanged(expression, result, relationships); + } + } + + /** + * Editing the type of an unchanged referent updates its result transitively; + * semantic transformation restores result subsetting after cache invalidation, + * while the reference, result and return membership retain their identities. + */ + @Test + public void preparedReferenceResultFollowsReferentTypeEdits() throws Exception { + Resource resource = parse("editedReferentType.kerml", """ + package EditedReferentType { + classifier A; + classifier B; + feature original : A; + feature copy = original; + } + """); + FeatureReferenceExpression expression = findSingle(resource, FeatureReferenceExpression.class); + Feature original = findByName(resource, "original", Feature.class); + Type a = findByName(resource, "A", Type.class); + Type b = findByName(resource, "B", Type.class); + var typing = original.getOwnedTyping().get(0); + TypeUtil.addResultParameterTo(expression); + Feature result = TypeUtil.getOwnedResultParameterOf(expression); + assertNotNull(result); + List relationships = List.copyOf(expression.getOwnedRelationship()); + + for (Type type : List.of(a, b, a)) { + // Only original's explicit typing changes; the expression still refers to original. + typing.setType(type); + invalidateEditedModel(resource); + assertSame(original, expression.getReferent()); + ElementUtil.transform(expression); + assertEquals(List.of(type), result.getType()); + assertPreparedResultUnchanged(expression, result, relationships); + } + } + + /** + * A constructor keeps its prepared result when its classifier changes, including + * an intermediate missing target. Restoring a target updates typing without a + * second preparation or a transformation. + */ + @Test + public void preparedConstructorResultSurvivesTargetReplacement() throws Exception { + Resource resource = parse("editedConstructor.kerml", """ + package EditedConstructor { + classifier A; + classifier B; + feature product = new A(); + } + """); + ConstructorExpression expression = findSingle(resource, ConstructorExpression.class); + Type a = findByName(resource, "A", Type.class); + Type b = findByName(resource, "B", Type.class); + Membership reference = expression.getOwnedMembership().stream() + .filter(membership -> membership.getMemberElement() == a).findFirst().orElseThrow(); + TypeUtil.addResultParameterTo(expression); + Feature result = TypeUtil.getOwnedResultParameterOf(expression); + assertNotNull(result); + List relationships = List.copyOf(expression.getOwnedRelationship()); + assertEquals(List.of(a), result.getType()); + + // Intermediate editor state: the instantiated type is temporarily absent. + reference.setMemberElement(null); + invalidateEditedModel(resource); + assertNull(expression.getInstantiatedType()); + assertPreparedResultUnchanged(expression, result, relationships); + assertFalse(result.getType().contains(a)); + + for (Type type : List.of(b, a)) { + // Equivalent to new B(), then new A(), while retaining the same constructor object. + reference.setMemberElement(type); + invalidateEditedModel(resource); + assertSame(type, expression.getInstantiatedType()); + assertEquals(List.of(type), result.getType()); + assertPreparedResultUnchanged(expression, result, relationships); + } + } + + /** + * Changing an invoked function updates the result's return-parameter redefinition + * and type while retaining its identity and positional argument. Returning to + * the original function must remove the intervening function's result semantics. + */ + @Test + public void preparedInvocationResultFollowsFunctionEdits() throws Exception { + Resource resource = parse("editedInvocation.sysml", """ + calc def NumberFunction { + in inputValue : ScalarValues::Integer; + return number : ScalarValues::Integer; + } + calc def FlagFunction { + in inputValue : ScalarValues::Integer; + return flag : ScalarValues::Boolean; + } + part p { ref value = NumberFunction(11); } + """); + InvocationExpression expression = findSingle(resource, InvocationExpression.class); + Type numberFunction = findByName(resource, "NumberFunction", Type.class); + Type flagFunction = findByName(resource, "FlagFunction", Type.class); + Feature number = findByName(resource, "number", Feature.class); + Feature flag = findByName(resource, "flag", Feature.class); + Membership reference = expression.getOwnedMembership().stream() + .filter(membership -> membership.getMemberElement() == numberFunction).findFirst().orElseThrow(); + TypeUtil.addResultParameterTo(expression); + Feature result = TypeUtil.getOwnedResultParameterOf(expression); + assertNotNull(result); + List arguments = List.copyOf(expression.getArgument()); + assertEquals(1, arguments.size()); + List relationships = List.copyOf(expression.getOwnedRelationship()); + + for (Feature returnParameter : List.of(number, flag, number)) { + // Change NumberFunction(11) to FlagFunction(11) and back, without rebuilding arguments. + Type function = returnParameter.getOwningType(); + assertTrue(function == numberFunction || function == flagFunction); + reference.setMemberElement(function); + invalidateEditedModel(resource); + assertSame(function, expression.getInstantiatedType()); + List redefined = FeatureUtil.getRedefinedFeaturesWithComputedOf(result); + // Redefinitions also include library results; the function-specific target must be unique. + assertEquals(List.of(returnParameter), redefined.stream() + .filter(feature -> feature == number || feature == flag).toList()); + assertEquals(returnParameter.getType(), result.getType()); + assertEquals(arguments, expression.getArgument()); + assertPreparedResultUnchanged(expression, result, relationships); + } + } + + /** Removes model adapters to invalidate cached semantics without preparing or transforming the model. */ + private void invalidateEditedModel(Resource resource) { + for (var contents = resource.getAllContents(); contents.hasNext();) { + if (contents.next() instanceof Element element) { + ElementUtil.clean(element); + } + } + } + + /** Checks that edits do not require replacing the prepared result or its membership. */ + private void assertPreparedResultUnchanged(Expression expression, Feature result, List relationships) { + boolean transformed = ElementUtil.isTransformed(expression); + assertSame(result, expression.getResult()); + assertEquals(FeatureDirectionKind.OUT, result.getDirection()); + assertEquals(relationships, expression.getOwnedRelationship()); + assertEquals(1, relationships.stream().filter(ReturnParameterMembership.class::isInstance).count()); + // Consultation must not transform the expression as a side effect. + assertEquals(transformed, ElementUtil.isTransformed(expression)); + } + + /** Checks the observable result contract without requiring consultation-time containment. */ + private void assertStableThroughTransformation(Resource resource, Expression expression, Feature result) { + assertEquals(FeatureDirectionKind.OUT, result.getDirection()); + assertSame(result, expression.getResult()); + // Clearing derived caches must not replace the result observed by the caller. + ElementUtil.clearCachesOf(expression); + assertSame(result, expression.getResult()); + // Explicit transformation owns exactly one result; repeating it must not add another. + ElementUtil.transformAll(resource, true); + ElementUtil.transformAll(resource, true); + assertSame(result, expression.getResult()); + assertEquals(1, expression.getOwnedRelationship().stream().filter(ReturnParameterMembership.class::isInstance).count()); + } +} diff --git a/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.63.0-SNAPSHOT.jar b/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.63.0-SNAPSHOT.jar index 9f62f5b33a..53eb23ff93 100644 Binary files a/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.63.0-SNAPSHOT.jar and b/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.63.0-SNAPSHOT.jar differ diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/ConstructorExpressionAdapter.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/ConstructorExpressionAdapter.java index 8be413ab5f..246573fc2e 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/ConstructorExpressionAdapter.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/ConstructorExpressionAdapter.java @@ -59,7 +59,6 @@ protected void addResultTyping() { @Override public void addAdditionalMembers() { - TypeUtil.addResultParameterTo(getTarget()); addResultTyping(); } diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureReferenceExpressionAdapter.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureReferenceExpressionAdapter.java index 0ee71ed678..643dad338e 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureReferenceExpressionAdapter.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureReferenceExpressionAdapter.java @@ -84,11 +84,6 @@ protected void addResultSubsetting() { } } - @Override - public void addAdditionalMembers() { - TypeUtil.addResultParameterTo(getTarget()); - } - @Override public void doTransform() { super.doTransform(); diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/InvocationExpressionAdapter.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/InvocationExpressionAdapter.java index b340516b9c..e191b36cbf 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/InvocationExpressionAdapter.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/InvocationExpressionAdapter.java @@ -119,11 +119,6 @@ protected static boolean isFunctionType(Type type) { type instanceof Feature && ((Feature)type).getType().stream().anyMatch(Function.class::isInstance); } - @Override - public void addAdditionalMembers() { - TypeUtil.addResultParameterTo(getTarget()); - } - @Override public void doTransform() { super.doTransform(); diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/EvaluationUtil.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/EvaluationUtil.java index 4bc09c4ff0..ddff593474 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/EvaluationUtil.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/EvaluationUtil.java @@ -109,6 +109,7 @@ public static Expression expressionFor(EList results, Element context) Type listOp = SysMLLibraryUtil.getLibraryType(context, ExpressionUtil.getOperatorQualifiedNames(",")); for (int i = 1; i < results.size(); i++) { InvocationExpression listExpr = SysMLFactory.eINSTANCE.createInvocationExpression(); + TypeUtil.addResultParameterTo(listExpr); TypeUtil.addOwnedParameterTo(listExpr, expression); TypeUtil.addOwnedParameterTo(listExpr, expressionFor(results.get(i))); NamespaceUtil.addMemberTo(listExpr, listOp); @@ -132,6 +133,7 @@ public static Expression expressionFor(Element result) { return value == null? literalInfinity(): (Expression)elementFor(value); } else if (result instanceof Feature) { FeatureReferenceExpression featureRef = SysMLFactory.eINSTANCE.createFeatureReferenceExpression(); + TypeUtil.addResultParameterTo(featureRef); NamespaceUtil.addMemberTo(featureRef, result); return featureRef; } else { @@ -335,6 +337,7 @@ public static void instantiateArguments(Feature target, List parameters valueExpr = (Expression)argument; } else { valueExpr = SysMLFactory.eINSTANCE.createFeatureReferenceExpression(); + TypeUtil.addResultParameterTo(valueExpr); NamespaceUtil.addMemberTo(valueExpr, argument); } @@ -352,6 +355,7 @@ public static void instantiateArguments(Feature target, List parameters public static InvocationExpression createInvocationOf(Type type, Element... arguments) { InvocationExpression invocation = SysMLFactory.eINSTANCE.createInvocationExpression(); + TypeUtil.addResultParameterTo(invocation); NamespaceUtil.addMemberTo(invocation, type); Specialization specialization = SysMLFactory.eINSTANCE.createSpecialization(); diff --git a/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.63.0-SNAPSHOT.jar b/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.63.0-SNAPSHOT.jar index 14d2eb57e4..e16b22f8e1 100644 Binary files a/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.63.0-SNAPSHOT.jar and b/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.63.0-SNAPSHOT.jar differ