diff --git a/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/FeatureChainExpressionParserPostProcessor.java b/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/FeatureChainExpressionParserPostProcessor.java new file mode 100644 index 000000000..27fe7f242 --- /dev/null +++ b/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/FeatureChainExpressionParserPostProcessor.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * SysML 2 Pilot Implementation + * Copyright (c) 2026 Model Driven Solutions, Inc. + * 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.kerml.xtext.postprocessing; + +import org.omg.sysml.lang.sysml.FeatureChainExpression; +import org.omg.sysml.util.ExpressionUtil; + +/** Prepares the owned source-target feature of a feature-chain expression. */ +public class FeatureChainExpressionParserPostProcessor extends InvocationExpressionParserPostProcessor { + + public FeatureChainExpressionParserPostProcessor(FeatureChainExpression element) { + super(element); + } + + @Override + public FeatureChainExpression getTarget() { + return (FeatureChainExpression)super.getTarget(); + } + + @Override + public void postProcess() { + super.postProcess(); + ExpressionUtil.prepareSourceTargetFeature(getTarget()); + } + +} diff --git a/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/InvocationExpressionParserPostProcessor.java b/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/InvocationExpressionParserPostProcessor.java new file mode 100644 index 000000000..89ce25dce --- /dev/null +++ b/org.omg.kerml.xtext/src/org/omg/kerml/xtext/postprocessing/InvocationExpressionParserPostProcessor.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * SysML 2 Pilot Implementation + * Copyright (c) 2026 Model Driven Solutions, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + * + * @license LGPL-3.0-or-later + * + *******************************************************************************/ + +package org.omg.kerml.xtext.postprocessing; + +import org.omg.sysml.lang.sysml.InvocationExpression; + +public class InvocationExpressionParserPostProcessor extends InstantiationExpressionParserPostProcessor { + + public InvocationExpressionParserPostProcessor(InvocationExpression element) { + super(element); + } + + @Override + public InvocationExpression getTarget() { + return (InvocationExpression)super.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 0c7b544ee..2f66de74b 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 @@ -88,6 +88,11 @@ public ElementParserPostProcessor caseFeature(Feature element) { return new FeatureParserPostProcessor(element); } + @Override + public ElementParserPostProcessor caseFeatureChainExpression(FeatureChainExpression element) { + return new FeatureChainExpressionParserPostProcessor(element); + } + @Override public ElementParserPostProcessor caseFeatureMembership(FeatureMembership element) { return new FeatureMembershipParserPostProcessor(element); @@ -123,6 +128,11 @@ public ElementParserPostProcessor caseIntersecting(Intersecting element) { return new IntersectingParserPostProcessor(element); } + @Override + public ElementParserPostProcessor caseInvocationExpression(InvocationExpression element) { + return new InvocationExpressionParserPostProcessor(element); + } + @Override public ElementParserPostProcessor caseLiteralExpression(LiteralExpression element) { return new LiteralExpressionParserPostProcessor(element); 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 index 43a19dbf4..6e319ddfb 100644 --- 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 @@ -1,6 +1,7 @@ /******************************************************************************* * SysML 2 Pilot Implementation * Copyright (c) 2026 Obeo + * 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 @@ -19,8 +20,6 @@ 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; @@ -29,11 +28,7 @@ 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; @@ -54,42 +49,7 @@ 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); - } +public class ExpressionResultConsultationTest extends SysMLInteractiveTest { private static T findSingle(Resource resource, Class kind) { T found = null; diff --git a/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/FeatureChainConsultationTest.java b/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/FeatureChainConsultationTest.java new file mode 100644 index 000000000..a967673e0 --- /dev/null +++ b/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/FeatureChainConsultationTest.java @@ -0,0 +1,448 @@ +/******************************************************************************* + * SysML 2 Pilot Implementation + * Copyright (c) 2026 Obeo + * 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.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.assertNotSame; +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.util.ArrayList; +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.Test; +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.FeatureChainExpression; +import org.omg.sysml.lang.sysml.FeatureDirectionKind; +import org.omg.sysml.lang.sysml.FeatureMembership; +import org.omg.sysml.lang.sysml.Membership; +import org.omg.sysml.lang.sysml.Relationship; +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.ExpressionUtil; +import org.omg.sysml.util.FeatureUtil; +import org.omg.sysml.util.TypeUtil; +import org.omg.sysml.xtext.postprocessing.SysMLParserPostProcessorFactory; + +/** Characterizes feature-chain structure and semantics across consultation and editing. */ +public class FeatureChainConsultationTest extends SysMLInteractiveTest { + + 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 cached textual chain retains its source target, output and argument order + * through queries, cache clearing and repeated materialization. + */ + @Test + public void chainRetainsSourceTargetAndResult() throws Exception { + Resource resource = parse("chainLifecycle.kerml", """ + package Chain { + struct V { feature n : ScalarValues::Integer; } + feature v : V; + feature selected = v.n; + } + """); + FeatureChainExpression chain = findSingle(resource, FeatureChainExpression.class); + Feature target = findByName(resource, "n", Feature.class); + // The parser has prepared the source target before semantic resolution. + EcoreUtil2.resolveLazyCrossReferences(resource, null); + // Pilot computes feature-chain specializations through explicit transformation. + ElementUtil.transformAll(resource, false); + Feature result = chain.getResult(); + Feature source = sourceParameter(chain); + Feature sourceTarget = chain.sourceTargetFeature(); + assertNotNull(sourceTarget); + assertEquals("", sourceTarget.getDeclaredName()); + assertSame(source, sourceTarget.getOwningType()); + assertEquals(List.of(sourceTarget), source.getOwnedFeature()); + List arguments = List.copyOf(chain.getArgument()); + assertEquals(1, arguments.size()); + assertEquals(target.getType(), result.getType()); + assertTrue(FeatureUtil.getRedefinedFeaturesWithComputedOf(sourceTarget).contains(target)); + assertTrue(FeatureUtil.getRedefinedFeaturesWithComputedOf(sourceTarget).stream() + .anyMatch(feature -> "ControlFunctions::'.'::source::target".equals(feature.getQualifiedName()))); + assertTrue(TypeUtil.getGeneralTypesOf(result).stream().anyMatch(type -> type instanceof Feature feature + && feature.getChainingFeature().equals(List.of(source, sourceTarget)))); + + // Cold derived queries and transformation must preserve structural identities and argument positions. + clearDerivedCaches(resource); + assertSame(result, chain.getResult()); + assertSame(sourceTarget, chain.sourceTargetFeature()); + ElementUtil.transformAll(resource, true); + ElementUtil.transformAll(resource, true); + assertSame(result, chain.getResult()); + assertSame(sourceTarget, chain.sourceTargetFeature()); + assertEquals(arguments, chain.getArgument()); + assertEquals(List.of(sourceTarget), source.getOwnedFeature()); + assertEquals(target.getType(), result.getType()); + } + + /** + * Retargeting a prepared chain A -> missing -> B -> A changes its redefinitions + * and result type without replacing its nested feature; explicit transformation refreshes semantics. + */ + @Test + public void targetEditsPreservePreparedStructure() throws Exception { + Resource resource = parse("editedChain.kerml", """ + package Edited { + struct V { + feature number : ScalarValues::Integer; + feature flag : ScalarValues::Boolean; + } + feature v : V; + feature selected = v.number; + } + """); + FeatureChainExpression chain = findSingle(resource, FeatureChainExpression.class); + Feature number = findByName(resource, "number", Feature.class); + Feature flag = findByName(resource, "flag", Feature.class); + EcoreUtil2.resolveLazyCrossReferences(resource, null); + // Pilot computes feature-chain specializations through explicit transformation. + ElementUtil.transformAll(resource, false); + Feature result = chain.getResult(); + Feature sourceTarget = chain.sourceTargetFeature(); + assertNotNull(sourceTarget); + Membership reference = chain.getOwnedMembership().stream() + .filter(member -> member.getMemberElement() == number).findFirst().orElseThrow(); + List relationships = List.copyOf(sourceParameter(chain).getOwnedRelationship()); + + // An absent target is an intermediate editor state, not a request to rebuild the source target. + reference.setMemberElement(null); + invalidateEditedModel(resource); + ElementUtil.transformAll(resource, false); + assertNull(chain.getTargetFeature()); + assertFalse(FeatureUtil.getRedefinedFeaturesWithComputedOf(sourceTarget).contains(number)); + assertSame(sourceTarget, chain.sourceTargetFeature()); + for (Feature target : List.of(flag, number)) { + // Equivalent texts: v.flag, then v.number, retaining the expression and its source parameter. + reference.setMemberElement(target); + invalidateEditedModel(resource); + ElementUtil.transformAll(resource, false); + List redefined = FeatureUtil.getRedefinedFeaturesWithComputedOf(sourceTarget); + assertEquals(List.of(target), redefined.stream().filter(f -> f == number || f == flag).toList()); + assertEquals(target.getType(), result.getType()); + assertSame(result, chain.getResult()); + assertSame(sourceTarget, chain.sourceTargetFeature()); + assertEquals(relationships, sourceParameter(chain).getOwnedRelationship()); + assertTrue(ElementUtil.isTransformed(chain)); + } + } + + /** + * Nested chains over cyclic source subsettings terminate and keep distinct + * source targets, while their results retain the recursive structure's type. + */ + @Test + public void nestedChainsWithCyclicSourcesRetainDistinctTargets() throws Exception { + Resource resource = parse("cyclicChain.kerml", """ + package Cyclic { + struct Node { feature next : Node; } + feature a : Node :> b; + feature b : Node :> a; + feature selected = (a.next).next; + } + """); + Feature selected = findByName(resource, "selected", Feature.class); + FeatureChainExpression outer = (FeatureChainExpression) FeatureUtil.getValuationFor(selected).getValue(); + EcoreUtil2.resolveLazyCrossReferences(resource, null); + // Pilot computes feature-chain specializations through explicit transformation. + ElementUtil.transformAll(resource, false); + FeatureChainExpression inner = (FeatureChainExpression) outer.getArgument().get(0); + Type node = findByName(resource, "Node", Type.class); + // Query the outer result first to exercise nested semantic consultation. + assertEquals(List.of(node), outer.getResult().getType()); + assertEquals(List.of(node), inner.getResult().getType()); + Feature outerTarget = outer.sourceTargetFeature(); + Feature innerTarget = inner.sourceTargetFeature(); + assertNotNull(outerTarget); + assertNotNull(innerTarget); + assertNotSame(innerTarget, outerTarget); + clearDerivedCaches(resource); + assertEquals(List.of(node), outer.getResult().getType()); + assertSame(outerTarget, outer.sourceTargetFeature()); + assertSame(innerTarget, inner.sourceTargetFeature()); + } + + /** + * An incomplete programmatic chain tolerates an absent parameter; once an input + * exists, explicit preparation supplies exactly one nested source target. + */ + @Test + public void programmaticChainSupportsDeferredSourceCreation() { + // No textual equivalent for this intermediate construction state: neither input nor output exists yet. + FeatureChainExpression chain = SysMLFactory.eINSTANCE.createFeatureChainExpression(); + ExpressionUtil.prepareSourceTargetFeature(chain); + assertNull(chain.sourceTargetFeature()); + chain.getFeature(); + assertTrue(chain.getOwnedRelationship().isEmpty()); + Feature source = TypeUtil.addOwnedParameterTo(chain, SysMLFactory.eINSTANCE.createLiteralInteger()) + .getOwnedMemberParameter(); + TypeUtil.addResultParameterTo(chain); + ElementUtil.clearCachesOf(chain); + // Reads must retain the operand's FeatureValue without adding a nested source target. + List relationships = List.copyOf(source.getOwnedRelationship()); + chain.getFeature(); + chain.getResult(); + assertEquals(relationships, source.getOwnedRelationship()); + assertTrue(source.getOwnedFeature().isEmpty()); + ExpressionUtil.prepareSourceTargetFeature(chain); + ExpressionUtil.prepareSourceTargetFeature(chain); + Feature sourceTarget = chain.sourceTargetFeature(); + assertNotNull(sourceTarget); + assertEquals("", sourceTarget.getDeclaredName()); + chain.getFeatureMembership(); + assertSame(sourceTarget, chain.sourceTargetFeature()); + assertEquals(List.of(sourceTarget), source.getOwnedFeature()); + } + + /** + * A caller-supplied nested feature is retained, including its name, rather than + * being replaced by an anonymous source target during consultation. + */ + @Test + public void explicitNestedSourceFeatureIsPreserved() { + // Programmatically supply the nested feature that the textual syntax normally leaves implicit. + FeatureChainExpression chain = SysMLFactory.eINSTANCE.createFeatureChainExpression(); + Feature source = TypeUtil.addOwnedParameterTo(chain, SysMLFactory.eINSTANCE.createLiteralInteger()) + .getOwnedMemberParameter(); + Feature explicit = SysMLFactory.eINSTANCE.createFeature(); + explicit.setDeclaredName("explicitTarget"); + TypeUtil.addOwnedFeatureTo(source, explicit); + TypeUtil.addResultParameterTo(chain); + ExpressionUtil.prepareSourceTargetFeature(chain); + ExpressionUtil.prepareSourceTargetFeature(chain); + chain.getFeature(); + chain.getResult(); + assertSame(explicit, chain.sourceTargetFeature()); + assertEquals("explicitTarget", explicit.getDeclaredName()); + assertEquals(List.of(explicit), source.getOwnedFeature()); + } + + /** + * An XMI chain lacking the nested feature requires explicit preparation and + * then retains its target identity and inferred type. + */ + @Test + public void xmiChainProvidesSourceTargetWithoutParserCallback() throws Exception { + Resource parsed = parse("xmiChain.kerml", """ + package XmiChain { + struct V { feature n : ScalarValues::Integer; } + feature v : V; + feature selected = v.n; + } + """); + EcoreUtil2.resolveLazyCrossReferences(parsed, null); + FeatureChainExpression original = findSingle(parsed, FeatureChainExpression.class); + Feature sourceTarget = original.sourceTargetFeature(); + assertNotNull(sourceTarget); + // Serialize a deliberately unprepared nested structure while preserving input, output and references. + sourceParameter(original).getOwnedRelationship().remove(sourceTarget.getOwningRelationship()); + Resource serialized = new XMIResourceImpl(URI.createURI("memory:/chainSerialized.xmi")); + serialized.getContents().addAll(parsed.getContents()); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + serialized.save(bytes, Map.of()); + Resource loaded = new XMIResourceImpl(URI.createURI("memory:/chainLoaded.xmi")); + try { + parsed.getResourceSet().getResources().add(loaded); + loaded.load(new ByteArrayInputStream(bytes.toByteArray()), Map.of()); + FeatureChainExpression chain = findSingle(loaded, FeatureChainExpression.class); + assertNull(chain.sourceTargetFeature()); + Feature result = chain.getResult(); + assertNull(chain.sourceTargetFeature()); + ExpressionUtil.prepareSourceTargetFeature(chain); + ElementUtil.transformAll(loaded, false); + Feature restored = chain.sourceTargetFeature(); + assertNotNull(restored); + Feature target = findByName(loaded, "n", Feature.class); + assertSame(target, chain.getTargetFeature()); + assertTrue(FeatureUtil.getRedefinedFeaturesWithComputedOf(restored).contains(target)); + assertEquals(target.getType(), result.getType()); + assertSame(restored, chain.sourceTargetFeature()); + } finally { + loaded.unload(); + parsed.getResourceSet().getResources().remove(loaded); + } + } + + /** + * Parser preparation supplies the nested feature before resolution and remains + * idempotent without resolving the target or source expression references. + */ + @Test + public void parserPreparesSourceTargetWithoutResolvingReferences() throws Exception { + Resource resource = parse("preparedChain.kerml", """ + package Prepared { + struct V { feature n : ScalarValues::Integer; } + feature v : V; + feature selected = v.n; + } + """); + FeatureChainExpression chain = findSingle(resource, FeatureChainExpression.class); + // Snapshot unresolved references before invoking any semantic query. + List references = new ArrayList<>(); + List proxies = new ArrayList<>(); + for (var contents = chain.eAllContents(); contents.hasNext();) { + if (contents.next() instanceof Membership membership + && membership.eClass() == SysMLPackage.Literals.MEMBERSHIP) { + EObject proxy = (EObject) membership.eGet(SysMLPackage.Literals.MEMBERSHIP__MEMBER_ELEMENT, false); + assertTrue(proxy.eIsProxy()); + references.add(membership); + proxies.add(proxy); + } + } + assertEquals(2, references.size()); + Feature source = sourceParameter(chain); + assertEquals(1, source.getOwnedFeature().size()); + Feature prepared = source.getOwnedFeature().get(0); + List relationships = List.copyOf(source.getOwnedRelationship()); + + // A repeated parser callback must preserve both containment and unresolved proxies. + SysMLParserPostProcessorFactory.getPostProcessor(chain).postProcess(); + assertEquals(relationships, source.getOwnedRelationship()); + assertSame(prepared, chain.sourceTargetFeature()); + for (int i = 0; i < references.size(); i++) { + assertSame(proxies.get(i), references.get(i).eGet(SysMLPackage.Literals.MEMBERSHIP__MEMBER_ELEMENT, false)); + assertTrue(proxies.get(i).eIsProxy()); + } + } + + /** + * Deleting a prepared target models an incomplete edit. Cold semantic reads + * must not recreate it; only explicit preparation may restore the structure. + */ + @Test + public void consultationDoesNotRecreateRemovedSourceTarget() throws Exception { + Resource resource = parse("removedChain.kerml", """ + package Removed { + struct V { feature n : ScalarValues::Integer; } + feature v : V; + feature selected = v.n; + } + """); + FeatureChainExpression chain = findSingle(resource, FeatureChainExpression.class); + EcoreUtil2.resolveLazyCrossReferences(resource, null); + Feature source = sourceParameter(chain); + Feature removed = chain.sourceTargetFeature(); + assertNotNull(removed); + source.getOwnedRelationship().remove(removed.getOwningRelationship()); + List relationships = List.copyOf(source.getOwnedRelationship()); + // In particular getResult and feature membership used to trigger structural creation. + for (int i = 0; i < 2; i++) { + clearDerivedCaches(resource); + chain.getFeature(); + chain.getFeatureMembership(); + chain.getResult().getType(); + assertNull(chain.sourceTargetFeature()); + assertEquals(relationships, source.getOwnedRelationship()); + } + // Pilot transformation must also tolerate the missing prepared feature without recreating it. + ElementUtil.clean(chain); + ElementUtil.transform(chain); + assertNull(chain.sourceTargetFeature()); + assertEquals(relationships, source.getOwnedRelationship()); + ExpressionUtil.prepareSourceTargetFeature(chain); + assertNotNull(chain.sourceTargetFeature()); + assertNotSame(removed, chain.sourceTargetFeature()); + } + + /** + * Preparation selects the first input even when the output precedes it and a + * later input already owns a feature. Replacing the source requires preparation + * of the replacement without modifying the detached old source. + */ + @Test + public void preparationFollowsSourceInputOwnership() { + // Deliberately construct an intermediate model with output before input. + FeatureChainExpression chain = SysMLFactory.eINSTANCE.createFeatureChainExpression(); + TypeUtil.addResultParameterTo(chain); + Feature result = chain.getResult(); + Feature source = TypeUtil.addOwnedParameterTo(chain, SysMLFactory.eINSTANCE.createLiteralInteger()) + .getOwnedMemberParameter(); + Feature later = TypeUtil.addOwnedParameterTo(chain, SysMLFactory.eINSTANCE.createLiteralInteger()) + .getOwnedMemberParameter(); + Feature laterTarget = SysMLFactory.eINSTANCE.createFeature(); + TypeUtil.addOwnedFeatureTo(later, laterTarget); + ExpressionUtil.prepareSourceTargetFeature(chain); + Feature prepared = chain.sourceTargetFeature(); + assertSame(source, prepared.getOwningType()); + assertTrue(result.getOwnedFeature().isEmpty()); + assertEquals(List.of(laterTarget), later.getOwnedFeature()); + + // Replace the source in the same membership; semantic reads must not repair it. + FeatureMembership membership = source.getOwningFeatureMembership(); + membership.getOwnedRelatedElement().clear(); + Feature replacement = SysMLFactory.eINSTANCE.createFeature(); + replacement.setDirection(FeatureDirectionKind.IN); + membership.setOwnedMemberFeature(replacement); + ElementUtil.clearCachesOf(chain); + chain.getFeatureMembership(); + assertTrue(replacement.getOwnedRelationship().isEmpty()); + ExpressionUtil.prepareSourceTargetFeature(chain); + ExpressionUtil.prepareSourceTargetFeature(chain); + assertSame(replacement, chain.sourceTargetFeature().getOwningType()); + assertEquals(1, replacement.getOwnedFeature().size()); + assertEquals(List.of(prepared), source.getOwnedFeature()); + assertEquals(List.of(laterTarget), later.getOwnedFeature()); + } + + private Feature sourceParameter(FeatureChainExpression chain) { + return chain.getOwnedFeature().stream().filter(feature -> feature.getDirection() == FeatureDirectionKind.IN) + .findFirst().orElseThrow(); + } + + /** Resets Pilot adapters so explicit transformation recomputes semantics after edits. */ + private void invalidateEditedModel(Resource resource) { + for (var contents = resource.getAllContents(); contents.hasNext();) { + if (contents.next() instanceof Element element) { + ElementUtil.clean(element); + } + } + } + + private void clearDerivedCaches(Resource resource) { + for (var contents = resource.getAllContents(); contents.hasNext();) { + if (contents.next() instanceof Element element) { + ElementUtil.clearCachesOf(element); + } + } + } +} diff --git a/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/MultiplicityPreparationTest.java b/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/MultiplicityPreparationTest.java index 39c50a0a9..c144bdb19 100644 --- a/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/MultiplicityPreparationTest.java +++ b/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/MultiplicityPreparationTest.java @@ -15,48 +15,24 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; -import java.nio.file.Path; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import org.eclipse.emf.ecore.EObject; import org.eclipse.emf.ecore.resource.Resource; -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.Element; import org.omg.sysml.lang.sysml.Multiplicity; import org.omg.sysml.lang.sysml.MultiplicityRange; import org.omg.sysml.lang.sysml.OwningMembership; import org.omg.sysml.lang.sysml.Relationship; import org.omg.sysml.lang.sysml.Subsetting; import org.omg.sysml.lang.sysml.SysMLPackage; -import org.omg.sysml.lang.sysml.Type; import org.omg.sysml.lang.sysml.Usage; import org.omg.sysml.util.ElementUtil; import org.omg.sysml.util.TypeUtil; import org.omg.sysml.xtext.postprocessing.SysMLParserPostProcessorFactory; /** Tests the boundary between parser preparation of multiplicities and derived consultation. */ -public class MultiplicityPreparationTest { - - protected static SysMLInteractive interactive; - - /** Loads and resolves the standard library once for the textual test 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 removeResource() { - interactive.removeResource(); - } +public class MultiplicityPreparationTest extends SysMLInteractiveTest { /** * Parsing end usages prepares one multiplicity before any feature consultation; @@ -235,23 +211,4 @@ public void defaultMultiplicityDependsOnKindAndOwner() throws Exception { } } - 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();) { - EObject object = contents.next(); - if (kind.isInstance(object) && name.equals(kind.cast(object).getDeclaredName())) { - return kind.cast(object); - } - } - throw new AssertionError("Missing " + name); - } - } diff --git a/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/SysMLInteractiveTest.java b/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/SysMLInteractiveTest.java index 5a81e9914..b74641e96 100644 --- a/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/SysMLInteractiveTest.java +++ b/org.omg.sysml.interactive.tests/src/org/omg/sysml/interactive/tests/SysMLInteractiveTest.java @@ -25,10 +25,17 @@ import static org.junit.Assert.assertTrue; +import java.nio.file.Path; +import java.util.Collections; import java.util.List; import org.omg.sysml.lang.sysml.Element; import org.omg.sysml.lang.sysml.Namespace; +import org.omg.sysml.lang.sysml.Type; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.junit.After; +import org.junit.BeforeClass; import org.omg.sysml.interactive.SysMLInteractive; import org.omg.sysml.interactive.SysMLInteractiveResult; @@ -36,18 +43,30 @@ public abstract class SysMLInteractiveTest { private final static String SYSML_LIBRARY_PATH_KEY = "libraryPath"; - public String getLibraryPath() { + /** Loads and resolves the standard library once for the textual test scenarios. */ + @BeforeClass + public static void loadLibraries() { + createSysMLInteractiveInstance(); + } + + /** Removes each test model while retaining the shared resolved libraries. */ + @After + public void removeResource() { + SysMLInteractive.getInstance().removeResource(); + } + + public static String getLibraryPath() { return System.getProperty(SYSML_LIBRARY_PATH_KEY); } - public SysMLInteractive createSysMLInteractiveInstance() { + public static SysMLInteractive createSysMLInteractiveInstance() { SysMLInteractive instance = SysMLInteractive.createInstance(); instance.setVerbose(false); - instance.loadLibrary(getLibraryPath()); + instance.loadLibrary(Path.of(getLibraryPath()).toAbsolutePath().toString()); return instance; } - public SysMLInteractive getSysMLInteractiveInstance() { + public static SysMLInteractive getSysMLInteractiveInstance() { SysMLInteractive instance = SysMLInteractive.getInstance(); instance.setVerbose(false); instance.loadLibrary(getLibraryPath()); @@ -68,4 +87,25 @@ public List process(SysMLInteractive instance, String input) { public List process(String input) { return process(null, input); } + + protected Resource parse(String name, String text) throws Exception { + SysMLInteractive interactive = SysMLInteractive.getInstance(); + 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(); + } + + protected static T findByName(Resource resource, String name, Class kind) { + for (var contents = resource.getAllContents(); contents.hasNext();) { + EObject object = contents.next(); + if (kind.isInstance(object) && name.equals(kind.cast(object).getDeclaredName())) { + return kind.cast(object); + } + } + throw new AssertionError("Missing " + name); + } + } diff --git a/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.64.0-SNAPSHOT.jar b/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.64.0-SNAPSHOT.jar index 418d87245..d02642a4e 100644 Binary files a/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.64.0-SNAPSHOT.jar and b/org.omg.sysml.logic.bundle/lib/org.omg.sysml.logic-0.64.0-SNAPSHOT.jar differ diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureChainExpressionAdapter.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureChainExpressionAdapter.java index 3d76856e2..28babc853 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureChainExpressionAdapter.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/FeatureChainExpressionAdapter.java @@ -1,6 +1,7 @@ /******************************************************************************* * SysML 2 Pilot Implementation * Copyright (c) 2021, 2022, 2025 Model Driven Solutions, Inc. + * Copyright (c) 2026 Obeo * * This program is free software: you can redistribute it and/or modify * it under the terms of the Eclipse Public License as published by @@ -23,7 +24,6 @@ import org.omg.sysml.lang.sysml.Feature; import org.omg.sysml.lang.sysml.FeatureChainExpression; import org.omg.sysml.lang.sysml.FeatureDirectionKind; -import org.omg.sysml.lang.sysml.SysMLFactory; import org.omg.sysml.lang.sysml.SysMLPackage; import org.omg.sysml.util.FeatureUtil; import org.omg.sysml.util.ImplicitGeneralizationMap; @@ -60,21 +60,6 @@ protected void addResultTyping() { } } - @Override - public void addAdditionalMembers() { - super.addAdditionalMembers(); - - // Add sourceTarget feature. - FeatureChainExpression target = getTarget(); - Feature sourceParameter = TypeUtil.getOwnedParameterOf(target, 0, Feature.class); - if (sourceParameter != null) { - if (sourceParameter.getOwnedFeature().isEmpty()) { - Feature sourceTarget = SysMLFactory.eINSTANCE.createFeature(); - sourceTarget.setDeclaredName(""); // To avoid effective naming. - TypeUtil.addOwnedFeatureTo(sourceParameter, sourceTarget); - } - } - } /** * @satisfies checkFeatureChainExpressionTargetRedefinition @@ -85,6 +70,9 @@ protected void addTargetRedefinition() { Feature sourceParameter = TypeUtil.getOwnedParameterOf(target, 0, Feature.class); if (sourceParameter != null) { Feature sourceTarget = target.sourceTargetFeature(); + if (sourceTarget == null) { + return; + } TypeUtil.addImplicitGeneralTypeTo(sourceTarget, SysMLPackage.eINSTANCE.getRedefinition(), //checkFeatureChainExpressionTargetRedefinition diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/UsageAdapter.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/UsageAdapter.java index edef5c8ed..923186b24 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/UsageAdapter.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/adapter/UsageAdapter.java @@ -31,7 +31,6 @@ import org.omg.sysml.lang.sysml.PartUsage; import org.omg.sysml.lang.sysml.StateSubactionKind; import org.omg.sysml.lang.sysml.StateSubactionMembership; -import org.omg.sysml.lang.sysml.Subsetting; import org.omg.sysml.lang.sysml.SysMLPackage; import org.omg.sysml.lang.sysml.Type; import org.omg.sysml.lang.sysml.Usage; diff --git a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ExpressionUtil.java b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ExpressionUtil.java index 26f30365a..da658e3b4 100644 --- a/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ExpressionUtil.java +++ b/org.omg.sysml.logic/src/main/java/org/omg/sysml/util/ExpressionUtil.java @@ -1,6 +1,7 @@ /******************************************************************************* * SysML 2 Pilot Implementation * Copyright (c) 2021-2023, 2025-2026 Model Driven Solutions, Inc. + * Copyright (c) 2026 Obeo * * This program is free software: you can redistribute it and/or modify * it under the terms of the Eclipse Public License as published by @@ -35,6 +36,8 @@ 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.FeatureChainExpression; +import org.omg.sysml.lang.sysml.FeatureDirectionKind; import org.omg.sysml.lang.sysml.FeatureMembership; import org.omg.sysml.lang.sysml.FeatureReferenceExpression; import org.omg.sysml.lang.sysml.InstantiationExpression; @@ -42,8 +45,10 @@ import org.omg.sysml.lang.sysml.LiteralBoolean; import org.omg.sysml.lang.sysml.Membership; import org.omg.sysml.lang.sysml.ParameterMembership; +import org.omg.sysml.lang.sysml.Relationship; import org.omg.sysml.lang.sysml.ResultExpressionMembership; 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.TransitionFeatureKind; import org.omg.sysml.lang.sysml.TransitionFeatureMembership; @@ -102,6 +107,39 @@ public static Element getReferentFor(FeatureReferenceExpression expression) { findFirst().orElse(null); } + /** + * Prepares the nested source-target feature of the first owned input feature. + * Existing nested features, including their names, are preserved. If there is + * no input, this operation does nothing and may be called again after one is added. + * Only local containment and stored direction are inspected; no expression + * references, types or specialization targets are resolved. + *

+ * Model producers must call this operation after constructing or replacing the + * source input, before semantic consultation. It mutates containment but does + * not install or invalidate caches. Repeated calls preserve element identities. + * + * @param expression the non-null expression to prepare + */ + public static void prepareSourceTargetFeature(FeatureChainExpression expression) { + for (Relationship relationship : expression.getOwnedRelationship()) { + if (relationship instanceof FeatureMembership membership) { + Feature source = membership.getOwnedMemberFeature(); + if (source != null && source.getDirection() == FeatureDirectionKind.IN) { + for (Relationship nested : source.getOwnedRelationship()) { + if (nested instanceof FeatureMembership nestedMembership + && nestedMembership.getOwnedMemberFeature() != null) { + return; + } + } + Feature sourceTarget = SysMLFactory.eINSTANCE.createFeature(); + sourceTarget.setDeclaredName(""); // Suppress effective naming. + TypeUtil.addOwnedFeatureTo(source, sourceTarget); + return; + } + } + } + } + public static Element getTargetFeatureFor(Expression expression) { List memberships = expression.getOwnedMembership(); return memberships.size() < 2? null: memberships.get(1).getMemberElement(); diff --git a/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.64.0-SNAPSHOT.jar b/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.64.0-SNAPSHOT.jar index 37a5d9546..40ca541f9 100644 Binary files a/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.64.0-SNAPSHOT.jar and b/org.omg.sysml.model.bundle/lib/org.omg.sysml.model-0.64.0-SNAPSHOT.jar differ